feat(game): skill XP on task completion + power bands in duration (#45)
CI / build + unit + e2e (pull_request) Successful in 2m48s

This commit is contained in:
2026-10-04 02:34:56 +00:00
parent 5bc411cb76
commit f3751b4747
13 changed files with 209 additions and 26 deletions
+9 -5
View File
@@ -51,9 +51,10 @@ function traitModifier(trait: string, type: TaskType): number {
// 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).
// constant 1 — queueTask's optional powerOf param feeds the skill band instead (PR B). 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,
@@ -119,7 +120,10 @@ export interface QueueResult {
// Queue a task. Validation order: unknown crew (404) → busy (409) → in transit (409, all
// types) → second jump (409) → cost (422, SHIP_* / RESEARCH only). Never advances the turn.
export function queueTask(state: QueueState, req: QueueTaskRequest): QueueResult {
// The optional powerOf (default: the constant 1) maps each team member to their power for
// the stored duration — the queueing routes pass makePowerOf over the roster's skill rows
// (PR B); callers without a skill context (tests, the immediate actions) use the default.
export function queueTask(state: QueueState, req: QueueTaskRequest, powerOf: (m: CrewMember) => number = () => 1): QueueResult {
const lead = state.crew.find((c) => c.id === req.leadCrewId)
if (!lead) throw new TaskError(404, `Unknown crew member ${req.leadCrewId}`)
const helpers: CrewDto[] = []
@@ -168,7 +172,7 @@ export function queueTask(state: QueueState, req: QueueTaskRequest): QueueResult
// lead chaining behind its own ACTIVE can be non-free.
const free = (id: number) => !inStatus(id, 'ACTIVE') && !inStatus(id, 'PENDING')
const hops = req.type === 'JUMP' ? (req.payload.hops as number) : 0
const duration = computeDuration(req.type, hops, lead, helpers)
const duration = computeDuration(req.type, hops, lead, helpers, powerOf)
const status: TaskStatus = [lead, ...helpers].every((c) => free(c.id)) ? 'ACTIVE' : 'PENDING'
return {