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

+35 -6
View File
@@ -25,7 +25,7 @@ npm run assets # = node tools/vendor.mjs && node tools/fetch-assets.mjs
| `--prune` | Delete files under `public/assets/` that the manifest no longer references, for example after a mapping change. Without this flag they are only listed in the report. `public/assets/local/` (written by `tools/local-extract`) is never pruned. Implies `--allow-shrink`. |
| `--allow-shrink` | Write `data/assets.json` even when it loses entries the current one has (see "The manifest never shrinks by accident" below). |
| `--add-only` | For a checkout whose `public/assets/` and `public/fonts/` are shared with another one (a git worktree with symlinked asset folders): download only the files missing on disk and never re-download, rewrite or delete an existing file — atlases already on disk are left as they are, the fonts are not rebuilt (the manifest keeps its current `fonts`). Not with `--prune` / `--force`. |
| `--local-spines` | Rewrite `tools/assets/local-enemy-spines.json` (the metadata of the enemy models only the local client has, see "Enemy aliases") from the models `tools/local-extract/extract.py` extracted to `public/assets/local/spine/enemy/`. Run it after a game update changed them; without it the committed file is used and a differing extraction only gets a warning. |
| `--local-spines` | Rewrite `tools/assets/local-enemy-spines.json` and `tools/assets/local-token-spines.json` (the metadata of the enemy and token models only the local client has, see "Enemy aliases" and "Token models from the local client") from the models `tools/local-extract/extract.py` extracted to `public/assets/local/spine/enemy/` and `public/assets/local/spine/token/`. Run it after a game update changed them; without it the committed files are used and a differing extraction only gets a warning. |
**The manifest never shrinks by accident.** An entry whose files are missing on this machine is left out of a rebuilt
manifest, so a run where some downloads failed (or whose upstream audio / model index lost them) would drop entries that
@@ -90,6 +90,7 @@ The research JSONs in `docs/research/` (03, 05, 07) define **which** ids are nee
| Token Spine | fexli: the default model, or else the first skin variant (`spine/{tokenId}/{variant}/Spine/`) | `spine/token/{tokenId}/{stem}.*` |
| Enemy Spine (PC build, premultiplied alpha) | isHarryh/Ark-Models `models_enemies/{key}/`, file names from `models_data.json` | `spine/enemy/{enemyId}/{stem}.*` |
| Enemy Spine that no dump carries (灼热源石虫 / 炽焰源石虫) | the local client only (`tools/local-extract/extract.py ENEMY_SPINES`, optional); never downloaded and never required: an overlay of the web alias (`enemies[id].spineLocal`) | `local/spine/enemy/{enemyId}/{stem}.*` (listed in `data/local-assets.json`) |
| Token Spine that no dump carries (39 summons: most 自选 summons, 凯瑟琳's 爬行号·防护单元, 凛御银灰's 风雪之眼) | the local client only (`tools/local-extract/extract.py TOKEN_SPINES`, optional); never downloaded and never required: an overlay (`tokens[id].spineLocal`; without it the avatar diamond) | `local/spine/token/{tokenId}/{stem}.*` (listed in `data/local-assets.json`) |
| BGM | AA2 `voice` branch `audio/sound_beta_2/music/**` (大厅/休整期 `act1autochess`, 开战 `act13side/m_bat_kazimierz2_{1,2}` — 骑士之日 / 无畏者; the 开战 track follows the round: `_2` 无畏者 rounds 1–7, `_1` 骑士之日 from round 8) | `audio/bgm/{file}.mp3` |
| SFX (UI, battle, per unit) | AA2 `voice` `audio/sound_beta_2/**`, mapped from `audio_data.json` banks | `audio/sfx/{same sub-path}.mp3` |
| 干员战斗语音 | AA2 `voice` `audio/sound_beta_2/voice_cn/{charId}/cn_nn.mp3` — the lines `charword_table.json` lists (`placeType` = when the game plays one, `voiceAsset` = the path); `--voice-lang=jp|en|kr` takes the same file names from `voice/`, `voice_en/`, `voice_kr/` | `audio/voice/{lang}/{charId}/{cn_nn}.mp3` |
@@ -100,7 +101,7 @@ The `stem` of a Spine model is the upstream file name. Two examples: `char_107_l
### Id scope
- **Operators:** all 138 pool charIds from `activity_table` (`charShopChessDatas[*].charId ∪ backupCharId`), including hidden chess and backup operators; plus every unit of `data/backups.json` research 07 does not list — the 71 自选 owned-6★ picks (`diy.ownedPool`, DATA.md §18; the collab operators are not in the data, and 焰狐龙梓兰's entries left the manifest when she left the pool in 0.2.0) — planned from 07's URL patterns (`tools/assets/plan.mjs patternOperator`, `tools/fetch-assets.mjs dataExtras`): avatar and portrait (E0–E1 and E2), the default-skin battle Spine Front / Back, the icon and skill sound of each skill, the sub-profession icon. 209 operators in all (206 with a Back model).
- **Tokens:** the 20 pool tokens, and the 38 summons of the 自选 picks (`data/backups.json tokens`) as extra tokens (below): their avatars; their battle Spine exists upstream only as skin variants, which the default locations miss — no Spine yet (the client draws the avatar).
- **Tokens:** the 20 pool tokens, and the 38 summons of the 自选 picks (`data/backups.json tokens`) as extra tokens (below): their avatars; no dump carries the battle Spine of the 自选 summons (upstream has at most skin variants, which the default locations miss), so the web manifest has no model for them — 35 of them, and 4 pool summons, have the official model as an optional local-client overlay ("Token models from the local client"); 3 have no model in the game at all.
- **Enemies:** 253 ids planned, 252 in the manifest (心烛 has no assets). The set is the union of:
- the 07 enemy list;
- every enemy in the `act1autochess_*` wave, boss and 联防 levels that act2 modes use (from `05-maps.json`; the tutorial is excluded);
@@ -149,7 +150,9 @@ All paths are URL paths relative to the site root, for example `/assets/char/ava
spineLocal?: { group, skel, atlas, textures, …Spine } } },
// spineLocal: an optional local-client model; file names in a data/local-assets.json group,
// not URLs, and always emitted (independent of the disk) — "Enemy aliases" below
tokens: { [tokenId]: { owner: charId|null, avatar?, spine?: Spine, spineVariant?: string } },
tokens: { [tokenId]: { owner: charId|null, avatar?, spine?: Spine, spineVariant?: string,
spineLocal?: { group, skel, atlas, textures, …Spine } } },
// spineLocal: as for enemies — "Token models from the local client" below
bonds: { [bondId]: url }, // white glyphs; tint in CSS/canvas
items: { [trapId]: url },
bands: { [bandId]: url },
@@ -317,13 +320,37 @@ Other renderer rules from research 07 §5.4–5.5:
round. A 2026-10-03 audit of every enemy of `data/enemies.json` (249) against the client's battle prefabs (the
skeleton each prefab's Spine renderer draws) found no other enemy drawn with another enemy's model; 伊利昂的木驮兽
(`enemy_10159_mntrjn`) starts on its `Full` skin (five passengers) in the game and is drawn with the `default` one.
- **Token models from the local client** (0.2.0): no dump carries the battle Spine of most 自选 summons (fetch-assets:
"missing skel"), nor of 凯瑟琳's 爬行号·防护单元 and 凛御银灰's 风雪之眼, so they were drawn as the avatar diamond. The
local client has them in its battle token packs (`pkgrps/btl_pfb_tokens_*.ab`, the Windows build carries all of them;
the iOS build lacks `btl_pfb_tokens_0` and has ASTC pages), the same overlay as the enemies above:
- `tools/local-extract/extract.py TOKEN_SPINES` (39 ids; `--only spine/token`) reads each token's battle prefab
(`dyn/battle/prefabs/[uc]tokens/<id>.prefab`), takes the skeleton of its Front renderer — a directional token has
Front / Back (/ Down) renderers, each with its own skeleton of the same name; the web tokens use Front too — and
writes it to `public/assets/local/spine/token/{tokenId}/` like an enemy model (skeleton, sized `pma: true` atlas,
premultiplied pages: the older tokens' `[alpha]` texture merged in, the newer RGBA pages kept with their own alpha).
- `tokens[id].spineLocal` = `{ group: 'spine/token/{tokenId}', skel, atlas, textures, pma, anims, … }` from the
committed `tools/assets/local-token-spines.json` (`fetch-assets --local-spines`), never from the disk;
`assets.js spineEntry` draws the model when `data/local-assets.json` lists every file of it, else (or when it fails
to load) the avatar diamond as before. The models are drawn like the web tokens: one `UNIT.modelScale`, no
per-prefab factor (the official prefabs scale most tokens by the standard 0.27; W's 此面向敌 0.4, 令's “清平” 0.25 and
“弦惊” 0.3, 傀影's 镜中虚影 0.26, 风雪之眼 and 淬羽赫默's 夜灯 0.25 — like the web tokens' 医疗探机 / 诅咒娃娃 0.4 and
香槟炸弹 0.25).
- Clip names the resolver cannot read are mapped in `tools/assets/spine.mjs LOCAL_SPINE_ROLES`: 电弧's 戴乌
(`C_Skill1_*`, beside a 0 s `C_Default` pose), 酒神's 本能的召唤 (`Loop` / `End`) and 白铁's 多功能平台 (`End` as it
goes) [ASSUMED: by the clip names], and 凯尔希·思衡托's 战术锚点, whose Start / Idle / Die clips hide its only
attachment: it stays on its `Default` pose (a white anchor mark) [ASSUMED].
- Not extracted: the tokens whose prefab draws nothing (an `EmptyAnimator` instead of a Spine renderer; no avatar in
either install's asset index either): 乌尔比安's 从不混淆的方向, 圣聆初雪's 保护目标(冻结状态), 酒神's 迷狂牢笼, 贝洛内's
牵绊 and 予愿安洁莉娜's “一会儿见!” — the last three keep the token fallback picture (`tokenAvatarUrl`: no avatar, no
owner in the manifest → the 召唤物 battle-card icon).
### Other fallbacks
- **Emotes and 玩法说明 pages** (`public/js/data.js artUrls / nextArtUrl`, `ui/guide.js guideStage`): the local-client picture (`data/local-assets.json`) first, then the mirror copy (`ui['emoticon/…']`, `ui['guide/…']`), each tried in turn when one fails to load; when none is left — none listed, or every copy failed (for example data/assets.json lists the downloaded pages but the files are not on disk yet: a `git pull` and restart without setup) — the neutral emote glyph, and for a page the official tips text (`config.tips`). The rest of the local-client art (the 3D board, the official HUD sprites, module type icons, the two enemy models above) is not downloaded: the client looks it up in `data/local-assets.json` only (most of the HUD sprites are on the mirror too, DESIGN §22.5); docs/DEPLOY.md §6 lists what falls back without it.
- **Emotes and 玩法说明 pages** (`public/js/data.js artUrls / nextArtUrl`, `ui/guide.js guideStage`): the local-client picture (`data/local-assets.json`) first, then the mirror copy (`ui['emoticon/…']`, `ui['guide/…']`), each tried in turn when one fails to load; when none is left — none listed, or every copy failed (for example data/assets.json lists the downloaded pages but the files are not on disk yet: a `git pull` and restart without setup) — the neutral emote glyph, and for a page the official tips text (`config.tips`). The rest of the local-client art (the 3D board, the official HUD sprites, module type icons, the two enemy models and the 39 token models above) is not downloaded: the client looks it up in `data/local-assets.json` only (most of the HUD sprites are on the mirror too, DESIGN §22.5); docs/DEPLOY.md §6 lists what falls back without it.
- **Tokens:**
- Without an avatar, use `chars[owner].avatar` with a 召唤物 badge, or `prof.battlecard.token` — except 圣聆初雪's 保护目标(冻结状态) (PRTS 无头像; the frozen gate), drawn as a procedural ice diamond (`render/units.js ICE_TOKENS`).
- Without a Spine, draw the avatar sprite with a bob tween.
- Without a Spine (and without its local-client model, "Token models from the local client"), draw the avatar sprite with a bob tween.
- `spineVariant` names the skin-variant model that stands in for the missing default model.
- **Enemies without a spine** (for example `enemy_9016_acstmr`): draw `icon` in a diamond. Enemies with no manifest entry at all (`enemy_5601_entlec` 心烛): draw a procedural glyph.
- **Battle effects** (projectiles per kind, hit sparks and slashes, skill bursts and auras, 蕾缪安's lock reticles and shells, 回环射手 boomerangs — DESIGN §17.3) are procedural: the FX atlas is drawn at run time (`public/js/render/textures.js`), so they need no downloaded or local art. The local client does have battle effect art — `battle/[pack]common.ab` holds per-weapon projectile sprites (`projectile_arrow(_new)`, `projectile_crossbow(_new)`, `projectile_yuki`, `img_fx_light_01/02`, `trail_11`), and the per-character `battle/prefabs/effects/*.ab` are particle systems whose textures live in other bundles — but none of it is extracted: the sim's `arrow` also covers gun snipers, and friends joining a game may not have the local art.
@@ -350,7 +377,9 @@ When `public/assets` exists, the same file also checks the generated output:
- Every manifest path exists on disk.
- Every pool operator has an avatar, a portrait and a Front model.
- Every atlas has `size:` lines, plus `pma: true` for enemies.
- Every Spine model loads the way the client loads it. That means pixi-spine's atlas reader with real page sizes, where a region outside its page throws, then `SkeletonBinary` with `AtlasAttachmentLoader`, where a missing region throws. Every resolved role is then posed.
- Every Spine model loads the way the client loads it — the local-client enemy and token models too, when `data/local-assets.json` lists them. That means pixi-spine's atlas reader with real page sizes, where a region outside its page throws, then `SkeletonBinary` with `AtlasAttachmentLoader`, where a missing region throws. Every resolved role is then posed.
`test/feedback1d-models.test.js` (enemies) and `test/local-token-models.test.js` (tokens) check the local-client overlays: the plan, the committed metadata, the manifest without `/assets/local/` URLs, the client's choice of model; `test/local-extract.test.js` the extractor's job table and helpers.
The 2026-09-27 verification pass also checked:
- **PNG:** all 2,211 PNGs pass a full CRC and inflate check.
+4 -1
View File
@@ -280,10 +280,13 @@ services:
| 官方 3D 棋盘(贴图、模型、地图特效) | 2D 棋盘,地块由程序绘制 |
| 部分官方界面图标与底板:交流按钮和表情面板的边框、暂停面板、装备替换窗口、干员调配界面、队友状态与漏怪标记、模组类型图标等 | 样式相近的替代图形、图标或文字 |
| 灼热 / 炽焰源石虫的官方模型 | 染成橙色 / 红橙色的普通源石虫 |
| 39 个召唤物的官方模型(多数自选召唤物,以及凯瑟琳的爬行号·防护单元、凛御银灰的风雪之眼;公开镜像没有) | 召唤物头像(菱形底板) |
表情(6 套 × 6 个)和「玩法说明」的 19 页教程图公开镜像也有:`node tools/setup.mjs` 会和其他素材一起下载(约 21 MB),不需要客户端;有本地素材时优先显示本地的。
**没有客户端的服务器**想要上表中的官方素材:从**同一版本**的完整包([Releases](https://github.com/sganggs/Stronghold-Protocol/releases))里,把 `public/assets/local/` 文件夹和 `data/local-assets.json` 复制到服务器项目目录下的相同位置。服务器每次请求都会重新读取这两处,不必重启,玩家刷新页面即可。一定要用与服务器代码相同版本的完整包:各版本提取的内容和清单可能不同(例如灼热 / 炽焰源石虫的模型是 0.1.0 之后才加入的),混用其他版本的文件会缺图或用错图。复制后 `node tools/doctor.mjs` 会显示本地素材的条目数和「3D 棋盘可用」。
**没有客户端的服务器**想要上表中的官方素材:从**同一版本**的完整包([Releases](https://github.com/sganggs/Stronghold-Protocol/releases))里,把 `public/assets/local/` 文件夹和 `data/local-assets.json` 复制到服务器项目目录下的相同位置。服务器每次请求都会重新读取这两处,不必重启,玩家刷新页面即可。一定要用与服务器代码相同版本的完整包:各版本提取的内容和清单可能不同(例如灼热 / 炽焰源石虫的模型是 0.1.0 之后才加入的,召唤物模型是 0.2.0 加入的),混用其他版本的文件会缺图或用错图。复制后 `node tools/doctor.mjs` 会显示本地素材的条目数和「3D 棋盘可用」。
**0.2.0 之前提取过的**:召唤物模型是 0.2.0 新增的提取项,旧的提取结果里没有(`node tools/setup.mjs` 会提示「缺少新版的自选召唤物模型」)。有客户端的电脑运行 `node tools/setup.mjs --local` 重新提取即可,只想补这一项也可以在提取用的 Python 环境里运行 `tools/local-extract/extract.py --only spine/token`(新文件写入 `public/assets/local/spine/token/`,清单里其他条目保持不变)。
**3D 棋盘贴图的下载量**:每位玩家进入对局时都要从开服的电脑下载 3D 棋盘的 12 张贴图。提取时会给这 12 张各写一份 WebP(颜色贴图有损、质量 95,法线和数据贴图无损),清单里列的是 WebP,同名 PNG 留在旁边给裁切工具和 setup 用。这部分下载量从约 6.7 MB 降到约 2 MB,网速慢的远程联机最明显。只有 PNG 的本地素材(例如在这一改动之前提取的)可以用提取时的 Python 环境运行 `tools/local-extract/extract.py --webp` 就地补上,只需要 Pillow,不需要客户端。
+1
View File
@@ -58,6 +58,7 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
- `guide/autochess_{home,shop,handbook}_N.png` — the 19 official tutorial pages; stored squashed to 1024² — **display at 16:9** (the mirror copies too, §22.5).
- `projectiles/*` — battle projectile sprites.
- `spine/enemy/<id>/` — enemy models no web dump carries (灼热 / 炽焰源石虫, `extract.py ENEMY_SPINES`): manifest `enemies[id].spineLocal` (metadata from the committed `tools/assets/local-enemy-spines.json`); drawn instead of the tinted web alias when every file is listed (§21.14).
- `spine/token/<id>/` — token (summon) models no web dump carries (39: most 自选 summons, 凯瑟琳's 爬行号·防护单元, 凛御银灰's 风雪之眼; `extract.py TOKEN_SPINES`, the Front renderer's skeleton of each battle token prefab): manifest `tokens[id].spineLocal` (metadata from the committed `tools/assets/local-token-spines.json`); drawn instead of the avatar diamond when every file is listed (docs/ASSETS.md "Token models from the local client").
- `module/<TYPE>` — the official module (uniequip) TYPE icons (white glyphs; keys are the client's mixed-case file names, e.g. `PRI-X`, `mar-x`, `isw-a`), matched **case-insensitively** against a ModuleRecord's `typeName` by `assetUrls.moduleTypeIconUrl(local, typeName)` (Greek type letters map to the file's Latin letter: `ISW-α` → `isw-a`) — shown on the 干员调配 module cards / module info, the detail panel's 模组 row and the shop card's module tag; lettered tiles / type text without them.
---