add minion routing

This commit is contained in:
2025-01-13 22:38:54 +09:00
parent 9d3fbda494
commit 92e06dedce
4 changed files with 76 additions and 58 deletions
+1 -1
View File
@@ -282,7 +282,7 @@ function connectWebSocket() {
for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) { for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) {
if (player.abilities[abilityIndex] != null) { if (player.abilities[abilityIndex] != null) {
const ability = player.abilities[abilityIndex] const ability = player.abilities[abilityIndex]
const lastCast = player.cooldowns[ability.id] ?? -99999 const lastCast = player.cooldowns[ability.id] ?? -Infinity
const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0 const cooldownDuration = (ability.cooldown * state.tickRate) ?? 0
const remainingCooldown = (lastCast + cooldownDuration) - state.currentTick const remainingCooldown = (lastCast + cooldownDuration) - state.currentTick
let cssPercentage = '100%' let cssPercentage = '100%'
+17 -24
View File
@@ -13,10 +13,11 @@ export default class Entity {
health = null health = null
height = 40 height = 40
maxHealth = 1 maxHealth = 1
memory = {} // TODO: WARNING: currently only used for minions (code smell?)
position = null
radius = 0 radius = 0
speed = 400 speed = 400
team = Team.neutral team = Team.neutral
memory = {} // TODO: WARNING: currently only used for minions (code smell?)
#attacking = false #attacking = false
#dest = null #dest = null
@@ -24,7 +25,6 @@ export default class Entity {
#logic = null #logic = null
#moving = false #moving = false
#path = [] #path = []
#position = null
#scheduledPathfinding = null #scheduledPathfinding = null
#spawnPosition = new Vector2() #spawnPosition = new Vector2()
@@ -34,8 +34,8 @@ 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) { if (this.position == null) {
this.#position = this.#spawnPosition.clone() this.position = this.#spawnPosition.clone()
} }
if (this.health == null) { if (this.health == null) {
this.health = this.maxHealth this.health = this.maxHealth
@@ -45,7 +45,6 @@ export default class Entity {
get destination() { return this.#dest } get destination() { return this.#dest }
get logic() { return this.#logic } get logic() { return this.#logic }
get game() { return this.#game } get game() { return this.#game }
get position() { return this.#position }
get scheduledPathfinding() { return this.#scheduledPathfinding } get scheduledPathfinding() { return this.#scheduledPathfinding }
get spawnPosition() { return this.#spawnPosition } get spawnPosition() { return this.#spawnPosition }
get x() { return this.position.x } get x() { return this.position.x }
@@ -54,7 +53,6 @@ export default class Entity {
set destination(value) { this.#dest = value } set destination(value) { this.#dest = value }
set logic(value) { this.#logic = value } set logic(value) { this.#logic = value }
set game(value) { this.#game = value } set game(value) { this.#game = 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 spawnPosition(value) { this.#spawnPosition = value }
set x(value) { this.position.x = value } set x(value) { this.position.x = value }
@@ -88,11 +86,11 @@ export default class Entity {
]) ])
} }
attackAction(x, y) { attackAction(cursor) {
this.moveAction(x, y, true) this.moveAction(cursor, true)
} }
castAction(slot, x, y, halt = true) { castAction(slot, cursor, halt = true) {
const ability = this.abilities[slot] const ability = this.abilities[slot]
if (this.casting != null) { if (this.casting != null) {
@@ -108,7 +106,6 @@ export default class Entity {
this.#moving = false this.#moving = false
} }
const cursor = new Vector2(x, y)
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
@@ -125,14 +122,14 @@ export default class Entity {
this.#moving = false this.#moving = false
} }
moveAction(x, y, attack = false) { moveAction(cursor, attack = false) {
if (this.casting != null && (!this.#attacking || 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.#attacking = attack this.#attacking = attack
this.#moving = 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(cursor, this.collidables(), this.radius, this.game?.width, this.game?.height)
} }
stopAction() { stopAction() {
@@ -170,8 +167,8 @@ export default class Entity {
this.game?.despawn(this) this.game?.despawn(this)
} }
distanceTo(vector) { distanceTo(cursor) {
return this.position.distanceTo(vector) return this.position.distanceTo(cursor)
} }
heal(amount) { heal(amount) {
@@ -179,7 +176,7 @@ export default class Entity {
} }
fixPosition() { fixPosition() {
this.#position = SATX.fixCollisions(this.#position, this.collidables(), this.radius, this.game?.width, this.game?.height).clone() this.position = SATX.fixCollisions(this.position, this.collidables(), this.radius, this.game?.width, this.game?.height).clone()
} }
isColliding(...colliders) { isColliding(...colliders) {
@@ -187,7 +184,7 @@ export default class Entity {
} }
respawn() { respawn() {
this.#position = this.#spawnPosition.clone() this.position = this.#spawnPosition.clone()
this.health = this.maxHealth this.health = this.maxHealth
this.dead = false this.dead = false
} }
@@ -195,15 +192,11 @@ export default class Entity {
state() { state() {
return { return {
...this, ...this,
position: {
x: this.x,
y: this.y,
},
} }
} }
teleport(position) { teleport(cursor) {
this.#position = position.clone() this.position = cursor.clone()
this.fixPosition() this.fixPosition()
} }
@@ -276,7 +269,7 @@ export default class Entity {
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 }
this.castAction(0, cursor.x, cursor.y, false) this.castAction(0, cursor, false)
return true return true
} }
} }
@@ -285,7 +278,7 @@ export default class Entity {
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)
const tunnel = SATX.entityTunnel(this.#position.x, this.#position.y, fixedDest.x, fixedDest.y, this.radius) const tunnel = SATX.entityTunnel(this.position.x, this.position.y, fixedDest.x, fixedDest.y, this.radius)
const destinationInLineOfSight = !SATX.collideObjects(tunnel, collidables) const destinationInLineOfSight = !SATX.collideObjects(tunnel, collidables)
if (this.#path.length > 0) { if (this.#path.length > 0) {
+18 -15
View File
@@ -31,17 +31,19 @@ app.ws('/ws', async (req, res) => {
const message = JSON.parse(rawData) const message = JSON.parse(rawData)
const entity = message.id != null ? game.entities.find((e) => e.id == message.id) : null const entity = message.id != null ? game.entities.find((e) => e.id == message.id) : null
if (entity == null) { if (entity == null) {
console.log({ error: { reason: 'Invalid ID', message } }) console.error({ error: { reason: 'Invalid ID', message } })
return return
} }
else {
console.log(message) console.log(message)
}
if (message.action == 'attack') { if (message.action == 'attack') {
entity.attackAction(message.x, message.y) entity.attackAction(new Vector2(message.x, message.y))
} }
if (message.action == 'cast') { if (message.action == 'cast') {
entity.castAction(message.slot, message.x, message.y) entity.castAction(message.slot, new Vector2(message.x, message.y))
} }
if (message.action == 'halt') { if (message.action == 'halt') {
@@ -53,7 +55,7 @@ app.ws('/ws', async (req, res) => {
} }
if (message.action == 'move') { if (message.action == 'move') {
entity.moveAction(message.x, message.y) entity.moveAction(new Vector2(message.x, message.y))
} }
}) })
}) })
@@ -65,6 +67,7 @@ function laneScenario() {
team: Team.blue, team: Team.blue,
})) }))
game.spawnEntity(player1) game.spawnEntity(player1)
player1.attackAction(new Vector2(500, 150))
const player2 = new Entity(Template.player({ const player2 = new Entity(Template.player({
id: '2', id: '2',
@@ -72,12 +75,10 @@ function laneScenario() {
team: Team.red, team: Team.red,
})) }))
game.spawnEntity(player2) game.spawnEntity(player2)
player2.attackAction(new Vector2(1600, 1800))
player1.attackAction(500, 150) const midWallStart = new Vector2(600, 600)
player2.attackAction(1600, 1800) const midWallEnd = new Vector2(1400, 1400)
const midWallStart = new Vector2(400, 400)
const midWallEnd = new Vector2(1600, 1600)
const midWallMiddle = new Vector2(800, 1200) const midWallMiddle = new Vector2(800, 1200)
const midWallThickness = midWallEnd.clone().sub(midWallStart).rotateAround(new Vector2(), -Math.PI / 2).normalize().multiplyScalar(50) const midWallThickness = midWallEnd.clone().sub(midWallStart).rotateAround(new Vector2(), -Math.PI / 2).normalize().multiplyScalar(50)
const midWallPoints = [ const midWallPoints = [
@@ -89,13 +90,13 @@ function laneScenario() {
midWallStart.clone().add(midWallThickness), midWallStart.clone().add(midWallThickness),
] ]
const midNorthWallOffset = new Vector2(-200, 200) const midNorthWallOffset = new Vector2(-400, 400)
const midNorthWallPoints = midWallPoints.map((p) => p.clone().add(midNorthWallOffset)) const midNorthWallPoints = midWallPoints.map((p) => p.clone().add(midNorthWallOffset))
const midNorthWall = new Terrain(midNorthWallPoints) const midNorthWall = new Terrain(midNorthWallPoints)
midNorthWall.id = 'midNorthWall' midNorthWall.id = 'midNorthWall'
game.addTerrain(midNorthWall) game.addTerrain(midNorthWall)
const midSouthWallOffset = new Vector2(200, -200) const midSouthWallOffset = new Vector2(0, 0)
const midSouthWallPoints = midWallPoints.map((p) => p.clone().add(midSouthWallOffset)) const midSouthWallPoints = midWallPoints.map((p) => p.clone().add(midSouthWallOffset))
const midSouthWall = new Terrain(midSouthWallPoints) const midSouthWall = new Terrain(midSouthWallPoints)
midSouthWall.id = 'midSouthWall' midSouthWall.id = 'midSouthWall'
@@ -104,14 +105,16 @@ function laneScenario() {
const gameLogic = function gameLogic() { const gameLogic = function gameLogic() {
const game = this const game = this
const blueRoute = [new Vector2(600, 1350), new Vector2(1900, 1900)]
const redRoute = [new Vector2(600, 1350), new Vector2(100, 100)]
if ([(0 * game.tickRate), (1 * game.tickRate), (2 * game.tickRate)].includes(game.currentTick % (30 * game.tickRate))) { if ([(0 * game.tickRate), (1 * game.tickRate), (2 * game.tickRate)].includes(game.currentTick % (30 * game.tickRate))) {
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: blueRoute })))
game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: false }))) game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: false, route: redRoute })))
} }
if ([(3 * game.tickRate), (4 * game.tickRate), (5 * game.tickRate)].includes(game.currentTick % (30 * game.tickRate))) { if ([(3 * game.tickRate), (4 * game.tickRate), (5 * game.tickRate)].includes(game.currentTick % (30 * game.tickRate))) {
game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true }))) game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: blueRoute })))
game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: true }))) game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: true, route: redRoute })))
} }
} }
game.logic = gameLogic game.logic = gameLogic
+35 -13
View File
@@ -7,7 +7,7 @@ export default class Template {
return { return {
abilities: [options.ranged ? Ability.rangedAttack : Ability.meleeAttack, null, null, null], abilities: [options.ranged ? Ability.rangedAttack : Ability.meleeAttack, null, null, null],
height: options.ranged ? 40 : 38, height: options.ranged ? 40 : 38,
logic: this.#minionLogic(team), logic: this.#minionLogic(options.route),
maxHealth: options.ranged ? 300 : 450, maxHealth: options.ranged ? 300 : 450,
position: team == Team.blue ? new Vector2(200, 200) : new Vector2(1800, 1800), position: team == Team.blue ? new Vector2(200, 200) : new Vector2(1800, 1800),
radius: options.ranged ? 46 : 48, radius: options.ranged ? 46 : 48,
@@ -33,33 +33,55 @@ export default class Template {
} }
} }
static #minionLogic(team) { // TODO: fix disabled incremental pathing causes lag spikes
const finalGoal = team == Team.blue ? new Vector2(1900, 1900) : new Vector2(100, 100) // TODO: minion aggro
const subGoal = new Vector2(850, 1150) static #minionLogic(route = []) {
const subGoalCheck = team == Team.blue ? ((entity) => entity.position.x < 800 || entity.position.y < 1100) : ((entity) => entity.position.x > 900 || entity.position.y > 1200) const checkpointSize = 300
const incrementalPathing = 100
return function builtMinionLogic() { return function builtMinionLogic() {
const entity = this const entity = this
if (entity.dead) { entity.despawn() } if (entity.dead) { entity.despawn() }
let goal = finalGoal if (route.length > 0) {
if (subGoalCheck(entity)) { const routeIndex = entity.memory.routeCheckpoint ?? 0
goal = subGoal const goal = route[routeIndex].clone()
const currentTick = entity.game?.currentTick ?? 0
if (goal instanceof Vector2) {
if (entity.distanceTo(goal) < checkpointSize) {
if (routeIndex + 1 < route.length) {
entity.memory.routeCheckpoint = routeIndex + 1
}
} }
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(100) if ((entity.memory.incrementalPathingTimeout ?? -Infinity) < currentTick) {
const fakeDestination = entity.position.clone().add(direction) const distanceToGoal = entity.distanceTo(goal)
entity.attackAction(fakeDestination.x, fakeDestination.y) if (distanceToGoal > entity.memory.distanceToGoal ?? -Infinity) {
entity.memory.incrementalPathingTimeout = currentTick + (1 * (entity.game.tickRate ?? 1))
}
else if (distanceToGoal > incrementalPathing) {
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(incrementalPathing)
goal.copy(entity.position.clone().add(direction))
}
entity.memory.distanceToGoal = distanceToGoal
}
entity.attackAction(goal)
}
if (entity.position.equals(route.at(-1))) {
entity.despawn()
}
}
} }
} }
// TODO: proper respawn // TODO: proper respawn
static #playerLogic() { static #playerLogic() {
return function playerLogic() {
const entity = this const entity = this
if (entity.dead) { if (entity.dead) {
entity.respawn() entity.respawn()
} }
} }
} }
}