157 lines
9.1 KiB
JavaScript
157 lines
9.1 KiB
JavaScript
// ==================== СПАВН: единая реализация для всех путей ====================
|
||
// Раньше логика спавна была продублирована в main3d.js (ensurePlayerOnGround)
|
||
// и в socket.js (обработчик world_state). Две копии расходились: одна искала
|
||
// площадку с 2 тайлами воздуха, другая — с 3, и одна считала feet как
|
||
// (top+1)*T, другая как (top-1)*T. Итог: игрок то тонул в грунте, то падал
|
||
// в яму, камера уходила в стену, схлопывалась в первое лицо (camDist = 0),
|
||
// и весь кадр занимало пустое небо.
|
||
//
|
||
// Теперь здесь ЕДИНАЯ реализация, которую зовут и main3d, и socket.js.
|
||
//
|
||
// ---------- МАТЕМАТИКА (ось Y ВНИЗ: чем больше y, тем НИЖЕ) ----------
|
||
// Тайл gy занимает [gy*T, (gy+1)*T]. Чем больше gy — тем НИЖЕ на экране.
|
||
//
|
||
// Опора top — самый верхний блок колонки (трава/песок/камень).
|
||
// Верхняя грань опоры = (top+1)*T. Игрок стоит НА ней:
|
||
// feet = (top+1)*T
|
||
// p.y = feet - h (p.y — ВЕРХ коллайдера)
|
||
//
|
||
// Тело занимает [p.y, feet] = [(top+1)*T - h, (top+1)*T].
|
||
// При h=34 и T=40 это последние 34 px тайла top плюс 0 px выше, то есть
|
||
// тело физически В тайле top. Чтобы тело оказалось ВЫШЕ опоры, feet должен
|
||
// быть (top+1)*T и h — 34, тогда тело = [top*T+6, (top+1)*T]: тайл top
|
||
// занят опорой, и гравитация опустит игрока на верхнюю грань top+1*T.
|
||
//
|
||
// ПРАКТИКА: после установки игрок должен оказаться в тайле top+1 (воздух).
|
||
// Проверяем это: getBlock(gx, top+1, gz) === null.
|
||
|
||
import { state } from './core/state.js';
|
||
import { getBlock } from './world/world-storage3d.js';
|
||
import { genColumn } from './world/generation3d.js';
|
||
import { BLOCKS } from './data/blocks.js';
|
||
|
||
const isWater = (gx, gy, gz) => {
|
||
const b = getBlock(gx, gy, gz);
|
||
if (!b) return false;
|
||
const d = BLOCKS[b.t];
|
||
return !!(d && d.fluid);
|
||
};
|
||
|
||
// Пусто = нет блока, либо блок не твёрдый, не жидкость, не лестница.
|
||
// Трава/мoss/snow имеют solid:true — они и есть поверхность, на ней стоят.
|
||
const isEmpty = (gx, gy, gz) => {
|
||
const b = getBlock(gx, gy, gz);
|
||
if (!b) return true;
|
||
const d = BLOCKS[b.t];
|
||
return !d || (!d.solid && !d.fluid && !d.climbable);
|
||
};
|
||
|
||
// Клетка годится, если:
|
||
// - top — верхний твёрдый тайл (не вода),
|
||
// - top+1 и top+2 пусты (тело игрока),
|
||
// - top+3 пуст (камера).
|
||
// Проверять top-1 бессмысленно: это ВСЕГДА грунт под поверхностью.
|
||
// Клетка годится, если top — верхний твёрдый тайл колонки (не вода),
|
||
// и тайлы top-1, top-2, top-3 пусты: ноги, голова и место для камеры.
|
||
// Обход СВЕРХУ ВНИЗ: берём самый верхний твёрдый тайл, у которого есть
|
||
// воздух. Если брать ниже, попадём внутрь толщи грунта.
|
||
// Трава/мoss/snow — декорация ПОВЕРХ грунта: она не твёрдая опора, а
|
||
// getBlock отдаёт её как обычный блок. Если брать самый верхний блок,
|
||
// получаем top = трава, а под ней (top-1) — dirt, и проверка «воздух
|
||
// выше» отвергает ВСЯ колонку: сухой площадки не найдено рядом.
|
||
//
|
||
// Правильно: опора = самый верхний блок, под которым начинается ВОЗДУХ.
|
||
// То есть идём сверху вниз, пропускаем decor-слой, и берём первый твёрдый
|
||
// слой, у которого выше (меньшие gy) 3 тайла пустых.
|
||
// Поверхность для стоянки = ВЕРХНЯЯ граница мира в колонке.
|
||
// Опора top = самый верхний блок. Но под ним (top-1) ВСЕГДА грунт —
|
||
// трава лежит на dirt, песок на stone. Проверять top-1 как «воздух» бессмысленно:
|
||
// это отвергало каждую колонку («сухой площадки не найдено рядом»).
|
||
//
|
||
// Что реально нужно: над верхним блоком колонки (top-1, top-2, top-3 —
|
||
// то есть МЕНЬШИЕ gy) должно быть ПУСТО. Для колонки dirt, grass:
|
||
// gy=10 grass, gy=9 dirt, gy=8 dirt, gy=7 … пусто
|
||
// верхний блок = 10 (grass). Выше = 9 (dirt) — ЗАНЯТО. Значит опора не 10.
|
||
// Настоящая свободная кромка = gy=7, опора под ней = 6.
|
||
//
|
||
// Значит ищем: тайл top, где top-1, top-2 пусты (ноги+голова), и top+1
|
||
// ТВЁРДЫЙ (это опора, на ней стоим). Камера требует ещё top-2 пуст.
|
||
// Опора top = САМЫЙ ВЕРХНИЙ блок колонки (не вода).
|
||
// Над ним (top+1, top+2, top+3 — БОЛЬШИЕ gy, то есть НИЖЕ по оси Y… нет,
|
||
// больше gy = ниже) — стоп. Здесь Y ВНИЗ, поэтому «выше опоры» = top+1?
|
||
// Нет: верхняя грань тайла top = (top+1)*T, значит пространство ВЫШЕ опоры
|
||
// лежит в тайле top-1 (меньший y = выше). Проверяем top-1, top-2, top-3.
|
||
const tryColumn = (gx, gz) => {
|
||
let top = -1;
|
||
for (let gy = 60; gy >= 1; gy--) {
|
||
const b = getBlock(gx, gy, gz);
|
||
if (b && !isWater(gx, gy, gz)) { top = gy; break; }
|
||
}
|
||
if (top < 4) return -1;
|
||
// свободное место ВЫШЕ опоры: тайлы с МЕНЬШИМ gy
|
||
if (!isEmpty(gx, top - 1, gz)) return -1;
|
||
if (!isEmpty(gx, top - 2, gz)) return -1;
|
||
if (!isEmpty(gx, top - 3, gz)) return -1;
|
||
return top;
|
||
};
|
||
|
||
// Расширяющаяся спираль: своя колонка, затем кольца до 40 тайлов.
|
||
export function findStandSpot(startGX, startGZ) {
|
||
for (let r = 0; r <= 40; r++) {
|
||
for (let dz = -r; dz <= r; dz++) {
|
||
for (let dx = -r; dx <= r; dx++) {
|
||
if (r > 0 && Math.max(Math.abs(dx), Math.abs(dz)) !== r) continue;
|
||
const gx = startGX + dx, gz = startGZ + dz;
|
||
const top = tryColumn(gx, gz);
|
||
if (top >= 0) return { gx, gz, top };
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Применяет найденную площадку к игроку. genRadius — сколько колонок
|
||
// сгенерировать вокруг, чтобы поиск имел смысл (в неготовом мире колонки
|
||
// пусты и tryColumn ничего не найдёт).
|
||
export function placePlayerOnStandSpot(startGX, startGZ, genRadius) {
|
||
const T = state.TILE;
|
||
const p = state.player;
|
||
if (typeof genRadius === 'number') {
|
||
for (let dx = -genRadius; dx <= genRadius; dx++) {
|
||
for (let dz = -genRadius; dz <= genRadius; dz++) {
|
||
// каждый 4-й по диагонали — экономия, остальные колонки рядом
|
||
// всё равно генерируются по мере надобности
|
||
if (((dx + dz) & 3) !== 0) continue;
|
||
try { genColumn(startGX + dx, startGZ + dz); } catch (e) { /* нет генератора */ }
|
||
}
|
||
}
|
||
}
|
||
const spot = findStandSpot(startGX, startGZ);
|
||
if (!spot) {
|
||
console.warn('[Spawn] сухой площадки не найдено рядом', startGX, startGZ);
|
||
return null;
|
||
}
|
||
p.x = spot.gx * T;
|
||
p.z = spot.gz * T;
|
||
// feet = top*T (верхняя грань опоры). player.y — ВЕРХ коллайдера,
|
||
// то есть на h пикселей ВЫШЕ feet по оси Y.
|
||
p.y = (spot.top + 1) * T - p.h; // верхняя грань опоры минус высота
|
||
p.vx = 0; p.vy = 0; p.vz = 0;
|
||
p.grounded = true;
|
||
p.fallStartY = p.y;
|
||
state.spawnPoint.x = p.x;
|
||
state.spawnPoint.y = p.y;
|
||
state.spawnPoint.z = p.z;
|
||
console.log('[Spawn] игрок на опоре', spot.gx, spot.top, spot.gz, 'p.y=', p.y);
|
||
return spot;
|
||
}
|
||
|
||
// Точка входа: читает текущую позицию игрока и ставит на площадку рядом.
|
||
export function ensurePlayerOnGround() {
|
||
const T = state.TILE;
|
||
const p = state.player;
|
||
const curGX = Math.floor((p.x + p.w / 2) / T);
|
||
const curGZ = Math.floor((p.z + p.d / 2) / T);
|
||
return placePlayerOnStandSpot(curGX, curGZ, 12);
|
||
}
|