117 lines
4.3 KiB
TypeScript
117 lines
4.3 KiB
TypeScript
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);
|
|
});
|
|
});
|