orbit/js/utils/NameGenerator.js

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);
},
};