- 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)
169 lines
5.3 KiB
JavaScript
169 lines
5.3 KiB
JavaScript
/**
|
|
* BuildMenu.test.js — S5.1: Build menu HUD
|
|
*
|
|
* Tests:
|
|
* 1. Constructor creates a container with 4 building buttons fixed to camera
|
|
* 2. Each button shows building label and cost text
|
|
* 3. Clicking a button calls onSelect callback with building type
|
|
* 4. updateAffordability disables buttons when player can't afford
|
|
* 5. updateAffordability re-enables buttons when player can afford again
|
|
* 6. destroy cleans up all Phaser objects
|
|
*/
|
|
|
|
import BuildMenu from 'Systems/BuildMenu';
|
|
|
|
function buildMockScene() {
|
|
const objects = [];
|
|
const inputOnHandlers = [];
|
|
|
|
return {
|
|
add: {
|
|
container: jest.fn((x, y) => {
|
|
const c = {
|
|
x, y,
|
|
list: [],
|
|
add: jest.fn(function (obj) { this.list.push(obj); return this; }),
|
|
setPosition: jest.fn().mockReturnThis(),
|
|
setVisible: jest.fn().mockReturnThis(),
|
|
setScrollFactor: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
active: true,
|
|
};
|
|
objects.push(c);
|
|
return c;
|
|
}),
|
|
rectangle: jest.fn((x, y, w, h) => {
|
|
const r = {
|
|
x, y, width: w, height: h,
|
|
setOrigin: jest.fn().mockReturnThis(),
|
|
setStrokeStyle: jest.fn().mockReturnThis(),
|
|
setInteractive: jest.fn().mockReturnThis(),
|
|
setFillStyle: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
on: jest.fn().mockReturnThis(),
|
|
off: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
active: true,
|
|
};
|
|
objects.push(r);
|
|
return r;
|
|
}),
|
|
text: jest.fn((x, y, text, style) => {
|
|
const t = {
|
|
x, y, text,
|
|
style,
|
|
setOrigin: jest.fn().mockReturnThis(),
|
|
setScrollFactor: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
setText: jest.fn(function (v) { this.text = v; return this; }),
|
|
setColor: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
active: true,
|
|
};
|
|
objects.push(t);
|
|
return t;
|
|
}),
|
|
},
|
|
cameras: {
|
|
main: { width: 1280, height: 720 },
|
|
},
|
|
input: {
|
|
on: jest.fn((evt, cb) => inputOnHandlers.push({ evt, cb })),
|
|
},
|
|
_objects: objects,
|
|
_inputHandlers: inputOnHandlers,
|
|
};
|
|
}
|
|
|
|
describe('BuildMenu', () => {
|
|
let scene;
|
|
let menu;
|
|
let onSelect;
|
|
|
|
beforeEach(() => {
|
|
scene = buildMockScene();
|
|
onSelect = jest.fn();
|
|
menu = new BuildMenu(scene, { onSelect, playerId: 'Player' });
|
|
});
|
|
|
|
afterEach(() => {
|
|
menu?.destroy?.();
|
|
});
|
|
|
|
// -- 1. Constructor -------------------------------------------------
|
|
test('constructor creates a container fixed to camera', () => {
|
|
expect(scene.add.container).toHaveBeenCalled();
|
|
expect(menu.container).toBeDefined();
|
|
expect(menu.container.setScrollFactor).toHaveBeenCalledWith(0, 0);
|
|
});
|
|
|
|
test('container has 4 building buttons', () => {
|
|
expect(menu.buttons).toBeDefined();
|
|
expect(menu.buttons.length).toBe(4);
|
|
});
|
|
|
|
// -- 2. Button labels + cost --------------------------------------
|
|
test('each button has a text label', () => {
|
|
const labels = menu.buttons.map((b) => b.label?.text);
|
|
expect(labels).toContain('Barracks');
|
|
expect(labels).toContain('Vehicle Depot');
|
|
expect(labels).toContain('Logistics Center');
|
|
expect(labels).toContain('Ammunition Factory');
|
|
});
|
|
|
|
test('each button shows its resource cost', () => {
|
|
const costs = menu.buttons.map((b) => b.costText?.text);
|
|
expect(costs.some((c) => c && c.includes('Ammo'))).toBe(true);
|
|
expect(costs.some((c) => c && c.includes('Fuel'))).toBe(true);
|
|
});
|
|
|
|
// -- 3. Clicking a button -----------------------------------------
|
|
test('clicking a button calls onSelect with building type', () => {
|
|
const barracksBtn = menu.buttons.find((b) => b.type === 'BARRACKS');
|
|
expect(barracksBtn).toBeDefined();
|
|
|
|
// Simulate the rectangle.on('pointerdown') callback
|
|
const pointerdownCall = barracksBtn.bg.on.mock.calls.find((c) => c[0] === 'pointerdown');
|
|
expect(pointerdownCall).toBeDefined();
|
|
|
|
pointerdownCall[1]();
|
|
expect(onSelect).toHaveBeenCalledWith('BARRACKS');
|
|
});
|
|
|
|
// -- 4. updateAffordability (disabled) -----------------------------
|
|
test('updateAffordability disables buttons player cannot afford', () => {
|
|
const economy = {
|
|
getResources: jest.fn(() => ({ fuel: 0, ammo: 0 })),
|
|
canAfford: jest.fn(() => false),
|
|
};
|
|
menu.updateAffordability(economy, 'Player');
|
|
|
|
menu.buttons.forEach((btn) => {
|
|
expect(btn.bg.setAlpha).toHaveBeenCalledWith(0.4);
|
|
});
|
|
});
|
|
|
|
// -- 5. updateAffordability (re-enabled) --------------------------
|
|
test('updateAffordability enables affordable buttons', () => {
|
|
const economy = {
|
|
getResources: jest.fn(() => ({ fuel: 200, ammo: 200 })),
|
|
canAfford: jest.fn(() => true),
|
|
};
|
|
menu.updateAffordability(economy, 'Player');
|
|
|
|
menu.buttons.forEach((btn) => {
|
|
expect(btn.bg.setAlpha).toHaveBeenCalledWith(1);
|
|
});
|
|
});
|
|
|
|
// -- 6. destroy ----------------------------------------------------
|
|
test('destroy cleans up container and buttons', () => {
|
|
const containerDestroy = menu.container.destroy;
|
|
menu.destroy();
|
|
expect(containerDestroy).toHaveBeenCalled();
|
|
expect(menu.buttons.length).toBe(0);
|
|
});
|
|
});
|