117 lines
3.1 KiB
TypeScript
117 lines
3.1 KiB
TypeScript
|
|
import type { Vec3 } from "../math/Vec3"
|
||
|
|
|
||
|
|
/** A 2.5D box: horizontal footprint plus top height. */
|
||
|
|
export type BoxCollider = {
|
||
|
|
shape: "box"
|
||
|
|
minX: number
|
||
|
|
maxX: number
|
||
|
|
minZ: number
|
||
|
|
maxZ: number
|
||
|
|
top: number
|
||
|
|
standable: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A 2.5D circle: horizontal footprint plus top height. */
|
||
|
|
export type CircleCollider = {
|
||
|
|
shape: "circle"
|
||
|
|
x: number
|
||
|
|
z: number
|
||
|
|
radius: number
|
||
|
|
top: number
|
||
|
|
standable: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Finite engine collision capabilities, not game-content identity. */
|
||
|
|
export type Collider = BoxCollider | CircleCollider
|
||
|
|
|
||
|
|
export namespace Collider {
|
||
|
|
export function centerX(collider: Collider): number {
|
||
|
|
return collider.shape === "circle"
|
||
|
|
? collider.x
|
||
|
|
: (collider.minX + collider.maxX) * 0.5
|
||
|
|
}
|
||
|
|
|
||
|
|
export function centerZ(collider: Collider): number {
|
||
|
|
return collider.shape === "circle"
|
||
|
|
? collider.z
|
||
|
|
: (collider.minZ + collider.maxZ) * 0.5
|
||
|
|
}
|
||
|
|
|
||
|
|
export function contains(collider: Collider, x: number, z: number): boolean {
|
||
|
|
if (collider.shape === "circle") {
|
||
|
|
const dx = x - collider.x
|
||
|
|
const dz = z - collider.z
|
||
|
|
return dx * dx + dz * dz <= collider.radius * collider.radius
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
x >= collider.minX &&
|
||
|
|
x <= collider.maxX &&
|
||
|
|
z >= collider.minZ &&
|
||
|
|
z <= collider.maxZ
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Push a horizontal player circle out of one collider. */
|
||
|
|
export function pushOut(
|
||
|
|
collider: Collider,
|
||
|
|
position: Vec3,
|
||
|
|
radius: number,
|
||
|
|
): void {
|
||
|
|
if (collider.shape === "circle") {
|
||
|
|
pushFromCircle(position, radius, collider)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
pushFromBox(position, radius, collider)
|
||
|
|
}
|
||
|
|
|
||
|
|
function pushFromBox(position: Vec3, radius: number, box: BoxCollider): void {
|
||
|
|
const cx = Math.max(box.minX, Math.min(box.maxX, position.x))
|
||
|
|
const cz = Math.max(box.minZ, Math.min(box.maxZ, position.z))
|
||
|
|
const dx = position.x - cx
|
||
|
|
const dz = position.z - cz
|
||
|
|
const distanceSquared = dx * dx + dz * dz
|
||
|
|
if (distanceSquared >= radius * radius) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if (distanceSquared > 1e-6) {
|
||
|
|
const distance = Math.sqrt(distanceSquared)
|
||
|
|
const push = (radius - distance) / distance
|
||
|
|
position.x += dx * push
|
||
|
|
position.z += dz * push
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const left = position.x - box.minX
|
||
|
|
const right = box.maxX - position.x
|
||
|
|
const near = position.z - box.minZ
|
||
|
|
const far = box.maxZ - position.z
|
||
|
|
const nearest = Math.min(left, right, near, far)
|
||
|
|
if (nearest === left) {
|
||
|
|
position.x = box.minX - radius
|
||
|
|
} else if (nearest === right) {
|
||
|
|
position.x = box.maxX + radius
|
||
|
|
} else if (nearest === near) {
|
||
|
|
position.z = box.minZ - radius
|
||
|
|
} else {
|
||
|
|
position.z = box.maxZ + radius
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function pushFromCircle(
|
||
|
|
position: Vec3,
|
||
|
|
radius: number,
|
||
|
|
circle: CircleCollider,
|
||
|
|
): void {
|
||
|
|
const dx = position.x - circle.x
|
||
|
|
const dz = position.z - circle.z
|
||
|
|
const reach = radius + circle.radius
|
||
|
|
const distanceSquared = dx * dx + dz * dz
|
||
|
|
if (distanceSquared >= reach * reach || distanceSquared < 1e-6) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
const distance = Math.sqrt(distanceSquared)
|
||
|
|
const push = (reach - distance) / distance
|
||
|
|
position.x += dx * push
|
||
|
|
position.z += dz * push
|
||
|
|
}
|
||
|
|
}
|