add bbox checks to pathfinding graphs
This commit is contained in:
+74
-24
@@ -25,6 +25,7 @@ export default class Entity {
|
|||||||
maxHealth = 1
|
maxHealth = 1
|
||||||
memory = {}
|
memory = {}
|
||||||
pathfindingCooldown = 0
|
pathfindingCooldown = 0
|
||||||
|
pathfindingObstacleLimit = null
|
||||||
position = null
|
position = null
|
||||||
radius = 0
|
radius = 0
|
||||||
rotation = 0
|
rotation = 0
|
||||||
@@ -266,16 +267,26 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closestTargetTo(cursor, range) {
|
closestTargetTo(cursor, range) {
|
||||||
const visibleEntityIds = this.visibleEntities()
|
|
||||||
const entities = this.game?.entities
|
const entities = this.game?.entities
|
||||||
if (entities == null) { return }
|
if (entities == null) { return }
|
||||||
|
const targetsInRange = entities.filter((it) => this.team != it.team && it.distanceTo(cursor) <= range + this.radius + it.radius)
|
||||||
|
if (targetsInRange.length < 1) { return }
|
||||||
|
|
||||||
return entities
|
const absoluteClosestTarget = targetsInRange.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
|
||||||
.filter((it) => visibleEntityIds.includes(it.id) && this.team != it.team && it.distanceTo(cursor) <= range + this.radius + it.radius)
|
const entityIdsInDirectVision = this.entitiesInVision()
|
||||||
.reduce((e1, e2) => (e1?.distanceTo(cursor) ?? Infinity) < e2.distanceTo(cursor) ? e1 : e2, null)
|
if (entityIdsInDirectVision.includes(absoluteClosestTarget.id)) {
|
||||||
|
return absoluteClosestTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetsInDirectVision = targetsInRange.filter((it) => entityIdsInDirectVision.includes(it.id))
|
||||||
|
if (targetsInDirectVision.length < 1) { return }
|
||||||
|
|
||||||
|
const visibleEntityIds = this.visibleEntities()
|
||||||
|
const visibleEntitiesInRange = targetsInRange.it((it) => visibleEntityIds.includes(it.id))
|
||||||
|
|
||||||
|
return visibleEntitiesInRange.filter((it) => visibleEntityIds.includes(it.id) && this.team != it.team && it.distanceTo(cursor) <= range + this.radius + it.radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: collision and ghosting checks are duplicated
|
|
||||||
customBboxCollidables(bbox) {
|
customBboxCollidables(bbox) {
|
||||||
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
||||||
return entitiesAndTerrains.filter((it) => it.collision && !(this.ghosting && it.ghostable) && SATX.bboxCheck(bbox, it.bbox))
|
return entitiesAndTerrains.filter((it) => it.collision && !(this.ghosting && it.ghostable) && SATX.bboxCheck(bbox, it.bbox))
|
||||||
@@ -339,6 +350,7 @@ export default class Entity {
|
|||||||
this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
|
this.health = Math.min(Math.max(0, this.health + amount), this.maxHealth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: something is pushing ghostables out of ghosted entities' way
|
||||||
fixPosition() {
|
fixPosition() {
|
||||||
const fixedPosition = this.fixFuturePosition(this.position)
|
const fixedPosition = this.fixFuturePosition(this.position)
|
||||||
if (this.position.equals(fixedPosition)) { return }
|
if (this.position.equals(fixedPosition)) { return }
|
||||||
@@ -347,17 +359,17 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fixFuturePosition(futurePosition) {
|
fixFuturePosition(futurePosition) {
|
||||||
|
const maxX = this.game?.width ?? Infinity
|
||||||
|
const maxY = this.game?.height ?? Infinity
|
||||||
|
const radius = this.radius
|
||||||
if (!this.willCollide(futurePosition)) {
|
if (!this.willCollide(futurePosition)) {
|
||||||
return futurePosition
|
return SATX.clamp(futurePosition, maxX, maxY, radius)
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction = new Vector2(0, 5)
|
let direction = new Vector2(0, 5)
|
||||||
let multiplier = 1
|
let multiplier = 1
|
||||||
const rotationSlices = 16
|
const rotationSlices = 16
|
||||||
const origin = new Vector2()
|
const origin = new Vector2()
|
||||||
const maxX = this.game?.width ?? Infinity
|
|
||||||
const maxY = this.game?.height ?? Infinity
|
|
||||||
const radius = this.radius
|
|
||||||
|
|
||||||
for (let limit = 1; limit <= 10000; limit++) {
|
for (let limit = 1; limit <= 10000; limit++) {
|
||||||
const rads = (limit % rotationSlices) * 2 * Math.PI / rotationSlices
|
const rads = (limit % rotationSlices) * 2 * Math.PI / rotationSlices
|
||||||
@@ -572,12 +584,19 @@ export default class Entity {
|
|||||||
|
|
||||||
const fixedDest = this.fixFuturePosition(this.#dest)
|
const fixedDest = this.fixFuturePosition(this.#dest)
|
||||||
const pathfinding = this.#noPathfindingUntil <= currentTick
|
const pathfinding = this.#noPathfindingUntil <= currentTick
|
||||||
|
const obstacles = new Map()
|
||||||
|
let pathGotObstructed = false
|
||||||
|
|
||||||
if (pathfinding && this.#path.length > 0) {
|
if (pathfinding && this.#path.length > 0) {
|
||||||
const sectionDest = this.#path.at(0)
|
const sectionDest = this.#path.at(0)
|
||||||
const lineOfSight = this.isInLineOfSight(sectionDest)
|
const sectionObstacles = this.obstaclesInStraightPath(sectionDest)
|
||||||
if (!lineOfSight) {
|
if (sectionObstacles.length > 0) {
|
||||||
this.#path = []
|
pathGotObstructed = true
|
||||||
|
for (const obstacle of sectionObstacles) {
|
||||||
|
if (!obstacles.has(obstacle.id)) {
|
||||||
|
obstacles.set(obstacle.id, obstacle)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,40 +607,71 @@ export default class Entity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathfinding && (this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) {
|
if (pathfinding && (pathGotObstructed || this.#path.length < 1 || (this.#path.at(-1)?.distanceTo(fixedDest) ?? 0) > 0.01)) {
|
||||||
const start = SATX.vectorToFloat32Array(this.position)
|
const start = SATX.vectorToFloat32Array(this.position)
|
||||||
const goal = SATX.vectorToFloat32Array(fixedDest)
|
const goal = SATX.vectorToFloat32Array(fixedDest)
|
||||||
const obstacles = new Map() // TODO: limit number of obstacles for non-important entities (property on the class?)
|
|
||||||
const obstacleWaypoints = new Map()
|
const obstacleWaypoints = new Map()
|
||||||
const obstacleColliders = new Map()
|
const obstacleColliders = new Map()
|
||||||
|
const obstacleBboxes = new Map()
|
||||||
|
|
||||||
// TODO: pathfinding takes longer after bbox check implementation (maybe separate obstacleColliders into two, and index match)
|
const initialObstaclesMargin = this.radius + 20
|
||||||
for (let failsafe = 0; failsafe < 1000; failsafe++) {
|
const initialObstacles = this.customBboxCollidables(new Float32Array([
|
||||||
|
this.position.y + initialObstaclesMargin,
|
||||||
|
this.position.x + initialObstaclesMargin,
|
||||||
|
this.position.y - initialObstaclesMargin,
|
||||||
|
this.position.x - initialObstaclesMargin,
|
||||||
|
]))
|
||||||
|
|
||||||
|
for (const obstacle of initialObstacles) {
|
||||||
|
if (!obstacles.has(obstacle.id)) {
|
||||||
|
obstacles.set(obstacle.id, obstacle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let failsafe = 0; failsafe <= (this.pathfindingObstacleLimit ?? 1000); failsafe++) {
|
||||||
|
if (failsafe >= 10) { console.error('Failsafe is reached!!!'); process.exit(0) }
|
||||||
const obstaclesArray = Array.from(obstacles.values())
|
const obstaclesArray = Array.from(obstacles.values())
|
||||||
|
|
||||||
for (const obstacle of obstaclesArray) {
|
for (const obstacle of obstaclesArray) {
|
||||||
if (!obstacleWaypoints.has(obstacle.id)) {
|
if (!obstacleWaypoints.has(obstacle.id)) {
|
||||||
const waypoint = obstacle.unadjustedWaypoints().map(([w, d]) => SATX.vectorToFloat32Array(this.adjustWaypoint(w, d)))
|
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 bbox = obstacle.bbox
|
||||||
const colliders = obstacle.colliders()
|
const colliders = obstacle.colliders()
|
||||||
obstacleColliders.set(obstacle.id, [bbox, colliders])
|
obstacleWaypoints.set(obstacle.id, waypoint)
|
||||||
|
obstacleColliders.set(obstacle.id, colliders)
|
||||||
|
obstacleBboxes.set(obstacle.id, bbox)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const waypoints = [
|
const waypoints = [
|
||||||
start,
|
start,
|
||||||
goal,
|
goal,
|
||||||
...obstaclesArray.map((it) => obstacleWaypoints.get(it.id)).flat()
|
...Array.from(obstacleWaypoints.values()).flat()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const bboxesSize = obstacleBboxes.size * 5
|
||||||
|
const bboxes = new Float32Array(bboxesSize)
|
||||||
|
let i = 0
|
||||||
|
for (const obstacle of obstacleBboxes.values()) {
|
||||||
|
bboxes[i] = obstacle[0]
|
||||||
|
bboxes[i + 1] = obstacle[1]
|
||||||
|
bboxes[i + 2] = obstacle[2]
|
||||||
|
bboxes[i + 3] = obstacle[3]
|
||||||
|
bboxes[i + 4] = Math.floor(i / 5)
|
||||||
|
i += 5
|
||||||
|
}
|
||||||
|
|
||||||
const colliders = Array.from(obstacleColliders.values())
|
const colliders = Array.from(obstacleColliders.values())
|
||||||
const graph = Pathfind.buildGraph(waypoints, colliders, this.radius)
|
const graph = Pathfind.buildGraph(waypoints, bboxes, colliders, this.radius)
|
||||||
const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
|
const path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1]))
|
||||||
|
|
||||||
if (path.length == 0) { break } // goal unreachable
|
if (path.length == 0) {
|
||||||
|
// WARNING: This unsets the destination because if an unreachable spot is clicked,
|
||||||
|
// pathfinding cycles all obstacles forever. A possible alternative could
|
||||||
|
// be setting a pathfinding timeout, but then moveAction must reset that!
|
||||||
|
this.#dest = null
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
let obstacleInPath = false
|
let obstacleInPath = false
|
||||||
let lastSection = this.position
|
let lastSection = this.position
|
||||||
@@ -639,8 +689,8 @@ export default class Entity {
|
|||||||
lastSection = section
|
lastSection = section
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#path = path
|
||||||
if (!obstacleInPath) {
|
if (!obstacleInPath) {
|
||||||
this.#path = path
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
import { WebSocketExpress } from 'websocket-express'
|
import { WebSocketExpress } from 'websocket-express'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import Game from './game.js'
|
import Game from './game.js'
|
||||||
import { Dungeon } from './level.js'
|
import { Dungeon, Ravine } from './level.js'
|
||||||
|
|
||||||
const app = new WebSocketExpress()
|
const app = new WebSocketExpress()
|
||||||
const port = 1280
|
const port = 1280
|
||||||
@@ -39,5 +39,6 @@ app.ws('/ws', async (req, res) => {
|
|||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.info(`Server started! Visit http://localhost:${port}`)
|
console.info(`Server started! Visit http://localhost:${port}`)
|
||||||
|
|
||||||
Dungeon.scenario(game)
|
// Dungeon.scenario(game)
|
||||||
|
Ravine.scenario(game)
|
||||||
})
|
})
|
||||||
|
|||||||
+43
-29
@@ -1,5 +1,4 @@
|
|||||||
import { Vector2 } from 'three'
|
import { Vector2 } from 'three'
|
||||||
import Ability from './ability.js'
|
|
||||||
import Entity from './entity.js'
|
import Entity from './entity.js'
|
||||||
import Team from './team.js'
|
import Team from './team.js'
|
||||||
import Template from './template.js'
|
import Template from './template.js'
|
||||||
@@ -10,36 +9,51 @@ export class Dungeon {
|
|||||||
game.width = 3000
|
game.width = 3000
|
||||||
game.height = 3000
|
game.height = 3000
|
||||||
|
|
||||||
const playerSpawn = new Vector2(game.width / 2, game.height / 2)
|
// const playerSpawn = new Vector2(game.width / 2, game.height / 2)
|
||||||
game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: playerSpawn, position: new Vector2(playerSpawn.x - 1300, playerSpawn.y - 500), team: Team.blue })))
|
// game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: playerSpawn, position: new Vector2(playerSpawn.x - 1300, playerSpawn.y - 500), team: Team.blue })))
|
||||||
const dummyLogic = function dummyLogic() {
|
// const dummyLogic = function dummyLogic() {
|
||||||
const entity = this
|
// const entity = this
|
||||||
if (entity.position.x > 1250) {
|
// if (entity.position.x > 1250) {
|
||||||
entity.moveAction(new Vector2(500, entity.position.y))
|
// entity.moveAction(new Vector2(500, entity.position.y))
|
||||||
}
|
// }
|
||||||
else if (entity.position.x < 550 || entity.destination == null) {
|
// else if (entity.position.x < 550 || entity.destination == null) {
|
||||||
entity.moveAction(new Vector2(1300, entity.position.y))
|
// entity.moveAction(new Vector2(1300, entity.position.y))
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (game.currentTick > 0 && game.currentTick % (6 * game.tickRate) == 0) {
|
// if (game.currentTick > 0 && game.currentTick % (6 * game.tickRate) == 0) {
|
||||||
entity.castAction('q', playerSpawn)
|
// entity.castAction('q', playerSpawn)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
// const dummy = { radius: 100, visualRadius: 50, abilities: { q: Ability.straightShot.id }, logic: dummyLogic }
|
||||||
|
// game.spawnEntity(new Entity({ ...dummy, position: new Vector2(1 * (game.width / 5), 1 * (game.height / 4)) }))
|
||||||
|
// game.spawnEntity(new Entity({ ...dummy, position: new Vector2(1 * (game.width / 5), 3 * (game.height / 4)) }))
|
||||||
|
// game.addTerrain(new Terrain([
|
||||||
|
// new Vector2(3.5 * (game.width / 10), 1.6 * (game.height / 5)),
|
||||||
|
// new Vector2(3.5 * (game.width / 10), 1.4 * (game.height / 5)),
|
||||||
|
// new Vector2(4 * (game.width / 10), 1.4 * (game.height / 5)),
|
||||||
|
// new Vector2(4 * (game.width / 10), 1.6 * (game.height / 5)),
|
||||||
|
// ]))
|
||||||
|
// game.addTerrain(new Terrain([
|
||||||
|
// new Vector2(3 * (game.width / 10), 2 * (game.height / 5)),
|
||||||
|
// new Vector2(3 * (game.width / 10), 1 * (game.height / 5)),
|
||||||
|
// new Vector2(4 * (game.width / 10), 1 * (game.height / 5)),
|
||||||
|
// new Vector2(4 * (game.width / 10), 2 * (game.height / 5)),
|
||||||
|
// ], false))
|
||||||
|
|
||||||
|
const from = new Vector2(100, 100)
|
||||||
|
game.height = 2000
|
||||||
|
game.spawnEntity(new Entity(Template.player({ id: '1', spawnPosition: from, pathfindingObstacleLimit: 1, pathfindingCooldown: 0 })))
|
||||||
|
for (let i = 100; i < game.width; i += 300) {
|
||||||
|
const highest = ((i - 100) % 600) == 0 ? 0 : 500
|
||||||
|
const lowest = ((i - 100) % 600) == 0 ? 1500 : 2000
|
||||||
|
game.addTerrain(new Terrain([
|
||||||
|
new Vector2(i + 100, game.height - highest),
|
||||||
|
new Vector2(i, game.height - highest),
|
||||||
|
new Vector2(i, game.height - lowest),
|
||||||
|
new Vector2(i + 100, game.height - lowest),
|
||||||
|
]))
|
||||||
}
|
}
|
||||||
const dummy = { radius: 100, visualRadius: 50, abilities: { q: Ability.straightShot.id }, logic: dummyLogic }
|
|
||||||
game.spawnEntity(new Entity({ ...dummy, position: new Vector2(1 * (game.width / 5), 1 * (game.height / 4)) }))
|
|
||||||
game.spawnEntity(new Entity({ ...dummy, position: new Vector2(1 * (game.width / 5), 3 * (game.height / 4)) }))
|
|
||||||
game.addTerrain(new Terrain([
|
|
||||||
new Vector2(3.5 * (game.width / 10), 1.6 * (game.height / 5)),
|
|
||||||
new Vector2(3.5 * (game.width / 10), 1.4 * (game.height / 5)),
|
|
||||||
new Vector2(4 * (game.width / 10), 1.4 * (game.height / 5)),
|
|
||||||
new Vector2(4 * (game.width / 10), 1.6 * (game.height / 5)),
|
|
||||||
]))
|
|
||||||
game.addTerrain(new Terrain([
|
|
||||||
new Vector2(3 * (game.width / 10), 2 * (game.height / 5)),
|
|
||||||
new Vector2(3 * (game.width / 10), 1 * (game.height / 5)),
|
|
||||||
new Vector2(4 * (game.width / 10), 1 * (game.height / 5)),
|
|
||||||
new Vector2(4 * (game.width / 10), 2 * (game.height / 5)),
|
|
||||||
], false))
|
|
||||||
|
|
||||||
game.start()
|
game.start()
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-12
@@ -77,17 +77,26 @@ export default class Pathfind {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
static buildGraph(waypoints = [], colliders = [], radius = 0, mergeNodes = true) {
|
static buildGraph(waypoints, bboxes, obstacles, radius) {
|
||||||
const filteredWaypoints = []
|
const filteredWaypoints = []
|
||||||
const checked = new Set()
|
const checked = new Set()
|
||||||
|
|
||||||
if (radius > 0) {
|
if (radius > 0) {
|
||||||
for (const waypoint of waypoints) {
|
for (const waypoint of waypoints) {
|
||||||
const bbox = Entity.bbox(waypoint[0], waypoint[1], radius) // TODO: duplicate bbox calculation logic for speed
|
const bbox = Entity.bbox(waypoint[0], waypoint[1], radius)
|
||||||
const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0]))
|
const bboxCheckedObstacles = []
|
||||||
|
for (let i = 0; i < bboxes.length; i += 5) {
|
||||||
|
if (bbox[0] <= bboxes[i + 2]) { continue }
|
||||||
|
if (bbox[1] <= bboxes[i + 3]) { continue }
|
||||||
|
if (bbox[2] >= bboxes[i]) { continue }
|
||||||
|
if (bbox[3] >= bboxes[i + 1]) { continue }
|
||||||
|
|
||||||
|
bboxCheckedObstacles.push(obstacles[bboxes[i + 4]])
|
||||||
|
}
|
||||||
|
|
||||||
if (bboxCheckedObstacles.length > 0) {
|
if (bboxCheckedObstacles.length > 0) {
|
||||||
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
|
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
|
||||||
const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(collider, c)))
|
const colliding = bboxCheckedObstacles.flat().some((it) => SATX.collideObject(collider, it))
|
||||||
if (colliding) {
|
if (colliding) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -111,7 +120,7 @@ export default class Pathfind {
|
|||||||
if (i == j) {
|
if (i == j) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(mergedWaypoints[i] - mergedWaypoints[j]) < Pathfind.precision && Math.abs(mergedWaypoints[i + 1] - mergedWaypoints[j + 1]) < Pathfind.precision) {
|
if (Math.abs(mergedWaypoints[i] - mergedWaypoints[j]) < Pathfind.precision && Math.abs(mergedWaypoints[i + 1] - mergedWaypoints[j + 1]) < Pathfind.precision) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -124,11 +133,21 @@ export default class Pathfind {
|
|||||||
checked.add(key)
|
checked.add(key)
|
||||||
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
|
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 bbox = Entity.tunnelBbox(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
|
||||||
const bboxCheckedObstacles = colliders.filter((it) => SATX.bboxCheck(bbox, it[0]))
|
|
||||||
|
const bboxCheckedObstacles = []
|
||||||
|
for (let i = 0; i < bboxes.length; i += 5) {
|
||||||
|
if (bbox[0] <= bboxes[i + 2]) { continue }
|
||||||
|
if (bbox[1] <= bboxes[i + 3]) { continue }
|
||||||
|
if (bbox[2] >= bboxes[i]) { continue }
|
||||||
|
if (bbox[3] >= bboxes[i + 1]) { continue }
|
||||||
|
|
||||||
|
bboxCheckedObstacles.push(obstacles[bboxes[i + 4]])
|
||||||
|
}
|
||||||
|
|
||||||
if (bboxCheckedObstacles.length > 0) {
|
if (bboxCheckedObstacles.length > 0) {
|
||||||
const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
|
const tunnel = Entity.tunnelCollider(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
|
||||||
const colliding = bboxCheckedObstacles.some((it) => it[1].some((c) => SATX.collideObject(tunnel, c)))
|
const colliding = bboxCheckedObstacles.some((it) => it.some((c) => SATX.collideObject(tunnel, c)))
|
||||||
if (colliding) {
|
if (colliding) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -152,10 +171,6 @@ export default class Pathfind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mergeNodes) {
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
const graph = new Float32Array(nodes.length * 5)
|
const graph = new Float32Array(nodes.length * 5)
|
||||||
let graphIndex = 0
|
let graphIndex = 0
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
@@ -167,6 +182,15 @@ export default class Pathfind {
|
|||||||
graphIndex += 5
|
graphIndex += 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const niceGraph = []
|
||||||
|
// for (let i = 0; i < graph.length / 5; i += 5) {
|
||||||
|
// niceGraph.push({
|
||||||
|
// from: [graph[i], graph[i + 1]],
|
||||||
|
// to: [graph[i + 2], graph[i + 3]],
|
||||||
|
// distance: graph[i + 4],
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// console.log(niceGraph)
|
||||||
return graph
|
return graph
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-9
@@ -1,14 +1,16 @@
|
|||||||
import { Vector2 } from 'three'
|
import { Vector2 } from 'three'
|
||||||
import Ability from './ability.js'
|
import Ability from './ability.js'
|
||||||
|
import Team from './team.js'
|
||||||
|
|
||||||
export default class Template {
|
export default class Template {
|
||||||
static minion(team, options = {}) {
|
static minion(team, options = {}) {
|
||||||
return {
|
return {
|
||||||
abilities: { a: options.ranged ? Ability.rangedAttack.id : Ability.meleeAttack.id },
|
abilities: { a: options.ranged ? Ability.rangedAttack.id : Ability.meleeAttack.id },
|
||||||
height: options.ranged ? 40 : 38,
|
height: options.ranged ? 40 : 38,
|
||||||
logic: this.#minionLogic(options.route),
|
logic: this.#minionLogic(options.route, (team != Team.blue)),
|
||||||
maxHealth: options.ranged ? 300 : 450,
|
maxHealth: options.ranged ? 300 : 450,
|
||||||
pathfindingCooldown: 0.2,
|
pathfindingCooldown: 0.2,
|
||||||
|
pathfindingObstacleLimit: 0,
|
||||||
position: options.route?.at(0) ?? options.position ?? new Vector2(0, 0),
|
position: options.route?.at(0) ?? options.position ?? new Vector2(0, 0),
|
||||||
radius: 48,
|
radius: 48,
|
||||||
speed: 325,
|
speed: 325,
|
||||||
@@ -29,15 +31,16 @@ export default class Template {
|
|||||||
height: 80,
|
height: 80,
|
||||||
logic: this.#playerLogic,
|
logic: this.#playerLogic,
|
||||||
maxHealth: 600,
|
maxHealth: 600,
|
||||||
spawnPosition: new Vector2(500, 150),
|
pathfindingObstacleLimit: 3,
|
||||||
radius: 65,
|
radius: 65,
|
||||||
|
spawnPosition: new Vector2(500, 150),
|
||||||
visionRange: 1350,
|
visionRange: 1350,
|
||||||
visualRadius: 40,
|
visualRadius: 40,
|
||||||
...overrides,
|
...overrides,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static #minionLogic(route = []) {
|
static #minionLogic(route = [], odd = false) {
|
||||||
const checkpointSize = 300
|
const checkpointSize = 300
|
||||||
const maxDestDistance = 100
|
const maxDestDistance = 100
|
||||||
const recalculateDestRadius = 50
|
const recalculateDestRadius = 50
|
||||||
@@ -47,8 +50,15 @@ export default class Template {
|
|||||||
const entity = this
|
const entity = this
|
||||||
if (entity.dead) { entity.despawn() }
|
if (entity.dead) { entity.despawn() }
|
||||||
|
|
||||||
|
const currentTick = entity.game?.currentTick ?? 0
|
||||||
|
const minionResponseTime = Math.floor(0.1 * (entity.game?.tickRate ?? 1))
|
||||||
|
if (!(currentTick % minionResponseTime == 0 && Math.floor(currentTick / minionResponseTime) % 2 == (odd ? 1 : 0))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const target = entity.closestTargetTo(entity.position, aggroRadius)
|
const target = entity.closestTargetTo(entity.position, aggroRadius)
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
|
entity.ghosting = false
|
||||||
entity.attackAction(target.position)
|
entity.attackAction(target.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,13 +73,14 @@ export default class Template {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((entity.destination?.distanceTo(entity.position) ?? 0) < recalculateDestRadius) {
|
if ((entity.destination?.distanceTo(entity.position) ?? 0) < recalculateDestRadius) {
|
||||||
const distanceToGoal = entity.distanceTo(goal)
|
// const distanceToGoal = entity.distanceTo(goal)
|
||||||
if (distanceToGoal > maxDestDistance) {
|
// if (distanceToGoal > maxDestDistance) {
|
||||||
const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(maxDestDistance)
|
// const direction = goal.clone().sub(entity.position).normalize().multiplyScalar(maxDestDistance)
|
||||||
goal.copy(entity.position.clone().add(direction))
|
// goal.copy(entity.position.clone().add(direction))
|
||||||
}
|
// }
|
||||||
|
|
||||||
entity.attackAction(goal)
|
entity.ghosting = true
|
||||||
|
entity.moveAction(goal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user