Online co-op browser remake of Arknights' Stronghold Protocol: Alliance (client-simulated combat, official waves/bonds/items/strategies, 3D board, facing wheel, operator loadout, official emotes, deployment scripts).
94 lines
4.8 KiB
JavaScript
94 lines
4.8 KiB
JavaScript
// 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');
|
||
});
|
||
|
||
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']);
|
||
});
|
||
});
|