grechka-game/srcv3/game/loop3d.js

403 lines
14 KiB
JavaScript
Raw 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.

// ==================== ЦИКЛ ИГРЫ 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);
}
}