97 lines
4.3 KiB
JavaScript
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 };
|
|
}
|