feedback6 R21F: enemy animation states, the client part of PR #275 by @xcdoge re-implemented — the resolver gives an enemy every numbered skill clip (anims.skills; the caller's first index stays the primary, operators unchanged, 宴's PREFAB_SPINE_ROLES fix kept), the stun spellings Stun_1 and Dizzy_Begin/Loop/End, a separate run role (Run_Begin/Loop/End); the renderer walks an enemy faster than moveSpeed 1 on its Run cycle (猎狗pro, 深池侦察犬 ×2; [ASSUMED] threshold), swaps an enemy's skill clip on a cast event (UnitView.setSkillSlot; a form in force is kept), and plays the 重生 of 巨大的丑东西 (Revive, then the fleeing 大祭司 Idle_2 / Move_2 / Stun_2) and 自在 (Revive_01); data/assets.json regenerated offline: 18 enemies gain anims.skills, 6 anims.run, 3 a stun clip (巨大的丑东西, 斩胄之剑, 破胄之锤), nothing else; the sim's cast slot (a new event) is not part of it; golden: unchanged
This commit is contained in:
1 parent
14df10a5a3
commit
76a2db7dd0
11 files changed
+315
-19
No files matched your search
@@ -77,6 +77,92 @@ describe('animation-role resolver (research 07 §5.4)', () => {
|
||||
assert.deepEqual(r.skill, { begin: 'Skill2_Begin', loop: 'Skill2_Loop', end: 'Skill2_End', index: 1, idle: null });
|
||||
});
|
||||
|
||||
// PR #275 (@xcdoge): an enemy's manifest only ever had skill index 0 (plan.mjs passes [0]), so a multi-skill boss could
|
||||
// show one cast clip — 盐风主教昆图斯 has Skill_01..04. With `numberedSkills` (spine.mjs: enemy models) every numbered
|
||||
// skill clip is resolved after the caller's indices, whose first stays the primary.
|
||||
test('numbered skill clips of an enemy are all resolved (a multi-skill boss), the primary stays the caller’s first', () => {
|
||||
const names = ['Attack', 'Default', 'Die', 'Idle', 'Skill_01', 'Skill_02', 'Skill_03', 'Skill_04'];
|
||||
assert.equal(resolveRoles(names, { skillIndices: [0] }).skills, undefined, 'without the option: index 0 only (operators)');
|
||||
const r = resolveRoles(names, { skillIndices: [0], numberedSkills: true });
|
||||
assert.deepEqual(Object.keys(r.skills).sort(), ['0', '1', '2', '3']);
|
||||
assert.equal(r.skills['1'].loop, 'Skill_02');
|
||||
assert.equal(r.skills['3'].loop, 'Skill_04');
|
||||
assert.equal(r.skill.loop, 'Skill_01', 'the first index stays the primary clip');
|
||||
const first = resolveRoles(names, { skillIndices: [2], numberedSkills: true });
|
||||
assert.equal(first.skill.index, 2, 'a caller-declared primary still wins');
|
||||
assert.equal(first.skill.loop, 'Skill_03');
|
||||
// 自在: Skill_01 / Skill_01_02 are one slot, Skill_02_Begin / Loop / End the next
|
||||
const xi = resolveRoles(['Attack_01', 'Die', 'Idle', 'Skill_01', 'Skill_01_02', 'Skill_02_Begin', 'Skill_02_End', 'Skill_02_Loop'], { numberedSkills: true });
|
||||
assert.deepEqual(xi.skills['1'], { begin: 'Skill_02_Begin', loop: 'Skill_02_Loop', end: 'Skill_02_End', index: 1, idle: null });
|
||||
// phase-style skill clips (no index in the name) stay a single primary skill
|
||||
assert.equal(resolveRoles(['Attack', 'Die', 'Idle', 'Skill_Begin', 'Skill_Loop', 'Skill_End'], { numberedSkills: true }).skills, undefined);
|
||||
});
|
||||
|
||||
// PR #275: the stun family is spelled Stun / Stun_End, Stun_1 / Stun_2 (巨大的丑东西: first form, second form) and
|
||||
// Dizzy_Begin / Dizzy_Loop / Dizzy_End (斩胄之剑 / 破胄之锤); without a role a stunned enemy freezes its clip.
|
||||
test('stun clips: numbered Stun_N and the Dizzy_* family are recognised', () => {
|
||||
const mc = resolveRoles(['Attack_1', 'Die', 'Idle_1', 'Idle_2', 'Move_1', 'Move_2', 'Stun_1', 'Stun_2'], {});
|
||||
assert.deepEqual(mc.stun, { begin: null, loop: 'Stun_1', end: null });
|
||||
const dz = resolveRoles(['Attack', 'Die', 'Idle_B', 'Move', 'Dizzy_Begin', 'Dizzy_Loop', 'Dizzy_End', 'Dizzy_Die'], {});
|
||||
assert.deepEqual(dz.stun, { begin: 'Dizzy_Begin', loop: 'Dizzy_Loop', end: 'Dizzy_End' });
|
||||
assert.deepEqual(resolveRoles(['Attack', 'Die', 'Idle', 'Stun', 'Stun_End'], {}).stun, { begin: null, loop: 'Stun', end: 'Stun_End' });
|
||||
assert.equal(resolveRoles(['Attack', 'Die', 'Idle', 'Move'], {}).stun, null);
|
||||
});
|
||||
|
||||
// PR #275: a model's own Run cycle is a role of its own (猎狗pro: Move_Loop 0.80 s, Run_Loop 0.53 s); the move choice
|
||||
// is unchanged, the renderer switches to it for a fast enemy.
|
||||
test('the model’s own Run cycle becomes a separate run role', () => {
|
||||
const names = ['Attack', 'Default', 'Die', 'Idle', 'Move_Begin', 'Move_End', 'Move_Loop', 'Run_Begin', 'Run_End', 'Run_Loop'];
|
||||
const r = resolveRoles(names, {});
|
||||
assert.deepEqual(r.move, { begin: 'Move_Begin', loop: 'Move_Loop', end: 'Move_End' }, 'the move role is unchanged');
|
||||
assert.deepEqual(r.run, { begin: 'Run_Begin', loop: 'Run_Loop', end: 'Run_End' });
|
||||
assert.ok(roleAnimationNames(r).includes('Run_Loop'), 'the manifest writer validates the Run clips');
|
||||
assert.equal(resolveRoles(['Attack', 'Die', 'Idle', 'Move'], {}).run, undefined, 'no Run cycle, no role');
|
||||
});
|
||||
|
||||
// The manifest (tools/fetch-assets.mjs --offline with this resolver): every model whose skeleton has a stun / Run clip
|
||||
// has the role, and every enemy with numbered skill clips has one clip per slot.
|
||||
describe('data/assets.json roles (PR #275)', () => {
|
||||
const manifest = JSON.parse(readFileSync(new URL('../data/assets.json', import.meta.url), 'utf8'));
|
||||
const models = [];
|
||||
for (const [id, e] of Object.entries(manifest.enemies || {})) if (e.spine) models.push([`enemies.${id}`, e.spine]);
|
||||
for (const [id, e] of Object.entries(manifest.tokens || {})) if (e.spine) models.push([`tokens.${id}`, e.spine]);
|
||||
for (const [id, e] of Object.entries(manifest.chars || {})) if (e.spine?.front) models.push([`chars.${id}`, e.spine.front]);
|
||||
const walk = (re, ok) => {
|
||||
const bad = [];
|
||||
let seen = 0;
|
||||
for (const [id, sp] of models) {
|
||||
if (!sp.animations || !Object.keys(sp.animations).some((n) => re.test(n))) continue;
|
||||
seen++;
|
||||
if (!ok(sp.anims || {}, id)) bad.push(id);
|
||||
}
|
||||
return { bad, seen };
|
||||
};
|
||||
test('every model with a stun clip has a stun role', () => {
|
||||
const { bad, seen } = walk(/^(stun|dizzy)/i, (a) => !!a.stun);
|
||||
assert.ok(seen >= 6, `models with a stun clip (${seen})`);
|
||||
assert.deepEqual(bad, []);
|
||||
});
|
||||
test('every model with a Run clip has a run role', () => {
|
||||
const { bad, seen } = walk(/^run/i, (a) => !!a.run);
|
||||
assert.ok(seen >= 3, `models with a Run clip (${seen})`);
|
||||
assert.deepEqual(bad, []);
|
||||
});
|
||||
test('every enemy with numbered skill clips has a clip per slot (盐风主教昆图斯: Skill_01..04)', () => {
|
||||
const { bad, seen } = walk(/^Skill_?0*[1-9](?:$|_)/i, (a, id) => !id.startsWith('enemies.') || !!a.skills);
|
||||
assert.ok(seen >= 10, `models with numbered skill clips (${seen})`);
|
||||
assert.deepEqual(bad, []);
|
||||
const q = manifest.enemies.enemy_1521_dslily.spine.anims;
|
||||
assert.deepEqual(Object.values(q.skills).map((c) => c.loop), ['Skill_01', 'Skill_02', 'Skill_03', 'Skill_04']);
|
||||
assert.equal(q.skill.loop, 'Skill_01', 'the primary clip is unchanged');
|
||||
// operators keep their pool's indices (宴's fix, PREFAB_SPINE_ROLES / DESIGN §25.22.9, stays as it was)
|
||||
const utage = manifest.chars.char_337_utage.spine.front.anims;
|
||||
assert.deepEqual(Object.keys(utage.skills).sort(), ['0', '1']);
|
||||
assert.equal(utage.skills['0'].loop, 'Skill_Loop');
|
||||
assert.equal(utage.skill.loop, 'Attack');
|
||||
});
|
||||
});
|
||||
|
||||
test('pure supporter without Attack (char_4134_cetsyr)', () => {
|
||||
const r = resolveRoles(['Die', 'Idle', 'Skill_1_Begin', 'Skill_1_End', 'Skill_1_Loop', 'Skill_2_Begin', 'Skill_2_Loop', 'Start', 'Stun', 'Stun_Begin'], { skillIndices: [1] });
|
||||
assert.deepEqual(r.attack, { begin: null, loop: 'Skill_1_Loop', end: null, via: 'skill' });
|
||||
|
||||
Reference in new issue
Block a user