import { describe, expect, it } from 'vitest' import type { SkillName } from './crew-costs' import { grantXp, levelFromXp, powerForLevel, titleFor, type SkillRow } from './skills' const pilot = { name: 'BEN OKONKWO', role: 'PILOT' } const row = (crewId: string, skill: SkillName, level: number, xp: number): SkillRow => ({ saveId: 's1', crewId, skill, level, xp }) const XP: [number, number][] = [[4, 0], [5, 1], [10, 2], [15, 3], [25, 5], [100, 5]] describe('levelFromXp (thresholds [5,10,15,20,25], cap 5)', () => { it('maps cumulative xp → highest level reached', () => { for (const [xp, lvl] of XP) expect(levelFromXp(xp), `xp ${xp}`).toBe(lvl) }) }) const POWER: [number, number][] = [[0, 1], [2, 1], [3, 2], [4, 2], [5, 3]] describe('powerForLevel (bands [1,1,1,2,2,3])', () => { it('L0–2 → 1 · L3–4 → 2 · L5 → 3', () => { for (const [lvl, p] of POWER) expect(powerForLevel(lvl), `L${lvl}`).toBe(p) }) }) describe('grantXp — +1 XP to every listed member; event only on a titled-band crossing', () => { const pilotRows = (): SkillRow[] => [ row('c1', 'NAVIGATION', 2, 14), row('c1', 'SCANNING', 0, 0), row('c1', 'ENGINEERING', 0, 0), row('c1', 'RESEARCH', 0, 0), ] it('L0 → L1: xp 4 +1 → 5 (L1), no event (no event for plain L1/L2)', () => { const { rows, events } = grantXp([row('c1', 'NAVIGATION', 0, 4)], ['c1'], 'NAVIGATION', { c1: pilot }) expect(rows[0]).toEqual({ saveId: 's1', crewId: 'c1', skill: 'NAVIGATION', level: 1, xp: 5 }) expect(events).toEqual([]) }) it('L2 → L3: xp 14 +1 → 15, SENIOR event in the exact spec §6 text', () => { const { rows, events } = grantXp(pilotRows(), ['c1'], 'NAVIGATION', { c1: pilot }) expect(rows.find((r) => r.skill === 'NAVIGATION')!.level).toBe(3) expect(events).toEqual(['BEN OKONKWO is now a SENIOR PILOT — NAVIGATION L3']) }) it('L4 → L5: xp 24 +1 → 25, MASTER event', () => { const { events } = grantXp([row('c1', 'NAVIGATION', 4, 24), row('c1', 'SCANNING', 0, 0)], ['c1'], 'NAVIGATION', { c1: pilot }) expect(events).toEqual(['BEN OKONKWO is now a MASTER PILOT — NAVIGATION L5']) }) it('no event when a member is already at the titled level (stays at L3 after gaining xp)', () => { // Member at SCANNING L3 (xp 16) gains 1 → xp 17, still L3 → NO event const rows = [row('c1', 'SCANNING', 3, 16), row('c1', 'NAVIGATION', 0, 0)] const { events } = grantXp(rows, ['c1'], 'SCANNING', { c1: { name: 'X', role: 'SCIENTIST' } }) expect(events).toHaveLength(0) }) }) describe('titleFor — from the member\'s HIGHEST skill level', () => { it('SCIENTIST with SCANNING L3 + RESEARCH L5 → MASTER SCIENTIST (highest wins)', () => { const scientist = { name: 'AVA VANCE', role: 'SCIENTIST' } const rows = [row('c2', 'SCANNING', 3, 15), row('c2', 'RESEARCH', 5, 25), row('c2', 'NAVIGATION', 0, 0), row('c2', 'ENGINEERING', 0, 0)] expect(titleFor(scientist, rows)).toBe('MASTER SCIENTIST') }) it('GUNNER with all L0 → null', () => { const gunner = { name: 'RIG HOLLOWAY', role: 'GUNNER' } const rows = [row('c3', 'NAVIGATION', 0, 0), row('c3', 'SCANNING', 0, 0), row('c3', 'ENGINEERING', 0, 0), row('c3', 'RESEARCH', 0, 0)] expect(titleFor(gunner, rows)).toBe(null) }) })