orbit/js/galaxy/StationFrames.js

97 lines
4.3 KiB
JavaScript

import { config } from '../config/Config.js';
import { Rng } from '../utils/Rng.js';
import { rollSystemComposition, settlementDensity } from './SystemGenerator.js';
/**
* STATION VARIANT SPREAD — the galaxy-wide deep-space-station frame
* assignment (the sibling of the planet frame pass, PlanetFrames.js).
*
* data/stations.json → variants lists the spacestations.png sheet frames
* a DEEP-SPACE STATION may wear (today 0..2 — the first three frames of
* the sheet; add a line when the art lands). Picking a random frame per
* system would let neighboring stars wear the same face; this pass
* spreads the variants instead: when a system's deep-space station needs
* a frame, it AVOIDS the frames its NEAREST stars already wear (the
* galaxy's neighbor pool, data/galaxy.json → neighbors), so the same
* station type reappears only far away.
*
* Only DEEP-SPACE STATIONS take a variant — a system holds at most one
* (rollSystemComposition rolls a boolean), and waypoints keep their
* beacon look (no sheet art for them). The landing handoff reads the
* same frame (settlement.stationFrame → data/landing.json →
* stationVideos), so a station always lands with the clip matching the
* art it is drawn with.
*
* Why the pass is order-independent (determinism): systems are visited
* in a FIXED order — sorted by (x, y), a pure function of the seeded
* roster — and each system only counts frames ALREADY assigned to its
* neighbors. Nothing depends on generation timing or visit order, so
* lazy (on-arrival) content generation and eager generateAll stamp the
* same frames. Ties are broken by a derived, per-pick Rng (seeded).
*
* Shared rolls: the pass re-derives each system's composition via the
* same exported roll and forks as the content generator
* (SystemGenerator.rollSystemComposition) — guaranteed to agree, since
* the forks are pure functions of (seed, id, type, density).
*
* Cost: one pool scan per station-bearing system — trivial at this
* galaxy size (data/galaxy.json → systemCount), computed once at galaxy
* build time (js/galaxy/Galaxy.js).
*/
export function assignStationFrames({ seed, records, homeId, params, neighborsOf, typeDefs = null }) {
const defs = typeDefs ?? config.get('systems.types', {});
const shim = { params: params ?? {} }; // settlementDensity's shape
// The variant pool (data/stations.json → variants). An empty/missing
// pool means "no variants yet" — nothing is stamped and the renderer
// keeps its procedural station.
const raw = config.get('stations.variants');
const pool = Array.isArray(raw)
? raw.filter((f) => Number.isInteger(f) && f >= 0)
: [];
const frames = new Map(); // id → frame (only station-bearing systems)
if (pool.length === 0) return { frames };
const assigned = new Map(); // id → frame (assigned so far)
// FIXED spatial order (x, then y): a pure function of the seeded
// roster, so the assignment never depends on generation timing or
// visit order (the planet pass's contract).
const ordered = records.slice().sort((a, b) => (a.x - b.x) || (a.y - b.y));
for (const rec of ordered) {
const isHome = rec.id === homeId;
const attr = defs[rec.type]?.attributes ?? {};
const density = settlementDensity(shim, rec);
const { deepSpace } = rollSystemComposition(
seed, rec, isHome, attr.settlements ?? {}, density, attr,
);
if (!deepSpace) continue; // no deep-space station here — nothing to frame
const neighbors = (typeof neighborsOf === 'function' ? neighborsOf(rec.id) : []) ?? [];
// usage(frame) = how many ALREADY-ASSIGNED neighbor stars wear it
// (the spread objective — the same lexicographic pick as the planet
// pass, minus the "own" axis: a system never wears two stations).
const usage = (f) => {
let nb = 0;
for (const n of neighbors) {
if (assigned.get(n.id) === f) nb++;
}
return nb;
};
// The least-used face among the neighbors; ties broken by a
// deterministic derived pick (seed, id).
let best = Infinity;
for (const f of pool) best = Math.min(best, usage(f));
const tied = pool.filter((f) => usage(f) === best);
const frame = tied.length === 1 ? tied[0] : Rng.derive(seed, 'station-frames', rec.id).pick(tied);
frames.set(rec.id, frame);
assigned.set(rec.id, frame);
}
return { frames };
}