/**
* Физика тела в воксельном мире (docs/plan.md, открытый вопрос «физика/
* коллизии»): AABB-тело против вокселей, занятый воксель = твёрдый.
* Тело — центр низа (ноги) + полный размер [w,h,d] + скорость.
* Разрешение коллизий ось-раздельное X→Z→Y с зажимом к граням вокселей —
* стандартная схема без застревания на стыках. Сабстепы (смещение за шаг
* < 1 вокселя) защищают от туннелирования сквозь тонкие стены.
* Чистая математика без рендера (урок №3): мир — точечная выборка get().
*/
import type { Vec3 } from '../animation/mat';
/** Мир с точечной выборкой твёрдости (структурно подходит VoxelWorld). */
export interface SolidWorld {
get(x: number, y: number, z: number): number;
}
export interface Body {
/** Центр низа AABB (точка под ногами). Мутабельный кортеж — физика его двигает. */
pos: [number, number, number];
vel: [number, number, number];
/** Полные размеры AABB: [w, h, d]. */
size: [number, number, number];
/** Стоит на твёрдом вокселе (обновляется за шаг). */
grounded: boolean;
}
/** Гравитация, вокс/с². */
export const GRAVITY = 24;
const EPS = 1e-4; // зазор от грани вокселя — не ловить свою же опору
const MAX_SUBSTEP = 0.9; // макс смещение за сабстеп (доля вокселя)
export function makeBody(pos: Vec3, size: readonly [number, number, number]): Body {
return { pos: [...pos], vel: [0, 0, 0], size: [...size], grounded: false };
}
/** Пересекает ли AABB тела хоть один твёрдый воксель (касание грани — нет). */
export function collides(world: SolidWorld, pos: Vec3, size: readonly [number, number, number]): boolean {
const [w, h, d] = size;
const x0 = Math.floor(pos[0] - w / 2), x1 = Math.ceil(pos[0] + w / 2) - 1;
const y0 = Math.floor(pos[1]), y1 = Math.ceil(pos[1] + h) - 1;
const z0 = Math.floor(pos[2] - d / 2), z1 = Math.ceil(pos[2] + d / 2) - 1;
for (let y = y0; y <= y1; y++)
for (let z = z0; z <= z1; z++)
for (let x = x0; x <= x1; x++)
if (world.get(x, y, z) !== 0) return true;
return false;
}
/** Один шаг физики: гравитация + перемещение с разрешением коллизий. */
export function stepBody(world: SolidWorld, body: Body, dt: number): void {
const maxDisp = Math.max(Math.abs(body.vel[0]), Math.abs(body.vel[1]), Math.abs(body.vel[2])) * dt;
const n = Math.max(1, Math.ceil(maxDisp / MAX_SUBSTEP));
const h = dt / n;
for (let s = 0; s < n; s++) substep(world, body, h);
}
/** Подшаг: оси по очереди; зажим к грани вокселя при столкновении. */
function substep(world: SolidWorld, body: Body, h: number): void {
const [w, hh, d] = body.size;
// X
if (body.vel[0] !== 0) {
const nx = body.pos[0] + body.vel[0] * h;
if (collides(world, [nx, body.pos[1], body.pos[2]], body.size)) {
body.pos[0] = body.vel[0] > 0
? Math.floor(nx + w / 2) - w / 2 - EPS
: Math.floor(nx - w / 2) + 1 + w / 2 + EPS;
body.vel[0] = 0;
} else body.pos[0] = nx;
}
// Z — симметрично X
if (body.vel[2] !== 0) {
const nz = body.pos[2] + body.vel[2] * h;
if (collides(world, [body.pos[0], body.pos[1], nz], body.size)) {
body.pos[2] = body.vel[2] > 0
? Math.floor(nz + d / 2) - d / 2 - EPS
: Math.floor(nz - d / 2) + 1 + d / 2 + EPS;
body.vel[2] = 0;
} else body.pos[2] = nz;
}
// Y: гравитация, пол/потолок
body.vel[1] -= GRAVITY * h;
const ny = body.pos[1] + body.vel[1] * h;
if (collides(world, [body.pos[0], ny, body.pos[2]], body.size)) {
if (body.vel[1] < 0) {
body.pos[1] = Math.floor(ny) + 1; // пол: встать ровно на грань
body.grounded = true;
} else {
body.pos[1] = Math.floor(ny + hh) - hh - EPS; // потолок
}
body.vel[1] = 0;
} else {
body.pos[1] = ny;
body.grounded = false;
}
}