Spaces:
Running
Running
| 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; | |
| } | |
| } | |