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
+16 -2
View File
@@ -1,6 +1,8 @@
// Pure skill-progression module (spec §6): XP accrual, level→power, titles. No lib/db imports — operates on plain SkillRow data (Task 12's crew_skills table); the frontend imports it for the CREW-tab display.
import type { SkillName } from './crew-costs'
import { POWER_BANDS, XP_THRESHOLDS } from './crew-costs'
import { ACTION_SKILL, POWER_BANDS, XP_THRESHOLDS, type SkillName } from './crew-costs'
import type { CrewMember } from './crew'
import type { CrewDto } from './save'
import type { TaskType } from './tasks'
export interface SkillRow {
saveId: string
crewId: string
@@ -51,3 +53,15 @@ export function grantXp(
}
return { rows, events }
}
// The duration formula's powerOf (Task 1) driven by the skill band (PR B, §6): each member's
// power is the band of that member's level in the action's skill (ACTION_SKILL[type]). Names
// map to crew ids (unique per single-player save); an unknown member or a missing row falls
// back to L0 → power 1 (Task 1's default). Called by save.ts's makePowerOfFor (the queueing
// routes) and — once /api/crew carries each member's skill rows — by the team builder's live
// preview (Task 15).
export function makePowerOf(roster: CrewDto[], skillRows: SkillRow[], type: TaskType): (m: CrewMember) => number {
const idByName = new Map(roster.map((c): [string, string] => [c.name, String(c.id)]))
const levelByCrew = new Map(skillRows.filter((r) => r.skill === ACTION_SKILL[type]).map((r): [string, number] => [r.crewId, r.level]))
return (m) => powerForLevel(levelByCrew.get(idByName.get(m.name) ?? '') ?? 0)
}