- 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)
188 lines
5.8 KiB
JavaScript
188 lines
5.8 KiB
JavaScript
/**
|
|
* ControlPointManager.test.js — Tests for placing CPs and wiring to EconomySystem.
|
|
*/
|
|
|
|
// Mock xstate before CPStateMachine imports it
|
|
jest.mock('xstate', () => ({
|
|
createMachine: jest.fn((config) => ({ config, id: config.id })),
|
|
interpret: jest.fn((machine) => ({
|
|
machine,
|
|
start: jest.fn(),
|
|
send: jest.fn(),
|
|
stop: jest.fn(),
|
|
state: {
|
|
value: 'NEUTRAL',
|
|
context: { owner: null, captureProgress: 0, unitsInRadius: {} },
|
|
},
|
|
})),
|
|
assign: jest.fn((fn) => fn),
|
|
}));
|
|
|
|
import ControlPointManager from 'Systems/ControlPointManager.js';
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────
|
|
|
|
function mockTilemap() {
|
|
return {
|
|
tileToWorldXY: jest.fn((tx, ty) => ({ x: tx * 32, y: ty * 32 })),
|
|
tileWidth: 32,
|
|
tileHeight: 32,
|
|
};
|
|
}
|
|
|
|
function mockScene() {
|
|
return {
|
|
add: {
|
|
zone: jest.fn((x, y) => ({
|
|
x: x ?? 0,
|
|
y: y ?? 0,
|
|
setName: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
body: { setCircle: jest.fn(), setOffset: jest.fn() },
|
|
})),
|
|
},
|
|
physics: {
|
|
world: { enableBody: jest.fn() },
|
|
},
|
|
events: { on: jest.fn(), emit: jest.fn(), off: jest.fn() },
|
|
};
|
|
}
|
|
|
|
function mockEconomy() {
|
|
return {
|
|
addIncome: jest.fn(),
|
|
getResources: jest.fn(),
|
|
initPlayer: jest.fn(),
|
|
};
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────
|
|
|
|
describe('ControlPointManager', () => {
|
|
let manager;
|
|
let scene;
|
|
let tilemap;
|
|
let economy;
|
|
|
|
beforeEach(() => {
|
|
scene = mockScene();
|
|
tilemap = mockTilemap();
|
|
economy = mockEconomy();
|
|
manager = new ControlPointManager(scene, tilemap, economy);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (manager) manager.destroy();
|
|
});
|
|
|
|
// ── constructor ─────────────────────────────────────────────────
|
|
describe('constructor', () => {
|
|
test('creates 4 control points', () => {
|
|
expect(manager.controlPoints.length).toBe(4);
|
|
});
|
|
|
|
test('accepts optional teamManager parameter', () => {
|
|
const tm = { getAllUnitsGrouped: jest.fn() };
|
|
const mgr = new ControlPointManager(scene, tilemap, economy, tm);
|
|
expect(mgr.teamManager).toBe(tm);
|
|
});
|
|
|
|
test('falls back to scene.teamManager when no teamManager passed', () => {
|
|
scene.teamManager = { getAllUnitsGrouped: jest.fn() };
|
|
const mgr = new ControlPointManager(scene, tilemap, economy);
|
|
expect(mgr.teamManager).toBe(scene.teamManager);
|
|
});
|
|
|
|
test('CPs are at clearing centers converted to world coords', () => {
|
|
const expectedTiles = [
|
|
{ tx: 32, ty: 32 },
|
|
{ tx: 96, ty: 32 },
|
|
{ tx: 32, ty: 96 },
|
|
{ tx: 96, ty: 96 },
|
|
];
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
const cp = manager.controlPoints[i];
|
|
expect(cp.zone.x).toBe(expectedTiles[i].tx * 32);
|
|
expect(cp.zone.y).toBe(expectedTiles[i].ty * 32);
|
|
}
|
|
});
|
|
|
|
test('tileToWorldXY is called for each CP', () => {
|
|
expect(tilemap.tileToWorldXY).toHaveBeenCalledTimes(4);
|
|
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(32, 32);
|
|
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(96, 32);
|
|
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(32, 96);
|
|
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(96, 96);
|
|
});
|
|
|
|
test('each CP has type=controlPoint, captureTime=60000, radius=5 tiles', () => {
|
|
for (const cp of manager.controlPoints) {
|
|
expect(cp.type).toBe('controlPoint');
|
|
expect(cp.captureTime).toBe(60000);
|
|
expect(cp.radiusTiles).toBe(5);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── update ──────────────────────────────────────────────────────
|
|
describe('update', () => {
|
|
test('ticks every CP', () => {
|
|
const tickSpies = manager.controlPoints.map((cp) =>
|
|
jest.spyOn(cp, 'tick').mockImplementation(() => {}),
|
|
);
|
|
|
|
manager.update(1000, 16);
|
|
|
|
// tick should be called with time and delta, not necessarily scene
|
|
for (const spy of tickSpies) {
|
|
expect(spy).toHaveBeenCalledWith(1000, 16, scene);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ── CP income (tick-driven CAPTURED check) ─────────────────────
|
|
describe('CP income', () => {
|
|
test('each CP is wired with the economy system on construction', () => {
|
|
for (const cp of manager.controlPoints) {
|
|
expect(cp.economySystem).toBe(economy);
|
|
}
|
|
});
|
|
|
|
test('does NOT call addIncome when state is not CAPTURED', () => {
|
|
const cp = manager.controlPoints[0];
|
|
cp.getState = jest.fn(() => 'NEUTRAL');
|
|
|
|
manager.update(1000, 1000);
|
|
|
|
expect(economy.addIncome).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('does NOT call addIncome when owner is null', () => {
|
|
const cp = manager.controlPoints[0];
|
|
cp.getState = jest.fn(() => 'CAPTURED');
|
|
cp.getOwner = jest.fn(() => null);
|
|
|
|
manager.update(1000, 1000);
|
|
|
|
expect(economy.addIncome).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ── destroy → stop generating ───────────────────────────────────
|
|
describe('destroy', () => {
|
|
test('destroys all CPs and clears the array', () => {
|
|
const destroySpies = manager.controlPoints.map((cp) =>
|
|
jest.spyOn(cp, 'destroy').mockImplementation(() => {}),
|
|
);
|
|
|
|
manager.destroy();
|
|
|
|
for (const spy of destroySpies) {
|
|
expect(spy).toHaveBeenCalled();
|
|
}
|
|
expect(manager.controlPoints.length).toBe(0);
|
|
});
|
|
});
|
|
});
|