grechka-game/srcv3/physics/collision3d.js

210 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==================== ФИЗИКА 3D — GrechkaCraft v3 ====================
// Осевое разрешение столкновений по X → Z → Y, как в Minecraft.
// Автошаг на 1 блок, прыжок, лестницы, плавание, лодка.
import { TILE } from '../core/constants.js';
import { BLOCKS } from '../data/blocks.js';
import { state } from '../core/state.js';
import { getBlock, isSolid } from '../world/world-storage3d.js';
const EPS = 0.001;
// AABB сущности. ВАЖНО: в игровом коде принято, что e.y — ЭТО ВЕРХ тела,
// а низ тела = e.y + e.h (ось Y направлена вниз, гравитация тянет в +y).
// Раньше здесь стояло y0: e.y, y1: e.y + e.h — то есть НИЗ=e.y. Это
// противоречие ломало всё: игрок оказывался внутри блока, grounded был
// всегда false, а прыжок гасился столкновением на первом шаге.
// Теперь: y0 = e.y + e.h (низ), y1 = e.y (верх) — согласовано с игрой.
function boxOf(e) {
return {
x0: e.x, x1: e.x + e.w,
y0: e.y + e.h, y1: e.y,
z0: e.z, z1: e.z + e.d,
};
}
function solidAt(gx, gy, gz) {
const b = getBlock(gx, gy, gz);
if (!b) return false;
const def = BLOCKS[b.t];
if (!def) return false;
if (def.fluid) return false;
if (def.decor || def.climbable) return false;
return !!def.solid;
}
function climbableAt(gx, gy, gz) {
const b = getBlock(gx, gy, gz);
return !!(b && BLOCKS[b.t] && BLOCKS[b.t].climbable);
}
function fluidAt(gx, gy, gz) {
const b = getBlock(gx, gy, gz);
return !!(b && BLOCKS[b.t] && BLOCKS[b.t].fluid);
}
// Пересекает ли коробка сущности твёрдый блок
// Пол под ногами. ТЕЛО ТОНКОЕ: при h = 34 и TILE = 40, как только y0
// пересекает границу тайла, floor(y0/TILE) указывает на пустой тайл над
// опорой и коллизия перестаёт видеть пол. Замер: игрок оседал на 0.85
// тайла внутрь блока. Поэтому дополнительно проверяем тайл строго ПОД
// нижней границей тела — на расстоянии EPS, чтобы поймать опору.
function groundSolidUnder(bx) {
const gy = Math.floor((bx.y0 + 0.5) / TILE);
const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE);
const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE);
for (let gx = x0; gx <= x1; gx++) {
for (let gz = z0; gz <= z1; gz++) {
if (solidAt(gx, gy, gz)) return true;
}
}
return false;
}
function hitsSolid(bx) {
if (groundSolidUnder(bx)) return true;
const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE);
// По Y берём тайлы СТРОГО пересекающиеся с телом. Низ тела (y0) может
// лежать ровно на границе тайла: когда игрок стоит на опоре, y0 кратно
// TILE и floor(y0/TILE) указывает на САМ блок опоры, а не на тайл под
// ним. Из-за этого игрок «вставал» на 0.85 тайла внутрь блока: замер
// давал feet_gy = 7.85 при опоре на тайле 7 (snow).
// Поэтому нижнюю границу округляем ВНИЗ, а верхнюю — вверх.
// Тайлы по Y: от верхнего до нижнего. Нижняя граница тела bx.y0 округляется
// ВНИЗ (bx.y0 - EPS): когда ноги стоят ровно на границе тайла, floor даёт
// сам блок опоры, а не тайл над ним — из-за этого grounded срабатывал
// слишком рано, а игрок оседал на 0.85 тайла ВНУТРЬ блока.
const y0 = Math.floor((bx.y0 - EPS) / TILE), y1 = Math.floor((bx.y1 + EPS) / TILE);
const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE);
for (let gx = x0; gx <= x1; gx++) {
for (let gy = y0; gy <= y1; gy++) {
for (let gz = z0; gz <= z1; gz++) {
if (solidAt(gx, gy, gz)) return true;
}
}
}
return false;
}
// Стоит ли сущность на чём-то (для grounded)
function checkGrounded(e) {
const bx = boxOf(e);
// Опора = тайл под ногами = под НИЗОМ тела (bx.y0). Раньше тут стоял
// bx.y1, который с новой boxOf — это ВЕРХ тела, то есть блок над головой.
// Опора — тайл СТРОГО ПОД ногами. Когда низ тела стоит ровно на границе
// (y0 кратно TILE), floor(y0/TILE) указывает на сам блок опоры, а не на
// тайл под ним, и grounded считается в тот момент, когда игрок ещё не
// встал. Поэтому округляем вниз: верхняя граница опоры = bx.y0.
const gy = Math.floor((bx.y0 - EPS) / TILE);
const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE);
const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE);
for (let gx = x0; gx <= x1; gx++) {
for (let gz = z0; gz <= z1; gz++) {
if (solidAt(gx, gy, gz)) return true;
}
}
return false;
}
function onLadder(e) {
const cx = Math.floor((e.x + e.w / 2) / TILE);
const cy = Math.floor((e.y + e.h / 2) / TILE);
const cz = Math.floor((e.z + e.d / 2) / TILE);
return climbableAt(cx, cy, cz);
}
function inFluid(e) {
const cx = Math.floor((e.x + e.w / 2) / TILE);
const cy = Math.floor((e.y + e.h * 0.4) / TILE);
const cz = Math.floor((e.z + e.d / 2) / TILE);
return fluidAt(cx, cy, cz);
}
// Разрешение одной оси. delta — смещение за кадр.
function moveAxis(e, axis, delta) {
if (delta === 0) return false;
const bx = boxOf(e);
if (axis === 'x') { bx.x0 += delta; bx.x1 += delta; }
else if (axis === 'z') { bx.z0 += delta; bx.z1 += delta; }
else { bx.y0 += delta; bx.y1 += delta; }
if (!hitsSolid(bx)) {
if (axis === 'x') e.x += delta;
else if (axis === 'z') e.z += delta;
else e.y += delta;
return false;
}
const dir = Math.sign(delta);
// Автошаг вверх на 1 блок
if ((axis === 'x' || axis === 'z') && e.grounded) {
const savedX = e.x, savedY = e.y, savedZ = e.z;
if (axis === 'x') e.x += delta; else e.z += delta;
e.y -= TILE;
if (!hitsSolid(boxOf(e))) {
return true; // шаг удался, продолжим движение
}
e.x = savedX; e.y = savedY; e.z = savedZ;
}
// Откат: двигаемся по 0.5 px, пока не упрёмся в твёрдый блок.
// ТЕЛО ТОНКОЕ (h = 34 < TILE = 40), поэтому нельзя полагаться только на
// floor() текущего положения: как только y0 пересекает границу тайла,
// тайл под ногами становится пустым и коллизия перестаёт видеть пол.
// Замер: игрок оседал на feet_gy = 8.85 при опоре на тайле 7.
// Поэтому шаг делаем ОТ ПОТОЛКА вниз и держим последнюю валидную позицию.
const stepPx = dir * 0.5;
for (let i = 0; i < TILE * 4; i++) {
const test = boxOf(e);
if (axis === 'x') { test.x0 += stepPx; test.x1 += stepPx; }
else if (axis === 'z') { test.z0 += stepPx; test.z1 += stepPx; }
else { test.y0 += stepPx; test.y1 += stepPx; }
if (hitsSolid(test)) break;
if (axis === 'x') e.x += stepPx;
else if (axis === 'z') e.z += stepPx;
else e.y += stepPx;
}
if (axis === 'x') e.vx = 0;
else if (axis === 'z') e.vz = 0;
else e.vy = 0;
return true;
}
export function resolveX(e, dt) { return moveAxis(e, 'x', e.vx * dt); }
export function resolveZ(e, dt) { return moveAxis(e, 'z', (e.vz || 0) * dt); }
export function resolveY(e, dt) {
const delta = e.vy * dt;
// Движение ВВЕРХ (vy < 0): блокируем только если реально упёрлись головой.
// Раньше moveAxis проверял весь объём коробки, и блок ПОД ногами гасил
// прыжок в тот же кадр (vy сразу становился 0).
let hit = false;
if (delta < 0) {
const probe = boxOf(e);
probe.y0 += delta; // низ тела
probe.y1 += delta; // верх тела (ось Y вниз => уменьшение = вверх)
if (hitsSolid(probe)) {
// откатываем ровно до грани
const step = -0.5;
for (let i = 0; i < TILE * 4; i++) {
const t = boxOf(e);
t.y0 += step; t.y1 += step;
if (hitsSolid(t)) break;
e.y += step;
}
hit = true;
} else {
e.y += delta;
}
e.grounded = false;
return hit;
}
hit = moveAxis(e, 'y', delta);
e.grounded = checkGrounded(e);
return hit;
}
export { boxOf, hitsSolid, checkGrounded, onLadder, inFluid, solidAt, fluidAt, climbableAt };