// ==================== ФИЗИКА 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 };