import { ChiRenAI, EnemyManager, HeiRenAI, QingRenAI, YaoFangAI } from '@logic/EnemyAI'; import { EnemyType, IEnemyConfig } from '@data/Interfaces'; function cfg(id: EnemyType, intervalSec: number, speed = 0): IEnemyConfig { return { id, displayName: id, size: { w: 16, h: 16 }, moveSpeed: speed, attackIntervalSec: intervalSec, attacks: ['shuriken'], hp: 1, }; } const idlePlayer = { x: 300, y: 16, isGrounded: true }; describe('QingRenAI (req 6.1)', () => { it('throws a shuriken at the player when out of melee range', () => { const ai = new QingRenAI(cfg(EnemyType.QingRen, 2.0), 0, 16); const actions = ai.update({ dtSec: 2.0, nowMs: 0, player: idlePlayer }); expect(actions[0].kind).toBe('fire_bullet'); expect(actions[0].attackType).toBe('shuriken'); expect(actions[0].velX).toBeGreaterThan(0); }); it('melee swings when player is close', () => { const ai = new QingRenAI(cfg(EnemyType.QingRen, 2.0), 310, 16); const actions = ai.update({ dtSec: 2.0, nowMs: 0, player: idlePlayer }); expect(actions[0].kind).toBe('melee_swing'); }); it('respects attack interval — no burst within one tick', () => { const ai = new QingRenAI(cfg(EnemyType.QingRen, 2.0), 0, 16); ai.update({ dtSec: 2.0, nowMs: 0, player: idlePlayer }); const actions = ai.update({ dtSec: 0.1, nowMs: 0, player: idlePlayer }); expect(actions.length).toBe(0); }); }); describe('ChiRenAI (req 6.2-6.3)', () => { it('moves horizontally toward the player at 120 px/s', () => { const ai = new ChiRenAI(cfg(EnemyType.ChiRen, 1.5, 120), 0, 16); ai.update({ dtSec: 1, nowMs: 0, player: idlePlayer }); expect(ai.pos.x).toBeGreaterThan(0); }); it('throws smoke bombs at the configured cadence', () => { const ai = new ChiRenAI(cfg(EnemyType.ChiRen, 1.5, 120), 0, 16); const actions = ai.update({ dtSec: 1.5, nowMs: 0, player: idlePlayer }); expect(actions.some((a) => a.attackType === 'smoke_bomb')).toBe(true); }); it('bumps upward when the player is idle within intercept range', () => { const ai = new ChiRenAI(cfg(EnemyType.ChiRen, 1.5, 120), 250, 16); const before = ai.pos.y; ai.update({ dtSec: 0.016, nowMs: 0, player: idlePlayer }); expect(ai.pos.y).toBeGreaterThan(before); }); }); describe('HeiRenAI (req 6.5)', () => { it('drops exactly one magic flute on kill', () => { const ai = new HeiRenAI(cfg(EnemyType.HeiRen, 2.5), 100, 16); const firstDrop = ai.onKilled(); const secondDrop = ai.onKilled(); expect(firstDrop[0].itemId).toBe('mo_di'); expect(secondDrop.length).toBe(0); }); }); describe('YaoFangAI (req 6.6)', () => { it('launches straight-line fireballs at 3s cadence', () => { const ai = new YaoFangAI(cfg(EnemyType.YaoFang, 3.0), 0, 16); const a = ai.update({ dtSec: 3.0, nowMs: 0, player: idlePlayer }); expect(a[0].attackType).toBe('fireball'); }); }); describe('EnemyManager culling (req 6.7)', () => { it('skips update for enemies outside the camera cull rect', () => { const mgr = new EnemyManager(); const far = new YaoFangAI(cfg(EnemyType.YaoFang, 3.0), 5000, 16); mgr.spawn(far); const actions = mgr.update(3.0, 0, idlePlayer, { leftX: 0, rightX: 960, topY: 540, bottomY: 0, }); expect(actions.length).toBe(0); }); it('updates enemies inside the cull rect', () => { const mgr = new EnemyManager(); const near = new YaoFangAI(cfg(EnemyType.YaoFang, 3.0), 200, 16); mgr.spawn(near); const actions = mgr.update(3.0, 0, idlePlayer, { leftX: 0, rightX: 960, topY: 540, bottomY: 0, }); expect(actions.length).toBe(1); expect(actions[0].kind).toBe('fire_bullet'); }); it('kill() converts Hei Ren to a magic-flute drop', () => { const mgr = new EnemyManager(); const hei = new HeiRenAI(cfg(EnemyType.HeiRen, 2.5), 50, 16); mgr.spawn(hei); const drops = mgr.kill(hei); expect(drops[0].itemId).toBe('mo_di'); expect(hei.alive).toBe(false); }); });