Files

118 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// test/render/loadout-skill.test.js — the equipped skill (DESIGN §16 operator loadout, user playtest #2 item 1) reaches
// the battle presentation: the sim's UnitInfo carries `skillIndex`, the Spine actor plays THAT skill's clip
// (`anims.skills[index]`, e.g. 史尔特尔 on S1 never shows the S3 黄昏 transformation) and the audio plays that skill's
// own ON_SKILL_START sound (`sfx.units[id].skills[index]`). Integration regressions (the loadout batch had the sim,
// data and UI, but every battle still animated / sounded the default skill).
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { installFakePixi, fakeViewCtx } from './fakepixi.js';
import { presetCamera } from '../../public/js/render/projection.js';
import { makeBattle } from '../helpers/battleHarness.js';
import { hasGeneratedData } from '../../server/sim/simdata.js';
import { AudioManager } from '../../public/js/audio.js';
const REAL = { skip: !hasGeneratedData() };
describe('UnitInfo.skillIndex (sim)', () => {
test('spawn events carry the equipped skill index: the loadout one, else the default', REAL, () => {
const h = makeBattle({
units: [
{ chessId: 'chess_char_5_07_a', row: 10, col: 5, uid: 1, skillIndex: 0 }, // 史尔特尔 on S1 (default S3)
{ chessId: 'chess_char_1_01_a', row: 11, col: 5, uid: 2 }, // 隐现 on its default S2
],
enemies: [], autoFinish: false, timeLimit: 10,
});
h.step();
const spawns = h.eventsOf('spawn').map((e) => e[1]).filter((u) => u.side === 'ally');
const byUid = new Map(spawns.map((u) => [u.uid, u]));
assert.equal(byUid.get(1)?.skillIndex, 0, '史尔特尔: S1');
assert.equal(byUid.get(2)?.skillIndex, 1, '隐现: default S2');
});
});
describe('Spine actor + audio use the equipped skill', () => {
let fake, UnitView;
before(async () => {
fake = installFakePixi();
({ UnitView } = await import('../../public/js/render/units.js'));
});
after(() => fake.restore());
const tick = () => new Promise((r) => setImmediate(r));
const cam = () => presetCamera('prep', { width: 1280, height: 720 });
const S1 = { begin: null, loop: 'Skill_1', end: null, index: 0, idle: null };
const S3 = { begin: 'Skill_3_Begin', loop: 'Skill_3_Loop', end: null, index: 2, idle: 'Skill_3_Idle' };
const entry = {
skel: '/s/x.skel', atlas: '/s/x.atlas', textures: ['/s/x.png'],
anims: { idle: 'Idle', attack: { begin: null, loop: 'Attack', end: null }, skill: S3, skills: { 0: S1, 2: S3 } },
animations: { Idle: 1, Attack: 1, Skill_1: 1, Skill_3_Begin: 0.6, Skill_3_Loop: 1, Skill_3_Idle: 1 },
};
const names = Object.keys(entry.animations).map((name) => ({ name }));
const assets = {
picture: () => null, image: async () => null, spineEntry: () => entry,
spine: { acquire: async () => ({ animations: names }), release() {} },
};
async function actorFor(skillIndex) {
const ctx = fakeViewCtx(fake.P, { assets, cam });
const v = new UnitView(ctx, { id: 1, side: 'ally', kind: 'op', defId: 'chess_char_5_07_a', spine: 'char_350_surtr', tier: 5, x: 5, y: 10, maxHp: 1000, skillIndex });
await tick(); await tick();
assert.ok(v.actor, 'spine actor built');
return v.actor;
}
test('S1 equipped: the skill clip is S1 (not the default S3 transformation)', async () => {
const a = await actorFor(0);
assert.equal(a.roles.skill.loop, 'Skill_1');
a.setSkill(true);
assert.equal(a._attackClip().loop, 'Skill_1', 'attacks during the skill use the S1 clip');
assert.notEqual(a.current, 'Skill_3_Begin', 'no S3 transformation');
const d = await actorFor(undefined);
d.setSkill(true);
assert.equal(d.current, 'Skill_3_Begin', 'control: the default S3 starts with its begin clip');
});
test('default / unknown index: the primary skill clip', async () => {
assert.equal((await actorFor(undefined)).roles.skill.loop, 'Skill_3_Loop');
assert.equal((await actorFor(2)).roles.skill.loop, 'Skill_3_Loop');
assert.equal((await actorFor(1)).roles.skill.loop, 'Skill_3_Loop', 'no clip for S2 in the model: primary');
});
// PR #275 (@xcdoge): the slot an enemy ability casts (render/app.js `case 'cast'` → UnitView.setSkillSlot) swaps its
// skill clip mid-battle, so a multi-skill boss does not show one cast animation for every ability.
test('an enemy cast slot swaps its skill clip mid-battle (a form in force is kept)', async () => {
const ctx = fakeViewCtx(fake.P, { assets, cam });
const v = new UnitView(ctx, { id: 9, side: 'enemy', kind: 'enemy', defId: 'enemy_1521_dslily', x: 5, y: 10, maxHp: 100 }, {});
await tick(); await tick();
assert.ok(v.actor, 'spine actor built');
assert.equal(v.actor.roles.skill.loop, 'Skill_3_Loop', 'the manifest primary clip to start with');
assert.equal(v.skillIndex, null, 'no slot reported yet');
v.setSkillSlot(0);
assert.equal(v.skillIndex, 0);
assert.equal(v.actor.roles.skill.loop, 'Skill_1', 'that slot’s own clip');
v.setSkillSlot(-1);
v.setSkillSlot(1.5);
assert.equal(v.skillIndex, 0, 'a non-slot is ignored');
v.actor.setForm({ idle: 'Attack' });
v.setSkillSlot(2);
assert.equal(v.actor.roles.skill.loop, 'Skill_3_Loop', 'the next slot');
assert.equal(v.actor.roles.idle, 'Attack', 'the form in force survives the slot change');
v.actor.setForm(null);
assert.equal(v.actor.roles.idle, 'Idle');
assert.equal(v.actor.roles.skill.loop, 'Skill_3_Loop', 'and the slot survives the form ending');
});
test('audio: the equipped skill’s own ON_SKILL_START sound', () => {
const manifest = { audio: { sfx: { units: { char_350_surtr: { skill: '/sfx/s3.mp3', skills: { 0: '/sfx/s1.mp3', 2: '/sfx/s3.mp3' } } } } } };
const a = new AudioManager({ win: null, getManifest: () => manifest });
a.ctx = {};
const played = [];
a._play = (url) => { played.push(url); };
a.setFieldUnits([
{ id: 1, side: 'ally', kind: 'op', defId: 'chess_char_5_07_a', spine: 'char_350_surtr', skillIndex: 0 },
{ id: 2, side: 'ally', kind: 'op', defId: 'chess_char_5_07_a', spine: 'char_350_surtr' },
]);
a.handleBattleEvents([['skill', 1, 1], ['skill', 2, 1]]);
assert.deepEqual(played, ['/sfx/s1.mp3', '/sfx/s3.mp3']);
});
});