This repository has been archived on 2026-05-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
instructions-clear/src/projectile.js
T

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))
}
}
}
}