import { Vector2 } from 'three' import Entity from './entity.js' import SAT from 'sat' import SATX from './satx.js' export default class Projectile { id = `projectile-${Projectile.nextId()}` static nextId() { return this.#nextUniqueId++ } static #nextUniqueId = 0 height = 50 owner = null position = new Vector2() radius = 0 speed = 1000 team = null visibleThroughTerrain = true visionRange = 0 visualRadius = null #after = null #bbox = new Float32Array(4) #dest = null #entitiesInVision = [] #game = null #homingTarget = null #logic = null #onCollide = null #projectilesInVision = [] get after() { return this.#after } get bbox() { return this.#bbox } get entitiesInVision() { return this.#entitiesInVision } get game() { return this.#game } get homingTarget() { return this.#homingTarget } get logic() { return this.#logic } get onCollide() { return this.#onCollide } get projectilesInVision() { return this.#projectilesInVision } set after(value) { this.#after = value } set bbox(value) { this.#bbox = value } set destination(value) { this.#dest = value } set game(value) { this.#game = value } set homingTarget(value) { this.#homingTarget = value } set logic(value) { this.#logic = value } set onCollide(value) { this.#onCollide = value } get destination() { return this.#dest ?? this.#homingTarget?.position } constructor(options = {}) { Object.entries(options).forEach(([key, value]) => this[key] = value) if (this.visualRadius == null) { this.visualRadius = this.radius } } collider() { return new SAT.Circle(new SAT.Vector(this.position.x, this.position.y), this.radius) } despawn() { this.game?.despawn(this) } isInLineOfVision(destination) { const bbox = Entity.tunnelBbox(this.position.x, this.position.y, destination.x, destination.y, 0) const terrains = this.game?.terrains ?? [] 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, 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() const collider = Entity.tunnelCollider(this.position.x, this.position.y, destination.x, destination.y, 0) return !colliders.some((it) => SATX.collideObject(collider, it)) } setPosition(vector) { this.position.copy(vector) this.#calculateBbox() } update() { this.#calculateVision() this.#move() this.#checkStationaryCollisions() this.#checkIfArrived() if (this.#logic != null) { this.#logic(this) } } #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 } #calculateVision() { const entities = this.game?.entities ?? [] const projectiles = this.game?.projectiles ?? [] const entitiesInVisionRange = entities.filter((it) => it.id != this.id && it.distanceTo(this.position) <= this.visionRange + it.radius) const entitiesInLineOfSight = entitiesInVisionRange.filter((it) => this.isInLineOfVision(it.position)) const projectilesInVisionRange = projectiles.filter((it) => this.position.distanceTo(it.position) <= this.visionRange + it.radius) const projectilesInLineOfSight = projectilesInVisionRange.filter((it) => it.visibleThroughTerrain || this.isInLineOfVision(it.position)) this.#entitiesInVision = entitiesInLineOfSight.concat([this]).map((it) => it.id) this.#projectilesInVision = projectilesInLineOfSight.map((it) => it.id) } #checkIfArrived() { if (this.destination == null) { return } if (!this.position.equals(this.destination)) { return } if (this.#after != null) { this.#after(this, this.#homingTarget) } if (this.destination == null) { return } if (!this.position.equals(this.destination)) { return } this.despawn() } #checkStationaryCollisions() { if (this.#onCollide == null) { return } const bbox = this.bbox const entitiesAndTerrains = this.game?.entities ?? [] const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && SATX.bboxCheck(bbox, it.bbox)) if (bboxCheckedObstacles.length > 0) { const collider = this.collider() const colliding = bboxCheckedObstacles.filter((it) => it.colliders().some((c) => SATX.collideObject(collider, c))) colliding.forEach((it) => this.#onCollide(this, it)) } } #move() { if (this.destination == null) { return } const speed = (this.speed / (this.game?.tickRate ?? 1)) const prevPos = this.position.clone() if (this.position.distanceTo(this.destination) < speed) { this.setPosition(this.destination) } else { const step = this.destination.clone().sub(this.position).normalize().multiplyScalar(speed) this.position.add(step) } if (this.#onCollide != null) { const bbox = Entity.tunnelBbox(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius) const entitiesAndTerrains = this.game?.entities ?? [] const bboxCheckedObstacles = entitiesAndTerrains.filter((it) => !it.dead && SATX.bboxCheck(bbox, it.bbox)) if (bboxCheckedObstacles.length > 0) { const collider = Entity.tunnelCollider(prevPos.x, prevPos.y, this.position.x, this.position.y, this.radius) const colliding = bboxCheckedObstacles.filter((it) => it.colliders().some((c) => SATX.collideObject(collider, c))) colliding.sort((a, b) => a.distanceTo(prevPos) > b.distanceTo(prevPos)).forEach((it) => this.#onCollide(this, it)) } } } }