- 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)
206 lines
5.5 KiB
JavaScript
206 lines
5.5 KiB
JavaScript
/**
|
|
* Jest Setup - Mock Phaser and browser APIs
|
|
*/
|
|
|
|
// Mock Phaser BEFORE it's imported
|
|
jest.mock('phaser', () => ({
|
|
Scene: class MockScene {
|
|
constructor(config) {
|
|
this.key = config?.key || '';
|
|
this.sys = { events: new (require('events').EventEmitter)() };
|
|
}
|
|
},
|
|
Physics: {
|
|
Arcade: {
|
|
DYNAMIC_BODY: 0,
|
|
Sprite: class MockSprite {
|
|
constructor(scene, x, y, texture) {
|
|
this.scene = scene;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.texture = texture;
|
|
this.body = {
|
|
allowGravity: false,
|
|
setSize: jest.fn(),
|
|
setOffset: jest.fn()
|
|
};
|
|
this.setScale = jest.fn();
|
|
this.setInteractive = jest.fn();
|
|
this.on = jest.fn();
|
|
this.setPosition = jest.fn(() => true);
|
|
this.setFlipX = jest.fn();
|
|
this.setTint = jest.fn();
|
|
this.clearTint = jest.fn();
|
|
// Stateful setData/getData so tests can read back what they wrote
|
|
this._data = {};
|
|
this.displayWidth = 32;
|
|
this.displayHeight = 32;
|
|
this.setData = jest.fn((key, value) => { this._data[key] = value; });
|
|
this.getData = jest.fn((key) => this._data[key] ?? null);
|
|
this.pulse = null;
|
|
}
|
|
destroy() {} // no-op so Unit.destroy() can safely call super.destroy()
|
|
static enable(scene, object) {
|
|
object.body = { allowGravity: false };
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Display: {
|
|
Color: {
|
|
GetColor32: (r, g, b, a) => (r << 24) | (g << 16) | (b << 8) | a
|
|
}
|
|
},
|
|
Tweens: {
|
|
Tween: class MockTween {
|
|
constructor(config) {
|
|
this.config = config;
|
|
}
|
|
getValue() { return 200; }
|
|
stop() {}
|
|
},
|
|
addCounter: config => {
|
|
const tween = {
|
|
getValue: () => 200,
|
|
stop: () => {}
|
|
};
|
|
// Fire onUpdate immediately so selection tests see setTint called
|
|
if (config.onUpdate) config.onUpdate(tween);
|
|
return tween;
|
|
}
|
|
},
|
|
Events: {
|
|
EventEmitter: class MockEventEmitter {
|
|
constructor() {
|
|
this.listeners = {};
|
|
}
|
|
on(event, fn) {
|
|
if (!this.listeners[event]) this.listeners[event] = [];
|
|
this.listeners[event].push(fn);
|
|
}
|
|
emit(event, ...args) {
|
|
if (this.listeners[event]) {
|
|
this.listeners[event].forEach(fn => fn(...args));
|
|
}
|
|
}
|
|
}
|
|
},
|
|
GameObjects: {
|
|
Zone: class MockZone {
|
|
constructor(scene, x, y, width, height) {
|
|
this.scene = scene;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.width = width;
|
|
this.height = height;
|
|
this.body = { setCircle: jest.fn(), checkCollision: { none: false } };
|
|
}
|
|
destroy() {}
|
|
},
|
|
Graphics: class MockGraphics {
|
|
constructor() {
|
|
this.clear = jest.fn();
|
|
this.fillStyle = jest.fn().mockReturnThis();
|
|
this.fillRect = jest.fn().mockReturnThis();
|
|
this.lineStyle = jest.fn().mockReturnThis();
|
|
this.strokeRect = jest.fn().mockReturnThis();
|
|
this.setDepth = jest.fn().mockReturnThis();
|
|
this.setPosition = jest.fn().mockReturnThis();
|
|
this.setVisible = jest.fn().mockReturnThis();
|
|
this.setAlpha = jest.fn().mockReturnThis();
|
|
this.active = true;
|
|
}
|
|
destroy() {}
|
|
}
|
|
},
|
|
Input: {
|
|
Keyboard: {
|
|
KeyCodes: {
|
|
A: 65, D: 68, W: 87, S: 83, SHIFT: 16, F: 70, CTRL: 17,
|
|
},
|
|
},
|
|
Events: {
|
|
POINTER_DOWN: 'pointerdown',
|
|
POINTER_MOVE: 'pointermove',
|
|
POINTER_UP: 'pointerup',
|
|
POINTER_WHEEL: 'wheel',
|
|
},
|
|
},
|
|
Cameras: {
|
|
Controls: {
|
|
SmoothedKeyControl: class MockSmoothedKeyControl {
|
|
constructor(config) {
|
|
this.config = config;
|
|
}
|
|
update() {}
|
|
}
|
|
}
|
|
},
|
|
Geom: {
|
|
Rectangle: class MockRectangle {
|
|
constructor(x, y, w, h) {
|
|
this.x = x; this.y = y; this.width = w; this.height = h;
|
|
}
|
|
},
|
|
Circle: class MockCircle {
|
|
constructor(x, y, r) {
|
|
this.x = x; this.y = y; this.radius = r;
|
|
}
|
|
}
|
|
},
|
|
Math: {
|
|
Vector2: class MockVector2 {
|
|
constructor(x, y) { this.x = x; this.y = y; }
|
|
},
|
|
Angle: {
|
|
BetweenPoints: (a, b) => Math.atan2(b.y - a.y, b.x - a.x)
|
|
},
|
|
RadToDeg: rad => {
|
|
let deg = rad * (180 / Math.PI);
|
|
while (deg < 0) deg += 360;
|
|
return deg % 360;
|
|
},
|
|
Distance: {
|
|
BetweenPoints: (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
|
|
},
|
|
Clamp: (v, min, max) => Math.max(min, Math.min(max, v))
|
|
},
|
|
}));
|
|
|
|
// Mock XState
|
|
jest.mock('xstate', () => ({
|
|
createMachine: jest.fn(config => ({ config })),
|
|
interpret: jest.fn(machine => ({
|
|
machine,
|
|
start: jest.fn(),
|
|
send: jest.fn(),
|
|
stop: jest.fn(),
|
|
state: { value: 'IDLING' }
|
|
})),
|
|
assign: jest.fn(fn => fn)
|
|
}));
|
|
|
|
// Mock EasyStar
|
|
jest.mock('easystarjs', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
js: jest.fn().mockImplementation(function () {
|
|
this.setGrid = jest.fn();
|
|
this.setIterationsPerCalculation = jest.fn();
|
|
this.findPath = jest.fn((x, y, toX, toY, callback) => {
|
|
setImmediate(() => callback([{ x, y }, { x: toX, y: toY }]));
|
|
});
|
|
this.setTileAtXY = jest.fn();
|
|
this.enableDiagonals = jest.fn();
|
|
this.enableCornerCutting = jest.fn();
|
|
this.setAcceptableTiles = jest.fn();
|
|
this.setTileCost = jest.fn();
|
|
this.setAdditionalPointCost = jest.fn();
|
|
this.calculate = jest.fn();
|
|
}),
|
|
},
|
|
}));
|
|
|
|
// Suppress console errors during tests
|
|
console.error = jest.fn();
|