import { Vector2 } from 'three' import SAT from 'sat' import SATX from './satx.js' import Pathfind from './pathfind.js' import Terrain from './terrain.js' export default class Entity { id = crypto.randomUUID() speed = 400 radius = 0 #position = new Vector2() #dest = null #game = null #path = [] static collider(x, y, radius) { return new SAT.Circle(new SAT.Vector(x, y), radius) } constructor(...options) { Object.entries(options).forEach((value, key) => this[key] = value) } get game() { return this.#game } get position() { return this.#position } get x() { return this.position.x } get y() { return this.position.y } set game(value) { this.#game = value } set x(value) { this.position.x = value } set y(value) { this.position.y = value } get collider() { return new SAT.Circle(new SAT.Vector(this.x, this.y), this.radius) } get colliders() { return [this.collider] } 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), ]) } 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) } isColliding(...colliders) { return SATX.collideObjects(this.collider, colliders) } moveAction(x, y) { this.#dest = new Vector2(x, y) } state() { return { ...this, position: { x: this.x, y: this.y, }, } } teleport(x, y) { this.position.set(x, y) } async takeStep(distanceTraveled = 0) { const speed = (this.speed / (this.game?.tickBudget ?? 1000)) - distanceTraveled const collidables = this.collidables() if (this.#dest != null) { const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, collidables, this.radius), this.game?.width, this.game?.height, this.radius) if (this.#path.length < 1 || !this.#path.at(-1).equals(fixedDest)) { console.time('pathfinding') console.time('waypoints') const start = SATX.vectorToFloat32Array(this.position) const goal = SATX.vectorToFloat32Array(fixedDest) const nonUniqueWaypoints = this.waypoints().map((w) => SATX.vectorToFloat32Array(w)).concat([start, goal]) const waypoints = Pathfind.uniqueWaypoints(nonUniqueWaypoints) console.timeEnd('waypoints') console.time('graph') const graph = Pathfind.buildGraph(waypoints, collidables, this.radius) // console.log(Pathfind.formatFloat32Array(graph, 5, true)) // const tunnels = [] // for (let i = 0; i < graph.length; i += 5) { // tunnels.push(SATX.entityTunnel(graph[i], graph[i + 1], graph[i + 2], graph[i + 3], 1)) // } // tunnels.map((t) => SATX.satPolygonToVectors(t)).forEach((t) => this.#game.add_terrain(new Terrain(t))) // this.#dest = null console.timeEnd('graph') console.time('path') this.#path = Pathfind.shortestPath(graph, start, goal).map((waypoint) => new Vector2(waypoint[0], waypoint[1])) console.log(this.#path) console.timeEnd('path') console.timeEnd('pathfinding') } if (this.#path.length > 0) { const destination = this.#path.at(0) const distance = this.position.clone().sub(destination).length() const direction = destination.clone().sub(this.position).normalize() const stepTaken = this.position.clone().add(direction.multiplyScalar(speed)) const position = distance <= speed ? destination : stepTaken const collider = Entity.collider(position.x, position.y, this.radius) const isColliding = SATX.collideObjects(collider, this.collidables()) if (!isColliding) { this.position.copy(position) } if (this.position.equals(destination)) { this.#path = this.#path.slice(1) if (this.#path.length > 0) { await this.takeStep(distance) } else { this.#dest = null } } } } } async update() { await Promise.allSettled([ this.takeStep(), ]) } waypoints() { const entityColliders = (this.game?.entities ?? []).filter((e) => e.id != this.id) const terrainColliders = (this.game?.terrains ?? []) const unadjustedWaypoints = entityColliders.concat(terrainColliders).map((e) => e.unadjustedWaypoints).flat() return unadjustedWaypoints.map(([waypoint, direction]) => waypoint.clone().add(direction.clone().multiplyScalar(this.radius))) ?? [] } }