Lanes A (engine), B (player), C (worldgen), D (machines/quest), E (audio/fx/ui) as landed, each with HANDOFF.md + update docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
4.2 KiB
TypeScript
112 lines
4.2 KiB
TypeScript
// Lane B — AABB-vs-voxel collision. Pure functions that mutate the passed
|
||
// position/velocity tuples in place (zero per-call allocation on the hot path).
|
||
//
|
||
// The player is an axis-aligned box centered horizontally on `pos` (feet
|
||
// center), spanning [pos.x-HALF_W, pos.x+HALF_W] × [pos.y, pos.y+HEIGHT] ×
|
||
// [pos.z-HALF_W, pos.z+HALF_W]. Callers substep so |disp| stays small; each
|
||
// resolveAxis therefore only has to clamp against the single voxel layer the
|
||
// box newly entered along that axis.
|
||
|
||
import type { IVoxelWorld } from '../core/types';
|
||
import { PLAYER } from '../core/constants';
|
||
|
||
export const HALF_W = PLAYER.width / 2;
|
||
export const HEIGHT = PLAYER.height;
|
||
export const STEP_HEIGHT = 1.0; // auto-step ledges up to 1 voxel tall
|
||
|
||
const EPS = 1e-4;
|
||
|
||
// Scratch velocity for the step-assist vertical probes (single-threaded JS).
|
||
const _dummy: [number, number, number] = [0, 0, 0];
|
||
|
||
/**
|
||
* Move the box along one axis by `disp`, then clamp it out of any solid voxel
|
||
* it entered. Zeros vel[axis] on contact. Returns true if it hit something.
|
||
* axis: 0 = X, 1 = Y, 2 = Z.
|
||
*/
|
||
export function resolveAxis(
|
||
world: IVoxelWorld,
|
||
pos: number[],
|
||
vel: number[],
|
||
axis: 0 | 1 | 2,
|
||
disp: number,
|
||
): boolean {
|
||
pos[axis] += disp;
|
||
if (disp === 0) return false;
|
||
|
||
const minX = pos[0] - HALF_W, maxX = pos[0] + HALF_W;
|
||
const minY = pos[1], maxY = pos[1] + HEIGHT;
|
||
const minZ = pos[2] - HALF_W, maxZ = pos[2] + HALF_W;
|
||
|
||
const cx0 = Math.floor(minX + EPS), cx1 = Math.floor(maxX - EPS);
|
||
const cy0 = Math.floor(minY + EPS), cy1 = Math.floor(maxY - EPS);
|
||
const cz0 = Math.floor(minZ + EPS), cz1 = Math.floor(maxZ - EPS);
|
||
|
||
let hit = false;
|
||
|
||
if (axis === 0) {
|
||
const layer = disp > 0 ? cx1 : cx0;
|
||
for (let y = cy0; y <= cy1 && !hit; y++)
|
||
for (let z = cz0; z <= cz1; z++)
|
||
if (world.isSolid(layer, y, z)) { hit = true; break; }
|
||
if (hit) { pos[0] = disp > 0 ? layer - HALF_W : layer + 1 + HALF_W; vel[0] = 0; }
|
||
} else if (axis === 1) {
|
||
const layer = disp > 0 ? cy1 : cy0;
|
||
for (let x = cx0; x <= cx1 && !hit; x++)
|
||
for (let z = cz0; z <= cz1; z++)
|
||
if (world.isSolid(x, layer, z)) { hit = true; break; }
|
||
if (hit) { pos[1] = disp > 0 ? layer - HEIGHT : layer + 1; vel[1] = 0; }
|
||
} else {
|
||
const layer = disp > 0 ? cz1 : cz0;
|
||
for (let x = cx0; x <= cx1 && !hit; x++)
|
||
for (let y = cy0; y <= cy1; y++)
|
||
if (world.isSolid(x, y, layer)) { hit = true; break; }
|
||
if (hit) { pos[2] = disp > 0 ? layer - HALF_W : layer + 1 + HALF_W; vel[2] = 0; }
|
||
}
|
||
|
||
return hit;
|
||
}
|
||
|
||
/**
|
||
* Horizontal move (X then Z, Minecraft order) with auto-step. If the flat move
|
||
* hits a wall and `canStep` is set, retry the move raised by STEP_HEIGHT and
|
||
* settle back down; keep whichever result travelled further horizontally. A
|
||
* 2-voxel wall naturally fails the retry (still blocked one voxel up) and is
|
||
* left un-stepped. The caller's subsequent Y resolution re-grounds the player.
|
||
*/
|
||
export function moveHorizontalWithStep(
|
||
world: IVoxelWorld,
|
||
pos: number[],
|
||
vel: number[],
|
||
dx: number,
|
||
dz: number,
|
||
canStep: boolean,
|
||
): void {
|
||
const sx = pos[0], sz = pos[2], svx = vel[0], svz = vel[2];
|
||
|
||
const hitX = resolveAxis(world, pos, vel, 0, dx);
|
||
const hitZ = resolveAxis(world, pos, vel, 2, dz);
|
||
if (!canStep || (!hitX && !hitZ)) return;
|
||
|
||
// Remember the flat (non-stepped) outcome.
|
||
const nx = pos[0], nz = pos[2], nvx = vel[0], nvz = vel[2];
|
||
const flatDist = (nx - sx) * (nx - sx) + (nz - sz) * (nz - sz);
|
||
|
||
// Retry raised by one voxel.
|
||
pos[0] = sx; pos[2] = sz; vel[0] = svx; vel[2] = svz;
|
||
const startY = pos[1];
|
||
_dummy[0] = 0; _dummy[1] = 0; _dummy[2] = 0;
|
||
resolveAxis(world, pos, _dummy, 1, STEP_HEIGHT); // clamps if there's a ceiling
|
||
const climbed = pos[1] - startY;
|
||
resolveAxis(world, pos, vel, 0, dx);
|
||
resolveAxis(world, pos, vel, 2, dz);
|
||
resolveAxis(world, pos, _dummy, 1, -climbed); // settle onto the ledge
|
||
if (pos[1] < startY) pos[1] = startY;
|
||
|
||
const stepDist = (pos[0] - sx) * (pos[0] - sx) + (pos[2] - sz) * (pos[2] - sz);
|
||
if (stepDist <= flatDist + 1e-6) {
|
||
// Stepping gained nothing (e.g. a 2-voxel wall) — revert to the flat move.
|
||
pos[0] = nx; pos[1] = startY; pos[2] = nz; vel[0] = nvx; vel[2] = nvz;
|
||
}
|
||
}
|