import { config } from '../config/Config.js'; import { Rng } from '../utils/Rng.js'; import { NameGenerator, ordinalLabel } from '../utils/NameGenerator.js'; 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 * generated system: star, planets, moons, debris belt, hazard flag. * * Deterministic contract: * same galaxy seed + same record id ⇒ identical content, every time. * The draw stream is derived from (seed, 'system', id) — NOT from the * galaxy-level sequence — so a system generated when the player arrives * is byte-for-byte identical to one generated during an up-front * generateAll(). That's what makes lazy generation safe. * * What the system actually contains is steered by the TYPE'S ATTRIBUTES * in data/systems.json (`types..attributes`): star classes, binary * chance, planet count spread, planet class weights, moon/belt chances, * habitability, hazard. Theme (`types..theme`) is for UI. Add new * attribute keys here + in the JSON and types become richer without * touching the galaxy generator. */ export function generateSystemContent(galaxy, record, typeDefs = null) { const defs = typeDefs ?? config.get('systems.types', {}); const type = defs[record.type] ?? { label: record.type, attributes: {} }; const attr = type.attributes ?? {}; const rng = Rng.derive(galaxy.seed, 'system', record.id); // --- Star ------------------------------------------------------------- const starClasses = attr.star?.classes ?? { G: 30, K: 35, M: 35 }; const massTable = attr.star?.mass ?? {}; const starClass = rng.weighted(starClasses, 'M'); const mass = Array.isArray(massTable[starClass]) ? massTable[starClass] : [0.3, 1.2]; const star = { name: NameGenerator.star(rng), class: starClass, mass: Number(rng.range(mass[0], mass[1]).toFixed(2)), binary: false, }; if (rng.chance(attr.binaryChance ?? 0.05)) { star.binary = true; star.secondary = { name: NameGenerator.star(rng), class: rng.weighted(starClasses, starClass), }; } // --- Planets ---------------------------------------------------------- const pc = attr.planetCount ?? { min: 3, max: 8, mean: 5 }; const count = Math.round( clamp(pc.mean + (rng.next() - 0.5) * (pc.max - pc.min), pc.min, pc.max), ); const classWeights = attr.planetClasses ?? { rocky: 45, gas: 25, ice: 18, lava: 12 }; const planets = []; for (let i = 1; i <= count; i++) { const pclass = rng.weighted(classWeights, 'rocky'); let moons = 0; if (rng.chance(attr.moonChance ?? 0.3)) { // Jovian/ice worlds drag moon systems; terrestrials mostly don't. moons = pclass === 'gas' || pclass === 'ice' ? rng.int(1, 6) : rng.int(0, 2); } planets.push({ name: `${NameGenerator.planetRoot(rng)} ${ordinalLabel(i)}`, ordinal: i, class: pclass, moons, habitable: pclass === 'rocky' && rng.chance(attr.habitability ?? 0.1), }); } // --- Debris belt & system-level hazard -------------------------------- const belt = { present: rng.chance(attr.beltChance ?? 0.35), kind: rng.pick(['asteroid', 'debris']) ?? 'asteroid', }; const hazard = rng.chance(attr.hazard ?? 0.1); return { name: record.name, type: record.type, star, planets, belt, hazard, }; }