diff --git a/src/entity.js b/src/entity.js index 8e03dd7..66ed4b1 100644 --- a/src/entity.js +++ b/src/entity.js @@ -158,26 +158,6 @@ export default class Entity { set x(value) { this.position.x = value } set y(value) { this.position.y = value } - get unadjustedWaypoints() { - const numberOfWaypoints = 8 - const margin = 1 - const enclosingRegularPolygonRadius = SATX.enclosingRegularPolygonRadius(numberOfWaypoints) - const radius = this.radius * enclosingRegularPolygonRadius + margin - const baseWaypoint = new Vector2(radius, 0) - const waypoints = [] - - const origin = new Vector2 - const unitOfRotation = (Math.PI * 2 / numberOfWaypoints) - for (let i = 0; i < numberOfWaypoints; i++) { - waypoints.push(baseWaypoint.clone().rotateAround(origin, unitOfRotation * i)) - } - - return waypoints.map((w) => [ - w.clone().add(this.position), - w.clone().normalize().multiplyScalar(enclosingRegularPolygonRadius), - ]) - } - attackAction(cursor) { this.moveAction(cursor, true) } @@ -424,8 +404,8 @@ export default class Entity { const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox)) if (bboxCheckedObstacles.length < 1) { return true } - const posCollider = Entity.collider(this.position.x, this.position.y, 1) // TODO: magic number for radius - const posBbox = Entity.bbox(this.position.x, this.position.y, 1) // TODO: magic number for radius + const posCollider = Entity.collider(this.position.x, this.position.y, 0) + const posBbox = Entity.bbox(this.position.x, this.position.y, 0) const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c)))) const colliders = unpassableTerrain.map((it) => it.colliders()).flat() @@ -472,9 +452,29 @@ export default class Entity { this.setPosition(this.fixFuturePosition(cursor)) } + unadjustedWaypoints() { + const numberOfWaypoints = 8 + const margin = 1 + const enclosingRegularPolygonRadius = SATX.enclosingRegularPolygonRadius(numberOfWaypoints) + const radius = this.radius * enclosingRegularPolygonRadius + margin + const baseWaypoint = new Vector2(radius, 0) + const waypoints = [] + + const origin = new Vector2 + const unitOfRotation = (Math.PI * 2 / numberOfWaypoints) + for (let i = 0; i < numberOfWaypoints; i++) { + waypoints.push(baseWaypoint.clone().rotateAround(origin, unitOfRotation * i)) + } + + return waypoints.map((w) => [ + w.clone().add(this.position), + w.clone().normalize().multiplyScalar(enclosingRegularPolygonRadius), + ]) + } + update() { if (this.dead) { - // TODO: do something while the entity is dead + // TODO: do something while the entity is dead (and disallow casting, etc) } else { this.#cast() @@ -537,6 +537,7 @@ export default class Entity { ability.effect(this, this.casting.cursor) this.casting = null + // TODO: only spawn castingVision if slightly outside regular vision (or obstructed) Ability.castingVision.effect(this, this.position) return true } @@ -590,16 +591,33 @@ export default class Entity { if (pathfinding && (this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) { const start = SATX.vectorToFloat32Array(this.position) const goal = SATX.vectorToFloat32Array(fixedDest) - const obstacles = [] + const obstacles = new Map() // TODO: limit number of obstacles for non-important entities (property on the class?) + const obstacleWaypoints = new Map() + const obstacleColliders = new Map() + // TODO: pathfinding takes longer after bbox check implementation (maybe separate obstacleColliders into two, and index match) for (let failsafe = 0; failsafe < 1000; failsafe++) { + const obstaclesArray = Array.from(obstacles.values()) + + for (const obstacle of obstaclesArray) { + if (!obstacleWaypoints.has(obstacle.id)) { + const waypoint = obstacle.unadjustedWaypoints().map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d))) + obstacleWaypoints.set(obstacle.id, waypoint) + } + if (!obstacleColliders.has(obstacle.id)) { + const bbox = obstacle.bbox + const colliders = obstacle.colliders() + obstacleColliders.set(obstacle.id, [bbox, colliders]) + } + } + const waypoints = [ start, goal, - ...obstacles.map((e) => e.unadjustedWaypoints.map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d)))).flat() + ...obstaclesArray.map((it) => obstacleWaypoints.get(it.id)).flat() ] - const colliders = obstacles.map((e) => e.colliders()).flat() + const colliders = Array.from(obstacleColliders.values()) const graph = Pathfind.buildGraph(waypoints, colliders, this.radius) const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1])) @@ -611,10 +629,9 @@ export default class Entity { const sectionObstacles = this.obstaclesInStraightPath(section, lastSection) if (sectionObstacles.length > 0) { obstacleInPath = true - const obstacleIds = obstacles.map((o) => o.id) for (const obstacle of sectionObstacles) { - if (!obstacleIds.includes(obstacle.id)) { - obstacles.push(obstacle) + if (!obstacles.has(obstacle.id)) { + obstacles.set(obstacle.id, obstacle) } } } diff --git a/src/game.js b/src/game.js index 284f78e..20d6789 100644 --- a/src/game.js +++ b/src/game.js @@ -36,10 +36,6 @@ export default class Game { get tickBudget() { return this.#tickBudget } set logic(value) { this.#logic = value } - get unadjustedWaypoints() { - return this.terrains.map((t) => t.unadjustedWaypoints).concat(this.entities.map((e) => e.unadjustedWaypoints)).flat() - } - action(id, options) { const entity = this.entities.find((it) => it.id == id) if (entity == null) { @@ -104,7 +100,7 @@ export default class Game { if (this.gameLoopIntervalId != null) { return } this.startTimestamp = performance.now() + (this.currentTick * this.tickBudget) - console.log(`Started game ${this.id} with ${this.tickRate} tps. Starting on tick ${this.currentTick}.`) + console.info(`Started game ${this.id} with ${this.tickRate} tps. Starting on tick ${this.currentTick}.`) this.gameLoopIntervalId = setInterval(this.#gameLoopCall.bind(this), this.tickBudget / 5) } @@ -113,7 +109,7 @@ export default class Game { clearInterval(this.gameLoopIntervalId) this.gameLoopIntervalId = null - console.log(`Stopped game ${this.id}. Stopped on tick ${this.currentTick}.`) + console.info(`Stopped game ${this.id}. Stopped on tick ${this.currentTick}.`) } subscription(websocket, id) { @@ -158,7 +154,6 @@ export default class Game { return entityVisionSources.concat(projectileVisionSources) } - // TODO: castingVision should not reveal casting in non-lanes (= only spawn castingVision if slightly outside regular vision [or obstructeed]) visionByTeam(team) { const visionSources = this.visionSources(team) const visibleEntities = new Set(visionSources.map((it) => it.entitiesInVision()).flat()) diff --git a/src/index.js b/src/index.js index 05489fe..6262f87 100644 --- a/src/index.js +++ b/src/index.js @@ -37,7 +37,7 @@ app.ws('/ws', async (req, res) => { }) app.listen(port, () => { - console.log(`Server started! Visit http://localhost:${port}`) + console.info(`Server started! Visit http://localhost:${port}`) Dungeon.scenario(game) }) diff --git a/src/pathfind.js b/src/pathfind.js index 93bc1a5..1902ff9 100644 --- a/src/pathfind.js +++ b/src/pathfind.js @@ -83,11 +83,17 @@ export default class Pathfind { if (radius > 0) { for (const waypoint of waypoints) { - const collider = Entity.collider(waypoint[0], waypoint[1], radius) - const waypointAvailable = !colliders.some((it) => SATX.collideObject(collider, it)) - if (waypointAvailable) { - filteredWaypoints.push(waypoint) + const bbox = Entity.bbox(waypoint[0], waypoint[1], radius) // TODO: duplicate bbox calculation logic for speed + const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0])) + if (bboxCheckedObstacles.length > 0) { + const collider = Entity.collider(waypoint[0], waypoint[1], radius) + const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(collider, c))) + if (colliding) { + continue + } } + + filteredWaypoints.push(waypoint) } } @@ -111,31 +117,38 @@ export default class Pathfind { } const key = Pathfind.floatKey4(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1]) - if (!checked.has(key)) { - checked.add(key) - checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1])) + if (checked.has(key)) { + continue + } - // TODO: optimize tunnelCollider using bounding boxes + checked.add(key) + checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1])) + + const bbox = Entity.tunnelBbox(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius) // TODO: duplicate bbox calculation logic for speed + const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0])) + if (bboxCheckedObstacles.length > 0) { const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius) - - if (!colliders.some((it) => SATX.collideObject(tunnel, it))) { - const node = new Float32Array(5) - node[0] = mergedWaypoints[i] - node[1] = mergedWaypoints[i + 1] - node[2] = mergedWaypoints[j] - node[3] = mergedWaypoints[j + 1] - node[4] = Math.hypot(mergedWaypoints[j] - mergedWaypoints[i], mergedWaypoints[j + 1] - mergedWaypoints[i + 1]) - nodes.push(node) - - const reverseNode = new Float32Array(5) - reverseNode[0] = mergedWaypoints[j] - reverseNode[1] = mergedWaypoints[j + 1] - reverseNode[2] = mergedWaypoints[i] - reverseNode[3] = mergedWaypoints[i + 1] - reverseNode[4] = node[4] // distance is the same, copying is less expensive - nodes.push(reverseNode) + const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(tunnel, c))) + if (colliding) { + continue } } + + const node = new Float32Array(5) + node[0] = mergedWaypoints[i] + node[1] = mergedWaypoints[i + 1] + node[2] = mergedWaypoints[j] + node[3] = mergedWaypoints[j + 1] + node[4] = Math.hypot(mergedWaypoints[j] - mergedWaypoints[i], mergedWaypoints[j + 1] - mergedWaypoints[i + 1]) + nodes.push(node) + + const reverseNode = new Float32Array(5) + reverseNode[0] = mergedWaypoints[j] + reverseNode[1] = mergedWaypoints[j + 1] + reverseNode[2] = mergedWaypoints[i] + reverseNode[3] = mergedWaypoints[i + 1] + reverseNode[4] = node[4] // distance is the same, copying is less expensive + nodes.push(reverseNode) } } diff --git a/src/projectile.js b/src/projectile.js index 21eeeeb..28023ed 100644 --- a/src/projectile.js +++ b/src/projectile.js @@ -75,8 +75,8 @@ export default class Projectile { const bboxCheckedObstacles = terrains.filter((it) => SATX.bboxCheck(bbox, it.bbox)) if (bboxCheckedObstacles.length < 1) { return true } - const posCollider = Entity.collider(this.position.x, this.position.y, 1) // TODO: magic number for radius - const posBbox = Entity.bbox(this.position.x, this.position.y, 1) // TODO: magic number for radius + const posCollider = Entity.collider(this.position.x, this.position.y, 0) + const posBbox = Entity.bbox(this.position.x, this.position.y, 0) const unpassableTerrain = bboxCheckedObstacles.filter((it) => !(SATX.bboxCheck(posBbox, it.bbox) && it.colliders().some((c) => SATX.collideObject(posCollider, c)))) const colliders = unpassableTerrain.map((it) => it.colliders()).flat() diff --git a/src/template.js b/src/template.js index 82cb270..12ac6b5 100644 --- a/src/template.js +++ b/src/template.js @@ -37,8 +37,6 @@ export default class Template { } } - // TODO: incremental pathfinding stuck in thicker than recalculateDestRadius walls - // TODO: minions despawn prematurely (too large checkpointSize?) static #minionLogic(route = []) { const checkpointSize = 300 const maxDestDistance = 100 @@ -82,7 +80,6 @@ export default class Template { } } - // TODO: proper respawn static #playerLogic() { const entity = this if (entity.dead) { diff --git a/src/terrain.js b/src/terrain.js index 1e7c2de..9a98322 100644 --- a/src/terrain.js +++ b/src/terrain.js @@ -32,8 +32,6 @@ export default class Terrain { this.#calculateUnadjustedWaypoints() this.#calculateBbox() } - - get unadjustedWaypoints() { return this.#unadjustedWaypoints } get vertices() { return this.#vertices } static waypointsForSide(fromVertex, toVertex, isClockwise = false) { @@ -54,6 +52,7 @@ export default class Terrain { } colliders() { return this.#colliders } + unadjustedWaypoints() { return this.#unadjustedWaypoints } #shape() { const complexShape = new Shape()