Add a lived-in layer: settlements in generated systems
The galaxy was settled long before the player arrives. Systems now
host colonies on habitable worlds, mining stations over resource
worlds, cloud bases riding gas giants, stations adrift in open
space, and beacons — or report "charted · unclaimed" when empty.
- data/settlements.json: the shared kind vocabulary (label, theme,
population range, anchor type); add a kind = JSON + naming pool.
- Per-archetype rates in data/systems.json (chance + required
planet class) and a core→rim density gradient in data/galaxy.json
(falloff/floor), keyed off each record's new rNorm.
- Draws stay in a stable order on the system's own Rng stream, so
lazy and eager generation still produce identical content.
- Every settlement keeps owner: null, a reserved seam for the
factions/pirates that will claim them later.
- NameGenerator.station() names from anchor + kind ("Kest Skyspire").
- SystemReport.js is a pure, testable formatter (dossier lines,
"charted · unclaimed" for empty); GameScene's HUD renders it and
Node tests cover structure, mix, gradient, naming, lazy === eager.
This commit is contained in:
parent
eafc3c4314
commit
9d40103cce
19
README.md
19
README.md
|
|
@ -28,10 +28,18 @@ python3 -m http.server 8080
|
||||||
(`data/systems.json`) with per-type distribution weights and radial
|
(`data/systems.json`) with per-type distribution weights and radial
|
||||||
bands — the first "how does the galaxy lay itself out" rules.
|
bands — the first "how does the galaxy lay itself out" rules.
|
||||||
- **Two-level generation**: the whole galaxy roster is generated at New
|
- **Two-level generation**: the whole galaxy roster is generated at New
|
||||||
Game (~70 ms); each system's planets/moons/belts are generated lazily on
|
Game (~70 ms); each system's planets/moons/belts/settlements are
|
||||||
arrival, deterministically (seed + system id), so lazy and eager give
|
generated lazily on arrival, deterministically (seed + system id), so
|
||||||
identical results. The current system's name/identity shows top-left in
|
lazy and eager give identical results.
|
||||||
the game scene.
|
- **A lived-in galaxy**: the galaxy was settled long before you arrive.
|
||||||
|
Systems host colonies on habitable worlds, mining stations over resource
|
||||||
|
worlds, cloud bases riding gas giants, stations adrift in open space, and
|
||||||
|
beacons — or report *charted · unclaimed* when nobody's there. Rates are
|
||||||
|
per-archetype (`data/systems.json`) and thin out from the settled core to
|
||||||
|
the wilder rim (`data/galaxy.json`). Each settlement has a name,
|
||||||
|
population, and an `owner` seam reserved for the factions/pirates to come.
|
||||||
|
The current system's dossier (name, identity, what's there) shows
|
||||||
|
top-left in the game scene.
|
||||||
- Game screen with a basic top-down ship: **click anywhere to fly there**
|
- Game screen with a basic top-down ship: **click anywhere to fly there**
|
||||||
in infinite, unbounded space (system boundaries/jumps come next)
|
in infinite, unbounded space (system boundaries/jumps come next)
|
||||||
- Camera gently trails the ship; the **parallax starfield** streams past
|
- Camera gently trails the ship; the **parallax starfield** streams past
|
||||||
|
|
@ -51,6 +59,7 @@ orbit/
|
||||||
│ ├── ship.json # ship feel: thrust, drag, maxSpeed, …
|
│ ├── ship.json # ship feel: thrust, drag, maxSpeed, …
|
||||||
│ ├── galaxy.json # galaxy scale & shape (count, radius, spiral…)
|
│ ├── galaxy.json # galaxy scale & shape (count, radius, spiral…)
|
||||||
│ ├── systems.json # system archetypes: theme, attributes, distribution
|
│ ├── systems.json # system archetypes: theme, attributes, distribution
|
||||||
|
│ ├── settlements.json # the lived-in layer: settlement kinds & populations
|
||||||
│ └── naming.json # syllable pools for names
|
│ └── naming.json # syllable pools for names
|
||||||
├── lib/ # vendored third-party libs (Phaser 4.2.1)
|
├── lib/ # vendored third-party libs (Phaser 4.2.1)
|
||||||
├── js/
|
├── js/
|
||||||
|
|
@ -58,7 +67,7 @@ orbit/
|
||||||
│ ├── config/ # Config singleton, ConfigLoader, game config
|
│ ├── config/ # Config singleton, ConfigLoader, game config
|
||||||
│ ├── scenes/ # MenuScene, GameScene (thin, orchestration)
|
│ ├── scenes/ # MenuScene, GameScene (thin, orchestration)
|
||||||
│ ├── entities/ # Ship (own behavior)
|
│ ├── entities/ # Ship (own behavior)
|
||||||
│ ├── galaxy/ # Galaxy (seeded world model), SystemGenerator
|
│ ├── galaxy/ # Galaxy (seeded world model), SystemGenerator, SystemReport
|
||||||
│ ├── ui/ # MenuButton (reusable)
|
│ ├── ui/ # MenuButton (reusable)
|
||||||
│ ├── visuals/ # Starfield (decorative)
|
│ ├── visuals/ # Starfield (decorative)
|
||||||
│ ├── utils/ # small pure helpers (Color, Rng, NameGenerator)
|
│ ├── utils/ # small pure helpers (Color, Rng, NameGenerator)
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,8 @@
|
||||||
"rules": []
|
"rules": []
|
||||||
},
|
},
|
||||||
"startingSystem": { "policy": "center" },
|
"startingSystem": { "policy": "center" },
|
||||||
|
"settlements": {
|
||||||
|
"gradient": { "falloff": 0.7, "floor": 0.22 }
|
||||||
|
},
|
||||||
"neighbors": 8
|
"neighbors": 8
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,14 @@
|
||||||
"syllables": ["an", "dro", "me", "mil", "ky", "cen", "tau", "rhi", "sa", "vel", "pyr", "os", "eth", "ni"],
|
"syllables": ["an", "dro", "me", "mil", "ky", "cen", "tau", "rhi", "sa", "vel", "pyr", "os", "eth", "ni"],
|
||||||
"minParts": 3,
|
"minParts": 3,
|
||||||
"maxParts": 5
|
"maxParts": 5
|
||||||
|
},
|
||||||
|
"station": {
|
||||||
|
"nouns": {
|
||||||
|
"colony": ["Reach", "Haven", "Landing", "Hold", "City", "Gate"],
|
||||||
|
"miningStation": ["Rig", "Dredge", "Claim", "Works", "Yard"],
|
||||||
|
"cloudBase": ["Skyspire", "Drift", "Aerie", "Balcony", "Cloudspire"],
|
||||||
|
"deepSpaceStation": ["Relay", "Gate", "Beacon", "Haven", "Anchor"],
|
||||||
|
"waypoint": ["Waypoint", "Beacon", "Marker", "Light"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"_comment": "The galaxy is already LIVED IN: systems host settlements — colonies on habitable worlds, mining stations over resource worlds, cloud bases riding gas giants, stations adrift in open space. kinds = the shared vocabulary (label, theme, population range, anchor type). HOW OFTEN each kind appears, and what world it needs, is a per-type attribute in data/systems.json (types.<id>.attributes.settlements). Core→rim density gradient lives in data/galaxy.json (settlements.gradient).",
|
||||||
|
"kinds": {
|
||||||
|
"colony": {
|
||||||
|
"label": "Colony",
|
||||||
|
"description": "A settled habitable world — cities, farms, life.",
|
||||||
|
"theme": { "color": "#7ce8a4" },
|
||||||
|
"population": { "min": 40000, "max": 9000000 },
|
||||||
|
"anchor": "planet"
|
||||||
|
},
|
||||||
|
"miningStation": {
|
||||||
|
"label": "Mining Station",
|
||||||
|
"description": "Orbital rigs and habitats striping minerals from a resource world.",
|
||||||
|
"theme": { "color": "#e8b45f" },
|
||||||
|
"population": { "min": 80, "max": 4000 },
|
||||||
|
"anchor": "planet"
|
||||||
|
},
|
||||||
|
"cloudBase": {
|
||||||
|
"label": "Cloud Base",
|
||||||
|
"description": "A mining city riding the upper clouds of a gas giant.",
|
||||||
|
"theme": { "color": "#6fc3e8" },
|
||||||
|
"population": { "min": 500, "max": 120000 },
|
||||||
|
"anchor": "planet"
|
||||||
|
},
|
||||||
|
"deepSpaceStation": {
|
||||||
|
"label": "Deep-Space Station",
|
||||||
|
"description": "A free-floating station adrift in open system space — refinery, gate, or trading post.",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,14 @@
|
||||||
"moonChance": 0.3,
|
"moonChance": 0.3,
|
||||||
"beltChance": 0.35,
|
"beltChance": 0.35,
|
||||||
"habitability": 0.1,
|
"habitability": 0.1,
|
||||||
"hazard": 0.1
|
"hazard": 0.1,
|
||||||
|
"settlements": {
|
||||||
|
"colony": { "chance": 0.45 },
|
||||||
|
"miningStation": { "chance": 0.3, "needs": ["rocky", "lava", "ice"] },
|
||||||
|
"cloudBase": { "chance": 0.35, "needs": ["gas"] },
|
||||||
|
"deepSpaceStation": { "chance": 0.25 },
|
||||||
|
"waypoint": { "chance": 0.35 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redDwarf": {
|
"redDwarf": {
|
||||||
|
|
@ -36,7 +43,14 @@
|
||||||
"moonChance": 0.45,
|
"moonChance": 0.45,
|
||||||
"beltChance": 0.3,
|
"beltChance": 0.3,
|
||||||
"habitability": 0.22,
|
"habitability": 0.22,
|
||||||
"hazard": 0.12
|
"hazard": 0.12,
|
||||||
|
"settlements": {
|
||||||
|
"colony": { "chance": 0.6 },
|
||||||
|
"miningStation": { "chance": 0.35, "needs": ["rocky", "ice"] },
|
||||||
|
"cloudBase": { "chance": 0.1, "needs": ["gas"] },
|
||||||
|
"deepSpaceStation": { "chance": 0.2 },
|
||||||
|
"waypoint": { "chance": 0.3 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binary": {
|
"binary": {
|
||||||
|
|
@ -55,7 +69,14 @@
|
||||||
"moonChance": 0.5,
|
"moonChance": 0.5,
|
||||||
"beltChance": 0.6,
|
"beltChance": 0.6,
|
||||||
"habitability": 0.06,
|
"habitability": 0.06,
|
||||||
"hazard": 0.18
|
"hazard": 0.18,
|
||||||
|
"settlements": {
|
||||||
|
"colony": { "chance": 0.1 },
|
||||||
|
"miningStation": { "chance": 0.3, "needs": ["rocky", "gas"] },
|
||||||
|
"cloudBase": { "chance": 0.5, "needs": ["gas"] },
|
||||||
|
"deepSpaceStation": { "chance": 0.3 },
|
||||||
|
"waypoint": { "chance": 0.3 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"habitable": {
|
"habitable": {
|
||||||
|
|
@ -74,7 +95,14 @@
|
||||||
"moonChance": 0.35,
|
"moonChance": 0.35,
|
||||||
"beltChance": 0.25,
|
"beltChance": 0.25,
|
||||||
"habitability": 0.5,
|
"habitability": 0.5,
|
||||||
"hazard": 0.05
|
"hazard": 0.05,
|
||||||
|
"settlements": {
|
||||||
|
"colony": { "chance": 0.85 },
|
||||||
|
"miningStation": { "chance": 0.2, "needs": ["rocky", "ice"] },
|
||||||
|
"cloudBase": { "chance": 0.2, "needs": ["gas"] },
|
||||||
|
"deepSpaceStation": { "chance": 0.25 },
|
||||||
|
"waypoint": { "chance": 0.4 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nebula": {
|
"nebula": {
|
||||||
|
|
@ -93,7 +121,14 @@
|
||||||
"moonChance": 0.2,
|
"moonChance": 0.2,
|
||||||
"beltChance": 0.8,
|
"beltChance": 0.8,
|
||||||
"habitability": 0.02,
|
"habitability": 0.02,
|
||||||
"hazard": 0.35
|
"hazard": 0.35,
|
||||||
|
"settlements": {
|
||||||
|
"colony": { "chance": 0.05 },
|
||||||
|
"miningStation": { "chance": 0.5, "needs": ["lava", "rocky"] },
|
||||||
|
"cloudBase": { "chance": 0.3, "needs": ["gas"] },
|
||||||
|
"deepSpaceStation": { "chance": 0.2 },
|
||||||
|
"waypoint": { "chance": 0.45 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"void": {
|
"void": {
|
||||||
|
|
@ -112,7 +147,14 @@
|
||||||
"moonChance": 0.1,
|
"moonChance": 0.1,
|
||||||
"beltChance": 0.2,
|
"beltChance": 0.2,
|
||||||
"habitability": 0.02,
|
"habitability": 0.02,
|
||||||
"hazard": 0.5
|
"hazard": 0.5,
|
||||||
|
"settlements": {
|
||||||
|
"colony": { "chance": 0.02 },
|
||||||
|
"miningStation": { "chance": 0.3, "needs": ["ice"] },
|
||||||
|
"cloudBase": { "chance": 0.05, "needs": ["gas"] },
|
||||||
|
"deepSpaceStation": { "chance": 0.15 },
|
||||||
|
"waypoint": { "chance": 0.25 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,11 +229,91 @@ let big;
|
||||||
const g = Galaxy.create('content-shape');
|
const g = Galaxy.create('content-shape');
|
||||||
const sys = g.currentSystem();
|
const sys = g.currentSystem();
|
||||||
const c = g.ensureContent(sys.id);
|
const c = g.ensureContent(sys.id);
|
||||||
check('content has star/planets/belt/hazard', !!c.star && Array.isArray(c.planets) && !!c.belt && typeof c.hazard === 'boolean');
|
check('content has star/planets/settlements/belt/hazard', !!c.star && Array.isArray(c.planets) && Array.isArray(c.settlements) && !!c.belt && typeof c.hazard === 'boolean');
|
||||||
check('planets have name/ordinal/class/moons/habitable', c.planets.every((p) => p.name && p.ordinal >= 1 && p.class && Number.isInteger(p.moons) && typeof p.habitable === 'boolean'));
|
check('planets have name/ordinal/class/moons/habitable', c.planets.every((p) => p.name && p.ordinal >= 1 && p.class && Number.isInteger(p.moons) && typeof p.habitable === 'boolean'));
|
||||||
check('type themes are defined (UI hook)', typeIds.every((id) => typeof types[id].theme?.color === 'string'));
|
check('type themes are defined (UI hook)', typeIds.every((id) => typeof types[id].theme?.color === 'string'));
|
||||||
check('galaxy name is deterministic per seed', Galaxy.create('content-shape').name === g.name);
|
check('galaxy name is deterministic per seed', Galaxy.create('content-shape').name === g.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// 5. The lived-in layer: settlements, gradient, report
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
const kinds = config.get('settlements.kinds', {});
|
||||||
|
check('settlement kinds defined with theme + population range', Object.keys(kinds).length >= 4 && Object.values(kinds).every((k) => typeof k.theme?.color === 'string' && k.population?.min >= 0 && k.population?.max >= k.population?.min));
|
||||||
|
|
||||||
|
// Structure across a big sample of the full galaxy.
|
||||||
|
const big2 = Galaxy.create(SEED);
|
||||||
|
const sample = big2.records.slice(0, 3000);
|
||||||
|
let structOk = true;
|
||||||
|
for (const r of sample) {
|
||||||
|
const c = big2.ensureContent(r.id);
|
||||||
|
for (const s of c.settlements) {
|
||||||
|
if (!kinds[s.kind]) { structOk = false; break; }
|
||||||
|
if (s.anchor.type === 'planet') {
|
||||||
|
const planet = c.planets.find((p) => p.ordinal === s.anchor.ordinal);
|
||||||
|
if (!planet) structOk = false;
|
||||||
|
} else if (s.anchor.type !== 'space') structOk = false;
|
||||||
|
const pop = kinds[s.kind].population;
|
||||||
|
if (s.population < pop.min || s.population > pop.max) structOk = false;
|
||||||
|
if (s.owner !== null) structOk = false; // reserved seam, unused for now
|
||||||
|
if (!s.name) structOk = false;
|
||||||
|
}
|
||||||
|
if (!structOk) break;
|
||||||
|
}
|
||||||
|
check('settlement structure: known kinds, valid anchors, population in range, owner=null (faction seam)', structOk);
|
||||||
|
|
||||||
|
// The lived-in mix actually shows up across the sample.
|
||||||
|
const seen = new Set();
|
||||||
|
let anchorRulesOk = true;
|
||||||
|
for (const r of sample) {
|
||||||
|
const c = big2.ensureContent(r.id);
|
||||||
|
for (const s of c.settlements) {
|
||||||
|
seen.add(s.kind);
|
||||||
|
const p = c.planets.find((pl) => pl.ordinal === s.anchor.ordinal);
|
||||||
|
if (s.kind === 'colony' && !p?.habitable) anchorRulesOk = false;
|
||||||
|
if (s.kind === 'cloudBase' && p?.class !== 'gas') 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('colonies sit on habitable worlds; cloud bases ride gas giants', anchorRulesOk);
|
||||||
|
|
||||||
|
// Not everything is inhabited — some systems stay unclaimed.
|
||||||
|
let unclaimed = 0;
|
||||||
|
for (const r of sample) if ((big2.ensureContent(r.id).settlements).length === 0) unclaimed++;
|
||||||
|
check(`some systems are charted-but-unclaimed (${unclaimed}/${sample.length} in sample)`, unclaimed > 0);
|
||||||
|
|
||||||
|
// Core→rim gradient: the settled heart is denser than the wilder rim.
|
||||||
|
const withCount = sample.map((r) => ({ rNorm: r.rNorm, n: big2.ensureContent(r.id).settlements.length }));
|
||||||
|
withCount.sort((a, b) => a.rNorm - b.rNorm);
|
||||||
|
const third = Math.floor(withCount.length / 3);
|
||||||
|
const inner = withCount.slice(0, third);
|
||||||
|
const outer = withCount.slice(-third);
|
||||||
|
const avg = (arr) => arr.reduce((s, x) => s + x.n, 0) / arr.length;
|
||||||
|
check(`core→rim settlement gradient (inner ${avg(inner).toFixed(2)}/system > outer ${avg(outer).toFixed(2)}/system)`, avg(inner) > avg(outer));
|
||||||
|
|
||||||
|
// Station naming + the pure report formatter.
|
||||||
|
const stName = NameGenerator.station(Rng.derive('x', 'station', 'S1'), 'cloudBase', 'Kest');
|
||||||
|
check('settlement names are anchor + noun ("Kest Skyspire"-style)', /Kest [A-Z]\w+/.test(stName));
|
||||||
|
|
||||||
|
const { formatSystemReport, formatPop } = await import(
|
||||||
|
pathToFileURL(join(__dirname, '../js/galaxy/SystemReport.js')).href
|
||||||
|
);
|
||||||
|
const anySys = big2.currentSystem();
|
||||||
|
const report = formatSystemReport(big2.ensureContent(anySys.id));
|
||||||
|
check('report has title/subtitle/settlements/summary', !!report.title && !!report.subtitle && Array.isArray(report.settlements) && typeof report.summary === 'string');
|
||||||
|
check('report lines name their anchor world or open space', report.settlements.every((s) => /on \w+ [IVX]+|in open space/.test(s.text)));
|
||||||
|
check('report population sums match', report.population === report.settlements.reduce((s, x) => s + x.population, 0));
|
||||||
|
check('formatPop() scales (1.2k / 9.0M)', formatPop(1234) === '1.2k' && formatPop(9000000) === '9.0M' && formatPop(12) === '12');
|
||||||
|
|
||||||
|
// Unclaimed systems read as "charted · unclaimed" in the report.
|
||||||
|
const unclaimedRec = sample.map((r) => big2.ensureContent(r.id)).find((c) => c.settlements.length === 0);
|
||||||
|
check('unclaimed systems report "charted · unclaimed"', unclaimedRec && formatSystemReport(unclaimedRec).summary === 'charted · unclaimed');
|
||||||
|
|
||||||
|
// Lazy === eager still holds with the lived-in layer (spot check).
|
||||||
|
const fresh2 = Galaxy.create(SEED);
|
||||||
|
check('settlements: lazy content === content from a fresh galaxy', deepEq(fresh2.ensureContent(big2.records[0].id).settlements, big2.ensureContent(big2.records[0].id).settlements));
|
||||||
|
}
|
||||||
|
|
||||||
console.log(failures === 0 ? '\nAll galaxy tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
|
console.log(failures === 0 ? '\nAll galaxy tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
|
||||||
process.exit(failures === 0 ? 0 : 1);
|
process.exit(failures === 0 ? 0 : 1);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,14 @@
|
||||||
Working agreements and conventions for building this game. Read this before
|
Working agreements and conventions for building this game. Read this before
|
||||||
adding new systems — these are the rules that keep the project scalable.
|
adding new systems — these are the rules that keep the project scalable.
|
||||||
|
|
||||||
|
## Commits (important)
|
||||||
|
|
||||||
|
- **Brian makes the commits, manually.** After finishing a change, leave the
|
||||||
|
work staged/unstaged in the working tree with a suggested commit message
|
||||||
|
(in the chat reply, or a note here), and do NOT run `git commit` yourself.
|
||||||
|
This gives Brian the chance to review the code before it enters history.
|
||||||
|
- Amends/reverts are fine if asked explicitly.
|
||||||
|
|
||||||
## What we're building
|
## What we're building
|
||||||
|
|
||||||
A procedurally generated space RPG in the spirit of **Privateer**, but in
|
A procedurally generated space RPG in the spirit of **Privateer**, but in
|
||||||
|
|
@ -76,12 +84,12 @@ menu (displayed, editable, rerollable; same seed ⇒ same galaxy).
|
||||||
(id, name, type, x, y) up front. 40,000 systems is ~70 ms, so the whole
|
(id, name, type, x, y) up front. 40,000 systems is ~70 ms, so the whole
|
||||||
galaxy is always known: the player can never "discover" a layout that
|
galaxy is always known: the player can never "discover" a layout that
|
||||||
wasn't already implied by the seed.
|
wasn't already implied by the seed.
|
||||||
2. **Contents** — planets/moons/belts/hazards are generated **lazily** on
|
2. **Contents** — planets/moons/belts/**settlements**/hazards are
|
||||||
first arrival (`galaxy.ensureContent(id)`), then cached. Each system's
|
generated **lazily** on first arrival (`galaxy.ensureContent(id)`),
|
||||||
draw stream is `Rng.derive(seed, 'system', id)` — independent of
|
then cached. Each system's draw stream is `Rng.derive(seed, 'system', id)`
|
||||||
generation order — so lazy and eager (`galaxy.generateAll()`) results
|
— independent of generation order — so lazy and eager
|
||||||
are identical. Pick whichever is cheaper at runtime; correctness never
|
(`galaxy.generateAll()`) results are identical. Pick whichever is
|
||||||
depends on it.
|
cheaper at runtime; correctness never depends on it.
|
||||||
|
|
||||||
**Determinism rules (keep them!):**
|
**Determinism rules (keep them!):**
|
||||||
- All generation draws go through `Rng` (`js/utils/Rng.js`). Never
|
- All generation draws go through `Rng` (`js/utils/Rng.js`). Never
|
||||||
|
|
@ -111,6 +119,33 @@ The player's **current system** starts at `galaxy.currentSystem()`
|
||||||
(the eventual star map / jump drives) will use `galaxy.neighborsOf(id)`
|
(the eventual star map / jump drives) will use `galaxy.neighborsOf(id)`
|
||||||
and the spatial hash already built for it.
|
and the spatial hash already built for it.
|
||||||
|
|
||||||
|
**The galaxy is already lived in.** It was settled long before the
|
||||||
|
player arrives. Every system's content can include **settlements**:
|
||||||
|
`colony` (on a habitable world), `miningStation` (over a resource
|
||||||
|
world), `cloudBase` (riding a gas giant), `deepSpaceStation` (adrift in
|
||||||
|
open space), `waypoint` (a small beacon — the faint trace of a crossed
|
||||||
|
galaxy). Not everything is inhabited: many systems report "charted ·
|
||||||
|
unclaimed". Model & seams:
|
||||||
|
- **Kinds vocabulary** — `data/settlements.json` (label, description,
|
||||||
|
theme color, population range, anchor type). Add a kind = JSON + naming
|
||||||
|
pool; the generator picks it up by name.
|
||||||
|
- **Per-type rates** — `types.<id>.attributes.settlements` in
|
||||||
|
`data/systems.json`: `chance` + optional `needs` (planet classes) per
|
||||||
|
kind. Same attribute-driven pattern as everything else.
|
||||||
|
- **Core→rim gradient** — `galaxy.settlements.gradient` in
|
||||||
|
`data/galaxy.json`: the settled heart is denser (factor 1.0), the rim
|
||||||
|
is thinner (clamped to `floor`). Each record carries `rNorm` (0 =
|
||||||
|
center, 1 = rim) so density is per-system, not global.
|
||||||
|
- **Reserved seam: `owner`** — every settlement has `owner: null`. That's
|
||||||
|
where **factions and pirates** will plug in later (claim, flag,
|
||||||
|
relations). Deliberately absent for now — no factions yet.
|
||||||
|
- **Pure report formatter** — `js/galaxy/SystemReport.js`
|
||||||
|
(`formatSystemReport(content)` → title/subtitle/settlements/summary).
|
||||||
|
The GameScene HUD renders it; future star map / terminal UI reuse it.
|
||||||
|
- Landing/exploration (a future feature) will treat settlements as points
|
||||||
|
of interest: the data already says what's there and where (anchor =
|
||||||
|
planet ordinal or open space).
|
||||||
|
|
||||||
## Phaser version
|
## Phaser version
|
||||||
|
|
||||||
- Pinned: **Phaser 4.2.1** ("Giedi"), vendored in `lib/phaser.min.js`.
|
- Pinned: **Phaser 4.2.1** ("Giedi"), vendored in `lib/phaser.min.js`.
|
||||||
|
|
@ -126,6 +161,15 @@ and the spatial hash already built for it.
|
||||||
- [x] Two-level worldgen: seeded galaxy roster (40k systems) + lazy,
|
- [x] Two-level worldgen: seeded galaxy roster (40k systems) + lazy,
|
||||||
order-independent system contents; system archetypes in JSON
|
order-independent system contents; system archetypes in JSON
|
||||||
(theme + attributes + distribution weight/radius band)
|
(theme + attributes + distribution weight/radius band)
|
||||||
|
- [x] The lived-in layer: settlements (colonies, mining stations, cloud
|
||||||
|
bases, deep-space stations, beacons) with per-type rates, a
|
||||||
|
core→rim density gradient, populations, and a `owner` seam reserved
|
||||||
|
for the factions/pirates to come; readable as a HUD dossier
|
||||||
|
(`SystemReport`)
|
||||||
|
- [ ] Factions & pirates: claim settlements (`owner`), flags, borders,
|
||||||
|
and the player's place in a populated galaxy
|
||||||
|
- [ ] Landing & exploration: settlements become points of interest you
|
||||||
|
can approach (the data — kind, anchor, population — is already there)
|
||||||
- [ ] Richer galaxy distribution rules (`galaxy.distribution.rules[]`:
|
- [ ] Richer galaxy distribution rules (`galaxy.distribution.rules[]`:
|
||||||
clustering by type, borders, adjacency affinity) — hook marked in
|
clustering by type, borders, adjacency affinity) — hook marked in
|
||||||
`Galaxy._generate()`
|
`Galaxy._generate()`
|
||||||
|
|
@ -133,8 +177,8 @@ and the spatial hash already built for it.
|
||||||
wire in current-system boundaries, jumps between systems (use
|
wire in current-system boundaries, jumps between systems (use
|
||||||
`galaxy.neighborsOf`), and a star map scene
|
`galaxy.neighborsOf`), and a star map scene
|
||||||
- [ ] Ship input beyond click-to-fly (throttle/brake keys, manual rotation)
|
- [ ] Ship input beyond click-to-fly (throttle/brake keys, manual rotation)
|
||||||
- [ ] HUD (speed, sector name, later: fuel/crew) — system name/identity
|
- [ ] HUD (speed, fuel/crew) — the current system's dossier (name,
|
||||||
already shown top-left
|
identity, settlements) is already shown top-left
|
||||||
- [ ] Save/load (the `config` + entity split should make this tractable;
|
- [ ] Save/load (the `config` + entity split should make this tractable;
|
||||||
a save = seed + player state, since the galaxy regenerates)
|
a save = seed + player state, since the galaxy regenerates)
|
||||||
- [ ] Economy/trading loop (the Privateer heart)
|
- [ ] Economy/trading loop (the Privateer heart)
|
||||||
|
|
|
||||||
|
|
@ -133,9 +133,11 @@ export class Galaxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name comes from a per-record fork so roster generation order can
|
// Name comes from a per-record fork so roster generation order can
|
||||||
// never leak into it.
|
// never leak into it. rNorm (0 = galactic center, 1 = rim) is kept on
|
||||||
|
// the record: the settlement generator uses it (core→rim density),
|
||||||
|
// and it's handy for any future "where am I in the galaxy" rules.
|
||||||
const name = NameGenerator.star(Rng.derive(this.seed, 'name', id));
|
const name = NameGenerator.star(Rng.derive(this.seed, 'name', id));
|
||||||
const rec = { id, name, type, x, y };
|
const rec = { id, name, type, x, y, rNorm };
|
||||||
records.push(rec);
|
records.push(rec);
|
||||||
this.byId.set(id, rec);
|
this.byId.set(id, rec);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ import { NameGenerator, ordinalLabel } from '../utils/NameGenerator.js';
|
||||||
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns a lightweight galaxy record (id, name, type, x, y) into a fully
|
* Turns a lightweight galaxy record (id, name, type, x, y, rNorm) into a
|
||||||
* generated system: star, planets, moons, debris belt, hazard flag.
|
* fully generated system: star, planets, moons, SETTLEMENTS, debris belt,
|
||||||
|
* hazard flag.
|
||||||
*
|
*
|
||||||
* Deterministic contract:
|
* Deterministic contract:
|
||||||
* same galaxy seed + same record id ⇒ identical content, every time.
|
* same galaxy seed + same record id ⇒ identical content, every time.
|
||||||
|
|
@ -15,12 +16,20 @@ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
||||||
* is byte-for-byte identical to one generated during an up-front
|
* is byte-for-byte identical to one generated during an up-front
|
||||||
* generateAll(). That's what makes lazy generation safe.
|
* generateAll(). That's what makes lazy generation safe.
|
||||||
*
|
*
|
||||||
* What the system actually contains is steered by the TYPE'S ATTRIBUTES
|
* What the system contains is steered by the TYPE'S ATTRIBUTES in
|
||||||
* in data/systems.json (`types.<id>.attributes`): star classes, binary
|
* data/systems.json (`types.<id>.attributes`): star classes, binary
|
||||||
* chance, planet count spread, planet class weights, moon/belt chances,
|
* chance, planet count spread, planet class weights, moon/belt chances,
|
||||||
* habitability, hazard. Theme (`types.<id>.theme`) is for UI. Add new
|
* habitability, hazard, and — the lived-in layer — `settlements` (chance
|
||||||
* attribute keys here + in the JSON and types become richer without
|
* + required planet class per settlement kind). Settlement kinds and their
|
||||||
* touching the galaxy generator.
|
* population ranges live in data/settlements.json; the core→rim density
|
||||||
|
* gradient in data/galaxy.json (`settlements.gradient`).
|
||||||
|
*
|
||||||
|
* The galaxy is ALREADY LIVED IN: it was settled long before the player.
|
||||||
|
* Colonies on habitable worlds, mining stations over resource worlds,
|
||||||
|
* cloud bases riding gas giants, stations adrift in open space — and a
|
||||||
|
* fair number of charted-but-unclaimed systems. Nothing here is hostile
|
||||||
|
* yet: `owner` on every settlement is a reserved seam for the factions
|
||||||
|
* and pirates we'll introduce later.
|
||||||
*/
|
*/
|
||||||
export function generateSystemContent(galaxy, record, typeDefs = null) {
|
export function generateSystemContent(galaxy, record, typeDefs = null) {
|
||||||
const defs = typeDefs ?? config.get('systems.types', {});
|
const defs = typeDefs ?? config.get('systems.types', {});
|
||||||
|
|
@ -61,15 +70,27 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
||||||
// Jovian/ice worlds drag moon systems; terrestrials mostly don't.
|
// Jovian/ice worlds drag moon systems; terrestrials mostly don't.
|
||||||
moons = pclass === 'gas' || pclass === 'ice' ? rng.int(1, 6) : rng.int(0, 2);
|
moons = pclass === 'gas' || pclass === 'ice' ? rng.int(1, 6) : rng.int(0, 2);
|
||||||
}
|
}
|
||||||
|
const root = NameGenerator.planetRoot(rng);
|
||||||
planets.push({
|
planets.push({
|
||||||
name: `${NameGenerator.planetRoot(rng)} ${ordinalLabel(i)}`,
|
name: `${root} ${ordinalLabel(i)}`,
|
||||||
ordinal: i,
|
ordinal: i,
|
||||||
|
root,
|
||||||
class: pclass,
|
class: pclass,
|
||||||
moons,
|
moons,
|
||||||
habitable: pclass === 'rocky' && rng.chance(attr.habitability ?? 0.1),
|
habitable: pclass === 'rocky' && rng.chance(attr.habitability ?? 0.1),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Settlements (the lived-in layer) ---------------------------------
|
||||||
|
const settlements = generateSettlements({
|
||||||
|
rng,
|
||||||
|
kindDefs: config.get('settlements.kinds', {}),
|
||||||
|
spec: attr.settlements ?? {},
|
||||||
|
planets,
|
||||||
|
starName: star.name,
|
||||||
|
density: settlementDensity(galaxy, record),
|
||||||
|
});
|
||||||
|
|
||||||
// --- Debris belt & system-level hazard --------------------------------
|
// --- Debris belt & system-level hazard --------------------------------
|
||||||
const belt = {
|
const belt = {
|
||||||
present: rng.chance(attr.beltChance ?? 0.35),
|
present: rng.chance(attr.beltChance ?? 0.35),
|
||||||
|
|
@ -82,7 +103,85 @@ export function generateSystemContent(galaxy, record, typeDefs = null) {
|
||||||
type: record.type,
|
type: record.type,
|
||||||
star,
|
star,
|
||||||
planets,
|
planets,
|
||||||
|
settlements,
|
||||||
belt,
|
belt,
|
||||||
hazard,
|
hazard,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core→rim density: the settled heart of the galaxy has more activity per
|
||||||
|
* system; the rim is thinner, lonelier. `factor` scales every settlement
|
||||||
|
* chance (clamped to a floor so the rim isn't dead). 0 = no gradient.
|
||||||
|
*/
|
||||||
|
function settlementDensity(galaxy, record) {
|
||||||
|
const g = galaxy?.params?.settlements?.gradient ?? {};
|
||||||
|
const falloff = Math.max(0, g.falloff ?? 0.7);
|
||||||
|
const floor = clamp(g.floor ?? 0.22, 0, 1);
|
||||||
|
const rNorm = clamp(record?.rNorm ?? 0, 0, 1);
|
||||||
|
return clamp(1 - rNorm * falloff, floor, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw settlements for one system. Stable draw order: planet-bound kinds in
|
||||||
|
* orbital order (colony, mining, cloud), then free-floating (deep-space
|
||||||
|
* station, waypoint). Every roll goes through the system's own stream.
|
||||||
|
*/
|
||||||
|
function generateSettlements({ rng, kindDefs, spec, planets, starName, density }) {
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
const make = (kind, anchor, rootWord) => {
|
||||||
|
const def = kindDefs[kind] ?? {};
|
||||||
|
out.push({
|
||||||
|
kind,
|
||||||
|
name: NameGenerator.station(rng, kind, rootWord),
|
||||||
|
anchor,
|
||||||
|
population: logPopulation(rng, def.population),
|
||||||
|
owner: null, // reserved: factions / pirates claim settlements later
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Planet-bound, in orbital order.
|
||||||
|
for (const p of planets) {
|
||||||
|
if (p.habitable && roll(rng, spec.colony?.chance ?? 0.3, density)) {
|
||||||
|
make('colony', { type: 'planet', ordinal: p.ordinal }, p.root);
|
||||||
|
}
|
||||||
|
if (needs(p, spec.miningStation?.needs, ['rocky', 'lava', 'ice']) &&
|
||||||
|
roll(rng, spec.miningStation?.chance ?? 0.2, density)) {
|
||||||
|
make('miningStation', { type: 'planet', ordinal: p.ordinal }, p.root);
|
||||||
|
}
|
||||||
|
if (needs(p, spec.cloudBase?.needs, ['gas']) &&
|
||||||
|
roll(rng, spec.cloudBase?.chance ?? 0.15, density)) {
|
||||||
|
make('cloudBase', { type: 'planet', ordinal: p.ordinal }, p.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free-floating, out in the dark.
|
||||||
|
if (roll(rng, spec.deepSpaceStation?.chance ?? 0.12, density)) {
|
||||||
|
make('deepSpaceStation', { type: 'space' }, starName);
|
||||||
|
}
|
||||||
|
if (roll(rng, spec.waypoint?.chance ?? 0.2, density)) {
|
||||||
|
make('waypoint', { type: 'space' }, starName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One deterministic roll, scaled by the core→rim density factor. */
|
||||||
|
function roll(rng, chance, density) {
|
||||||
|
return rng.chance(chance * density);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Does the planet satisfy the kind's requirements? */
|
||||||
|
function needs(planet, needsList, defaults) {
|
||||||
|
const list = Array.isArray(needsList) && needsList.length ? needsList : defaults;
|
||||||
|
return list.includes(planet.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Log-uniform population in [min, max] (a few towns to a few megacities). */
|
||||||
|
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
|
||||||
|
return Math.round(Math.exp(rng.range(Math.log(Math.max(1, min)), Math.log(max))));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { config } from '../config/Config.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a generated system into a readable "dossier" for UI.
|
||||||
|
*
|
||||||
|
* Pure — no Phaser — so it's testable in Node and reusable anywhere a
|
||||||
|
* system needs describing (the HUD today; the star map, tooltips, saves,
|
||||||
|
* or a terminal UI later). Scenes stay thin: they render what this
|
||||||
|
* returns.
|
||||||
|
*
|
||||||
|
* const report = formatSystemReport(content);
|
||||||
|
* // { title, subtitle, settlements: [{ text, color, population }],
|
||||||
|
* // summary, population }
|
||||||
|
*/
|
||||||
|
export function formatSystemReport(content, typeDefs = null, kindDefs = null) {
|
||||||
|
const types = typeDefs ?? config.get('systems.types', {});
|
||||||
|
const kinds = kindDefs ?? config.get('settlements.kinds', {});
|
||||||
|
const typeDef = types[content.type] ?? {};
|
||||||
|
|
||||||
|
const planetName = (ordinal) => {
|
||||||
|
const p = content.planets.find((pl) => pl.ordinal === ordinal);
|
||||||
|
return p ? p.name : `planet ${ordinal}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const settlements = [];
|
||||||
|
for (const s of content.settlements ?? []) {
|
||||||
|
const def = kinds[s.kind] ?? {};
|
||||||
|
const where =
|
||||||
|
s.anchor?.type === 'planet' ? `on ${planetName(s.anchor.ordinal)}` : 'in open space';
|
||||||
|
settlements.push({
|
||||||
|
text: `${s.name} · ${def.label ?? s.kind} ${where}`,
|
||||||
|
color: def.theme?.color ?? '#8fa0c9',
|
||||||
|
population: s.population ?? 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const population = settlements.reduce((sum, s) => sum + s.population, 0);
|
||||||
|
const n = settlements.length;
|
||||||
|
const summary =
|
||||||
|
n === 0
|
||||||
|
? 'charted · unclaimed'
|
||||||
|
: `${n} settlement${n === 1 ? '' : 's'} · pop ~${formatPop(population)}`;
|
||||||
|
|
||||||
|
const planetN = content.planets.length;
|
||||||
|
return {
|
||||||
|
title: content.name,
|
||||||
|
subtitle: `${typeDef.label ?? content.type} system · star ${content.star.class} · ${planetN} planet${planetN === 1 ? '' : 's'}`,
|
||||||
|
settlements,
|
||||||
|
summary,
|
||||||
|
population,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 1234 → "1.2k", 9000000 → "9.0M" */
|
||||||
|
export function formatPop(n) {
|
||||||
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
||||||
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
||||||
|
return String(n);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import { config } from '../config/Config.js';
|
||||||
import { toColor } from '../utils/Color.js';
|
import { toColor } from '../utils/Color.js';
|
||||||
import { Rng } from '../utils/Rng.js';
|
import { Rng } from '../utils/Rng.js';
|
||||||
import { Galaxy } from '../galaxy/Galaxy.js';
|
import { Galaxy } from '../galaxy/Galaxy.js';
|
||||||
|
import { formatSystemReport } from '../galaxy/SystemReport.js';
|
||||||
import { Ship } from '../entities/Ship.js';
|
import { Ship } from '../entities/Ship.js';
|
||||||
import { Starfield } from '../visuals/Starfield.js';
|
import { Starfield } from '../visuals/Starfield.js';
|
||||||
|
|
||||||
|
|
@ -56,7 +57,10 @@ export class GameScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-left HUD: the current system's name and identity.
|
* Top-left HUD: the current system's dossier — name, identity, and what
|
||||||
|
*'s ALREADY THERE: colonies, mining stations, cloud bases, stations adrift
|
||||||
|
* in open space (or "charted · unclaimed" when nobody's settled here).
|
||||||
|
* Formatted by the pure SystemReport helper; this method only renders.
|
||||||
*
|
*
|
||||||
* The galaxy comes from the shared registry (built by the menu from the
|
* The galaxy comes from the shared registry (built by the menu from the
|
||||||
* chosen seed). Dev boots that skip the menu (dev/test-game.html) get a
|
* chosen seed). Dev boots that skip the menu (dev/test-game.html) get a
|
||||||
|
|
@ -78,30 +82,32 @@ export class GameScene extends Phaser.Scene {
|
||||||
|
|
||||||
const current = this.galaxy.currentSystem();
|
const current = this.galaxy.currentSystem();
|
||||||
const content = this.galaxy.ensureContent(current.id);
|
const content = this.galaxy.ensureContent(current.id);
|
||||||
const typeDef = this.galaxy.typeDefs?.[current.type] ?? {};
|
const report = formatSystemReport(content);
|
||||||
const fam = FONT_FALLBACK;
|
const fam = FONT_FALLBACK;
|
||||||
|
|
||||||
|
let y = 14;
|
||||||
|
const line = (text, style) => {
|
||||||
this.add
|
this.add
|
||||||
.text(16, 14, content.name, {
|
.text(16, y, text, style)
|
||||||
fontFamily: fam,
|
|
||||||
fontSize: '17px',
|
|
||||||
fontStyle: 'bold',
|
|
||||||
color: toColor(typeDef.theme?.color ?? '#9fb4e8'),
|
|
||||||
})
|
|
||||||
.setOrigin(0, 0)
|
.setOrigin(0, 0)
|
||||||
.setScrollFactor(0) // UI: pinned to the screen, not the world
|
.setScrollFactor(0) // UI: pinned to the screen, not the world
|
||||||
.setDepth(30);
|
.setDepth(30);
|
||||||
|
y += 20 + (style.fontSize === '17px' ? 6 : 0);
|
||||||
|
};
|
||||||
|
|
||||||
const label = typeDef.label ?? current.type;
|
line(report.title, {
|
||||||
this.add
|
|
||||||
.text(16, 38, `${label} \u00b7 star ${content.star.class} \u00b7 ${content.planets.length} planets \u00b7 seed ${this.galaxy.seed}`, {
|
|
||||||
fontFamily: fam,
|
fontFamily: fam,
|
||||||
fontSize: '12px',
|
fontSize: '17px',
|
||||||
color: '#8fa0c9',
|
fontStyle: 'bold',
|
||||||
})
|
color: toColor(this.galaxy.typeDefs?.[current.type]?.theme?.color ?? '#9fb4e8'),
|
||||||
.setOrigin(0, 0)
|
});
|
||||||
.setScrollFactor(0)
|
line(report.subtitle, { fontFamily: fam, fontSize: '12px', color: '#8fa0c9' });
|
||||||
.setDepth(30);
|
y += 2;
|
||||||
|
for (const s of report.settlements) {
|
||||||
|
line(s.text, { fontFamily: fam, fontSize: '13px', color: toColor(s.color, 0x8fa0c9) });
|
||||||
|
}
|
||||||
|
line(report.summary, { fontFamily: fam, fontSize: '12px', color: '#54608a' });
|
||||||
|
line(`seed ${this.galaxy.seed}`, { fontFamily: fam, fontSize: '11px', color: '#3d476b' });
|
||||||
}
|
}
|
||||||
|
|
||||||
update(_time, delta) {
|
update(_time, delta) {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,15 @@ const FALLBACK = {
|
||||||
minParts: 3,
|
minParts: 3,
|
||||||
maxParts: 5,
|
maxParts: 5,
|
||||||
},
|
},
|
||||||
|
station: {
|
||||||
|
nouns: {
|
||||||
|
colony: ['Reach', 'Haven', 'Landing'],
|
||||||
|
miningStation: ['Rig', 'Dredge', 'Claim'],
|
||||||
|
cloudBase: ['Skyspire', 'Drift', 'Aerie'],
|
||||||
|
deepSpaceStation: ['Relay', 'Beacon', 'Anchor'],
|
||||||
|
waypoint: ['Waypoint', 'Beacon', 'Light'],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX'];
|
const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX'];
|
||||||
|
|
@ -70,4 +79,17 @@ export const NameGenerator = {
|
||||||
const syllables = Array.isArray(g.syllables) && g.syllables.length ? g.syllables : FALLBACK.galaxy.syllables;
|
const syllables = Array.isArray(g.syllables) && g.syllables.length ? g.syllables : FALLBACK.galaxy.syllables;
|
||||||
return joinSyllables(rng, syllables, g.minParts ?? 3, g.maxParts ?? 5);
|
return joinSyllables(rng, syllables, g.minParts ?? 3, g.maxParts ?? 5);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A settlement name from its anchor + kind: "Kest Reach", "Cortan Relay".
|
||||||
|
* `anchorWord` is the planet's root word (planet-bound settlements) or the
|
||||||
|
* star's name (free-floating ones).
|
||||||
|
*/
|
||||||
|
station(rng, kind, anchorWord) {
|
||||||
|
const cfg = section('station', FALLBACK.station);
|
||||||
|
const all = cfg.nouns ?? FALLBACK.station.nouns;
|
||||||
|
const pool = Array.isArray(all?.[kind]) && all[kind].length ? all[kind] : (FALLBACK.station.nouns[kind] ?? ['Post']);
|
||||||
|
const noun = rng.pick(pool) ?? 'Post';
|
||||||
|
return `${anchorWord ?? 'Outer'} ${noun}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue