// Pure, seeded crew generation — no DB, no Date, no Math.random. Same seed → same starting // crew; the exploration crew roll draws from the caller's per-planet rng stream so a found // member is deterministic per save+planet (same derivation as the resource/part haul). import { mulberry32 } from './world' export interface CrewMember { name: string role: string traitGood: string traitBad: string } const FIRST_NAMES = [ 'AVA', 'BEN', 'CASS', 'DANA', 'ELI', 'FERN', 'GIDEON', 'HALYA', 'IVAR', 'JUNO', 'KAI', 'LORE', 'MARA', 'NICO', 'ODESSA', 'PETRA', ] const LAST_NAMES = [ 'VANCE', 'OKONKWO', 'SORENSEN', 'DELACRUZ', 'HALLOWAY', 'IBARRA', 'KOVAC', 'LINDQVIST', 'MERCER', 'NADEAU', 'ORLOV', 'QUINTANA', 'REYES', 'STALKE', 'TANAKA', 'UNDERWOOD', 'VALDEZ', 'WHITLOCK', 'YAMADA', 'ZUBEROV', 'ASHFORD', 'BLACKWELL', ] const ROLES = ['COMMANDER', 'PILOT', 'ENGINEER', 'MEDIC', 'NAVIGATOR', 'SCIENTIST', 'GUNNER', 'TECHNICIAN'] const GOOD_TRAITS = [ 'STEADY UNDER PRESSURE', 'RECKLESS COURAGE', 'CALM IN A CRISIS', 'FIXES ANYTHING', 'SHARP SHOOTER', 'READS STARS BLIND', 'QUICK HANDS', 'IRON WILL', 'FINDS HIDDEN THINGS', 'TALKS TO ANYONE', ] const BAD_TRAITS = [ 'RIGID — SLOW TO ADAPT', 'SHORT FUSE', 'OVERCAUTIOUS', 'CARELESS WITH FUSES', 'SLOWS THE CREW DOWN', 'ARGUES WITH EVERYONE', 'FORGETS TO EAT', 'JINXED IN A DOGFIGHT', 'HOARDS RATIONS', 'DOUBTS THE CAPTAIN', ] const STARTING_CREW_MIN = 2 const STARTING_CREW_SPREAD = 2 // 2..3 starting members export const CREW_CHANCE = 0.18 // a scan finds a crew member who joins the ship (~1 in 5-6 scans) function title(word: string): string { return word.charAt(0) + word.slice(1).toLowerCase() } function pick(rng: () => number, arr: T[]): T { return arr[Math.floor(rng() * arr.length)] } // A name unique against `taken` (the current roster), claimed into the set on success. // The pool is finite (16×22=352) and the roster has no cap, so — like makeName — fall back to // numbered variants once every base is taken instead of spinning forever. export function makeCrewName(rng: () => number, taken: Set): string { for (;;) { const base = `${title(pick(rng, FIRST_NAMES))} ${pick(rng, LAST_NAMES)}` if (!taken.has(base)) { taken.add(base) return base } if (taken.size >= FIRST_NAMES.length * LAST_NAMES.length) { for (let n = 2; ; n++) { const name = `${base} ${n}` if (!taken.has(name)) { taken.add(name) return name } } } } } // The crew a new game starts with: 2–3 members, unique names and roles (roles are drawn // without replacement). Pure — the caller derives the rng from the save seed. export function generateStartingCrew(rng: () => number): CrewMember[] { const taken = new Set() const roles = [...ROLES] const crew: CrewMember[] = [] for (let i = 0; i < STARTING_CREW_MIN + Math.floor(rng() * STARTING_CREW_SPREAD); i++) { const role = roles.splice(Math.floor(rng() * roles.length), 1)[0] crew.push({ name: makeCrewName(rng, taken), role, traitGood: pick(rng, GOOD_TRAITS), traitBad: pick(rng, BAD_TRAITS) }) } return crew } // The exploration crew roll: with CREW_CHANCE a member is found and joins the ship. Draws // from the SAME per-planet rng stream as the resource/part haul (called after it in // save.ts), so the outcome stays deterministic per save+planet. `taken` is the current // roster's names, keeping every name on the ship unique. export function rollCrewMember(rng: () => number, taken: Set): CrewMember | null { if (rng() >= CREW_CHANCE) return null const role = pick(rng, ROLES) return { name: makeCrewName(rng, taken), role, traitGood: pick(rng, GOOD_TRAITS), traitBad: pick(rng, BAD_TRAITS) } } // Independent PRNG stream for the starting crew of one save (same pattern as bodyRng / // explorationRng in world.ts — a fixed offset keeps it away from every other derivation). export function startingCrewRng(seed: string): () => number { return mulberry32((Number(seed) + 0x51ed270b) >>> 0) }