import { EventEmitter } from 'node:events' import Ability from './ability.js' import Buff from './buff.js' import Entity from './entity.js' import Projectile from './projectile.js' import Terrain from './terrain.js' export default class Game { abilities = Object.values({...Ability}) buffs = Object.values({...Buff}) averageTick = 0 currentTick = 0 entities = [] height = 2000 projectiles = [] secondToSlowestTick = 0 terrains = [] tickRate = 30 width = 2000 #currentTiming = 0 #eventEmitter = new EventEmitter() #logic = null #nextTickAt = 0 #tickBudget = 1000 / this.tickRate #timings = new Float32Array(this.tickRate) get logic() { return this.#logic } get eventEmitter() { return this.#eventEmitter } 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 } 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.#calculateTickMetrics() this.eventEmitter.emit('tick') this.currentTick++ } #calculateTickMetrics() { this.averageTick = Math.floor(10 * this.#timings.reduce((sum, t) => sum += t, 0) / this.#timings.length) / 10 this.secondToSlowestTick = Math.floor(10 * this.#timings.toSorted().at(-2)) / 10 } #gameLoop() { const tickBudget = this.#tickBudget if (this.#nextTickAt != null) { const nextTickAt = this.#nextTickAt this.#nextTickAt = null let start = 0 while (start < nextTickAt) { start = performance.now() } const before = performance.now() this.update() this.#nextTickAt = start + tickBudget const after = performance.now() const taken = (after - before) this.#timings[this.#currentTiming] = taken if (this.#currentTiming++ > this.#timings.length) { this.#currentTiming = 0 } if (after - before > tickBudget) { console.warn(`Can't keep up! A tick took ${taken.toFixed(1)} ms (Budget: ${tickBudget.toFixed(1)} ms)`) } } } }