first commmit

This commit is contained in:
jakciehan
2026-05-06 08:17:32 +08:00
commit 427a33c55b
269 changed files with 20906 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
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);
});
});