import { TutorialMgr } from '@logic/TutorialMgr'; import { ScoreSystem, BASE_ENEMY_SCORE, COMBO_BONUS } from '@logic/ScoreSystem'; import { WeaponType } from '@data/Interfaces'; import { StorageMgr } from '@common/StorageMgr'; function mem() { const m = new Map(); return { getItem: (k: string) => (m.has(k) ? (m.get(k) as string) : null), setItem: (k: string, v: string) => { m.set(k, v); }, removeItem: (k: string) => { m.delete(k); }, }; } describe('TutorialMgr — built-in sequences for 1-1..1-3 (req 11.1-11.3)', () => { it('maybeStart returns the first step of 1-1', () => { const t = new TutorialMgr(new StorageMgr(mem())); const step = t.maybeStart('1-1'); expect(step).toBeDefined(); expect(step!.id).toBe('attack'); }); it('reportAction advances through the sequence', () => { const t = new TutorialMgr(new StorageMgr(mem())); t.maybeStart('1-1'); expect(t.reportAction('fire_shuriken')).toMatchObject({ id: 'joystick' }); expect(t.reportAction('move')).toMatchObject({ id: 'jump' }); expect(t.reportAction('jump')).toBe('finished'); }); it('completed tutorials are persisted and skipped on replay (req 11.4)', () => { const storage = new StorageMgr(mem()); const t = new TutorialMgr(storage); t.maybeStart('1-1'); t.reportAction('fire_shuriken'); t.reportAction('move'); t.reportAction('jump'); expect(t.isCompleted('1-1')).toBe(true); expect(t.maybeStart('1-1')).toBeNull(); }); it('resetAll clears the completion set (req 11.5)', () => { const t = new TutorialMgr(new StorageMgr(mem())); t.maybeStart('1-2'); t.reportAction('parabolic_jump'); t.reportAction('attack_switch'); t.reportAction('parry'); t.reportAction('jump_attack'); t.reportAction('pickup_crystal'); t.resetAll(); expect(t.maybeStart('1-2')).toBeDefined(); }); it('reportAction with wrong action is a no-op', () => { const t = new TutorialMgr(new StorageMgr(mem())); t.maybeStart('1-1'); expect(t.reportAction('wrong')).toBe('no_op'); expect(t.currentStep()!.id).toBe('attack'); }); }); describe('ScoreSystem — scoring table (req 12.1-12.6)', () => { it('sword kill is ×2, shuriken kill is ×1', () => { const s = new ScoreSystem(); s.recordEnemyKill(WeaponType.NinjaSword); s.recordEnemyKill(WeaponType.Shuriken); expect(s.snapshot().baseScore).toBe(BASE_ENEMY_SCORE * 3); }); it('parry kill is ×3 base', () => { const s = new ScoreSystem(); s.recordParryKill(); expect(s.snapshot().baseScore).toBe(BASE_ENEMY_SCORE * 3); }); it('5 blade contacts award a +1500 combo bonus (req 12.4)', () => { const s = new ScoreSystem(); for (let i = 0; i < 5; i++) s.recordBladeContact(); const snap = s.snapshot(); expect(snap.comboBonus).toBe(COMBO_BONUS); expect(snap.comboCount).toBe(1); expect(snap.consecutiveBladeHits).toBe(0); }); it('breakBladeChain resets the streak', () => { const s = new ScoreSystem(); s.recordBladeContact(); s.recordBladeContact(); s.breakBladeChain(); s.recordBladeContact(); expect(s.snapshot().consecutiveBladeHits).toBe(1); }); it('flawless run triples total score; taking damage removes the bonus (req 12.5)', () => { const s = new ScoreSystem(); s.recordEnemyKill(WeaponType.Shuriken); const flawlessSnap = s.snapshot(); expect(flawlessSnap.flawlessMultiplier).toBe(3); s.markTaken(); const damagedSnap = s.snapshot(); expect(damagedSnap.flawlessMultiplier).toBe(1); }); it('remaining time bonus adds 10 pts / sec (req 12.6)', () => { const s = new ScoreSystem(); s.recordEnemyKill(WeaponType.Shuriken); s.setRemainingTimeBonus(30); expect(s.snapshot().timeBonus).toBe(300); }); });