Remove waypoint kind; allow up to two deep-space stations per system
- Waypoints were comms-only beacons with no landing or trade; a second deep-space station is a more useful destination in their place - The old waypoint chance in systems.json becomes deepSpaceStation.second, so a system can roll 0–2 stations - StationFrames now assigns a distinct sprite variant per station (Galaxy.stationFrames is id → [frame, …]) and a two-station system always wears two different variants - Rename the route-gate pulse from "destination's waypoint" to "route's next stop" so the term no longer refers to a removed kind
This commit is contained in:
parent
afa7a00c29
commit
54d2886c91
|
|
@ -72,7 +72,7 @@
|
|||
"rippleAlpha": 0.85
|
||||
},
|
||||
"objectPulse": {
|
||||
"_comment": "DESTINATION OBJECT PULSE (js/entities/RoutePulse.js via Planet.setRouteTarget / Station.setRouteTarget): in the DESTINATION system the route's last stop is the destination OBJECT (a planet or a space station — GameScene._routeDestinationEntity). It wears the SAME pulse as the route gate, SUBDUED: the same shape + timing language, but lower alpha, a smaller reach and a slower cycle — 'the place to go', without shouting like the gate's full pulse (pulse). The geometry still scales with the object's keepout radius (a small waypoint and a gas giant read the same).",
|
||||
"_comment": "DESTINATION OBJECT PULSE (js/entities/RoutePulse.js via Planet.setRouteTarget / Station.setRouteTarget): in the DESTINATION system the route's last stop is the destination OBJECT (a planet or a space station — GameScene._routeDestinationEntity). It wears the SAME pulse as the route gate, SUBDUED: the same shape + timing language, but lower alpha, a smaller reach and a slower cycle — 'the place to go', without shouting like the gate's full pulse (pulse). The geometry still scales with the object's keepout radius (a station and a gas giant read the same).",
|
||||
"baseRadius": 1.12,
|
||||
"expand": 0.4,
|
||||
"period": 1.8,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"VARIANT frames (0..2 — the deep-space station types, data/stations.json →",
|
||||
"variants; the same frame the station is drawn with and its comms label",
|
||||
"reads, data/stations.json → typeLabels). See data/stations.json for the",
|
||||
"station variants; waypoints are beacons, not a port (no clips).",
|
||||
"station variants.",
|
||||
"",
|
||||
"`land` — the full-screen one-shot clip played when the ship lands.",
|
||||
"`surface` — the looping clip shown on the surface deck behind the action bar.",
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
"procedural": [
|
||||
"Deep Space SC-145", "Orbital Station KX-2210", "Nav Relay V-77", "Forward Base SC-09", "Survey Camp 8812",
|
||||
"Transit Gate SG-4", "Habitat Ring 330", "Dock Platform 12", "Relay Node 210", "Anchor Point 55",
|
||||
"Beacon 4491", "Waypoint 7", "Station 9012", "Outpost Delta-7", "Habitat 44",
|
||||
"Beacon 4491", "Relay 7", "Station 9012", "Outpost Delta-7", "Habitat 44",
|
||||
"Ring Station 12", "Platform 44", "Module 7", "Sector 8", "Node 44",
|
||||
"Port 88", "Dock 11", "Berth 22", "Pad 33", "Bay 55",
|
||||
"Quay 66", "Spire 77", "Tower 88", "Rig 44", "Yard 55",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "The galaxy is already LIVED IN: it was settled long before the player. For now EVERY planet hosts a settlement (allPlanetsSettled): each world gets the one kind that fits its class (settledKindByClass — a habitable rocky OR arid world earns a colony, every other world gets its mining outfit, gas giants ride cloud bases, and the four CAPITAL worlds host the capital kind). Arid worlds are habitable by rule (data/systems.json doesn't roll it — js/galaxy/SystemGenerator.js sets habitable=true for every arid), so they settle as colonies like rocky habitables. The CAPITALS (data/factions.json — neutral/apex/mindweave/zenith) are designated systems whose outermost world is a class-`capital*` planet (data/planets.json → capitals); that world hosts the capital kind and its settlement carries the faction's `owner` (the reputation faction seam) — the neutral capital's owner stays null. Flip allPlanetsSettled to false to return to the old probabilistic layer (per-type chance + needs in data/systems.json). The free-space kinds (deepSpaceStation, waypoint) still roll per type. kinds = the shared vocabulary (label, theme, population range, anchor type). Core→rim density gradient lives in data/galaxy.json (settlements.gradient).",
|
||||
"_comment": "The galaxy is already LIVED IN: it was settled long before the player. For now EVERY planet hosts a settlement (allPlanetsSettled): each world gets the one kind that fits its class (settledKindByClass — a habitable rocky OR arid world earns a colony, every other world gets its mining outfit, gas giants ride cloud bases, and the four CAPITAL worlds host the capital kind). Arid worlds are habitable by rule (data/systems.json doesn't roll it — js/galaxy/SystemGenerator.js sets habitable=true for every arid), so they settle as colonies like rocky habitables. The CAPITALS (data/factions.json — neutral/apex/mindweave/zenith) are designated systems whose outermost world is a class-`capital*` planet (data/planets.json → capitals); that world hosts the capital kind and its settlement carries the faction's `owner` (the reputation faction seam) — the neutral capital's owner stays null. Flip allPlanetsSettled to false to return to the old probabilistic layer (per-type chance + needs in data/systems.json). The free-space kind (deepSpaceStation) still rolls per type — up to two per system (data/systems.json → settlements.deepSpaceStation `chance` + `second`). kinds = the shared vocabulary (label, theme, population range, anchor type). Core→rim density gradient lives in data/galaxy.json (settlements.gradient).",
|
||||
"allPlanetsSettled": true,
|
||||
"settledKindByClass": {
|
||||
"rocky": { "habitable": "colony", "default": "miningStation" },
|
||||
|
|
@ -47,13 +47,6 @@
|
|||
"theme": { "color": "#c9a7ff" },
|
||||
"population": { "min": 200, "max": 60000 },
|
||||
"anchor": "space"
|
||||
},
|
||||
"waypoint": {
|
||||
"label": "Waypoint",
|
||||
"description": "A small navigation beacon — the faint trace of a crossed galaxy.",
|
||||
"theme": { "color": "#8fa0c9" },
|
||||
"population": { "min": 0, "max": 12 },
|
||||
"anchor": "space"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "SPACE STATIONS — free-space settlements (deepSpaceStation, waypoint) rendered as world objects (js/entities/Station.js). enabled = draw them in-world (solid + discoverable + comms targets); shipClearance = the edge gap the ship holds at their surface (px — never crosses, like planets); compassColor = the compass-arrow accent for stations (bright purple — planets get green from data/planets.json, clusters their own gray from data/asteroids.json); kinds.<kind>.size = the station's keepout radius (px — the wings/ring extent); kinds.<kind>.ringSpeed = the deep-space station's ring rotation (rad/s, the procedural fallback only). texture/frameWidth/frameHeight = the station spritesheet (frame 0 = top-left); variants = the sheet frames a DEEP-SPACE STATION may wear — the galaxy-wide pass (js/galaxy/StationFrames.js) spreads them across the galaxy (a system avoids what its nearest stars already wear, like the planet frames — js/galaxy/PlanetFrames.js). typeLabels = the comms/HUD label a variant reads as (keyed by sheet frame — the fallback for an unlabeled frame is the kind's label, data/settlements.json → kinds.<kind>.label). The landing/surface/take-off/shop clips a variant plays come from data/landing.json → stationVideos (same sheet-frame key) — surface/shop are null until they are authored.",
|
||||
"_comment": "SPACE STATIONS — the free-space settlements (deepSpaceStation — the only free-space kind) rendered as world objects (js/entities/Station.js). enabled = draw them in-world (solid + discoverable + comms targets); shipClearance = the edge gap the ship holds at their surface (px — never crosses, like planets); compassColor = the compass-arrow accent for stations (bright purple — planets get green from data/planets.json, clusters their own gray from data/asteroids.json); kinds.<kind>.size = the station's keepout radius (px — the wings/ring extent); kinds.<kind>.ringSpeed = the deep-space station's ring rotation (rad/s, the procedural fallback only). texture/frameWidth/frameHeight = the station spritesheet (frame 0 = top-left); variants = the sheet frames a DEEP-SPACE STATION may wear — the galaxy-wide pass (js/galaxy/StationFrames.js) spreads them across the galaxy (a system avoids what its nearest stars already wear, like the planet frames — js/galaxy/PlanetFrames.js). typeLabels = the comms/HUD label a variant reads as (keyed by sheet frame — the fallback for an unlabeled frame is the kind's label, data/settlements.json → kinds.<kind>.label). The landing/surface/take-off/shop clips a variant plays come from data/landing.json → stationVideos (same sheet-frame key) — surface/shop are null until they are authored.",
|
||||
"enabled": true,
|
||||
"texture": "assets/images/spacestations.png",
|
||||
"frameWidth": 256,
|
||||
|
|
@ -16,9 +16,6 @@
|
|||
"deepSpaceStation": {
|
||||
"size": 108,
|
||||
"ringSpeed": 0.08
|
||||
},
|
||||
"waypoint": {
|
||||
"size": 46
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"_comment": "System archetypes. Each type is themable (theme) and has attributes that steer the SystemGenerator: star classes, binary chance, planet class weights, moon/belt chances, habitability, hazard, and free-space settlement odds (settlements: deepSpaceStation / waypoint). objectCount is a GLOBAL rule, not per-type: the starting system is exempt (it always holds exactly two generated planets — gas giant + rocky — beside the home world, plus at most one free-space station). Every other system rolls its TOTAL object count — planets + free-space stations together — from this table: `barren` of systems hold ZERO objects (a gate-only dead-end LEAF of the jump network — the center is empty, the star is invisible flavor, and the only things there are the single gate and 1–2 asteroid clusters drifting inside its tether, data/asteroids.json → barren), the rest hold `objects` counts. The split is resolved by rolling the free-space stations first (per-type odds in attributes.settlements × the home→far density gradient, at most two — deepSpaceStation + waypoint), then planets = N − stations; if that roll would leave the system with NO planet (stations = N), one station is demoted to a planet — every non-barren system keeps ≥ 1 planet, the gate's tether anchor and the world the player can build out from. Every planet is settled by rule (data/settlements.json → allPlanetsSettled + settledKindByClass). distribution.weight sets how common the type is GLOBALLY; which types favor WHICH REGION of the galaxy (the home corner, the middle, the far/deep corner) is the per-zone mix in data/galaxy.json → distribution.zoneMix, keyed off each system's home→far diagonal zone (record.zone).",
|
||||
"_comment": "System archetypes. Each type is themable (theme) and has attributes that steer the SystemGenerator: star classes, binary chance, planet class weights, moon/belt chances, habitability, hazard, and free-space settlement odds (settlements.deepSpaceStation: `chance` of a station, `second` of a second one — up to two per system). objectCount is a GLOBAL rule, not per-type: the starting system is exempt (it always holds exactly two generated planets — gas giant + rocky — beside the home world, plus at most one free-space station). Every other system rolls its TOTAL object count — planets + free-space stations together — from this table: `barren` of systems hold ZERO objects (a gate-only dead-end LEAF of the jump network — the center is empty, the star is invisible flavor, and the only things there are the single gate and 1–2 asteroid clusters drifting inside its tether, data/asteroids.json → barren), the rest hold `objects` counts. The split is resolved by rolling the free-space stations first (per-type odds in attributes.settlements × the home→far density gradient, at most two), then planets = N − stations; if that roll would leave the system with NO planet (stations = N), one station is demoted to a planet — every non-barren system keeps ≥ 1 planet, the gate's tether anchor and the world the player can build out from. Every planet is settled by rule (data/settlements.json → allPlanetsSettled + settledKindByClass). distribution.weight sets how common the type is GLOBALLY; which types favor WHICH REGION of the galaxy (the home corner, the middle, the far/deep corner) is the per-zone mix in data/galaxy.json → distribution.zoneMix, keyed off each system's home→far diagonal zone (record.zone).",
|
||||
"_effect": "Per-type VISUAL EFFECT (js/visuals/SystemEffects.js): the star's character. Each sub-effect is an OPTIONAL block — a type wears exactly what it lists (an empty {} = no effect, renders untouched). CAMERA FILTERS (WebGL; the HUD is excluded by the UI-camera split) — 'ripple' { center: screen|star, strength (wave cycles), amplitude (displacement, fraction of screen), speed (rad/s), padding (framebuffer slack, keep >= amplitude*width*(1+flare.rippleBoost)*centers), centers: 1|2, orbit {radius, period} (two centers circling each other) }; 'grade' { tint (hex hue), amount (0..1), saturation (1=none), brightness, lift {amount, color — or a+b for two directional hues (the binary's two lights glow opposite sides of the view, across the seeded axis)}, grain {amount, parallax}, split {a, b hex, mix 0..1, axis deg} (a two-color directional wash) }. 'flare' { interval, duration (seconds), rippleBoost x, flash 0..1, flashColor } is a SHARED seeded rhythm that surges the ripple's amplitude AND flashes the grade warm — the star flaring (a ripple with no flare just shimmers at its base amplitude). 'breath' { period (s), depth 0..0.5 } — the binary's two lights slowly trade brightness (take their turn in the sky). CONTENT (world-space sprites, Canvas-safe, depth 3, behind the art) — 'particles' (ember dust), 'wanderer' (a distant companion star: coreSize, haloSize, parallax, speed — camera-anchored so it stays in the sky), 'haze' (a full-screen atmospheric color+grit layer behind the stars, redDwarf's red void), 'void' (a full-screen NEUTRAL dark+grit base behind the stars — the binary's two lights lift/breathe it into a two-color wash; without it the clear color bypasses the filter and the void can't be tinted), 'lightning' (OVERT bolts streaking the background sky — the nebula's far-sky discharge: a jagged edge-to-edge trunk + random branches in the gas's base/accent colors (alternating per strike), strobed, optional soft distant thunder; world Graphics at depth 2.9 — the sky itself, behind the back gas; interval [s], strike (s), levels, branches, glowWidth, coreWidth, glowAlpha, coreAlpha, parallax, thunder). LIVE TODAY: nebula = a steady ripple + occasional far-sky lightning bolts (in the gas colors); redDwarf = a SUPER-subtle ripple that SURGES on each flare + a warm grade with shadow lift, gritty grain and a red haze + ember dust; binary = a two-color split grade + a two-light void glow (lift painted on a neutral void base) + a slow breath + a wandering companion star (NO ripple — two stars, one steady light). Per-system variation (flare phase, split axis, breath phase, wanderer path) is seeded from the system id in SystemGenerator (content.fx), so each system is stable but distinct. Tuning is a JSON edit.",
|
||||
"objectCount": {
|
||||
"barren": 0.10,
|
||||
|
|
@ -29,8 +29,7 @@
|
|||
"habitability": 0.1,
|
||||
"hazard": 0.1,
|
||||
"settlements": {
|
||||
"deepSpaceStation": { "chance": 0.25 },
|
||||
"waypoint": { "chance": 0.35 }
|
||||
"deepSpaceStation": { "chance": 0.25, "second": 0.35 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -58,8 +57,7 @@
|
|||
"habitability": 0.22,
|
||||
"hazard": 0.12,
|
||||
"settlements": {
|
||||
"deepSpaceStation": { "chance": 0.2 },
|
||||
"waypoint": { "chance": 0.3 }
|
||||
"deepSpaceStation": { "chance": 0.2, "second": 0.3 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -86,8 +84,7 @@
|
|||
"habitability": 0.06,
|
||||
"hazard": 0.18,
|
||||
"settlements": {
|
||||
"deepSpaceStation": { "chance": 0.3 },
|
||||
"waypoint": { "chance": 0.3 }
|
||||
"deepSpaceStation": { "chance": 0.3, "second": 0.3 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -124,8 +121,7 @@
|
|||
"habitability": 0.5,
|
||||
"hazard": 0.05,
|
||||
"settlements": {
|
||||
"deepSpaceStation": { "chance": 0.25 },
|
||||
"waypoint": { "chance": 0.4 }
|
||||
"deepSpaceStation": { "chance": 0.25, "second": 0.4 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -162,8 +158,7 @@
|
|||
"habitability": 0.02,
|
||||
"hazard": 0.35,
|
||||
"settlements": {
|
||||
"deepSpaceStation": { "chance": 0.2 },
|
||||
"waypoint": { "chance": 0.45 }
|
||||
"deepSpaceStation": { "chance": 0.2, "second": 0.45 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -185,8 +180,7 @@
|
|||
"habitability": 0.02,
|
||||
"hazard": 0.5,
|
||||
"settlements": {
|
||||
"deepSpaceStation": { "chance": 0.15 },
|
||||
"waypoint": { "chance": 0.25 }
|
||||
"deepSpaceStation": { "chance": 0.15, "second": 0.25 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ check('lerpAngle eases the short way (3→−3, no long-way sweep)', Math.abs(le
|
|||
check('isFarTarget: one px past ⇒ compact', isFarTarget({ x: 5121, y: 0 }, { x: 0, y: 0 }, 5120) === true);
|
||||
check('isFarTarget: no ship ⇒ full (standalone path)', isFarTarget({ x: 99999, y: 0 }, null, 5120) === false);
|
||||
check('isFarTarget: farDistance off (0) ⇒ full', isFarTarget({ x: 99999, y: 0 }, { x: 0, y: 0 }, 0) === false);
|
||||
check('isFarTarget: alwaysFull overrides distance (route waypoint)', isFarTarget({ x: 99999, y: 0, alwaysFull: true }, { x: 0, y: 0 }, 5120) === false);
|
||||
check('isFarTarget: alwaysFull overrides distance (route next stop)', isFarTarget({ x: 99999, y: 0, alwaysFull: true }, { x: 0, y: 0 }, 5120) === false);
|
||||
check('isFarTarget: alwaysFull absent ⇒ distance rule holds', isFarTarget({ x: 99999, y: 0 }, { x: 0, y: 0 }, 5120) === true);
|
||||
|
||||
const fakeScene = {
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ let big;
|
|||
if (s.kind === 'capital' && !String(p?.class ?? '').startsWith('capital')) anchorRulesOk = false;
|
||||
}
|
||||
}
|
||||
check('the lived-in mix appears (colonies, miners, cloud bases, stations, beacons)', ['colony', 'miningStation', 'cloudBase', 'deepSpaceStation', 'waypoint'].every((k) => seen.has(k)));
|
||||
check('the lived-in mix appears (colonies, miners, cloud bases, stations)', ['colony', 'miningStation', 'cloudBase', 'deepSpaceStation'].every((k) => seen.has(k)));
|
||||
check('colonies sit on habitable worlds; cloud bases ride gas giants; capitals sit on capital worlds', anchorRulesOk);
|
||||
|
||||
// The all-planets-settled rule (for now): EVERY world hosts a settlement,
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ gateB.update(3000);
|
|||
check('active gate: the swirl spins (rotation increases with time — clockwise)',
|
||||
r1 > 0 && swirlB.rotation > r1);
|
||||
|
||||
// --- The route-target pulse (the destination's WAYPOINT mark) -----------
|
||||
// --- The route-target pulse (the route's next-stop mark) -----------
|
||||
const bR = config.get('gates.route.pulse.baseRadius', 1.18);
|
||||
check('fresh gate: no route pulse by default', gate.routePulse.on === false && gate.routePulse.ring.visible === false);
|
||||
gate.setRouteTarget(true);
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ const { RoutePulse } = await import(pathToFileURL(join(__dirname, '../js/entitie
|
|||
const { Station } = await import(pathToFileURL(join(__dirname, '../js/entities/Station.js')).href);
|
||||
const scene = makeScene(false); // sheet missing → the procedural build
|
||||
const st = new Station(scene, {
|
||||
id: 'set-wp-test', kind: 'waypoint', name: 'Waypoint One',
|
||||
id: 'set-station-test', kind: 'deepSpaceStation', name: 'Station One',
|
||||
anchor: { type: 'space' }, x: 40, y: 70,
|
||||
});
|
||||
check('fresh station: the route pulse is off + hidden by default',
|
||||
|
|
|
|||
|
|
@ -50,17 +50,18 @@ const POOL = Math.max(1, Math.floor(g.params.neighbors ?? 8));
|
|||
const POOLV = (config.get('stations.variants') ?? []).filter((f) => Number.isInteger(f) && f >= 0);
|
||||
check('data/stations.json → variants is a non-empty integer pool', POOLV.length >= 1, JSON.stringify(config.get('stations.variants')));
|
||||
|
||||
// The station-bearing systems and their stamped frame (via content).
|
||||
const stations = new Map(); // id → { rec, frame, name }
|
||||
// The station-bearing systems and their stamped frames (via content).
|
||||
const stations = new Map(); // id → [{ rec, frame, name }] (one per station)
|
||||
for (const rec of g.records) {
|
||||
const c = g.ensureContent(rec.id);
|
||||
for (const s of c.settlements) {
|
||||
if (s.kind === 'deepSpaceStation') {
|
||||
stations.set(rec.id, { rec, frame: s.stationFrame, name: s.name });
|
||||
if (!stations.has(rec.id)) stations.set(rec.id, []);
|
||||
stations.get(rec.id).push({ rec, frame: s.stationFrame, name: s.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` (galaxy: ${g.records.length} systems, ${stations.size} deep-space stations, pool [${POOLV}])`);
|
||||
console.log(` (galaxy: ${g.records.length} systems, ${[...stations.values()].reduce((n, a) => n + a.length, 0)} deep-space stations across ${stations.size} systems, pool [${POOLV}])`);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 1. Frames are present and in-pool
|
||||
|
|
@ -68,16 +69,33 @@ console.log(` (galaxy: ${g.records.length} systems, ${stations.size} deep-space
|
|||
{
|
||||
let inPool = true;
|
||||
let why = '';
|
||||
for (const [id, st] of stations) {
|
||||
if (typeof st.frame !== 'number' || !Number.isInteger(st.frame) || !POOLV.includes(st.frame)) {
|
||||
inPool = false;
|
||||
if (!why) why = `${id}: frame ${st.frame} ∉ [${POOLV}]`;
|
||||
for (const [id, sts] of stations) {
|
||||
for (const st of sts) {
|
||||
if (typeof st.frame !== 'number' || !Number.isInteger(st.frame) || !POOLV.includes(st.frame)) {
|
||||
inPool = false;
|
||||
if (!why) why = `${id}: frame ${st.frame} ∉ [${POOLV}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
check('every deep-space station wears a pool variant (settlement.stationFrame)', inPool, why);
|
||||
check('the galaxy pass covers exactly the station-bearing systems',
|
||||
stations.size === g.stationFrames.size,
|
||||
`${stations.size} settlements vs ${g.stationFrames.size} pass entries`);
|
||||
// A system holding two stations spreads them — two different variants.
|
||||
let distinctOk = true;
|
||||
let whyD = '';
|
||||
if (POOLV.length > 1) {
|
||||
for (const [id, sts] of stations) {
|
||||
if (sts.length > 1) {
|
||||
const frames = sts.map((s) => s.frame);
|
||||
if (new Set(frames).size !== frames.length) {
|
||||
distinctOk = false;
|
||||
if (!whyD) whyD = `${id}: ${frames.join(', ')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
check('a two-station system wears two different variants', distinctOk, whyD);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
|
@ -96,23 +114,31 @@ console.log(` (galaxy: ${g.records.length} systems, ${stations.size} deep-space
|
|||
let checks = 0;
|
||||
for (const sd of SEEDS) {
|
||||
const gg = Galaxy.create(sd);
|
||||
const sts = new Map(); // id → stamped frame
|
||||
for (const rec of gg.records)
|
||||
for (const s of gg.ensureContent(rec.id).settlements)
|
||||
if (s.kind === 'deepSpaceStation') sts.set(rec.id, s.stationFrame);
|
||||
const sts = new Map(); // id → [stamped frame, ...]
|
||||
for (const rec of gg.records) {
|
||||
for (const s of gg.ensureContent(rec.id).settlements) {
|
||||
if (s.kind === 'deepSpaceStation') {
|
||||
if (!sts.has(rec.id)) sts.set(rec.id, []);
|
||||
sts.get(rec.id).push(s.stationFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Naive: independent random pool picks (seeded, per station).
|
||||
const naiveF = {};
|
||||
for (const id of sts.keys())
|
||||
naiveF[id] = POOLV.length === 1
|
||||
const naiveF = new Map(); // id → [frame, ...]
|
||||
for (const [id, frames] of sts) {
|
||||
naiveF.set(id, frames.map((_, i) => POOLV.length === 1
|
||||
? POOLV[0]
|
||||
: POOLV[Math.floor(Rng.derive(sd, 'naive-station', id).next() * POOLV.length)];
|
||||
for (const [id, fr] of sts) {
|
||||
for (const nb of gg.neighborsOf(id, POOL)) {
|
||||
checks++;
|
||||
const nfr = sts.get(nb.id);
|
||||
if (nfr !== undefined && nfr === fr) passColl++;
|
||||
const nnfr = naiveF[nb.id];
|
||||
if (nnfr !== undefined && nnfr === fr) naiveColl++;
|
||||
: POOLV[Math.floor(Rng.derive(sd, 'naive-station', id, i).next() * POOLV.length)]));
|
||||
}
|
||||
for (const [id, frames] of sts) {
|
||||
for (const fr of frames) {
|
||||
for (const nb of gg.neighborsOf(id, POOL)) {
|
||||
checks++;
|
||||
const nfs = sts.get(nb.id) ?? [];
|
||||
const nnfs = naiveF.get(nb.id) ?? [];
|
||||
if (nfs.includes(fr)) passColl++;
|
||||
if (nnfs.includes(fr)) naiveColl++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +151,7 @@ console.log(` (galaxy: ${g.records.length} systems, ${stations.size} deep-space
|
|||
// The spread should also produce ALL variants across the galaxy
|
||||
// (with 3 stations' worth of pool breadth and this many stations,
|
||||
// pigeonhole is generous) — the point is variety, not just local spread.
|
||||
const used = new Set(stations.size === 0 ? [] : [...stations.values()].map((s) => s.frame));
|
||||
const used = new Set([...stations.values()].flatMap((sts) => sts.map((s) => s.frame)));
|
||||
check('the galaxy wears more than one station type (the pool shows variety)',
|
||||
used.size >= Math.min(2, POOLV.length, stations.size),
|
||||
`used [${[...used].sort()}] of pool [${POOLV}]`);
|
||||
|
|
@ -143,10 +169,11 @@ console.log(` (galaxy: ${g.records.length} systems, ${stations.size} deep-space
|
|||
// Content stamp === galaxy pass (the stamping contract).
|
||||
let stamped = true;
|
||||
let why = '';
|
||||
for (const [id, st] of stations) {
|
||||
if (g2.stationFrames.get(id) !== st.frame) {
|
||||
for (const [id, sts] of stations) {
|
||||
const pass = g2.stationFrames.get(id) ?? [];
|
||||
if (!sts.every((st) => pass.includes(st.frame))) {
|
||||
stamped = false;
|
||||
if (!why) why = `${id}: content ${st.frame} vs pass ${g2.stationFrames.get(id)}`;
|
||||
if (!why) why = `${id}: content ${sts.map((s) => s.frame).join(',')} vs pass ${JSON.stringify(pass)}`;
|
||||
}
|
||||
}
|
||||
check('content settlement frames === the galaxy pass (lazy === eager stamping)', stamped, why);
|
||||
|
|
|
|||
|
|
@ -130,10 +130,10 @@ function verifyPick(spec, pick, why) {
|
|||
const c = contentOf(pick.record.id);
|
||||
if (spec.kind === 'station') {
|
||||
if (!pick.station) return `missing station flag on ${pick.record.id}`;
|
||||
const s = c.settlements.find((x) => x.kind === 'deepSpaceStation');
|
||||
if (!s) return `${pick.record.id} has no deepSpaceStation in content`;
|
||||
if (!pick.reskin && spec.frame != null && s.stationFrame !== spec.frame) {
|
||||
return `${pick.record.id} station wears ${s.stationFrame}, spec wants ${spec.frame}`;
|
||||
const sts = c.settlements.filter((x) => x.kind === 'deepSpaceStation');
|
||||
if (sts.length === 0) return `${pick.record.id} has no deepSpaceStation in content`;
|
||||
if (!pick.reskin && spec.frame != null && !sts.some((s) => s.stationFrame === spec.frame)) {
|
||||
return `${pick.record.id} stations wear ${sts.map((s) => s.stationFrame).join(',')} spec wants ${spec.frame}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -311,8 +311,9 @@ function verifyPick(spec, pick, why) {
|
|||
const worn = new Set();
|
||||
for (const rec of g.records) {
|
||||
const c = contentOf(rec.id);
|
||||
const s = c.settlements.find((x) => x.kind === 'deepSpaceStation');
|
||||
if (s?.stationFrame != null) worn.add(s.stationFrame);
|
||||
for (const s of c.settlements) {
|
||||
if (s.kind === 'deepSpaceStation' && s.stationFrame != null) worn.add(s.stationFrame);
|
||||
}
|
||||
}
|
||||
let ok = true;
|
||||
let why = '';
|
||||
|
|
@ -419,8 +420,8 @@ function verifyPick(spec, pick, why) {
|
|||
jumpGatesFor: (id) => Array.from({ length: id === sA.id ? 3 : 1 }, (_, i) => ({ id: `${id}-gate-${i}` })),
|
||||
};
|
||||
const other = stationVariants.find((f) => f !== variant) ?? stationVariants[0];
|
||||
gsf.stationFrames.set(sA.id, other);
|
||||
gsf.stationFrames.set(sB.id, variant);
|
||||
gsf.stationFrames.set(sA.id, [other]);
|
||||
gsf.stationFrames.set(sB.id, [variant]);
|
||||
const pick = pickPreviewSystem(gsf, { kind: 'station', frame: variant });
|
||||
check('ranking (station): exact beats a richer reskin (B wins)', pick?.record?.id === sB.id && pick.station === true && pick.reskin === false, `got ${pick?.record?.id} reskin=${pick?.reskin}`);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -165,10 +165,9 @@ player arrives. **Every planet is settled (for now)**: each world hosts
|
|||
the one settlement kind that fits its class (`data/settlements.json →
|
||||
allPlanetsSettled` + `settledKindByClass`) — `colony` on a habitable
|
||||
rocky world, `miningStation` over every other rocky/ice/lava world,
|
||||
`cloudBase` riding every gas giant. Plus the free-space kinds:
|
||||
`deepSpaceStation` (adrift in open space) and `waypoint` (a small beacon
|
||||
— the faint trace of a crossed galaxy), still rolled per archetype and
|
||||
thinned core→rim. **Barren systems** (the `objectCount` → 0 stops) are
|
||||
`cloudBase` riding every gas giant. Plus the free-space kind:
|
||||
`deepSpaceStation` (adrift in open space — up to two per system), still
|
||||
rolled per archetype and thinned core→rim. **Barren systems** (the `objectCount` → 0 stops) are
|
||||
the one exception to "lived in": they hold no planets and no stations —
|
||||
just the jump gate — and are reachable only by jumping in (strong
|
||||
connectivity keeps them on the network). Their "charted · unclaimed" report
|
||||
|
|
@ -182,8 +181,8 @@ branch is now the normal case, not a defensive fallback. Model & seams:
|
|||
probabilistic layer (per-type `chance` + `needs` — the code path is
|
||||
kept behind the flag).
|
||||
- **Per-type free-space rates** — `types.<id>.attributes.settlements` in
|
||||
`data/systems.json`: `chance` per free-space kind (deep-space station,
|
||||
waypoint). Same attribute-driven pattern as everything else.
|
||||
`data/systems.json`: `deepSpaceStation.chance` of a station (+ `second`
|
||||
of a second one). Same attribute-driven pattern as everything else.
|
||||
- **Home→far gradient** — `galaxy.settlements.gradient` in
|
||||
`data/galaxy.json`: the settled heart (the home corner, `d = 0`) is
|
||||
denser (factor 1.0), the deep corner (`d = 1`) is thinner (clamped to
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export function goodsTextureReady(scene) {
|
|||
* The economic class of a settlement — the single key the catalog prices off.
|
||||
* planet → its own class (terran/arid/rocky/gas/ice/lava), or 'capital'
|
||||
* for a capital world (class capital*).
|
||||
* station → 'station' (a deepSpaceStation trades; a waypoint does not).
|
||||
* station → 'station' (a deepSpaceStation — the only free-space kind).
|
||||
* @returns {'terran'|'arid'|'rocky'|'gas'|'ice'|'lava'|'station'|'capital'|null}
|
||||
*/
|
||||
export function economicClass(planet = null, settlement = null) {
|
||||
|
|
@ -493,7 +493,7 @@ export function buildEconomy(galaxy) {
|
|||
const comp = rollSystemComposition(galaxy.seed, record, isHome, spec, density, attr, stamped);
|
||||
const set = new Set(comp.classes);
|
||||
if (isHome) set.add(homeCls); // the home world (a terran body by rule)
|
||||
if (comp.deepSpace) set.add('station');
|
||||
if ((comp.stations ?? 0) > 0) set.add('station');
|
||||
if (record.capital) set.add('capital');
|
||||
classBySystem.set(id, set);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { Planet } from './Planet.js';
|
|||
*
|
||||
* ROUTE TARGET (data/gates.json → route.pulse): while a destination is
|
||||
* set and this gate is the route's NEXT STOP in the current system
|
||||
* (GameScene.routeNextGate — the destination's WAYPOINT here), the scene
|
||||
* (GameScene.routeNextGate — the route's NEXT STOP here), the scene
|
||||
* marks the gate (setRouteTarget) and update() animates the FULL route
|
||||
* pulse around it (js/entities/RoutePulse.js — the shared halo, also
|
||||
* worn in a subdued form by the destination OBJECT, Planet/Station):
|
||||
|
|
@ -74,7 +74,7 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
this.theme = this._theme();
|
||||
this.ringBody = null;
|
||||
this.swirl = null;
|
||||
this.routePulse = null; // the destination's waypoint mark (GameScene → setRouteTarget)
|
||||
this.routePulse = null; // the route's next-stop mark (GameScene → setRouteTarget)
|
||||
this.setDepth(o.depth ?? 5);
|
||||
this.rotation = gate.rotation ?? 0; // face the destination star
|
||||
|
||||
|
|
@ -241,9 +241,9 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
|
||||
/**
|
||||
* ROUTE-TARGET state (GameScene.update, each frame): true while this
|
||||
* gate is the route's NEXT STOP — the destination's waypoint in the
|
||||
* current system (routeNextGate). True shows the FULL pulse (update()
|
||||
* animates it); false clears + hides it. A no-op when unchanged.
|
||||
* gate is the route's NEXT STOP in the current system (routeNextGate).
|
||||
* True shows the FULL pulse (update() animates it); false clears +
|
||||
* hides it. A no-op when unchanged.
|
||||
*/
|
||||
setRouteTarget(on) {
|
||||
this.routePulse?.set(on);
|
||||
|
|
@ -252,7 +252,7 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
/** The swirl spins + breathes (active); the fallback art drifts. (GameScene.update drives this.) */
|
||||
update(time) {
|
||||
const t = time / 1000;
|
||||
// The route-target pulse (the destination's waypoint in this system).
|
||||
// The route-target pulse (the route's next stop in this system).
|
||||
// It runs even on a DORMANT gate — the player still has to find the
|
||||
// gate to use it. (A no-op while unmarked.)
|
||||
this.routePulse?.draw(t);
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
* Shape: a steady target ring at `baseRadius` × the object's keepout
|
||||
* radius, plus two radar ripples (staggered half a period) that expand
|
||||
* a further ×`expand` past it and fade out. All the geometry scales with
|
||||
* the object's keepout radius, so it reads the same on a small waypoint
|
||||
* beacon and a gas giant.
|
||||
* the object's keepout radius, so it reads the same on a small station
|
||||
* and a gas giant.
|
||||
*
|
||||
* Who wears it (GameScene.update marks exactly one object per frame —
|
||||
* the route's NEXT STOP):
|
||||
* · the ROUTE GATE (the destination's waypoint in an intermediate
|
||||
* · the ROUTE GATE (the destination's next stop in an intermediate
|
||||
* system — GameScene.routeNextGate) — the FULL pulse
|
||||
* (data/gates.json → route.pulse), via JumpGate.setRouteTarget
|
||||
* · the DESTINATION OBJECT in the destination system (a planet or a
|
||||
|
|
|
|||
|
|
@ -7,30 +7,27 @@ import { Planet } from './Planet.js';
|
|||
|
||||
/**
|
||||
* A SPACE STATION — a free-space settlement (SystemGenerator:
|
||||
* `anchor.type === 'space'`, kinds `deepSpaceStation` / `waypoint`)
|
||||
* rendered as a world object:
|
||||
* `anchor.type === 'space'`, kind `deepSpaceStation` — the only
|
||||
* free-space kind) rendered as a world object:
|
||||
*
|
||||
* DEEP-SPACE STATION — a variant from the shared spritesheet
|
||||
* (data/stations.json → texture: spacestations.png, 256×256 frames;
|
||||
* `variants` lists the frames in use, today 0..2 — the first three
|
||||
* frames). Which variant it wears comes from the galaxy-wide spread
|
||||
* pass (js/galaxy/StationFrames.js, stamped as `settlement.stationFrame`):
|
||||
* A variant from the shared spritesheet (data/stations.json →
|
||||
* texture: spacestations.png, 256×256 frames; `variants` lists the
|
||||
* frames in use, today 0..2 — the first three frames). Which variant
|
||||
* it wears comes from the galaxy-wide spread pass
|
||||
* (js/galaxy/StationFrames.js, stamped as `settlement.stationFrame`):
|
||||
* the frame avoids what the NEAREST stars already wear, so the same
|
||||
* station type is spread across the galaxy instead of clustering. The
|
||||
* SAME frame picks its landing/take-off clips (data/landing.json →
|
||||
* stationVideos) — a station always lands with the clip matching the
|
||||
* art it is drawn with.
|
||||
*
|
||||
* WAYPOINT — a small nav beacon: a base, a mast, a breathing light
|
||||
* (procedural — beacons have no sheet art).
|
||||
*
|
||||
* Solid like a planet (the ship keeps its clearance — the same plain
|
||||
* circle rule, GameScene.solids), discoverable (a compass arrow + the
|
||||
* discovery toast, at the scale of its keepout), and a comms target —
|
||||
* click it and the comms panel opens there (the click is NOT a
|
||||
* fly-here — the ship stays put; GameScene.openCommsPanel). Deep-space
|
||||
* stations are also a PORT: REQUEST LANDING launches the surface
|
||||
* sequence (GameScene.startLanding → SurfaceScene, the station's clips).
|
||||
* fly-here — the ship stays put; GameScene.openCommsPanel). A station
|
||||
* is a PORT: REQUEST LANDING launches the surface sequence
|
||||
* (GameScene.startLanding → SurfaceScene, the station's clips).
|
||||
*
|
||||
* The deep-space station's art fills its frame, so it is scaled to 2×size
|
||||
* (size = the keepout RADIUS — data/stations.json → kinds.<kind>.size):
|
||||
|
|
@ -71,28 +68,24 @@ export class Station extends Phaser.GameObjects.Container {
|
|||
this.discoveryId = settlement.id;
|
||||
this.discoveryName = settlement.name;
|
||||
|
||||
this.size = config.get(`stations.kinds.${this.kind}.size`, this.kind === 'waypoint' ? 46 : 108);
|
||||
this.size = config.get(`stations.kinds.${this.kind}.size`, 108);
|
||||
this.radius = this.size; // the keepout circle's radius
|
||||
this.bound = this.size; // the discovery radius (the compass/toast scale)
|
||||
this.clearance = config.get('stations.shipClearance', 50);
|
||||
this.ringSpeed = config.get(`stations.kinds.${this.kind}.ringSpeed`, 0.08);
|
||||
this.ringBody = null;
|
||||
this.sprite = null;
|
||||
// The spacestations.png frame this station wears (null = no sheet art:
|
||||
// waypoints, or a pool with no frames yet).
|
||||
this.sheetFrame = null;
|
||||
if (this.kind === 'deepSpaceStation') {
|
||||
const v = settlement.stationFrame;
|
||||
if (Number.isInteger(v) && v >= 0) {
|
||||
this.sheetFrame = v; // the spread pass's pick (data/stations.json → variants)
|
||||
} else {
|
||||
// Fallback for content that predates the pass (dev tools, old
|
||||
// fixtures): a deterministic pool pick keyed on the settlement's
|
||||
// own (seed-derived) id — same galaxy ⇒ same face.
|
||||
const pool = config.get('stations.variants');
|
||||
const list = Array.isArray(pool) && pool.length > 0 ? pool : [0];
|
||||
this.sheetFrame = Rng.derive('orbit-station-variant', settlement.id).pick(list) ?? 0;
|
||||
}
|
||||
// The spacestations.png frame this station wears (null = no pool yet).
|
||||
const v = settlement.stationFrame;
|
||||
if (Number.isInteger(v) && v >= 0) {
|
||||
this.sheetFrame = v; // the spread pass's pick (data/stations.json → variants)
|
||||
} else {
|
||||
// Fallback for content that predates the pass (dev tools, old
|
||||
// fixtures): a deterministic pool pick keyed on the settlement's
|
||||
// own (seed-derived) id — same galaxy ⇒ same face.
|
||||
const pool = config.get('stations.variants');
|
||||
const list = Array.isArray(pool) && pool.length > 0 ? pool : [0];
|
||||
this.sheetFrame = Rng.derive('orbit-station-variant', settlement.id).pick(list) ?? 0;
|
||||
}
|
||||
this.setDepth(o.depth ?? 5);
|
||||
|
||||
|
|
@ -100,8 +93,7 @@ export class Station extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
/**
|
||||
* The variant art (deep-space stations) — or the procedural hardware
|
||||
* (waypoints, and the deep-space fallback when the sheet is missing).
|
||||
* The variant art — or the procedural hardware fallback (sheet missing).
|
||||
*/
|
||||
build() {
|
||||
// The route-target pulse (data/gates.json → route.objectPulse): when
|
||||
|
|
@ -146,83 +138,67 @@ export class Station extends Phaser.GameObjects.Container {
|
|||
const g = scene.add.graphics();
|
||||
this.add(g);
|
||||
|
||||
if (this.kind === 'waypoint') {
|
||||
// A small nav beacon: a base, a mast, a breathing light.
|
||||
g.fillStyle(0x4a3a2c, 1);
|
||||
g.fillRect(-S * 0.22, S * 0.28, S * 0.44, S * 0.16); // the base
|
||||
g.fillStyle(0x57493a, 1);
|
||||
g.fillRect(-S * 0.06, -S * 0.45, S * 0.12, S * 0.78); // the mast
|
||||
g.fillStyle(0x6e3a1f, 0.5); // a rust band
|
||||
g.fillRect(-S * 0.06, 0, S * 0.12, S * 0.18);
|
||||
g.lineStyle(1.5, 0x2c2118, 1);
|
||||
g.strokeCircle(0, -S * 0.5, S * 0.13);
|
||||
g.fillStyle(0x3a2f26, 1);
|
||||
g.fillCircle(0, -S * 0.5, S * 0.1);
|
||||
} else {
|
||||
// ---- Deep-space station -----------------------------------------
|
||||
const wingX0 = S * 0.24;
|
||||
const wingX1 = S * 0.85;
|
||||
const wingH = S * 0.18;
|
||||
// Solar wings — weathered blue-gray panels in a rusty frame.
|
||||
for (const dir of [-1, 1]) {
|
||||
const x0 = dir === 1 ? wingX0 : -wingX1;
|
||||
const w = wingX1 - wingX0;
|
||||
g.lineStyle(3, 0x57493a, 1);
|
||||
g.lineBetween(dir * S * 0.1, 0, dir * wingX0, 0); // the truss
|
||||
g.fillStyle(0x3f5468, 1);
|
||||
g.fillRect(x0, -wingH / 2, w, wingH);
|
||||
g.fillStyle(0x6e3a1f, 0.35); // rust streak on the outer edge
|
||||
g.fillRect(dir === 1 ? wingX1 - 6 : -wingX1, -wingH / 2, 6, wingH);
|
||||
g.lineStyle(2, 0x22303e, 1);
|
||||
g.strokeRect(x0, -wingH / 2, w, wingH);
|
||||
g.lineStyle(1, 0x22303e, 0.9);
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const x = x0 + (w * i) / 5;
|
||||
g.lineBetween(x, -wingH / 2, x, wingH / 2);
|
||||
}
|
||||
// ---- Deep-space station -----------------------------------------
|
||||
const wingX0 = S * 0.24;
|
||||
const wingX1 = S * 0.85;
|
||||
const wingH = S * 0.18;
|
||||
// Solar wings — weathered blue-gray panels in a rusty frame.
|
||||
for (const dir of [-1, 1]) {
|
||||
const x0 = dir === 1 ? wingX0 : -wingX1;
|
||||
const w = wingX1 - wingX0;
|
||||
g.lineStyle(3, 0x57493a, 1);
|
||||
g.lineBetween(dir * S * 0.1, 0, dir * wingX0, 0); // the truss
|
||||
g.fillStyle(0x3f5468, 1);
|
||||
g.fillRect(x0, -wingH / 2, w, wingH);
|
||||
g.fillStyle(0x6e3a1f, 0.35); // rust streak on the outer edge
|
||||
g.fillRect(dir === 1 ? wingX1 - 6 : -wingX1, -wingH / 2, 6, wingH);
|
||||
g.lineStyle(2, 0x22303e, 1);
|
||||
g.strokeRect(x0, -wingH / 2, w, wingH);
|
||||
g.lineStyle(1, 0x22303e, 0.9);
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const x = x0 + (w * i) / 5;
|
||||
g.lineBetween(x, -wingH / 2, x, wingH / 2);
|
||||
}
|
||||
// The rotating ring (spokes + pods) — its own container so
|
||||
// update() can spin it.
|
||||
this.ringBody = new Phaser.GameObjects.Container(scene, 0, 0);
|
||||
const rg = scene.add.graphics();
|
||||
const ringR = S * 0.48;
|
||||
rg.lineStyle(S * 0.065, 0x6a5a48, 0.95);
|
||||
rg.strokeCircle(0, 0, ringR);
|
||||
rg.lineStyle(S * 0.03, 0x57493a, 1);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a = (i / 3) * Math.PI * 2;
|
||||
rg.lineBetween(
|
||||
Math.cos(a) * S * 0.12,
|
||||
Math.sin(a) * S * 0.12,
|
||||
Math.cos(a) * ringR,
|
||||
Math.sin(a) * ringR,
|
||||
);
|
||||
rg.fillStyle(0x8a6a48, 1);
|
||||
rg.fillCircle(Math.cos(a) * ringR, Math.sin(a) * ringR, S * 0.037);
|
||||
}
|
||||
this.ringBody.add(rg);
|
||||
this.add(this.ringBody);
|
||||
// The hub (over the ring): a weathered core + rust.
|
||||
const hubR = S * 0.205;
|
||||
g.fillStyle(0x57493a, 1);
|
||||
g.fillCircle(0, 0, hubR);
|
||||
g.lineStyle(2, 0x2c2118, 1);
|
||||
g.strokeCircle(0, 0, hubR);
|
||||
g.fillStyle(0x3a2f26, 1);
|
||||
g.fillCircle(0, 0, hubR * 0.55);
|
||||
g.fillStyle(0x8a6a48, 1);
|
||||
g.fillCircle(0, 0, hubR * 0.22);
|
||||
g.fillStyle(0x6e3a1f, 0.5);
|
||||
g.fillCircle(hubR * 0.5, -hubR * 0.35, 3);
|
||||
g.fillCircle(-hubR * 0.4, hubR * 0.5, 2.5);
|
||||
}
|
||||
// The rotating ring (spokes + pods) — its own container so
|
||||
// update() can spin it.
|
||||
this.ringBody = new Phaser.GameObjects.Container(scene, 0, 0);
|
||||
const rg = scene.add.graphics();
|
||||
const ringR = S * 0.48;
|
||||
rg.lineStyle(S * 0.065, 0x6a5a48, 0.95);
|
||||
rg.strokeCircle(0, 0, ringR);
|
||||
rg.lineStyle(S * 0.03, 0x57493a, 1);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const a = (i / 3) * Math.PI * 2;
|
||||
rg.lineBetween(
|
||||
Math.cos(a) * S * 0.12,
|
||||
Math.sin(a) * S * 0.12,
|
||||
Math.cos(a) * ringR,
|
||||
Math.sin(a) * ringR,
|
||||
);
|
||||
rg.fillStyle(0x8a6a48, 1);
|
||||
rg.fillCircle(Math.cos(a) * ringR, Math.sin(a) * ringR, S * 0.037);
|
||||
}
|
||||
this.ringBody.add(rg);
|
||||
this.add(this.ringBody);
|
||||
// The hub (over the ring): a weathered core + rust.
|
||||
const hubR = S * 0.205;
|
||||
g.fillStyle(0x57493a, 1);
|
||||
g.fillCircle(0, 0, hubR);
|
||||
g.lineStyle(2, 0x2c2118, 1);
|
||||
g.strokeCircle(0, 0, hubR);
|
||||
g.fillStyle(0x3a2f26, 1);
|
||||
g.fillCircle(0, 0, hubR * 0.55);
|
||||
g.fillStyle(0x8a6a48, 1);
|
||||
g.fillCircle(0, 0, hubR * 0.22);
|
||||
g.fillStyle(0x6e3a1f, 0.5);
|
||||
g.fillCircle(hubR * 0.5, -hubR * 0.35, 3);
|
||||
g.fillCircle(-hubR * 0.4, hubR * 0.5, 2.5);
|
||||
|
||||
// The beacon light (both kinds) — the thing that says "occupied".
|
||||
const hy = this.kind === 'waypoint' ? -S * 0.5 : -S * 0.26;
|
||||
const hue = this.kind === 'waypoint' ? 0xa8ffc4 : 0xff7a5c;
|
||||
const glow = this.kind === 'waypoint' ? 0x7dffb0 : 0xff5a3c;
|
||||
this.beaconGlow = scene.add.circle(0, hy, this.kind === 'waypoint' ? S * 0.24 : 7, glow, 0.2);
|
||||
this.beacon = scene.add.circle(0, hy, this.kind === 'waypoint' ? S * 0.07 : 2.6, hue, 1);
|
||||
// The beacon light — the thing that says "occupied".
|
||||
const hy = -S * 0.26;
|
||||
this.beaconGlow = scene.add.circle(0, hy, 7, 0xff5a3c, 0.2);
|
||||
this.beacon = scene.add.circle(0, hy, 2.6, 0xff7a5c, 1);
|
||||
this.add([this.beaconGlow, this.beacon]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export class Galaxy {
|
|||
this.planetFrames = new Map(); // id → [frame, ...] — the frame-diversity pass
|
||||
this.planetClasses = null; // id → [class, ...] — the even-mix deal (or null, balance off)
|
||||
this.homeWorldFrame = null; // the starting system's home world frame
|
||||
this.stationFrames = new Map(); // id → frame — the station-variant spread pass
|
||||
this.stationFrames = new Map(); // id → [frame, ...] — the station-variant spread pass
|
||||
this.name = NameGenerator.galaxy(Rng.derive(seed, 'galaxy', 'name'));
|
||||
}
|
||||
|
||||
|
|
@ -338,12 +338,12 @@ export class Galaxy {
|
|||
this.homeWorldFrame = homeFrame;
|
||||
|
||||
// STATION VARIANT SPREAD (js/galaxy/StationFrames.js): each system's
|
||||
// deep-space station wears one of the spacestations.png variants
|
||||
// (data/stations.json → variants) — the frame avoids what the
|
||||
// NEAREST stars already wear, so the same station type is spread
|
||||
// across the galaxy instead of clustering in one region. Fixed
|
||||
// roster order ⇒ visit-order independent; read back by the content
|
||||
// generator (settlement.stationFrame) — lazy === eager preserved.
|
||||
// deep-space stations (up to two) wear spacestations.png variants
|
||||
// (data/stations.json → variants) — a frame avoids what the NEAREST
|
||||
// stars already wear, so the same station type is spread across the
|
||||
// galaxy instead of clustering in one region. Fixed roster order ⇒
|
||||
// visit-order independent; read back by the content generator
|
||||
// (settlement.stationFrame) — lazy === eager preserved.
|
||||
const { frames: stationFrames } = assignStationFrames({
|
||||
seed: this.seed,
|
||||
records,
|
||||
|
|
@ -351,7 +351,7 @@ export class Galaxy {
|
|||
params: this.params,
|
||||
neighborsOf: (id) => this.neighborsOf(id, pool8),
|
||||
});
|
||||
this.stationFrames = stationFrames; // Map id → frame (station-bearing systems)
|
||||
this.stationFrames = stationFrames; // Map id → [frame, ...] (station-bearing systems)
|
||||
|
||||
// TRADE ECONOMY (js/economy/Economy.js): the galaxy-level trade map —
|
||||
// per-system economic classes + each good's hop-distance to its nearest
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export function assignPlanetClasses({ seed, records, homeId, params, typeDefs =
|
|||
const comp = rollSystemComposition(
|
||||
seed, rec, false, attr.settlements ?? {}, settlementDensity(shim, rec), attr,
|
||||
);
|
||||
const slots = Math.max(0, Number(comp.objects) - Number(comp.deepSpace) - Number(comp.waypoint));
|
||||
const slots = Math.max(0, Number(comp.objects) - Number(comp.stations));
|
||||
if (slots === 0) continue;
|
||||
demand.push({
|
||||
id: rec.id,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@ import { rollSystemComposition, settlementDensity } from './SystemGenerator.js';
|
|||
* 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 →
|
||||
* Only DEEP-SPACE STATIONS take a variant — and a system holds up to TWO
|
||||
* of them (rollSystemComposition rolls a count): each station picks its
|
||||
* own frame, avoiding the frames its NEAREST stars already wear AND, for
|
||||
* the second station, its own system's first frame. 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.
|
||||
*
|
||||
|
|
@ -34,9 +35,9 @@ import { rollSystemComposition, settlementDensity } from './SystemGenerator.js';
|
|||
* (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).
|
||||
* Cost: one pool scan per station 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', {});
|
||||
|
|
@ -49,10 +50,10 @@ export function assignStationFrames({ seed, records, homeId, params, neighborsOf
|
|||
const pool = Array.isArray(raw)
|
||||
? raw.filter((f) => Number.isInteger(f) && f >= 0)
|
||||
: [];
|
||||
const frames = new Map(); // id → frame (only station-bearing systems)
|
||||
const frames = new Map(); // id → [frame, ...] (one per station, draw order)
|
||||
if (pool.length === 0) return { frames };
|
||||
|
||||
const assigned = new Map(); // id → frame (assigned so far)
|
||||
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
|
||||
|
|
@ -63,33 +64,40 @@ export function assignStationFrames({ seed, records, homeId, params, neighborsOf
|
|||
const isHome = rec.id === homeId;
|
||||
const attr = defs[rec.type]?.attributes ?? {};
|
||||
const density = settlementDensity(shim, rec);
|
||||
const { deepSpace } = rollSystemComposition(
|
||||
const { stations } = rollSystemComposition(
|
||||
seed, rec, isHome, attr.settlements ?? {}, density, attr,
|
||||
);
|
||||
if (!deepSpace) continue; // no deep-space station here — nothing to frame
|
||||
if (!stations) 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;
|
||||
};
|
||||
// One frame PER STATION (a system holds up to two): a system never
|
||||
// wears the same face twice (hard — the original "one station per
|
||||
// system" invariant, now per slot), and among the allowed faces each
|
||||
// pick MINIMIZES usage among the NEIGHBORS' stations (the spread
|
||||
// objective — the same lexicographic pick as the planet pass).
|
||||
// Remaining ties broken by a deterministic derived pick (seeded, per
|
||||
// slot).
|
||||
const picked = [];
|
||||
for (let slot = 0; slot < stations; slot++) {
|
||||
const allowed = pool.filter((f) => !picked.includes(f));
|
||||
const cand = allowed.length > 0 ? allowed : pool; // 1-variant pool ⇒ repeat
|
||||
const nbUsage = (f) => {
|
||||
let nb = 0;
|
||||
for (const n of neighbors) {
|
||||
const nfs = assigned.get(n.id);
|
||||
if (nfs) nb += nfs.filter((x) => x === f).length;
|
||||
}
|
||||
return nb;
|
||||
};
|
||||
let best = Infinity;
|
||||
for (const f of cand) best = Math.min(best, nbUsage(f));
|
||||
const tied = cand.filter((f) => nbUsage(f) === best);
|
||||
picked.push(tied.length === 1 ? tied[0] : Rng.derive(seed, 'station-frames', rec.id, slot).pick(tied));
|
||||
}
|
||||
|
||||
// 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);
|
||||
frames.set(rec.id, picked);
|
||||
assigned.set(rec.id, picked);
|
||||
}
|
||||
|
||||
return { frames };
|
||||
|
|
|
|||
|
|
@ -233,9 +233,9 @@ export function systemChartSnapshot(systemId, content, o = {}) {
|
|||
kind: 'station',
|
||||
x: s.x,
|
||||
y: s.y,
|
||||
radius: config.get(`stations.kinds.${kind}.size`, kind === 'waypoint' ? 46 : 108),
|
||||
radius: config.get(`stations.kinds.${kind}.size`, 108),
|
||||
name: s.name,
|
||||
typeLabel: kind === 'waypoint' ? 'Waypoint' : 'Station',
|
||||
typeLabel: 'Station',
|
||||
discovered: isDisc(s.id),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,12 +119,11 @@ const DEG = Math.PI / 180;
|
|||
* `content.homeFrame` here; the renderer prefers those over a random pick.
|
||||
*
|
||||
* STATION VARIANTS (js/galaxy/StationFrames.js): the same spreading for
|
||||
* the deep-space stations — each system's free-space station wears one of
|
||||
* the spacestations.png variants (data/stations.json → variants), chosen
|
||||
* to avoid what the NEAREST stars already wear. The pass stamps
|
||||
* `settlement.stationFrame` here (the landing handoff reads the same
|
||||
* frame — data/landing.json → stationVideos); waypoints keep their beacon
|
||||
* look and take no frame.
|
||||
* the deep-space stations — each system's free-space stations (up to two)
|
||||
* wear spacestations.png variants (data/stations.json → variants), chosen
|
||||
* to avoid what the NEAREST stars already wear (and each other). The pass
|
||||
* stamps `settlement.stationFrame` here (the landing handoff reads the same
|
||||
* frame — data/landing.json → stationVideos).
|
||||
*
|
||||
* Place identity (the reputation/trading/faction keys): every planet and
|
||||
* settlement carries a stable `id`, seed-deterministic because it is
|
||||
|
|
@ -266,7 +265,7 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
Rng.derive(galaxy.seed, 'system', record.id, 'names', 'stations')
|
||||
),
|
||||
density: settlementDensity(galaxy, record),
|
||||
stations: { deepSpace: composition.deepSpace, waypoint: composition.waypoint },
|
||||
stations: composition.stations,
|
||||
isHome,
|
||||
// FACTION ownership (data/factions.json — the roster's faction pass):
|
||||
// the system's territory (null = neutral wild space) claims its
|
||||
|
|
@ -276,14 +275,16 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
});
|
||||
|
||||
// STATION VARIANTS (js/galaxy/StationFrames.js — the galaxy-wide
|
||||
// spread pass): stamp the assigned spacestations.png frame on this
|
||||
// system's deep-space station, so the same station type is spread
|
||||
// across the galaxy (the renderer prefers settlement.stationFrame over
|
||||
// a fallback pick; the landing handoff reads the same frame).
|
||||
const stationFrame = galaxy?.stationFrames?.get(record.id);
|
||||
if (typeof stationFrame === 'number') {
|
||||
// spread pass): stamp the assigned spacestations.png frames on this
|
||||
// system's deep-space stations (up to two, in draw order), so the same
|
||||
// station type is spread across the galaxy (the renderer prefers
|
||||
// settlement.stationFrame over a fallback pick; the landing handoff
|
||||
// reads the same frame).
|
||||
const stationFrames = galaxy?.stationFrames?.get(record.id);
|
||||
if (Array.isArray(stationFrames)) {
|
||||
let i = 0;
|
||||
for (const s of settlements) {
|
||||
if (s.kind === 'deepSpaceStation') s.stationFrame = stationFrame;
|
||||
if (s.kind === 'deepSpaceStation' && i < stationFrames.length) s.stationFrame = stationFrames[i++];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -437,7 +438,7 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
|||
* consecutive gap : neighborMin..(level-1 − edgeMargin) (2048..4608 px)
|
||||
*
|
||||
* What counts as an object: planets (all of them) and free-space
|
||||
* settlements (anchor.type 'space' — deep-space stations, waypoints).
|
||||
* settlements (anchor.type 'space' — deep-space stations).
|
||||
* Planet-bound settlements are features OF their planet (a colony sits on
|
||||
* it) and so are not layout objects of their own. The central body is the
|
||||
* origin (the game renders the home world there in the starting system;
|
||||
|
|
@ -1321,15 +1322,16 @@ function mixTint(hex, strength) {
|
|||
* data/galaxy.json → settlements.gradient), in one deterministic roll:
|
||||
*
|
||||
* home system → fixed: 2 planets (gas giant + rocky) beside the home
|
||||
* world, + at most one free-space station (deepSpace only —
|
||||
* by design: the home system stays a small start the player
|
||||
* expands out of; the arc layout could seat more)
|
||||
* world, + at most one free-space station (by design: the
|
||||
* home system stays a small start the player expands out
|
||||
* of; the arc layout could seat more)
|
||||
* every other → 1) the FINAL object count N from the configured table
|
||||
* (0 = barren, then 2/3/4/5 at the configured weights — the
|
||||
* distribution is on the FINAL count, by design);
|
||||
* 2) the free-space stations (0–2, per-type odds × the
|
||||
* density gradient; at most 2, which never exceeds the
|
||||
* minimum non-barren budget of 2);
|
||||
* 2) the free-space deep-space stations (0–2 — the first at
|
||||
* the type's chance, the second at `second`; per-type
|
||||
* odds × the density gradient; at most 2, which never
|
||||
* exceeds the minimum non-barren budget of 2);
|
||||
* 3) the planets — N − stations classes, from the STAMPED
|
||||
* GALAXY-DECK DEAL when `stampedClasses` is given
|
||||
* (data/systems.json → planetClassBalance — the even
|
||||
|
|
@ -1342,13 +1344,15 @@ function mixTint(hex, strength) {
|
|||
* their inputs, so they always agree. The stamped classes are themselves
|
||||
* a pure function of (seed, roster, config) (stamped once at galaxy
|
||||
* build), so lazy (on-arrival) === eager (generateAll) holds for them.
|
||||
* → { objects, deepSpace, waypoint, classes }
|
||||
* → { objects, stations, classes } (stations = the free-space deep-space
|
||||
* station count, 0–2)
|
||||
*/
|
||||
export function rollSystemComposition(seed, record, isHome, spec, density, attr, stampedClasses = null) {
|
||||
if (isHome) {
|
||||
const rng = Rng.derive(seed, 'system', record.id, 'settlements');
|
||||
const deepSpace = rng.chance((spec?.deepSpaceStation?.chance ?? 0.12) * density);
|
||||
return { objects: 2, deepSpace, waypoint: false, classes: ['gas', 'rocky'] };
|
||||
// At most one free-space station — the home band can't hold 5 points.
|
||||
const stations = rng.chance((spec?.deepSpaceStation?.chance ?? 0.12) * density) ? 1 : 0;
|
||||
return { objects: 2, stations, classes: ['gas', 'rocky'] };
|
||||
}
|
||||
// 1) The final object count — the configured composition table.
|
||||
const oc = config.get('systems.objectCount', {
|
||||
|
|
@ -1365,27 +1369,23 @@ export function rollSystemComposition(seed, record, isHome, spec, density, attr,
|
|||
|
||||
// BARREN (N = 0): a jump-gate-only system — no stations, no planets.
|
||||
if (objects === 0) {
|
||||
return { objects, deepSpace: false, waypoint: false, classes: [] };
|
||||
return { objects, stations: 0, classes: [] };
|
||||
}
|
||||
|
||||
// 2) The free-space stations (≤ 2 — never more than the min budget of 2).
|
||||
// 2) The free-space deep-space stations (≤ 2 — never more than the min
|
||||
// budget of 2): the first at the type's chance, the second at its own
|
||||
// (the old waypoint slot — now a station too).
|
||||
const rngS = Rng.derive(seed, 'system', record.id, 'settlements');
|
||||
const deepSpace = rngS.chance((spec?.deepSpaceStation?.chance ?? 0.12) * density);
|
||||
const waypoint = rngS.chance((spec?.waypoint?.chance ?? 0.2) * density);
|
||||
let stations = 0;
|
||||
if (rngS.chance((spec?.deepSpaceStation?.chance ?? 0.12) * density)) stations++;
|
||||
if (rngS.chance((spec?.deepSpaceStation?.second ?? 0.2) * density)) stations++;
|
||||
|
||||
// 3) The planets fill the rest of the budget (N − stations ≥ 0).
|
||||
// Every non-barren system keeps AT LEAST ONE PLANET: a gate anchors
|
||||
// its tether to a world (layoutGates), and the player must be able to
|
||||
// reach a planet and build out from it — a station-only budget
|
||||
// (N = 2 stations) demotes one station to a planet.
|
||||
let stations = Number(deepSpace) + Number(waypoint);
|
||||
let wp = waypoint;
|
||||
let ds = deepSpace;
|
||||
while (stations >= objects && stations > 0) {
|
||||
if (wp) wp = false;
|
||||
else ds = false;
|
||||
stations = Number(ds) + Number(wp);
|
||||
}
|
||||
stations = Math.min(stations, objects - 1);
|
||||
// The classes: the GALAXY-DECK DEAL when stamped (the even galaxy-wide
|
||||
// mix — PlanetClasses.js), else the per-type weighted draws (standalone
|
||||
// callers: dev tools, tests without a galaxy object). A stamped list of
|
||||
|
|
@ -1397,7 +1397,7 @@ export function rollSystemComposition(seed, record, isHome, spec, density, attr,
|
|||
Array.isArray(stampedClasses) && stampedClasses.length === count
|
||||
? stampedClasses.slice()
|
||||
: Array.from({ length: count }, () => rngP.weighted(classWeights, 'rocky'));
|
||||
return { objects, deepSpace: ds, waypoint: wp, classes };
|
||||
return { objects, stations, classes };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1411,7 +1411,7 @@ export function rollPlanetClasses(seed, record, isHome, attr, spec, density) {
|
|||
/** Backwards-compatible roll (tests/tools) — the stations of a system. */
|
||||
export function rollStationCount(seed, record, isHome, spec, density) {
|
||||
const c = rollSystemComposition(seed, record, isHome, spec, density, {});
|
||||
return { deepSpace: c.deepSpace, waypoint: c.waypoint, count: Number(c.deepSpace) + Number(c.waypoint) };
|
||||
return { count: c.stations };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1432,8 +1432,8 @@ export function settlementDensity(galaxy, record) {
|
|||
|
||||
/**
|
||||
* Draw settlements for one system. Stable draw order: planet-bound
|
||||
* settlements in orbital order, then free-floating (deep-space station,
|
||||
* waypoint). Every roll goes through the system's own stream. Each
|
||||
* settlements in orbital order, then free-floating (the deep-space
|
||||
* stations). Every roll goes through the system's own stream. Each
|
||||
* settlement gets a stable `id` (`<systemId>-s<n>`, n = its position in
|
||||
* this order) — the reputation/factions key. `owner` is the FACTION
|
||||
* SEAM (data/factions.json): the system's territory (systemFaction, from
|
||||
|
|
@ -1492,13 +1492,12 @@ function generateSettlements({ rng, systemId, kindDefs, spec, planets, stationDe
|
|||
}
|
||||
}
|
||||
|
||||
// Free-floating, out in the dark — the PRE-ROLLED flags (rollStationCount:
|
||||
// per-type odds × the home→far density gradient; the home system never
|
||||
// rolls a waypoint — its band can't hold 5 points). A BARREN system
|
||||
// Free-floating, out in the dark — the PRE-ROLLED count (per-type odds
|
||||
// × the home→far density gradient; the home system never rolls a second
|
||||
// station — its band can't hold 5 points). A BARREN system
|
||||
// (objectCount → 0) rolled no stations — it is a jump-gate-only stop,
|
||||
// deliberately.
|
||||
if (stations?.deepSpace) make('deepSpaceStation', { type: 'space' }, systemFaction);
|
||||
if (stations?.waypoint) make('waypoint', { type: 'space' }, systemFaction);
|
||||
for (let i = 0; i < (stations ?? 0); i++) make('deepSpaceStation', { type: 'space' }, systemFaction);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
|
@ -1528,6 +1527,6 @@ function needs(planet, needsList, defaults) {
|
|||
function logPopulation(rng, pop) {
|
||||
const min = pop?.min ?? 1;
|
||||
const max = Math.max(min, pop?.max ?? min);
|
||||
if (min <= 0 && rng.chance(0.5)) return 0; // e.g. unmanned waypoints
|
||||
if (min <= 0 && rng.chance(0.5)) return 0; // some kinds roll to zero
|
||||
return Math.round(Math.exp(rng.range(Math.log(Math.max(1, min)), Math.log(max))));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,14 +210,14 @@ export function pickPreviewSystem(galaxy, spec, systemType = null) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// STATIONS — at most one deep-space station per system.
|
||||
if (!comp.deepSpace) continue;
|
||||
const frame = stationFrames?.get(rec.id) ?? null;
|
||||
// STATIONS — up to two deep-space stations per system.
|
||||
if (!comp.stations) continue;
|
||||
const frames = stationFrames?.get(rec.id) ?? null;
|
||||
if (spec.frame == null) {
|
||||
consider(rec, { station: true }, true);
|
||||
continue;
|
||||
}
|
||||
if (frame === spec.frame) consider(rec, { station: true }, true);
|
||||
if (Array.isArray(frames) && frames.includes(spec.frame)) consider(rec, { station: true }, true);
|
||||
else consider(rec, { station: true }, false); // reskin the variant
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ export function buildSystemTree(o = {}) {
|
|||
* central body ('home') — the starting system only, where it is the home
|
||||
* world — then the planets (by name), the free-space stations (settlement
|
||||
* id), and the jump gates (gate id). Asteroid clusters are NOT NAV points
|
||||
* — they're objects you can mine, not chart waypoints. A system without a
|
||||
* — they're objects you can mine, not chart nav points. A system without a
|
||||
* central body (every non-home system — the star is invisible flavor)
|
||||
* simply charts its planets, stations, and gates.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -164,8 +164,8 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
// The space-station spritesheet (data/stations.json → texture): the
|
||||
// deep-space station variants (variants 0..2 today — the first three
|
||||
// 256×256 frames); waypoints keep their procedural beacon. Station
|
||||
// falls back to its procedural hardware if the sheet is missing.
|
||||
// 256×256 frames). Station falls back to its procedural hardware if
|
||||
// the sheet is missing.
|
||||
const stationTexture = config.get('stations.texture', '');
|
||||
if (stationTexture && config.get('stations.enabled', true) !== false) {
|
||||
this.load.spritesheet(
|
||||
|
|
@ -618,7 +618,7 @@ export class GameScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
// Free-space stations (settlements with `anchor.type === 'space'`
|
||||
// — deep-space stations and waypoints): settlements that float in
|
||||
// — deep-space stations): settlements that float in
|
||||
// the void, rendered as world objects (js/entities/Station.js).
|
||||
// Solid (the ship keeps its clearance), discoverable (compass +
|
||||
// toast), and comms targets — a click opens the comms panel there
|
||||
|
|
@ -1735,7 +1735,7 @@ export class GameScene extends Phaser.Scene {
|
|||
const t = this.typeTarget;
|
||||
if (!t) return null;
|
||||
if (t.station) {
|
||||
// At most one deep-space station per system — match the entity
|
||||
// Up to two deep-space stations per system — match the entity
|
||||
// to the settlement record by id (name-safe).
|
||||
const rec = (this.systemContent?.settlements ?? []).find((s) => s?.kind === 'deepSpaceStation');
|
||||
if (!rec) return null;
|
||||
|
|
@ -2802,7 +2802,7 @@ export class GameScene extends Phaser.Scene {
|
|||
* (both wear a spritesheet frame — the planet's class
|
||||
* frame / the station's variant frame — and land with
|
||||
* the matching clips: data/landing.json → videos /
|
||||
* stationVideos). Waypoints are comms, not a port.
|
||||
* stationVideos).
|
||||
*/
|
||||
commsTargetFor(obj) {
|
||||
let key = null;
|
||||
|
|
@ -2823,8 +2823,7 @@ export class GameScene extends Phaser.Scene {
|
|||
// Deep-space stations wear a spacestations.png variant (the
|
||||
// galaxy-wide spread pass — js/galaxy/StationFrames.js) and label
|
||||
// themselves by it (data/stations.json → typeLabels — the fallback
|
||||
// is the kind's label). Waypoints are a nav beacon: comms, not a
|
||||
// port, and no sheet frame.
|
||||
// is the kind's label).
|
||||
isStation = obj.kind === 'deepSpaceStation';
|
||||
const variantLabel = typeof obj.sheetFrame === 'number'
|
||||
? config.get(`stations.typeLabels.${obj.sheetFrame}`)
|
||||
|
|
@ -2861,8 +2860,7 @@ export class GameScene extends Phaser.Scene {
|
|||
reputation: rep,
|
||||
// Landing = the surface sequence: a PLANTED world (population > 0)
|
||||
// or a deep-space station (always settled) with enough goodwill.
|
||||
// Waypoints are not a port (isStation stays false), and the home
|
||||
// world's button is hidden as a UI fact (the comms panel).
|
||||
// The home world's button is hidden as a UI fact (the comms panel).
|
||||
canLand: settled && rep > -4 && landingOn && (isPlanet || isStation),
|
||||
kindLabel,
|
||||
isPlanet,
|
||||
|
|
@ -2937,8 +2935,8 @@ export class GameScene extends Phaser.Scene {
|
|||
this.startLanding(target);
|
||||
return;
|
||||
}
|
||||
// 'request-landing' on a waypoint — a nav beacon is not a port; the
|
||||
// click is acknowledged on the console for now.
|
||||
// A landing press on a non-port target (defensive — the panel only
|
||||
// offers it on planets + stations) — acknowledged on the console.
|
||||
console.info(`[orbit] comms: ${id} — ${target.name} (key: ${target.key ?? 'none'})`);
|
||||
}
|
||||
|
||||
|
|
@ -4427,7 +4425,7 @@ export class GameScene extends Phaser.Scene {
|
|||
y: st.y,
|
||||
radius: st.bound ?? st.size,
|
||||
name: st.discoveryName,
|
||||
typeLabel: st.kind === 'waypoint' ? 'Waypoint' : 'Station',
|
||||
typeLabel: 'Station',
|
||||
discovered: isDisc(st.discoveryId),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.planetType = String(data.type ?? ''); // e.g. "Rocky World" / "Smuggler Outpost"
|
||||
this.tetherLevel = Math.max(0, Math.round(Number(data.tetherLevel ?? 0)));
|
||||
// A deep-space STATION surface? (GameScene.startLanding hands over
|
||||
// `station: true` for a deepSpaceStation — waypoints never land.)
|
||||
// `station: true` for a deepSpaceStation — the only free-space kind
|
||||
// that lands.)
|
||||
// Same deck, same flow; the frame indexes a different clip set
|
||||
// (data/landing.json → stationVideos) and a different music key
|
||||
// (data/music.json → stationFrames). Surface/shop clips are authored
|
||||
|
|
@ -887,7 +888,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
const p = match(g?.systemPlanets) ?? (g?.planet?.discoveryName === name ? g.planet : null);
|
||||
if (p && p.class) return Economy.economicClass(p);
|
||||
const st = match(g?.systemStations);
|
||||
if (st) return 'station'; // both deepSpaceStation + waypoint shops are the 'station' class
|
||||
if (st) return 'station'; // free-space stations are the 'station' class
|
||||
const sysId = g?.systemRecord?.id;
|
||||
const raw = g?.galaxy?.economy?.classBySystem?.get?.(sysId);
|
||||
const classes = raw ? [...raw] : [];
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ function wrapPI(a) {
|
|||
*/
|
||||
export function isFarTarget(t, ship, farDistance) {
|
||||
if (!t || !ship || farDistance <= 0) return false;
|
||||
if (t.alwaysFull === true) return false; // always prominent (route waypoint)
|
||||
if (t.alwaysFull === true) return false; // always prominent (route next stop)
|
||||
return Math.hypot(t.x - ship.x, t.y - ship.y) > farDistance;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue