- 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)
229 lines
7.3 KiB
JavaScript
229 lines
7.3 KiB
JavaScript
/**
|
|
* VictoryScene Unit Tests
|
|
*/
|
|
|
|
// Minimal Phaser Scene mock
|
|
jest.mock('phaser', () => ({
|
|
Scene: class MockScene {
|
|
constructor(config) {
|
|
this.key = config?.key;
|
|
this.scene = {
|
|
start: jest.fn(),
|
|
stop: jest.fn(),
|
|
};
|
|
this.events = {
|
|
emit: jest.fn(),
|
|
on: jest.fn(),
|
|
};
|
|
this.add = {
|
|
text: jest.fn(() => mockTextObj()),
|
|
rectangle: jest.fn(() => mockRectObj()),
|
|
container: jest.fn(() => mockContainerObj()),
|
|
graphics: jest.fn(() => mockGraphicsObj()),
|
|
image: jest.fn(() => mockImageObj()),
|
|
};
|
|
this.cameras = {
|
|
main: { width: 1920, height: 1080, centerX: 960, centerY: 540 },
|
|
};
|
|
this.input = {
|
|
on: jest.fn(),
|
|
off: jest.fn(),
|
|
};
|
|
this.scale = {
|
|
baseSize: { width: 1920, height: 1080 },
|
|
};
|
|
this.game = {
|
|
loop: { delta: 1000 / 60 },
|
|
};
|
|
}
|
|
},
|
|
GameObjects: {
|
|
Text: class {},
|
|
Rectangle: class {},
|
|
Container: class {},
|
|
Graphics: class {},
|
|
},
|
|
Display: {
|
|
Color: {
|
|
GetColor: jest.fn(() => 0xffffff),
|
|
},
|
|
},
|
|
}));
|
|
|
|
function mockTextObj() {
|
|
const obj = {
|
|
setOrigin: jest.fn(() => obj),
|
|
setPosition: jest.fn(() => obj),
|
|
setScale: jest.fn(() => obj),
|
|
setAlpha: jest.fn(() => obj),
|
|
setInteractive: jest.fn(() => obj),
|
|
setDepth: jest.fn(() => obj),
|
|
setStyle: jest.fn(() => obj),
|
|
setText: jest.fn(() => obj),
|
|
setVisible: jest.fn(() => obj),
|
|
destroy: jest.fn(),
|
|
on: jest.fn(() => obj),
|
|
once: jest.fn(() => obj),
|
|
off: jest.fn(() => obj),
|
|
x: 0, y: 0,
|
|
active: true, visible: true,
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
function mockRectObj() {
|
|
const obj = {
|
|
setOrigin: jest.fn(() => obj),
|
|
setPosition: jest.fn(() => obj),
|
|
setFillStyle: jest.fn(() => obj),
|
|
setAlpha: jest.fn(() => obj),
|
|
setDepth: jest.fn(() => obj),
|
|
setInteractive: jest.fn(() => obj),
|
|
destroy: jest.fn(),
|
|
on: jest.fn(() => obj),
|
|
off: jest.fn(() => obj),
|
|
x: 0, y: 0,
|
|
active: true,
|
|
width: 1920, height: 1080,
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
function mockContainerObj() {
|
|
const obj = {
|
|
add: jest.fn(() => obj),
|
|
setPosition: jest.fn(() => obj),
|
|
setOrigin: jest.fn(() => obj),
|
|
setAlpha: jest.fn(() => obj),
|
|
setDepth: jest.fn(() => obj),
|
|
setVisible: jest.fn(() => obj),
|
|
destroy: jest.fn(),
|
|
x: 0, y: 0,
|
|
active: true, visible: true,
|
|
list: [],
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
function mockGraphicsObj() {
|
|
const obj = {
|
|
fillStyle: jest.fn(() => obj),
|
|
fillRect: jest.fn(() => obj),
|
|
setAlpha: jest.fn(() => obj),
|
|
setDepth: jest.fn(() => obj),
|
|
clear: jest.fn(() => obj),
|
|
destroy: jest.fn(),
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
function mockImageObj() {
|
|
const obj = {
|
|
setOrigin:
|
|
jest.fn(() => obj),
|
|
setPosition: jest.fn(() => obj),
|
|
setScale: jest.fn(() => obj),
|
|
setAlpha: jest.fn(() => obj),
|
|
setDepth: jest.fn(() => obj),
|
|
setInteractive: jest.fn(() => obj),
|
|
destroy: jest.fn(),
|
|
on: jest.fn(() => obj),
|
|
x: 0, y: 0,
|
|
active: true,
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
import VictoryScene from '../src/scenes/VictoryScene';
|
|
|
|
describe('VictoryScene', () => {
|
|
let scene;
|
|
|
|
beforeEach(() => {
|
|
scene = new VictoryScene();
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create dark overlay background', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
expect(scene.add.rectangle).toHaveBeenCalledWith(960, 540, 1920, 1080, 0x000000);
|
|
});
|
|
|
|
it('should show VICTORY when local player is winner', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasVictory = textCalls.some((c) => typeof c[2] === 'string' && c[2].includes('VICTORY'));
|
|
expect(hasVictory).toBe(true);
|
|
});
|
|
|
|
it('should show DEFEAT when local player is not winner', () => {
|
|
scene.create({ winnerPlayerId: 'player2', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasDefeat = textCalls.some((c) => typeof c[2] === 'string' && c[2].includes('DEFEAT'));
|
|
expect(hasDefeat).toBe(true);
|
|
});
|
|
|
|
it('should display elapsed time formatted as mm:ss', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 125000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasTime = textCalls.some((c) => {
|
|
const text = typeof c[2] === 'string' ? c[2] : c[2]?.text;
|
|
return typeof text === 'string' && /02:05/.test(text);
|
|
});
|
|
expect(hasTime).toBe(true);
|
|
});
|
|
|
|
it('should display unit kill count', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 7, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasKills = textCalls.some((c) => {
|
|
const text = typeof c[2] === 'string' ? c[2] : c[2]?.text;
|
|
return typeof text === 'string' && text.includes('7');
|
|
});
|
|
expect(hasKills).toBe(true);
|
|
});
|
|
|
|
it('should display buildings built count', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 4, cpCaptured: 100 } });
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasBuildings = textCalls.some((c) => {
|
|
const text = typeof c[2] === 'string' ? c[2] : c[2]?.text;
|
|
return typeof text === 'string' && text.includes('4');
|
|
});
|
|
expect(hasBuildings).toBe(true);
|
|
});
|
|
|
|
it('should display CP captured count', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasCp = textCalls.some((c) => {
|
|
const text = typeof c[2] === 'string' ? c[2] : c[2]?.text;
|
|
return typeof text === 'string' && text.includes('100');
|
|
});
|
|
expect(hasCp).toBe(true);
|
|
});
|
|
|
|
it('should create a Play Again button', () => {
|
|
scene.create({ winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } });
|
|
expect(scene.add.text).toHaveBeenCalled();
|
|
const textCalls = scene.add.text.mock.calls;
|
|
const hasPlayAgain = textCalls.some((c) => {
|
|
const text = typeof c[2] === 'string' ? c[2] : c[2]?.text;
|
|
return typeof text === 'string' && text.toLowerCase().includes('play again');
|
|
});
|
|
expect(hasPlayAgain).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('interaction', () => {
|
|
it('should launch Server_Connector scene on play again click', () => {
|
|
const data = { winnerPlayerId: 'player1', localPlayerId: 'player1', stats: { elapsedMs: 120000, unitsKilled: 5, buildingsBuilt: 2, cpCaptured: 100 } };
|
|
scene.create(data);
|
|
|
|
const startSpy = jest.spyOn(scene.scene, 'start');
|
|
scene._onPlayAgain();
|
|
expect(startSpy).toHaveBeenCalledWith('Server_Connector');
|
|
});
|
|
});
|
|
});
|