tower-defense / src /game /GameState.js
victor's picture
victor HF Staff
Initial commit
b29710c
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;
}
}