feat(game): crew-costs balance module + computeDuration duration model (#44)

This commit is contained in:
2026-10-02 12:58:57 +00:00
parent 2a8cb4da06
commit f52a59c5fc
3 changed files with 210 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
// Pure crew-balance module (spec §11): every tuning knob for the duration model and skill
// progression lives here, so tuning a number means editing one file. No lib/db imports —
// the frontend imports this for the live duration preview in the crew picker (same precedent
// as ship-costs.ts).
import type { TaskType } from './tasks'
export type SkillName = 'NAVIGATION' | 'SCANNING' | 'ENGINEERING' | 'RESEARCH'
export const WORK_POINTS = { EXPLORE: 4, JUMP_PER_HOP: 3, SHIP_WORK: 6, RESEARCH: 9 } as const
export const ACTION_SKILL: Record<TaskType, SkillName> = {
JUMP: 'NAVIGATION',
EXPLORE: 'SCANNING',
SHIP_UPGRADE: 'ENGINEERING',
SHIP_EXPAND: 'ENGINEERING',
SHIP_BUILD: 'ENGINEERING',
RESEARCH: 'RESEARCH',
}
export const PREFERRED_ROLES: Record<TaskType, string[]> = {
JUMP: ['PILOT', 'NAVIGATOR'],
EXPLORE: ['SCIENTIST'],
SHIP_UPGRADE: ['ENGINEER', 'TECHNICIAN'],
SHIP_EXPAND: ['ENGINEER', 'TECHNICIAN'],
SHIP_BUILD: ['ENGINEER', 'TECHNICIAN'],
RESEARCH: ['SCIENTIST'],
}
export const POWER_BANDS = [1, 1, 1, 2, 2, 3] // skill level 0..5 → power (L0–L2 → 1 · L3–L4 → 2 · L5 → 3)
export const XP_THRESHOLDS = [5, 10, 15, 20, 25] // cumulative task completions for L1..L5 (cap 5)
// Role → starting skill levels (spec §6): specialists start at L2, everyone else at L0.
export const STARTING_LEVELS: Record<string, Record<SkillName, number>> = {
COMMANDER: { NAVIGATION: 0, SCANNING: 0, ENGINEERING: 0, RESEARCH: 0 },
PILOT: { NAVIGATION: 2, SCANNING: 0, ENGINEERING: 0, RESEARCH: 0 },
ENGINEER: { NAVIGATION: 0, SCANNING: 0, ENGINEERING: 2, RESEARCH: 0 },
MEDIC: { NAVIGATION: 0, SCANNING: 0, ENGINEERING: 0, RESEARCH: 0 },
NAVIGATOR: { NAVIGATION: 2, SCANNING: 0, ENGINEERING: 0, RESEARCH: 0 },
SCIENTIST: { NAVIGATION: 0, SCANNING: 2, ENGINEERING: 0, RESEARCH: 2 },
GUNNER: { NAVIGATION: 0, SCANNING: 0, ENGINEERING: 0, RESEARCH: 0 },
TECHNICIAN: { NAVIGATION: 0, SCANNING: 0, ENGINEERING: 2, RESEARCH: 0 },
}
// The lead's good trait that cancels the role-mismatch penalty (spec §5).
export const MISMATCH_CANCEL_TRAIT = 'STEADY UNDER PRESSURE'
// All 15 duration traits of spec §5, keyed by the EXACT trait strings from lib/game/crew.ts.
// Value = additive turn modifier keyed by action type or 'ANY'; empty object = no additive
// modifier (the trait's effect is the mismatch cancel, see MISMATCH_CANCEL_TRAIT). Traits
// with no entry (the three STANDBY combat traits and #45's outcome traits) contribute 0.
// "SHIP work" covers all three SHIP types.
export const TRAIT_DURATION_MODIFIERS: Record<string, Partial<Record<TaskType | 'ANY', number>>> = {
'STEADY UNDER PRESSURE': {},
'RECKLESS COURAGE': { JUMP: -1 },
'CALM IN A CRISIS': { EXPLORE: -1 },
'FIXES ANYTHING': { SHIP_UPGRADE: -1, SHIP_EXPAND: -1, SHIP_BUILD: -1 },
'READS STARS BLIND': { JUMP: -1 },
'QUICK HANDS': { ANY: -1 },
'IRON WILL': { RESEARCH: -1 },
'RIGID — SLOW TO ADAPT': { RESEARCH: 1 },
'OVERCAUTIOUS': { ANY: 1 },
'SLOWS THE CREW DOWN': { ANY: 1 },
'CARELESS WITH FUSES': { SHIP_UPGRADE: 1, SHIP_EXPAND: 1, SHIP_BUILD: 1 },
'HOARDS RATIONS': { SHIP_UPGRADE: 1, SHIP_EXPAND: 1, SHIP_BUILD: 1 },
'FORGETS TO EAT': { EXPLORE: 1 },
'ARGUES WITH EVERYONE': { RESEARCH: 1 },
'DOUBTS THE CAPTAIN': { JUMP: 1 },
}
export const RECRUIT_CHANCE_BASE = 0.18 // a scan finds a joining member (matches crew.ts's current CREW_CHANCE)
export const RECRUIT_CHANCE_TALKER = 0.3 // lead with TALKS TO ANYONE (wired in PR C)