From 6326dd4d0b2a944ef4b1c2c60806175b07ba1ed5 Mon Sep 17 00:00:00 2001 From: sganggs Date: Thu, 8 Oct 2026 19:20:55 +0800 Subject: [PATCH] =?UTF-8?q?feedback7=20R22-RENDER:=20an=20attack=20seen=20?= =?UTF-8?q?late=20plays=20its=20whole=20clip=20from=20the=20wind-up=20when?= =?UTF-8?q?=20the=20rhythm=20leaves=20room=20for=20it=20=E2=80=94=20?= =?UTF-8?q?=E9=87=8D=E7=8A=AF's=20iron=20ball=20lifts=20before=20the=20sla?= =?UTF-8?q?m=20(#246=20by=20@TsangAsuna,=20accepted=20by=20the=20owner);?= =?UTF-8?q?=20golden:=20unchanged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpineActor.attack() starts an attack that was not wound up (no look-ahead: a batch that arrived late) on its strike frame — right for a quick rhythm, but for a slow clip-per-attack enemy the wind-up was cut out: 重犯 (Attack_grey 2.333 s, strike at 1.0 s, attacks every 3 s) showed the slam without the ball ever rising, and only 1.33 s of the clip. The owner's decision of 2026-10-07 on #246: re-implement on the current spine.js so that, when the attack interval leaves room for the whole clip, it plays once completely from the start. A clip-per-attack actor (enemies) with interval ≥ the clip now plays it from 0 at its own speed for the whole clip, the strike landing a little late; a quicker rhythm keeps the strike-frame start, and operators' looping attacks are unchanged (the PR's condition had it the other way round: it changed the looping attacks and left out the clip-per-attack enemies it was written for). Tests: test/render/feedback2-enemy-attack-clip.test.js (+1: 重犯 plays the whole clip from its wind-up, a quicker rhythm and an operator start on the strike frame) 5/5; render eyemask, unitview, texas skill clip, skill idle 51/51. --- public/js/render/spine.js | 10 ++++++--- .../feedback2-enemy-attack-clip.test.js | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/public/js/render/spine.js b/public/js/render/spine.js index d86bce68..e805074e 100644 --- a/public/js/render/spine.js +++ b/public/js/render/spine.js @@ -367,9 +367,13 @@ export class SpineActor { this.windUntil = null; this.wound = false; if (!wasAttacking) { - // not wound up (no look-ahead, e.g. a batch that arrived late): the sim already resolved the hit, so show - // the strike frame now - this._play(clip.loop, !single, { timeScale: ts, start: hit, mix: 0.06 }); + // not wound up (no look-ahead, e.g. a batch that arrived late): the sim already resolved the hit, so show the + // strike frame now — unless a clip-per-attack actor's rhythm leaves room for the whole clip (interval ≥ clip): + // then the clip plays complete from its wind-up at its own speed, the strike a little late (重犯's iron ball + // lifts before the slam; #246 by @TsangAsuna, accepted by the owner on 2026-10-07) + const whole = per && this.interval >= loopDur; + if (whole) this.attackUntil = this.clock + loopDur / ts; + this._play(clip.loop, !single, { timeScale: ts, start: whole ? 0 : hit, mix: 0.06 }); } else if (single) { const e = this.spine.state.tracks[0]; if (e) e.timeScale = ts; diff --git a/test/render/feedback2-enemy-attack-clip.test.js b/test/render/feedback2-enemy-attack-clip.test.js index 999877a1..5c560624 100644 --- a/test/render/feedback2-enemy-attack-clip.test.js +++ b/test/render/feedback2-enemy-attack-clip.test.js @@ -71,6 +71,27 @@ test('an enemy attacking quicker than its clip plays it faster, one clip per att assert.equal(a.current, 'Move', 'the rest of the clip at 2×: 0.23 s'); }); +// #246 (by @TsangAsuna, accepted by the owner on 2026-10-07): seen late, an attack whose rhythm leaves room for the whole +// clip plays it complete from the wind-up — 重犯's iron ball lifts before the slam instead of the slam appearing alone +test('an attack seen late plays the whole clip from its wind-up when the rhythm leaves room for it (重犯), else from the strike frame', () => { + const LIFBOS = 'enemy_1121_lifbos'; // 重犯: Attack_grey 2.333 s, strike at 1.0 s; attacks every 3 s + const a = actor(LIFBOS, true); + a.attack(3); + assert.equal(a.current, 'Attack_grey'); + assert.equal(track(a).trackTime, 0, 'from the wind-up (the ball lifts), not the 1.0 s strike frame'); + assert.equal(track(a).timeScale, 1, 'at its own speed'); + run(a, 2.25); + assert.equal(a.current, 'Attack_grey', 'the whole 2.333 s clip plays (it used to end 1.333 s after the strike frame)'); + run(a, 0.15); + assert.equal(a.current, 'Move_grey', 'then the resting clip'); + const quick = actor(LIFBOS, true); + quick.attack(2); // quicker than the clip: no room for the wind-up + assert.ok(Math.abs(track(quick).trackTime - 1) < 1e-9, 'from the strike frame'); + const op = actor(LIFBOS, false); + op.attack(3); // an operator's looping attack is unchanged + assert.ok(Math.abs(track(op).trackTime - 1) < 1e-9); +}); + test('an operator keeps its attack loop stretched over the attack rhythm (unchanged)', () => { const a = actor(JSHOOT, false); a.attack(2.7);