// 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; } }