import { config } from '../config/Config.js'; /** * Names. * * Two different strategies, both seed-deterministic: * * STARS & THE GALAXY — synthesised from syllable pools (data/naming.json → * star / galaxy). Short, consistent, and effectively unlimited: there are * 60 systems and no reason to run out of star names. * * PLANETS & STATIONS — drawn from finite, curated NAME BANKS (data/ * naming.json → banks). A deep pool of hand-picked names — colonial * "New Denver" worlds and alien "Klaxoria" worlds; official designations * like "Deep Space SC-145" and smuggler hangouts like "Hell's Hideout". * Within a single system a planet never repeats another planet's name and * a station never repeats another station's name (no-duplicate drawing). * Across the galaxy a finite pool must eventually recur — that's the cost * of keeping generation LAZY and ORDER-INDEPENDENT (see * docs/PROJECT_NOTES.md → "Determinism rules"). * * How the banks are dealt out: * const deck = NameGenerator.planetDeck(Rng.derive(seed, 'system', id, 'names', 'planets')); * const name = deck[i % deck.length]; // i = the system's i-th planet * * The deck is a seeded SHUFFLE of the whole bank, so the first N draws are * N distinct names until the bank is exhausted. `Rng.derive(..., 'system', * id, ...)` keeps it independent per system ⇒ lazy === eager. */ const FALLBACK = { star: { syllables: ['ka', 'vel', 'thu', 'ori', 'an', 'esh', 'mar', 'dy', 'neth', 'avi', 'cor', 'lu', 'tan', 'ys', 'brei', 'hal', 'ion'], minParts: 2, maxParts: 4, }, galaxy: { syllables: ['an', 'dro', 'me', 'mil', 'ky', 'cen', 'tau', 'rhi', 'sa', 'vel', 'pyr', 'os'], minParts: 3, maxParts: 5, }, asteroid: { syllables: ['ka', 'ver', 'dor', 'thra', 'nix', 'oru', 'mal', 'cra', 'zeth', 'vel', 'kor'], minParts: 2, maxParts: 3, suffixes: ['Field', 'Drift', 'Patch', 'Reef', 'Shoals', 'Belt'], }, // Tiny built-in pools so the game still names things if data/naming.json // is missing. The real banks live in data/naming.json → banks. planet: ['New Denver', 'Klaxoria', 'New Austin', 'Zyneatha', 'New Phoenix', 'Vexithunhal', 'New Dallas', 'Kordrasul'], station: ['Deep Space SC-145', "Hell's Hideout", 'Beacon 4491', 'The Rusty Anchor', 'Station 9012', 'The Shadow Quay'], }; const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX']; function section(name, fallback) { const v = config.get(`naming.${name}`); return (v && typeof v === 'object') ? v : fallback; } function capitalize(s) { return s.length ? s.charAt(0).toUpperCase() + s.slice(1) : s; } function joinSyllables(rng, syllables, minParts, maxParts) { const parts = rng.int(minParts, maxParts); let out = ''; for (let i = 0; i < parts; i++) out += rng.pick(syllables) ?? 'a'; return capitalize(out); } /** * The combined name bank for a category: the curated sub-pools concatenated * (planets: colonial + alien; stations: procedural + smuggler). Deduplicated * defensively in case the JSON has an accidental repeat. */ function combinedBank(pools) { const out = []; const seen = new Set(); for (const pool of pools) { if (!Array.isArray(pool)) continue; for (const name of pool) { if (typeof name !== 'string' || !name) continue; if (seen.has(name)) continue; seen.add(name); out.push(name); } } return out; } /** "1" → "I" … "20" → "XX"; beyond that, plain digits. */ export function ordinalLabel(n) { return ROMAN[n - 1] ?? String(n); } export const NameGenerator = { /** A star / system name: "Kethavoryn". */ star(rng) { const s = section('star', FALLBACK.star); const syllables = Array.isArray(s.syllables) && s.syllables.length ? s.syllables : FALLBACK.star.syllables; return joinSyllables(rng, syllables, s.minParts ?? 2, s.maxParts ?? 4); }, /** A galaxy name: "Vethani". */ galaxy(rng) { const g = section('galaxy', FALLBACK.galaxy); const syllables = Array.isArray(g.syllables) && g.syllables.length ? g.syllables : FALLBACK.galaxy.syllables; return joinSyllables(rng, syllables, g.minParts ?? 3, g.maxParts ?? 5); }, /** * An asteroid cluster name: "Kaveru Field" — synthesised syllables plus a * field-like suffix (unbounded, like stars; data/naming.json → asteroid). */ asteroid(rng) { const s = section('asteroid', FALLBACK.asteroid); const syllables = Array.isArray(s.syllables) && s.syllables.length ? s.syllables : FALLBACK.asteroid.syllables; const suffixes = Array.isArray(s.suffixes) && s.suffixes.length ? s.suffixes : FALLBACK.asteroid.suffixes; return `${joinSyllables(rng, syllables, s.minParts ?? 2, s.maxParts ?? 3)} ${rng.pick(suffixes) ?? 'Field'}`; }, /** * A shuffled deck of PLANET names for one system. The first N draws are N * distinct names (until the bank is exhausted). Pure function of (rng) ⇒ * deterministic and order-independent when the rng is a per-system derive. */ planetDeck(rng) { const b = config.get('naming.banks.planet', null); const pools = (b && (Array.isArray(b.colonial) || Array.isArray(b.alien))) ? [b.colonial, b.alien] : [FALLBACK.planet]; const names = combinedBank(pools); return names.length ? rng.shuffle(names) : Array.from(FALLBACK.planet); }, /** * A shuffled deck of STATION names for one system (official designations + * smuggler hangouts). Same no-duplicate-until-exhausted guarantee. */ stationDeck(rng) { const b = config.get('naming.banks.station', null); const pools = (b && (Array.isArray(b.procedural) || Array.isArray(b.smuggler))) ? [b.procedural, b.smuggler] : [FALLBACK.station]; const names = combinedBank(pools); return names.length ? rng.shuffle(names) : Array.from(FALLBACK.station); }, };