diff --git a/src/engine/BabylonScene.js b/src/engine/BabylonScene.js index e68d323..99d1006 100644 --- a/src/engine/BabylonScene.js +++ b/src/engine/BabylonScene.js @@ -5678,6 +5678,10 @@ export class BabylonScene { if (hit?.mesh && this.zombieManager) { this.zombieManager.damageByMesh(hit.mesh, hit.damage || 25); } + // Урон скриптовым NPC (киты-враги) → авто-floater над мобом (задача 40). + if (hit?.mesh && this.npcManager) { + try { this.npcManager.damageByMesh(hit.mesh, hit.damage || 25); } catch (e) {} + } if (this._onWeaponHit) { try { this._onWeaponHit(hit); } catch (e) {} } diff --git a/src/engine/GameRuntime.js b/src/engine/GameRuntime.js index ded168b..06651fd 100644 --- a/src/engine/GameRuntime.js +++ b/src/engine/GameRuntime.js @@ -1772,6 +1772,15 @@ export class GameRuntime { } catch (e) { /* ignore */ } return; } + if (cmd === 'fx.autoMobFloaters') { + try { + if (this.scene3d?.npcManager) { + this.scene3d.npcManager._autoFloater = payload?.enabled + ? { opts: payload?.opts || {} } : null; + } + } catch (e) { /* ignore */ } + return; + } if (cmd === 'fx.create') { // payload: { kind: 'beam'|'trail', localRef, ... } diff --git a/src/engine/NpcManager.js b/src/engine/NpcManager.js index ffaca5e..8ee0017 100644 --- a/src/engine/NpcManager.js +++ b/src/engine/NpcManager.js @@ -293,10 +293,36 @@ export class NpcManager { damage(id, amount) { const npc = this.npcs.get(Number(id)); if (!npc || npc.dead) return; - npc.hp = Math.max(0, npc.hp - (Number(amount) || 0)); + const amt = Number(amount) || 0; + npc.hp = Math.max(0, npc.hp - amt); + // Авто-floater над мобом (задача 40 доп): game.fx.autoMobFloaters(true). + if (this._autoFloater && amt > 0 && this.scene3d?.floaters) { + try { + this.scene3d.floaters.spawn( + { x: npc.x, y: (npc.y || 0) + 2.2, z: npc.z }, amt, this._autoFloater.opts || {}); + } catch (e) { /* ignore */ } + } if (npc.hp <= 0) this._killNpc(npc); } + /** Нанести урон NPC по мешу-попаданию (бластер/оружие). Ищет NPC, чьи меши + * содержат hit-меш (или предка). Вызывает damage() → авто-floater. */ + damageByMesh(mesh, amount) { + if (!mesh) return false; + for (const npc of this.npcs.values()) { + if (npc.dead) continue; + const root = npc.data && npc.data.rootMesh; + if (!root) continue; + let m = mesh, hit = false; + for (let i = 0; i < 8 && m; i++) { // вверх по иерархии до rootMesh + if (m === root) { hit = true; break; } + m = m.parent; + } + if (hit) { this.damage(npc.id, amount); return true; } + } + return false; + } + /** Удалить NPC по id (без эффекта смерти — просто убрать). */ removeNpc(id) { const npc = this.npcs.get(Number(id)); diff --git a/src/engine/ScriptSandboxWorker.js b/src/engine/ScriptSandboxWorker.js index c0aa49a..921f81c 100644 --- a/src/engine/ScriptSandboxWorker.js +++ b/src/engine/ScriptSandboxWorker.js @@ -3029,6 +3029,16 @@ const game = { const pos = _normFxPoint(position); _send('fx.damageFloater', { position: pos, value, opts: opts || {} }); }, + /** + * Авто-floater'ы над мобами (NPC) при потере HP (задача 40 доп). + * Включил один раз — и любой урон по NPC (от бластера, скрипта, врага) + * сам показывает облачко «-N» над целью. + * game.fx.autoMobFloaters(true); + * game.fx.autoMobFloaters(true, { color:'#ff5a4a' }); + */ + autoMobFloaters(enabled, opts) { + _send('fx.autoMobFloaters', { enabled: enabled !== false, opts: opts || {} }); + }, /** * Луч между двумя точками. opts: { from, to — {x,y,z} или ref * объекта (тогда луч следит за ним); color: '#hex', width }.