fix(studio): враг бьёт с анимацией удара, ключ исчезает при подборе
1+2) Враг с HP / волна врагов: радиус удара 3.5 (NPC останавливался на
followGap=2.5, урон-чек d<2.5 не срабатывал). Добавлена анимация атаки —
R15Animator.attack (выпад рукой), npc.setAttacking, NpcManager.setAttacking.
3) Ключ исчезает при подборе: scene.setVisible теперь парсит ref ('primitive:N')
— obj.visible=false слал {ref} без kind/id, поэтому ключ не пропадал.
4) Машина — остаётся рантайм vehicle:car (особенность транспорта).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c4d184257b
commit
cf34f9cdb6
@ -1731,6 +1731,11 @@ export class GameRuntime {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cmd === 'npc.setAttacking') {
|
||||||
|
this._npcCmd(payload?.ref, (nid) =>
|
||||||
|
this.scene3d?.npcManager?.setAttacking?.(nid, !!payload?.on));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (cmd === 'npc.stop') {
|
if (cmd === 'npc.stop') {
|
||||||
this._npcCmd(payload?.ref, (nid) =>
|
this._npcCmd(payload?.ref, (nid) =>
|
||||||
this.scene3d?.npcManager?.stopNpc(nid));
|
this.scene3d?.npcManager?.stopNpc(nid));
|
||||||
@ -3577,8 +3582,13 @@ export class GameRuntime {
|
|||||||
}
|
}
|
||||||
if (cmd === 'scene.setVisible') {
|
if (cmd === 'scene.setVisible') {
|
||||||
try {
|
try {
|
||||||
const kind = payload?.kind;
|
let kind = payload?.kind;
|
||||||
const id = payload?.id;
|
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;
|
const visible = !!payload?.visible;
|
||||||
if (id == null) return;
|
if (id == null) return;
|
||||||
if (kind === 'primitive') {
|
if (kind === 'primitive') {
|
||||||
|
|||||||
@ -765,17 +765,19 @@ game.self.onClick(() => {
|
|||||||
// Невидимый триггер-якорь; рядом спавнится NPC-враг.
|
// Невидимый триггер-якорь; рядом спавнится 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: 'Якорь врага' }],
|
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:
|
scripts: [{ attachTo: 'on-target', code:
|
||||||
`// Враг-персонаж: спавним NPC, он преследует игрока и бьёт при касании.
|
`// Враг-персонаж: преследует игрока, бьёт с анимацией удара при сближении.
|
||||||
const p = game.self.position;
|
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');
|
if (enemy && enemy.follow) enemy.follow('player');
|
||||||
let cd = 0;
|
let cd = 0, atk = false;
|
||||||
game.onTick((dt) => {
|
game.onTick((dt) => {
|
||||||
if (!enemy || !enemy.position) return;
|
if (!enemy || !enemy.position) return;
|
||||||
cd -= dt;
|
cd -= dt;
|
||||||
const pl = game.player.position, e = enemy.position;
|
const pl = game.player.position, e = enemy.position;
|
||||||
const d = Math.sqrt((pl.x-e.x)**2 + (pl.z-e.z)**2);
|
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 }); } }
|
if (e){ if (e.follow) e.follow('player'); enemies.push({ npc:e, cd:0 }); } }
|
||||||
}
|
}
|
||||||
game.after(2, wave); game.every(5, wave);
|
game.after(2, wave); game.every(5, wave);
|
||||||
// Урон игроку при касании любого врага (кулдаун у каждого свой).
|
// Урон + анимация удара при сближении (у каждого врага свой кулдаун).
|
||||||
game.onTick((dt) => {
|
game.onTick((dt) => {
|
||||||
const pl = game.player.position;
|
const pl = game.player.position;
|
||||||
for (const en of enemies){
|
for (const en of enemies){
|
||||||
@ -801,7 +803,9 @@ game.onTick((dt) => {
|
|||||||
en.cd -= dt;
|
en.cd -= dt;
|
||||||
const e = en.npc.position;
|
const e = en.npc.position;
|
||||||
const d = Math.sqrt((pl.x-e.x)**2 + (pl.z-e.z)**2);
|
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; }
|
||||||
}
|
}
|
||||||
});` }],
|
});` }],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -274,6 +274,12 @@ export class NpcManager {
|
|||||||
npc.isMoving = false;
|
npc.isMoving = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Включить/выключить анимацию атаки (R15-NPC машет руками). */
|
||||||
|
setAttacking(id, on) {
|
||||||
|
const npc = this.npcs.get(Number(id));
|
||||||
|
if (npc) npc.attacking = !!on;
|
||||||
|
}
|
||||||
|
|
||||||
/** Реплика над головой NPC на duration секунд. */
|
/** Реплика над головой NPC на duration секунд. */
|
||||||
say(id, text, duration = 3) {
|
say(id, text, duration = 3) {
|
||||||
const npc = this.npcs.get(Number(id));
|
const npc = this.npcs.get(Number(id));
|
||||||
@ -403,10 +409,10 @@ export class NpcManager {
|
|||||||
root.rotation.z = lean;
|
root.rotation.z = lean;
|
||||||
// data.x/y/z — чтобы scene.find/getPosition видели NPC.
|
// data.x/y/z — чтобы scene.find/getPosition видели NPC.
|
||||||
data.x = npc.x; data.y = npc.y; data.z = npc.z;
|
data.x = npc.x; data.y = npc.y; data.z = npc.z;
|
||||||
// R15-NPC (skin_*): процедурная анимация бега/покоя через R15Animator.
|
// R15-NPC (skin_*): процедурная анимация бега/покоя/атаки через R15Animator.
|
||||||
if (npc.r15Animator) {
|
if (npc.r15Animator) {
|
||||||
try {
|
try {
|
||||||
npc.r15Animator.setState(moving ? 'run' : 'idle');
|
npc.r15Animator.setState(npc.attacking ? 'attack' : (moving ? 'run' : 'idle'));
|
||||||
npc.r15Animator.update(dt);
|
npc.r15Animator.update(dt);
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,6 +131,23 @@ const ANIMS_STD = {
|
|||||||
{ bone: B.SPINE, axis: AXIS_RIGHT, angleDeg: 10,
|
{ bone: B.SPINE, axis: AXIS_RIGHT, angleDeg: 10,
|
||||||
times: [0.0, 0.4, 0.8], values: [0.0, 1.0, 0.0] },
|
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) ===
|
// === ЭМОЦИИ (game.player.playAnimation) ===
|
||||||
// Разовые анимации поверх авто-состояния. loop=false — играют один раз,
|
// Разовые анимации поверх авто-состояния. loop=false — играют один раз,
|
||||||
|
|||||||
@ -229,6 +229,10 @@ function _makeNpcProxy(ref) {
|
|||||||
damage(amount) {
|
damage(amount) {
|
||||||
_send('npc.damage', { ref, amount: Number(amount) || 0 });
|
_send('npc.damage', { ref, amount: Number(amount) || 0 });
|
||||||
},
|
},
|
||||||
|
/** Включить/выключить анимацию атаки (удары руками). */
|
||||||
|
setAttacking(on) {
|
||||||
|
_send('npc.setAttacking', { ref, on: !!on });
|
||||||
|
},
|
||||||
/** Убрать NPC со сцены. */
|
/** Убрать NPC со сцены. */
|
||||||
remove() {
|
remove() {
|
||||||
_send('npc.remove', { ref });
|
_send('npc.remove', { ref });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user