Files
restitution/tests/WinCondition.test.js
kaykayyali 8fc45968b5 M2.3 + M2.4 + TeamManager integration: projectile sprites, death handling, build menu, production panel, building placer
- 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)
2026-06-01 05:18:33 +00:00

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));
});
});
});