170 lines
5.9 KiB
JavaScript
170 lines
5.9 KiB
JavaScript
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))
|
|
}
|
|
}
|
|
}
|
|
}
|