feedback5 WX: the 39 summon models no web dump carries come from the local client (extract.py TOKEN_SPINES) and are drawn instead of the avatar diamond when data/local-assets.json lists them

- tools/local-extract/extract.py TOKEN_SPINES (`--only spine/token`): each token's battle prefab in the
  battle token packs (pkgrps/btl_pfb_tokens_*.ab; the Windows build has all 39, the iOS build lacks
  btl_pfb_tokens_0), the skeleton of its Front (or only) Spine renderer, written to
  public/assets/local/spine/token/<id>/ like the enemy models (sized pma atlas, premultiplied pages;
  the newer RGBA pages keep their own alpha). The enemy writer is shared (write_spine; the two enemy
  models re-extract byte for byte). The skinpack job of 乌尔比安's block is gone: it extracted
  nothing, and that token draws no model in the game (nor do 圣聆初雪's, 酒神's, 贝洛内's and
  予愿安洁莉娜's hidden tokens: no Spine renderer, no avatar in either install).
- tools/assets/local-token-spines.json (committed, `fetch-assets --local-spines`) → data/assets.json
  tokens[id].spineLocal for 35 自选 summons, 凯瑟琳's 爬行号·防护单元 and 凛御银灰's three 风雪之眼; the
  manifest is the same with or without the extraction (JSON diff: only the 39 spineLocal entries and
  the hash). spine.mjs LOCAL_SPINE_ROLES maps clips the resolver cannot read (电弧's 戴乌 C_Skill1_*,
  酒神's 本能的召唤 Loop / End, 白铁's platforms End, 战术锚点 on its Default pose) [ASSUMED where named].
- assets.js spineEntry: a token resolves its spineLocal like an enemy (no web model behind it: the
  diamond stays when it is not listed or fails to load). Scale as the web tokens (UNIT.modelScale).
- setup reports an extraction without the summon models; docs: ASSETS.md "Token models from the local
  client", DEPLOY.md §6, DESIGN §13 (design/client.md), README.
- Tests: test/local-token-models.test.js (new), test/local-extract.test.js (job table, prefab walk,
  alpha merge), test/assets.test.js (the local token models load like the client does),
  test/package.test.js (the full zip ships them), test/static-local-art.test.js. Golden unchanged.
This commit is contained in:
sganggs committed 2026-10-06 20:16:20 +08:00
1 parent 83d76e507d
commit 575333137c
17 files changed
+2738 -111

No files matched your search

+17 -5
View File
@@ -198,20 +198,32 @@ export function checkAssets() {
* the 玩法说明 pages are not in the list: step 5 downloads them from the public mirror with the other assets (GitHub
* issue #42). Shown by setup and doctor.
*/
export const LOCAL_ART_FALLBACK = '3D 棋盘改用 2D,部分官方界面图标和灼热/炽焰源石虫模型用替代样式';
export const LOCAL_ART_FALLBACK = '3D 棋盘改用 2D,部分官方界面图标、灼热/炽焰源石虫模型和多数自选召唤物模型用替代样式';
/** Where a machine without the client gets the local art (docs/DEPLOY.md §6「本地客户端素材」); shown by doctor (setup's row,
* printed on every start by scripts/launch.mjs, only points to that section). */
export const LOCAL_ART_COPY_HINT = '没有客户端的服务器可以从同一版本的整合包(完整包)复制 public/assets/local 和 data/local-assets.json';
/**
* Local-client art (optional): manifest entry count, whether the 3D board atlas is on disk and whether the extraction
* has the enemy models only the client has (`spine/enemy/*` groups, extract.py ENEMY_SPINES — added after 0.1.0).
* has the enemy and token models only the client has (`spine/enemy/*` groups, extract.py ENEMY_SPINES — added after
* 0.1.0; `spine/token/*` groups, TOKEN_SPINES — added in 0.2.0).
*/
export function checkLocal() {
const m = readJson(LOCAL_MANIFEST);
const count = m && m.groups ? Object.values(m.groups).reduce((n, g) => n + Object.keys(g || {}).length, 0) : 0;
const enemySpines = !!(m && m.groups && Object.keys(m.groups).some((g) => g.startsWith('spine/enemy/')));
return { manifest: !!m, count, board3d: exists(LOCAL_BOARD_ATLAS), tiles: exists(LOCAL_BOARD_TILES), enemySpines, dirPresent: exists(path.join(ROOT, 'public', 'assets', 'local')) };
const has = (prefix) => !!(m && m.groups && Object.keys(m.groups).some((g) => g.startsWith(prefix)));
return {
manifest: !!m, count, board3d: exists(LOCAL_BOARD_ATLAS), tiles: exists(LOCAL_BOARD_TILES), enemySpines: has('spine/enemy/'),
tokenSpines: has('spine/token/'), dirPresent: exists(path.join(ROOT, 'public', 'assets', 'local')),
};
}
/** What an extraction made by an older extract.py lacks (setup's 本地客户端美术 row; re-extract with --local). */
export function localGaps(local) {
const gaps = [];
if (local && !local.enemySpines) gaps.push('灼热/炽焰源石虫模型');
if (local && !local.tokenSpines) gaps.push('自选召唤物模型');
return gaps.length ? `,缺少新版的${gaps.join('和')}` : '';
}
/**
@@ -445,7 +457,7 @@ async function main() {
add(already ? 'ok' : 'warn', '本地客户端美术(可选)', `${client.kind} 客户端缺少卫戍协议资源(请在游戏内下载全部资源):${client.path}`);
} else if (already && opts.local !== 'force') {
if (local.board3d && !local.tiles && !opts.check) cropBoardTiles(log);
add('ok', '本地客户端美术(可选)', `已提取 ${local.count} 项${local.board3d ? ',3D 棋盘可用' : ''}${local.enemySpines ? '' : ',缺少新版的灼热/炽焰源石虫模型'}(重新提取:--local)`);
add('ok', '本地客户端美术(可选)', `已提取 ${local.count} 项${local.board3d ? ',3D 棋盘可用' : ''}${localGaps(local)}(重新提取:--local)`);
} else if (opts.check) {
add('skip', '本地客户端美术(可选)', `检测到 ${client.kind} 客户端,可运行 node tools/setup.mjs --local 提取`);
} else {