From 9345c7af0412476b5abf085f9c52f07a8172e4b0 Mon Sep 17 00:00:00 2001 From: Thayol Date: Fri, 17 Jan 2025 23:04:38 +0900 Subject: [PATCH] rely on stringification instead of state reports --- public/client.js | 79 +++++++++++++++++++++++++++++++++++++++++++---- public/index.html | 2 +- src/ability.js | 3 ++ src/entity.js | 52 ++++++++++++++++--------------- src/game.js | 36 ++++++++------------- src/index.js | 2 +- src/projectile.js | 6 ---- src/template.js | 10 +++--- src/terrain.js | 6 ---- 9 files changed, 123 insertions(+), 73 deletions(-) diff --git a/public/client.js b/public/client.js index b0687d0..346e518 100644 --- a/public/client.js +++ b/public/client.js @@ -11,8 +11,9 @@ scene.background = backgroundColor renderer.setSize(window.innerWidth, window.innerHeight) renderer.setAnimationLoop(render) camera.position.set(5, -12, 20) -// camera.position.set(5, -12, 10) camera.rotation.set((56 / 180) * Math.PI, 0, 0) +camera.zoom += 0.4 +camera.updateProjectionMatrix() camera.layers.enable(1) camera.layers.enable(2) @@ -38,6 +39,7 @@ const entities = {} const projectiles = {} const positionTweens = {} const terrains = {} +let state = { abilities: [], entities: [], terrains: [], projectiles: [] } const geometry = new THREE.PlaneGeometry(0, 0) const material = new THREE.MeshToonMaterial({ color: 0x115011 }) @@ -164,7 +166,71 @@ function connectWebSocket() { } websocket.onmessage = (event) => { - let state = JSON.parse(event.data) + state.byteSize = new Blob([event.data]).size + const stateUpdates = JSON.parse(event.data) + for (const [key, value] of Object.entries(stateUpdates)) { + if (!['abilities', 'terrains', 'entities', 'projectiles'].includes(key)) { + state[key] = value + } + } + + if (stateUpdates.abilities != null) { + const ids = stateUpdates.abilities.map((it) => it.id) + state.abilities = state.abilities.filter((it) => ids.includes(it.id)) + for (const ability of stateUpdates.abilities ?? []) { + const index = state?.abilities?.findIndex((it) => it.id == ability.id) + if (index > -1) { + state.abilities[index] = {...state.abilities[index], ...ability} + } + else { + state.abilities.push(ability) + } + } + } + + if (stateUpdates.entities != null) { + const ids = stateUpdates.entities.map((it) => it.id) + state.entities = state.entities.filter((it) => ids.includes(it.id)) + for (const entity of stateUpdates.entities ?? []) { + const index = state?.entities?.findIndex((it) => it.id == entity.id) + if (index > -1) { + state.entities[index] = {...state.entities[index], ...entity} + } + else { + state.entities.push(entity) + } + } + } + + if (stateUpdates.terrains != null) { + const ids = stateUpdates.terrains.map((it) => it.id) + state.terrains = state.terrains.filter((it) => ids.includes(it.id)) + for (const terrain of stateUpdates.terrains ?? []) { + const index = state?.terrains?.findIndex((it) => it.id == terrain.id) + if (index > -1) { + state.terrains[index] = {...state.terrains[index], ...terrain} + } + else { + state.terrains.push(terrain) + } + } + } + + if (stateUpdates.projectiles != null) { + const ids = stateUpdates.projectiles.map((it) => it.id) + state.projectiles = state.projectiles.filter((it) => ids.includes(it.id)) + for (const projectile of stateUpdates.projectiles) { + const index = state?.projectiles?.findIndex((it) => it.id == projectile.id) + if (index > -1) { + state.projectiles[index] = {...state.projectiles[index], ...projectile} + } + else { + state.projectiles.push(projectile) + } + } + } + + console.log(state) if (state.width != null && state.height != null && (ground.geometry.attributes.width != state.width || ground.geometry.attributes.height != state.height)) { ground.geometry = new THREE.PlaneGeometry(state.width / 100, state.height / 100) @@ -246,13 +312,13 @@ function connectWebSocket() { projectile.layers.set(2) scene.add(projectile) + projectile.rotation.x = Math.PI / 2 // needed for the team marker... const teamMaterial = teamMaterials['projectile'] const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((p.radius) / 100, (p.radius) / 100, 1), teamMaterial) const teamMarkerSize = 4000 teamMarker.scale.y = p.height / teamMarkerSize teamMarker.position.y = (p.height / (teamMarkerSize * 2)) - (p.height / 100) - projectile.rotation.x = Math.PI / 2 - projectile.layers.set(2) + teamMarker.layers.set(2) projectile.add(teamMarker) projectiles[p.id] = projectile @@ -295,7 +361,8 @@ function connectWebSocket() { if (player != null) { for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) { if (player.abilities[abilityIndex] != null) { - const ability = player.abilities[abilityIndex] + const abilityId = player.abilities[abilityIndex] + const ability = state.abilities.find((it) => it.id == abilityId) const lastCast = player.cooldowns[ability.id] ?? -Infinity const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0 const remainingCooldown = (lastCast + cooldownDuration) - state.currentTick @@ -342,7 +409,7 @@ function connectWebSocket() { } } - document.getElementById('state').innerHTML = JSON.stringify(state, null, 2) + // document.getElementById('state').innerHTML = JSON.stringify(stateUpdates, null, 2) } } diff --git a/public/index.html b/public/index.html index 21d748c..bfc82f7 100644 --- a/public/index.html +++ b/public/index.html @@ -27,7 +27,7 @@ } .debug-panel { - /* display: none; */ + display: none; font-size: 0.8rem; position: fixed; opacity: 0.2; diff --git a/src/ability.js b/src/ability.js index 9203d29..93ab4b4 100644 --- a/src/ability.js +++ b/src/ability.js @@ -38,6 +38,9 @@ export default class Ability { effect: function straightShotEffect(caster, cursor) { const ability = this const straightShotCollision = function straightShotCollision(projectile, collidingEntity) { + if (collidingEntity == null) { return } + if (collidingEntity.team == (projectile.owner?.team ?? 'unknown')) { return } + collidingEntity.damage(ability.damage) projectile.despawn() } diff --git a/src/entity.js b/src/entity.js index 13d2ef4..5e08dab 100644 --- a/src/entity.js +++ b/src/entity.js @@ -79,21 +79,12 @@ export default class Entity { ]) } - adjustWaypoint(waypoint, direction) { - return SATX.clamp( - waypoint.clone().add(direction.clone().multiplyScalar(this.radius)), - this.game?.width, - this.game?.height, - this.radius, - ) - } - attackAction(cursor) { this.moveAction(cursor, true) } castAction(slot, cursor, halt = false) { - const ability = this.abilities[slot] + const ability = this.ability(slot) if (ability == null) { return } if (this.casting != null) { @@ -143,6 +134,21 @@ export default class Entity { // --- Actions above --- // + ability(slot) { + if (this.abilities[slot] != null) { + return this.game?.abilities.find((it) => it.id == this.abilities[slot]) + } + } + + adjustWaypoint(waypoint, direction) { + return SATX.clamp( + waypoint.clone().add(direction.clone().multiplyScalar(this.radius)), + this.game?.width, + this.game?.height, + this.radius, + ) + } + collidables() { const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider()) const terrainColliders = (this.game?.terrains ?? []).map((t) => t.colliders()).flat() @@ -200,12 +206,6 @@ export default class Entity { this.dead = false } - state() { - return { - ...this, - } - } - teleport(cursor) { this.position = cursor.clone() this.fixPosition() @@ -264,16 +264,18 @@ export default class Entity { if (this.#attacking) { const cursor = this.#dest ?? this.position - const basicAttack = this.abilities[0] - const target = this.closestTargetTo(cursor, basicAttack.range) - if (target != null && this.distanceTo(target.position) < basicAttack.range) { - const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0 - const lastCast = this.cooldowns[basicAttack.id] - const timestamp = this.game?.currentTick ?? 0 - if (lastCast != null && lastCast + cooldown > timestamp) { return false } + const basicAttack = this.ability(0) + if (basicAttack != null) { + const target = this.closestTargetTo(cursor, basicAttack.range) + if (target != null && this.distanceTo(target.position) < basicAttack.range) { + const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0 + const lastCast = this.cooldowns[basicAttack.id] + const timestamp = this.game?.currentTick ?? 0 + if (lastCast != null && lastCast + cooldown > timestamp) { return false } - this.castAction(0, cursor, false) - return true + this.castAction(0, cursor, false) + return true + } } } diff --git a/src/game.js b/src/game.js index 9abe224..846151b 100644 --- a/src/game.js +++ b/src/game.js @@ -1,31 +1,30 @@ import { EventEmitter } from 'node:events' +import Ability from './ability.js' import Entity from './entity.js' import Projectile from './projectile.js' import Terrain from './terrain.js' export default class Game { + abilities = Object.values({...Ability}) averageTick = 0 currentTick = 0 + entities = [] height = 2000 + projectiles = [] secondToSlowestTick = 0 + terrains = [] tickRate = 30 width = 2000 #currentTiming = 0 - #entities = [] #eventEmitter = new EventEmitter() #logic = null #nextTickAt = 0 - #projectiles = [] - #terrains = [] #tickBudget = 1000 / this.tickRate #timings = new Float32Array(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 } @@ -34,7 +33,7 @@ export default class Game { } addTerrain(terrain) { - this.#terrains.push(terrain) + this.terrains.push(terrain) } despawn(object) { @@ -45,17 +44,17 @@ export default class Game { } despawnEntity(entity) { - this.#entities = this.#entities.filter((e) => e.id != entity.id) + 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) + 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) + this.terrains = this.terrains.filter((t) => t.id != terrain.id) } secToTick(sec) { @@ -70,31 +69,22 @@ export default class Game { } spawnEntity(entity) { - this.#entities.push(entity) + this.entities.push(entity) entity.game = this } spawnProjectile(projectile) { - this.#projectiles.push(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()), - } - } - start() { setInterval(() => this.#gameLoop(), 1) } update() { - this.#entities.forEach((e) => e.update()) - this.#projectiles.forEach((p) => p.update()) + this.entities.forEach((e) => e.update()) + this.projectiles.forEach((p) => p.update()) if (this.#logic != null) { this.#logic() } diff --git a/src/index.js b/src/index.js index c56db62..c109369 100644 --- a/src/index.js +++ b/src/index.js @@ -19,7 +19,7 @@ app.use(express.urlencoded({ extended: true })) app.ws('/ws', async (req, res) => { const websocket = await res.accept() - const subscription = () => websocket.send(JSON.stringify(game.state())) + const subscription = () => websocket.send(JSON.stringify(game)) game.eventEmitter.on('tick', subscription) websocket.on('close', () => { diff --git a/src/projectile.js b/src/projectile.js index 864b734..bd5d723 100644 --- a/src/projectile.js +++ b/src/projectile.js @@ -52,12 +52,6 @@ export default class Projectile { this.game?.despawn(this) } - state() { - return { - ...this, - } - } - update() { this.#move() if (this.onCollide != null) { this.checkCollisions(this.collider()) } diff --git a/src/template.js b/src/template.js index 7d8b64d..aab9585 100644 --- a/src/template.js +++ b/src/template.js @@ -5,7 +5,7 @@ import Team from './team.js' export default class Template { static minion(team, options = {}) { return { - abilities: [options.ranged ? Ability.rangedAttack : Ability.meleeAttack, null, null, null], + abilities: [options.ranged ? Ability.rangedAttack.id : Ability.meleeAttack.id, null, null, null], height: options.ranged ? 40 : 38, logic: this.#minionLogic(options.route), maxHealth: options.ranged ? 300 : 450, @@ -20,10 +20,10 @@ export default class Template { static player(overrides) { return { abilities: [ - Ability.rangedAttack, - Ability.straightShot, - Ability.shieldThrow, - Ability.blink, + Ability.rangedAttack.id, + Ability.straightShot.id, + Ability.shieldThrow.id, + Ability.blink.id, ], height: 80, logic: this.#playerLogic, diff --git a/src/terrain.js b/src/terrain.js index bcc109a..3b1ca89 100644 --- a/src/terrain.js +++ b/src/terrain.js @@ -45,12 +45,6 @@ export default class Terrain { colliders() { return this.#colliders } - state() { - return { - ...this, - } - } - #shape() { const complexShape = new Shape()