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; | |
} | |
} | |