Files
SpaceWanderer/lib/game/skills.ts
T
kamilkrzywda 7f2121a717
CI / build + unit + e2e (pull_request) Successful in 2m53s
feat(game): skills.ts — XP, levels, power bands, titles (#45)
2026-10-04 01:33:05 +00:00

54 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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'
export interface SkillRow {
saveId: string
crewId: string
skill: SkillName
level: number
xp: number
}
export type CrewIdentity = { name: string; role: string }
// Highest level 0–5 with xp ≥ XP_THRESHOLDS[level−1]; thresholds [5,10,15,20,25], cap 5.
export function levelFromXp(xp: number): number {
let level = 0
for (let l = 1; l <= XP_THRESHOLDS.length; l++) if (xp >= XP_THRESHOLDS[l - 1]) level = l
return level
}
// L0–2 → 1 · L3–4 → 2 · L5 → 3 (direct lookup into crew-costs POWER_BANDS).
export function powerForLevel(level: number): number {
return POWER_BANDS[level]
}
// From the member's HIGHEST skill level: ≥5 → "MASTER <ROLE>", ≥3 → "SENIOR <ROLE>", below → null.
export function titleFor(member: CrewIdentity, skillRows: SkillRow[]): string | null {
const highest = skillRows.reduce((max, r) => Math.max(max, r.level), 0)
if (highest >= 5) return `MASTER ${member.role}`
if (highest >= 3) return `SENIOR ${member.role}`
return null
}
// +1 XP to every listed member's row for `skill`; returns updated rows + any level-up events. An event
// fires when the gained skill steps into a titled band (3 → SENIOR, 5 → MASTER), e.g. "BEN OKONKWO is now a SENIOR PILOT — NAVIGATION L3" (title from the member's HIGHEST skill).
export function grantXp(
skillRows: SkillRow[],
crewIds: string[],
skill: SkillName,
members: Record<string, CrewIdentity>,
): { rows: SkillRow[]; events: string[] } {
const rows = skillRows.map((r) =>
r.skill === skill && crewIds.includes(r.crewId)
? { ...r, xp: r.xp + 1, level: levelFromXp(r.xp + 1) }
: r,
)
const events: string[] = []
for (const crewId of crewIds) {
const member = members[crewId]
if (!member) continue
const newLevel = rows.find((r) => r.crewId === crewId && r.skill === skill)?.level ?? 0
if (newLevel === 3 || newLevel === 5) {
const title = titleFor(member, rows.filter((r) => r.crewId === crewId))
if (title) events.push(`${member.name} is now a ${title} — ${skill} L${newLevel}`)
}
}
return { rows, events }
}