- 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)
159 lines
4.5 KiB
JavaScript
159 lines
4.5 KiB
JavaScript
/**
|
|
* ControlPointManager.test.js — TeamManager-aware tests.
|
|
* Tests use Jest globals.
|
|
*/
|
|
|
|
// xstate mock with working state transitions
|
|
jest.mock('xstate', () => {
|
|
function createService(initialState = 'NEUTRAL', initialContext = {}) {
|
|
const ctx = {
|
|
owner: null,
|
|
captureProgress: 0,
|
|
captureTime: 60000,
|
|
unitsInRadius: {},
|
|
...initialContext,
|
|
};
|
|
|
|
const svc = {
|
|
state: { value: initialState, context: ctx },
|
|
send: jest.fn((event) => {
|
|
const e = typeof event === 'string' ? { type: event } : event;
|
|
const current = svc.state.value;
|
|
if (current === 'NEUTRAL' && e.type === 'UNITS_ENTERED') {
|
|
svc.state.value = 'CONTESTED';
|
|
} else if (current === 'CONTESTED' && e.type === 'PROGRESS_COMPLETE') {
|
|
svc.state.value = 'CAPTURED';
|
|
ctx.owner = e.owner || e.teamId || null;
|
|
} else if (current === 'CONTESTED' && e.type === 'UNITS_LEFT') {
|
|
svc.state.value = 'NEUTRAL';
|
|
} else if (current === 'CAPTURED' && e.type === 'ENEMY_UNITS_ENTERED') {
|
|
svc.state.value = 'CONTESTED';
|
|
}
|
|
if (e.owner != null) ctx.owner = e.owner;
|
|
if (e.teamId != null) ctx.owner = e.teamId;
|
|
}),
|
|
start: jest.fn(),
|
|
stop: jest.fn(),
|
|
status: 1,
|
|
};
|
|
return svc;
|
|
}
|
|
|
|
return {
|
|
createMachine: jest.fn((config) => ({
|
|
config,
|
|
id: config?.id,
|
|
initial: config?.initial,
|
|
})),
|
|
interpret: jest.fn((machine) => {
|
|
const cfg = machine?.config || machine;
|
|
return createService(cfg?.initial);
|
|
}),
|
|
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(teamManager = null) {
|
|
return {
|
|
add: {
|
|
zone: jest.fn((x, y, w, h) => ({
|
|
x: x ?? 0,
|
|
y: y ?? 0,
|
|
width: w ?? 0,
|
|
height: h ?? 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() },
|
|
teamManager,
|
|
children: { list: [] },
|
|
};
|
|
}
|
|
|
|
function mockEconomy() {
|
|
return {
|
|
addIncome: jest.fn(),
|
|
getResources: jest.fn(),
|
|
initPlayer: jest.fn(),
|
|
};
|
|
}
|
|
|
|
function mockTeamManager(units = {}) {
|
|
const map = new Map();
|
|
for (const [tid, list] of Object.entries(units)) {
|
|
map.set(tid, new Set(list));
|
|
}
|
|
return {
|
|
getAllUnitsGrouped: jest.fn(() => map),
|
|
getTeamUnits: jest.fn((tid) => map.get(tid) || new Set()),
|
|
};
|
|
}
|
|
|
|
// ── Tests ───────────────────────────────────────────────────────────
|
|
|
|
describe('ControlPointManager', () => {
|
|
let manager;
|
|
let scene;
|
|
let tilemap;
|
|
let economy;
|
|
let teamManager;
|
|
|
|
beforeEach(() => {
|
|
tilemap = mockTilemap();
|
|
economy = mockEconomy();
|
|
teamManager = mockTeamManager({});
|
|
scene = mockScene(teamManager);
|
|
manager = new ControlPointManager(scene, tilemap, economy, teamManager);
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (manager) manager.destroy();
|
|
});
|
|
|
|
// ── Test 1: constructor accepts teamManager ─────────────────────
|
|
test('constructor accepts teamManager parameter', () => {
|
|
expect(manager.teamManager).toBe(teamManager);
|
|
});
|
|
|
|
// ── Test 2: update queries teamManager for unit counts ──────────
|
|
test('update delegates tick to each CP using teamManager for unit counts', () => {
|
|
const tickSpies = manager.controlPoints.map((cp) =>
|
|
jest.spyOn(cp, 'tick').mockImplementation(() => {}),
|
|
);
|
|
|
|
manager.update(1000, 16);
|
|
|
|
for (const spy of tickSpies) {
|
|
expect(spy).toHaveBeenCalledWith(1000, 16, scene);
|
|
}
|
|
});
|
|
|
|
test('each CP has a reference to teamManager on construction', () => {
|
|
for (const cp of manager.controlPoints) {
|
|
expect(cp.teamManager).toBe(teamManager);
|
|
}
|
|
});
|
|
|
|
test('falls back to scene.teamManager when no teamManager passed', () => {
|
|
const mgr = new ControlPointManager(scene, tilemap, economy);
|
|
expect(mgr.teamManager).toBe(scene.teamManager);
|
|
});
|
|
});
|