89 lines
3.3 KiB
JavaScript
89 lines
3.3 KiB
JavaScript
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.<id>.attributes`): star classes, binary
|
|
* chance, planet count spread, planet class weights, moon/belt chances,
|
|
* habitability, hazard. Theme (`types.<id>.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,
|
|
};
|
|
}
|