227 lines
7.8 KiB
JavaScript
227 lines
7.8 KiB
JavaScript
// ==================== МОБЫ 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';
|
|
|
|
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();
|
|
|
|
// ---------- ночные мобы днём горят ----------
|
|
if ((m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton') && !night) {
|
|
m.hp -= 30 * dt;
|
|
m.vx *= 0.5;
|
|
m.vz *= 0.5;
|
|
return;
|
|
}
|
|
|
|
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);
|
|
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;
|
|
m.flying = true;
|
|
} 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) {
|
|
const gx = Math.floor((m.x + (m.vx > 0 ? m.w + 2 : -2)) / TILE);
|
|
const gz = Math.floor(((m.z || 0) + (m.vz > 0 ? (m.d || m.w) + 2 : -2)) / TILE);
|
|
const gy = Math.floor((m.y + m.h - 1) / TILE);
|
|
const bx = getBlock(gx, gy, Math.floor(cz(m)));
|
|
const bz = getBlock(Math.floor(cx(m)), gy, gz);
|
|
if (bx || bz) m.vy = -280;
|
|
}
|
|
}
|
|
|
|
// ---------- ФИЗИКА ----------
|
|
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] || [];
|
|
}
|