Files
ark-stronghold-protocol/test/render/loadout-skill.test.js
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
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).
2026-09-29 16:21:31 +08:00

94 lines
4.8 KiB
JavaScript
Raw 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');
});
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']);
});
});