97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
// 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;
|
|
}
|