import { INITIAL_MONEY, INITIAL_LIVES, getWaveParams, } from "../config/gameConfig.js"; /** * Minimal event emitter for local use. * Backward-compatible and small-footprint; no external deps. */ class SimpleEventEmitter { constructor() { this._events = new Map(); } on(type, handler) { if (!this._events.has(type)) this._events.set(type, new Set()); this._events.get(type).add(handler); } off(type, handler) { const set = this._events.get(type); if (!set) return; set.delete(handler); if (set.size === 0) this._events.delete(type); } emit(type, ...args) { const set = this._events.get(type); if (!set) return; // Copy to array to avoid mutation issues during emit [...set].forEach((h) => { try { h(...args); } catch { // swallow to avoid breaking game loop } }); } } export class GameState { constructor() { // Internal event bus this._events = new SimpleEventEmitter(); this.reset(); } /** * Subscribe to GameState events. * Usage: gameState.on('moneyChanged', (newMoney, prevMoney) => {}) */ on(type, handler) { this._events.on(type, handler); } /** * Unsubscribe from GameState events. */ off(type, handler) { this._events.off(type, handler); } /** * Convenience subscription helpers for moneyChanged event. */ subscribeMoneyChanged(handler) { this.on("moneyChanged", handler); } unsubscribeMoneyChanged(handler) { this.off("moneyChanged", handler); } reset() { this.money = INITIAL_MONEY; this.lives = INITIAL_LIVES; this.waveIndex = 0; // 0-based; wave number = waveIndex + 1 this.gameOver = false; this.gameWon = false; // no longer used for wave completion, kept for compatibility this.totalWaves = Infinity; // for compatibility with any UI that reads it // Wave spawning state (accumulator-based; in seconds) this.lastSpawnTime = 0; // kept for compatibility but unused by new accumulator this.spawnAccum = 0; this.spawnedThisWave = 0; this.waveActive = false; // Gameplay speed (1x or 2x) this.gameSpeed = 1; // Entity arrays this.enemies = []; this.towers = []; this.projectiles = []; // Selection state this.selectedTower = null; } setGameSpeed(speed) { const s = speed === 2 ? 2 : 1; this.gameSpeed = s; } getGameSpeed() { return this.gameSpeed; } startWave() { if (this.gameOver) return false; this.waveActive = true; // reset accumulator timing this.lastSpawnTime = performance.now() / 1000; this.spawnAccum = 0; this.spawnedThisWave = 0; return true; } getCurrentWave() { // wave number is 1-based const waveNum = this.waveIndex + 1; return getWaveParams(waveNum); } nextWave() { this.waveIndex++; // prepare next wave accumulator and state this.spawnAccum = 0; this.spawnedThisWave = 0; this.waveActive = false; // never set gameWon due to infinite waves } takeDamage(amount = 1) { this.lives -= amount; if (this.lives <= 0) { this.gameOver = true; } } /** * Increment money and emit moneyChanged if value changed. */ addMoney(amount) { if (!amount) return; const prev = this.money; this.money += amount; if (this.money !== prev) { this._events.emit("moneyChanged", this.money, prev); } } /** * Spend money if enough funds; emits moneyChanged on success. * Returns true if spent, false otherwise. */ spendMoney(amount) { if (this.money >= amount) { const prev = this.money; this.money -= amount; if (this.money !== prev) { this._events.emit("moneyChanged", this.money, prev); } return true; } return false; } /** * Set absolute money value; emits moneyChanged if changed. */ setMoney(amount) { const prev = this.money; this.money = amount; if (this.money !== prev) { this._events.emit("moneyChanged", this.money, prev); } } /** * Returns whether there is enough money for amount. */ canAfford(amount) { return this.money >= amount; } // TODO(deprecation): Avoid direct assignments to `money` outside GameState. // Migrate any external direct writes to use addMoney/spendMoney/setMoney. addEnemy(enemy) { this.enemies.push(enemy); } removeEnemy(enemy) { const index = this.enemies.indexOf(enemy); if (index > -1) { this.enemies.splice(index, 1); } } addTower(tower) { this.towers.push(tower); } removeTower(tower) { const index = this.towers.indexOf(tower); if (index > -1) { this.towers.splice(index, 1); } } addProjectile(projectile) { this.projectiles.push(projectile); } removeProjectile(projectile) { const index = this.projectiles.indexOf(projectile); if (index > -1) { this.projectiles.splice(index, 1); } } setSelectedTower(tower) { this.selectedTower = tower; } isGameActive() { return !this.gameOver; } }