Compare commits
10 Commits
55e5e8117c
...
8ae113b2cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
8ae113b2cf
|
|||
|
11ec464d27
|
|||
|
e799be0b59
|
|||
|
78c52c2cc8
|
|||
|
ff4483e9cf
|
|||
|
2b2336bf70
|
|||
|
de3c175914
|
|||
|
52a0da10fe
|
|||
|
305980b7f9
|
|||
|
de4c82fd8b
|
@@ -0,0 +1 @@
|
|||||||
|
nodejs 23.6.1
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 543 B |
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
Binary file not shown.
|
After Width: | Height: | Size: 95 B |
+141
-66
@@ -1,11 +1,13 @@
|
|||||||
import * as THREE from 'three'
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
|
||||||
import { Tween } from '@tweenjs/tween.js'
|
import { Tween } from '@tweenjs/tween.js'
|
||||||
|
import * as THREE from 'three'
|
||||||
import Stats from 'stats.js'
|
import Stats from 'stats.js'
|
||||||
|
|
||||||
const global = (0,eval)('this')
|
const global = (0,eval)('this')
|
||||||
const scene = new THREE.Scene()
|
const scene = new THREE.Scene()
|
||||||
const raycaster = new THREE.Raycaster()
|
const raycaster = new THREE.Raycaster()
|
||||||
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
|
const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||||
|
const clock = new THREE.Clock()
|
||||||
const renderer = new THREE.WebGLRenderer()
|
const renderer = new THREE.WebGLRenderer()
|
||||||
const backgroundColor = new THREE.Color().setHex(0x112233)
|
const backgroundColor = new THREE.Color().setHex(0x112233)
|
||||||
scene.background = backgroundColor
|
scene.background = backgroundColor
|
||||||
@@ -24,7 +26,6 @@ camera.layers.enable(2)
|
|||||||
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xcccccc })
|
const projectileMaterial = new THREE.MeshToonMaterial({ color: 0xcccccc })
|
||||||
const terrainMaterial = new THREE.MeshToonMaterial({ color: 0x5c4033 })
|
const terrainMaterial = new THREE.MeshToonMaterial({ color: 0x5c4033 })
|
||||||
const passableTerrainMaterial = new THREE.MeshToonMaterial({ color: 0x228822, transparent: true, opacity: 0.65 })
|
const passableTerrainMaterial = new THREE.MeshToonMaterial({ color: 0x228822, transparent: true, opacity: 0.65 })
|
||||||
// const bboxMaterial = new THREE.MeshToonMaterial({ color: 0xffd700, transparent: true, opacity: 0.2 })
|
|
||||||
const opacity = 0.3
|
const opacity = 0.3
|
||||||
const teamMaterials = {
|
const teamMaterials = {
|
||||||
blue: new THREE.MeshToonMaterial({ color: 0x4444ff }),
|
blue: new THREE.MeshToonMaterial({ color: 0x4444ff }),
|
||||||
@@ -35,6 +36,7 @@ const teamMaterials = {
|
|||||||
redTransparent: new THREE.MeshToonMaterial({ color: 0xff4444, transparent: true, opacity }),
|
redTransparent: new THREE.MeshToonMaterial({ color: 0xff4444, transparent: true, opacity }),
|
||||||
projectile: new THREE.MeshToonMaterial({ color: 0xff00ff, transparent: true, opacity }),
|
projectile: new THREE.MeshToonMaterial({ color: 0xff00ff, transparent: true, opacity }),
|
||||||
range: new THREE.MeshToonMaterial({ color: 0x00ffff, transparent: true, opacity: opacity / 2 }),
|
range: new THREE.MeshToonMaterial({ color: 0x00ffff, transparent: true, opacity: opacity / 2 }),
|
||||||
|
visionRange: new THREE.MeshToonMaterial({ color: 0x00ffff, transparent: true, opacity: opacity / 6 }),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: draw lines of path for minimap camera
|
// TODO: draw lines of path for minimap camera
|
||||||
@@ -46,16 +48,51 @@ minimapRenderer.setSize(300, 300)
|
|||||||
minimapRenderer.setAnimationLoop(minimapRender)
|
minimapRenderer.setAnimationLoop(minimapRender)
|
||||||
minimapCamera.position.set(10, 10, 10)
|
minimapCamera.position.set(10, 10, 10)
|
||||||
|
|
||||||
|
const animationActions = {}
|
||||||
const entities = {}
|
const entities = {}
|
||||||
const projectiles = {}
|
const gltf = {}
|
||||||
|
const mixers = {}
|
||||||
const positionTweens = {}
|
const positionTweens = {}
|
||||||
|
const projectiles = {}
|
||||||
|
const rotationTweens = {}
|
||||||
const terrains = {}
|
const terrains = {}
|
||||||
var state = { abilities: [], entities: [], terrains: [], projectiles: [] }
|
var state = { abilities: [], entities: [], terrains: [], projectiles: [] }
|
||||||
|
|
||||||
|
global.animationActions = animationActions
|
||||||
global.entities = entities
|
global.entities = entities
|
||||||
|
global.gltf = gltf
|
||||||
|
global.mixers = mixers
|
||||||
global.projectiles = projectiles
|
global.projectiles = projectiles
|
||||||
global.terrains = terrains
|
|
||||||
global.state = state
|
global.state = state
|
||||||
|
global.terrains = terrains
|
||||||
|
|
||||||
|
const gltfLoader = new GLTFLoader()
|
||||||
|
const preloadGLTF = function loadTemplate(path) {
|
||||||
|
gltfLoader.load(path, (loadedGLTF) => gltf[path] = loadedGLTF)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addGLTF = function addGLTF(scene, path, id, additionalSteps = function noAdditionalSteps() {}) {
|
||||||
|
if (gltf[path] == null) {
|
||||||
|
setTimeout(() => addGLTF(scene, path, id, additionalSteps), 200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scale = 2
|
||||||
|
const model = gltf[path].scene.clone()
|
||||||
|
|
||||||
|
const mixer = new THREE.AnimationMixer(model)
|
||||||
|
mixers[id] = mixer
|
||||||
|
|
||||||
|
animationActions[id] = {}
|
||||||
|
gltf[path].animations.forEach((it) => {
|
||||||
|
const animation = mixer.clipAction(it)
|
||||||
|
animationActions[id][it.name] = animation
|
||||||
|
})
|
||||||
|
|
||||||
|
model.scale.set(scale, scale, scale)
|
||||||
|
additionalSteps(model)
|
||||||
|
scene.add(model)
|
||||||
|
}
|
||||||
|
|
||||||
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 })
|
||||||
@@ -82,10 +119,14 @@ const mouse = {}
|
|||||||
var stats = new Stats()
|
var stats = new Stats()
|
||||||
stats.showPanel(0)
|
stats.showPanel(0)
|
||||||
|
|
||||||
|
var delta = 0
|
||||||
function render() {
|
function render() {
|
||||||
stats.begin()
|
stats.begin()
|
||||||
|
delta = clock.getDelta()
|
||||||
cameraMovement()
|
cameraMovement()
|
||||||
Object.values(positionTweens).forEach((tween) => tween.update()) // TODO: clean up tweens
|
Object.values(positionTweens).forEach((tween) => tween.update())
|
||||||
|
Object.values(rotationTweens).forEach((tween) => tween.update())
|
||||||
|
Object.values(mixers).forEach((mixer) => mixer.update(delta))
|
||||||
renderer.render(scene, camera)
|
renderer.render(scene, camera)
|
||||||
stats.end()
|
stats.end()
|
||||||
}
|
}
|
||||||
@@ -94,11 +135,14 @@ function minimapRender() {
|
|||||||
minimapRenderer.render(scene, minimapCamera)
|
minimapRenderer.render(scene, minimapCamera)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lockedCameraSpeedMultiplier = 3
|
||||||
var cameraLocked = true
|
var cameraLocked = true
|
||||||
function followCamera() {
|
function followCamera() {
|
||||||
const entity = entities[playerId]
|
const entity = entities[playerId]
|
||||||
if (entity == null) { return }
|
if (entity == null) { return }
|
||||||
|
|
||||||
|
const cameraSpeed = lockedCameraSpeedMultiplier * delta
|
||||||
|
|
||||||
const distanceX = Math.abs((entity.position.x + cameraOffsetX) - camera.position.x)
|
const distanceX = Math.abs((entity.position.x + cameraOffsetX) - camera.position.x)
|
||||||
const distanceY = Math.abs((entity.position.y + cameraOffsetY) - camera.position.y)
|
const distanceY = Math.abs((entity.position.y + cameraOffsetY) - camera.position.y)
|
||||||
|
|
||||||
@@ -128,13 +172,15 @@ function followCamera() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cameraSpeed = 0.03
|
const cameraSpeedMultiplier = 10
|
||||||
function cameraMovement() {
|
function cameraMovement() {
|
||||||
if (cameraLocked) {
|
if (cameraLocked) {
|
||||||
followCamera()
|
followCamera()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cameraSpeed = cameraSpeedMultiplier * delta
|
||||||
|
|
||||||
if (keysDown.ArrowLeft) { camera.position.x -= cameraSpeed }
|
if (keysDown.ArrowLeft) { camera.position.x -= cameraSpeed }
|
||||||
else if (keysDown.ArrowRight) { camera.position.x += cameraSpeed }
|
else if (keysDown.ArrowRight) { camera.position.x += cameraSpeed }
|
||||||
|
|
||||||
@@ -164,6 +210,7 @@ var websocket = null
|
|||||||
global.websocket = null
|
global.websocket = null
|
||||||
var timerId = null
|
var timerId = null
|
||||||
var playerId = null
|
var playerId = null
|
||||||
|
var playerTeam = null
|
||||||
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
websocket = new WebSocket(`ws://${window.location.hostname}:1280/ws`)
|
websocket = new WebSocket(`ws://${window.location.hostname}:1280/ws`)
|
||||||
@@ -192,6 +239,11 @@ function connectWebSocket() {
|
|||||||
tweenDuration = 1000 / stateUpdates.tickRate
|
tweenDuration = 1000 / stateUpdates.tickRate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stateUpdates.width != null && stateUpdates.height != null && stateUpdates.width != (state.width ?? -1) && stateUpdates.height != (state.height ?? -1) ) {
|
||||||
|
ground.geometry = new THREE.PlaneGeometry(stateUpdates.width / 100, stateUpdates.height / 100)
|
||||||
|
ground.position.set(stateUpdates.width / 200, stateUpdates.height / 200, 0)
|
||||||
|
}
|
||||||
|
|
||||||
if (stateUpdates.width != null && stateUpdates.height != null) {
|
if (stateUpdates.width != null && stateUpdates.height != null) {
|
||||||
state.width = stateUpdates.width
|
state.width = stateUpdates.width
|
||||||
state.height = stateUpdates.height
|
state.height = stateUpdates.height
|
||||||
@@ -273,32 +325,35 @@ function connectWebSocket() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.position.set(state.width / 200, state.height / 200, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const e of Object.values(entities)) {
|
for (const e of Object.values(entities)) {
|
||||||
e.userData.flaggedForRemoval = true
|
e.userData.flaggedForRemoval = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const e of state.entities ?? []) {
|
for (const e of state.entities ?? []) {
|
||||||
let entity
|
let entity
|
||||||
|
let created = false
|
||||||
|
|
||||||
|
if (e.id == playerId && playerTeam != e.team) {
|
||||||
|
playerTeam = e.team
|
||||||
|
}
|
||||||
|
|
||||||
if (e.id in entities) {
|
if (e.id in entities) {
|
||||||
entity = entities[e.id]
|
entity = entities[e.id]
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const entityMaterial = teamMaterials[e.team]
|
created = true
|
||||||
entity = new THREE.Mesh(new THREE.CylinderGeometry(e.visualRadius / 100, e.visualRadius / 100, e.height / 50), entityMaterial)
|
|
||||||
|
entity = new THREE.Group()
|
||||||
entity.rotation.x = Math.PI / 2
|
entity.rotation.x = Math.PI / 2
|
||||||
|
entity.scale.set(e.visualRadius / 100, e.visualRadius / 100, e.visualRadius / 100)
|
||||||
entity.userData.type = 'entity'
|
entity.userData.type = 'entity'
|
||||||
entity.userData.id = e.id
|
entity.userData.id = e.id
|
||||||
entity.position.set(e.position.x / 100, e.position.y / 100, e.height / 100)
|
entity.position.set(e.position.x / 100, e.position.y / 100, 0)
|
||||||
scene.add(entity)
|
scene.add(entity)
|
||||||
|
|
||||||
const hpMargin = 0.4
|
const hpMargin = 0.5
|
||||||
const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 }))
|
const maxHp = new THREE.Sprite(new THREE.SpriteMaterial({ color: 0xd03333 }))
|
||||||
maxHp.position.set(0, (e.height / 100) + hpMargin, 0)
|
maxHp.position.set(0, 0, 0)
|
||||||
maxHp.scale.set(1.5, 0.2, 1)
|
maxHp.scale.set(1.5, 0.2, 1)
|
||||||
maxHp.layers.set(1)
|
maxHp.layers.set(1)
|
||||||
entity.add(maxHp)
|
entity.add(maxHp)
|
||||||
@@ -310,83 +365,107 @@ function connectWebSocket() {
|
|||||||
maxHp.add(hp)
|
maxHp.add(hp)
|
||||||
|
|
||||||
const teamMaterial = teamMaterials[`${e.team}Transparent`]
|
const teamMaterial = teamMaterials[`${e.team}Transparent`]
|
||||||
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.radius) / 100, (e.radius) / 100, 1), teamMaterial)
|
const teamMarker = new THREE.Mesh(new THREE.CylinderGeometry(1, 0.00001, 1), teamMaterial)
|
||||||
const teamMarkerSize = 4000
|
teamMarker.position.y = -0.493
|
||||||
teamMarker.scale.y = e.height / teamMarkerSize
|
|
||||||
teamMarker.position.y = (e.height / (teamMarkerSize * 2)) - (e.height / 100)
|
|
||||||
teamMarker.position.y += 0.01
|
|
||||||
teamMarker.layers.set(1)
|
|
||||||
entity.add(teamMarker)
|
entity.add(teamMarker)
|
||||||
|
|
||||||
const buffMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, transparent: true, opacity: 0.4 })
|
const buffMaterial = new THREE.MeshToonMaterial({ color: 0xffff00, transparent: true, opacity: 0.4 })
|
||||||
const buffMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.visualRadius + 10) / 100, (e.visualRadius + 10) / 100, 1), buffMaterial)
|
const buffMarker = new THREE.Mesh(new THREE.TorusGeometry(0.95, 0.15), buffMaterial)
|
||||||
const buffMarkerSize = 400
|
buffMarker.rotation.x = Math.PI / 2
|
||||||
buffMarker.scale.y = e.height / buffMarkerSize
|
|
||||||
buffMarker.layers.set(1)
|
buffMarker.layers.set(1)
|
||||||
buffMarker.visible = false
|
buffMarker.visible = false
|
||||||
entity.add(buffMarker)
|
entity.add(buffMarker)
|
||||||
|
|
||||||
const rotationBase = new THREE.Object3D()
|
|
||||||
entity.add(rotationBase)
|
|
||||||
|
|
||||||
const castingMaterial = new THREE.MeshToonMaterial({ color: 0x10dde0, transparent: true, opacity: 0.4 })
|
|
||||||
const castingMarker = new THREE.Mesh(new THREE.CylinderGeometry((e.height * 0.9) / 100, (e.height * 0.9) / 100, 1), castingMaterial)
|
|
||||||
const castingMarkerSize = 800
|
|
||||||
castingMarker.rotation.z = Math.PI / 2
|
|
||||||
castingMarker.position.x = (e.radius) / 100
|
|
||||||
castingMarker.scale.y = e.height / castingMarkerSize
|
|
||||||
castingMarker.layers.set(1)
|
|
||||||
buffMarker.visible = false
|
|
||||||
rotationBase.add(castingMarker)
|
|
||||||
|
|
||||||
const rangeMaterial = teamMaterials['range']
|
const rangeMaterial = teamMaterials['range']
|
||||||
// const rangeSize = e.visionRange ?? 0
|
|
||||||
const rangeSize = (state.abilities.find((it) => it.id == e.abilities?.a)?.range ?? 0) + e.radius
|
const rangeSize = (state.abilities.find((it) => it.id == e.abilities?.a)?.range ?? 0) + e.radius
|
||||||
const rangeMarker = new THREE.Mesh(new THREE.CylinderGeometry((rangeSize) / 100, (rangeSize) / 100, 1), rangeMaterial)
|
const rangeMarker = new THREE.Mesh(new THREE.CylinderGeometry(rangeSize / e.visualRadius, rangeSize / e.visualRadius, 0.001), rangeMaterial)
|
||||||
const rangeMarkerSize = 5000
|
rangeMarker.position.y = 0.004
|
||||||
rangeMarker.scale.y = e.height / rangeMarkerSize
|
|
||||||
rangeMarker.position.y = (e.height / (rangeMarkerSize * 2)) - (e.height / 100)
|
|
||||||
rangeMarker.layers.set(1)
|
rangeMarker.layers.set(1)
|
||||||
rangeMarker.visible = false
|
rangeMarker.visible = false
|
||||||
entity.add(rangeMarker)
|
entity.add(rangeMarker)
|
||||||
|
|
||||||
|
const modelRotationBase = new THREE.Object3D()
|
||||||
|
modelRotationBase.rotation.y = e.rotation - (Math.PI / 2)
|
||||||
|
modelRotationBase.layers.set(1)
|
||||||
|
entity.add(modelRotationBase)
|
||||||
|
|
||||||
|
const visionRangeMaterial = teamMaterials['visionRange']
|
||||||
|
const visionRangeSize = e.visionRange ?? 0
|
||||||
|
const visionRangeMarker = new THREE.Mesh(new THREE.CylinderGeometry(visionRangeSize / e.visualRadius, visionRangeSize / e.visualRadius, 0.001), visionRangeMaterial)
|
||||||
|
visionRangeMarker.position.y = 0.002
|
||||||
|
visionRangeMarker.layers.set(1)
|
||||||
|
visionRangeMarker.visible = false
|
||||||
|
entity.add(visionRangeMarker)
|
||||||
|
|
||||||
|
if (e.model != null) {
|
||||||
|
addGLTF(modelRotationBase, e.model, e.id, function(model) {
|
||||||
|
const box = new THREE.Box3().setFromObject(model)
|
||||||
|
const size = box.getSize(new THREE.Vector3())
|
||||||
|
maxHp.position.set(0, size.y + hpMargin, 0)
|
||||||
|
buffMarker.position.y = size.y / 2
|
||||||
|
buffMarker.scale.z = size.y / 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
entities[e.id] = entity
|
entities[e.id] = entity
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.children.at(0).visible = !e.dead
|
entity.children.at(0).visible = !e.dead
|
||||||
entity.children.at(1).visible = !e.dead
|
entity.children.at(1).visible = !e.dead
|
||||||
entity.children.at(2).visible = e.buffs.some((it) => it.id == 'exposed') // TODO: only works for Exposed now
|
entity.children.at(2).visible = !e.dead && e.buffs.some((it) => it.id == 'exposed')
|
||||||
|
|
||||||
let z = e.height / 100
|
const animations = animationActions[e.id] ?? {}
|
||||||
|
const fadeIn = created ? 0 : 0.15
|
||||||
|
|
||||||
if (e.dead) {
|
if (e.dead) {
|
||||||
entity.rotation.x = 0
|
if (!animations.dead?.isRunning()) {
|
||||||
entity.position.z = 0
|
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
|
||||||
z = 0
|
animations.dead?.reset().fadeIn(fadeIn).play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.casting != null) {
|
||||||
|
if (!animations.cast?.isRunning()) {
|
||||||
|
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
|
||||||
|
animations.cast?.reset().fadeIn(fadeIn).play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
entity.rotation.x = Math.PI / 2
|
if (!animations.default?.isRunning()) {
|
||||||
entity.position.z = e.height / 100
|
Object.values(animations).forEach((it) => it.isRunning() && it.reset().fadeOut(fadeIn).play())
|
||||||
|
animations.default?.reset().fadeIn(fadeIn).play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.userData.flaggedForRemoval = false
|
entity.userData.flaggedForRemoval = false
|
||||||
entity.children.at(3).rotation.y = e.rotation
|
const oldRotationY = entity.children.at(4).rotation.y
|
||||||
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z }, tweenDuration).start()
|
const newRotationY = e.rotation - (Math.PI / 2)
|
||||||
|
if (Math.abs((oldRotationY - (2 * Math.PI)) - newRotationY) < Math.abs(oldRotationY - newRotationY)) {
|
||||||
|
entity.children.at(4).rotation.y = oldRotationY - (2 * Math.PI)
|
||||||
|
}
|
||||||
|
if (Math.abs((oldRotationY + (2 * Math.PI)) - newRotationY) < Math.abs(oldRotationY - newRotationY)) {
|
||||||
|
entity.children.at(4).rotation.y = oldRotationY + (2 * Math.PI)
|
||||||
|
}
|
||||||
|
|
||||||
|
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z: 0 }, tweenDuration).start()
|
||||||
|
rotationTweens[entity.id] = new Tween(entity.children.at(4).rotation).to({ x: 0, y: newRotationY, z: 0 }, tweenDuration).start()
|
||||||
|
|
||||||
const hp = entity.children.at(0).children.at(0)
|
const hp = entity.children.at(0).children.at(0)
|
||||||
const percentageHp = e.health / e.maxHealth
|
const percentageHp = e.health / e.maxHealth
|
||||||
hp.scale.x = percentageHp
|
hp.scale.x = percentageHp
|
||||||
hp.position.x = -(1 - percentageHp) / 2
|
hp.position.x = -(1 - percentageHp) / 2
|
||||||
|
|
||||||
// entity.children.at(4).visible = e.id == playerId
|
entity.children.at(3).visible = !e.dead && e.id == playerId
|
||||||
entity.children.at(3).children.at(0).visible = e.casting != null
|
// entity.children.at(5).visible = !e.dead && e.team == playerTeam // TODO: clipping makes the screen unviewable
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const e of Object.values(entities)) {
|
for (const e of Object.values(entities)) {
|
||||||
if (e.userData.flaggedForRemoval) {
|
if (e.userData.flaggedForRemoval) {
|
||||||
scene.remove(e)
|
scene.remove(e)
|
||||||
|
delete animationActions[e.userData.id]
|
||||||
delete entities[e.userData.id]
|
delete entities[e.userData.id]
|
||||||
|
delete mixers[e.userData.id]
|
||||||
delete positionTweens[e.userData.id]
|
delete positionTweens[e.userData.id]
|
||||||
|
delete rotationTweens[e.userData.id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,18 +526,6 @@ function connectWebSocket() {
|
|||||||
terrain.userData.id = t.id
|
terrain.userData.id = t.id
|
||||||
scene.add(terrain)
|
scene.add(terrain)
|
||||||
terrains[t.id] = terrain
|
terrains[t.id] = terrain
|
||||||
|
|
||||||
// // TODO: bboxes aren't tracked and can leak memory
|
|
||||||
// const bboxValues = Object.values(t.bbox)
|
|
||||||
// if (bboxValues.length >= 4) {
|
|
||||||
// const width = (bboxValues[1] - bboxValues[3]) / 100
|
|
||||||
// const height = (bboxValues[0] - bboxValues[2]) / 100
|
|
||||||
|
|
||||||
// const bbox = new THREE.Mesh(new THREE.BoxGeometry(width, height, 0.2), bboxMaterial)
|
|
||||||
// bbox.position.set((bboxValues[3] / 100) + (width / 2), (bboxValues[2] / 100) + (height / 2), 0)
|
|
||||||
// bbox.layers.set(1)
|
|
||||||
// scene.add(bbox)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
terrain.position.set(t.position.x / 100, t.position.y / 100, 0)
|
terrain.position.set(t.position.x / 100, t.position.y / 100, 0)
|
||||||
@@ -549,10 +616,18 @@ function connectWebSocket() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
|
preloadGLTF('models/generic-bam-placeholder.gltf')
|
||||||
|
preloadGLTF('models/generic-player-placeholder.gltf')
|
||||||
|
preloadGLTF('models/generic-player-placeholder-red.gltf')
|
||||||
|
|
||||||
const params = Object.fromEntries(new URLSearchParams(window.location.search).entries())
|
const params = Object.fromEntries(new URLSearchParams(window.location.search).entries())
|
||||||
playerId = params.id
|
playerId = params.id
|
||||||
if (playerId == null) {
|
if (playerId == null) {
|
||||||
playerId = prompt('Player ID:')
|
playerId = prompt('Player ID:')
|
||||||
|
if (playerId == '') {
|
||||||
|
window.location.href = '/menu/'
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWebSocket()
|
connectWebSocket()
|
||||||
|
|||||||
@@ -159,6 +159,7 @@
|
|||||||
|
|
||||||
.buff:hover {
|
.buff:hover {
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
height: fit-content;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +168,7 @@
|
|||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
min-width: 200px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
a:link, a:hover, a:active, a:visited {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<h1>Take control of a unit:</h1>
|
||||||
|
<ul id="links"></ul>
|
||||||
|
<script>
|
||||||
|
websocket = new WebSocket(`ws://${window.location.hostname}:1280/ws`)
|
||||||
|
websocket.onopen = () => { websocket.send(JSON.stringify({ action: 'entities' })) }
|
||||||
|
websocket.onmessage = (event) => {
|
||||||
|
const message = JSON.parse(event.data)
|
||||||
|
const entityIds = message?.entities
|
||||||
|
if (entityIds == null) { return }
|
||||||
|
|
||||||
|
websocket.close()
|
||||||
|
let links = ''
|
||||||
|
entityIds.forEach((entityId) => links += `<li><a href="/?id=${encodeURI(entityId)}">${entityId}</a></li>`)
|
||||||
|
document.getElementById('links').innerHTML = links
|
||||||
|
}
|
||||||
|
</script>
|
||||||
+3
-3
@@ -235,7 +235,7 @@ export default class Ability {
|
|||||||
const entityId = collidingEntity.id
|
const entityId = collidingEntity.id
|
||||||
|
|
||||||
if (!collided.has(entityId)) {
|
if (!collided.has(entityId)) {
|
||||||
collidingEntity.heal(amount, caster)
|
collidingEntity.applyBuff(Buff.shieldThrowShield.id, caster.id)
|
||||||
collided.add(entityId)
|
collided.add(entityId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,8 +252,8 @@ export default class Ability {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shieldThrowSecondAfter = function shieldThrowSecondAfter(projectile, homingTarget) {
|
const shieldThrowSecondAfter = function shieldThrowSecondAfter(projectile, homingTarget) {
|
||||||
caster.heal(amount, caster)
|
caster.applyBuff(Buff.shieldThrowShield.id, caster.id)
|
||||||
caster.heal(amount, caster) // NOTE: duplicated on purpose
|
caster.applyBuff(Buff.shieldThrowShield.id, caster.id) // NOTE: duplicated on purpose
|
||||||
}
|
}
|
||||||
|
|
||||||
const shieldThrowFirstAfter = function shieldThrowFirstAfter(projectile, homingTarget) {
|
const shieldThrowFirstAfter = function shieldThrowFirstAfter(projectile, homingTarget) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default class Buff {
|
|||||||
|
|
||||||
damageMultiplier = null
|
damageMultiplier = null
|
||||||
duration = 0
|
duration = 0
|
||||||
|
shield = null
|
||||||
|
|
||||||
#effect = null
|
#effect = null
|
||||||
|
|
||||||
@@ -25,4 +26,11 @@ export default class Buff {
|
|||||||
duration: 4,
|
duration: 4,
|
||||||
onHitMultiplier: 3,
|
onHitMultiplier: 3,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
static shieldThrowShield = new Buff({
|
||||||
|
id: 'shield_throw_shield',
|
||||||
|
name: 'Shield (of Shield Throw)',
|
||||||
|
duration: 5,
|
||||||
|
shield: 200,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-27
@@ -18,8 +18,9 @@ export default class Entity {
|
|||||||
dead = false
|
dead = false
|
||||||
ghosting = false
|
ghosting = false
|
||||||
health = null
|
health = null
|
||||||
height = 40
|
height = null
|
||||||
maxHealth = 1
|
maxHealth = 1
|
||||||
|
model = null
|
||||||
position = null
|
position = null
|
||||||
radius = 0
|
radius = 0
|
||||||
rotation = 0
|
rotation = 0
|
||||||
@@ -28,21 +29,21 @@ export default class Entity {
|
|||||||
visionRange = 900
|
visionRange = 900
|
||||||
visualRadius = null
|
visualRadius = null
|
||||||
|
|
||||||
#collision = true
|
|
||||||
#ghostable = true
|
|
||||||
#attacking = false
|
#attacking = false
|
||||||
#bbox = new Float32Array(4)
|
#bbox = new Float32Array(4)
|
||||||
#colliders = []
|
#colliders = []
|
||||||
#entitiesInVision = []
|
#collision = true
|
||||||
#projectilesInVision = []
|
|
||||||
#pathfindingCooldown = 0
|
|
||||||
#pathfindingObstacleLimit = null
|
|
||||||
#dest = null
|
#dest = null
|
||||||
|
#entitiesInVision = []
|
||||||
#game = null
|
#game = null
|
||||||
|
#ghostable = true
|
||||||
#logic = null
|
#logic = null
|
||||||
#moving = false
|
#moving = false
|
||||||
#path = []
|
|
||||||
#noPathfindingUntil = 0
|
#noPathfindingUntil = 0
|
||||||
|
#path = []
|
||||||
|
#pathfindingCooldown = 0
|
||||||
|
#pathfindingObstacleLimit = null
|
||||||
|
#projectilesInVision = []
|
||||||
#spawnPosition = new Vector2()
|
#spawnPosition = new Vector2()
|
||||||
|
|
||||||
static bbox(x, y, radius) {
|
static bbox(x, y, radius) {
|
||||||
@@ -140,6 +141,9 @@ export default class Entity {
|
|||||||
if (this.visualRadius == null) {
|
if (this.visualRadius == null) {
|
||||||
this.visualRadius = this.radius
|
this.visualRadius = this.radius
|
||||||
}
|
}
|
||||||
|
if (this.height == null) {
|
||||||
|
this.height = this.visualRadius ?? this.radius
|
||||||
|
}
|
||||||
|
|
||||||
this.#calculateCollider()
|
this.#calculateCollider()
|
||||||
}
|
}
|
||||||
@@ -177,7 +181,6 @@ export default class Entity {
|
|||||||
this.moveAction(cursor, true)
|
this.moveAction(cursor, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: buffer skill inputs
|
|
||||||
castAction(slot, cursor, halt = false) {
|
castAction(slot, cursor, halt = false) {
|
||||||
if (this.dead) { return }
|
if (this.dead) { return }
|
||||||
|
|
||||||
@@ -267,16 +270,27 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyBuff(id, sourceId = null) {
|
applyBuff(id, sourceId = null) {
|
||||||
|
const buff = (this.game?.buffs ?? []).find((it) => it.id == id)
|
||||||
|
if (buff == null) { return false }
|
||||||
|
|
||||||
const index = this.buffs.findIndex((it) => it.id == id)
|
const index = this.buffs.findIndex((it) => it.id == id)
|
||||||
const source = sourceId ?? this.id
|
const source = sourceId ?? this.id
|
||||||
const timestamp = this.game?.currentTick ?? 0
|
const timestamp = this.game?.currentTick ?? 0
|
||||||
|
|
||||||
if (index > -1) {
|
if (index < 0) {
|
||||||
this.buffs[index].timestamp = timestamp
|
const entityBuff = { id, source, timestamp }
|
||||||
this.buffs[index].source = source
|
if (buff.shield != null) {
|
||||||
|
entityBuff.shield = buff.shield
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buffs.push(entityBuff)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.buffs.push({ id, source, timestamp })
|
this.buffs[index].timestamp = timestamp
|
||||||
|
this.buffs[index].source = source
|
||||||
|
if (buff.shield != null) {
|
||||||
|
this.buffs[index].shield = (this.buffs[index].shield ?? 0) + buff.shield
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,24 +336,34 @@ export default class Entity {
|
|||||||
return entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
|
return entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add shielding logic
|
|
||||||
damage(amount, source = null) {
|
damage(amount, source = null) {
|
||||||
if (this.dead) { return }
|
if (this.dead) { return }
|
||||||
|
|
||||||
let customMultipliers = 0
|
let customMultipliers = 0
|
||||||
if (this.hasBuff(Buff.exposed.id)) {
|
if (this.hasBuff(Buff.exposed.id)) {
|
||||||
const buff = this.getBuff(Buff.exposed.id)
|
const buff = this.getBuff(Buff.exposed.id)
|
||||||
if (buff.source == source.id) {
|
if (buff.source == source?.id) {
|
||||||
customMultipliers += (buff.onHitMultiplier - 1)
|
customMultipliers += (buff.onHitMultiplier - 1)
|
||||||
this.removeBuff(Buff.exposed.id)
|
this.removeBuff(Buff.exposed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const damageMultiplerBuffs = source.buffs.map((it) => it.getBuff).filter((it) => it != null && it.damageMultiplier != null)
|
const buffs = this.buffs ?? []
|
||||||
|
const damageMultiplerBuffs = buffs.map((it) => it.getBuff).filter((it) => it != null && it.damageMultiplier != null)
|
||||||
const buffPassiveDamageMultiplier = damageMultiplerBuffs.reduce((it) => it.damageMultiplier - 1, 0)
|
const buffPassiveDamageMultiplier = damageMultiplerBuffs.reduce((it) => it.damageMultiplier - 1, 0)
|
||||||
|
|
||||||
const damageMultipler = 1 + buffPassiveDamageMultiplier + customMultipliers
|
const damageMultipler = 1 + buffPassiveDamageMultiplier + customMultipliers
|
||||||
const damage = amount * damageMultipler
|
let damage = amount * damageMultipler
|
||||||
|
|
||||||
|
if (damage >= 0) {
|
||||||
|
buffs.filter((it) => it.shield != null && it.shield > 0).forEach((it) => {
|
||||||
|
if (damage <= 0) { return }
|
||||||
|
|
||||||
|
const shielded = Math.max(0, Math.min(damage, it.shield))
|
||||||
|
it.shield -= shielded
|
||||||
|
damage -= shielded
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.health = Math.min(Math.max(0, this.health - damage), this.maxHealth)
|
this.health = Math.min(Math.max(0, this.health - damage), this.maxHealth)
|
||||||
}
|
}
|
||||||
@@ -395,6 +419,10 @@ export default class Entity {
|
|||||||
return { ...buffDefinition, ...entityBuff }
|
return { ...buffDefinition, ...entityBuff }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBuffs() {
|
||||||
|
return this.buffs.map((it) => this.getBuff(it.id)).filter((it) => it != null)
|
||||||
|
}
|
||||||
|
|
||||||
hasBuff(id) {
|
hasBuff(id) {
|
||||||
if (this.dead) { return false }
|
if (this.dead) { return false }
|
||||||
|
|
||||||
@@ -440,7 +468,7 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`Can't fix position ([${futurePosition.x}, ${futurePosition.y}]) of entity ID: ${this.id}`)
|
console.error({ error: 'position_unfixable', id: this.id, futurePosition })
|
||||||
}
|
}
|
||||||
|
|
||||||
isColliding() {
|
isColliding() {
|
||||||
@@ -472,9 +500,10 @@ export default class Entity {
|
|||||||
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
||||||
if (bboxCheckedObstacles.length < 1) { return true }
|
if (bboxCheckedObstacles.length < 1) { return true }
|
||||||
|
|
||||||
const posCollider = Entity.collider(this.position.x, this.position.y, 0)
|
const inWallVisionBypassRadius = Math.max(0, this.radius - 1)
|
||||||
const posBbox = Entity.bbox(this.position.x, this.position.y, 0)
|
const posCollider = Entity.collider(this.position.x, this.position.y, inWallVisionBypassRadius)
|
||||||
const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c))))
|
const posBbox = Entity.bbox(this.position.x, this.position.y, inWallVisionBypassRadius)
|
||||||
|
const unpassableTerrain = bboxCheckedObstacles.filter((it) => !SATX.bboxCheck(posBbox, it.bbox) || !it.colliders().some((c) => SATX.collideObject(posCollider, c)))
|
||||||
|
|
||||||
const colliders = unpassableTerrain.map((it) => it.colliders()).flat()
|
const colliders = unpassableTerrain.map((it) => it.colliders()).flat()
|
||||||
const collider = Entity.tunnelCollider(this.position.x, this.position.y, destination.x, destination.y, 0)
|
const collider = Entity.tunnelCollider(this.position.x, this.position.y, destination.x, destination.y, 0)
|
||||||
@@ -633,11 +662,8 @@ export default class Entity {
|
|||||||
|
|
||||||
#castingVision() {
|
#castingVision() {
|
||||||
const enemyTeam = this.team == Team.blue ? Team.red : (this.team == Team.red ? Team.blue : null)
|
const enemyTeam = this.team == Team.blue ? Team.red : (this.team == Team.red ? Team.blue : null)
|
||||||
if (enemyTeam == null) {
|
|
||||||
return // only blue/red teams have casting vision
|
|
||||||
}
|
|
||||||
|
|
||||||
const enemiesNearby = (this.game?.entities ?? []).some((it) => !it.dead && it.team == enemyTeam && it.distanceTo(this.position) <= (it.visionRange + this.radius))
|
const enemiesNearby = (this.game?.entities ?? []).some((it) => !it.dead && (enemyTeam == null || it.team == enemyTeam) && it.distanceTo(this.position) <= (it.visionRange + this.radius))
|
||||||
if (enemiesNearby) {
|
if (enemiesNearby) {
|
||||||
const radius = 300
|
const radius = 300
|
||||||
const duration = this.game?.secToTick(2) ?? 0
|
const duration = this.game?.secToTick(2) ?? 0
|
||||||
@@ -658,6 +684,7 @@ export default class Entity {
|
|||||||
owner: this.id,
|
owner: this.id,
|
||||||
position: this.position.clone(),
|
position: this.position.clone(),
|
||||||
visionRange: radius,
|
visionRange: radius,
|
||||||
|
team: enemyTeam,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.game?.spawnProjectile(projectile)
|
this.game?.spawnProjectile(projectile)
|
||||||
@@ -742,8 +769,7 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let failsafe = 0; failsafe <= (this.pathfindingObstacleLimit ?? 1000); failsafe++) {
|
for (let failsafe = 0; failsafe <= (this.pathfindingObstacleLimit ?? 10); failsafe++) {
|
||||||
if (failsafe >= 10) { console.error('Failsafe is reached!!!'); process.exit(0) }
|
|
||||||
const obstaclesArray = Array.from(obstacles.values())
|
const obstaclesArray = Array.from(obstacles.values())
|
||||||
|
|
||||||
for (const obstacle of obstaclesArray) {
|
for (const obstacle of obstaclesArray) {
|
||||||
|
|||||||
+15
-11
@@ -35,7 +35,7 @@ export default class Game {
|
|||||||
action(id, options) {
|
action(id, options) {
|
||||||
const entity = this.entities.find((it) => it.id == id)
|
const entity = this.entities.find((it) => it.id == id)
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
console.error({ error: 'Invalid ID' })
|
console.info({ info: 'action_invalid_id', id, options })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export default class Game {
|
|||||||
if (object instanceof Entity) { this.despawnEntity(object) }
|
if (object instanceof Entity) { this.despawnEntity(object) }
|
||||||
else if (object instanceof Terrain) { this.removeTerrain(object) }
|
else if (object instanceof Terrain) { this.removeTerrain(object) }
|
||||||
else if (object instanceof Projectile) { this.despawnProjectile(object) }
|
else if (object instanceof Projectile) { this.despawnProjectile(object) }
|
||||||
else { console.error({ error: { reason: 'Can\'t despawn object', object } }) }
|
else { console.error({ error: 'despawn_unknown_object', object }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
despawnEntity(entity) {
|
despawnEntity(entity) {
|
||||||
@@ -92,7 +92,7 @@ export default class Game {
|
|||||||
if (object instanceof Entity) { this.spawnEntity(object) }
|
if (object instanceof Entity) { this.spawnEntity(object) }
|
||||||
else if (object instanceof Terrain) { this.addTerrain(object) }
|
else if (object instanceof Terrain) { this.addTerrain(object) }
|
||||||
else if (object instanceof Projectile) { this.spawnProjectile(object) }
|
else if (object instanceof Projectile) { this.spawnProjectile(object) }
|
||||||
else { console.error({ error: { reason: 'Can\'t spawn object', object } }) }
|
else { console.error({ error: 'spawn_unknown_object', object }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
spawnEntity(entity) {
|
spawnEntity(entity) {
|
||||||
@@ -109,7 +109,7 @@ export default class Game {
|
|||||||
if (this.#gameLoopIntervalId != null) { return }
|
if (this.#gameLoopIntervalId != null) { return }
|
||||||
|
|
||||||
this.#startTimestamp = performance.now() + (this.currentTick * this.tickBudget)
|
this.#startTimestamp = performance.now() + (this.currentTick * this.tickBudget)
|
||||||
console.info(`Started game ${this.id} with ${this.tickRate} tps. Starting on tick ${this.currentTick}.`)
|
console.info({ event: 'game_start', id: this.id, tickRate: this.tickRate, currentTick: this.currentTick })
|
||||||
this.#gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), this.tickBudget / 5)
|
this.#gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), this.tickBudget / 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,15 +118,20 @@ export default class Game {
|
|||||||
|
|
||||||
clearInterval(this.#gameLoopIntervalId)
|
clearInterval(this.#gameLoopIntervalId)
|
||||||
this.#gameLoopIntervalId = null
|
this.#gameLoopIntervalId = null
|
||||||
console.info(`Stopped game ${this.id}. Stopped on tick ${this.currentTick}.`)
|
console.info({ event: 'game_stop', id: this.id, currentTick: this.currentTick })
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription(websocket, id) {
|
subscription(websocket, id) {
|
||||||
return function builtSubscription() {
|
return function builtSubscription(query = null) {
|
||||||
const game = this
|
const game = this
|
||||||
|
if (query == 'id') { return id }
|
||||||
|
if (query != null) { return }
|
||||||
|
|
||||||
const entity = game.entities.find((it) => it.id == id)
|
const entity = game.entities.find((it) => it.id == id)
|
||||||
if (entity == null) { return }
|
if (entity == null) {
|
||||||
|
websocket.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const team = entity.team
|
const team = entity.team
|
||||||
const state = game.visionByTeam(team)
|
const state = game.visionByTeam(team)
|
||||||
@@ -189,15 +194,14 @@ export default class Game {
|
|||||||
const before = performance.now()
|
const before = performance.now()
|
||||||
this.update()
|
this.update()
|
||||||
const after = performance.now()
|
const after = performance.now()
|
||||||
const taken = (after - before)
|
const tickTaken = after - before
|
||||||
|
|
||||||
const useAbsoluteBehind = true
|
const useAbsoluteBehind = true
|
||||||
const absoluteBehind = Math.max(0, (start - this.#startTimestamp) - ((this.currentTick) * tickBudget))
|
const absoluteBehind = Math.max(0, (start - this.#startTimestamp) - ((this.currentTick) * tickBudget))
|
||||||
this.#nextTickAt = start + tickBudget - (useAbsoluteBehind ? absoluteBehind : prevBehind)
|
this.#nextTickAt = start + tickBudget - (useAbsoluteBehind ? absoluteBehind : prevBehind)
|
||||||
|
|
||||||
if (after - before > tickBudget) {
|
if (tickTaken > tickBudget) {
|
||||||
const behindNotice = absoluteBehind > 0.1 ? `(Was already behind ${absoluteBehind.toFixed(1)} ms)` : ``
|
console.warn({ warn: 'overload', tickTaken, tickBudget, absoluteBehind })
|
||||||
console.warn(`Can't keep up! A tick took ${taken.toFixed(1)} ms / ${tickBudget.toFixed(1)} ms. ${behindNotice}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-6
@@ -1,4 +1,4 @@
|
|||||||
import { Dungeon } from './level.js'
|
import * as LEVEL from './level.js'
|
||||||
import { WebSocketExpress } from 'websocket-express'
|
import { WebSocketExpress } from 'websocket-express'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import Game from './game.js'
|
import Game from './game.js'
|
||||||
@@ -9,7 +9,7 @@ try {
|
|||||||
os.setPriority(process.pid, os.constants.priority.PRIORITY_HIGHEST)
|
os.setPriority(process.pid, os.constants.priority.PRIORITY_HIGHEST)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.warn('Could not adjust process priority on startup.')
|
console.warn({ warn: 'process_priority_unadjustable' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = new WebSocketExpress()
|
const app = new WebSocketExpress()
|
||||||
@@ -23,6 +23,8 @@ app.use('/@tweenjs/', express.static('node_modules/@tweenjs'))
|
|||||||
app.use('/stats.js/', express.static('node_modules/stats.js'))
|
app.use('/stats.js/', express.static('node_modules/stats.js'))
|
||||||
|
|
||||||
app.use('/', express.static('public'))
|
app.use('/', express.static('public'))
|
||||||
|
app.use('/models', express.static('models'))
|
||||||
|
|
||||||
app.use('/tools/', express.static('tools'))
|
app.use('/tools/', express.static('tools'))
|
||||||
|
|
||||||
app.ws('/ws', async (req, res) => {
|
app.ws('/ws', async (req, res) => {
|
||||||
@@ -30,16 +32,27 @@ app.ws('/ws', async (req, res) => {
|
|||||||
|
|
||||||
websocket.on('message', (rawData) => {
|
websocket.on('message', (rawData) => {
|
||||||
const message = JSON.parse(rawData)
|
const message = JSON.parse(rawData)
|
||||||
console.log(message)
|
console.info(message)
|
||||||
|
if (message.action == 'entities') {
|
||||||
|
websocket.send(JSON.stringify({ entities: game.entities.map((it) => it.id) }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (message.action == 'join') {
|
if (message.action == 'join') {
|
||||||
const id = message.id
|
const id = message.id
|
||||||
const connectionId = crypto.randomUUID()
|
const connectionId = crypto.randomUUID()
|
||||||
|
if (!game.entities.some((it) => it.id == id)) {
|
||||||
|
console.info({ error: 'join_invalid_id', id, connectionId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info({ event: 'connected', id, connectionId })
|
||||||
websocket.send(JSON.stringify(game.joinReport()))
|
websocket.send(JSON.stringify(game.joinReport()))
|
||||||
const subscription = game.subscription(websocket, id).bind(game)
|
const subscription = game.subscription(websocket, id).bind(game)
|
||||||
game.subscriptions.set(connectionId, subscription)
|
game.subscriptions.set(connectionId, subscription)
|
||||||
|
|
||||||
websocket.on('close', () => {
|
websocket.on('close', () => {
|
||||||
console.log({ event: 'disconnected', id })
|
console.info({ event: 'disconnected', id })
|
||||||
game.subscriptions.delete(connectionId)
|
game.subscriptions.delete(connectionId)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -50,7 +63,8 @@ app.ws('/ws', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.info(`Server started! Visit http://localhost:${port}`)
|
console.info({ event: 'startup', visit: `http://localhost:${port}/menu/` })
|
||||||
|
|
||||||
Dungeon.scenario(game)
|
LEVEL.Chase.scenario(game)
|
||||||
|
game.start()
|
||||||
})
|
})
|
||||||
|
|||||||
+17
-6
@@ -26,8 +26,24 @@ export class Dungeon {
|
|||||||
game.spawnEntity(new Entity(Template.player({ id: '2', spawnPosition: new Vector2(1500, 700), team, dead: true })))
|
game.spawnEntity(new Entity(Template.player({ id: '2', spawnPosition: new Vector2(1500, 700), team, dead: true })))
|
||||||
|
|
||||||
game.spawnEntity(new Entity(Template.basilisk({ id: 'boss', spawnPosition: new Vector2(2200, 750), team: Team.neutral })))
|
game.spawnEntity(new Entity(Template.basilisk({ id: 'boss', spawnPosition: new Vector2(2200, 750), team: Team.neutral })))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
game.start()
|
export class Chase {
|
||||||
|
static scenario(game) {
|
||||||
|
game.width = 1000
|
||||||
|
game.height = 1000
|
||||||
|
|
||||||
|
const chaserTemplate = {
|
||||||
|
// TODO: TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
const runnerTemplate = {
|
||||||
|
// TODO: TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
game.spawnEntity(new Entity({ ...chaserTemplate }))
|
||||||
|
game.spawnEntity(new Entity({ ...runnerTemplate }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +63,6 @@ export class Zigzag {
|
|||||||
new Vector2(i + 100, game.height - lowest),
|
new Vector2(i + 100, game.height - lowest),
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
game.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +81,6 @@ export class Ravine {
|
|||||||
team,
|
team,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
game.start()
|
|
||||||
}
|
}
|
||||||
static logic() {
|
static logic() {
|
||||||
const game = this
|
const game = this
|
||||||
|
|||||||
@@ -182,15 +182,6 @@ export default class Pathfind {
|
|||||||
graphIndex += 5
|
graphIndex += 5
|
||||||
}
|
}
|
||||||
|
|
||||||
// const niceGraph = []
|
|
||||||
// for (let i = 0; i < graph.length / 5; i += 5) {
|
|
||||||
// niceGraph.push({
|
|
||||||
// from: [graph[i], graph[i + 1]],
|
|
||||||
// to: [graph[i + 2], graph[i + 3]],
|
|
||||||
// distance: graph[i + 4],
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// console.log(niceGraph)
|
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-12
@@ -5,13 +5,12 @@ import Team from './team.js'
|
|||||||
export default class Template {
|
export default class Template {
|
||||||
static basilisk(overrides) {
|
static basilisk(overrides) {
|
||||||
return {
|
return {
|
||||||
abilities: {},
|
abilities: { a: Ability.rangedAttack.id },
|
||||||
height: 100,
|
logic: this.#basiliskLogic(),
|
||||||
logic: this.#basiliskLogic,
|
maxHealth: 300,
|
||||||
|
model: 'models/generic-bam-placeholder.gltf',
|
||||||
radius: 180,
|
radius: 180,
|
||||||
speed: 230,
|
speed: 230,
|
||||||
visualRadius: 170,
|
|
||||||
maxHealth: 3000,
|
|
||||||
...overrides,
|
...overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,17 +18,16 @@ export default class Template {
|
|||||||
static minion(team, options = {}) {
|
static minion(team, options = {}) {
|
||||||
return {
|
return {
|
||||||
abilities: { a: options.ranged ? Ability.rangedAttack.id : Ability.meleeAttack.id },
|
abilities: { a: options.ranged ? Ability.rangedAttack.id : Ability.meleeAttack.id },
|
||||||
height: options.ranged ? 40 : 38,
|
|
||||||
logic: this.#minionLogic(options.route, (team != Team.blue)),
|
logic: this.#minionLogic(options.route, (team != Team.blue)),
|
||||||
maxHealth: options.ranged ? 300 : 450,
|
maxHealth: options.ranged ? 300 : 450,
|
||||||
|
model: Team.blue == (team ?? Team.blue) ? 'models/generic-player-placeholder.gltf' : 'models/generic-player-placeholder-red.gltf',
|
||||||
pathfindingCooldown: 0.2,
|
pathfindingCooldown: 0.2,
|
||||||
pathfindingObstacleLimit: 0,
|
pathfindingObstacleLimit: 0,
|
||||||
position: options.route?.at(0) ?? options.position ?? new Vector2(0, 0),
|
position: options.route?.at(0) ?? options.position ?? new Vector2(0, 0),
|
||||||
radius: 48,
|
radius: options.ranged ? 36 : 38,
|
||||||
speed: 325,
|
speed: 325,
|
||||||
team,
|
team,
|
||||||
visionRange: 1200,
|
visionRange: 1200,
|
||||||
visualRadius: options.ranged ? 36 : 38,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,22 +42,55 @@ export default class Template {
|
|||||||
d: Ability.circleOfResurrection.id,
|
d: Ability.circleOfResurrection.id,
|
||||||
f: Ability.blink.id,
|
f: Ability.blink.id,
|
||||||
},
|
},
|
||||||
height: 80,
|
|
||||||
logic: this.#playerLogic,
|
logic: this.#playerLogic,
|
||||||
maxHealth: 600,
|
maxHealth: 600,
|
||||||
|
model: Team.blue == (overrides.team ?? Team.blue) ? 'models/generic-player-placeholder.gltf' : 'models/generic-player-placeholder-red.gltf',
|
||||||
pathfindingObstacleLimit: 3,
|
pathfindingObstacleLimit: 3,
|
||||||
radius: 65,
|
radius: 65,
|
||||||
spawnPosition: new Vector2(500, 150),
|
spawnPosition: new Vector2(500, 150),
|
||||||
visionRange: 1350,
|
visionRange: 1350,
|
||||||
visualRadius: 40,
|
|
||||||
...overrides,
|
...overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static #basiliskLogic() {
|
static #basiliskLogic() {
|
||||||
const entity = this
|
let diedOnTick = null
|
||||||
|
let targetInRangeSince = null
|
||||||
|
|
||||||
return
|
return function builtBasiliskLogic() {
|
||||||
|
const entity = this
|
||||||
|
if (Array.from(entity.game?.subscriptions.values()).some((it) => it('id') == entity.id)) { return }
|
||||||
|
|
||||||
|
const attackDelaySec = 2
|
||||||
|
const despawnDelaySec = 5
|
||||||
|
|
||||||
|
const despawnDelay = entity.game?.secToTick(despawnDelaySec) ?? 1
|
||||||
|
const timestamp = entity.game?.currentTick ?? 0
|
||||||
|
|
||||||
|
if (entity.dead && diedOnTick == null) { diedOnTick = timestamp }
|
||||||
|
if (entity.dead && diedOnTick != null && diedOnTick + despawnDelay < timestamp) { entity.despawn() }
|
||||||
|
if (!entity.dead) { diedOnTick = null }
|
||||||
|
if (entity.dead) { return }
|
||||||
|
|
||||||
|
const target = entity.closestTargetTo(entity.position, 500)
|
||||||
|
if (target == null) {
|
||||||
|
targetInRangeSince = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetInRangeSince == null) {
|
||||||
|
targetInRangeSince = timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
const attackDelay = entity.game?.secToTick(attackDelaySec) ?? 1
|
||||||
|
if (targetInRangeSince + attackDelay < timestamp) {
|
||||||
|
entity.castAction('a', target.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const directionToTarget = target.position.clone().sub(entity.position).normalize()
|
||||||
|
const entityRotationVector = new Vector2(1, 0).rotateAround(new Vector2(), entity.rotation)
|
||||||
|
entity.rotation = directionToTarget.clone().add(entityRotationVector).add(entityRotationVector).add(entityRotationVector).angle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static #minionLogic(route = [], odd = false) {
|
static #minionLogic(route = [], odd = false) {
|
||||||
|
|||||||
Reference in New Issue
Block a user