diff --git a/public/client.js b/public/client.js index 98432e7..fcadfbe 100644 --- a/public/client.js +++ b/public/client.js @@ -282,7 +282,7 @@ function connectWebSocket() { for (let abilityIndex = 0; abilityIndex < 4; abilityIndex++) { if (player.abilities[abilityIndex] != null) { 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 remainingCooldown = (lastCast + cooldownDuration) - state.currentTick let cssPercentage = '100%' diff --git a/src/entity.js b/src/entity.js index b4f9f1f..50f13e5 100644 --- a/src/entity.js +++ b/src/entity.js @@ -13,10 +13,11 @@ export default class Entity { health = null height = 40 maxHealth = 1 + memory = {} // TODO: WARNING: currently only used for minions (code smell?) + position = null radius = 0 speed = 400 team = Team.neutral - memory = {} // TODO: WARNING: currently only used for minions (code smell?) #attacking = false #dest = null @@ -24,7 +25,6 @@ export default class Entity { #logic = null #moving = false #path = [] - #position = null #scheduledPathfinding = null #spawnPosition = new Vector2() @@ -34,8 +34,8 @@ 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.position == null) { + this.position = this.#spawnPosition.clone() } if (this.health == null) { this.health = this.maxHealth @@ -45,7 +45,6 @@ export default class Entity { get destination() { return this.#dest } get logic() { return this.#logic } 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 } @@ -54,7 +53,6 @@ export default class Entity { set destination(value) { this.#dest = value } set logic(value) { this.#logic = value } 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 } @@ -88,11 +86,11 @@ export default class Entity { ]) } - attackAction(x, y) { - this.moveAction(x, y, true) + attackAction(cursor) { + this.moveAction(cursor, true) } - castAction(slot, x, y, halt = true) { + castAction(slot, cursor, halt = true) { const ability = this.abilities[slot] if (this.casting != null) { @@ -108,7 +106,6 @@ export default class Entity { this.#moving = false } - const cursor = new Vector2(x, y) const cooldown = this.game?.secToTick(ability.cooldown) ?? 0 const lastCast = this.cooldowns[ability.id] const timestamp = this.game?.currentTick ?? 0 @@ -125,14 +122,14 @@ export default class Entity { 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)) { this.casting = null } this.#attacking = attack 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() { @@ -170,8 +167,8 @@ export default class Entity { this.game?.despawn(this) } - distanceTo(vector) { - return this.position.distanceTo(vector) + distanceTo(cursor) { + return this.position.distanceTo(cursor) } heal(amount) { @@ -179,7 +176,7 @@ export default class Entity { } 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) { @@ -187,7 +184,7 @@ export default class Entity { } respawn() { - this.#position = this.#spawnPosition.clone() + this.position = this.#spawnPosition.clone() this.health = this.maxHealth this.dead = false } @@ -195,15 +192,11 @@ export default class Entity { state() { return { ...this, - position: { - x: this.x, - y: this.y, - }, } } - teleport(position) { - this.#position = position.clone() + teleport(cursor) { + this.position = cursor.clone() this.fixPosition() } @@ -276,7 +269,7 @@ export default class Entity { const timestamp = this.game?.currentTick ?? 0 if (lastCast != null && lastCast + cooldown > timestamp) { return false } - this.castAction(0, cursor.x, cursor.y, false) + this.castAction(0, cursor, false) return true } } @@ -285,7 +278,7 @@ export default class Entity { const collidables = this.collidables() 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) if (this.#path.length > 0) { diff --git a/src/index.js b/src/index.js index 8f4b798..ed5c5fc 100644 --- a/src/index.js +++ b/src/index.js @@ -31,17 +31,19 @@ app.ws('/ws', async (req, res) => { const message = JSON.parse(rawData) const entity = message.id != null ? game.entities.find((e) => e.id == message.id) : null if (entity == null) { - console.log({ error: { reason: 'Invalid ID', message } }) + console.error({ error: { reason: 'Invalid ID', message } }) return } - console.log(message) + else { + console.log(message) + } if (message.action == 'attack') { - entity.attackAction(message.x, message.y) + entity.attackAction(new Vector2(message.x, message.y)) } 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') { @@ -53,7 +55,7 @@ app.ws('/ws', async (req, res) => { } 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, })) game.spawnEntity(player1) + player1.attackAction(new Vector2(500, 150)) const player2 = new Entity(Template.player({ id: '2', @@ -72,12 +75,10 @@ function laneScenario() { team: Team.red, })) game.spawnEntity(player2) + player2.attackAction(new Vector2(1600, 1800)) - player1.attackAction(500, 150) - player2.attackAction(1600, 1800) - - const midWallStart = new Vector2(400, 400) - const midWallEnd = new Vector2(1600, 1600) + const midWallStart = new Vector2(600, 600) + const midWallEnd = new Vector2(1400, 1400) const midWallMiddle = new Vector2(800, 1200) const midWallThickness = midWallEnd.clone().sub(midWallStart).rotateAround(new Vector2(), -Math.PI / 2).normalize().multiplyScalar(50) const midWallPoints = [ @@ -89,13 +90,13 @@ function laneScenario() { 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 midNorthWall = new Terrain(midNorthWallPoints) midNorthWall.id = '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 midSouthWall = new Terrain(midSouthWallPoints) midSouthWall.id = 'midSouthWall' @@ -104,14 +105,16 @@ function laneScenario() { const gameLogic = function gameLogic() { 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))) { - game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false }))) - game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: false }))) + game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: false, route: blueRoute }))) + 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))) { - game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true }))) - game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: true }))) + game.spawnEntity(new Entity(Template.minion(Team.blue, { ranged: true, route: blueRoute }))) + game.spawnEntity(new Entity(Template.minion(Team.red, { ranged: true, route: redRoute }))) } } game.logic = gameLogic diff --git a/src/template.js b/src/template.js index dd03e0e..af5d880 100644 --- a/src/template.js +++ b/src/template.js @@ -7,7 +7,7 @@ export default class Template { return { abilities: [options.ranged ? Ability.rangedAttack : Ability.meleeAttack, null, null, null], height: options.ranged ? 40 : 38, - logic: this.#minionLogic(team), + logic: this.#minionLogic(options.route), maxHealth: options.ranged ? 300 : 450, position: team == Team.blue ? new Vector2(200, 200) : new Vector2(1800, 1800), radius: options.ranged ? 46 : 48, @@ -33,33 +33,55 @@ export default class Template { } } - static #minionLogic(team) { - const finalGoal = team == Team.blue ? new Vector2(1900, 1900) : new Vector2(100, 100) - const subGoal = new Vector2(850, 1150) - const subGoalCheck = team == Team.blue ? ((entity) => entity.position.x < 800 || entity.position.y < 1100) : ((entity) => entity.position.x > 900 || entity.position.y > 1200) + // TODO: fix disabled incremental pathing causes lag spikes + // TODO: minion aggro + static #minionLogic(route = []) { + const checkpointSize = 300 + const incrementalPathing = 100 return function builtMinionLogic() { const entity = this if (entity.dead) { entity.despawn() } - let goal = finalGoal - if (subGoalCheck(entity)) { - goal = subGoal - } + if (route.length > 0) { + const routeIndex = entity.memory.routeCheckpoint ?? 0 + 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) - const fakeDestination = entity.position.clone().add(direction) - entity.attackAction(fakeDestination.x, fakeDestination.y) + if ((entity.memory.incrementalPathingTimeout ?? -Infinity) < currentTick) { + const distanceToGoal = entity.distanceTo(goal) + 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 static #playerLogic() { - return function playerLogic() { - const entity = this - if (entity.dead) { - entity.respawn() - } + const entity = this + if (entity.dead) { + entity.respawn() } } }