add vision
This commit is contained in:
+14
-8
@@ -23,12 +23,13 @@ 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 bboxMaterial = new THREE.MeshToonMaterial({ color: 0xffd700, transparent: true, opacity: 0.2 })
|
// 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 }),
|
||||||
blueTransparent: new THREE.MeshToonMaterial({ color: 0x4444ff, transparent: true, opacity }),
|
blueTransparent: new THREE.MeshToonMaterial({ color: 0x4444ff, transparent: true, opacity }),
|
||||||
neutral: new THREE.MeshToonMaterial({ color: 0x22dd22, transparent: true, opacity }),
|
neutral: new THREE.MeshToonMaterial({ color: 0xcccccc }),
|
||||||
|
neutralTransparent: new THREE.MeshToonMaterial({ color: 0xcccccc, transparent: true, opacity }),
|
||||||
red: new THREE.MeshToonMaterial({ color: 0xff4444 }),
|
red: new THREE.MeshToonMaterial({ color: 0xff4444 }),
|
||||||
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 }),
|
||||||
@@ -169,6 +170,7 @@ function connectWebSocket() {
|
|||||||
websocket.onopen = () => {
|
websocket.onopen = () => {
|
||||||
document.getElementById('connection').innerHTML = 'open'
|
document.getElementById('connection').innerHTML = 'open'
|
||||||
clearInterval(timerId)
|
clearInterval(timerId)
|
||||||
|
websocket.send(JSON.stringify({ action: 'join', id: playerId }))
|
||||||
}
|
}
|
||||||
websocket.onclose = () => {
|
websocket.onclose = () => {
|
||||||
websocket = null
|
websocket = null
|
||||||
@@ -329,9 +331,10 @@ function connectWebSocket() {
|
|||||||
rotationBase.add(castingMarker)
|
rotationBase.add(castingMarker)
|
||||||
|
|
||||||
const rangeMaterial = teamMaterials['range']
|
const rangeMaterial = teamMaterials['range']
|
||||||
const rangeSize = (state.abilities.find((it) => it.id == e.abilities?.a)?.range ?? 0) + e.radius
|
const rangeSize = e.visionRange ?? 0
|
||||||
|
// 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) / 100, (rangeSize) / 100, 1), rangeMaterial)
|
||||||
const rangeMarkerSize = 4000
|
const rangeMarkerSize = 5000
|
||||||
rangeMarker.scale.y = e.height / rangeMarkerSize
|
rangeMarker.scale.y = e.height / rangeMarkerSize
|
||||||
rangeMarker.position.y = (e.height / (rangeMarkerSize * 2)) - (e.height / 100)
|
rangeMarker.position.y = (e.height / (rangeMarkerSize * 2)) - (e.height / 100)
|
||||||
rangeMarker.layers.set(1)
|
rangeMarker.layers.set(1)
|
||||||
@@ -382,7 +385,7 @@ function connectWebSocket() {
|
|||||||
scene.add(projectile)
|
scene.add(projectile)
|
||||||
|
|
||||||
projectile.rotation.x = Math.PI / 2 // needed for the team marker...
|
projectile.rotation.x = Math.PI / 2 // needed for the team marker...
|
||||||
const teamMaterial = teamMaterials['projectile']
|
const teamMaterial = teamMaterials[`${p.team}Transparent`] ?? 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
|
||||||
@@ -462,7 +465,7 @@ function connectWebSocket() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (player.casting?.ability?.id == ability.id) {
|
if (player.casting?.ability == ability.id) {
|
||||||
document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(50% 0%, 0% 100%, 100% 100%)` // triangle
|
document.getElementById(`ability-${abilityIndex}-cooldown`).style.clipPath = `polygon(50% 0%, 0% 100%, 100% 100%)` // triangle
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -485,7 +488,9 @@ function connectWebSocket() {
|
|||||||
let castIndicatorDisplay = 'none'
|
let castIndicatorDisplay = 'none'
|
||||||
if (player.casting != null) {
|
if (player.casting != null) {
|
||||||
castIndicatorDisplay = 'block'
|
castIndicatorDisplay = 'block'
|
||||||
const castDuration = (player.casting.ability.castTime * state.tickRate) ?? 0
|
const ability = state.abilities.find((it) => it.id == player.casting.ability)
|
||||||
|
if (ability != null) {
|
||||||
|
const castDuration = (ability.castTime * state.tickRate) ?? 0
|
||||||
const remainingCastTime = (player.casting.timestamp + castDuration) - state.currentTick
|
const remainingCastTime = (player.casting.timestamp + castDuration) - state.currentTick
|
||||||
let cssPercentage = '100%'
|
let cssPercentage = '100%'
|
||||||
if (remainingCastTime > 0) {
|
if (remainingCastTime > 0) {
|
||||||
@@ -494,7 +499,8 @@ function connectWebSocket() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('cast_indicator_progress').style.clipPath = `polygon(0 0, ${cssPercentage} 0, ${cssPercentage} 100%, 0% 100%)`
|
document.getElementById('cast_indicator_progress').style.clipPath = `polygon(0 0, ${cssPercentage} 0, ${cssPercentage} 100%, 0% 100%)`
|
||||||
document.getElementById('cast_indicator_name').innerHTML = player.casting.ability?.name ?? ''
|
document.getElementById('cast_indicator_name').innerHTML = ability.name ?? ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('cast_indicator').style.display = castIndicatorDisplay
|
document.getElementById('cast_indicator').style.display = castIndicatorDisplay
|
||||||
|
|||||||
@@ -216,4 +216,34 @@ export default class Ability {
|
|||||||
cooldown: 5,
|
cooldown: 5,
|
||||||
effect: function controlEffect(caster, cursor) { },
|
effect: function controlEffect(caster, cursor) { },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
static castingVision = new Ability({
|
||||||
|
id: 'casting_vision',
|
||||||
|
name: 'Casting Vision',
|
||||||
|
radius: 300,
|
||||||
|
duration: 2,
|
||||||
|
effect: function castingVisionEffect(caster, cursor) {
|
||||||
|
const ability = this
|
||||||
|
|
||||||
|
const currentTick = caster.game?.currentTick ?? 0
|
||||||
|
const duration = caster.game?.secToTick(ability.duration) ?? 0
|
||||||
|
const despawnAfter = currentTick + duration
|
||||||
|
|
||||||
|
const castingVisionLogic = function castingVisionLogic(projectile) {
|
||||||
|
const currentTick = projectile.game?.currentTick ?? 0
|
||||||
|
if (currentTick > despawnAfter) {
|
||||||
|
projectile.despawn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectile = new Projectile({
|
||||||
|
logic: castingVisionLogic,
|
||||||
|
owner: caster.id,
|
||||||
|
position: cursor.clone(),
|
||||||
|
visionRange: ability.radius,
|
||||||
|
})
|
||||||
|
|
||||||
|
caster.game?.spawnProjectile(projectile)
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-20
@@ -1,9 +1,10 @@
|
|||||||
import { Vector2 } from 'three'
|
import { Vector2 } from 'three'
|
||||||
|
import Ability from './ability.js'
|
||||||
|
import Buff from './buff.js'
|
||||||
import Pathfind from './pathfind.js'
|
import Pathfind from './pathfind.js'
|
||||||
import SAT from 'sat'
|
import SAT from 'sat'
|
||||||
import SATX from './satx.js'
|
import SATX from './satx.js'
|
||||||
import Team from './team.js'
|
import Team from './team.js'
|
||||||
import Buff from './buff.js'
|
|
||||||
|
|
||||||
export default class Entity {
|
export default class Entity {
|
||||||
id = `entity-${Entity.nextId()}`
|
id = `entity-${Entity.nextId()}`
|
||||||
@@ -179,8 +180,8 @@ export default class Entity {
|
|||||||
if (ability == null) { return }
|
if (ability == null) { return }
|
||||||
|
|
||||||
if (this.casting != null) {
|
if (this.casting != null) {
|
||||||
const abilityBeingCasted = this.casting.ability
|
const abilityBeingCasted = this.game?.abilities.filter((it) => it.id == this.casting.ability)
|
||||||
if (abilityBeingCasted.id == ability.id) {
|
if (abilityBeingCasted != null && abilityBeingCasted.id == ability.id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ export default class Entity {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.casting = { ability, cursor, timestamp } // TODO: use ID only for ability
|
this.casting = { ability: ability.id, cursor, timestamp }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -213,8 +214,8 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveAction(cursor, attack = false) {
|
moveAction(cursor, attack = false) {
|
||||||
if (this.casting != null && this.casting.ability.moveCancelable) {
|
if (this.casting != null && this.game?.abilities.filter((it) => it.id == this.casting.ability)?.moveCancelable) {
|
||||||
if (!attack && !(this.casting != null && this.casting.ability.id == this.abilities[0])) {
|
if (!attack && !(this.casting != null && this.casting.ability == this.abilities[0])) {
|
||||||
this.casting = null
|
this.casting = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,10 +279,12 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closestTargetTo(cursor, range) {
|
closestTargetTo(cursor, range) {
|
||||||
return this
|
const visibleEntityIds = this.visibleEntities()
|
||||||
.game
|
const entities = this.game?.entities
|
||||||
?.entities
|
if (entities == null) { return }
|
||||||
.filter((e) => this.team != e.team && e.distanceTo(cursor) <= range + this.radius + e.radius)
|
|
||||||
|
return entities
|
||||||
|
.filter((it) => visibleEntityIds.includes(it.id) && this.team != it.team && it.distanceTo(cursor) <= range + this.radius + it.radius)
|
||||||
.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
|
.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +309,16 @@ export default class Entity {
|
|||||||
return this.position.distanceTo(cursor)
|
return this.position.distanceTo(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entitiesInVision() {
|
||||||
|
const entities = this.game?.entities
|
||||||
|
if (entities == null) { return }
|
||||||
|
|
||||||
|
const entitiesInVisionRange = entities.filter((it) => it.id != this.id && it.distanceTo(this.position) <= this.visionRange + it.radius)
|
||||||
|
const entitiesInLineOfSight = entitiesInVisionRange.filter((it) => this.isInLineOfVision(it.position))
|
||||||
|
|
||||||
|
return entitiesInLineOfSight.concat([this]).map((it) => it.id)
|
||||||
|
}
|
||||||
|
|
||||||
futureCollidables(futurePosition) {
|
futureCollidables(futurePosition) {
|
||||||
return this.customBboxCollidables(new Float32Array([
|
return this.customBboxCollidables(new Float32Array([
|
||||||
futurePosition.y + this.radius,
|
futurePosition.y + this.radius,
|
||||||
@@ -386,6 +399,28 @@ export default class Entity {
|
|||||||
return colliders.some((it) => SATX.collideObject(collider, it))
|
return colliders.some((it) => SATX.collideObject(collider, it))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInLineOfSight(destination, position = this.position) {
|
||||||
|
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
|
||||||
|
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
||||||
|
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
||||||
|
if (bboxCheckedObstacles.length < 1) { return true }
|
||||||
|
|
||||||
|
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
|
||||||
|
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
|
||||||
|
return !colliders.some((it) => SATX.collideObject(collider, it))
|
||||||
|
}
|
||||||
|
|
||||||
|
isInLineOfVision(destination) {
|
||||||
|
const bbox = Entity.tunnelBbox(this.position.x, this.position.y, destination.x, destination.y, 0)
|
||||||
|
const terrains = this.game?.terrains ?? []
|
||||||
|
const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
||||||
|
if (bboxCheckedObstacles.length < 1) { return true }
|
||||||
|
|
||||||
|
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
|
||||||
|
const collider = Entity.tunnelCollider(this.position.x, this.position.y, destination.x, destination.y, 0)
|
||||||
|
return !colliders.some((it) => SATX.collideObject(collider, it))
|
||||||
|
}
|
||||||
|
|
||||||
obstaclesInStraightPath(destination, position = this.position) {
|
obstaclesInStraightPath(destination, position = this.position) {
|
||||||
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
|
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
|
||||||
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
||||||
@@ -396,16 +431,14 @@ export default class Entity {
|
|||||||
return bboxCheckedObstacles.filter((obstacle) => obstacle.colliders().some((it) => SATX.collideObject(collider, it)))
|
return bboxCheckedObstacles.filter((obstacle) => obstacle.colliders().some((it) => SATX.collideObject(collider, it)))
|
||||||
}
|
}
|
||||||
|
|
||||||
isInLineOfSight(destination, position = this.position) {
|
projectilesInVision() {
|
||||||
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
|
const projectiles = this.game?.projectiles
|
||||||
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
if (projectiles == null) { return }
|
||||||
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
|
||||||
if (bboxCheckedObstacles.length < 1) { return true }
|
|
||||||
|
|
||||||
|
const projectilesInVisionRange = projectiles.filter((it) => this.distanceTo(it.position) <= this.visionRange + it.radius)
|
||||||
|
const projectilesInLineOfSight = projectilesInVisionRange.filter((it) => it.visibleThroughTerrain || this.isInLineOfVision(it.position))
|
||||||
|
|
||||||
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
|
return projectilesInLineOfSight.map((it) => it.id)
|
||||||
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
|
|
||||||
return !colliders.some((it) => SATX.collideObject(collider, it))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeBuff(id) {
|
removeBuff(id) {
|
||||||
@@ -444,6 +477,10 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visibleEntities() {
|
||||||
|
return this.game?.visibleEntities(this.team)
|
||||||
|
}
|
||||||
|
|
||||||
waypoints() {
|
waypoints() {
|
||||||
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id)
|
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id)
|
||||||
const terrainColliders = (this.game?.terrains ?? [])
|
const terrainColliders = (this.game?.terrains ?? [])
|
||||||
@@ -481,16 +518,22 @@ export default class Entity {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const castTime = this.game?.secToTick(this.casting.ability.castTime) ?? 0
|
const ability = this.game?.abilities.find((it) => it.id == this.casting.ability)
|
||||||
|
if (ability == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const castTime = this.game?.secToTick(ability.castTime) ?? 0
|
||||||
const castStart = this.casting.timestamp
|
const castStart = this.casting.timestamp
|
||||||
const timestamp = this.game?.currentTick ?? 0
|
const timestamp = this.game?.currentTick ?? 0
|
||||||
if (castStart + castTime > timestamp) {
|
if (castStart + castTime > timestamp) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.casting.ability.effect(this, this.casting.cursor)
|
ability.effect(this, this.casting.cursor)
|
||||||
|
|
||||||
this.casting = null
|
this.casting = null
|
||||||
|
Ability.castingVision.effect(this, this.position)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-5
@@ -1,4 +1,5 @@
|
|||||||
import { EventEmitter } from 'node:events'
|
import { EventEmitter } from 'node:events'
|
||||||
|
import { Vector2 } from 'three'
|
||||||
import Ability from './ability.js'
|
import Ability from './ability.js'
|
||||||
import Buff from './buff.js'
|
import Buff from './buff.js'
|
||||||
import Entity from './entity.js'
|
import Entity from './entity.js'
|
||||||
@@ -11,12 +12,13 @@ export default class Game {
|
|||||||
averageTick = 0
|
averageTick = 0
|
||||||
currentTick = 0
|
currentTick = 0
|
||||||
entities = []
|
entities = []
|
||||||
height = 1000
|
gameLoopIntervalId = null
|
||||||
|
height = 0
|
||||||
projectiles = []
|
projectiles = []
|
||||||
secondToSlowestTick = 0
|
secondToSlowestTick = 0
|
||||||
terrains = []
|
terrains = []
|
||||||
tickRate = 30
|
tickRate = 30
|
||||||
width = 1000
|
width = 0
|
||||||
|
|
||||||
#behindMs = 0
|
#behindMs = 0
|
||||||
#currentTiming = 0
|
#currentTiming = 0
|
||||||
@@ -35,6 +37,20 @@ export default class Game {
|
|||||||
return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat()
|
return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
action(id, options) {
|
||||||
|
const entity = this.entities.find((it) => it.id == id)
|
||||||
|
if (entity == null) {
|
||||||
|
console.error({ error: 'Invalid ID' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.action == 'attack') { entity.attackAction(new Vector2(options.x, options.y)) }
|
||||||
|
if (options.action == 'cast') { entity.castAction(options.slot, new Vector2(options.x, options.y)) }
|
||||||
|
if (options.action == 'halt') { entity.haltAction(), delay }
|
||||||
|
if (options.action == 'stop') { entity.stopAction() }
|
||||||
|
if (options.action == 'move') { entity.moveAction(new Vector2(options.x, options.y)) }
|
||||||
|
}
|
||||||
|
|
||||||
addTerrain(terrain) {
|
addTerrain(terrain) {
|
||||||
this.terrains.push(terrain)
|
this.terrains.push(terrain)
|
||||||
}
|
}
|
||||||
@@ -82,7 +98,28 @@ export default class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
setInterval(this.#gameLoopCall.bind(this), 1)
|
if (this.gameLoopIntervalId != null) { return }
|
||||||
|
|
||||||
|
this.gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (this.gameLoopIntervalId == null) { return }
|
||||||
|
|
||||||
|
clearInterval(this.gameLoopIntervalId)
|
||||||
|
this.gameLoopIntervalId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription(websocket, id) {
|
||||||
|
return function builtSubscription() {
|
||||||
|
const game = this
|
||||||
|
const entity = game.entities.find((it) => it.id == id)
|
||||||
|
if (entity == null) { return }
|
||||||
|
|
||||||
|
const team = entity.team
|
||||||
|
const message = game.visionByTeam(team)
|
||||||
|
websocket.send(JSON.stringify(message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -98,8 +135,31 @@ export default class Game {
|
|||||||
this.currentTick++
|
this.currentTick++
|
||||||
}
|
}
|
||||||
|
|
||||||
visionByTeam() {
|
visibleEntities(team) {
|
||||||
return null // TODO: vision
|
const visionSources = this.visionSources(team)
|
||||||
|
return Array.from(new Set(visionSources.map((it) => it.entitiesInVision()).flat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleProjectiles(team) {
|
||||||
|
const visionSources = this.visionSources(team)
|
||||||
|
return Array.from(new Set(visionSources.map((it) => it.projectilesInVision()).flat()))
|
||||||
|
}
|
||||||
|
|
||||||
|
visionSources(team) {
|
||||||
|
const entityVisionSources = this.entities.filter((it) => it.team == team)
|
||||||
|
const projectileVisionSources = this.projectiles.filter((it) => it.visionRange > 0 && (it.team == null || it.team == team))
|
||||||
|
return entityVisionSources.concat(projectileVisionSources)
|
||||||
|
}
|
||||||
|
|
||||||
|
visionByTeam(team) {
|
||||||
|
const visionSources = this.visionSources(team)
|
||||||
|
const visibleEntities = new Set(visionSources.map((it) => it.entitiesInVision()).flat())
|
||||||
|
const visibleProjectiles = new Set(visionSources.map((it) => it.projectilesInVision()).flat())
|
||||||
|
return {
|
||||||
|
...this,
|
||||||
|
entities: this.entities.filter((it) => visibleEntities.has(it.id)),
|
||||||
|
projectiles: this.projectiles.filter((it) => visibleProjectiles.has(it.id)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#calculateTickMetrics() {
|
#calculateTickMetrics() {
|
||||||
|
|||||||
+8
-36
@@ -1,8 +1,7 @@
|
|||||||
import { Vector2 } from 'three'
|
|
||||||
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'
|
||||||
import { Dungeon, Ravine } from './level.js'
|
import { Dungeon } from './level.js'
|
||||||
|
|
||||||
const app = new WebSocketExpress()
|
const app = new WebSocketExpress()
|
||||||
const port = 1280
|
const port = 1280
|
||||||
@@ -20,52 +19,25 @@ app.use('/tools/', express.static('tools'))
|
|||||||
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))
|
websocket.on('message', (rawData) => {
|
||||||
|
const message = JSON.parse(rawData)
|
||||||
|
console.log(message)
|
||||||
|
if (message.action == 'join') {
|
||||||
|
const subscription = game.subscription(websocket, message.id).bind(game)
|
||||||
game.eventEmitter.on('tick', subscription)
|
game.eventEmitter.on('tick', subscription)
|
||||||
|
|
||||||
websocket.on('close', () => {
|
websocket.on('close', () => {
|
||||||
game.eventEmitter.removeListener('tick', subscription)
|
game.eventEmitter.removeListener('tick', subscription)
|
||||||
})
|
})
|
||||||
|
|
||||||
websocket.on('message', (rawData) => {
|
|
||||||
let delay = 0
|
|
||||||
const message = JSON.parse(rawData)
|
|
||||||
const entity = message.id != null ? game.entities.find((e) => e.id == message.id) : null
|
|
||||||
if (entity == null) {
|
|
||||||
console.error({ error: { reason: 'Invalid ID', message } })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
console.log(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action == 'attack') {
|
game.action(message.id, message)
|
||||||
setTimeout(() => entity.attackAction(new Vector2(message.x, message.y)), delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action == 'cast') {
|
|
||||||
setTimeout(() => entity.castAction(message.slot, new Vector2(message.x, message.y)), delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action == 'halt') {
|
|
||||||
setTimeout(() => entity.haltAction(), delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action == 'stop') {
|
|
||||||
setTimeout(() => entity.stopAction(), delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.action == 'move') {
|
|
||||||
setTimeout(() => entity.moveAction(new Vector2(message.x, message.y)), delay)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Server started! Visit http://localhost:${port}`)
|
console.log(`Server started! Visit http://localhost:${port}`)
|
||||||
|
|
||||||
// Dungeon.scenario(game)
|
Dungeon.scenario(game)
|
||||||
Ravine.scenario(game)
|
|
||||||
|
|
||||||
game.start()
|
|
||||||
})
|
})
|
||||||
|
|||||||
+27
-1
@@ -1,11 +1,35 @@
|
|||||||
import { Vector2 } from 'three'
|
import { Vector2 } from 'three'
|
||||||
|
import Ability from './ability.js'
|
||||||
|
import Entity from './entity.js'
|
||||||
import Team from './team.js'
|
import Team from './team.js'
|
||||||
import Template from './template.js'
|
import Template from './template.js'
|
||||||
import Terrain from './terrain.js'
|
import Terrain from './terrain.js'
|
||||||
import Entity from './entity.js'
|
|
||||||
|
|
||||||
export class Dungeon {
|
export class Dungeon {
|
||||||
|
static scenario(game) {
|
||||||
|
game.width = 3000
|
||||||
|
game.height = 3000
|
||||||
|
|
||||||
|
const playerSpawn = new Vector2(game.width / 2, game.height / 2)
|
||||||
|
game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: playerSpawn, team: Team.blue })))
|
||||||
|
game.entities.at(0).moveAction(playerSpawn.clone().add(new Vector2(0, -200)))
|
||||||
|
const dummyLogic = function dummyLogic() {
|
||||||
|
if (game.currentTick % (3 * game.tickRate) == 0) {
|
||||||
|
this.castAction('q', playerSpawn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dummy = { radius: 100, visualRadius: 50, abilities: { q: Ability.straightShot.id }, logic: dummyLogic }
|
||||||
|
game.spawnEntity(new Entity({ ...dummy, position: new Vector2(1 * (game.width / 5), 1 * (game.height / 4)) }))
|
||||||
|
game.spawnEntity(new Entity({ ...dummy, position: new Vector2(1 * (game.width / 5), 3 * (game.height / 4)) }))
|
||||||
|
game.addTerrain(new Terrain([
|
||||||
|
new Vector2(3 * (game.width / 10), 2 * (game.height / 5)),
|
||||||
|
new Vector2(3 * (game.width / 10), 1 * (game.height / 5)),
|
||||||
|
new Vector2(4 * (game.width / 10), 1 * (game.height / 5)),
|
||||||
|
new Vector2(4 * (game.width / 10), 2 * (game.height / 5)),
|
||||||
|
]))
|
||||||
|
|
||||||
|
game.start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Ravine {
|
export class Ravine {
|
||||||
@@ -21,6 +45,8 @@ export class Ravine {
|
|||||||
team: Team.blue,
|
team: Team.blue,
|
||||||
}))
|
}))
|
||||||
game.spawnEntity(player1)
|
game.spawnEntity(player1)
|
||||||
|
|
||||||
|
game.start()
|
||||||
}
|
}
|
||||||
static logic() {
|
static logic() {
|
||||||
const game = this
|
const game = this
|
||||||
|
|||||||
+24
-3
@@ -1,7 +1,7 @@
|
|||||||
import { Vector2 } from 'three'
|
import { Vector2 } from 'three'
|
||||||
|
import Entity from './entity.js'
|
||||||
import SAT from 'sat'
|
import SAT from 'sat'
|
||||||
import SATX from './satx.js'
|
import SATX from './satx.js'
|
||||||
import Entity from './entity.js'
|
|
||||||
|
|
||||||
export default class Projectile {
|
export default class Projectile {
|
||||||
id = `projectile-${Projectile.nextId()}`
|
id = `projectile-${Projectile.nextId()}`
|
||||||
@@ -13,8 +13,11 @@ export default class Projectile {
|
|||||||
memory = {}
|
memory = {}
|
||||||
owner = null
|
owner = null
|
||||||
position = new Vector2()
|
position = new Vector2()
|
||||||
radius = 5
|
radius = 0
|
||||||
speed = 1000
|
speed = 1000
|
||||||
|
team = null
|
||||||
|
visibleThroughTerrain = true
|
||||||
|
visionRange = 0
|
||||||
visualRadius = null
|
visualRadius = null
|
||||||
|
|
||||||
#after = null
|
#after = null
|
||||||
@@ -56,6 +59,15 @@ export default class Projectile {
|
|||||||
this.game?.despawn(this)
|
this.game?.despawn(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entitiesInVision() {
|
||||||
|
const entities = this.game?.entities
|
||||||
|
if (entities == null) { return }
|
||||||
|
|
||||||
|
const entitiesInVisionRange = entities.filter((it) => it.id != this.id && it.distanceTo(this.position) <= this.visionRange + it.radius)
|
||||||
|
|
||||||
|
return entitiesInVisionRange.concat([this]).map((it) => it.id)
|
||||||
|
}
|
||||||
|
|
||||||
setPosition(vector) {
|
setPosition(vector) {
|
||||||
this.position.copy(vector)
|
this.position.copy(vector)
|
||||||
this.#calculateBbox()
|
this.#calculateBbox()
|
||||||
@@ -66,10 +78,19 @@ export default class Projectile {
|
|||||||
this.#checkStationaryCollisions()
|
this.#checkStationaryCollisions()
|
||||||
this.#checkIfArrived()
|
this.#checkIfArrived()
|
||||||
if (this.#logic != null) {
|
if (this.#logic != null) {
|
||||||
this.#logic()
|
this.#logic(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectilesInVision() {
|
||||||
|
const projectiles = this.game?.projectiles
|
||||||
|
if (projectiles == null) { return }
|
||||||
|
|
||||||
|
const projectilesInVisionRange = projectiles.filter((it) => this.position.distanceTo(it.position) <= this.visionRange + it.radius)
|
||||||
|
|
||||||
|
return projectilesInVisionRange.map((it) => it.id)
|
||||||
|
}
|
||||||
|
|
||||||
#calculateBbox() {
|
#calculateBbox() {
|
||||||
this.bbox[0] = this.position.y + this.radius
|
this.bbox[0] = this.position.y + this.radius
|
||||||
this.bbox[1] = this.position.x + this.radius
|
this.bbox[1] = this.position.x + this.radius
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export default class Template {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: minion aggro
|
|
||||||
// TODO: incremental pathfinding stuck in thicker than recalculateDestRadius walls
|
// TODO: incremental pathfinding stuck in thicker than recalculateDestRadius walls
|
||||||
// TODO: minions despawn prematurely (too large checkpointSize?)
|
// TODO: minions despawn prematurely (too large checkpointSize?)
|
||||||
static #minionLogic(route = []) {
|
static #minionLogic(route = []) {
|
||||||
|
|||||||
Reference in New Issue
Block a user