use bounding boxes to optimize collision detection
This commit is contained in:
+183
-23
@@ -8,6 +8,7 @@ import Buff from './buff.js'
|
||||
export default class Entity {
|
||||
id = crypto.randomUUID()
|
||||
abilities = {}
|
||||
bbox = new Float32Array(4)
|
||||
buffs = []
|
||||
casting = null
|
||||
cooldowns = {}
|
||||
@@ -35,6 +36,82 @@ export default class Entity {
|
||||
return new SAT.Circle(new SAT.Vector(x, y), radius)
|
||||
}
|
||||
|
||||
// deliberate code duplication for performance
|
||||
static tunnelCollider(fromX, fromY, toX, toY, radius) {
|
||||
if (radius <= 0) {
|
||||
return SATX.line(fromX, fromY, toX, toY)
|
||||
}
|
||||
|
||||
const sides = new Float32Array(5)
|
||||
sides[0] = toX - fromX
|
||||
sides[1] = toY - fromY
|
||||
sides[4] = Math.hypot(sides[0], sides[1])
|
||||
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
|
||||
sides[3] = (sides[0] / sides[4]) * radius
|
||||
|
||||
return new SAT.Polygon(new SAT.Vector(fromX - sides[2], fromY - sides[3]), [
|
||||
new SAT.Vector(),
|
||||
new SAT.Vector(sides[0], sides[1]),
|
||||
new SAT.Vector(sides[0] + (2 * sides[2]), sides[1] + (2 * sides[3])),
|
||||
new SAT.Vector(2 * sides[2], 2 * sides[3]),
|
||||
])
|
||||
}
|
||||
|
||||
// deliberate code duplication for performance
|
||||
static tunnelVertices(fromX, fromY, toX, toY, radius) {
|
||||
const sides = new Float32Array(5)
|
||||
sides[0] = toX - fromX
|
||||
sides[1] = toY - fromY
|
||||
sides[4] = Math.hypot(sides[0], sides[1])
|
||||
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
|
||||
sides[3] = (sides[0] / sides[4]) * radius
|
||||
|
||||
return [
|
||||
new Vector2(fromX - sides[2], fromY - sides[3]),
|
||||
new Vector2(fromX - sides[2] + sides[0], fromY - sides[3] + sides[1]),
|
||||
new Vector2(fromX + sides[2] + sides[0], fromY + sides[3] + sides[1]),
|
||||
new Vector2(fromX + sides[2], fromY + sides[3]),
|
||||
]
|
||||
}
|
||||
|
||||
// deliberate code duplication for performance
|
||||
static tunnelBbox(fromX, fromY, toX, toY, radius) {
|
||||
if (radius <= 0) {
|
||||
return new Float32Array([
|
||||
Math.max(fromY, toY),
|
||||
Math.max(fromX, toX),
|
||||
Math.min(fromY, toY),
|
||||
Math.min(fromX, toX),
|
||||
])
|
||||
}
|
||||
|
||||
const sides = new Float32Array(5)
|
||||
sides[0] = toX - fromX
|
||||
sides[1] = toY - fromY
|
||||
sides[4] = Math.hypot(sides[0], sides[1])
|
||||
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
|
||||
sides[3] = (sides[0] / sides[4]) * radius
|
||||
|
||||
const offsetX = fromX + sides[0]
|
||||
const x1 = fromX - sides[2]
|
||||
const x2 = fromX + sides[2]
|
||||
const x3 = offsetX - sides[2]
|
||||
const x4 = offsetX + sides[2]
|
||||
|
||||
const offsetY = fromY + sides[1]
|
||||
const y1 = fromY - sides[3]
|
||||
const y2 = fromY + sides[3]
|
||||
const y3 = offsetY - sides[3]
|
||||
const y4 = offsetY + sides[3]
|
||||
|
||||
return new Float32Array([
|
||||
Math.max(y1, y2, y3, y4),
|
||||
Math.max(x1, x2, x3, x4),
|
||||
Math.min(y1, y2, y3, y4),
|
||||
Math.min(x1, x2, x3, x4),
|
||||
])
|
||||
}
|
||||
|
||||
constructor(options = {}) {
|
||||
Object.entries(options).forEach(([key, value]) => this[key] = value)
|
||||
if (this.position == null) {
|
||||
@@ -48,6 +125,7 @@ export default class Entity {
|
||||
}
|
||||
}
|
||||
|
||||
get attacking() { return this.#attacking }
|
||||
get destination() { return this.#dest }
|
||||
get logic() { return this.#logic }
|
||||
get game() { return this.#game }
|
||||
@@ -133,7 +211,7 @@ export default class Entity {
|
||||
|
||||
this.#attacking = attack
|
||||
this.#moving = true
|
||||
this.#dest = SATX.fixCollisions(cursor, this.collidables(), this.radius, this.game?.width, this.game?.height)
|
||||
this.#dest = cursor.clone()
|
||||
}
|
||||
|
||||
stopAction() {
|
||||
@@ -174,14 +252,11 @@ export default class Entity {
|
||||
}
|
||||
|
||||
collidables() {
|
||||
const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id).map((e) => e.collider())
|
||||
const terrainColliders = (this.game?.terrains ?? []).map((t) => t.colliders()).flat()
|
||||
|
||||
return entityColliders.concat(terrainColliders)
|
||||
return this.customBboxCollidables(this.bbox)
|
||||
}
|
||||
|
||||
collider() {
|
||||
return new SAT.Circle(new SAT.Vector(this.position.x, this.position.y), this.radius)
|
||||
return Entity.collider(this.position.x, this.position.y, this.radius)
|
||||
}
|
||||
|
||||
colliders() {
|
||||
@@ -221,6 +296,20 @@ export default class Entity {
|
||||
return this.position.distanceTo(cursor)
|
||||
}
|
||||
|
||||
futureCollidables(futurePosition) {
|
||||
return this.customBboxCollidables(new Float32Array([
|
||||
futurePosition.y + this.radius,
|
||||
futurePosition.x + this.radius,
|
||||
futurePosition.y - this.radius,
|
||||
futurePosition.x - this.radius,
|
||||
]))
|
||||
}
|
||||
|
||||
customBboxCollidables(bbox) {
|
||||
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
||||
return entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
||||
}
|
||||
|
||||
getBuff(id) {
|
||||
const entityBuff = this.buffs.find((it) => it.id == id)
|
||||
if (entityBuff == null) { return }
|
||||
@@ -240,11 +329,70 @@ export default class Entity {
|
||||
}
|
||||
|
||||
fixPosition() {
|
||||
this.position = SATX.fixCollisions(this.position, this.collidables(), this.radius, this.game?.width, this.game?.height).clone()
|
||||
this.position = this.fixFuturePosition(this.position.clone()).clone()
|
||||
}
|
||||
|
||||
isColliding(...colliders) {
|
||||
return SATX.collideObjects(this.collider(), colliders)
|
||||
fixFuturePosition(futurePosition) {
|
||||
if (!this.willCollide(futurePosition)) {
|
||||
return futurePosition
|
||||
}
|
||||
|
||||
let direction = new Vector2(0, 5)
|
||||
let multiplier = 1
|
||||
const rotationSlices = 16
|
||||
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++) {
|
||||
const rads = (limit % rotationSlices) * 2 * Math.PI / rotationSlices
|
||||
const offset = direction.clone().rotateAround(origin, rads).multiplyScalar(multiplier)
|
||||
const position = SATX.clamp(futurePosition.clone().add(offset), maxX, maxY, radius)
|
||||
if (!this.willCollide(position)) {
|
||||
return position
|
||||
}
|
||||
|
||||
if (limit % rotationSlices == 0) {
|
||||
multiplier++
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Can't fix position ([${futurePosition.x}, ${futurePosition.y}]) of entity ID: ${this.id}`)
|
||||
}
|
||||
|
||||
isColliding() {
|
||||
const collidables = this.collidables()
|
||||
if (collidables.length < 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const colliders = collidables.map((it) => it.colliders()).flat()
|
||||
const collider = this.collider()
|
||||
|
||||
return colliders.some((it) => SATX.collideObject(collider, it))
|
||||
}
|
||||
|
||||
obstaclesInStraightPath(destination, position = this.position) {
|
||||
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
|
||||
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
||||
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
||||
if (bboxCheckedObstacles.length < 1) { return [] }
|
||||
|
||||
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
|
||||
return bboxCheckedObstacles.filter((obstacle) => obstacle.colliders().some((it) => SATX.collideObject(collider, it)))
|
||||
}
|
||||
|
||||
isInLineOfSight(destination, position = this.position) {
|
||||
const bbox = Entity.tunnelBbox(position.x, position.y, destination.x, destination.y, this.radius)
|
||||
const entitiesAndTerrains = (this.game?.entities ?? []).filter((it) => it.id != this.id).concat(this.game?.terrains ?? [])
|
||||
const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => SATX.bboxCheck(bbox, it.bbox))
|
||||
if (bboxCheckedObstacles.length < 1) { return true }
|
||||
|
||||
|
||||
const colliders = bboxCheckedObstacles.map((it) => it.colliders()).flat()
|
||||
const collider = Entity.tunnelCollider(position.x, position.y, destination.x, destination.y, this.radius)
|
||||
return !colliders.some((it) => SATX.collideObject(collider, it))
|
||||
}
|
||||
|
||||
removeBuff(id) {
|
||||
@@ -277,6 +425,8 @@ export default class Entity {
|
||||
if (this.#logic != null) {
|
||||
this.#logic()
|
||||
}
|
||||
|
||||
this.#calculateBbox()
|
||||
}
|
||||
|
||||
waypoints() {
|
||||
@@ -287,6 +437,25 @@ export default class Entity {
|
||||
return unadjustedWaypoints.map(([waypoint, direction]) => this.adjustWaypoint(waypoint, direction)) ?? []
|
||||
}
|
||||
|
||||
willCollide(futurePosition) {
|
||||
const collidables = this.futureCollidables(futurePosition)
|
||||
if (collidables.length < 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const colliders = collidables.map((it) => it.colliders()).flat()
|
||||
const collider = Entity.collider(futurePosition.x, futurePosition.y, this.radius)
|
||||
|
||||
return colliders.some((it) => SATX.collideObject(collider, it))
|
||||
}
|
||||
|
||||
#calculateBbox() {
|
||||
this.bbox[0] = this.position.y + this.radius
|
||||
this.bbox[1] = this.position.x + this.radius
|
||||
this.bbox[2] = this.position.y - this.radius
|
||||
this.bbox[3] = this.position.x - this.radius
|
||||
}
|
||||
|
||||
#cast() {
|
||||
if (this.casting == null) {
|
||||
return false
|
||||
@@ -333,22 +502,18 @@ export default class Entity {
|
||||
|
||||
if (!this.#moving || this.#dest == null) { return false }
|
||||
|
||||
// TODO: bounding boxes to early discard a lot of 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 = this.fixFuturePosition(this.#dest)
|
||||
|
||||
if (this.#path.length > 0) {
|
||||
const sectionDest = this.#path.at(0)
|
||||
const sectionTunnel = SATX.entityTunnel(this.position.x, this.position.y, sectionDest.x, sectionDest.y, this.radius)
|
||||
const lineOfSight = !SATX.collideObjects(sectionTunnel, collidables)
|
||||
const lineOfSight = this.isInLineOfSight(sectionDest)
|
||||
if (!lineOfSight) {
|
||||
this.#path = []
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) {
|
||||
const tunnel = SATX.entityTunnel(this.position.x, this.position.y, fixedDest.x, fixedDest.y, this.radius)
|
||||
const lineOfSight = !SATX.collideObjects(tunnel, collidables)
|
||||
const lineOfSight = this.isInLineOfSight(fixedDest)
|
||||
if (lineOfSight) {
|
||||
this.#path = [fixedDest]
|
||||
}
|
||||
@@ -375,9 +540,7 @@ export default class Entity {
|
||||
let obstacleInPath = false
|
||||
let lastSection = this.position
|
||||
for (const section of path) {
|
||||
const tunnel = SATX.entityTunnel(lastSection.x, lastSection.y, section.x, section.y, this.radius)
|
||||
const globalObstacles = this.game.terrains.concat(this.game.entities.filter((e) => e.id != this.id))
|
||||
const sectionObstacles = SATX.collideObstacles(tunnel, globalObstacles)
|
||||
const sectionObstacles = this.obstaclesInStraightPath(section, lastSection)
|
||||
if (sectionObstacles.length > 0) {
|
||||
obstacleInPath = true
|
||||
const obstacleIds = obstacles.map((o) => o.id)
|
||||
@@ -408,12 +571,9 @@ export default class Entity {
|
||||
const position = distance <= speed ? destination : stepTaken
|
||||
const rotation = direction.angle()
|
||||
|
||||
const collider = Entity.collider(position.x, position.y, this.radius)
|
||||
const isColliding = SATX.collideObjects(collider, this.collidables())
|
||||
|
||||
this.rotation = rotation
|
||||
|
||||
if (!isColliding) {
|
||||
if (!this.willCollide(position)) {
|
||||
this.position.copy(position)
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -81,7 +81,7 @@ export default class Game {
|
||||
}
|
||||
|
||||
start() {
|
||||
setInterval(() => this.#gameLoop(), 1)
|
||||
setInterval(this.#gameLoopCall.bind(this), 1)
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -128,4 +128,8 @@ export default class Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#gameLoopCall() {
|
||||
this.#gameLoop()
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -42,7 +42,7 @@ export default class Map {
|
||||
// new Vector2(3234, 1378),
|
||||
// ],
|
||||
|
||||
// top-left wall
|
||||
// top-left wall (bottom part)
|
||||
[
|
||||
new Vector2(0, 10000),
|
||||
new Vector2(0, 820),
|
||||
@@ -69,6 +69,13 @@ export default class Map {
|
||||
new Vector2(660, 8968),
|
||||
new Vector2(705, 9049),
|
||||
new Vector2(771, 9127),
|
||||
new Vector2(760, 9104),
|
||||
],
|
||||
|
||||
// top-left wall (top part)
|
||||
[
|
||||
new Vector2(0, 10000),
|
||||
new Vector2(760, 9104),
|
||||
new Vector2(849, 9193),
|
||||
new Vector2(930, 9220),
|
||||
new Vector2(1008, 9238),
|
||||
@@ -94,8 +101,9 @@ export default class Map {
|
||||
new Vector2(9186, 10000),
|
||||
],
|
||||
|
||||
// bottom-right wall
|
||||
// bottom-right wall (right part)
|
||||
[
|
||||
new Vector2(10000, 0),
|
||||
new Vector2(10000, 9127),
|
||||
new Vector2(9678, 9004),
|
||||
new Vector2(9684, 7003),
|
||||
@@ -122,6 +130,13 @@ export default class Map {
|
||||
new Vector2(9357, 1093),
|
||||
new Vector2(9324, 1006),
|
||||
new Vector2(9288, 943),
|
||||
new Vector2(9268, 904),
|
||||
],
|
||||
|
||||
// bottom-right wall (bottom part)
|
||||
[
|
||||
new Vector2(10000, 0),
|
||||
new Vector2(9268, 904),
|
||||
new Vector2(9246, 883),
|
||||
new Vector2(9186, 835),
|
||||
new Vector2(9105, 796),
|
||||
|
||||
+5
-4
@@ -84,7 +84,7 @@ export default class Pathfind {
|
||||
if (radius > 0) {
|
||||
for (const waypoint of waypoints) {
|
||||
const collider = Entity.collider(waypoint[0], waypoint[1], radius)
|
||||
const waypointAvailable = !SATX.collideObjects(collider, colliders)
|
||||
const waypointAvailable = !colliders.some((it) => SATX.collideObject(collider, it))
|
||||
if (waypointAvailable) {
|
||||
filteredWaypoints.push(waypoint)
|
||||
}
|
||||
@@ -115,9 +115,10 @@ export default class Pathfind {
|
||||
checked.add(key)
|
||||
checked.add(Pathfind.floatKey4(mergedWaypoints[j], mergedWaypoints[j + 1], mergedWaypoints[i], mergedWaypoints[i + 1]))
|
||||
|
||||
const tunnel = SATX.entityTunnel(mergedWaypoints[i], mergedWaypoints[i + 1], mergedWaypoints[j], mergedWaypoints[j + 1], radius)
|
||||
|
||||
if (!SATX.collideObjects(tunnel, colliders)) {
|
||||
// TODO: optimize tunnelCollider using bounding boxes
|
||||
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]
|
||||
|
||||
+13
-1
@@ -1,10 +1,12 @@
|
||||
import { Vector2 } from 'three'
|
||||
import SAT from 'sat'
|
||||
import SATX from './satx.js'
|
||||
import Entity from './entity.js'
|
||||
|
||||
export default class Projectile {
|
||||
id = crypto.randomUUID()
|
||||
after = null // TODO: hide from reports but keep public
|
||||
bbox = new Float32Array(4)
|
||||
height = 50
|
||||
memory = {} // TODO: hide from reports but keep public
|
||||
onCollide = null // TODO: hide from reports but keep public
|
||||
@@ -36,6 +38,7 @@ export default class Projectile {
|
||||
|
||||
checkCollisions(collider) {
|
||||
(this.game?.entities ?? []).filter((e) => e.id != this.id).forEach((e) => {
|
||||
if (this.game == null) { return }
|
||||
if (e.id == this.owner?.id) { return }
|
||||
|
||||
if (SATX.collideObject(collider, e.collider())) {
|
||||
@@ -54,10 +57,18 @@ export default class Projectile {
|
||||
|
||||
update() {
|
||||
this.#move()
|
||||
this.#calculateBbox()
|
||||
if (this.onCollide != null) { this.checkCollisions(this.collider()) }
|
||||
this.#checkIfArrived()
|
||||
}
|
||||
|
||||
#calculateBbox() {
|
||||
this.bbox[0] = this.position.y + this.radius
|
||||
this.bbox[1] = this.position.x + this.radius
|
||||
this.bbox[2] = this.position.y - this.radius
|
||||
this.bbox[3] = this.position.x - this.radius
|
||||
}
|
||||
|
||||
#checkIfArrived() {
|
||||
if (this.destination == null) { return }
|
||||
if (!this.position.equals(this.destination)) { return }
|
||||
@@ -82,7 +93,8 @@ export default class Projectile {
|
||||
this.position.add(step)
|
||||
}
|
||||
|
||||
const tunnel = SATX.entityTunnel(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius)
|
||||
// TODO: decouple from entity's tunnel collider
|
||||
const tunnel = Entity.tunnelCollider(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius)
|
||||
if (this.onCollide != null) { this.checkCollisions(tunnel) }
|
||||
}
|
||||
}
|
||||
|
||||
+9
-57
@@ -1,8 +1,16 @@
|
||||
import { Vector2 } from 'three'
|
||||
import Entity from './entity.js'
|
||||
import SAT from 'sat'
|
||||
|
||||
export default class SATX {
|
||||
static bboxCheck(bbox1, bbox2) {
|
||||
if (bbox1[0] <= bbox2[2]) { return false }
|
||||
if (bbox1[1] <= bbox2[3]) { return false }
|
||||
if (bbox1[2] >= bbox2[0]) { return false }
|
||||
if (bbox1[3] >= bbox2[1]) { return false }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
static clamp(vectorOrObject, maxX = Infinity, maxY = Infinity, radius = 0) {
|
||||
let modified = null
|
||||
if (vectorOrObject instanceof Vector2) {
|
||||
@@ -41,66 +49,10 @@ export default class SATX {
|
||||
return false
|
||||
}
|
||||
|
||||
static collideObjects(collider, colliders) {
|
||||
return colliders.some((c) => this.collideObject(collider, c))
|
||||
}
|
||||
|
||||
static collideObstacles(collider, obstacles) {
|
||||
return obstacles.filter((obstacle) => obstacle.colliders().some((c) => this.collideObject(collider, c)))
|
||||
}
|
||||
|
||||
static enclosingRegularPolygonRadius(numberOfVertices) {
|
||||
return 1 / Math.cos(Math.PI / numberOfVertices)
|
||||
}
|
||||
|
||||
static entityTunnel(fromX, fromY, toX, toY, radius = 0) {
|
||||
if (radius <= 0) {
|
||||
return this.line(fromX, fromY, toX, toY)
|
||||
}
|
||||
|
||||
const sides = new Float32Array(5)
|
||||
sides[0] = toX - fromX
|
||||
sides[1] = toY - fromY
|
||||
sides[4] = Math.hypot(sides[0], sides[1])
|
||||
sides[2] = (sides[1] / sides[4]) * -radius // optimization: negation and swapping rotates
|
||||
sides[3] = (sides[0] / sides[4]) * radius
|
||||
|
||||
return new SAT.Polygon(new SAT.Vector(fromX - sides[2], fromY - sides[3]), [
|
||||
new SAT.Vector(),
|
||||
new SAT.Vector(sides[0], sides[1]),
|
||||
new SAT.Vector(sides[0] + (2 * sides[2]), sides[1] + (2 * sides[3])),
|
||||
new SAT.Vector(2 * sides[2], 2 * sides[3]),
|
||||
])
|
||||
}
|
||||
|
||||
static fixCollisions(entityPosition, colliders, radius = 0, maxX = Infinity, maxY = Infinity) {
|
||||
if (!this.collideObjects(Entity.collider(entityPosition.x, entityPosition.y, radius), colliders)) {
|
||||
return entityPosition
|
||||
}
|
||||
// console.time('fixCollisions')
|
||||
|
||||
let direction = new Vector2(0, 5)
|
||||
let multiplier = 1
|
||||
const rotationSlices = 16
|
||||
|
||||
for (let limit = 1; limit <= 10000; limit++) {
|
||||
const rads = (limit % rotationSlices) * 2 * Math.PI / rotationSlices
|
||||
const offset = direction.clone().rotateAround(new Vector2(), rads).multiplyScalar(multiplier)
|
||||
const position = SATX.clamp(entityPosition.clone().add(offset), maxX, maxY, radius)
|
||||
if (!this.collideObjects(Entity.collider(position.x, position.y, radius), colliders)) {
|
||||
// console.timeEnd('fixCollisions')
|
||||
return position
|
||||
}
|
||||
|
||||
if (limit % rotationSlices == 0) {
|
||||
multiplier++
|
||||
}
|
||||
}
|
||||
|
||||
// console.timeEnd('fixCollisions')
|
||||
console.error('ERROR: can\'t fix collision')
|
||||
}
|
||||
|
||||
static line(fromX, fromY, toX, toY) {
|
||||
return new SAT.Polygon(new SAT.Vector(fromX, fromY), [new SAT.Vector(), new SAT.Vector(toX - fromX, toY - fromY)])
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,5 @@
|
||||
import { Vector2 } from 'three'
|
||||
import Ability from './ability.js'
|
||||
import Team from './team.js'
|
||||
|
||||
export default class Template {
|
||||
static minion(team, options = {}) {
|
||||
@@ -37,6 +36,7 @@ export default class Template {
|
||||
|
||||
// TODO: minion aggro
|
||||
// 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
|
||||
|
||||
@@ -4,6 +4,7 @@ import SAT from 'sat'
|
||||
export default class Terrain {
|
||||
id = crypto.randomUUID()
|
||||
|
||||
bbox = new Float32Array(4)
|
||||
position = new Vector2()
|
||||
relativeVertices = []
|
||||
|
||||
@@ -21,6 +22,7 @@ export default class Terrain {
|
||||
this.#calculatePosition()
|
||||
this.#calculateRelativeVertices()
|
||||
this.#calculateUnadjustedWaypoints()
|
||||
this.#calculateBbox()
|
||||
}
|
||||
|
||||
get unadjustedWaypoints() { return this.#unadjustedWaypoints }
|
||||
@@ -54,6 +56,31 @@ export default class Terrain {
|
||||
return complexShape
|
||||
}
|
||||
|
||||
#calculateBbox() {
|
||||
const firstVertex = this.vertices.at(0)
|
||||
if (firstVertex != null) {
|
||||
this.bbox[0] = firstVertex.y
|
||||
this.bbox[1] = firstVertex.x
|
||||
this.bbox[2] = firstVertex.y
|
||||
this.bbox[3] = firstVertex.x
|
||||
}
|
||||
|
||||
this.vertices.forEach((v) => {
|
||||
if (v.y > this.bbox[0]) {
|
||||
this.bbox[0] = v.y
|
||||
}
|
||||
if (v.x > this.bbox[1]) {
|
||||
this.bbox[1] = v.x
|
||||
}
|
||||
if (v.y < this.bbox[2]) {
|
||||
this.bbox[2] = v.y
|
||||
}
|
||||
if (v.x < this.bbox[3]) {
|
||||
this.bbox[3] = v.x
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#calculateColliders() {
|
||||
const points = this.#shape().extractPoints(16)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user