add dead state
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
+108
-65
@@ -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.dead) {
|
||||||
|
// TODO: do something while the entity is dead
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.#cast()
|
||||||
|
this.#checkHealth()
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
#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.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)) {
|
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 cooldown = this.game?.secToTick(this.abilities[0].cooldown) ?? 0
|
||||||
const lastCast = this.cooldowns[this.abilities[0].id]
|
const lastCast = this.cooldowns[this.abilities[0].id]
|
||||||
const timestamp = this.game?.currentTick ?? 0
|
const timestamp = this.game?.currentTick ?? 0
|
||||||
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
|
if (lastCast != null && lastCast + cooldown > timestamp) { return false }
|
||||||
|
|
||||||
const target = this.#dest ?? this.position
|
this.castAction(0, attackCursor.x, attackCursor.y, false)
|
||||||
this.castAction(0, target.x, target.y, false)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.#move || this.#dest == null) { return false }
|
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
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user