- ProjectileSprite.js: physics arcade sprite, faction tint, off-screen culling - CombatSystem: refactored enemy selection to use TeamManager instead of legacy containers - Death handling: DYING alpha tween (500ms), smoke puff (300ms), unit:killed event, cleanup - TeamManager: centralized team registry replacing goodGuys/badGuys containers - HealthBarSystem, ResourceBar, CaptureProgressUI, BuildMenu, BuildingPlacer, BuildingRenderer, ProductionPanel - Map_Player: wired new subsystems, removed legacy container creation - Tests: ProjectileSprite (4), DeathHandling (13), CombatSystem updated 47 tests passed at dev time (M2.3), 158/158 at dev time (M2.4)
233 lines
7.4 KiB
JavaScript
233 lines
7.4 KiB
JavaScript
/**
|
|
* WinCondition Unit Tests
|
|
*/
|
|
|
|
// Minimal Phaser Events mock
|
|
const createMockEventEmitter = () => ({
|
|
emit: jest.fn(),
|
|
on: jest.fn(),
|
|
off: jest.fn(),
|
|
once: jest.fn(),
|
|
});
|
|
|
|
// Minimal EconomySystem mock
|
|
const createMockEconomySystem = (playerResources = {}) => {
|
|
const players = new Map();
|
|
for (const [pid, res] of Object.entries(playerResources)) {
|
|
players.set(pid, res);
|
|
}
|
|
return {
|
|
events: createMockEventEmitter(),
|
|
players,
|
|
getResources: jest.fn((playerId) => players.get(playerId) ?? null),
|
|
};
|
|
};
|
|
|
|
const createMockScene = () => ({
|
|
events: createMockEventEmitter(),
|
|
time: { elapsed: 0 },
|
|
});
|
|
|
|
import WinCondition from '../src/systems/WinCondition';
|
|
|
|
describe('WinCondition', () => {
|
|
let scene;
|
|
let economy;
|
|
let winCondition;
|
|
|
|
beforeEach(() => {
|
|
scene = createMockScene();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (winCondition) winCondition.destroy();
|
|
});
|
|
|
|
describe('initialization', () => {
|
|
it('should set threshold to 100 by default', () => {
|
|
economy = createMockEconomySystem();
|
|
winCondition = new WinCondition(scene, economy);
|
|
expect(winCondition.victoryThreshold).toBe(100);
|
|
});
|
|
|
|
it('should accept custom threshold', () => {
|
|
economy = createMockEconomySystem();
|
|
winCondition = new WinCondition(scene, economy, { threshold: 50 });
|
|
expect(winCondition.victoryThreshold).toBe(50);
|
|
});
|
|
|
|
it('should track game start time', () => {
|
|
economy = createMockEconomySystem();
|
|
const before = Date.now();
|
|
winCondition = new WinCondition(scene, economy);
|
|
const after = Date.now();
|
|
expect(winCondition.stats.gameStartTime).toBeGreaterThanOrEqual(before);
|
|
expect(winCondition.stats.gameStartTime).toBeLessThanOrEqual(after);
|
|
});
|
|
|
|
it('should register combat:unitDamaged listener on economy events', () => {
|
|
economy = createMockEconomySystem();
|
|
const onSpy = jest.spyOn(economy.events, 'on');
|
|
winCondition = new WinCondition(scene, economy);
|
|
expect(onSpy).toHaveBeenCalledWith('combat:unitDamaged', expect.any(Function));
|
|
});
|
|
});
|
|
|
|
describe('victory detection', () => {
|
|
it('should emit game:victory when a player reaches threshold', () => {
|
|
economy = createMockEconomySystem({
|
|
'player1': { fuel: 100, ammo: 100, capturePoints: 100 },
|
|
});
|
|
winCondition = new WinCondition(scene, economy);
|
|
const emitSpy = jest.spyOn(scene.events, 'emit');
|
|
|
|
winCondition.update(16000); // simulate 16s elapsed
|
|
|
|
expect(emitSpy).toHaveBeenCalledWith(
|
|
'game:victory',
|
|
expect.objectContaining({
|
|
winnerPlayerId: 'player1',
|
|
stats: expect.objectContaining({
|
|
unitsKilled: expect.any(Number),
|
|
buildingsBuilt: expect.any(Number),
|
|
cpCaptured: expect.any(Number),
|
|
elapsedMs: expect.any(Number),
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should emit victory only once per game', () => {
|
|
economy = createMockEconomySystem({
|
|
'player1': { fuel: 100, ammo: 100, capturePoints: 100 },
|
|
});
|
|
winCondition = new WinCondition(scene, economy);
|
|
const emitSpy = jest.spyOn(scene.events, 'emit');
|
|
|
|
winCondition.update(16000);
|
|
winCondition.update(16016);
|
|
winCondition.update(16033);
|
|
|
|
const victoryCalls = emitSpy.mock.calls.filter(
|
|
(call) => call[0] === 'game:victory'
|
|
);
|
|
expect(victoryCalls.length).toBe(1);
|
|
});
|
|
|
|
it('should not emit victory when no player has reached threshold', () => {
|
|
economy = createMockEconomySystem({
|
|
'player1': { fuel: 100, ammo: 100, capturePoints: 99 },
|
|
});
|
|
winCondition = new WinCondition(scene, economy);
|
|
const emitSpy = jest.spyOn(scene.events, 'emit');
|
|
|
|
winCondition.update(16000);
|
|
|
|
expect(emitSpy).not.toHaveBeenCalledWith('game:victory', expect.anything());
|
|
expect(winCondition.victoryEmitted).toBe(false);
|
|
});
|
|
|
|
it('should detect victory for the correct player when multiple exist', () => {
|
|
economy = createMockEconomySystem({
|
|
'player1': { fuel: 100, ammo: 100, capturePoints: 50 },
|
|
'player2': { fuel: 100, ammo: 100, capturePoints: 100 },
|
|
});
|
|
winCondition = new WinCondition(scene, economy);
|
|
const emitSpy = jest.spyOn(scene.events, 'emit');
|
|
|
|
winCondition.update(16000);
|
|
|
|
const call = emitSpy.mock.calls.find((c) => c[0] === 'game:victory');
|
|
expect(call[1].winnerPlayerId).toBe('player2');
|
|
});
|
|
});
|
|
|
|
describe('stats tracking', () => {
|
|
it('should increment unitsKilled on combat:unitDamaged when target dies', () => {
|
|
economy = createMockEconomySystem();
|
|
winCondition = new WinCondition(scene, economy);
|
|
|
|
economy.events.on.mock.calls
|
|
.filter((c) => c[0] === 'combat:unitDamaged')
|
|
.forEach((c) => {
|
|
const handler = c[1];
|
|
// simulate two units dying
|
|
handler({ target: { dead: true }, damage: 20 });
|
|
handler({ target: { dead: true }, damage: 15 });
|
|
});
|
|
|
|
expect(winCondition.stats.unitsKilled).toBe(2);
|
|
});
|
|
|
|
it('should not increment unitsKilled when target does not die', () => {
|
|
economy = createMockEconomySystem();
|
|
winCondition = new WinCondition(scene, economy);
|
|
|
|
economy.events.on.mock.calls
|
|
.filter((c) => c[0] === 'combat:unitDamaged')
|
|
.forEach((c) => {
|
|
const handler = c[1];
|
|
handler({ target: { dead: false, getData: () => 50 }, damage: 10 });
|
|
});
|
|
|
|
expect(winCondition.stats.unitsKilled).toBe(0);
|
|
});
|
|
|
|
it('should increment buildingsBuilt on building:spawned', () => {
|
|
economy = createMockEconomySystem();
|
|
winCondition = new WinCondition(scene, economy);
|
|
|
|
scene.events.on.mock.calls
|
|
.filter((c) => c[0] === 'building:spawned')
|
|
.forEach((c) => {
|
|
const handler = c[1];
|
|
handler({});
|
|
handler({});
|
|
});
|
|
|
|
expect(winCondition.stats.buildingsBuilt).toBe(2);
|
|
});
|
|
|
|
it('should increment cpCaptured on economy:incomeReceived with capturePoints', () => {
|
|
economy = createMockEconomySystem();
|
|
winCondition = new WinCondition(scene, economy);
|
|
|
|
economy.events.on.mock.calls
|
|
.filter((c) => c[0] === 'economy:incomeReceived')
|
|
.forEach((c) => {
|
|
const handler = c[1];
|
|
handler({ income: { capturePoints: 2 }, resources: { capturePoints: 10 } });
|
|
handler({ income: { capturePoints: 3 }, resources: { capturePoints: 13 } });
|
|
});
|
|
|
|
expect(winCondition.stats.cpCaptured).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe('elapsed time', () => {
|
|
it('should calculate elapsed time from game start to victory', () => {
|
|
economy = createMockEconomySystem({
|
|
'player1': { fuel: 100, ammo: 100, capturePoints: 100 },
|
|
});
|
|
winCondition = new WinCondition(scene, economy);
|
|
const emitSpy = jest.spyOn(scene.events, 'emit');
|
|
|
|
winCondition.update(120000); // 2 minutes
|
|
|
|
const call = emitSpy.mock.calls.find((c) => c[0] === 'game:victory');
|
|
// 120 seconds in ms
|
|
expect(call[1].stats.elapsedMs).toBe(120000);
|
|
});
|
|
});
|
|
|
|
describe('cleanup', () => {
|
|
it('should remove listeners on destroy', () => {
|
|
economy = createMockEconomySystem();
|
|
const offSpy = jest.spyOn(economy.events, 'off');
|
|
winCondition = new WinCondition(scene, economy);
|
|
winCondition.destroy();
|
|
expect(offSpy).toHaveBeenCalledWith('combat:unitDamaged', expect.any(Function));
|
|
});
|
|
});
|
|
});
|