- 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)
381 lines
12 KiB
JavaScript
381 lines
12 KiB
JavaScript
/**
|
|
* Map_Player + Interface tests — M1.2: Disable legacy pointers, add F-key spawn
|
|
*
|
|
* Tests:
|
|
* 1. F-key spawns unit in scene
|
|
* 2. Spawned unit is selectable (physics body present for SelectionSystem)
|
|
* 3. Interface.init(false) does NOT create pathfinder or wire pointer events
|
|
*/
|
|
|
|
// ── Module-level mocks (before any imports) ───────────────────────
|
|
|
|
jest.mock('PhaserClasses/CustomConstants', () => ({
|
|
__esModule: true,
|
|
default: { TINTS: { RED: 0xff0000, BLUE: 0x0000ff, GREEN: 0x00ff00 } },
|
|
}));
|
|
|
|
jest.mock('Systems/SystemOrchestrator.js', () => ({
|
|
__esModule: true,
|
|
default: jest.fn().mockImplementation(() => ({
|
|
init: jest.fn(),
|
|
initPathfinding: jest.fn(),
|
|
initControlPoints: jest.fn(),
|
|
update: jest.fn(),
|
|
shutdown: jest.fn(),
|
|
registerBuilding: jest.fn((building, config) => ({
|
|
building,
|
|
config,
|
|
getState: jest.fn(() => 'ACTIVE'),
|
|
destroy: jest.fn(),
|
|
})),
|
|
unregisterBuilding: jest.fn(),
|
|
systems: {
|
|
selection: null,
|
|
pathfinding: null,
|
|
combat: { },
|
|
economy: {
|
|
initPlayer: jest.fn(),
|
|
events: {
|
|
on: jest.fn(),
|
|
},
|
|
},
|
|
network: null,
|
|
EntityStateMachine: {},
|
|
BuildingStateMachine: {},
|
|
ControlPointStateMachine: {},
|
|
},
|
|
})),
|
|
}));
|
|
|
|
jest.mock('Systems/NetworkSystem.js', () => ({
|
|
NetworkSystemClient: jest.fn(),
|
|
}));
|
|
|
|
// Mock entity skins to avoid real Phaser sprite construction
|
|
const createMockSkin = (scene, tile) => ({
|
|
scene,
|
|
tile,
|
|
x: (tile?.x ?? 0) * 32,
|
|
y: (tile?.y ?? 0) * 32,
|
|
name: 'test-unit',
|
|
body: null, // set by physics.enable
|
|
setScale: jest.fn().mockReturnThis(),
|
|
setData: jest.fn().mockReturnThis(),
|
|
setName: jest.fn().mockReturnThis(),
|
|
select: jest.fn(),
|
|
unSelect: jest.fn(),
|
|
destroy: jest.fn(),
|
|
});
|
|
|
|
jest.mock('Entities/skins/ukrainian-infantry', () => ({
|
|
__esModule: true,
|
|
default: jest.fn().mockImplementation(createMockSkin),
|
|
}));
|
|
|
|
jest.mock('Entities/skins/russian-infantry', () => ({
|
|
__esModule: true,
|
|
default: jest.fn().mockImplementation(createMockSkin),
|
|
}));
|
|
|
|
jest.mock('Entities/skins/russian-tank', () => ({
|
|
__esModule: true,
|
|
default: jest.fn().mockImplementation(createMockSkin),
|
|
}));
|
|
|
|
// ── Imports (after mocks) ─────────────────────────────────────────
|
|
|
|
import Map_Player from 'Scenes/Map_Player';
|
|
import Interface from 'PhaserClasses/interface';
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Build a mock Phaser scene with the surface area that create() + spawnTestUnit need.
|
|
*/
|
|
function buildMockScene() {
|
|
const keyboardHandlers = {};
|
|
const inputOnHandlers = {};
|
|
|
|
const mockScene = {
|
|
// Phaser.Scene basics
|
|
key: 'Map_Player',
|
|
scene: { key: 'Map_Player' },
|
|
game: { colyseus: null },
|
|
|
|
// Camera
|
|
cameras: {
|
|
main: {
|
|
setBounds: jest.fn(),
|
|
zoomTo: jest.fn(),
|
|
centerOn: jest.fn(),
|
|
get zoom() { return 1; },
|
|
},
|
|
},
|
|
|
|
// Input
|
|
input: {
|
|
setDefaultCursor: jest.fn(),
|
|
keyboard: {
|
|
addKey: jest.fn(() => ({ isDown: false })),
|
|
on: jest.fn((event, cb) => {
|
|
keyboardHandlers[event] = cb;
|
|
}),
|
|
_handlers: keyboardHandlers,
|
|
_fire: (event) => {
|
|
if (keyboardHandlers[event]) keyboardHandlers[event]();
|
|
},
|
|
},
|
|
on: jest.fn((event, cb) => {
|
|
inputOnHandlers[event] = cb;
|
|
}),
|
|
_handlers: inputOnHandlers,
|
|
_fire: (event, ...args) => {
|
|
if (inputOnHandlers[event]) inputOnHandlers[event](...args);
|
|
},
|
|
},
|
|
|
|
// Add game objects
|
|
add: {
|
|
container: jest.fn(() => ({
|
|
setName: jest.fn().mockReturnThis(),
|
|
add: jest.fn(),
|
|
setScrollFactor: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
setPosition: jest.fn().mockReturnThis(),
|
|
setVisible: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
active: true,
|
|
})),
|
|
rectangle: jest.fn(() => ({
|
|
setStrokeStyle: jest.fn().mockReturnThis(),
|
|
setOrigin: jest.fn().mockReturnThis(),
|
|
setPosition: jest.fn().mockReturnThis(),
|
|
setScrollFactor: jest.fn().mockReturnThis(),
|
|
setInteractive: jest.fn().mockReturnThis(),
|
|
setFillStyle: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
setVisible: jest.fn().mockReturnThis(),
|
|
on: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
active: true,
|
|
})),
|
|
graphics: jest.fn(() => ({
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
clear: jest.fn(),
|
|
fillStyle: jest.fn(),
|
|
fillRect: jest.fn(),
|
|
lineStyle: jest.fn(),
|
|
strokeRect: jest.fn(),
|
|
get active() { return true; },
|
|
})),
|
|
text: jest.fn(() => ({
|
|
setScrollFactor: jest.fn().mockReturnThis(),
|
|
setOrigin: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
setText: jest.fn().mockReturnThis(),
|
|
setVisible: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
setPosition: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
get active() { return true; },
|
|
})),
|
|
sprite: jest.fn(() => ({
|
|
setScale: jest.fn().mockReturnThis(),
|
|
setPosition: jest.fn().mockReturnThis(),
|
|
setVisible: jest.fn().mockReturnThis(),
|
|
play: jest.fn(),
|
|
setOrigin: jest.fn().mockReturnThis(),
|
|
setAlpha: jest.fn().mockReturnThis(),
|
|
setScrollFactor: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
setDisplaySize: jest.fn().mockReturnThis(),
|
|
setTint: jest.fn().mockReturnThis(),
|
|
destroy: jest.fn(),
|
|
active: true,
|
|
})),
|
|
existing: jest.fn(),
|
|
},
|
|
|
|
// Physics
|
|
physics: {
|
|
world: { enableBody: jest.fn() },
|
|
overlapRect: jest.fn(() => []),
|
|
add: {
|
|
existing: jest.fn(),
|
|
},
|
|
},
|
|
|
|
// Events
|
|
events: {
|
|
on: jest.fn(),
|
|
emit: jest.fn(),
|
|
off: jest.fn(),
|
|
},
|
|
|
|
// Tweens
|
|
tweens: {
|
|
addCounter: jest.fn((config) => {
|
|
const tween = { getValue: () => 200, stop: jest.fn() };
|
|
if (config.onUpdate) config.onUpdate(tween);
|
|
return tween;
|
|
}),
|
|
},
|
|
|
|
// Animations
|
|
anims: {
|
|
create: jest.fn(),
|
|
generateFrameNumbers: jest.fn(() => []),
|
|
},
|
|
|
|
// Make (tilemap factory)
|
|
make: {
|
|
tilemap: jest.fn(() => ({
|
|
addTilesetImage: jest.fn(() => ({})),
|
|
createLayer: jest.fn(() => ({
|
|
setCollisionByProperty: jest.fn().mockReturnThis(),
|
|
setDepth: jest.fn().mockReturnThis(),
|
|
getTileAtWorldXY: jest.fn(() => ({ x: 5, y: 5, index: 0, z: 0 })),
|
|
getTilesWithinShape: jest.fn(() => [{ x: 0, y: 0 }]),
|
|
tileToWorldXY: jest.fn((x, y) => ({ x: x * 32, y: y * 32 })),
|
|
})),
|
|
widthInPixels: 640,
|
|
heightInPixels: 480,
|
|
worldToTileXY: jest.fn(() => ({ x: 0, y: 0 })),
|
|
tileToWorldXY: jest.fn(() => ({ x: 0, y: 0 })),
|
|
})),
|
|
},
|
|
|
|
// Scene-specific properties (added by create)
|
|
map: null,
|
|
groundLayer: null,
|
|
rockLayer: null,
|
|
goodGuys: null,
|
|
infantry: null,
|
|
interface: null,
|
|
orchestrator: {
|
|
registerBuilding: jest.fn((building, config) => ({
|
|
building,
|
|
config,
|
|
getState: jest.fn(() => 'ACTIVE'),
|
|
destroy: jest.fn(),
|
|
})),
|
|
unregisterBuilding: jest.fn(),
|
|
systems: {},
|
|
},
|
|
tints: {},
|
|
};
|
|
|
|
return mockScene;
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────
|
|
|
|
describe('Map_Player — F-key spawn', () => {
|
|
let scene;
|
|
let mapPlayer;
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
scene = buildMockScene();
|
|
|
|
// Instantiate Map_Player with our mock scene
|
|
mapPlayer = new Map_Player();
|
|
// Monkey-patch the scene reference (Phaser does this internally)
|
|
mapPlayer.scene = scene;
|
|
// Attach scene properties that create() would set
|
|
Object.assign(mapPlayer, {
|
|
add: scene.add,
|
|
input: scene.input,
|
|
cameras: scene.cameras,
|
|
make: scene.make,
|
|
physics: scene.physics,
|
|
events: scene.events,
|
|
tweens: scene.tweens,
|
|
anims: scene.anims,
|
|
game: scene.game,
|
|
});
|
|
|
|
// Stub createMap to set the layers
|
|
mapPlayer.createMap = jest.fn(function () {
|
|
this.map = this.make.tilemap({ key: 'test1' });
|
|
this.groundLayer = this.map.createLayer('Floor', {}, 0, 0);
|
|
this.rockLayer = this.map.createLayer('Rocks', {}, 0, 0).setCollisionByProperty({ collides: true }).setDepth(10);
|
|
});
|
|
|
|
// Stub createInfantry to do nothing (test F-key spawn instead)
|
|
mapPlayer.createInfantry = jest.fn();
|
|
mapPlayer.createFriendlyPlatoon = jest.fn();
|
|
mapPlayer.createFriendlyInfantry = jest.fn();
|
|
});
|
|
|
|
test('F-key spawns unit in scene', () => {
|
|
// Call create() — this registers the F-key handler
|
|
mapPlayer.create();
|
|
|
|
// Verify F-key handler was registered
|
|
expect(scene.input.keyboard.on).toHaveBeenCalledWith(
|
|
'keydown-F',
|
|
expect.any(Function),
|
|
);
|
|
|
|
// Simulate F keypress
|
|
scene.input.keyboard._fire('keydown-F');
|
|
|
|
// spawnTestUnit should have created an infantry and added it to physics
|
|
// The mock Ukrainian_Rifle constructor was called
|
|
const Ukrainian_Rifle = require('Entities/skins/ukrainian-infantry').default;
|
|
expect(Ukrainian_Rifle).toHaveBeenCalled();
|
|
|
|
// The unit should have been added to the physics group via physics.add.existing
|
|
expect(scene.physics.add.existing).toHaveBeenCalled();
|
|
});
|
|
|
|
test('spawned unit is selectable', () => {
|
|
mapPlayer.create();
|
|
|
|
// Simulate F keypress
|
|
scene.input.keyboard._fire('keydown-F');
|
|
|
|
// The spawned unit should have a physics body so SelectionSystem can find it
|
|
// Mock the unit's physics body
|
|
const Ukrainian_Rifle = require('Entities/skins/ukrainian-infantry').default;
|
|
|
|
// Get the last created unit mock
|
|
const lastCall = Ukrainian_Rifle.mock.results[Ukrainian_Rifle.mock.results.length - 1];
|
|
const unit = lastCall ? lastCall.value : null;
|
|
|
|
expect(unit).toBeDefined();
|
|
// Unit must have a body (set by physics.add.existing or scene.physics.world.enableBody)
|
|
// Verify physics.add.existing was called with the unit
|
|
expect(scene.physics.add.existing).toHaveBeenCalledWith(unit);
|
|
});
|
|
});
|
|
|
|
describe('Interface — init(false)', () => {
|
|
test('init(false) does NOT wire pointer DOWN/MOVE/UP or create pathfinder', () => {
|
|
const scene = buildMockScene();
|
|
// createCamera() needs scene.map to be set for setBounds
|
|
scene.map = { widthInPixels: 640, heightInPixels: 480 };
|
|
const iface = new Interface(scene);
|
|
|
|
// Call init with useLegacyPointers=false
|
|
iface.init(false);
|
|
|
|
// Should NOT have registered pointer DOWN/MOVE/UP
|
|
const inputCalls = scene.input.on.mock.calls.map((c) => c[0]);
|
|
expect(inputCalls).not.toContain('pointerdown');
|
|
expect(inputCalls).not.toContain('pointermove');
|
|
expect(inputCalls).not.toContain('pointerup');
|
|
|
|
// Should still register POINTER_WHEEL (for zoom)
|
|
const wheelEvent = 'wheel';
|
|
expect(inputCalls).toContain(wheelEvent);
|
|
|
|
// Should NOT have a pathfinder
|
|
expect(iface.pathfinder).toBeUndefined();
|
|
});
|
|
});
|