186 lines
6.8 KiB
JavaScript
186 lines
6.8 KiB
JavaScript
// Моб AI
|
||
import { state } from '../core/state.js';
|
||
import { GRAV, GRAV_WATER, TILE } from '../core/constants.js';
|
||
import { updateWaterFlag } from '../physics/water-detect.js';
|
||
import { calculateDamage } from '../entities/player.js';
|
||
import { playSound } from '../audio/sound-engine.js';
|
||
import { explodeAt } from '../world/tnt.js';
|
||
import { resolveY } from '../physics/collision.js';
|
||
import { resolveX } from '../physics/collision.js';
|
||
import { isSolid } from '../world/world-storage.js';
|
||
import { getCachedBiome } from '../world/generation.js';
|
||
|
||
export function mobAI(m, dt) {
|
||
updateWaterFlag(m);
|
||
|
||
if (m.kind === 'zombie') {
|
||
const night = isNight();
|
||
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
|
||
const dir = Math.sign((state.player.x) - m.x);
|
||
m.vx = dir * m.speed;
|
||
if (m.inWater && Math.random() < 0.06) m.vy = -260;
|
||
if (Math.abs((m.x + m.w / 2) - (state.player.x + state.player.w / 2)) < 28 &&
|
||
Math.abs((m.y + m.h / 2) - (state.player.y + state.player.h / 2)) < 40 &&
|
||
state.player.invuln <= 0) {
|
||
const damage = calculateDamage(15);
|
||
state.player.hp -= damage;
|
||
state.player.invuln = 0.8;
|
||
state.player.vx += dir * 420;
|
||
state.player.vy -= 260;
|
||
playSound('hit1');
|
||
}
|
||
} else if (m.kind === 'creeper') {
|
||
const night = isNight();
|
||
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
|
||
const dir = Math.sign((state.player.x) - m.x);
|
||
const dist = Math.hypot((state.player.x + state.player.w / 2) - (m.x + m.w / 2), (state.player.y + state.player.h / 2) - (m.y + m.h / 2));
|
||
m.vx = dir * m.speed;
|
||
if (m.inWater && Math.random() < 0.06) m.vy = -260;
|
||
if (dist < 60) {
|
||
m.fuse -= dt;
|
||
if (m.fuse <= 0) {
|
||
explodeAt(Math.floor((m.x + m.w / 2) / TILE), Math.floor((m.y + m.h / 2) / TILE));
|
||
m.hp = 0;
|
||
}
|
||
} else {
|
||
if (dist < 40) {
|
||
m.fuse = 0.5;
|
||
}
|
||
}
|
||
} else if (m.kind === 'skeleton') {
|
||
const night = isNight();
|
||
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
|
||
const dir = Math.sign((state.player.x) - m.x);
|
||
const dist = Math.hypot((state.player.x + state.player.w / 2) - (m.x + m.w / 2), (state.player.y + state.player.h / 2) - (m.y + m.h / 2));
|
||
m.vx = dir * m.speed;
|
||
if (m.inWater && Math.random() < 0.06) m.vy = -260;
|
||
m.shootCooldown -= dt;
|
||
if (dist < 300 && m.shootCooldown <= 0) {
|
||
m.shootCooldown = 2.0;
|
||
const dx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2);
|
||
const dy = (state.player.y + state.player.h / 2) - (m.y + m.h / 2);
|
||
const angle = Math.atan2(dy, dx);
|
||
const speed = 450;
|
||
state.projectiles.push({
|
||
x: m.x + m.w / 2,
|
||
y: m.y + m.h / 3,
|
||
vx: Math.cos(angle) * speed,
|
||
vy: Math.sin(angle) * speed,
|
||
dmg: 6,
|
||
owner: 'mob',
|
||
life: 3
|
||
});
|
||
}
|
||
} else if (m.kind === 'scorpion') {
|
||
// Скорпион — бежит к игроку, яд (замедление)
|
||
const dir = Math.sign((state.player.x) - m.x);
|
||
m.vx = dir * m.speed;
|
||
if (m.inWater && Math.random() < 0.06) m.vy = -260;
|
||
if (Math.abs((m.x + m.w / 2) - (state.player.x + state.player.w / 2)) < 28 &&
|
||
Math.abs((m.y + m.h / 2) - (state.player.y + state.player.h / 2)) < 40 &&
|
||
state.player.invuln <= 0) {
|
||
const damage = calculateDamage(8);
|
||
state.player.hp -= damage;
|
||
state.player.invuln = 0.8;
|
||
state.player.slowTimer = 3;
|
||
state.player.vx += dir * 300;
|
||
state.player.vy -= 200;
|
||
playSound('hit1');
|
||
}
|
||
} else if (m.kind === 'polar_bear') {
|
||
// Белый медведь — нейтрален, бродит
|
||
m.aiT -= dt;
|
||
if (m.aiT <= 0) {
|
||
m.aiT = 2.0 + Math.random() * 3;
|
||
m.dir = Math.random() < 0.5 ? -1 : 1;
|
||
if (Math.random() < 0.3) m.dir = 0;
|
||
}
|
||
m.vx = m.dir * m.speed;
|
||
if (m.inWater) m.vy = -120;
|
||
} else if (m.kind === 'slime') {
|
||
// Слизь — прыгает к игроку
|
||
const dir = Math.sign((state.player.x + state.player.w / 2) - (m.x + m.w / 2));
|
||
m.aiT -= dt;
|
||
if (m.aiT <= 0) {
|
||
m.aiT = 1.5 + Math.random() * 1.5;
|
||
m.dir = dir;
|
||
m.vy = -200;
|
||
}
|
||
m.vx = m.dir * m.speed;
|
||
} else if (m.kind === 'eagle') {
|
||
// Орёл — летает, атакует пикированием
|
||
const dx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2);
|
||
const dy = (state.player.y + state.player.h / 2) - (m.y + m.h / 2);
|
||
const dist = Math.hypot(dx, dy);
|
||
if (dist < 400) {
|
||
m.vx = Math.sign(dx) * m.speed;
|
||
m.vy = dy > 0 ? 60 : -60;
|
||
} else {
|
||
m.aiT -= dt;
|
||
if (m.aiT <= 0) {
|
||
m.aiT = 2 + Math.random() * 3;
|
||
m.dir = Math.random() < 0.5 ? -1 : 1;
|
||
}
|
||
m.vx = m.dir * m.speed * 0.5;
|
||
m.vy = Math.sin(performance.now() / 1000) * 30;
|
||
}
|
||
// Атака при касании
|
||
if (dist < 30 && state.player.invuln <= 0) {
|
||
const damage = calculateDamage(10);
|
||
state.player.hp -= damage;
|
||
state.player.invuln = 0.8;
|
||
state.player.vy -= 200;
|
||
playSound('hit1');
|
||
}
|
||
// Орёл не падает — летающий моб
|
||
m.vy *= 0.5;
|
||
m.grounded = false;
|
||
} else {
|
||
// животные (pig, chicken)
|
||
m.aiT -= dt;
|
||
if (m.aiT <= 0) {
|
||
m.aiT = 1.8 + Math.random() * 2.5;
|
||
m.dir = Math.random() < 0.5 ? -1 : 1;
|
||
if (Math.random() < 0.25) m.dir = 0;
|
||
}
|
||
m.vx = m.dir * (m.kind === 'chicken' ? 55 : 40);
|
||
if (m.inWater) m.vy = -120;
|
||
}
|
||
|
||
// Мобы прыгают при столкновении со стеной
|
||
if (m.grounded && Math.abs(m.vx) > 5 && m.kind !== 'eagle') {
|
||
const probeX = m.vx > 0 ? m.x + m.w + 2 : m.x - 2;
|
||
const probeGY = Math.floor((m.y + m.h - 1) / TILE);
|
||
if (isSolid(Math.floor(probeX / TILE), probeGY)) {
|
||
m.vy = -280;
|
||
}
|
||
}
|
||
|
||
// физика моба
|
||
const g = (m.inWater && m.kind !== 'eagle') ? GRAV_WATER : GRAV;
|
||
m.vy += g * dt;
|
||
|
||
m.y += m.vy * dt; m.grounded = false; resolveY(m);
|
||
m.x += m.vx * dt; resolveX(m);
|
||
}
|
||
|
||
export function isNight() {
|
||
return state.worldTime > 0.5;
|
||
}
|
||
|
||
// Mob loot table
|
||
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] || [];
|
||
}
|