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)
}
// TODO: skill seemingly going right through minions without a registered hit
static straightShot = new Ability({
id: 'straight_shot',
name: 'Straight Shot',
+116 -73
View File
@@ -7,11 +7,6 @@ import Team from './team.js'
export default class Entity {
id = crypto.randomUUID()
speed = 400
radius = 0
health = 1
maxHealth = 1
height = 40
abilities = [
Ability.rangedAttack,
Ability.straightShot,
@@ -19,18 +14,24 @@ export default class Entity {
Ability.blink,
]
casting = null
cooldowns = {}
dead = false
health = null
height = 40
maxHealth = 1
radius = 0
speed = 400
team = Team.neutral
cooldowns = {}
#attack = false
#attacking = false
#dest = null
#game = null
#logic = null
#move = false
#moving = false
#path = []
#position = new Vector2()
#position = null
#scheduledPathfinding = null
#spawnPosition = new Vector2()
static collider(x, y, radius) {
return new SAT.Circle(new SAT.Vector(x, y), radius)
@@ -38,6 +39,12 @@ export default class Entity {
constructor(options = {}) {
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 }
@@ -45,6 +52,7 @@ export default class Entity {
get game() { return this.#game }
get position() { return this.#position }
get scheduledPathfinding() { return this.#scheduledPathfinding }
get spawnPosition() { return this.#spawnPosition }
get x() { return this.position.x }
get y() { return this.position.y }
@@ -53,6 +61,7 @@ export default class Entity {
set game(value) { this.#game = value }
set position(value) { this.#position = value }
set scheduledPathfinding(value) { this.#scheduledPathfinding = value }
set spawnPosition(value) { this.#spawnPosition = value }
set x(value) { this.position.x = value }
set y(value) { this.position.y = value }
@@ -101,7 +110,7 @@ export default class Entity {
}
if (halt) {
this.#move = false
this.#moving = false
}
const cursor = new Vector2(x, y)
@@ -118,42 +127,26 @@ export default class Entity {
}
haltAction() {
this.#move = false
this.#moving = 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.#attack = attack
this.#move = true
this.#attacking = attack
this.#moving = true
this.#dest = SATX.fixCollisions(new Vector2(x, y), this.collidables(), this.radius, this.game?.width, this.game?.height)
}
stopAction() {
this.casting = null
this.#move = true
this.#attack = false
this.#moving = true
this.#attacking = false
}
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
}
// --- Actions above --- //
collidables() {
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)
}
respawn() {
this.#position = this.#spawnPosition.clone()
this.health = this.maxHealth
this.dead = false
}
state() {
return {
...this,
@@ -201,21 +200,89 @@ export default class Entity {
this.fixPosition()
}
move(distanceTraveled = 0) {
if (this.casting != null) { return false }
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
const lastCast = this.cooldowns[this.abilities[0].id]
const timestamp = this.game?.currentTick ?? 0
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
const target = this.#dest ?? this.position
this.castAction(0, target.x, target.y, false)
return true
update() {
if (this.dead) {
// TODO: do something while the entity is dead
}
else {
this.#cast()
this.#checkHealth()
this.#move()
this.fixPosition()
}
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 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)) {
this.#path = this.#path.slice(1)
if (this.#path.length > 0) {
this.move(distance)
this.#move(distance)
}
else {
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
width = 2000
height = 2000
running = false
nextTick = 0
nextTickAt = 0
#logic = null
#entities = []
@@ -88,15 +87,15 @@ export default class Game {
gameLoop() {
const tickBudget = this.#tickBudget
if (this.nextTick != null) {
const nextTick = this.nextTick
this.nextTick = null
if (this.nextTickAt != null) {
const nextTickAt = this.nextTickAt
this.nextTickAt = null
let start = performance.now()
while (start < nextTick) { start = performance.now() }
while (start < nextTickAt) { start = performance.now() }
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() {
// TODO: proper respawn
const playerLogic = function playerLogic() {
const entity = this
if (entity.dead) {
entity.respawn()
}
}
const entity1 = new Entity({
id: '1',
health: 100,
height: 80,
logic: playerLogic,
maxHealth: 100,
position: new Vector2(500, 150),
spawnPosition: new Vector2(500, 150),
radius: 50,
team: Team.blue,
})
@@ -72,10 +80,10 @@ function laneScenario() {
const entity2 = new Entity({
id: '2',
health: 100,
height: 80,
logic: playerLogic,
maxHealth: 100,
position: new Vector2(1600, 1800),
spawnPosition: new Vector2(1600, 1800),
radius: 50,
team: Team.red,
})
@@ -107,30 +115,10 @@ function laneScenario() {
midSouthWall.id = '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 entity = this
if (entity.dead) { entity.despawn() }
let goal = new Vector2(1900, 1900)
if (entity.position.x < 800 || entity.position.y < 1100) {
goal = new Vector2(850, 1150)
@@ -139,14 +127,12 @@ function laneScenario() {
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
const subGoal = entity.position.clone().add(direction)
entity.attackAction(subGoal.x, subGoal.y)
if (entity.health <= 0) {
entity.despawn()
}
}
const redMinionLogic = function minionLogic() {
const entity = this
if (entity.dead) { entity.despawn() }
let goal = new Vector2(100, 100)
if (entity.position.x > 900 || entity.position.y > 1200) {
goal = new Vector2(850, 1150)
@@ -155,10 +141,6 @@ function laneScenario() {
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(75)
const subGoal = entity.position.clone().add(direction)
entity.attackAction(subGoal.x, subGoal.y)
if (entity.health <= 0) {
entity.despawn()
}
}
const minionTemplate = {
@@ -177,7 +159,6 @@ function laneScenario() {
team: Team.blue,
position: new Vector2(200, 200),
})
// blueMinion.scheduledPathfinding = game.entities.length % game.tickRate
const blueMeleeMinion = new Entity({
...minionTemplate,
@@ -193,7 +174,6 @@ function laneScenario() {
team: Team.red,
position: new Vector2(1800, 1800),
})
// redMinion.scheduledPathfinding = game.entities.length % game.tickRate
const redMeleeMinion = new Entity({
...minionTemplate,