// ==================== МОБЫ 3D: ИИ + ФИЗИКА — GrechkaCraft v3 ==================== // Вся логика из 2D-игры (все 9 видов, ночные/дневные, лут), но цель — // настоящая 3D-точка игрока (x,y,z) и прыжки через стены по всем осям. import { state } from '../core/state.js'; import { GRAV, GRAV_WATER, TILE } from '../core/constants.js'; import { updateWaterFlag3 } from '../physics/water-detect3d.js'; import { calculateDamage } from '../entities/player.js'; import { playSound } from '../audio/sound-engine.js'; import { explodeTNT3D } from '../world/tnt3d.js'; import { resolveY, resolveX, resolveZ } from '../physics/collision3d.js'; import { getBlock } from '../world/world-storage3d.js'; import { rebuildHotbar } from '../ui/hotbar.js'; export function isNight() { return state.worldTime > 0.5; } // Центры сущностей в тайлах function cx(e) { return (e.x + e.w / 2) / TILE; } function cy(e) { return (e.y + e.h / 2) / TILE; } function cz(e) { return ((e.z || 0) + (e.d || e.w) / 2) / TILE; } function playerC() { const p = state.player; return { x: cx(p), y: cy(p), z: cz(p) }; } function dist3(a, b) { return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z); } // Тянет моба к игроку по обеим горизонтальным осям (X и Z) function steerToPlayer(m, pc) { const dx = pc.x - cx(m); const dz = pc.z - cz(m); const len = Math.hypot(dx, dz) || 1; m.vx = (dx / len) * (m.speed || 60); m.vz = (dz / len) * (m.speed || 60); m.dir = dx >= 0 ? 1 : -1; m.faceYaw = Math.atan2(dz, dx); } function touchPlayer(m, pc, damage, knock = 1) { const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); if (d < 1.6 && state.player.invuln <= 0) { state.player.hp -= calculateDamage(damage); state.player.invuln = 0.8; const dx = state.player.x - m.x; const dz = (state.player.z || 0) - (m.z || 0); const len = Math.hypot(dx, dz) || 1; state.player.vx += (dx / len) * 420 * knock; state.player.vz += (dz / len) * 420 * knock; state.player.vy -= 260 * knock; playSound('hit1'); return true; } return false; } export function mobAI(m, dt) { updateWaterFlag3(m, dt); const pc = playerC(); const night = isNight(); // ---------- ночные мобы днём горят ---------- // ВАЖНО: не делаем ранний return — иначе моб зависает в воздухе // (физика ниже не выполняется). Ставим флаг и идём дальше. const burning = (m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton') && !night; if (burning) { m.burning = true; m.hp -= 30 * dt; m.vx *= 0.5; m.vz *= 0.5; } else { m.burning = false; } if (burning) { // горящий моб только падает и горит — никакого ИИ } else if (m.kind === 'zombie') { steerToPlayer(m, pc); if (m.inWater && Math.random() < 0.06) m.vy = -260; touchPlayer(m, pc, 15); } else if (m.kind === 'creeper') { steerToPlayer(m, pc); const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); if (m.inWater && Math.random() < 0.06) m.vy = -260; if (d < 3) { m.fuse -= dt; if (m.fuse <= 0) { explodeTNT3D({ gx: Math.floor(cx(m)), gy: Math.floor(cy(m)), gz: Math.floor(cz(m)) }, state); m.hp = 0; } } else if (d < 2.5) { m.fuse = 0.5; } } else if (m.kind === 'skeleton') { steerToPlayer(m, pc); const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); m.shootCooldown -= dt; if (d < 15 && m.shootCooldown <= 0) { m.shootCooldown = 2.0; const ox = m.x + m.w / 2, oy = m.y + m.h / 3, oz = (m.z || 0) + (m.d || m.w) / 2; const tx = state.player.x + state.player.w / 2; const ty = state.player.y + state.player.h / 2; const tz = (state.player.z || 0) + (state.player.d || state.player.w) / 2; // полёт стрелы по 3D const ddx = tx - ox, ddy = ty - oy, ddz = tz - oz; const dist = Math.hypot(ddx, ddy, ddz) || 1; const speed = 450; state.projectiles.push({ x: ox, y: oy, z: oz, vx: ddx / dist * speed, vy: ddy / dist * speed, vz: ddz / dist * speed, dmg: 6, owner: 'mob', life: 3, }); } } else if (m.kind === 'scorpion') { steerToPlayer(m, pc); if (m.inWater && Math.random() < 0.06) m.vy = -260; if (touchPlayer(m, pc, 8)) state.player.slowTimer = 3; } else if (m.kind === 'polar_bear') { // нейтрален: бродит m.aiT -= dt; if (m.aiT <= 0) { m.aiT = 2.0 + Math.random() * 3; const a = Math.random() * Math.PI * 2; m.walkX = Math.cos(a); m.walkZ = Math.sin(a); if (Math.random() < 0.3) { m.walkX = 0; m.walkZ = 0; } } m.vx = (m.walkX || 0) * m.speed; m.vz = (m.walkZ || 0) * m.speed; if (m.inWater) m.vy = -120; touchPlayer(m, pc, 10, 1.2); } else if (m.kind === 'slime') { // прыгает к игроку m.aiT -= dt; if (m.aiT <= 0) { m.aiT = 1.5 + Math.random() * 1.5; steerToPlayer(m, pc); m.vy = -200; } touchPlayer(m, pc, 8); } else if (m.kind === 'eagle') { // летает, пикирует const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); m.flying = true; // флаг ДО физики, иначе орёл получает гравитацию if (d < 20) { const dx = pc.x - cx(m), dz = pc.z - cz(m), dy = pc.y - cy(m); const hl = Math.hypot(dx, dz) || 1; m.vx = dx / hl * m.speed; m.vz = dz / hl * m.speed; m.vy = dy > 0 ? 60 : -60; } else { m.aiT -= dt; if (m.aiT <= 0) { m.aiT = 2 + Math.random() * 3; const a = Math.random() * Math.PI * 2; m.walkX = Math.cos(a); m.walkZ = Math.sin(a); } m.vx = (m.walkX || 0) * m.speed * 0.5; m.vz = (m.walkZ || 0) * m.speed * 0.5; m.vy = Math.sin(performance.now() / 1000) * 30; } if (d < 1.6 && state.player.invuln <= 0) { state.player.hp -= calculateDamage(10); state.player.invuln = 0.8; state.player.vy -= 200; playSound('hit1'); } m.vy *= 0.5; } else { // животные (pig, chicken): бродят m.aiT -= dt; if (m.aiT <= 0) { m.aiT = 1.8 + Math.random() * 2.5; const a = Math.random() * Math.PI * 2; m.walkX = Math.cos(a); m.walkZ = Math.sin(a); if (Math.random() < 0.25) { m.walkX = 0; m.walkZ = 0; } } const sp = m.kind === 'chicken' ? 55 : 40; m.vx = (m.walkX || 0) * sp; m.vz = (m.walkZ || 0) * sp; if (m.inWater) m.vy = -120; } // ---------- прыжок через препятствие по всем осям ---------- if (m.grounded && !m.flying && m.kind !== 'eagle') { const speed = Math.hypot(m.vx || 0, m.vz || 0); if (speed > 5) { // Проверяем отдельно по X и по Z, каждая на своей оси: // препятствие по X смотрим в колонке Z моба, и наоборот. const feetGY = Math.floor((m.y + m.h - 1) / TILE); const bodyGY = Math.floor((m.y + m.h / 2) / TILE); const mcz = Math.floor(cz(m)); let blocked = false; if (m.vx > 0) blocked = blocked || !!getBlock(Math.floor((m.x + m.w + 2) / TILE), bodyGY, mcz); if (m.vx < 0) blocked = blocked || !!getBlock(Math.floor((m.x - 2) / TILE), bodyGY, mcz); if (m.vz > 0) blocked = blocked || !!getBlock(Math.floor(cx(m)), bodyGY, Math.floor(((m.z || 0) + (m.d || m.w) + 2) / TILE)); if (m.vz < 0) blocked = blocked || !!getBlock(Math.floor(cx(m)), bodyGY, Math.floor(((m.z || 0) - 2) / TILE)); if (blocked) m.vy = -280; void feetGY; } } // ---------- ФИЗИКА ---------- if (!m.flying) { const g = m.inWater ? GRAV_WATER : GRAV; m.vy += g * dt; } m.y += m.vy * dt; m.x += (m.vx || 0) * dt; m.z += (m.vz || 0) * dt; resolveX(m, dt); resolveZ(m, dt); resolveY(m, dt); // орёл не падает if (m.flying) m.grounded = false; } // Лут — та же таблица, что в 2D export function getMobLoot(kind) { const table = { chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}], pig: [{item:'meat',min:1,max:2,chance:1}], zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}], skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}], creeper: [{item:'gunpowder',min:1,max:2,chance:1}], scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}], polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}], slime: [{item:'slime_ball',min:1,max:2,chance:1}], eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}], }; return table[kind] || []; } // Выдаёт лут моба в инвентарь. Возвращает список выданных предметов. export function grantMobLoot(kind) { const granted = []; for (const drop of getMobLoot(kind)) { if (Math.random() > drop.chance) continue; const qty = drop.min + Math.floor(Math.random() * (drop.max - drop.min + 1)); if (qty <= 0) continue; if (state.inv[drop.item] === undefined) state.inv[drop.item] = 0; state.inv[drop.item] += qty; granted.push({ item: drop.item, qty }); } if (granted.length) rebuildHotbar(); return granted; } // Убийство моба: помечает мёртвым, даёт лут, чистит из массива. // Вызывается из mouse-handler (удар) и из цикла (смерть от урона/взрыва). export function killMob(m) { if (m.dead) return []; m.dead = true; m.hp = 0; const granted = grantMobLoot(m.kind); if (m.kind === 'chicken') playSound('hurt_chicken'); else playSound('attack'); const i = state.mobs.indexOf(m); if (i >= 0) state.mobs.splice(i, 1); return granted; } // Смерть от урона (взрыв, падение в лаву и т.п.) — вызывается из цикла каждый кадр. export function reapDeadMobs() { for (let i = state.mobs.length - 1; i >= 0; i--) { const m = state.mobs[i]; if (m.hp <= 0) killMob(m); } }