// Pure task-engine model (spec §3–§4): the task types and the duration formula. No lib/db // imports — the frontend imports this for the live duration preview in the crew picker // (same precedent as tech.ts / ship-costs.ts). import { MISMATCH_CANCEL_TRAIT, PREFERRED_ROLES, TRAIT_DURATION_MODIFIERS, WORK_POINTS } from './crew-costs' import type { CrewMember } from './crew' export type TaskType = 'JUMP' | 'EXPLORE' | 'SHIP_UPGRADE' | 'SHIP_EXPAND' | 'SHIP_BUILD' | 'RESEARCH' // Work points per action (spec §4): JUMP scales with hop count, the rest are flat. function workPoints(type: TaskType, hopCount: number): number { switch (type) { case 'JUMP': return hopCount * WORK_POINTS.JUMP_PER_HOP case 'EXPLORE': return WORK_POINTS.EXPLORE case 'RESEARCH': return WORK_POINTS.RESEARCH default: return WORK_POINTS.SHIP_WORK // SHIP_UPGRADE | SHIP_EXPAND | SHIP_BUILD } } // One trait's modifier for a lead on an action type: its per-type entry plus the 'ANY' entry. // Traits without a map entry contribute 0. function traitModifier(trait: string, type: TaskType): number { const mods = TRAIT_DURATION_MODIFIERS[trait] if (!mods) return 0 return (mods[type] ?? 0) + (mods['ANY'] ?? 0) } // duration = max(1, ceil(work / totalPower) + mismatchPenalty + traitModifierSum) — spec §4. // totalPower = Σ powerOf over the WHOLE team (lead + helpers); the default powerOf is the // constant 1 until PR B feeds the skill band. The mismatch penalty and the trait modifiers // apply to the LEAD only — helpers contribute power, are never penalised, and their traits do // not apply. `hopCount` is only meaningful for JUMP (pass 1 for the other types). export function computeDuration( type: TaskType, hopCount: number, lead: CrewMember, helpers: CrewMember[], powerOf: (m: CrewMember) => number = () => 1, ): number { const totalPower = Math.max(1, [lead, ...helpers].reduce((sum, m) => sum + powerOf(m), 0)) const mismatchPenalty = lead.traitGood === MISMATCH_CANCEL_TRAIT || PREFERRED_ROLES[type].includes(lead.role) ? 0 : 1 const traitModifierSum = traitModifier(lead.traitGood, type) + traitModifier(lead.traitBad, type) return Math.max(1, Math.ceil(workPoints(type, hopCount) / totalPower) + mismatchPenalty + traitModifierSum) }