147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
import { Terrain } from "../engine/scene/Terrain"
|
|
import type { Vec3 } from "../engine/math/Vec3"
|
|
import type { Aabb, Level } from "./level"
|
|
|
|
/** The player as a vertical cylinder. `position` is at the feet; the camera
|
|
* eye sits EYE_HEIGHT above it. */
|
|
export type Player = {
|
|
position: Vec3
|
|
yaw: number
|
|
pitch: number
|
|
velocityY: number
|
|
onGround: boolean
|
|
}
|
|
|
|
export const EYE_HEIGHT = 1.6
|
|
const RADIUS = 0.35
|
|
const SPEED = 4
|
|
const GRAVITY = 22
|
|
const JUMP_SPEED = 8
|
|
const NPC_RADIUS = 0.5
|
|
|
|
export namespace Player {
|
|
/** Advance the player one frame: jump, horizontal move + collision, gravity. */
|
|
export function update(player: Player, keys: Set<string>, dt: number, level: Level): void {
|
|
if (keys.has("Space") && player.onGround) {
|
|
player.velocityY = JUMP_SPEED
|
|
player.onGround = false
|
|
}
|
|
moveHorizontal(player, keys, dt)
|
|
collide(player, level)
|
|
fall(player, dt, level)
|
|
}
|
|
|
|
function moveHorizontal(player: Player, keys: Set<string>, dt: number): void {
|
|
const speed = SPEED * dt
|
|
const fx = Math.sin(player.yaw)
|
|
const fz = -Math.cos(player.yaw)
|
|
const rx = Math.cos(player.yaw)
|
|
const rz = Math.sin(player.yaw)
|
|
const p = player.position
|
|
if (keys.has("KeyW")) {
|
|
p.x += fx * speed
|
|
p.z += fz * speed
|
|
}
|
|
if (keys.has("KeyS")) {
|
|
p.x -= fx * speed
|
|
p.z -= fz * speed
|
|
}
|
|
if (keys.has("KeyD")) {
|
|
p.x += rx * speed
|
|
p.z += rz * speed
|
|
}
|
|
if (keys.has("KeyA")) {
|
|
p.x -= rx * speed
|
|
p.z -= rz * speed
|
|
}
|
|
}
|
|
|
|
/** Push the player's circle out of any solid it overlaps: level colliders it
|
|
* is not standing above, and the NPC. This is what makes walls and the NPC
|
|
* impassable while still letting you stand on the crate. */
|
|
function collide(player: Player, level: Level): void {
|
|
for (const aabb of level.colliders) {
|
|
if (player.position.y < aabb.top - 0.01) {
|
|
pushFromAabb(player.position, aabb)
|
|
}
|
|
}
|
|
pushFromCircle(player.position, level.npcPosition.x, level.npcPosition.z, NPC_RADIUS)
|
|
}
|
|
|
|
/** Apply gravity and land on the highest ground under the player. */
|
|
function fall(player: Player, dt: number, level: Level): void {
|
|
player.velocityY -= GRAVITY * dt
|
|
player.position.y += player.velocityY * dt
|
|
const ground = groundHeight(player.position, level)
|
|
if (player.position.y <= ground) {
|
|
player.position.y = ground
|
|
player.velocityY = 0
|
|
player.onGround = true
|
|
} else {
|
|
player.onGround = false
|
|
}
|
|
}
|
|
|
|
function groundHeight(position: Vec3, level: Level): number {
|
|
let ground = Terrain.height(level.terrain, position.x, position.z)
|
|
for (const aabb of level.colliders) {
|
|
if (
|
|
aabb.standable &&
|
|
position.x >= aabb.minX &&
|
|
position.x <= aabb.maxX &&
|
|
position.z >= aabb.minZ &&
|
|
position.z <= aabb.maxZ
|
|
) {
|
|
ground = Math.max(ground, aabb.top)
|
|
}
|
|
}
|
|
return ground
|
|
}
|
|
|
|
function pushFromAabb(position: Vec3, aabb: Aabb): void {
|
|
const cx = Math.max(aabb.minX, Math.min(aabb.maxX, position.x))
|
|
const cz = Math.max(aabb.minZ, Math.min(aabb.maxZ, position.z))
|
|
const dx = position.x - cx
|
|
const dz = position.z - cz
|
|
const d2 = dx * dx + dz * dz
|
|
if (d2 >= RADIUS * RADIUS) {
|
|
return
|
|
}
|
|
if (d2 > 1e-6) {
|
|
const d = Math.sqrt(d2)
|
|
const push = (RADIUS - d) / d
|
|
position.x += dx * push
|
|
position.z += dz * push
|
|
return
|
|
}
|
|
// Center is inside the box: eject through the nearest face.
|
|
const left = position.x - aabb.minX
|
|
const rightSide = aabb.maxX - position.x
|
|
const near = position.z - aabb.minZ
|
|
const far = aabb.maxZ - position.z
|
|
const m = Math.min(left, rightSide, near, far)
|
|
if (m === left) {
|
|
position.x = aabb.minX - RADIUS
|
|
} else if (m === rightSide) {
|
|
position.x = aabb.maxX + RADIUS
|
|
} else if (m === near) {
|
|
position.z = aabb.minZ - RADIUS
|
|
} else {
|
|
position.z = aabb.maxZ + RADIUS
|
|
}
|
|
}
|
|
|
|
function pushFromCircle(position: Vec3, cx: number, cz: number, otherRadius: number): void {
|
|
const dx = position.x - cx
|
|
const dz = position.z - cz
|
|
const reach = RADIUS + otherRadius
|
|
const d2 = dx * dx + dz * dz
|
|
if (d2 >= reach * reach || d2 < 1e-6) {
|
|
return
|
|
}
|
|
const d = Math.sqrt(d2)
|
|
const push = (reach - d) / d
|
|
position.x += dx * push
|
|
position.z += dz * push
|
|
}
|
|
}
|