// collision against blocks, shared by the client's entities and the server's, so both move things the same way import { TICK_DELTA } from "./constants.ts"; // how far inside a block face still counts as touching it, so boxes resting exactly on a face don't snag const EPSILON = 1e-7; // the two axes that aren't the one being moved along const OTHER_AXES = [[1, 2], [0, 2], [0, 1]] as const; // something with a box that moves. position is the middle of its feet, velocity is in blocks per second export interface Body { x: number; y: number; z: number; vx: number; vy: number; vz: number; // width is used for both x and z width: number; height: number; } // which way the body hit something on each axis: 1 or -1, 0 for nothing. 1 on y means it landed on something export interface Collisions { x: number; y: number; z: number; } // falls and moves a body by its velocity for one tick. like minecraft it moves along y, then x, then z, each // time only as far as it can before touching a block, and stops its velocity on the axes it hit something. // blocks it's already inside don't stop it, so it can get out of them export function move_body(body: Body, gravity: number, is_solid: (x: number, y: number, z: number) => boolean) { // the average of this tick's start and end speed, so the arc is the same at any tick rate const wanted_y = (body.vy + gravity * TICK_DELTA / 2) * TICK_DELTA; body.vy += gravity * TICK_DELTA; const wanted = [body.vx * TICK_DELTA, wanted_y, body.vz * TICK_DELTA]; const half = body.width / 2; const min = [body.x - half, body.y, body.z - half]; const max = [body.x + half, body.y + body.height, body.z + half]; const moved = [0, 0, 0]; for (const axis of [1, 0, 2]) { const distance = clip(min, max, axis, wanted[axis], is_solid); min[axis] += distance; max[axis] += distance; moved[axis] = distance; } const hit = (axis: number) => moved[axis] !== wanted[axis] ? -Math.sign(wanted[axis]) : 0; const collisions: Collisions = { x: hit(0), y: hit(1), z: hit(2) }; if (collisions.x !== 0) body.vx = 0; if (collisions.y !== 0) body.vy = 0; if (collisions.z !== 0) body.vz = 0; body.x += moved[0]; body.y += moved[1]; body.z += moved[2]; return collisions; } // how far the box can go along an axis before it runs into a block function clip( min: number[], max: number[], axis: number, distance: number, is_solid: (x: number, y: number, z: number) => boolean, ) { if (distance === 0) { return 0; } const [a, b] = OTHER_AXES[axis]; const from = Math.floor(Math.min(min[axis], min[axis] + distance)); const to = Math.floor(Math.max(max[axis], max[axis] + distance)); const position = [0, 0, 0]; for (let i = from; i <= to; i++) { for (let j = Math.floor(min[a] + EPSILON); j <= Math.floor(max[a] - EPSILON); j++) { for (let k = Math.floor(min[b] + EPSILON); k <= Math.floor(max[b] - EPSILON); k++) { position[axis] = i; position[a] = j; position[b] = k; if (!is_solid(position[0], position[1], position[2])) { continue; } if (distance > 0 && i >= max[axis] - EPSILON) { distance = Math.min(distance, i - max[axis]); } else if (distance < 0 && i + 1 <= min[axis] + EPSILON) { distance = Math.max(distance, i + 1 - min[axis]); } } } } return distance; }