// ==================== ЦИКЛ ИГРЫ 3D — GrechkaCraft v3 ==================== // Всё как в 2D-игре, но движение по трём осям: X (вперёд-назад по миру), // Y (вверх-вниз), Z (вбок). WASD работают относительно взгляда камеры. import { state } from '../core/state.js'; import { BLOCKS } from '../data/blocks.js'; import { playSound } from '../audio/sound-engine.js'; import { getBlock } from '../world/world-storage3d.js'; import { updateWaterFlag3 } from '../physics/water-detect3d.js'; import { resolveY, resolveX, resolveZ, onLadder } from '../physics/collision3d.js'; import { calculateDamage } from '../entities/player.js'; import { tickFurnaces } from '../ui/furnace.js'; import { renderFurnaceUI } from '../ui/furnace.js'; import { updateWeather, updateRain } from '../world/weather.js'; import { ensureGenAroundCamera, surfaceGyAt, getCachedBiome, WORLD_DEPTH } from '../world/generation3d.js'; import { Zombie, Creeper, Skeleton, Pig, Chicken, Scorpion, PolarBear, Slime, Eagle } from '../entities/mobs3d.js'; import { isNight, mobAI } from '../entities/mob-ai3d.js'; import { sendPlayerPosition } from '../multiplayer/socket-helpers.js'; import { renderFrame3D } from '../render/render3d.js'; import { explodeTNT3D } from '../world/tnt3d.js'; let last = performance.now(); let prevJump = false; document.addEventListener('visibilitychange', () => { if (!document.hidden) last = performance.now(); }); // Направление взгляда камеры (задаётся рендером) — WASD относительно него export const camDir = { yaw: 0, pitch: 0 }; export function loop(now) { const rawDt = Math.min(0.05, (now - last) / 1000); last = now; const PHYSICS_STEP = 0.016; const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP)); const dt = rawDt / steps; const player = state.player; const inp = state.inp; const mobs = state.mobs; const projectiles = state.projectiles; const boat = state.boat; const jumpPressed = inp.j && !prevJump; prevJump = inp.j; // ---------- день/ночь ---------- if (!state.dayNightCycle) { // время стоит } else if (player.sleeping && isNight()) { state.worldTime += dt * 8 / state.DAY_LEN; player.hp = Math.min(100, player.hp + dt * 20); if (!isNight()) player.sleeping = false; } else { state.worldTime += dt / state.DAY_LEN; } if (state.worldTime >= 1) state.worldTime -= 1; // ---------- генерация мира ---------- ensureGenAroundCamera(); // ---------- кислород/утопление ---------- if (player.headInWater) { player.o2 = Math.max(0, player.o2 - 6 * dt); if (player.o2 === 0) player.hp -= calculateDamage(4 * dt); } else { player.o2 = Math.min(100, player.o2 + 10 * dt); } player.hunger = Math.max(0, player.hunger - dt * 0.2); // ---------- температура по биому (теперь биом 2D) ---------- if (player.temperature === undefined) player.temperature = 15; const BIOME_TEMP = { snow: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 }; const COLD_THRESHOLD = -5; const HEAT_THRESHOLD = 35; const pGX = Math.floor((player.x + player.w / 2) / state.TILE); const pGZ = Math.floor((player.z + player.d / 2) / state.TILE); const pGY = Math.floor((player.y + player.h / 2) / state.TILE); const biome = getCachedBiome(pGX, pGZ); const sGY = surfaceGyAt(pGX, pGZ); const isUnderground = (pGY - sGY) > 2; let targetTemp = BIOME_TEMP[biome] || 15; if (isUnderground) targetTemp = 15; if (isNight() && !isUnderground) targetTemp -= 10; if ((state.weatherType === 'rain' || state.weatherType === 'storm') && !isUnderground) targetTemp -= 5; player.temperature += (targetTemp - player.temperature) * dt * 0.3; // костры/факелы греют, вода в пустыне холодит — ищем в кубе вокруг let nearHeat = false, nearCool = false; for (let dx = -2; dx <= 2 && !nearHeat; dx++) { for (let dy = -2; dy <= 2 && !nearHeat; dy++) { for (let dz = -2; dz <= 2 && !nearHeat; dz++) { const b = getBlock(pGX + dx, pGY + dy, pGZ + dz); if (b && (b.t === 'campfire' || b.t === 'torch' || b.t === 'furnace')) nearHeat = true; } } } if (!nearHeat && biome === 'desert') { for (let dx = -2; dx <= 2 && !nearCool; dx++) { for (let dz = -2; dz <= 2 && !nearCool; dz++) { const b = getBlock(pGX + dx, pGY, pGZ + dz); if (b && b.t === 'water') nearCool = true; } } } if (nearHeat) { player.temperature += (30 - player.temperature) * dt * 2; if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt); } if (nearCool) { player.temperature += (15 - player.temperature) * dt * 1.5; if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) player.hp = Math.min(100, player.hp + 8 * dt); } if (isUnderground && !nearHeat && player.hp < 100 && player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) { player.hp = Math.min(100, player.hp + 2 * dt); } const aboveBlk = getBlock(pGX, pGY + 1, pGZ); if (aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass' && !isUnderground && !nearCool) { player.temperature -= 5 * dt; } if (player.temperature < COLD_THRESHOLD) { player.hp -= 1 * dt; player.hunger -= 0.5 * dt; } if (player.slowTimer > 0) player.slowTimer -= dt; // ---------- кактус: урон в кубе ---------- for (let dx = -1; dx <= 1; dx++) { for (let dy = -1; dy <= 1; dy++) { for (let dz = -1; dz <= 1; dz++) { const cb = getBlock(pGX + dx, pGY + dy, pGZ + dz); if (cb && BLOCKS[cb.t] && BLOCKS[cb.t].hurt && player.invuln <= 0) { player.hp -= 2; player.invuln = 0.5; } } } } // ---------- лёд: скольжение ---------- const underBlk = getBlock(pGX, pGY - 1, pGZ); if (underBlk && BLOCKS[underBlk.t] && BLOCKS[underBlk.t].slip && player.grounded) { player.vx *= 0.98; player.vz *= 0.98; } // ---------- ДВИЖЕНИЕ: WASD относительно камеры ---------- if (player.sleeping) { player.vx = player.vy = player.vz = 0; } else { // локальные оси ввода let fwd = 0, side = 0; if (inp.up) fwd -= 1; if (inp.r || inp.right) side += 1; if (inp.l || inp.left) side -= 1; if (inp.d || inp.back) fwd += 1; // в мировых осях через угол камеры const cy = Math.cos(camDir.yaw), sy = Math.sin(camDir.yaw); let dx = side * cy - fwd * sy; let dz = side * sy + fwd * cy; const len = Math.hypot(dx, dz); if (len > 0.0001) { dx /= len; dz /= len; player.vx = dx * state.MOVE; player.vz = dz * state.MOVE; player.facing = Math.atan2(dz, dx); } else { player.vx *= 0.82; player.vz *= 0.82; } } // шаги if (player.grounded && !player.inWater && Math.hypot(player.vx, player.vz) > 50) { if (now / 1000 - player.lastStepTime > 0.35) { playSound('step'); player.lastStepTime = now / 1000; } } // ---------- прыжок / плавание / лодка ---------- if (player.inBoat) { player.vy = 0; player.x = boat.x + 2; player.y = boat.y - 4; player.z = boat.z + 2; player.grounded = true; player.inWater = false; if (jumpPressed) { state.inv.boat = (state.inv.boat || 0) + 1; player.inBoat = false; boat.active = false; player.y += state.TILE; player.vy = -state.JUMP * 0.5; playSound('splash'); } } else if (player.inWater) { player.vx *= 0.90; player.vz *= 0.90; player.vy *= 0.92; if (!jumpPressed && !inp.j) { player.vy += state.GRAV_WATER * dt; } else if (jumpPressed) { player.vy = Math.min(player.vy, -520); } else if (inp.j) { player.vy = Math.min(player.vy, -260); } } else { if (jumpPressed && player.grounded && !player.sleeping) { player.vy = -state.JUMP; player.grounded = false; player.fallStartY = player.y; } } if (!player.inWater && !player.inBoat) { player.vy += state.GRAV * dt; } // ---------- лодка: плывёт, тонет вне воды ---------- if (boat.active) { boat.x += boat.vx * dt; boat.y += boat.vy * dt; boat.z += (boat.vz || 0) * dt; const boatGX = Math.floor(boat.x / state.TILE); const boatGY = Math.floor(boat.y / state.TILE); const boatGZ = Math.floor(boat.z / state.TILE); const below = getBlock(boatGX, boatGY + 1, boatGZ); if (!below || (below.t !== 'water' && below.t !== 'swamp_water')) { state.inv.boat = (state.inv.boat || 0) + 1; player.inBoat = false; boat.active = false; player.y += state.TILE; player.vy = -200; playSound('splash'); } } if (player.inBoat && !boat.active) { state.inv.boat = (state.inv.boat || 0) + 1; player.inBoat = false; player.y += state.TILE; player.vy = -200; playSound('splash'); } // ---------- границы мира по Z ---------- const maxZ = WORLD_DEPTH * state.TILE; if (player.z < -maxZ) { player.z = -maxZ; player.vz = 0; } if (player.z > maxZ) { player.z = maxZ; player.vz = 0; } // ---------- ФИЗИКА: X → Z → Y ---------- const sub = dt; for (let step = 0; step < steps; step++) { player.x += player.vx * sub; player.z += player.vz * sub; player.y += player.vy * sub; resolveX(player, sub); resolveZ(player, sub); resolveY(player, sub); } // лестница: вертикальное перемещение if (onLadder(player)) { if (inp.j) player.vy = -200; else if (inp.s || inp.down) player.vy = 100; else player.vy = 0; } // урон от падения: дистанция от последнего касания земли if (player.grounded) { if (player.fallStartY !== undefined) { const fallDist = player.fallStartY - player.y; if (fallDist > state.TILE * 6) { player.hp -= calculateDamage((fallDist / state.TILE - 6) * 2.5); } } player.fallStartY = player.y; } else if (player.vy > 0 && player.fallStartY === undefined) { player.fallStartY = player.y; } // вода updateWaterFlag3(player, dt); sendPlayerPosition(); updateWeather(dt); updateRain(dt); player.invuln = Math.max(0, player.invuln - dt); // ---------- мобы ---------- state.spawnT += dt; if (state.spawnT > 6) { state.spawnT = 0; spawnMobs3D(); } for (const m of mobs) { mobAI(m, dt); // физика 3D внутри mob-ai3d } // снаряды for (let i = projectiles.length - 1; i >= 0; i--) { const pr = projectiles[i]; pr.x += pr.vx * dt; pr.y += pr.vy * dt; pr.life -= dt; // столкновение с блоком (по Z игрока/снаряда) const gx = Math.floor(pr.x / state.TILE); const gy = Math.floor(pr.y / state.TILE); const gz = Math.floor((pr.z || player.z) / state.TILE); const b = getBlock(gx, gy, gz); if (b && BLOCKS[b.t] && BLOCKS[b.t].solid) { projectiles.splice(i, 1); continue; } if (pr.life <= 0) projectiles.splice(i, 1); } // ---------- TNT ---------- for (const b of state.blocks) { if (b.dead || b.t !== 'tnt' || !b.active) continue; b.fuse -= dt; if (b.fuse <= 0) { explodeTNT3D(b, state); } } // ---------- печь ---------- tickFurnaces(dt); if (state.currentFurnaceKey && Math.random() < 0.1) renderFurnaceUI(); // ---------- смерть/возрождение ---------- if (player.hp <= 0 && state.deathEl && state.deathEl.style.display !== 'flex') { player.hp = 0; state.deathEl.style.display = 'flex'; } else if (player.hp > 0 && state.deathEl && state.deathEl.style.display === 'flex') { state.deathEl.style.display = 'none'; } // ---------- HUD ---------- if (Math.random() < 0.25) { state.hpEl.textContent = Math.max(0, Math.ceil(player.hp)); state.foodEl.textContent = Math.ceil(player.hunger); const o2El = document.getElementById('o2'); if (o2El) o2El.textContent = Math.ceil(player.o2); const tempEl = document.getElementById('temp'); if (tempEl) { const t = Math.round(player.temperature); tempEl.textContent = t + '°C'; if (player.temperature < COLD_THRESHOLD) tempEl.style.color = '#74b9ff'; else if (player.temperature > HEAT_THRESHOLD) tempEl.style.color = '#e17055'; else tempEl.style.color = '#55efc4'; } state.sxEl.textContent = pGX; state.syEl.textContent = pGY; state.todEl.textContent = isNight() ? 'Ночь' : 'День'; state.worldIdEl.textContent = state.worldId; if (state.isMultiplayer) { const st = document.getElementById('multiplayerStatus'); if (st) st.style.display = 'flex'; state.playerCountEl.textContent = state.otherPlayers.size + 1; } else { const st = document.getElementById('multiplayerStatus'); if (st) st.style.display = 'none'; } } // сон — DOM-оверлей const sleepOv = document.getElementById('sleepOverlay'); if (sleepOv) sleepOv.style.display = player.sleeping ? 'flex' : 'none'; // ---------- РЕНДЕР ---------- renderFrame3D(now); requestAnimationFrame(loop); } // ---------- спавн мобов в 3D ---------- function spawnMobs3D() { const p = state.player; const pcx = p.x / state.TILE; const pcy = p.y / state.TILE; const pcz = (p.z || 0) / state.TILE; const kinds = [Zombie, Pig, Chicken, Creeper, Skeleton, Scorpion, PolarBear, Slime, Eagle]; for (let i = 0; i < 2; i++) { const ang = Math.random() * Math.PI * 2; const dist = 14 + Math.random() * 12; const mx = Math.floor(pcx + Math.cos(ang) * dist); const mz = Math.floor(pcz + Math.sin(ang) * dist); if (Math.abs(mz) > WORLD_DEPTH) continue; const sy = surfaceGyAt(mx, mz) - 1; if (sy < 1) continue; const K = kinds[Math.floor(Math.random() * kinds.length)]; const m = new K(mx * state.TILE, sy * state.TILE, mz * state.TILE); m.z = mz * state.TILE; m.d = m.d || m.w; m.vz = 0; state.mobs.push(m); } }