grechka-game/srcv3/spawn3d.js

157 lines
9.1 KiB
JavaScript
Raw Permalink 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.

// ==================== СПАВН: единая реализация для всех путей ====================
// Раньше логика спавна была продублирована в 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);
}