grechka-game/srcv3/physics/collision3d.js

147 lines
4.6 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-коробка сущности: x/z — центр, y — низ (как в 2D-игре: e.x, e.y = левый-ниний)
function boxOf(e) {
return {
x0: e.x, x1: e.x + e.w,
y0: e.y, y1: e.y + e.h,
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);
}
// Пересекает ли коробка сущности твёрдый блок
function hitsSolid(bx) {
const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE);
const y0 = Math.floor(bx.y0 / 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);
const gy = Math.floor((bx.y1 + 1) / 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;
}
// Откат к границе блока
// шаг за блок, пока не пересечение
const stepPx = dir * 0.5;
let moved = 0;
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;
moved += stepPx;
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 hit = moveAxis(e, 'y', e.vy * dt);
e.grounded = false;
if (hit && e.vy >= 0) {
e.grounded = checkGrounded(e);
} else if (!hit) {
e.grounded = checkGrounded(e);
}
return hit;
}
export { boxOf, hitsSolid, checkGrounded, onLadder, inFluid, solidAt, fluidAt, climbableAt };