import { Vector2 } from 'three' import SAT from 'sat' import SATX from './satx.js' import Pathfind from './pathfind.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 if (this.#dest != null) { const fixedDest = SATX.clamp(SATX.fixCollisions(this.#dest, this.collidables(), this.radius), this.game?.width, this.game?.height, this.radius) if (this.#path.length < 1 || !this.#path.at(-1).equals(this.#dest)) { console.time('pathfinding') console.time('waypoints') const waypoints = (this.game?.unadjustedWaypoints.map(([unadjusted, direction]) => unadjusted.clone().add(direction.clone().multiplyScalar(this.radius))) ?? []).concat([this.position, fixedDest]) console.timeEnd('waypoints') console.time('graph') const graph = Pathfind.buildGraph(waypoints, this.collidables(), this.radius) console.timeEnd('graph') console.time('path') this.#path = Pathfind.shortestPath(graph, this.position, fixedDest) 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(), ]) } }