Files
KateLegend2_proj/tests/logic/AttackController.test.ts
T
2026-05-06 08:17:32 +08:00

107 lines
4.1 KiB
TypeScript

import { AttackController, IJumpStateProvider } from '@logic/AttackController';
import { WeaponType } from '@data/Interfaces';
import { PlayerColorState } from '@common/Constants';
function makeJumpState(ts?: number): IJumpStateProvider {
return {
lastJumpPressTs: () => ts,
isGrounded: () => ts === undefined,
};
}
describe('AttackController — mutual exclusion (req 3.1-3.3)', () => {
it('first-pressed weapon wins when both buttons go down together', () => {
const ac = new AttackController();
ac.press(WeaponType.Shuriken, 10);
ac.press(WeaponType.NinjaSword, 11);
expect(ac.getActive()).toBe(WeaponType.Shuriken);
});
it('releasing the active weapon transfers activation to the still-held one', () => {
const ac = new AttackController();
ac.press(WeaponType.Shuriken, 0);
ac.press(WeaponType.NinjaSword, 10);
ac.release(WeaponType.Shuriken);
expect(ac.getActive()).toBe(WeaponType.NinjaSword);
});
it('releasing the only pressed weapon deactivates everything', () => {
const ac = new AttackController();
ac.press(WeaponType.Shuriken, 0);
ac.release(WeaponType.Shuriken);
expect(ac.getActive()).toBe('none');
});
});
describe('AttackController — firing intervals (req 3.4, 3.6)', () => {
it('shuriken fires every 300ms for red/green, 250ms for yellow', () => {
const ac = new AttackController();
ac.press(WeaponType.Shuriken, 0);
expect(ac.tick(0, PlayerColorState.Red).length).toBe(1);
expect(ac.tick(299, PlayerColorState.Red).length).toBe(0);
expect(ac.tick(300, PlayerColorState.Red).length).toBe(1);
const fast = new AttackController();
fast.press(WeaponType.Shuriken, 0);
fast.tick(0, PlayerColorState.Yellow);
expect(fast.tick(249, PlayerColorState.Yellow).length).toBe(0);
expect(fast.tick(250, PlayerColorState.Yellow).length).toBe(1);
});
it('sword fires every 500ms', () => {
const ac = new AttackController();
ac.press(WeaponType.NinjaSword, 0);
ac.tick(0, PlayerColorState.Red);
expect(ac.tick(499, PlayerColorState.Red).length).toBe(0);
expect(ac.tick(500, PlayerColorState.Red).length).toBe(1);
});
it('shuriken burst index caps at SHURIKEN_BURST_MAX', () => {
const ac = new AttackController();
ac.press(WeaponType.Shuriken, 0);
const indexes: number[] = [];
for (let i = 0; i <= 600; i += 300) {
const fires = ac.tick(i, PlayerColorState.Red);
if (fires.length) indexes.push(fires[0].burstIndex);
}
expect(indexes).toEqual([1, 2, 3]);
// One more attempt must still cap at 3 not 4.
const more = ac.tick(900, PlayerColorState.Red);
expect(more[0].burstIndex).toBe(3);
});
});
describe('AttackController — combo window (req 4.1)', () => {
it('comboWithJump is true when jump timestamp is within 100ms', () => {
const ac = new AttackController(makeJumpState(95));
ac.press(WeaponType.Shuriken, 100);
const fires = ac.tick(100, PlayerColorState.Red);
expect(fires[0].comboWithJump).toBe(true);
});
it('comboWithJump is false when jump was pressed >100ms ago', () => {
const ac = new AttackController(makeJumpState(0));
ac.press(WeaponType.Shuriken, 200);
const fires = ac.tick(200, PlayerColorState.Red);
expect(fires[0].comboWithJump).toBe(false);
});
it('comboWithJump is false when the player has not jumped yet', () => {
const ac = new AttackController(makeJumpState(undefined));
ac.press(WeaponType.Shuriken, 0);
const fires = ac.tick(0, PlayerColorState.Red);
expect(fires[0].comboWithJump).toBe(false);
});
});
describe('AttackController — reset()', () => {
it('clears active / pressed / cooldowns', () => {
const ac = new AttackController();
ac.press(WeaponType.Shuriken, 0);
ac.tick(0, PlayerColorState.Red);
ac.reset();
expect(ac.getActive()).toBe('none');
expect(ac.isPressed(WeaponType.Shuriken)).toBe(false);
});
});