66 lines
2.8 KiB
TypeScript
66 lines
2.8 KiB
TypeScript
// 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)
|
|
}
|