fix dead state
This commit is contained in:
+38
-8
@@ -197,11 +197,18 @@ function connectWebSocket() {
|
|||||||
state.height = stateUpdates.height
|
state.height = stateUpdates.height
|
||||||
|
|
||||||
minimapCamera.top = state.height / 200
|
minimapCamera.top = state.height / 200
|
||||||
minimapCamera.right = state.height / 200
|
minimapCamera.right = state.width / 200
|
||||||
minimapCamera.bottom = -state.height / 200
|
minimapCamera.bottom = -state.height / 200
|
||||||
minimapCamera.left = -state.height / 200
|
minimapCamera.left = -state.width / 200
|
||||||
minimapCamera.updateProjectionMatrix()
|
minimapCamera.updateProjectionMatrix()
|
||||||
minimapCamera.position.set(state.width / 200, state.height / 200, minimapCameraZ)
|
minimapCamera.position.set(state.width / 200, state.height / 200, minimapCameraZ)
|
||||||
|
|
||||||
|
const size = 300
|
||||||
|
const wide = state.width > state.height
|
||||||
|
minimapRenderer.setSize(
|
||||||
|
wide ? size : (state.width / state.height) * size,
|
||||||
|
wide ? (state.height / state.width) * size : size,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(stateUpdates)) {
|
for (const [key, value] of Object.entries(stateUpdates)) {
|
||||||
@@ -333,8 +340,8 @@ function connectWebSocket() {
|
|||||||
rotationBase.add(castingMarker)
|
rotationBase.add(castingMarker)
|
||||||
|
|
||||||
const rangeMaterial = teamMaterials['range']
|
const rangeMaterial = teamMaterials['range']
|
||||||
const rangeSize = e.visionRange ?? 0
|
// 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) / 100, (rangeSize) / 100, 1), rangeMaterial)
|
||||||
const rangeMarkerSize = 5000
|
const rangeMarkerSize = 5000
|
||||||
rangeMarker.scale.y = e.height / rangeMarkerSize
|
rangeMarker.scale.y = e.height / rangeMarkerSize
|
||||||
@@ -346,18 +353,32 @@ function connectWebSocket() {
|
|||||||
entities[e.id] = entity
|
entities[e.id] = entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entity.children.at(0).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.buffs.some((it) => it.id == 'exposed') // TODO: only works for Exposed now
|
||||||
|
|
||||||
|
let z = e.height / 100
|
||||||
|
|
||||||
|
if (e.dead) {
|
||||||
|
entity.rotation.x = 0
|
||||||
|
entity.position.z = 0
|
||||||
|
z = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entity.rotation.x = Math.PI / 2
|
||||||
|
entity.position.z = e.height / 100
|
||||||
|
}
|
||||||
|
|
||||||
entity.userData.flaggedForRemoval = false
|
entity.userData.flaggedForRemoval = false
|
||||||
entity.children.at(3).rotation.y = e.rotation
|
entity.children.at(3).rotation.y = e.rotation
|
||||||
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z: e.height / 100 }, tweenDuration).start()
|
positionTweens[entity.id] = new Tween(entity.position).to({ x: e.position.x / 100, y: e.position.y / 100, z }, 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 // TODO: undo, just for clarity
|
// entity.children.at(4).visible = e.id == playerId
|
||||||
entity.children.at(3).children.at(0).visible = e.casting != null
|
entity.children.at(3).children.at(0).visible = e.casting != null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,8 +467,8 @@ function connectWebSocket() {
|
|||||||
if (playerId != null) {
|
if (playerId != null) {
|
||||||
const player = state.entities.find((e) => e.id == playerId)
|
const player = state.entities.find((e) => e.id == playerId)
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) {
|
for (let abilityIndex = 0; abilityIndex < 7; abilityIndex++) {
|
||||||
const abilityKey = ['a', 'q', 'w', 'e'][abilityIndex]
|
const abilityKey = ['a', 'q', 'w', 'e', 'r', 'd', 'f'][abilityIndex]
|
||||||
if (player.abilities[abilityKey] != null) {
|
if (player.abilities[abilityKey] != null) {
|
||||||
const abilityId = player.abilities[abilityKey]
|
const abilityId = player.abilities[abilityKey]
|
||||||
const ability = state.abilities.find((it) => it.id == abilityId)
|
const ability = state.abilities.find((it) => it.id == abilityId)
|
||||||
@@ -565,6 +586,15 @@ window.addEventListener('load', () => {
|
|||||||
if (event.code == 'KeyE') {
|
if (event.code == 'KeyE') {
|
||||||
websocket.send(JSON.stringify({ action: 'cast', slot: 'e', id: playerId, x, y }))
|
websocket.send(JSON.stringify({ action: 'cast', slot: 'e', id: playerId, x, y }))
|
||||||
}
|
}
|
||||||
|
if (event.code == 'KeyR') {
|
||||||
|
websocket.send(JSON.stringify({ action: 'cast', slot: 'r', id: playerId, x, y }))
|
||||||
|
}
|
||||||
|
if (event.code == 'KeyD') {
|
||||||
|
websocket.send(JSON.stringify({ action: 'cast', slot: 'd', id: playerId, x, y }))
|
||||||
|
}
|
||||||
|
if (event.code == 'KeyF') {
|
||||||
|
websocket.send(JSON.stringify({ action: 'cast', slot: 'f', id: playerId, x, y }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,21 @@
|
|||||||
<div id="ability-3-cooldown" class="cooldown"></div>
|
<div id="ability-3-cooldown" class="cooldown"></div>
|
||||||
<div id="ability-3-cooldown-text" class="cooldown-text"></div>
|
<div id="ability-3-cooldown-text" class="cooldown-text"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="ability-4" class="ability">
|
||||||
|
R
|
||||||
|
<div id="ability-4-cooldown" class="cooldown"></div>
|
||||||
|
<div id="ability-4-cooldown-text" class="cooldown-text"></div>
|
||||||
|
</div>
|
||||||
|
<div id="ability-5" class="ability">
|
||||||
|
D
|
||||||
|
<div id="ability-5-cooldown" class="cooldown"></div>
|
||||||
|
<div id="ability-5-cooldown-text" class="cooldown-text"></div>
|
||||||
|
</div>
|
||||||
|
<div id="ability-6" class="ability">
|
||||||
|
F
|
||||||
|
<div id="ability-6-cooldown" class="cooldown"></div>
|
||||||
|
<div id="ability-6-cooldown-text" class="cooldown-text"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="buffs" class="buffs"></div>
|
<div id="buffs" class="buffs"></div>
|
||||||
<script type="module" src="client.js"></script>
|
<script type="module" src="client.js"></script>
|
||||||
|
|||||||
+71
-5
@@ -10,7 +10,7 @@ export default class Ability {
|
|||||||
|
|
||||||
name = 'Ability'
|
name = 'Ability'
|
||||||
|
|
||||||
castTime = 0
|
castTime = null
|
||||||
cooldown = 0
|
cooldown = 0
|
||||||
damage = 0
|
damage = 0
|
||||||
moveCancelable = false
|
moveCancelable = false
|
||||||
@@ -18,15 +18,17 @@ export default class Ability {
|
|||||||
range = 0
|
range = 0
|
||||||
speed = 1000
|
speed = 1000
|
||||||
|
|
||||||
#effect = () => {}
|
#effect = null
|
||||||
|
|
||||||
get effect() { return this.#effect }
|
get effect() { return this.#effect ?? Ability.noEffect }
|
||||||
set effect(value) { this.#effect = value }
|
set effect(value) { this.#effect = value }
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get noEffect() { return function noEffect() {} }
|
||||||
|
|
||||||
static straightShot = new Ability({
|
static straightShot = new Ability({
|
||||||
id: 'straight_shot',
|
id: 'straight_shot',
|
||||||
name: 'Straight Shot',
|
name: 'Straight Shot',
|
||||||
@@ -155,8 +157,7 @@ export default class Ability {
|
|||||||
static blink = new Ability({
|
static blink = new Ability({
|
||||||
id: 'blink',
|
id: 'blink',
|
||||||
name: 'Blink',
|
name: 'Blink',
|
||||||
castTime: 0.25,
|
cooldown: 10,
|
||||||
cooldown: 2,
|
|
||||||
range: 475,
|
range: 475,
|
||||||
effect: function blinkEffect(caster, cursor) {
|
effect: function blinkEffect(caster, cursor) {
|
||||||
const ability = this
|
const ability = this
|
||||||
@@ -246,4 +247,69 @@ export default class Ability {
|
|||||||
caster.game?.spawnProjectile(projectile)
|
caster.game?.spawnProjectile(projectile)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
static circleOfResurrectionChannel = new Ability({
|
||||||
|
id: 'channel:circle_of_resurrection',
|
||||||
|
name: 'Channeling: Circle of Resurrection',
|
||||||
|
castTime: 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
static circleOfResurrection = new Ability({
|
||||||
|
id: 'circle_of_resurrection',
|
||||||
|
name: 'Circle of Resurrection',
|
||||||
|
castTime: 0.5,
|
||||||
|
cooldown: 100,
|
||||||
|
duration: 3,
|
||||||
|
radius: 300,
|
||||||
|
range: 300,
|
||||||
|
effect: function circleOfResurrectionEffect(caster, cursor) {
|
||||||
|
const ability = this
|
||||||
|
caster.haltAction()
|
||||||
|
|
||||||
|
const direction = cursor.clone().sub(caster.position)
|
||||||
|
if (direction.length() > ability.range) {
|
||||||
|
direction.normalize().multiplyScalar(ability.range)
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = caster.position.clone().add(direction)
|
||||||
|
|
||||||
|
const team = caster.team
|
||||||
|
const currentTick = caster.game?.currentTick ?? 0
|
||||||
|
const duration = caster.game?.secToTick(ability.duration) ?? 0
|
||||||
|
const despawnAfter = currentTick + duration
|
||||||
|
const casterPosition = caster.position.clone()
|
||||||
|
|
||||||
|
const circleOfResurrectionLogic = function castingVisionLogic(projectile) {
|
||||||
|
const currentTick = projectile.game?.currentTick ?? 0
|
||||||
|
if (casterPosition.distanceTo(caster.position) > 1) {
|
||||||
|
projectile.despawn()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTick > despawnAfter) {
|
||||||
|
const entities = projectile.game?.entities ?? []
|
||||||
|
const pos = projectile.position
|
||||||
|
projectile.despawn()
|
||||||
|
const nearbyDeadTeammates = entities.filter((it) => it.dead && it.team == team)
|
||||||
|
const closestDeadTeammate = nearbyDeadTeammates.reduce((e1, e2) => (e1?.distanceTo(pos) ?? Infinity) < e2.distanceTo(pos) ? e1 : e2, null)
|
||||||
|
if (closestDeadTeammate != null) {
|
||||||
|
closestDeadTeammate.revive(closestDeadTeammate.maxHealth / 4)
|
||||||
|
caster.cooldown(ability.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectile = new Projectile({
|
||||||
|
logic: circleOfResurrectionLogic,
|
||||||
|
owner: caster.id,
|
||||||
|
position: destination,
|
||||||
|
radius: ability.radius,
|
||||||
|
visualRadius: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
caster.game?.spawnProjectile(projectile)
|
||||||
|
if (caster.casting != null) {
|
||||||
|
caster.forceCast(Ability.circleOfResurrectionChannel.id, destination)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -7,11 +7,13 @@ export default class Buff {
|
|||||||
|
|
||||||
duration = 0
|
duration = 0
|
||||||
|
|
||||||
#effect = () => {}
|
#effect = null
|
||||||
|
|
||||||
get effect() { return this.#effect }
|
get effect() { return this.#effect ?? Buff.noEffect }
|
||||||
set effect(value) { this.#effect = value }
|
set effect(value) { this.#effect = value }
|
||||||
|
|
||||||
|
static get noEffect() { return function noEffect() {} }
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-7
@@ -172,10 +172,14 @@ export default class Entity {
|
|||||||
set y(value) { this.position.y = value }
|
set y(value) { this.position.y = value }
|
||||||
|
|
||||||
attackAction(cursor) {
|
attackAction(cursor) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
this.moveAction(cursor, true)
|
this.moveAction(cursor, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
castAction(slot, cursor, halt = false) {
|
castAction(slot, cursor, halt = false) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
const ability = this.ability(slot)
|
const ability = this.ability(slot)
|
||||||
if (ability == null) { return }
|
if (ability == null) { return }
|
||||||
|
|
||||||
@@ -197,6 +201,12 @@ export default class Entity {
|
|||||||
this.rotation = targetPosition.clone().sub(this.position).angle()
|
this.rotation = targetPosition.clone().sub(this.position).angle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ability.castTime == null) {
|
||||||
|
ability.effect(this, cursor)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const cooldown = this.game?.secToTick(ability.cooldown) ?? 0
|
const cooldown = this.game?.secToTick(ability.cooldown) ?? 0
|
||||||
const lastCast = this.cooldowns[ability.id]
|
const lastCast = this.cooldowns[ability.id]
|
||||||
const timestamp = this.game?.currentTick ?? 0
|
const timestamp = this.game?.currentTick ?? 0
|
||||||
@@ -210,10 +220,14 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
haltAction() {
|
haltAction() {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
this.#moving = false
|
this.#moving = false
|
||||||
}
|
}
|
||||||
|
|
||||||
moveAction(cursor, attack = false) {
|
moveAction(cursor, attack = false) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
if (this.casting != null && this.game?.abilities.filter((it) => it.id == 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 == this.abilities[0])) {
|
if (!attack && !(this.casting != null && this.casting.ability == this.abilities[0])) {
|
||||||
this.casting = null
|
this.casting = null
|
||||||
@@ -226,6 +240,8 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopAction() {
|
stopAction() {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
this.casting = null
|
this.casting = null
|
||||||
this.#moving = true
|
this.#moving = true
|
||||||
this.#attacking = false
|
this.#attacking = false
|
||||||
@@ -301,10 +317,12 @@ export default class Entity {
|
|||||||
|
|
||||||
customBboxCollidables(bbox) {
|
customBboxCollidables(bbox) {
|
||||||
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 ?? [])
|
||||||
return entitiesAndTerrains.filter((it) => 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
damage(amount, source = null) {
|
damage(amount, source = null) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
let damage = amount
|
let damage = amount
|
||||||
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)
|
||||||
@@ -325,6 +343,27 @@ export default class Entity {
|
|||||||
return this.position.distanceTo(cursor)
|
return this.position.distanceTo(cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceCast(abilityId, cursor) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
|
const ability = this.game?.abilities.find((it) => it.id == abilityId)
|
||||||
|
if (ability == null) { return }
|
||||||
|
|
||||||
|
const targetPosition = (cursor instanceof Vector2) ? cursor : this.game?.entities.find((it) => it.id == cursor)?.position
|
||||||
|
if (targetPosition instanceof Vector2) {
|
||||||
|
this.rotation = targetPosition.clone().sub(this.position).angle()
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = this.game?.currentTick ?? 0
|
||||||
|
|
||||||
|
if (ability.castTime == null) {
|
||||||
|
ability.effect(this, cursor)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.casting = { ability: ability.id, cursor, timestamp }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
futureCollidables(futurePosition) {
|
futureCollidables(futurePosition) {
|
||||||
return this.customBboxCollidables(new Float32Array([
|
return this.customBboxCollidables(new Float32Array([
|
||||||
futurePosition.y + this.radius,
|
futurePosition.y + this.radius,
|
||||||
@@ -335,6 +374,8 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getBuff(id) {
|
getBuff(id) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
const entityBuff = this.buffs.find((it) => it.id == id)
|
const entityBuff = this.buffs.find((it) => it.id == id)
|
||||||
if (entityBuff == null) { return }
|
if (entityBuff == null) { return }
|
||||||
|
|
||||||
@@ -345,10 +386,14 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasBuff(id) {
|
hasBuff(id) {
|
||||||
|
if (this.dead) { return false }
|
||||||
|
|
||||||
return this.buffs.some((it) => it.id == id) && this.game?.buffs.some((it) => it.id == id)
|
return this.buffs.some((it) => it.id == id) && this.game?.buffs.some((it) => it.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
heal(amount) {
|
heal(amount) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
|
this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +448,7 @@ export default class Entity {
|
|||||||
isInLineOfSight(destination, position = this.position) {
|
isInLineOfSight(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 ?? [])
|
||||||
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
|
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
|
||||||
if (bboxCheckedObstacles.length < 1) { return true }
|
if (bboxCheckedObstacles.length < 1) { return true }
|
||||||
|
|
||||||
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
|
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
|
||||||
@@ -429,7 +474,7 @@ export default class Entity {
|
|||||||
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 ?? [])
|
||||||
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
|
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && it.collision && !((this.ghosting && it.ghostable) || (this.ghostable && it.ghosting)) && SATX.bboxCheck(bbox, it.bbox))
|
||||||
if (bboxCheckedObstacles.length < 1) { return [] }
|
if (bboxCheckedObstacles.length < 1) { return [] }
|
||||||
|
|
||||||
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
|
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
|
||||||
@@ -437,6 +482,8 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeBuff(id) {
|
removeBuff(id) {
|
||||||
|
if (this.dead) { return }
|
||||||
|
|
||||||
this.buffs = this.buffs.filter((it) => it.id != id)
|
this.buffs = this.buffs.filter((it) => it.id != id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +493,15 @@ export default class Entity {
|
|||||||
this.dead = false
|
this.dead = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
revive(startingHealth = null) {
|
||||||
|
this.dead = false
|
||||||
|
const health = (startingHealth ?? this.maxHealth)
|
||||||
|
this.health = Math.max(0, Math.min(health, this.maxHealth))
|
||||||
|
|
||||||
|
this.#calculateCollider()
|
||||||
|
this.#calculateVision()
|
||||||
|
}
|
||||||
|
|
||||||
setPosition(vector) {
|
setPosition(vector) {
|
||||||
this.position.copy(vector)
|
this.position.copy(vector)
|
||||||
this.#calculateCollider()
|
this.#calculateCollider()
|
||||||
@@ -475,15 +531,14 @@ export default class Entity {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make non-race-condition calculations multi-threaded
|
|
||||||
update() {
|
update() {
|
||||||
|
this.#calculateVision()
|
||||||
|
this.#checkHealth()
|
||||||
if (this.dead) {
|
if (this.dead) {
|
||||||
// TODO: do something while the entity is dead (and disallow casting, vision, etc)
|
// TODO: do something while the entity is dead (and disallow casting, vision, etc)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.#calculateVision()
|
|
||||||
this.#cast()
|
this.#cast()
|
||||||
this.#checkHealth()
|
|
||||||
this.#move()
|
this.#move()
|
||||||
this.#tickBuffs()
|
this.#tickBuffs()
|
||||||
this.fixPosition()
|
this.fixPosition()
|
||||||
@@ -523,6 +578,12 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#calculateVision() {
|
#calculateVision() {
|
||||||
|
if (this.dead) {
|
||||||
|
this.#entitiesInVision = [this.id]
|
||||||
|
this.#projectilesInVision = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const entities = this.game?.entities ?? []
|
const entities = this.game?.entities ?? []
|
||||||
const projectiles = this.game?.projectiles ?? []
|
const projectiles = this.game?.projectiles ?? []
|
||||||
|
|
||||||
@@ -555,15 +616,22 @@ export default class Entity {
|
|||||||
|
|
||||||
ability.effect(this, this.casting.cursor)
|
ability.effect(this, this.casting.cursor)
|
||||||
|
|
||||||
|
if (this.casting.ability == ability.id) {
|
||||||
this.casting = null
|
this.casting = null
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: only spawn castingVision if slightly outside regular vision (or obstructed)
|
// TODO: only spawn castingVision if slightly outside regular vision (or obstructed)
|
||||||
Ability.castingVision.effect(this, this.position)
|
Ability.castingVision.effect(this, this.position)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
#checkHealth() {
|
#checkHealth() {
|
||||||
if (this.health <= 0) {
|
if (!this.dead && this.health <= 0) {
|
||||||
this.dead = true
|
this.dead = true
|
||||||
|
this.buffs = []
|
||||||
|
}
|
||||||
|
else if (this.dead && this.health > 0) {
|
||||||
|
this.health = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -19,21 +19,18 @@ export default class Game {
|
|||||||
tickRate = 30
|
tickRate = 30
|
||||||
width = 0
|
width = 0
|
||||||
|
|
||||||
#eventEmitter = new EventEmitter()
|
|
||||||
#gameLoopIntervalId = null
|
#gameLoopIntervalId = null
|
||||||
#logic = null
|
#logic = null
|
||||||
#nextTickAt = 0
|
#nextTickAt = 0
|
||||||
#startTimestamp = 0
|
#startTimestamp = 0
|
||||||
|
#subscriptions = new Map()
|
||||||
#tickBudget = 1000 / this.tickRate
|
#tickBudget = 1000 / this.tickRate
|
||||||
|
|
||||||
get logic() { return this.#logic }
|
get logic() { return this.#logic }
|
||||||
get eventEmitter() { return this.#eventEmitter }
|
|
||||||
get tickBudget() { return this.#tickBudget }
|
get tickBudget() { return this.#tickBudget }
|
||||||
set logic(value) { this.#logic = value }
|
get subscriptions() { return this.#subscriptions }
|
||||||
|
|
||||||
constructor() {
|
set logic(value) { this.#logic = value }
|
||||||
this.#eventEmitter.setMaxListeners(20)
|
|
||||||
}
|
|
||||||
|
|
||||||
action(id, options) {
|
action(id, options) {
|
||||||
const entity = this.entities.find((it) => it.id == id)
|
const entity = this.entities.find((it) => it.id == id)
|
||||||
@@ -140,6 +137,10 @@ export default class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
for (const subscription of this.#subscriptions.values()) {
|
||||||
|
subscription()
|
||||||
|
}
|
||||||
|
|
||||||
const callUpdate = function callUpdate(object) { object.update() }
|
const callUpdate = function callUpdate(object) { object.update() }
|
||||||
this.entities.forEach(callUpdate) // TODO: entity with lower ID has unfair collision advantage (regular loop + until it fully loops around with an offset?)
|
this.entities.forEach(callUpdate) // TODO: entity with lower ID has unfair collision advantage (regular loop + until it fully loops around with an offset?)
|
||||||
this.projectiles.forEach(callUpdate)
|
this.projectiles.forEach(callUpdate)
|
||||||
@@ -147,8 +148,6 @@ export default class Game {
|
|||||||
this.#logic()
|
this.#logic()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.eventEmitter.emit('tick')
|
|
||||||
|
|
||||||
this.currentTick++
|
this.currentTick++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
import { Dungeon, Ravine } from './level.js'
|
import { Dungeon } 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'
|
||||||
@@ -33,13 +33,14 @@ app.ws('/ws', async (req, res) => {
|
|||||||
console.log(message)
|
console.log(message)
|
||||||
if (message.action == 'join') {
|
if (message.action == 'join') {
|
||||||
const id = message.id
|
const id = message.id
|
||||||
|
const connectionId = crypto.randomUUID()
|
||||||
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.eventEmitter.on('tick', subscription)
|
game.subscriptions.set(connectionId, subscription)
|
||||||
|
|
||||||
websocket.on('close', () => {
|
websocket.on('close', () => {
|
||||||
console.log({ event: 'disconnected', id })
|
console.log({ event: 'disconnected', id })
|
||||||
game.eventEmitter.removeListener('tick', subscription)
|
game.subscriptions.delete(connectionId)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,5 @@ app.ws('/ws', async (req, res) => {
|
|||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.info(`Server started! Visit http://localhost:${port}`)
|
console.info(`Server started! Visit http://localhost:${port}`)
|
||||||
|
|
||||||
// Dungeon.scenario(game)
|
Dungeon.scenario(game)
|
||||||
Ravine.scenario(game)
|
|
||||||
})
|
})
|
||||||
|
|||||||
+20
-3
@@ -6,11 +6,28 @@ import Terrain from './terrain.js'
|
|||||||
|
|
||||||
export class Dungeon {
|
export class Dungeon {
|
||||||
static scenario(game) {
|
static scenario(game) {
|
||||||
game.width = 3000
|
game.width = 2500
|
||||||
game.height = 3000
|
game.height = 1500
|
||||||
|
|
||||||
const from = new Vector2(100, 100)
|
const team = Team.blue
|
||||||
|
const enemy = Team.neutral
|
||||||
|
|
||||||
|
game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: new Vector2(1500, 700), team })))
|
||||||
|
game.spawnEntity(new Entity(Template.player({ id: '2', spawnPosition: new Vector2(200, 1300), team, health: 10 })))
|
||||||
|
|
||||||
|
game.spawnEntity(new Entity(Template.basilisk({ id: 'boss', spawnPosition: new Vector2(2200, 750), team: enemy })))
|
||||||
|
|
||||||
|
setTimeout(() => game.entities.find((it) => it.id == '1').damage(9999), 10)
|
||||||
|
|
||||||
|
game.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Zigzag {
|
||||||
|
static scenario(game) {
|
||||||
|
game.width = 3000
|
||||||
game.height = 2000
|
game.height = 2000
|
||||||
|
const from = new Vector2(100, 100)
|
||||||
game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: from, pathfindingObstacleLimit: 1, pathfindingCooldown: 0 })))
|
game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: from, pathfindingObstacleLimit: 1, pathfindingCooldown: 0 })))
|
||||||
for (let i = 100; i < game.width; i += 300) {
|
for (let i = 100; i < game.width; i += 300) {
|
||||||
const highest = ((i - 100) % 600) == 0 ? 0 : 500
|
const highest = ((i - 100) % 600) == 0 ? 0 : 500
|
||||||
|
|||||||
+25
-4
@@ -3,6 +3,19 @@ import Ability from './ability.js'
|
|||||||
import Team from './team.js'
|
import Team from './team.js'
|
||||||
|
|
||||||
export default class Template {
|
export default class Template {
|
||||||
|
static basilisk(overrides) {
|
||||||
|
return {
|
||||||
|
abilities: {},
|
||||||
|
height: 100,
|
||||||
|
logic: this.#basiliskLogic,
|
||||||
|
radius: 180,
|
||||||
|
speed: 230,
|
||||||
|
visualRadius: 170,
|
||||||
|
maxHealth: 3000,
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 },
|
||||||
@@ -26,7 +39,9 @@ export default class Template {
|
|||||||
a: Ability.rangedAttack.id,
|
a: Ability.rangedAttack.id,
|
||||||
q: Ability.straightShot.id,
|
q: Ability.straightShot.id,
|
||||||
w: Ability.expose.id,
|
w: Ability.expose.id,
|
||||||
e: Ability.blink.id,
|
e: Ability.control.id,
|
||||||
|
d: Ability.circleOfResurrection.id,
|
||||||
|
f: Ability.blink.id,
|
||||||
},
|
},
|
||||||
height: 80,
|
height: 80,
|
||||||
logic: this.#playerLogic,
|
logic: this.#playerLogic,
|
||||||
@@ -40,6 +55,12 @@ export default class Template {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static #basiliskLogic() {
|
||||||
|
const entity = this
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
static #minionLogic(route = [], odd = false) {
|
static #minionLogic(route = [], odd = false) {
|
||||||
const checkpointSize = 300
|
const checkpointSize = 300
|
||||||
const recalculateDestRadius = 50
|
const recalculateDestRadius = 50
|
||||||
@@ -87,8 +108,8 @@ export default class Template {
|
|||||||
|
|
||||||
static #playerLogic() {
|
static #playerLogic() {
|
||||||
const entity = this
|
const entity = this
|
||||||
if (entity.dead) {
|
// if (entity.dead) {
|
||||||
entity.respawn()
|
// entity.respawn()
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export default class Terrain {
|
|||||||
this.#calculateUnadjustedWaypoints()
|
this.#calculateUnadjustedWaypoints()
|
||||||
this.#calculateBbox()
|
this.#calculateBbox()
|
||||||
}
|
}
|
||||||
|
|
||||||
get vertices() { return this.#vertices }
|
get vertices() { return this.#vertices }
|
||||||
|
get dead() { return false }
|
||||||
|
|
||||||
static waypointsForSide(fromVertex, toVertex, isClockwise = false) {
|
static waypointsForSide(fromVertex, toVertex, isClockwise = false) {
|
||||||
const from = isClockwise ? toVertex : fromVertex
|
const from = isClockwise ? toVertex : fromVertex
|
||||||
|
|||||||
Reference in New Issue
Block a user