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 }; }