import { EventEmitter } from 'node:events' import Entity from './entity.js' import Terrain from './terrain.js' import Projectile from './projectile.js' export default class Game { tickRate = 30 currentTick = 0 width = 2000 height = 2000 nextTickAt = 0 #logic = null #entities = [] #eventEmitter = new EventEmitter() #projectiles = [] #terrains = [] #tickBudget = 1000 / this.tickRate get logic() { return this.#logic } get entities() { return this.#entities } get eventEmitter() { return this.#eventEmitter } get projectiles() { return this.#projectiles } get terrains() { return this.#terrains } get tickBudget() { return this.#tickBudget } set logic(value) { this.#logic = value } get unadjustedWaypoints() { return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat() } addTerrain(terrain) { this.#terrains.push(terrain) } despawn(object) { if (object instanceof Entity) { this.despawnEntity(object) } else if (object instanceof Terrain) { this.removeTerrain(object) } else if (object instanceof Projectile) { this.despawnProjectile(object) } else { console.error({ error: { reason: 'Can\'t despawn object', object } }) } } despawnEntity(entity) { this.#entities = this.#entities.filter((e) => e.id != entity.id) entity.game = null } despawnProjectile(projectile) { this.#projectiles = this.#projectiles.filter((p) => p.id != projectile.id) projectile.game = null } removeTerrain(terrain) { this.#terrains = this.#terrains.filter((t) => t.id != terrain.id) } secToTick(sec) { return Math.floor(this.tickRate * sec) } spawn(object) { if (object instanceof Entity) { this.spawnEntity(object) } else if (object instanceof Terrain) { this.addTerrain(object) } else if (object instanceof Projectile) { this.spawnProjectile(object) } else { console.error({ error: { reason: 'Can\'t spawn object', object } }) } } spawnEntity(entity) { this.#entities.push(entity) entity.game = this } spawnProjectile(projectile) { this.#projectiles.push(projectile) projectile.game = this } state() { return { ...this, entities: this.#entities.map((e) => e.state()), terrains: this.#terrains.map((t) => t.state()), projectiles: this.#projectiles.map((p) => p.state()), } } gameLoop() { const tickBudget = this.#tickBudget if (this.nextTickAt != null) { const nextTickAt = this.nextTickAt this.nextTickAt = null let start = performance.now() while (start < nextTickAt) { start = performance.now() } this.update() this.nextTickAt = start + tickBudget } } start() { setInterval(() => this.gameLoop(), 1) } update() { this.#entities.forEach((e) => e.update()) this.#projectiles.forEach((p) => p.update()) if (this.#logic != null) { this.#logic() } this.currentTick++ this.eventEmitter.emit('tick') } }