- Replace socket.io relay with Colyseus 0.15 authoritative server - GameRoom with GameState schema (players, units, resources) - Pure TS services: CombatResolver, EconomyService, PathfindingService, UnitManager - POST /api/create-room → 4-char invite code - React/MUI LobbyScreen: Create (shows code + START GAME) / Join by code - ColyseusClient: joinOrCreate/join by room type = invite code - Nginx: static assets direct, all else proxied to Colyseus (WS upgrade) - Content-hashed JS bundles for Cloudflare cache-busting - 1-player lobbies: START GAME button bypasses 2-player wait
136 lines
3.5 KiB
JavaScript
136 lines
3.5 KiB
JavaScript
/**
|
|
* Jest Setup - Mock Phaser and browser APIs
|
|
*/
|
|
|
|
// Mock Phaser BEFORE it's imported
|
|
jest.mock('phaser', () => ({
|
|
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.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 };
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Math: {
|
|
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))
|
|
},
|
|
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() {}
|
|
}
|
|
}
|
|
}));
|
|
|
|
// 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', () => {
|
|
return jest.fn().mockImplementation(() => ({
|
|
setGrid: jest.fn(),
|
|
setIterationsPerCalculation: jest.fn(),
|
|
findPath: jest.fn((x, y, toX, toY, callback) => {
|
|
setTimeout(() => callback([{ x, y }, { x: toX, y: toY }]), 0);
|
|
}),
|
|
setTileAtXY: jest.fn()
|
|
}));
|
|
});
|
|
|
|
// Suppress console errors during tests
|
|
console.error = jest.fn();
|