Files
KateLegend2_proj/tests/logic/PlayerStateMachine.test.ts
T
2026-06-07 22:10:03 +08:00

121 lines
4.3 KiB
TypeScript

import { PlayerStateMachine } from '@logic/PlayerStateMachine';
import { PlayerColorState, PLAYER_IFRAME_SECONDS } from '@common/Constants';
describe('PlayerStateMachine — auto-upgrade (req 5.1-5.2)', () => {
it('Red → Green on first crystal pickup', () => {
const sm = new PlayerStateMachine();
expect(sm.pickupCrystalJade()).toBe(PlayerColorState.Green);
});
it('Green → Yellow on second crystal pickup', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
expect(sm.pickupCrystalJade()).toBe(PlayerColorState.Yellow);
});
it('Yellow → Yellow on additional crystal pickups (no overflow)', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
sm.pickupCrystalJade();
expect(sm.pickupCrystalJade()).toBe(PlayerColorState.Yellow);
});
it('Zeng Wan adds one life (req 7.5)', () => {
const sm = new PlayerStateMachine(1);
expect(sm.pickupZengWan()).toBe(2);
});
});
describe('PlayerStateMachine — damage (req 5.3-5.6, 10.4-10.5)', () => {
it('downgrades from Yellow to Red on shuriken hit', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
sm.pickupCrystalJade();
const out = sm.takeHit('shuriken');
expect(out).toEqual({ kind: 'downgraded', from: PlayerColorState.Yellow, to: PlayerColorState.Red });
expect(sm.color).toBe(PlayerColorState.Red);
});
it('downgrades from Green to Red on sword hit', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
const out = sm.takeHit('sword');
expect(out.kind).toBe('downgraded');
expect(sm.color).toBe(PlayerColorState.Red);
});
it('Red + shuriken → life loss (downgraded) when lives > 1', () => {
const sm = new PlayerStateMachine(2);
const out = sm.takeHit('shuriken');
expect(out.kind).toBe('downgraded');
expect(sm.lives).toBe(1);
expect(sm.isDead).toBe(false);
});
it('fireball reduces one life regardless of color', () => {
const sm = new PlayerStateMachine(2);
sm.pickupCrystalJade();
sm.pickupCrystalJade();
const out = sm.takeHit('fireball');
expect(out.kind).toBe('downgraded');
expect(sm.lives).toBe(1);
expect(sm.isDead).toBe(false);
});
it('smoke bomb is always lethal', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
const out = sm.takeHit('smoke_bomb');
expect(out.kind).toBe('died');
});
it('zero lives → isDead=true', () => {
const sm = new PlayerStateMachine(1);
sm.takeHit('sword');
expect(sm.lives).toBe(0);
expect(sm.isDead).toBe(true);
});
});
describe('PlayerStateMachine — iframes & sword parry (req 3.7-3.8, 10.2-10.3)', () => {
it('i-frames start at PLAYER_IFRAME_SECONDS after a hit', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
sm.takeHit('shuriken');
expect(sm.snapshot.iframeSec).toBe(PLAYER_IFRAME_SECONDS);
});
it('hits during i-frames are ignored', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
sm.takeHit('shuriken');
const out = sm.takeHit('shuriken');
expect(out).toEqual({ kind: 'no_effect', reason: 'iframe' });
});
it('tick() drains i-frames so the player is vulnerable again', () => {
const sm = new PlayerStateMachine();
sm.pickupCrystalJade();
sm.takeHit('shuriken');
sm.tick(PLAYER_IFRAME_SECONDS + 0.01);
expect(sm.snapshot.iframeSec).toBe(0);
});
it('sword parry nullifies shuriken/sword damage', () => {
const sm = new PlayerStateMachine();
sm.setSwordActive(true);
const out = sm.takeHit('shuriken');
expect(out).toEqual({ kind: 'no_effect', reason: 'parried' });
expect(sm.color).toBe(PlayerColorState.Red);
});
it('sword parry does NOT nullify fireball / smoke_bomb (req 3.8, 10.4-10.5)', () => {
const sm = new PlayerStateMachine();
sm.setSwordActive(true);
expect(sm.takeHit('fireball').kind).toBe('died');
const sm2 = new PlayerStateMachine();
sm2.setSwordActive(true);
expect(sm2.takeHit('smoke_bomb').kind).toBe('died');
});
});