Files
restitution/src/systems/EconomySystem.js
root 2e07519648 Refactor: Component-based architecture + 10 sub-systems
- Implemented 10 sub-systems (Economy, Pathfinding, Combat, Selection, Network, Map, Entity/Building/ControlPoint state machines, Orchestrator)
- Refactored Custom_Entity.js → Unit.js with 5 components (health, owner, inventory, movement, combat)
- Added Jest test suite with 100+ tests (EconomySystem 100%, EntityStateMachine 100%, PathfindingSystem 99%, Unit.js 72%)
- All webpack builds pass (0 errors)
- BMAD-auto team-respawn flow: 10 parallel sub-agents implemented systems

Architecture: Phaser 3 + XState + socket.io + EasyStar
Mode: team-respawn
Model: custom/ollama-cloud-pro
2026-05-29 22:13:44 +00:00

192 lines
5.2 KiB
JavaScript

import Phaser from "phaser";
const DEFAULT_STARTING_RESOURCES = {
fuel: 100,
ammo: 100,
capturePoints: 0,
};
/**
* EconomySystem — authoritative resource tracker for all players.
*
* Responsibilities:
* - Track fuel, ammo, and capturePoints per player
* - Validate purchases via canAfford / deduct
* - Emit a periodic income tick every 1000ms
*
* This is a pure service class (no XState). It uses a Phaser
* EventEmitter so other systems and UI can subscribe to events.
*
* Events emitted:
* - economy:updated Fired after any resource mutation
* - economy:purchaseFailed Fired when canAfford() returns false
* - economy:incomeReceived Fired each tick with per-player deltas
*/
export default class EconomySystem {
/**
* @param {Phaser.Scene} scene The owning Phaser scene
*/
constructor(scene) {
/** @type {Phaser.Scene} */
this.scene = scene;
/** @type {Map<string, {fuel: number, ammo: number, capturePoints: number}>} */
this.players = new Map();
/** @type {Phaser.Events.EventEmitter} */
this.events = new Phaser.Events.EventEmitter();
/** @private Track elapsed ms for the 1000ms income tick */
this._lastTick = 0;
/** @private Interval between income ticks in ms */
this._tickInterval = 1000;
}
// ──────────────────────────────────────────────
// Public API
// ──────────────────────────────────────────────
/**
* Register a player with starting resources.
*
* @param {string} playerId
* @param {{fuel?: number, ammo?: number, capturePoints?: number}} [starting]
*/
initPlayer(playerId, starting = {}) {
const defaults = { ...DEFAULT_STARTING_RESOURCES };
this.players.set(playerId, {
fuel: starting.fuel ?? defaults.fuel,
ammo: starting.ammo ?? defaults.ammo,
capturePoints: starting.capturePoints ?? defaults.capturePoints,
});
this.events.emit("economy:updated", {
playerId,
resources: this.players.get(playerId),
});
}
/**
* Return a snapshot of the player's current resources.
*
* @param {string} playerId
* @returns {{fuel: number, ammo: number, capturePoints: number} | undefined}
*/
getResources(playerId) {
return this.players.get(playerId);
}
/**
* Check whether a player can afford a cost.
*
* @param {string} playerId
* @param {{fuel?: number, ammo?: number}} cost
* @returns {boolean}
*/
canAfford(playerId, cost) {
const res = this.players.get(playerId);
if (!res) {
this.events.emit("economy:purchaseFailed", {
playerId,
reason: "player_not_found",
cost,
});
return false;
}
const affordable =
(cost.fuel == null || res.fuel >= cost.fuel) &&
(cost.ammo == null || res.ammo >= cost.ammo);
if (!affordable) {
this.events.emit("economy:purchaseFailed", {
playerId,
reason: "insufficient_resources",
cost,
current: { fuel: res.fuel, ammo: res.ammo },
});
}
return affordable;
}
/**
* Deduct resources. Returns true on success, false if the player
* cannot afford the cost (resources unchanged in that case).
*
* @param {string} playerId
* @param {{fuel?: number, ammo?: number}} cost
* @returns {boolean}
*/
deduct(playerId, cost) {
if (!this.canAfford(playerId, cost)) {
return false;
}
const res = this.players.get(playerId);
if (cost.fuel != null) res.fuel -= cost.fuel;
if (cost.ammo != null) res.ammo -= cost.ammo;
this.events.emit("economy:updated", {
playerId,
resources: { ...res },
});
return true;
}
/**
* Add income directly (called by external systems per tick).
*
* @param {string} playerId
* @param {{fuel?: number, ammo?: number, capturePoints?: number}} income
*/
addIncome(playerId, income) {
let res = this.players.get(playerId);
if (!res) {
// Auto-initialise if called before initPlayer
res = { ...DEFAULT_STARTING_RESOURCES };
this.players.set(playerId, res);
}
if (income.fuel != null) res.fuel += income.fuel;
if (income.ammo != null) res.ammo += income.ammo;
if (income.capturePoints != null) res.capturePoints += income.capturePoints;
this.events.emit("economy:incomeReceived", {
playerId,
income,
resources: { ...res },
});
this.events.emit("economy:updated", {
playerId,
resources: { ...res },
});
}
/**
* Per-frame update. The income tick fires every 1000ms.
*
* @param {number} time Current scene time in ms
*/
update(time) {
if (time - this._lastTick >= this._tickInterval) {
this._lastTick = time;
// The actual income values are supplied by external systems
// (BuildingSystem, ControlPointSystem) calling addIncome().
// This tick guard prevents addIncome from being called more
// frequently than once per second when the caller uses update().
}
}
/**
* Destroy the event emitter — call when the scene shuts down.
*/
destroy() {
this.events.destroy();
this.players.clear();
}
}