add dead state

This commit is contained in:
2025-01-13 14:08:10 +09:00
parent 03bbea4862
commit 16429a6e1b
4 changed files with 139 additions and 116 deletions
+1
View File
@@ -24,6 +24,7 @@ export default class Ability {
Object.entries(options).forEach(([key, value]) => this[key] = value) Object.entries(options).forEach(([key, value]) => this[key] = value)
} }
// TODO: skill seemingly going right through minions without a registered hit
static straightShot = new Ability({ static straightShot = new Ability({
id: 'straight_shot', id: 'straight_shot',
name: 'Straight Shot', name: 'Straight Shot',
+116 -73
View File
@@ -7,11 +7,6 @@ import Team from './team.js'
export default class Entity { export default class Entity {
id = crypto.randomUUID() id = crypto.randomUUID()
speed = 400
radius = 0
health = 1
maxHealth = 1
height = 40
abilities = [ abilities = [
Ability.rangedAttack, Ability.rangedAttack,
Ability.straightShot, Ability.straightShot,
@@ -19,18 +14,24 @@ export default class Entity {
Ability.blink, Ability.blink,
] ]
casting = null casting = null
cooldowns = {}
dead = false
health = null
height = 40
maxHealth = 1
radius = 0
speed = 400
team = Team.neutral team = Team.neutral
cooldowns = {} #attacking = false
#attack = false
#dest = null #dest = null
#game = null #game = null
#logic = null #logic = null
#move = false #moving = false
#path = [] #path = []
#position = new Vector2() #position = null
#scheduledPathfinding = null #scheduledPathfinding = null
#spawnPosition = new Vector2()
static collider(x, y, radius) { static collider(x, y, radius) {
return new SAT.Circle(new SAT.Vector(x, y), radius) return new SAT.Circle(new SAT.Vector(x, y), radius)
@@ -38,6 +39,12 @@ export default class Entity {
constructor(options = {}) { constructor(options = {}) {
Object.entries(options).forEach(([key, value]) => this[key] = value) Object.entries(options).forEach(([key, value]) => this[key] = value)
if (this.#position == null) {
this.#position = this.#spawnPosition.clone()
}
if (this.health == null) {
this.health = this.maxHealth
}
} }
get destination() { return this.#dest } get destination() { return this.#dest }
@@ -45,6 +52,7 @@ export default class Entity {
get game() { return this.#game } get game() { return this.#game }
get position() { return this.#position } get position() { return this.#position }
get scheduledPathfinding() { return this.#scheduledPathfinding } get scheduledPathfinding() { return this.#scheduledPathfinding }
get spawnPosition() { return this.#spawnPosition }
get x() { return this.position.x } get x() { return this.position.x }
get y() { return this.position.y } get y() { return this.position.y }
@@ -53,6 +61,7 @@ export default class Entity {
set game(value) { this.#game = value } set game(value) { this.#game = value }
set position(value) { this.#position = value } set position(value) { this.#position = value }
set scheduledPathfinding(value) { this.#scheduledPathfinding = value } set scheduledPathfinding(value) { this.#scheduledPathfinding = value }
set spawnPosition(value) { this.#spawnPosition = value }
set x(value) { this.position.x = value } set x(value) { this.position.x = value }
set y(value) { this.position.y = value } set y(value) { this.position.y = value }
@@ -101,7 +110,7 @@ export default class Entity {
} }
if (halt) { if (halt) {
this.#move = false this.#moving = false
} }
const cursor = new Vector2(x, y) const cursor = new Vector2(x, y)
@@ -118,42 +127,26 @@ export default class Entity {
} }
haltAction() { haltAction() {
this.#move = false this.#moving = false
} }
moveAction(x, y, attack = false) { moveAction(x, y, attack = false) {
if (this.casting != null && (!this.#attack || this.casting.ability.id != this.abilities[0].id)) { if (this.casting != null && (!this.#attacking || this.casting.ability.id != this.abilities[0].id)) {
this.casting = null this.casting = null
} }
this.#attack = attack this.#attacking = attack
this.#move = true this.#moving = true
this.#dest = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius, this.game?.width, this.game?.height) this.#dest = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius, this.game?.width, this.game?.height)
} }
stopAction() { stopAction() {
this.casting = null this.casting = null
this.#move = true this.#moving = true
this.#attack = false this.#attacking = false
} }
cast() { // --- Actions above --- //
if (this.casting == null) {
return false
}
const castTime = this.game?.secToTick(this.casting.ability.castTime) ?? 0
const castStart = this.casting.timestamp
const timestamp = this.game?.currentTick ?? 0
if (castStart + castTime > timestamp) {
return false
}
this.casting.ability.effect(this, this.casting.cursor)
this.casting = null
return true
}
collidables() { collidables() {
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider) const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider)
@@ -186,6 +179,12 @@ export default class Entity {
return SATX.collideObjects(this.collider, colliders) return SATX.collideObjects(this.collider, colliders)
} }
respawn() {
this.#position = this.#spawnPosition.clone()
this.health = this.maxHealth
this.dead = false
}
state() { state() {
return { return {
...this, ...this,
@@ -201,21 +200,89 @@ export default class Entity {
this.fixPosition() this.fixPosition()
} }
move(distanceTraveled = 0) { update() {
if (this.casting != null) { return false } if (this.dead) {
// TODO: do something while the entity is dead
if (this.#attack && this.game?.entities.some((e) => e.team != this.team && e.position.clone().sub(this.position).length() < this.abilities[0].range)) { }
const cooldown = this.game?.secToTick(this.abilities[0].cooldown) ?? 0 else {
const lastCast = this.cooldowns[this.abilities[0].id] this.#cast()
const timestamp = this.game?.currentTick ?? 0 this.#checkHealth()
if (lastCast != null && lastCast + cooldown > timestamp) { return false } this.#move()
this.fixPosition()
const target = this.#dest ?? this.position
this.castAction(0, target.x, target.y, false)
return true
} }
if (!this.#move || this.#dest == null) { return false } if (this.#logic != null) {
this.#logic()
}
}
waypoints() {
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id)
const terrainColliders = (this.game?.terrains ?? [])
const unadjustedWaypoints = entityColliders.concat(terrainColliders).map((e) => e.unadjustedWaypoints).flat()
return unadjustedWaypoints.map(([waypoint, direction]) => {
return SATX.clamp(
waypoint.clone().add(direction.clone().multiplyScalar(this.radius)),
this.game?.width,
this.game?.height,
this.radius,
)
}) ?? []
}
#cast() {
if (this.casting == null) {
return false
}
const castTime = this.game?.secToTick(this.casting.ability.castTime) ?? 0
const castStart = this.casting.timestamp
const timestamp = this.game?.currentTick ?? 0
if (castStart + castTime > timestamp) {
return false
}
this.casting.ability.effect(this, this.casting.cursor)
this.casting = null
return true
}
#checkHealth() {
if (this.health <= 0) {
this.dead = true
}
}
// TODO: make scheduled pathfinding continue until collision to make the entity more "alive"
#move(distanceTraveled = 0) {
if (this.casting != null) { return false }
if (this.#attacking) {
const attackCursor = this.#dest ?? this.position
const targets = this.game?.entities.filter((e) => e.team != this.team && e.position.clone().sub(attackCursor).length() < this.abilities[0].range)
const target = targets.reduce((prev, e) => {
if (prev == null || e.position.clone().sub(attackCursor).length() > prev.position.clone().sub(attackCursor).length()) {
return e
}
else {
return prev
}
}, null)
if (target != null && target.position.clone().sub(this.position).length() < this.abilities[0].range) {
const cooldown = this.game?.secToTick(this.abilities[0].cooldown) ?? 0
const lastCast = this.cooldowns[this.abilities[0].id]
const timestamp = this.game?.currentTick ?? 0
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
this.castAction(0, attackCursor.x, attackCursor.y, false)
return true
}
}
if (!this.#moving || this.#dest == null) { return false }
const collidables = this.collidables() const collidables = this.collidables()
const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius) const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius)
@@ -262,37 +329,13 @@ export default class Entity {
if (this.position.equals(destination)) { if (this.position.equals(destination)) {
this.#path = this.#path.slice(1) this.#path = this.#path.slice(1)
if (this.#path.length > 0) { if (this.#path.length > 0) {
this.move(distance) this.#move(distance)
} }
else { else {
this.#dest = null this.#dest = null
this.#move = false this.#moving = false
} }
} }
} }
} }
update() {
this.cast()
this.move()
this.fixPosition()
if (this.#logic != null) {
this.#logic()
}
}
waypoints() {
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id)
const terrainColliders = (this.game?.terrains ?? [])
const unadjustedWaypoints = entityColliders.concat(terrainColliders).map((e) => e.unadjustedWaypoints).flat()
return unadjustedWaypoints.map(([waypoint, direction]) => {
return SATX.clamp(
waypoint.clone().add(direction.clone().multiplyScalar(this.radius)),
this.game?.width,
this.game?.height,
this.radius,
)
}) ?? []
}
} }
+6 -7
View File
@@ -8,8 +8,7 @@ export default class Game {
currentTick = 0 currentTick = 0
width = 2000 width = 2000
height = 2000 height = 2000
running = false nextTickAt = 0
nextTick = 0
#logic = null #logic = null
#entities = [] #entities = []
@@ -88,15 +87,15 @@ export default class Game {
gameLoop() { gameLoop() {
const tickBudget = this.#tickBudget const tickBudget = this.#tickBudget
if (this.nextTick != null) { if (this.nextTickAt != null) {
const nextTick = this.nextTick const nextTickAt = this.nextTickAt
this.nextTick = null this.nextTickAt = null
let start = performance.now() let start = performance.now()
while (start < nextTick) { start = performance.now() } while (start < nextTickAt) { start = performance.now() }
this.update() this.update()
this.nextTick = start + tickBudget this.nextTickAt = start + tickBudget
} }
} }
+16 -36
View File
@@ -58,12 +58,20 @@ app.ws('/ws', async (req, res) => {
}) })
function laneScenario() { function laneScenario() {
// TODO: proper respawn
const playerLogic = function playerLogic() {
const entity = this
if (entity.dead) {
entity.respawn()
}
}
const entity1 = new Entity({ const entity1 = new Entity({
id: '1', id: '1',
health: 100,
height: 80, height: 80,
logic: playerLogic,
maxHealth: 100, maxHealth: 100,
position: new Vector2(500, 150), spawnPosition: new Vector2(500, 150),
radius: 50, radius: 50,
team: Team.blue, team: Team.blue,
}) })
@@ -72,10 +80,10 @@ function laneScenario() {
const entity2 = new Entity({ const entity2 = new Entity({
id: '2', id: '2',
health: 100,
height: 80, height: 80,
logic: playerLogic,
maxHealth: 100, maxHealth: 100,
position: new Vector2(1600, 1800), spawnPosition: new Vector2(1600, 1800),
radius: 50, radius: 50,
team: Team.red, team: Team.red,
}) })
@@ -107,30 +115,10 @@ function laneScenario() {
midSouthWall.id = 'midSouthWall' midSouthWall.id = 'midSouthWall'
game.addTerrain(midSouthWall) game.addTerrain(midSouthWall)
// TODO: proper death and respawn
const playerLogic = function playerLogic() {
const entity = this
if (entity.health <= 0) {
if (entity.id == '1' || entity.id == '2') {
entity.health = entity.maxHealth
if (entity.id == '1') {
entity.teleport(new Vector2(500, 150))
}
if (entity.id == '2') {
entity.teleport(new Vector2(1600, 1800))
}
if (entity.id == '3') {
entity.teleport(new Vector2(1800, 1600))
}
}
}
}
entity1.logic = playerLogic
entity2.logic = playerLogic
const blueMinionLogic = function minionLogic() { const blueMinionLogic = function minionLogic() {
const entity = this const entity = this
if (entity.dead) { entity.despawn() }
let goal = new Vector2(1900, 1900) let goal = new Vector2(1900, 1900)
if (entity.position.x < 800 || entity.position.y < 1100) { if (entity.position.x < 800 || entity.position.y < 1100) {
goal = new Vector2(850, 1150) goal = new Vector2(850, 1150)
@@ -139,14 +127,12 @@ function laneScenario() {
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75) const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
const subGoal = entity.position.clone().add(direction) const subGoal = entity.position.clone().add(direction)
entity.attackAction(subGoal.x, subGoal.y) entity.attackAction(subGoal.x, subGoal.y)
if (entity.health <= 0) {
entity.despawn()
}
} }
const redMinionLogic = function minionLogic() { const redMinionLogic = function minionLogic() {
const entity = this const entity = this
if (entity.dead) { entity.despawn() }
let goal = new Vector2(100, 100) let goal = new Vector2(100, 100)
if (entity.position.x > 900 || entity.position.y > 1200) { if (entity.position.x > 900 || entity.position.y > 1200) {
goal = new Vector2(850, 1150) goal = new Vector2(850, 1150)
@@ -155,10 +141,6 @@ function laneScenario() {
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75) const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
const subGoal = entity.position.clone().add(direction) const subGoal = entity.position.clone().add(direction)
entity.attackAction(subGoal.x, subGoal.y) entity.attackAction(subGoal.x, subGoal.y)
if (entity.health <= 0) {
entity.despawn()
}
} }
const minionTemplate = { const minionTemplate = {
@@ -177,7 +159,6 @@ function laneScenario() {
team: Team.blue, team: Team.blue,
position: new Vector2(200, 200), position: new Vector2(200, 200),
}) })
// blueMinion.scheduledPathfinding = game.entities.length % game.tickRate
const blueMeleeMinion = new Entity({ const blueMeleeMinion = new Entity({
...minionTemplate, ...minionTemplate,
@@ -193,7 +174,6 @@ function laneScenario() {
team: Team.red, team: Team.red,
position: new Vector2(1800, 1800), position: new Vector2(1800, 1800),
}) })
// redMinion.scheduledPathfinding = game.entities.length % game.tickRate
const redMeleeMinion = new Entity({ const redMeleeMinion = new Entity({
...minionTemplate, ...minionTemplate,