Files
ark-stronghold-protocol/test/render/feedback3-skill-idle.test.js
T

97 lines
5.0 KiB
JavaScript

// 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 (宴 S1) 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 (宴 S1: 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]]);
// 宴's S1 分神 (index 0: Skill_Start / Loop / End, no idle — her sit-down rest); her S2 plays no skill clip since
// 0.2.0 (tools/assets/spine.mjs PREFAB_SPINE_ROLES, DESIGN §25.22.9)
const u2 = actor('char_337_utage', 1);
u2.setSkill(true);
assert.deepEqual(u2.log.filter((x) => /^Skill/.test(x[1])), [], '宴 S2: no skill clip');
const u = actor('char_337_utage', 0);
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');
});