diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 15a896b..af975dc 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -1731,6 +1731,11 @@ export class GameRuntime { }); return; } + if (cmd === 'npc.setAttacking') { + this._npcCmd(payload?.ref, (nid) => + this.scene3d?.npcManager?.setAttacking?.(nid, !!payload?.on)); + return; + } if (cmd === 'npc.stop') { this._npcCmd(payload?.ref, (nid) => this.scene3d?.npcManager?.stopNpc(nid)); @@ -3577,8 +3582,13 @@ export class GameRuntime { } if (cmd === 'scene.setVisible') { try { - const kind = payload?.kind; - const id = payload?.id; + let kind = payload?.kind; + let id = payload?.id; + // obj.visible=false шлёт {ref:'primitive:N'} без kind/id — парсим ref. + if ((kind == null || id == null) && typeof payload?.ref === 'string') { + const colon = payload.ref.indexOf(':'); + if (colon > 0) { kind = payload.ref.slice(0, colon); id = payload.ref.slice(colon + 1); } + } const visible = !!payload?.visible; if (id == null) return; if (kind === 'primitive') { diff --git a/src/editor/engine/GameplayKits.js b/src/editor/engine/GameplayKits.js index 4736879..b120af7 100644 --- a/src/editor/engine/GameplayKits.js +++ b/src/editor/engine/GameplayKits.js @@ -765,17 +765,19 @@ game.self.onClick(() => { // Невидимый триггер-якорь; рядом спавнится NPC-враг. prims: [{ type: 'cube', x: 0, y: 1, z: 0, sx: 1, sy: 2, sz: 1, color: '#7a2030', material: 'matte', visible: false, canCollide: false, name: 'Якорь врага' }], scripts: [{ attachTo: 'on-target', code: -`// Враг-персонаж: спавним NPC, он преследует игрока и бьёт при касании. +`// Враг-персонаж: преследует игрока, бьёт с анимацией удара при сближении. const p = game.self.position; -const enemy = game.scene.spawnNpc('skin_retro-zombie', { x: p.x, z: p.z, name: 'Враг', hp: 100, speed: 3 }); +const enemy = game.scene.spawnNpc('skin_retro-zombie', { x: p.x, z: p.z, name: 'Враг', hp: 100, speed: 3.5 }); if (enemy && enemy.follow) enemy.follow('player'); -let cd = 0; +let cd = 0, atk = false; game.onTick((dt) => { if (!enemy || !enemy.position) return; cd -= dt; const pl = game.player.position, e = enemy.position; const d = Math.sqrt((pl.x-e.x)**2 + (pl.z-e.z)**2); - if (d < 2.5 && cd <= 0) { game.player.damage(10); cd = 1; } // удар раз в секунду + const inRange = d < 3.5; + if (inRange !== atk) { atk = inRange; enemy.setAttacking && enemy.setAttacking(inRange); } + if (inRange && cd <= 0) { game.player.damage(10); cd = 1; } // удар раз в секунду });` }], }, { @@ -793,7 +795,7 @@ function wave(){ if (e){ if (e.follow) e.follow('player'); enemies.push({ npc:e, cd:0 }); } } } game.after(2, wave); game.every(5, wave); -// Урон игроку при касании любого врага (кулдаун у каждого свой). +// Урон + анимация удара при сближении (у каждого врага свой кулдаун). game.onTick((dt) => { const pl = game.player.position; for (const en of enemies){ @@ -801,7 +803,9 @@ game.onTick((dt) => { en.cd -= dt; const e = en.npc.position; const d = Math.sqrt((pl.x-e.x)**2 + (pl.z-e.z)**2); - if (d < 2.5 && en.cd <= 0){ game.player.damage(8); en.cd = 1; } + const inRange = d < 3.5; + if (inRange !== en.atk){ en.atk = inRange; en.npc.setAttacking && en.npc.setAttacking(inRange); } + if (inRange && en.cd <= 0){ game.player.damage(8); en.cd = 1; } } });` }], }, diff --git a/src/editor/engine/NpcManager.js b/src/editor/engine/NpcManager.js index 773d3c9..d1a2506 100644 --- a/src/editor/engine/NpcManager.js +++ b/src/editor/engine/NpcManager.js @@ -274,6 +274,12 @@ export class NpcManager { npc.isMoving = false; } + /** Включить/выключить анимацию атаки (R15-NPC машет руками). */ + setAttacking(id, on) { + const npc = this.npcs.get(Number(id)); + if (npc) npc.attacking = !!on; + } + /** Реплика над головой NPC на duration секунд. */ say(id, text, duration = 3) { const npc = this.npcs.get(Number(id)); @@ -403,10 +409,10 @@ export class NpcManager { root.rotation.z = lean; // data.x/y/z — чтобы scene.find/getPosition видели NPC. data.x = npc.x; data.y = npc.y; data.z = npc.z; - // R15-NPC (skin_*): процедурная анимация бега/покоя через R15Animator. + // R15-NPC (skin_*): процедурная анимация бега/покоя/атаки через R15Animator. if (npc.r15Animator) { try { - npc.r15Animator.setState(moving ? 'run' : 'idle'); + npc.r15Animator.setState(npc.attacking ? 'attack' : (moving ? 'run' : 'idle')); npc.r15Animator.update(dt); } catch (e) { /* ignore */ } } diff --git a/src/editor/engine/R15Animator.js b/src/editor/engine/R15Animator.js index 6d373ca..f570567 100644 --- a/src/editor/engine/R15Animator.js +++ b/src/editor/engine/R15Animator.js @@ -131,6 +131,23 @@ const ANIMS_STD = { { bone: B.SPINE, axis: AXIS_RIGHT, angleDeg: 10, times: [0.0, 0.4, 0.8], values: [0.0, 1.0, 0.0] }, ]), + // Удар правой рукой вперёд (для враждебных NPC). loop=true — постоянно + // машет, пока NPC в режиме атаки. + attack: makeAnim(0.5, true, [ + // Правая рука выбрасывается вперёд (замах назад → удар). + { bone: B.RIGHT_ARM, axis: AXIS_RIGHT, angleDeg: -95, + times: [0.0, 0.15, 0.3, 0.5], values: [0.3, 1.0, 0.2, 0.3] }, + { bone: B.RIGHT_FOREARM, axis: AXIS_RIGHT, angleDeg: -50, + times: [0.0, 0.15, 0.3, 0.5], values: [1.0, 0.2, 1.0, 1.0] }, + // Левая рука тоже в боевой стойке. + { bone: B.LEFT_ARM, axis: AXIS_RIGHT, angleDeg: -45, + times: [0.0, 0.5], values: [1.0, 1.0] }, + { bone: B.LEFT_FOREARM, axis: AXIS_RIGHT, angleDeg: -70, + times: [0.0, 0.5], values: [1.0, 1.0] }, + // Корпус подаётся в удар. + { bone: B.SPINE, axis: AXIS_RIGHT, angleDeg: -12, + times: [0.0, 0.15, 0.3, 0.5], values: [0.0, 1.0, 0.3, 0.0] }, + ]), // === ЭМОЦИИ (game.player.playAnimation) === // Разовые анимации поверх авто-состояния. loop=false — играют один раз, diff --git a/src/editor/engine/ScriptSandboxWorker.js b/src/editor/engine/ScriptSandboxWorker.js index 1b8b98f..0cdfe10 100644 --- a/src/editor/engine/ScriptSandboxWorker.js +++ b/src/editor/engine/ScriptSandboxWorker.js @@ -229,6 +229,10 @@ function _makeNpcProxy(ref) { damage(amount) { _send('npc.damage', { ref, amount: Number(amount) || 0 }); }, + /** Включить/выключить анимацию атаки (удары руками). */ + setAttacking(on) { + _send('npc.setAttacking', { ref, on: !!on }); + }, /** Убрать NPC со сцены. */ remove() { _send('npc.remove', { ref });