Files
ark-stronghold-protocol/test/ui/loadout-quick.test.js
T
farridge 519f04e2d5 feedback7 R22-LOADOUT: 干员调配's detail starts with the operator's 特质 (the in-match card's block), and the compact skill tags of the quick choices; golden: unchanged
Substantially PR #301 by @farridge (its non-layout part, ported; the owner's decision of 2026-10-08: 「可以做,具体布局
怎么优化你来决定」 — the card layout is redesigned in the next commit):
- ui/detailPanel.js exports GarrisonBlock; screens/loadout.js LoadoutGarrisons renders the 特质 of the form the detail's
  普通 / 精锐 skill toggle shows (garrisons.json `descRaw` with its trigger markers, 「<进入休整期时>」 …) at the top of the
  detail; garrisons.json loads with the overlay; its reading sizes in loadout.css.
- ui/loadoutModel.js quickSkillTags: SP recovery (攻回 / 受回 / 自回 / 被动, 部署触发 for a deploy-and-expire passive) and
  duration (s, 弹药, 常驻, ∞, 瞬发) from the generated SkillRecord, never from a localized description.
- i18n: the six new strings in the four packs (en from the PR; ja / ko / zh-TW added).
- Tests: test/ui/loadout-quick.test.js (from the PR, 4): quick tags of 空弦 / deployment skills / ammo / infinite, the 特质
  blocks of 空弦 / 星熊 / 缄默德克萨斯 both forms; loadout-stats.test.js reads the new import.
2026-10-08 21:06:30 +08:00

81 lines
3.8 KiB
JavaScript

// Quick loadout previews use the same generated records as the in-match detail, without changing a loadout.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { quickSkillTags } from '../../public/js/ui/loadoutModel.js';
const chess = JSON.parse(readFileSync(new URL('../../data/chess.json', import.meta.url), 'utf8'));
const operator = (name) => Object.values(chess).find((c) => c.name === name && !c.isGolden && !c.isDiy);
test('quick skill previews show the generated normal/elite SP and duration', () => {
const base = operator('空弦');
const elite = chess[base.goldenId];
for (const c of [base, elite]) {
for (const s of c.skills) {
const tags = quickSkillTags(s);
assert.equal(tags.recovery, '攻回');
assert.equal(tags.init, s.initSp);
assert.equal(tags.cost, s.spCost);
assert.equal(tags.duration, s.duration > 0 ? `${s.duration}s` : '瞬发');
}
}
});
test('deployment skills have no SP cycle, while permanent passives remain passive (in either language)', () => {
for (const name of ['缄默德克萨斯', '砾', '斯卡蒂', '野鬃']) {
const c = operator(name);
for (const form of [c, chess[c.goldenId]]) {
for (const s of form.skills.filter((s) => s.spType === 'ON_DEPLOY')) {
const tags = quickSkillTags({ ...s, desc: 'localized text', descRaw: '' });
assert.equal(tags.recovery, '—', `${name} S${s.index + 1}`);
assert.equal(tags.init, null);
assert.equal(tags.cost, null);
assert.equal(tags.duration, `${s.duration > 0 ? s.duration : s.bb.duration}s`);
}
}
}
const passive = quickSkillTags(operator('星熊').skills[1]);
assert.equal(passive.recovery, '被动');
assert.equal(passive.duration, '常驻');
assert.equal(passive.cost, null);
});
test('ammo and infinite skills keep their duration semantics', () => {
assert.equal(quickSkillTags({ skillType: 'MANUAL', spType: 'INCREASE_WITH_TIME', durationType: 'AMMO' }).duration, '弹药');
assert.equal(quickSkillTags({ skillType: 'MANUAL', spType: 'INCREASE_WITH_TIME', duration: -1 }).duration, '∞');
assert.equal(quickSkillTags(null).recovery, '—');
});
test('loadout attributes use the in-match block and keep obtain/rest triggers for both forms', async () => {
globalThis.fetch = async (url) => {
try {
const value = JSON.parse(readFileSync(new URL(`../../data/${String(url).split('/').pop()}`, import.meta.url), 'utf8'));
return { ok: true, json: async () => value };
} catch { return { ok: false, status: 404 }; }
};
const { data } = await import('../../public/js/data.js');
const { LoadoutGarrisons } = await import('../../public/js/screens/loadout.js');
const { GarrisonBlock } = await import('../../public/js/ui/detailPanel.js');
await data.loadAll('chess', 'garrisons');
function* walk(v) {
if (Array.isArray(v)) { for (const n of v) yield* walk(n); return; }
if (!v || typeof v !== 'object') return;
yield v;
yield* walk(v.props?.children);
}
for (const [name, trigger] of [['空弦', '<进入休整期时>'], ['星熊', '<获得时>'], ['缄默德克萨斯', '<进入休整期时>']]) {
const base = operator(name);
for (const rec of [base, chess[base.goldenId]]) {
const nodes = [...walk(LoadoutGarrisons({ chess: rec, m: null }))].filter((n) => n.type === GarrisonBlock);
assert.equal(nodes.length, rec.garrisonIds.length);
for (const node of nodes) {
const original = data.lookup('garrisons', rec.garrisonIds[0]);
assert.deepEqual(node.props.garrison, original);
const raw = [...walk(GarrisonBlock(node.props))].find((n) => n.type?.name === 'RichText').props.text;
assert.ok(raw.startsWith(trigger), `${name}: ${raw}`);
assert.equal(raw, original.descRaw || original.desc);
}
}
}
});