210 lines
9.3 KiB
JavaScript
210 lines
9.3 KiB
JavaScript
// ==================== ФИЗИКА 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 };
|