// 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' export type TaskStatus = 'ACTIVE' | 'PENDING' | 'DONE' // One queued/active/done action (spec §3). `payload` is a JSON string whose shape depends on // the TaskType: JUMP {systemId, hops} · SHIP_UPGRADE/EXPAND {systemId} · SHIP_BUILD {type} · // RESEARCH {techId} · EXPLORE {planetId}. export interface Task { id: number saveId: number type: TaskType payload: string leadCrewId: number helpers: number[] duration: number remaining: number status: TaskStatus queuedAtTurn: number } // 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) }