fix dead state

This commit is contained in:
2025-01-22 22:52:08 +09:00
parent 4f8dcebcd1
commit 0db1ceeedc
10 changed files with 263 additions and 43 deletions
+38 -8
View File
@@ -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 }))
}
} }
}) })
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
} // }
} }
} }
+2
View File
@@ -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