feat: game/engine split refactor + skybox
This commit is contained in:
parent
581e5892b0
commit
eeedcb8e48
34 changed files with 610 additions and 218 deletions
169
game/player.ts
Normal file
169
game/player.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { Terrain } from "./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 = 6
|
||||
/** Speed multiplier while a Run key (Shift) is held. Tweak to taste; set high to
|
||||
* blast across the big terrain -- move+collision is substepped, so walls stay
|
||||
* solid even at big multipliers. */
|
||||
const RUN_MULTIPLIER = 2
|
||||
const GRAVITY = 22
|
||||
const JUMP_SPEED = 14
|
||||
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
|
||||
}
|
||||
// Move + collide in small substeps: collision is discrete (move, then push
|
||||
// out), so a single big running step could otherwise skip clean through a
|
||||
// wall. Substepping keeps each advance short enough to always hit it.
|
||||
const steps = moveSubsteps(keys, dt)
|
||||
for (let i = 0; i < steps; i++) {
|
||||
moveHorizontal(player, keys, dt / steps)
|
||||
collide(player, level)
|
||||
}
|
||||
fall(player, dt, level)
|
||||
}
|
||||
|
||||
/** Run-speed factor for the frame: RUN_MULTIPLIER while Shift is held, else 1. */
|
||||
function runFactor(keys: Set<string>): number {
|
||||
return keys.has("ShiftLeft") || keys.has("ShiftRight") ? RUN_MULTIPLIER : 1
|
||||
}
|
||||
|
||||
/** Number of move+collide substeps so each advances at most ~RADIUS, keeping
|
||||
* the player from tunneling walls however fast they run. */
|
||||
function moveSubsteps(keys: Set<string>, dt: number): number {
|
||||
const perFrame = SPEED * runFactor(keys) * dt * Math.SQRT2
|
||||
return Math.max(1, Math.ceil(perFrame / RADIUS))
|
||||
}
|
||||
|
||||
function moveHorizontal(player: Player, keys: Set<string>, dt: number): void {
|
||||
const speed = SPEED * runFactor(keys) * 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue