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