Downloaded 1,295 files, 127.2 MB (133,426,089 bytes) into the shared public/assets; no existing file changed or removed. data/assets.json only gains entries: 72 chars (all with avatar, portrait, Front and Back Spine), 38 tokens (avatars; no Spine — upstream has skin variants only), 208 skill icons, 10 sub-profession icons, 102 SFX units, 131 module type icons.
94 lines
7.4 KiB
JavaScript
94 lines
7.4 KiB
JavaScript
// test/assets-diy.test.js — the art of the 自选 picks in the asset pipeline (tools/fetch-assets.mjs dataExtras,
|
|
// tools/assets/plan.mjs patternOperator / buildPlan `extraOperators` / `moduleTypes`; docs/ASSETS.md): the owned-6★
|
|
// units of data/backups.json are planned from research 07's URL patterns (avatar, portrait — E2 too —, battle Spine
|
|
// Front / Back, every skill icon), their summons as tokens, and every module's type icon as manifest `modules`; the
|
|
// committed data/assets.json lists them (checked against public/assets when present); --add-only never rewrites a file.
|
|
// Run: node --test test/assets-diy.test.js
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dataExtras, parseArgs } from '../tools/fetch-assets.mjs';
|
|
import { patternOperator, buildPlan } from '../tools/assets/plan.mjs';
|
|
import { indexAudio } from '../tools/assets/audio.mjs';
|
|
import { Downloader } from '../tools/assets/downloader.mjs';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const load = (f) => JSON.parse(readFileSync(join(ROOT, 'data', `${f}.json`), 'utf8'));
|
|
const BACKUPS = load('backups');
|
|
const CHESS = load('chess');
|
|
const YUAN = 'https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/';
|
|
const FEXLI = 'https://raw.githubusercontent.com/fexli/ArknightsResource/main/';
|
|
|
|
test('dataExtras: every backups.json unit (skills by index, once), the 自选 summons, the type icon of every module of chess.json and backups.json', () => {
|
|
const x = dataExtras(BACKUPS, CHESS);
|
|
assert.deepEqual(Object.keys(x.extraOperators), Object.keys(BACKUPS.units));
|
|
assert.deepEqual(x.extraOperators.char_112_siege, {
|
|
name: '推进之王', subProfessionId: 'pioneer', nationId: 'victoria',
|
|
skills: [{ index: 0, skillId: 'skcom_charge_cost[3]', iconId: 'skcom_charge_cost[3]' }, { index: 1, skillId: 'skchr_siege_2', iconId: 'skchr_siege_2' }, { index: 2, skillId: 'skchr_siege_3', iconId: 'skchr_siege_3' }],
|
|
});
|
|
assert.deepEqual(x.tokenIds, Object.keys(BACKUPS.tokens).sort());
|
|
const types = new Set();
|
|
for (const c of Object.values(CHESS)) for (const m of c.modules || []) types.add(m.typeIcon);
|
|
for (const u of Object.values(BACKUPS.units)) for (const f of Object.values(u.forms)) for (const m of f.modules || []) types.add(m.typeIcon);
|
|
assert.deepEqual(x.moduleTypes, [...types].sort());
|
|
assert.ok(x.moduleTypes.includes('sol-x') && x.moduleTypes.includes('sol-y'));
|
|
assert.deepEqual(dataExtras(null, null), { extraOperators: {}, tokenIds: [], moduleTypes: [] });
|
|
});
|
|
|
|
test('patternOperator / buildPlan: an operator research 07 lacks gets the 07 URL patterns; known operators keep their research record; module type icons become `modules`', () => {
|
|
const o = patternOperator('char_112_siege', { subProfessionId: 'pioneer', skills: [{ index: 0, skillId: 'skcom_charge_cost[3]', iconId: 'skcom_charge_cost[3]' }] });
|
|
assert.deepEqual([o.avatar.e0e1.url, o.avatar.e2.url, o.portrait.e0e1.url, o.portrait.e2.url],
|
|
[`${YUAN}avatar/char_112_siege.png`, `${YUAN}avatar/char_112_siege_2.png`, `${YUAN}portrait/char_112_siege_1.png`, `${YUAN}portrait/char_112_siege_2.png`]);
|
|
assert.equal(o.battleSpine.front.skel, `${FEXLI}spine/char_112_siege/char_112_siege/Front/char_112_siege.skel`);
|
|
assert.equal(o.battleSpine.back.png, `${FEXLI}spine/char_112_siege/char_112_siege/Back/char_112_siege.png`);
|
|
assert.equal(o.skills[0].icon.url, `${YUAN}skill/skill_icon_skcom_charge_cost%5B3%5D.png`, 'brackets percent-encoded');
|
|
assert.match(o.subProfessionIcon, /arts\/ui\/subprofessionicon\/sub_pioneer_icon\.png$/);
|
|
const known = { name: 'x', avatar: { e0e1: { url: 'https://example.invalid/known.png' } }, skills: [] };
|
|
const t = buildPlan({
|
|
assets07: { operators: { char_609_acguad: known } }, ops03: {}, enemies05: {}, maps05: {}, audio: indexAudio({}), modelsData: {},
|
|
extraOperators: { char_112_siege: dataExtras(BACKUPS, CHESS).extraOperators.char_112_siege, char_609_acguad: { skills: [] }, 'bad id': {} },
|
|
moduleTypes: ['sol-x', 'sol-x', 'bad/type'],
|
|
});
|
|
const c = t.template.chars.char_112_siege;
|
|
assert.deepEqual([c.avatar.alts[0].rel, c.avatarE2.alts[0].rel, c.portrait.alts[0].rel, c.portraitE2.alts[0].rel],
|
|
['char/avatar/char_112_siege.png', 'char/avatar/char_112_siege_2.png', 'char/portrait/char_112_siege_1.png', 'char/portrait/char_112_siege_2.png']);
|
|
assert.deepEqual(c.spine, { front: { model: 'op:char_112_siege:front' }, back: { model: 'op:char_112_siege:back' } });
|
|
assert.deepEqual(t.models.get('op:char_112_siege:front').skillIndices, [0, 1, 2], 'a Spine clip per skill index');
|
|
for (const id of ['skcom_charge_cost[3]', 'skchr_siege_2', 'skchr_siege_3']) assert.ok(t.template.skills[id] && t.template.skillsById[id] === id, id);
|
|
assert.equal(t.template.chars.char_609_acguad.avatar.alts[0].urls[0], 'https://example.invalid/known.png', 'research 07 wins for an operator it lists');
|
|
assert.equal(t.template.chars['bad id'], undefined);
|
|
assert.deepEqual(Object.keys(t.template.modules), ['sol-x']);
|
|
assert.match(t.template.modules['sol-x'].alts[0].urls[0], /arts\/ui\/uniequiptype\/sol-x\.png$/);
|
|
assert.equal(t.template.prof.sub.pioneer.alts[0].rel, 'prof/sub/pioneer.png');
|
|
});
|
|
|
|
test('--add-only: parsed, refused with --prune / --force; its downloader keeps every existing file and records what it wrote', async () => {
|
|
assert.equal(parseArgs(['--add-only']).addOnly, true);
|
|
assert.equal(parseArgs([]).addOnly, false);
|
|
assert.throws(() => parseArgs(['--add-only', '--prune']), /never deletes or rewrites/);
|
|
assert.throws(() => parseArgs(['--add-only', '--force']), /never deletes or rewrites/);
|
|
const dl = new Downloader({ root: join(ROOT, 'data'), ledgerPath: join(ROOT, '.cache', 'test-never-written.json'), keepExisting: true, force: true, log() {} });
|
|
assert.equal(dl.force, false, '--force never applies');
|
|
assert.equal(await dl.existingSize({ rel: 'chess.json', urls: [], kind: 'png', bytes: 1 }), readFileSync(join(ROOT, 'data', 'chess.json')).length, 'an existing file is kept whatever its size / kind');
|
|
assert.equal(await dl.existingSize({ rel: 'no-such-file.png', urls: [], kind: 'png' }), -1);
|
|
assert.deepEqual([...dl.written], []);
|
|
});
|
|
|
|
test('the committed data/assets.json lists every 自选 operator\'s avatar, portrait, Front Spine and skill icons, and a type icon per module (files on disk when public/assets is here)', () => {
|
|
const m = load('assets');
|
|
const disk = existsSync(join(ROOT, 'public', 'assets', 'char'));
|
|
const onDisk = (u) => !disk || existsSync(join(ROOT, 'public', u));
|
|
for (const id of BACKUPS.diy.ownedPool) {
|
|
const c = m.chars[id];
|
|
assert.ok(c && c.avatar && c.portrait && c.spine?.front?.skel, `${id}: art`);
|
|
for (const u of [c.avatar, c.portrait, c.spine.front.skel, c.spine.front.atlas, ...c.spine.front.textures]) assert.ok(onDisk(u), `${id}: ${u} on disk`);
|
|
assert.deepEqual(Object.keys(c.spine.front.anims.skills || {}).sort(), ['0', '1', '2'], `${id}: a clip per skill index`);
|
|
for (const s of BACKUPS.units[id].forms['2/1/4/0'].skills) assert.ok(m.skills[m.skillsById[s.skillId]] || m.skills[s.iconId], `${id}: skill icon ${s.skillId}`);
|
|
}
|
|
for (const t of dataExtras(BACKUPS, CHESS).moduleTypes) assert.ok(m.modules[t] && onDisk(m.modules[t]), `module type icon ${t}`);
|
|
assert.equal(m.stats.modules, Object.keys(m.modules).length);
|
|
});
|