feat(studio): авто-floater над мобами + урон NPC от оружия (задача 40 доп)

game.fx.autoMobFloaters(true) — включает облачка урона над NPC при любой потере
HP (NpcManager.damage). NpcManager.damageByMesh — оружие (бластер/меч) наносит
урон скриптовым NPC (weapons.setOnHit → npcManager.damageByMesh). Связка:
выстрел бластера → урон NPC → авто-floater «-N» над целью.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-07 13:41:41 +03:00
parent c93070170b
commit 854074bfa2
4 changed files with 47 additions and 1 deletions

View File

@ -6229,6 +6229,10 @@ export class BabylonScene {
if (hit?.mesh && this.zombieManager) { if (hit?.mesh && this.zombieManager) {
this.zombieManager.damageByMesh(hit.mesh, hit.damage || 25); 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) { if (this._onWeaponHit) {
try { this._onWeaponHit(hit); } catch (e) {} try { this._onWeaponHit(hit); } catch (e) {}
} }

View File

@ -1910,6 +1910,15 @@ export class GameRuntime {
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
return; return;
} }
if (cmd === 'fx.autoMobFloaters') {
try {
if (this.scene3d?.npcManager) {
this.scene3d.npcManager._autoFloater = payload?.enabled
? { opts: payload?.opts || {} } : null;
}
} catch (e) { /* ignore */ }
return;
}
// === Beam / Trail — лучи и следы (Фаза 5.2) === // === Beam / Trail — лучи и следы (Фаза 5.2) ===
if (cmd === 'fx.create') { if (cmd === 'fx.create') {

View File

@ -292,10 +292,36 @@ export class NpcManager {
damage(id, amount) { damage(id, amount) {
const npc = this.npcs.get(Number(id)); const npc = this.npcs.get(Number(id));
if (!npc || npc.dead) return; 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); 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++) {
if (m === root) { hit = true; break; }
m = m.parent;
}
if (hit) { this.damage(npc.id, amount); return true; }
}
return false;
}
/** Удалить NPC по id (без эффекта смерти — просто убрать). */ /** Удалить NPC по id (без эффекта смерти — просто убрать). */
removeNpc(id) { removeNpc(id) {
const npc = this.npcs.get(Number(id)); const npc = this.npcs.get(Number(id));

View File

@ -3455,6 +3455,13 @@ const game = {
const pos = _normFxPoint(position); const pos = _normFxPoint(position);
_send('fx.damageFloater', { position: pos, value, opts: opts || {} }); _send('fx.damageFloater', { position: pos, value, opts: opts || {} });
}, },
/**
* Авто-floater'ы над мобами (NPC) при потере HP. Включил один раз любой
* урон по NPC сам показывает облачко «-N». game.fx.autoMobFloaters(true).
*/
autoMobFloaters(enabled, opts) {
_send('fx.autoMobFloaters', { enabled: enabled !== false, opts: opts || {} });
},
/** /**
* Луч между двумя точками. opts: { from, to {x,y,z} или ref * Луч между двумя точками. opts: { from, to {x,y,z} или ref
* объекта (тогда луч следит за ним); color: '#hex', width }. * объекта (тогда луч следит за ним); color: '#hex', width }.