150 lines
5.8 KiB
JavaScript
150 lines
5.8 KiB
JavaScript
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);
|
|
},
|
|
};
|