feedback3 WD: a skill with its own idle clip stands in it between attacks, its loop only on attacks (community report #23, 折桠 S2)
This commit is contained in:
1 parent
bdb0765c55
commit
5a8c271d19
2 files changed
+115
-4
No files matched your search
@@ -13,7 +13,10 @@
|
||||
// GitHub #58): every attack plays the clip once — at its own speed, faster
|
||||
// only when the attacks come quicker than the clip —, then base (Move while
|
||||
// the sim walks it: it stands for that clip, server/sim/ai.js attackStand)
|
||||
// setSkill(on) skill begin→loop while active (skill idle replaces idle), end on stop
|
||||
// setSkill(on) skill begin, then — when the skill has an idle clip of its own (skill.idle,
|
||||
// not its loop) — that idle between attacks, its loop (the skill's attack clip)
|
||||
// only on attacks (community report #23: 折桠's S2 jump attack looped with no
|
||||
// enemy engaged); a skill without one keeps its loop as the stance; end on stop
|
||||
// deploy() 'Start' once, then base
|
||||
// die() die clip once (callers fade out afterwards); a skeleton without one holds its
|
||||
// idle clip's first frame (GitHub issue #25: the attack loop went on)
|
||||
@@ -24,7 +27,9 @@
|
||||
// 重生), landing in `roles`
|
||||
// update(dt) advances the skeleton (autoUpdate is off: one clock for everything)
|
||||
// Attack mode lasts until ~1.4 attack intervals without a new attack (a `once` cast and every attack of a
|
||||
// `clipPerAttack` actor: to the end of its clip), then the end clip (if any) and base.
|
||||
// `clipPerAttack` actor: to the end of its clip), then the end clip (if any) and base — except the attacks of a skill
|
||||
// with its own idle clip, which go straight back to that idle: the skill's end clip closes the skill, not each spell of
|
||||
// attacks while it runs.
|
||||
|
||||
const clampN = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
|
||||
|
||||
@@ -327,6 +332,17 @@ export class SpineActor {
|
||||
return !!sk && sk.loop === this.roles.idle;
|
||||
}
|
||||
|
||||
/**
|
||||
* The running skill's own idle clip when its attacks are its loop clip (anims skill.idle ≠ skill.loop: 折桠's
|
||||
* Skill_2_Idle beside the Skill_2_Loop jump attack, 史尔特尔's Skill_3_Idle, 耀骑士临光's Skill_3_Idle …): the pose
|
||||
* between its attacks. null otherwise — no skill on, no idle clip, or the loop is that idle (蕾缪安's S2 / S3).
|
||||
*/
|
||||
_skillIdle() {
|
||||
const sk = this.roles.skill;
|
||||
if (!this.skillOn || !sk || sk.idle === sk.loop || !this.has(sk.idle)) return null;
|
||||
return this._attackClip() === sk ? sk.idle : null;
|
||||
}
|
||||
|
||||
_hitTime(anim, dur) {
|
||||
const hits = this.entry.hits && this.entry.hits[anim];
|
||||
if (Array.isArray(hits) && hits.length && Number.isFinite(hits[0])) return clampN(hits[0], 0, dur);
|
||||
@@ -345,7 +361,9 @@ export class SpineActor {
|
||||
this.mode = 'skillBegin';
|
||||
this._play(sk.begin, false, { mix: 0.08 });
|
||||
this.skillBeginUntil = this.clock + this.dur(sk.begin);
|
||||
if (this.has(sk.loop)) this._queue(sk.loop, true);
|
||||
// then the skill's own idle until an attack plays its loop (community report #23); without one, the loop
|
||||
const next = this._skillIdle() || (this.has(sk.loop) ? sk.loop : null);
|
||||
if (next) this._queue(next, true);
|
||||
} else if (this.mode === 'base') this._play(this._baseName(), true);
|
||||
} else if (!on && sk) {
|
||||
if (this.has(sk.end)) {
|
||||
@@ -429,7 +447,9 @@ export class SpineActor {
|
||||
this.mode = 'base';
|
||||
this.wound = false;
|
||||
const clip = this._attackClip() || this.roles.attack;
|
||||
if (clip && this.has(clip.end)) { this._play(clip.end, false); this._queue(this._baseName(), true); }
|
||||
// a skill with its own idle: back to that idle (its end clip is for the end of the skill, setSkill(false))
|
||||
const toSkillIdle = clip === this.roles.skill && !!this._skillIdle();
|
||||
if (clip && this.has(clip.end) && !toSkillIdle) { this._play(clip.end, false); this._queue(this._baseName(), true); }
|
||||
else this._play(this._baseName(), true, { mix: 0.15 });
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// test/render/feedback3-skill-idle.test.js — community report #23 (0.1.3): "折娅开了技能就会不停做跳起来打人的动作,哪怕没有接敌".
|
||||
// 折桠's S2 model (char_4207_branch) has Skill_2_Begin, Skill_2_Loop (the jump attack, OnAttack at 0.8 s), Skill_2_End
|
||||
// and Skill_2_Idle; the actor queued the loop after the begin clip, so it jumped with nobody to hit (the sim made no
|
||||
// attack). Now a skill with an idle clip of its own (anims skill.idle ≠ skill.loop) stands in that idle between attacks,
|
||||
// plays the loop only on attacks, and goes back to the idle after a spell of attacks (its End clip only when the skill
|
||||
// ends). Skills whose loop IS their idle (蕾缪安 S3) and skills without an idle (宴) are unchanged. Real manifest entries,
|
||||
// headless fake PIXI (test/render/fakepixi.js).
|
||||
|
||||
import { test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { installFakePixi } from './fakepixi.js';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const assets = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8'));
|
||||
|
||||
let fake, SpineActor;
|
||||
before(async () => {
|
||||
fake = installFakePixi();
|
||||
({ SpineActor } = await import('../../public/js/render/spine.js'));
|
||||
});
|
||||
after(() => fake.restore());
|
||||
|
||||
/** An actor of a real operator's Front model with its equipped skill, recording every clip set / queued. */
|
||||
function actor(charId, skillIndex) {
|
||||
const entry = assets.chars[charId].spine.front;
|
||||
const a = new SpineActor({ animations: Object.keys(entry.animations).map((name) => ({ name })) }, entry);
|
||||
a.setSkillIndex(skillIndex);
|
||||
a.log = [];
|
||||
const st = a.spine.state;
|
||||
const set = st.setAnimation, add = st.addAnimation;
|
||||
st.setAnimation = (i, name, loop) => { a.log.push(['set', name, !!loop]); return set(i, name, loop); };
|
||||
st.addAnimation = (i, name, loop) => { a.log.push(['queue', name, !!loop]); return add(i, name, loop); };
|
||||
return a;
|
||||
}
|
||||
const run = (a, s, dt = 1 / 60) => { for (let t = 0; t < s - 1e-9; t += dt) a.update(dt); };
|
||||
const names = (a) => a.log.map(([, n]) => n);
|
||||
|
||||
test('折桠 S2: begin, then Skill_2_Idle — the jump attack only on attacks, back to the idle after them, End only when the skill ends', () => {
|
||||
const a = actor('char_4207_branch', 1);
|
||||
assert.deepEqual([a.roles.skill.loop, a.roles.skill.idle], ['Skill_2_Loop', 'Skill_2_Idle'], 'the manifest roles');
|
||||
a.setSkill(true);
|
||||
assert.deepEqual(a.log, [['set', 'Skill_2_Begin', false], ['queue', 'Skill_2_Idle', true]], 'no Skill_2_Loop queued after the begin clip');
|
||||
run(a, 3);
|
||||
assert.equal(a.mode, 'base');
|
||||
assert.ok(!names(a).includes('Skill_2_Loop'), 'no attack: no jump attack');
|
||||
// she is hit, an enemy stands on her tile: attacks every 1.5 s
|
||||
a.log.length = 0;
|
||||
a.attack(1.5);
|
||||
assert.equal(a.current, 'Skill_2_Loop', 'the attack plays the skill loop');
|
||||
run(a, 1.5); a.attack(1.5);
|
||||
run(a, 1.5 * 1.4 + 0.1);
|
||||
assert.equal(a.mode, 'base');
|
||||
assert.equal(a.current, 'Skill_2_Idle', 'the enemy is gone: back to the skill idle');
|
||||
assert.ok(!names(a).includes('Skill_2_End'), 'the skill End clip is not played between attacks');
|
||||
a.setSkill(false);
|
||||
assert.equal(a.current, 'Skill_2_End', 'the skill ends on its End clip');
|
||||
run(a, 0.2);
|
||||
assert.equal(a.current, 'Idle');
|
||||
});
|
||||
|
||||
test('the other skills with an own idle clip follow the same rule (史尔特尔 S3 begin → Skill_3_Idle); 耀骑士临光 S3 (no begin) already idled', () => {
|
||||
const s = actor('char_350_surtr', 2);
|
||||
s.setSkill(true);
|
||||
assert.deepEqual(s.log, [['set', 'Skill_3_Begin', false], ['queue', 'Skill_3_Idle', true]]);
|
||||
s.attack(1.25);
|
||||
assert.equal(s.current, 'Skill_3_Loop');
|
||||
const n = actor('char_1014_nearl2', 2);
|
||||
n.setSkill(true);
|
||||
assert.deepEqual(n.log, [['set', 'Skill_3_Idle', true]], 'unchanged: the skill idle at once');
|
||||
n.attack(1.2);
|
||||
assert.equal(n.current, 'Skill_3');
|
||||
run(n, 1.2 * 1.4 + 0.1);
|
||||
assert.equal(n.current, 'Skill_3_Idle');
|
||||
});
|
||||
|
||||
test('unchanged: a loop that is the skill idle (蕾缪安 S3) and a skill without an idle clip (宴: its loop stays the stance)', () => {
|
||||
const l = actor('char_4193_lemuen', 2);
|
||||
l.setSkill(true);
|
||||
assert.deepEqual(l.log, [['set', 'Skill_3_Begin', false], ['queue', 'Skill_3_Idle', true]]);
|
||||
const u = actor('char_337_utage', 1);
|
||||
assert.equal(u.roles.skill.idle, null);
|
||||
u.setSkill(true);
|
||||
assert.deepEqual(u.log, [['set', 'Skill_Start', false], ['queue', 'Skill_Loop', true]], 'begin → loop, as before');
|
||||
u.log.length = 0;
|
||||
u.attack(1.2);
|
||||
run(u, 1.2 * 1.4 + 0.1);
|
||||
assert.deepEqual(names(u).slice(-2), ['Skill_End', 'Idle'], 'its spell of attacks still ends as before');
|
||||
});
|
||||
Reference in new issue
Block a user