Files
SpaceWanderer/lib/game/crew.ts
T

91 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<T>(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.
// 16×22 = 352 combinations — a roster realistically never approaches that size.
export function makeCrewName(rng: () => number, taken: Set<string>): string {
for (;;) {
const name = `${title(pick(rng, FIRST_NAMES))} ${pick(rng, LAST_NAMES)}`
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<string>()
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<string>): 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)
}