feedback1d WN: the local-client slug models are an optional overlay; the manifest never depends on the extraction

data/assets.json pointed 灼热源石虫 / 炽焰源石虫 at /assets/local/ files that only exist
where tools/local-extract ran, with no fallback: without the extraction the
asset tests failed, setup re-ran fetch-assets (rewriting the tracked manifest)
and the slugs drew as icon diamonds.

- The web model stays the 源石虫 alias (spine + spineAliasOf). The official
  model is enemies[id].spineLocal: file names in the data/local-assets.json
  group spine/enemy/<id> plus the parsed metadata, from the committed
  tools/assets/local-enemy-spines.json (fetch-assets --local-spines refreshes
  it from the extracted files, read only). The manifest is byte-identical with
  or without the extraction and has no /assets/local/ URL.
- Client: assets.js spineEntry uses spineLocal only when the local manifest
  lists every file (createFieldView awaits assets.local() with the manifest);
  UnitView loads the web model when the local one fails.
- extract.py writes the atlas with size: / pma: true itself (the same text as
  atlas.mjs normalizeAtlas), so the extraction is usable without a fetch pass.
- setup notes when an older extraction lacks the enemy models.
This commit is contained in:
sganggs committed 2026-10-03 05:18:31 +08:00
1 parent f705797a86
commit 04634f5bf7
14 files changed
+633 -119

No files matched your search

+10 -1
View File
@@ -2,7 +2,8 @@
// data/assets.json. The pure helpers (animation-role resolver, atlas
// normalizer, PNG/WOFF2/audio helpers, plan id sets) are always tested; the
// on-disk checks run only when public/assets exists (it is git-ignored and
// produced by `npm run assets`).
// produced by `npm run assets`); the optional local-client enemy models
// (spineLocal, tools/local-extract) only when data/local-assets.json lists them.
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
@@ -19,6 +20,7 @@ import { assetToPath, pickUnitSfx, indexAudio } from '../tools/assets/audio.mjs'
import { mirrorUrl, safeName, encodePath } from '../tools/assets/sources.mjs';
import { collectEnemyIds, skillIndicesByChar } from '../tools/assets/plan.mjs';
import { resolveTemplate, collectLeaves } from '../tools/assets/manifest.mjs';
import { spineEntry } from '../public/js/assets.js';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const PUBLIC = join(ROOT, 'public');
@@ -583,6 +585,13 @@ describe('generated manifest data/assets.json', () => {
for (const e of Object.values(manifest.enemies)) if (e.spine) models.set(e.spine.skel, e.spine);
for (const t of Object.values(manifest.tokens)) if (t.spine) models.set(t.spine.skel, t.spine);
assert.ok(models.size > 400);
// the official enemy models of the local client, as the client resolves them (DESIGN §13: only when listed)
const localPath = join(ROOT, 'data', 'local-assets.json');
const local = existsSync(localPath) ? JSON.parse(readFileSync(localPath, 'utf8')) : null;
for (const id of Object.keys(manifest.enemies)) {
const s = local ? spineEntry(manifest, id, { local }) : null;
if (s?.local && [s.skel, s.atlas, ...s.textures].every((u) => existsSync(join(PUBLIC, u)))) models.set(s.skel, s);
}
for (const s of models.values()) {
const dir = dirname(s.atlas);
// pixi-spine finds the atlas by swapping the extension of the skel URL