rely on stringification instead of state reports

This commit is contained in:
2025-01-17 23:04:38 +09:00
parent 80ccb92815
commit 9345c7af04
9 changed files with 123 additions and 73 deletions
+73 -6
View File
@@ -11,8 +11,9 @@ scene.background = backgroundColor
renderer.setSize(window.innerWidth, window.innerHeight) renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setAnimationLoop(render) renderer.setAnimationLoop(render)
camera.position.set(5, -12, 20) camera.position.set(5, -12, 20)
// camera.position.set(5, -12, 10)
camera.rotation.set((56 / 180) * Math.PI, 0, 0) camera.rotation.set((56 / 180) * Math.PI, 0, 0)
camera.zoom += 0.4
camera.updateProjectionMatrix()
camera.layers.enable(1) camera.layers.enable(1)
camera.layers.enable(2) camera.layers.enable(2)
@@ -38,6 +39,7 @@ const entities = {}
const projectiles = {} const projectiles = {}
const positionTweens = {} const positionTweens = {}
const terrains = {} const terrains = {}
let state = { abilities: [], entities: [], terrains: [], projectiles: [] }
const geometry = new THREE.PlaneGeometry(0, 0) const geometry = new THREE.PlaneGeometry(0, 0)
const material = new THREE.MeshToonMaterial({ color: 0x115011 }) const material = new THREE.MeshToonMaterial({ color: 0x115011 })
@@ -164,7 +166,71 @@ function connectWebSocket() {
} }
websocket.onmessage = (event) => { 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)) { 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) ground.geometry = new THREE.PlaneGeometry(state.width / 100, state.height / 100)
@@ -246,13 +312,13 @@ function connectWebSocket() {
projectile.layers.set(2) projectile.layers.set(2)
scene.add(projectile) scene.add(projectile)
projectile.rotation.x = Math.PI / 2 // needed for the team marker...
const teamMaterial = teamMaterials['projectile'] const teamMaterial = teamMaterials['projectile']
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((p.radius) / 100, (p.radius) / 100, 1), teamMaterial) const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((p.radius) / 100, (p.radius) / 100, 1), teamMaterial)
const teamMarkerSize = 4000 const teamMarkerSize = 4000
teamMarker.scale.y = p.height / teamMarkerSize teamMarker.scale.y = p.height / teamMarkerSize
teamMarker.position.y = (p.height / (teamMarkerSize * 2)) - (p.height / 100) teamMarker.position.y = (p.height / (teamMarkerSize * 2)) - (p.height / 100)
projectile.rotation.x = Math.PI / 2 teamMarker.layers.set(2)
projectile.layers.set(2)
projectile.add(teamMarker) projectile.add(teamMarker)
projectiles[p.id] = projectile projectiles[p.id] = projectile
@@ -295,7 +361,8 @@ function connectWebSocket() {
if (player != null) { if (player != null) {
for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) { for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) {
if (player.abilities[abilityIndex] != null) { 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 lastCast = player.cooldowns[ability.id] ?? -Infinity
const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0 const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0
const remainingCooldown = (lastCast + cooldownDuration) - state.currentTick 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)
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@
} }
.debug-panel { .debug-panel {
/* display: none; */ display: none;
font-size: 0.8rem; font-size: 0.8rem;
position: fixed; position: fixed;
opacity: 0.2; opacity: 0.2;
+3
View File
@@ -38,6 +38,9 @@ export default class Ability {
effect: function straightShotEffect(caster, cursor) { effect: function straightShotEffect(caster, cursor) {
const ability = this const ability = this
const straightShotCollision = function straightShotCollision(projectile, collidingEntity) { const straightShotCollision = function straightShotCollision(projectile, collidingEntity) {
if (collidingEntity == null) { return }
if (collidingEntity.team == (projectile.owner?.team ?? 'unknown')) { return }
collidingEntity.damage(ability.damage) collidingEntity.damage(ability.damage)
projectile.despawn() projectile.despawn()
} }
+27 -25
View File
@@ -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) { attackAction(cursor) {
this.moveAction(cursor, true) this.moveAction(cursor, true)
} }
castAction(slot, cursor, halt = false) { castAction(slot, cursor, halt = false) {
const ability = this.abilities[slot] const ability = this.ability(slot)
if (ability == null) { return } if (ability == null) { return }
if (this.casting != null) { if (this.casting != null) {
@@ -143,6 +134,21 @@ export default class Entity {
// --- Actions above --- // // --- 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() { collidables() {
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider()) 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() const terrainColliders = (this.game?.terrains ?? []).map((t) => t.colliders()).flat()
@@ -200,12 +206,6 @@ export default class Entity {
this.dead = false this.dead = false
} }
state() {
return {
...this,
}
}
teleport(cursor) { teleport(cursor) {
this.position = cursor.clone() this.position = cursor.clone()
this.fixPosition() this.fixPosition()
@@ -264,16 +264,18 @@ export default class Entity {
if (this.#attacking) { if (this.#attacking) {
const cursor = this.#dest ?? this.position const cursor = this.#dest ?? this.position
const basicAttack = this.abilities[0] const basicAttack = this.ability(0)
const target = this.closestTargetTo(cursor, basicAttack.range) if (basicAttack != null) {
if (target != null && this.distanceTo(target.position) < basicAttack.range) { const target = this.closestTargetTo(cursor, basicAttack.range)
const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0 if (target != null && this.distanceTo(target.position) < basicAttack.range) {
const lastCast = this.cooldowns[basicAttack.id] const cooldown = this.game?.secToTick(basicAttack.cooldown) ?? 0
const timestamp = this.game?.currentTick ?? 0 const lastCast = this.cooldowns[basicAttack.id]
if (lastCast != null && lastCast + cooldown > timestamp) { return false } const timestamp = this.game?.currentTick ?? 0
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
this.castAction(0, cursor, false) this.castAction(0, cursor, false)
return true return true
}
} }
} }
+13 -23
View File
@@ -1,31 +1,30 @@
import { EventEmitter } from 'node:events' import { EventEmitter } from 'node:events'
import Ability from './ability.js'
import Entity from './entity.js' import Entity from './entity.js'
import Projectile from './projectile.js' import Projectile from './projectile.js'
import Terrain from './terrain.js' import Terrain from './terrain.js'
export default class Game { export default class Game {
abilities = Object.values({...Ability})
averageTick = 0 averageTick = 0
currentTick = 0 currentTick = 0
entities = []
height = 2000 height = 2000
projectiles = []
secondToSlowestTick = 0 secondToSlowestTick = 0
terrains = []
tickRate = 30 tickRate = 30
width = 2000 width = 2000
#currentTiming = 0 #currentTiming = 0
#entities = []
#eventEmitter = new EventEmitter() #eventEmitter = new EventEmitter()
#logic = null #logic = null
#nextTickAt = 0 #nextTickAt = 0
#projectiles = []
#terrains = []
#tickBudget = 1000 / this.tickRate #tickBudget = 1000 / this.tickRate
#timings = new Float32Array(this.tickRate) #timings = new Float32Array(this.tickRate)
get logic() { return this.#logic } get logic() { return this.#logic }
get entities() { return this.#entities }
get eventEmitter() { return this.#eventEmitter } get eventEmitter() { return this.#eventEmitter }
get projectiles() { return this.#projectiles }
get terrains() { return this.#terrains }
get tickBudget() { return this.#tickBudget } get tickBudget() { return this.#tickBudget }
set logic(value) { this.#logic = value } set logic(value) { this.#logic = value }
@@ -34,7 +33,7 @@ export default class Game {
} }
addTerrain(terrain) { addTerrain(terrain) {
this.#terrains.push(terrain) this.terrains.push(terrain)
} }
despawn(object) { despawn(object) {
@@ -45,17 +44,17 @@ export default class Game {
} }
despawnEntity(entity) { 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 entity.game = null
} }
despawnProjectile(projectile) { 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 projectile.game = null
} }
removeTerrain(terrain) { removeTerrain(terrain) {
this.#terrains = this.#terrains.filter((t) => t.id != terrain.id) this.terrains = this.terrains.filter((t) => t.id != terrain.id)
} }
secToTick(sec) { secToTick(sec) {
@@ -70,31 +69,22 @@ export default class Game {
} }
spawnEntity(entity) { spawnEntity(entity) {
this.#entities.push(entity) this.entities.push(entity)
entity.game = this entity.game = this
} }
spawnProjectile(projectile) { spawnProjectile(projectile) {
this.#projectiles.push(projectile) this.projectiles.push(projectile)
projectile.game = this 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() { start() {
setInterval(() => this.#gameLoop(), 1) setInterval(() => this.#gameLoop(), 1)
} }
update() { update() {
this.#entities.forEach((e) => e.update()) this.entities.forEach((e) => e.update())
this.#projectiles.forEach((p) => p.update()) this.projectiles.forEach((p) => p.update())
if (this.#logic != null) { if (this.#logic != null) {
this.#logic() this.#logic()
} }
+1 -1
View File
@@ -19,7 +19,7 @@ app.use(express.urlencoded({ extended: true }))
app.ws('/ws', async (req, res) => { app.ws('/ws', async (req, res) => {
const websocket = await res.accept() 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) game.eventEmitter.on('tick', subscription)
websocket.on('close', () => { websocket.on('close', () => {
-6
View File
@@ -52,12 +52,6 @@ export default class Projectile {
this.game?.despawn(this) this.game?.despawn(this)
} }
state() {
return {
...this,
}
}
update() { update() {
this.#move() this.#move()
if (this.onCollide != null) { this.checkCollisions(this.collider()) } if (this.onCollide != null) { this.checkCollisions(this.collider()) }
+5 -5
View File
@@ -5,7 +5,7 @@ import Team from './team.js'
export default class Template { export default class Template {
static minion(team, options = {}) { static minion(team, options = {}) {
return { 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, height: options.ranged ? 40 : 38,
logic: this.#minionLogic(options.route), logic: this.#minionLogic(options.route),
maxHealth: options.ranged ? 300 : 450, maxHealth: options.ranged ? 300 : 450,
@@ -20,10 +20,10 @@ export default class Template {
static player(overrides) { static player(overrides) {
return { return {
abilities: [ abilities: [
Ability.rangedAttack, Ability.rangedAttack.id,
Ability.straightShot, Ability.straightShot.id,
Ability.shieldThrow, Ability.shieldThrow.id,
Ability.blink, Ability.blink.id,
], ],
height: 80, height: 80,
logic: this.#playerLogic, logic: this.#playerLogic,
-6
View File
@@ -45,12 +45,6 @@ export default class Terrain {
colliders() { return this.#colliders } colliders() { return this.#colliders }
state() {
return {
...this,
}
}
#shape() { #shape() {
const complexShape = new Shape() const complexShape = new Shape()