- 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
162 lines
5.3 KiB
JavaScript
162 lines
5.3 KiB
JavaScript
/**
|
|
* EconomySystem Unit Tests
|
|
*/
|
|
import EconomySystem from '../src/systems/EconomySystem';
|
|
|
|
// Mock Phaser Scene
|
|
const createMockScene = () => ({
|
|
events: {
|
|
emit: jest.fn(),
|
|
on: jest.fn()
|
|
}
|
|
});
|
|
|
|
describe('EconomySystem', () => {
|
|
let scene;
|
|
let economy;
|
|
|
|
beforeEach(() => {
|
|
scene = createMockScene();
|
|
economy = new EconomySystem(scene);
|
|
});
|
|
|
|
describe('initPlayer', () => {
|
|
it('should initialize player with default resources', () => {
|
|
economy.initPlayer('player1');
|
|
const resources = economy.getResources('player1');
|
|
|
|
expect(resources.fuel).toBe(100);
|
|
expect(resources.ammo).toBe(100);
|
|
expect(resources.capturePoints).toBe(0);
|
|
});
|
|
|
|
it('should initialize player with custom resources', () => {
|
|
economy.initPlayer('player1', { fuel: 200, ammo: 50, capturePoints: 10 });
|
|
const resources = economy.getResources('player1');
|
|
|
|
expect(resources.fuel).toBe(200);
|
|
expect(resources.ammo).toBe(50);
|
|
expect(resources.capturePoints).toBe(10);
|
|
});
|
|
});
|
|
|
|
describe('canAfford', () => {
|
|
beforeEach(() => {
|
|
economy.initPlayer('player1', { fuel: 100, ammo: 50 });
|
|
});
|
|
|
|
it('should return true when player has enough resources', () => {
|
|
expect(economy.canAfford('player1', { fuel: 50, ammo: 25 })).toBe(true);
|
|
});
|
|
|
|
it('should return false when player lacks fuel', () => {
|
|
expect(economy.canAfford('player1', { fuel: 150, ammo: 25 })).toBe(false);
|
|
});
|
|
|
|
it('should return false when player lacks ammo', () => {
|
|
expect(economy.canAfford('player1', { fuel: 50, ammo: 100 })).toBe(false);
|
|
});
|
|
|
|
it('should return false for non-existent player', () => {
|
|
expect(economy.canAfford('player2', { fuel: 10 })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('deduct', () => {
|
|
beforeEach(() => {
|
|
economy.initPlayer('player1', { fuel: 100, ammo: 50 });
|
|
});
|
|
|
|
it('should deduct resources and return true', () => {
|
|
const result = economy.deduct('player1', { fuel: 30, ammo: 20 });
|
|
const resources = economy.getResources('player1');
|
|
|
|
expect(result).toBe(true);
|
|
expect(resources.fuel).toBe(70);
|
|
expect(resources.ammo).toBe(30);
|
|
});
|
|
|
|
it('should not deduct and return false when insufficient resources', () => {
|
|
const result = economy.deduct('player1', { fuel: 150, ammo: 20 });
|
|
const resources = economy.getResources('player1');
|
|
|
|
expect(result).toBe(false);
|
|
expect(resources.fuel).toBe(100); // Unchanged
|
|
expect(resources.ammo).toBe(50); // Unchanged
|
|
});
|
|
|
|
it('should emit economy:purchaseFailed on insufficient resources', () => {
|
|
// Production code emits on economy.events (internal EventEmitter), not scene.events
|
|
const emitSpy = jest.spyOn(economy.events, 'emit');
|
|
|
|
economy.deduct('player1', { fuel: 150, ammo: 20 });
|
|
|
|
expect(emitSpy).toHaveBeenCalledWith(
|
|
'economy:purchaseFailed',
|
|
expect.objectContaining({ playerId: 'player1', reason: expect.any(String) })
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('addIncome', () => {
|
|
it('should add income to player resources', () => {
|
|
economy.initPlayer('player1', { fuel: 100, ammo: 50 });
|
|
economy.addIncome('player1', { fuel: 25, ammo: 10, capturePoints: 5 });
|
|
|
|
const resources = economy.getResources('player1');
|
|
expect(resources.fuel).toBe(125);
|
|
expect(resources.ammo).toBe(60);
|
|
expect(resources.capturePoints).toBe(5);
|
|
});
|
|
|
|
it('should auto-initialize player if not exists', () => {
|
|
// Auto-init gives DEFAULT_STARTING_RESOURCES (fuel:100) + income (fuel:50) = 150
|
|
economy.addIncome('player2', { fuel: 50 });
|
|
const resources = economy.getResources('player2');
|
|
|
|
expect(resources.fuel).toBe(150);
|
|
});
|
|
|
|
it('should emit economy:incomeReceived and economy:updated', () => {
|
|
// Production code emits on economy.events (internal EventEmitter)
|
|
const emitSpy = jest.spyOn(economy.events, 'emit');
|
|
|
|
economy.initPlayer('player1');
|
|
economy.addIncome('player1', { fuel: 10 });
|
|
|
|
expect(emitSpy).toHaveBeenCalledWith(
|
|
'economy:incomeReceived',
|
|
expect.objectContaining({ playerId: 'player1' })
|
|
);
|
|
expect(emitSpy).toHaveBeenCalledWith(
|
|
'economy:updated',
|
|
expect.objectContaining({ playerId: 'player1' })
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
it('should track elapsed time for income tick guard', () => {
|
|
// update() is a guard only — external systems call addIncome().
|
|
// Verify update() doesn't throw and advances _lastTick.
|
|
expect(() => economy.update(1000)).not.toThrow();
|
|
expect(economy._lastTick).toBe(1000);
|
|
|
|
// Second call at 1500ms — not enough time passed since last tick (1000)
|
|
economy._lastTick = 0; // reset
|
|
expect(() => economy.update(500)).not.toThrow();
|
|
expect(economy._lastTick).toBe(0); // guard didn't fire
|
|
});
|
|
|
|
it('should not fire tick before 1000ms', () => {
|
|
// At 500ms, guard not triggered
|
|
economy._lastTick = 0;
|
|
const emitSpy = jest.spyOn(economy.events, 'emit');
|
|
economy.update(500);
|
|
// Update is a pure guard — no events are emitted by update() itself.
|
|
// External systems call addIncome() which does the event emission.
|
|
expect(economy._lastTick).toBe(0);
|
|
});
|
|
});
|
|
});
|