136 lines
3.6 KiB
JavaScript
136 lines
3.6 KiB
JavaScript
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 = 10000 * 1.6
|
|
projectiles = []
|
|
secondToSlowestTick = 0
|
|
terrains = []
|
|
tickRate = 30
|
|
width = 10000 * 1.6
|
|
|
|
#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.#gameLoopCall.bind(this), 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. Entity count: ${this.entities.length}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
#gameLoopCall() {
|
|
this.#gameLoop()
|
|
}
|
|
}
|