fix: 优先使用香槟炸弹原皮的本地模型覆盖 (#442)

This commit is contained in:
Siornya committed 2026-10-10 04:12:18 +08:00
1 parent 1db8e51023
commit 7ecc80d537
10 files changed
+111 -23

No files matched your search

+2 -2
View File
@@ -92,7 +92,7 @@ npm start # 启动服务器:http://localhost:3000
也可以直接运行启动脚本(Windows `scripts\start-windows.bat`,macOS / Linux `scripts/start.sh`):首次会自动安装依赖、下载素材,然后启动服务器并打开浏览器。 也可以直接运行启动脚本(Windows `scripts\start-windows.bat`,macOS / Linux `scripts/start.sh`):首次会自动安装依赖、下载素材,然后启动服务器并打开浏览器。
- **本地客户端素材(可选)**:官方 3D 棋盘、部分官方界面图标(交流按钮与表情面板的边框、模组类型图标等)、灼热 / 炽焰源石虫和 39 个召唤物(多数自选召唤物)的官方模型需要从本机的《明日方舟》PC 客户端提取(Windows 原生客户端、macOS 的 CrossOver 或 PlayCover)。`npm run setup` 检测到客户端时会询问是否提取(需要 Python 3.8+,依赖装在项目内的 `.venv-extract`,不影响系统);之后可以用 `node tools/setup.mjs --local` 重新提取,或用 `--game "<…/StreamingAssets/AB/Windows>"` 指定路径。没有客户端时游戏照常运行,这几样换成替代样式:2D 棋盘、样式相近的图标、染色的普通源石虫、召唤物头像。表情和「玩法说明」的教程图随上面的素材一起从公开镜像下载,不需要客户端。没有客户端的服务器(例如 Linux VPS)也可以从**同一版本**的整合包(完整包;精简包没有)里复制 `public/assets/local/` 和 `data/local-assets.json`,见 [docs/DEPLOY.md](docs/DEPLOY.md) 的「本地客户端素材」。 - **本地客户端素材(可选)**:官方 3D 棋盘、部分官方界面图标(交流按钮与表情面板的边框、模组类型图标等)、灼热 / 炽焰源石虫和 40 个召唤物(多数自选召唤物)的官方模型需要从本机的《明日方舟》PC 客户端提取(Windows 原生客户端、macOS 的 CrossOver 或 PlayCover)。`npm run setup` 检测到客户端时会询问是否提取(需要 Python 3.8+,依赖装在项目内的 `.venv-extract`,不影响系统);之后可以用 `node tools/setup.mjs --local` 重新提取,或用 `--game "<…/StreamingAssets/AB/Windows>"` 指定路径。没有客户端时游戏照常运行,这几样换成替代样式:2D 棋盘、样式相近的图标、染色的普通源石虫、召唤物头像。表情和「玩法说明」的教程图随上面的素材一起从公开镜像下载,不需要客户端。没有客户端的服务器(例如 Linux VPS)也可以从**同一版本**的整合包(完整包;精简包没有)里复制 `public/assets/local/` 和 `data/local-assets.json`,见 [docs/DEPLOY.md](docs/DEPLOY.md) 的「本地客户端素材」。
- 素材下载优先使用 GitHub,失败时自动改用 jsDelivr 镜像。 - 素材下载优先使用 GitHub,失败时自动改用 jsDelivr 镜像。
- `npm run doctor`(即 `node tools/doctor.mjs`)可以随时诊断:Node 版本、素材是否完整、端口占用、局域网地址和防火墙。 - `npm run doctor`(即 `node tools/doctor.mjs`)可以随时诊断:Node 版本、素材是否完整、端口占用、局域网地址和防火墙。
@@ -250,7 +250,7 @@ node tools/perfbench.mjs --cpu 1,4,6 --profile # 真实战斗的帧
An **unofficial, non-commercial fan remake** of Arknights' seasonal auto-chess tower-defense mode *Stronghold Protocol: Alliance*, played in the browser: solo, or 1–4 player co-op (AI teammates can fill seats). Combat is simulated in each player's browser, so a low-power PC can host. An **unofficial, non-commercial fan remake** of Arknights' seasonal auto-chess tower-defense mode *Stronghold Protocol: Alliance*, played in the browser: solo, or 1–4 player co-op (AI teammates can fill seats). Combat is simulated in each player's browser, so a low-power PC can host.
- **Run:** download the full bundle `Stronghold-Protocol-v<version>.zip` (~505 MB, all the art inside, the Chinese and Japanese operator voices included) from [Releases](../../releases/latest) — or the lite one, `…-lite.zip` (~22 MB), which downloads the art (~550 MB) on its first start; from 0.2.1 on, `…-update.zip` holds only the files changed since the earlier 0.2.x releases: stop the server, extract it over an existing 0.2.x folder and start again (the first start deletes the files the new version dropped and verifies the install) — install Node.js 22 or 24, then double-click `scripts\start-windows.bat` (Windows) or run `./scripts/start.sh` (macOS / Linux) and open <http://localhost:3000>. From source: `npm install && npm run setup && npm start` (setup downloads ~550 MB of art from public mirrors, the emotes and the how-to-play pages included; the official 3D board, some official HUD icons, two enemy models and 39 summon models are extracted from a local Arknights client — without one the game uses the 2D board and look-alike stand-ins (the summons show their avatars), and a server can copy `public/assets/local/` and `data/local-assets.json` from the full bundle of the same version). - **Run:** download the full bundle `Stronghold-Protocol-v<version>.zip` (~505 MB, all the art inside, the Chinese and Japanese operator voices included) from [Releases](../../releases/latest) — or the lite one, `…-lite.zip` (~22 MB), which downloads the art (~550 MB) on its first start; from 0.2.1 on, `…-update.zip` holds only the files changed since the earlier 0.2.x releases: stop the server, extract it over an existing 0.2.x folder and start again (the first start deletes the files the new version dropped and verifies the install) — install Node.js 22 or 24, then double-click `scripts\start-windows.bat` (Windows) or run `./scripts/start.sh` (macOS / Linux) and open <http://localhost:3000>. From source: `npm install && npm run setup && npm start` (setup downloads ~550 MB of art from public mirrors, the emotes and the how-to-play pages included; the official 3D board, some official HUD icons, two enemy models and 40 summon models are extracted from a local Arknights client — without one the game uses the 2D board and look-alike stand-ins (the summons show their avatars), and a server can copy `public/assets/local/` and `data/local-assets.json` from the full bundle of the same version).
- **Languages:** Chinese (the default), English, 日本語, 한국어 and 繁體中文 — switch on the title screen or in Settings. Game texts come from the official clients; the Japanese, Korean and Traditional Chinese interface strings are machine translations (corrections welcome: [docs/I18N.md](docs/I18N.md)). - **Languages:** Chinese (the default), English, 日本語, 한국어 and 繁體中文 — switch on the title screen or in Settings. Game texts come from the official clients; the Japanese, Korean and Traditional Chinese interface strings are machine translations (corrections welcome: [docs/I18N.md](docs/I18N.md)).
- **Play with friends:** create a co-op room and share the 4-letter key or the `?room=KEY` link. On a LAN, use the address printed at start; otherwise use a virtual-LAN tool, a tunnel or a VPS — see [docs/DEPLOY.md](docs/DEPLOY.md). - **Play with friends:** create a co-op room and share the 4-letter key or the `?room=KEY` link. On a LAN, use the address printed at start; otherwise use a virtual-LAN tool, a tunnel or a VPS — see [docs/DEPLOY.md](docs/DEPLOY.md).
- **Disclaimer:** not affiliated with or endorsed by Hypergryph or Yostar. All Arknights names, art, audio, text and data are © their respective owners and are **not** covered by this project's GPL licence. For study and personal non-commercial use only — no selling, paid distribution, paid servers or monetisation of any kind. Content will be removed on request of the rights holders. Provided "as is", without warranty. - **Disclaimer:** not affiliated with or endorsed by Hypergryph or Yostar. All Arknights names, art, audio, text and data are © their respective owners and are **not** covered by this project's GPL licence. For study and personal non-commercial use only — no selling, paid distribution, paid servers or monetisation of any kind. Content will be removed on request of the rights holders. Provided "as is", without warranty.
+1 -1
View File
@@ -1 +1 @@
{"version":1,"hash":"cf275d509d29","generator":"tools/fetch-assets.mjs","stats":{"files":10956,"bytes":581063469,"chars":210,"charsWithBack":207,"enemies":252,"enemiesWithSpine":251,"tokens":58,"tokensWithSpine":13,"spineModels":673,"bonds":23,"items":59,"bands":40,"skills":525,"modules":131,"ui":602,"sfxUnits":484,"voiceChars":192,"voiceJpChars":192},"chars":{"char_003_kalts":{"avatar":"/assets/char/avatar/char_003_kalts.png","avatarE2":"/assets/char/avatar/char_003_kalts_2.png","portrait":"/assets/char/portrait/char_003_kalts_1.png","portraitE2":"/assets/char/portrait/char_003_kalts_2.png","spine":{"front":{"skel":"/assets/spine/op/char_003_kalts/front/char_003_kalts.skel","atlas":"/assets/spine/op/char_003_kalts/front/char_003_kalts.atlas","textures":["/assets/spine/op/char_003_kalts/front/char_003_kalts.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"die":"Die","move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"1":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":1,"idle":null},"2":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":2,"idle":null}}},"animations":{"Attack":1,"Default":0,"Die":1,"Idle":9,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433]},"bounds":{"x":-227.579,"y":-138.244,"width":532.04,"height":551.479}},"back":{"skel":"/assets/spine/op/char_003_kalts/back/char_003_kalts.skel","atlas":"/assets/spine/op/char_003_kalts/back/char_003_kalts.atlas","textures":["/assets/spine/op/char_003_kalts/back/char_003_kalts.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"die":null,"move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"1":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":1,"idle":null},"2":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":2,"idle":null}}},"animations":{"Attack":1,"Default":0,"Idle":9,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433]},"bounds":{"x":410.785,"y":-2.26,"width":531.785,"height":416.492}}}},"char_010_chen":{"avatar":"/assets/char/avatar/char_010_chen.png","avatarE2":"/assets/char/avatar/char_010_chen_2.png","portrait":"/assets/char/portrait/char_010_chen_1.png","portraitE2":"/assets/char/portrait/char_010_chen_2.png","spine":{"front":{"skel":"/assets/spine/op/char_010_chen/front/char_010_chen.skel","atlas":"/assets/spine/op/char_010_chen/front/char_010_chen.atlas","textures":["/assets/spine/op/char_010_chen/front/char_010_chen.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"die":"Die","move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"1":{"begin":null,"loop":"Skill_2","end":null,"index":1,"idle":null},"2":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":null}}},"animations":{"Attack":1.5,"Attack_End":0.6,"Attack_Pre":0.6,"Default":0,"Die":1,"Idle":2,"Skill":1,"Skill_2":1.167,"Skill_3":2.933,"Skill_End":0.6,"Skill_End_2":0.6,"Skill_End_3":0.3,"Start":0.833},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433,1],"Skill":[0.467],"Skill_2":[0.467],"Skill_3":[0.6,0.8,1,1.2,1.4,1.6,1.8,2,2.2,2.833,2.933]},"bounds":{"x":-322.335,"y":-109.274,"width":663.549,"height":524.806}},"back":{"skel":"/assets/spine/op/char_010_chen/back/char_010_chen.skel","atlas":"/assets/spine/op/char_010_chen/back/char_010_chen.atlas","textures":["/assets/spine/op/char_010_chen/back/char_010_chen.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"die":null,"move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"1":{"begin":null,"loop":"Skill","end":"Skill_End","index":1,"idle":null},"2":{"begin":null,"loop":"Skill","end":"Skill_End","index":2,"idle":null}}},"animations":{"Attack":1.5,"Attack_End":0.6,"Attack_Pre":0.6,"Default":0,"Idle":2,"Skill":1,"Skill_End":0.6,"Start":0.833},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433,1],"Skill":[0.467]},"bounds":{"x":-173.082,"y":-13.829,"width":315.887,"height":404.623}}}},"char_017_huang":{"avatar":"/assets/char/avatar/char_017_huang.png","avatarE2":"/assets/char/avatar/char_017_huang_2.png","portrait":"/assets/char/portrait/char_017_huang_1.png","portraitE2":"/assets/char/portrait/char_017_huang_2.png","spine":{"front":{"skel":"/assets/spine/op/char_017_huang/frontLine truncated {"version":1,"hash":"2424fdc0a6b5","generator":"tools/fetch-assets.mjs","stats":{"files":10956,"bytes":581063469,"chars":210,"charsWithBack":207,"enemies":252,"enemiesWithSpine":251,"tokens":58,"tokensWithSpine":13,"spineModels":673,"bonds":23,"items":59,"bands":40,"skills":525,"modules":131,"ui":602,"sfxUnits":484,"voiceChars":192,"voiceJpChars":192},"chars":{"char_003_kalts":{"avatar":"/assets/char/avatar/char_003_kalts.png","avatarE2":"/assets/char/avatar/char_003_kalts_2.png","portrait":"/assets/char/portrait/char_003_kalts_1.png","portraitE2":"/assets/char/portrait/char_003_kalts_2.png","spine":{"front":{"skel":"/assets/spine/op/char_003_kalts/front/char_003_kalts.skel","atlas":"/assets/spine/op/char_003_kalts/front/char_003_kalts.atlas","textures":["/assets/spine/op/char_003_kalts/front/char_003_kalts.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"die":"Die","move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"1":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":1,"idle":null},"2":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":2,"idle":null}}},"animations":{"Attack":1,"Default":0,"Die":1,"Idle":9,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433]},"bounds":{"x":-227.579,"y":-138.244,"width":532.04,"height":551.479}},"back":{"skel":"/assets/spine/op/char_003_kalts/back/char_003_kalts.skel","atlas":"/assets/spine/op/char_003_kalts/back/char_003_kalts.atlas","textures":["/assets/spine/op/char_003_kalts/back/char_003_kalts.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"die":null,"move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":0,"idle":null},"1":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":1,"idle":null},"2":{"begin":null,"loop":"Attack","end":null,"via":"attack","index":2,"idle":null}}},"animations":{"Attack":1,"Default":0,"Idle":9,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433]},"bounds":{"x":410.785,"y":-2.26,"width":531.785,"height":416.492}}}},"char_010_chen":{"avatar":"/assets/char/avatar/char_010_chen.png","avatarE2":"/assets/char/avatar/char_010_chen_2.png","portrait":"/assets/char/portrait/char_010_chen_1.png","portraitE2":"/assets/char/portrait/char_010_chen_2.png","spine":{"front":{"skel":"/assets/spine/op/char_010_chen/front/char_010_chen.skel","atlas":"/assets/spine/op/char_010_chen/front/char_010_chen.atlas","textures":["/assets/spine/op/char_010_chen/front/char_010_chen.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"die":"Die","move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"1":{"begin":null,"loop":"Skill_2","end":null,"index":1,"idle":null},"2":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":null}}},"animations":{"Attack":1.5,"Attack_End":0.6,"Attack_Pre":0.6,"Default":0,"Die":1,"Idle":2,"Skill":1,"Skill_2":1.167,"Skill_3":2.933,"Skill_End":0.6,"Skill_End_2":0.6,"Skill_End_3":0.3,"Start":0.833},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433,1],"Skill":[0.467],"Skill_2":[0.467],"Skill_3":[0.6,0.8,1,1.2,1.4,1.6,1.8,2,2.2,2.833,2.933]},"bounds":{"x":-322.335,"y":-109.274,"width":663.549,"height":524.806}},"back":{"skel":"/assets/spine/op/char_010_chen/back/char_010_chen.skel","atlas":"/assets/spine/op/char_010_chen/back/char_010_chen.atlas","textures":["/assets/spine/op/char_010_chen/back/char_010_chen.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"die":null,"move":null,"stun":null,"skills":{"0":{"begin":null,"loop":"Skill","end":"Skill_End","index":0,"idle":null},"1":{"begin":null,"loop":"Skill","end":"Skill_End","index":1,"idle":null},"2":{"begin":null,"loop":"Skill","end":"Skill_End","index":2,"idle":null}}},"animations":{"Attack":1.5,"Attack_End":0.6,"Attack_Pre":0.6,"Default":0,"Idle":2,"Skill":1,"Skill_End":0.6,"Start":0.833},"events":["OnAttack","OnStart"],"hits":{"Attack":[0.433,1],"Skill":[0.467]},"bounds":{"x":-173.082,"y":-13.829,"width":315.887,"height":404.623}}}},"char_017_huang":{"avatar":"/assets/char/avatar/char_017_huang.png","avatarE2":"/assets/char/avatar/char_017_huang_2.png","portrait":"/assets/char/portrait/char_017_huang_1.png","portraitE2":"/assets/char/portrait/char_017_huang_2.png","spine":{"front":{"skel":"/assets/spine/op/char_017_huang/frontLine truncated
+7 -3
View File
@@ -159,7 +159,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}.*` | | 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 (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`) | | 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`) | | Token Spine that no dump carries (40 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` | | 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` | | 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` (Chinese, `audio.voice`) and `voice/{charId}/cn_nn.mp3` (Japanese, `audio.voiceJp`: the same file names) — the lines `charword_table.json` lists (`placeType` = when the game plays one, `voiceAsset` = the path); `--voice-lang=jp|en|kr` fills `audio.voice` from `voice/`, `voice_en/`, `voice_kr/` instead | `audio/voice/cn/{charId}/{cn_nn}.mp3`, `audio/voice/jp/{charId}/{cn_nn}.mp3` (`audio/voice/{lang}/…` for another `--voice-lang`) | | 干员战斗语音 | AA2 `voice` `audio/sound_beta_2/voice_cn/{charId}/cn_nn.mp3` (Chinese, `audio.voice`) and `voice/{charId}/cn_nn.mp3` (Japanese, `audio.voiceJp`: the same file names) — the lines `charword_table.json` lists (`placeType` = when the game plays one, `voiceAsset` = the path); `--voice-lang=jp|en|kr` fills `audio.voice` from `voice/`, `voice_en/`, `voice_kr/` instead | `audio/voice/cn/{charId}/{cn_nn}.mp3`, `audio/voice/jp/{charId}/{cn_nn}.mp3` (`audio/voice/{lang}/…` for another `--voice-lang`) |
@@ -418,7 +418,7 @@ Other renderer rules from research 07 §5.4–5.5:
"missing skel"), nor of 凯瑟琳's 爬行号·防护单元 and 凛御银灰's 风雪之眼, so they were drawn as the avatar diamond. The "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; 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: 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 - `tools/local-extract/extract.py TOKEN_SPINES` (40 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 (`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 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, writes it to `public/assets/local/spine/token/{tokenId}/` like an enemy model (skeleton, sized `pma: true` atlas,
@@ -430,6 +430,10 @@ Other renderer rules from research 07 §5.4–5.5:
per-prefab factor (the official prefabs scale most tokens by the standard 0.27; W's 此面向敌 0.4, 令's “清平” 0.25 and 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.3, 傀影's 镜中虚影 0.26, 风雪之眼 and 淬羽赫默's 夜灯 0.25 — like the web tokens' 医疗探机 / 诅咒娃娃 0.4 and
香槟炸弹 0.25). 香槟炸弹 0.25).
- 香槟炸弹的原皮也纳入此覆盖(#442)。[PRTS 模型清单](https://torappu.prts.wiki/assets/char_spine/token_10031_swire2_gdtrap/meta.json)
的 `默认/正面` 提供 `.skel`、同名 `.atlas` 及图集引用的 `.png`;放入
`public/assets/local/spine/token/token_10031_swire2_gdtrap/` 并登记到 `data/local-assets.json` 后优先显示原皮。
未安装完整原皮时回退到现有皮肤模型;正常素材下载不会下载此本地覆盖。
- Clip names the resolver cannot read are mapped in `tools/assets/spine.mjs LOCAL_SPINE_ROLES`: 电弧's 戴乌 - 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 (`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 goes) [ASSUMED: by the clip names], and 凯尔希·思衡托's 战术锚点, whose Start / Idle / Die clips hide its only
@@ -441,7 +445,7 @@ Other renderer rules from research 07 §5.4–5.5:
### Other fallbacks ### 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 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. - **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 40 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:** - **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 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 (and without its local-client model, "Token models from the local client"), 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.
+1 -1
View File
@@ -302,7 +302,7 @@ services:
| 官方 3D 棋盘(贴图、模型、地图特效) | 2D 棋盘,地块由程序绘制 | | 官方 3D 棋盘(贴图、模型、地图特效) | 2D 棋盘,地块由程序绘制 |
| 部分官方界面图标与底板:交流按钮和表情面板的边框、暂停面板、装备替换窗口、干员调配界面、队友状态与漏怪标记、模组类型图标等 | 样式相近的替代图形、图标或文字 | | 部分官方界面图标与底板:交流按钮和表情面板的边框、暂停面板、装备替换窗口、干员调配界面、队友状态与漏怪标记、模组类型图标等 | 样式相近的替代图形、图标或文字 |
| 灼热 / 炽焰源石虫的官方模型 | 染成橙色 / 红橙色的普通源石虫 | | 灼热 / 炽焰源石虫的官方模型 | 染成橙色 / 红橙色的普通源石虫 |
| 39 个召唤物的官方模型(多数自选召唤物,以及凯瑟琳的爬行号·防护单元、凛御银灰的风雪之眼;公开镜像没有) | 召唤物头像(菱形底板) | | 40 个召唤物的官方模型(多数自选召唤物,以及凯瑟琳的爬行号·防护单元、凛御银灰的风雪之眼、香槟炸弹的原皮模型) | 召唤物头像(菱形底板);香槟炸弹回退到现有皮肤模型 |
表情(6 套 × 6 个)和「玩法说明」的 19 页教程图公开镜像也有:`node tools/setup.mjs` 会和其他素材一起下载(约 21 MB),不需要客户端;有本地素材时优先显示本地的。 表情(6 套 × 6 个)和「玩法说明」的 19 页教程图公开镜像也有:`node tools/setup.mjs` 会和其他素材一起下载(约 21 MB),不需要客户端;有本地素材时优先显示本地的。
+4 -1
View File
@@ -63,11 +63,14 @@ 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). - `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. - `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/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"). - `spine/token/<id>/` — token (summon) models no web dump carries (40: 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. - `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.
--- ---
香槟炸弹也使用原皮的本地模型覆盖(#442):`tokens[id].spineLocal` 对应的全部文件已在本地清单登记时
优先显示原皮;任一文件缺失或加载失败时仍回退到现有皮肤模型。此覆盖不改变爆炸触发或战斗规则。
## 15. Official 3D board scene (v2 visual target) ## 15. Official 3D board scene (v2 visual target)
Findings (2026-09-28): 卫戍协议 levels have no scene bundle (`mapId` null; the official hot-update list contains none). The client **builds the board at runtime** from the stage grid using the map theme `arts/maps/map_autochess` (materials `MT_autochess` (albedo `TX_autochessi_D` 2048², normal `_N`, metallic/gloss `_M`, emission `_E`), `MT_autochess_common` (`common_D/_E`: device/gate icons), `MT_autochess_Transparent` (`Transparent_D/N/M/E`), `MT_autochessi_BG` (`TX_autochessi_BG`)), the background meshes `arts/maps/map_autochess/bkg_mesh.ab` (`S_Background_common`, `S_Background_shadow`) and `arts/maps/common/meshes/s_background_common.ab`, device meshes (`s_common_box_01` crate, `s_wind_device` blower, …), plus the standard gate/objective effects. The level's `options.configBlackBoard` carries the official camera framing (`default_pos_x/y`, `default_size`, `enemy_pos_x/y`, …). Findings (2026-09-28): 卫戍协议 levels have no scene bundle (`mapId` null; the official hot-update list contains none). The client **builds the board at runtime** from the stage grid using the map theme `arts/maps/map_autochess` (materials `MT_autochess` (albedo `TX_autochessi_D` 2048², normal `_N`, metallic/gloss `_M`, emission `_E`), `MT_autochess_common` (`common_D/_E`: device/gate icons), `MT_autochess_Transparent` (`Transparent_D/N/M/E`), `MT_autochessi_BG` (`TX_autochessi_BG`)), the background meshes `arts/maps/map_autochess/bkg_mesh.ab` (`S_Background_common`, `S_Background_shadow`) and `arts/maps/common/meshes/s_background_common.ab`, device meshes (`s_common_box_01` crate, `s_wind_device` blower, …), plus the standard gate/objective effects. The level's `options.configBlackBoard` carries the official camera framing (`default_pos_x/y`, `default_size`, `enemy_pos_x/y`, …).
+11
View File
@@ -479,3 +479,14 @@ imports and typecheck pass. This round changes no production UI and does not rer
the `roster`, `bonds`, `fields`, `matches`, `standins` families are unchanged. the `roster`, `bonds`, `fields`, `matches`, `standins` families are unchanged.
- **Contracts updated:** design/engine.md, SIM.md (cabin), design/client.md, design/network.md, META.md and PLAYING.md - **Contracts updated:** design/engine.md, SIM.md (cabin), design/client.md, design/network.md, META.md and PLAYING.md
(local identity recovery and handshake). (local identity recovery and handshake).
### 28.28 香槟炸弹原皮的本地模型覆盖 — #442
- **问题**:琳琅诗怀雅使用原皮,而香槟炸弹使用公开素材镜像中的 `ambienceSynesthesia_4` 皮肤模型。
- **来源**:[PRTS 模型清单](https://torappu.prts.wiki/assets/char_spine/token_10031_swire2_gdtrap/meta.json) 的
`默认/正面` 指向 `defaultskin/front/token_10031_swire2_gdtrap`,提供完整的 Spine 3.8.99 骨骼、图集和贴图。
- **现在**:原皮纳入 `TOKEN_SPINES` 及本地模型元数据;`tokens[id].spineLocal` 的全部文件安装并登记到
`data/local-assets.json` 后,现有加载器优先使用原皮。任一文件缺失或加载失败时保留皮肤回退。
正常素材下载不包含此本地覆盖,美术文件仍不入 Git。伤害、治疗和爆炸触发规则不变。
- **验证**:`test/local-extract.test.js` 验证提取列表,`test/local-token-models.test.js` 验证原皮优先、
缺失任一文件时的回退及元数据一致性。本机 Safari 已用项目的 Pixi/Spine 加载器显示原皮。黄金结果不变。
+4 -3
View File
@@ -48,9 +48,9 @@ describe('extract.py helpers (no UnityPy needed)', { skip: !PY && 'no python3' }
assert.equal(tokenSpines.bundles, 'pkgrps/btl_pfb_tokens_*.ab'); assert.equal(tokenSpines.bundles, 'pkgrps/btl_pfb_tokens_*.ab');
assert.equal(tokenSpines.sub, 'spine/token'); assert.equal(tokenSpines.sub, 'spine/token');
assert.equal(enemySpines.sub, 'spine/enemy'); assert.equal(enemySpines.sub, 'spine/enemy');
// every listed id is a token of the game data without a web model (data/assets.json tokens[id].spine), sorted, unique // Sorted game tokens whose original model needs local art; champagne has a web skin fallback.
const ids = tokenSpines.ids; const ids = tokenSpines.ids;
assert.equal(ids.length, 39); assert.equal(ids.length, 40);
assert.deepEqual([...ids].sort(), ids); assert.deepEqual([...ids].sort(), ids);
assert.equal(new Set(ids).size, ids.length); assert.equal(new Set(ids).size, ids.length);
const tokens = JSON.parse(readFileSync(path.join(ROOT, 'data/tokens.json'), 'utf8')); const tokens = JSON.parse(readFileSync(path.join(ROOT, 'data/tokens.json'), 'utf8'));
@@ -58,7 +58,8 @@ describe('extract.py helpers (no UnityPy needed)', { skip: !PY && 'no python3' }
const web = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8')).tokens; const web = JSON.parse(readFileSync(path.join(ROOT, 'data/assets.json'), 'utf8')).tokens;
for (const id of ids) { for (const id of ids) {
assert.ok(tokens[id] || backups.tokens[id], `${id}: a token of the game data`); assert.ok(tokens[id] || backups.tokens[id], `${id}: a token of the game data`);
assert.equal(web[id]?.spine, undefined, `${id}: no web model`); if (id === 'token_10031_swire2_gdtrap') assert.match(web[id]?.spine?.skel, /ambienceSynesthesia_4/, 'champagne keeps its skin fallback');
else assert.equal(web[id]?.spine, undefined, `${id}: no web model`);
} }
// the 自选 summons whose prefab draws nothing (an EmptyAnimator: displayType HIDDEN / an effect) are left out // the 自选 summons whose prefab draws nothing (an EmptyAnimator: displayType HIDDEN / an effect) are left out
for (const id of ['token_10039_ulpia_block', 'token_10055_phatm2_mndclv', 'token_10058_sbell2_icetgt', 'token_10065_demetr_dmtpos', for (const id of ['token_10039_ulpia_block', 'token_10055_phatm2_mndclv', 'token_10058_sbell2_icetgt', 'token_10065_demetr_dmtpos',
+29 -11
View File
@@ -1,5 +1,5 @@
// test/local-token-models.test.js — the token (summon) models only the local client has (0.2.0): 39 summons — most of the // test/local-token-models.test.js — 40 optional original summon models, including champagne's PRTS model (#442).
// 自选 picks' (data/backups.json `tokens`), 凯瑟琳's 爬行号·防护单元 and 凛御银灰's 风雪之眼 — have no battle Spine in // Most 自选 picks' (data/backups.json `tokens`), 凯瑟琳's 爬行号·防护单元 and 凛御银灰's 风雪之眼 have no battle Spine in
// any community dump (fetch-assets reported "missing skel"), so the client drew their avatar diamond. Their official // any community dump (fetch-assets reported "missing skel"), so the client drew their avatar diamond. Their official
// skeletons come from the battle token packs of the local client (tools/local-extract/extract.py TOKEN_SPINES → // skeletons come from the battle token packs of the local client (tools/local-extract/extract.py TOKEN_SPINES →
// public/assets/local/spine/token/<id>/, optional and git-ignored) and are an OVERLAY like the enemy models of // public/assets/local/spine/token/<id>/, optional and git-ignored) and are an OVERLAY like the enemy models of
@@ -55,7 +55,7 @@ function localManifest(ids, drop = null) {
describe('the 自选 summon models from the local client are an optional overlay', () => { describe('the 自选 summon models from the local client are an optional overlay', () => {
test('the committed metadata: the TOKEN_SPINES of extract.py, each a skeleton + same-stem atlas + pages, its roles on its own clips', () => { test('the committed metadata: the TOKEN_SPINES of extract.py, each a skeleton + same-stem atlas + pages, its roles on its own clips', () => {
assert.equal(IDS.length, 39); assert.equal(IDS.length, 40);
if (TOKEN_SPINES) assert.deepEqual(IDS, [...TOKEN_SPINES].sort(), 'one entry per extract.py TOKEN_SPINES id'); if (TOKEN_SPINES) assert.deepEqual(IDS, [...TOKEN_SPINES].sort(), 'one entry per extract.py TOKEN_SPINES id');
for (const id of IDS) { for (const id of IDS) {
const m = COMMITTED[id]; const m = COMMITTED[id];
@@ -116,11 +116,14 @@ describe('the 自选 summon models from the local client are an optional overlay
assert.deepEqual(withLocal, IDS); assert.deepEqual(withLocal, IDS);
for (const id of IDS) { for (const id of IDS) {
assert.deepEqual(MANIFEST.tokens[id].spineLocal, { group: `spine/token/${id}`, ...COMMITTED[id] }, `${id}: the committed metadata`); assert.deepEqual(MANIFEST.tokens[id].spineLocal, { group: `spine/token/${id}`, ...COMMITTED[id] }, `${id}: the committed metadata`);
assert.equal(MANIFEST.tokens[id].spine, undefined, `${id}: no web model (it was the avatar diamond)`); const fallback = MANIFEST.tokens[id].spine ?? null;
assert.equal(spineEntry(MANIFEST, id), null, `${id}: without the local art the client keeps the diamond`); assert.equal(spineEntry(MANIFEST, id), fallback, `${id}: without the local art the client keeps its fallback`);
} }
// the tokens that always had a web model keep it, without an overlay // Existing web models stay valid; only registered originals add an overlay.
for (const [id, t] of Object.entries(MANIFEST.tokens)) if (t.spine) { assert.ok(validSpine(t.spine), id); assert.equal(t.spineLocal, undefined, id); } for (const [id, t] of Object.entries(MANIFEST.tokens)) if (t.spine) {
assert.ok(validSpine(t.spine), id);
if (!IDS.includes(id)) assert.equal(t.spineLocal, undefined, id);
}
}); });
test('the committed metadata is what the extracted models parse to', { skip: !IDS.every((id) => existsSync(path.join(ASSETS, 'local/spine/token', id))) && 'token models not extracted (tools/local-extract)' }, async () => { test('the committed metadata is what the extracted models parse to', { skip: !IDS.every((id) => existsSync(path.join(ASSETS, 'local/spine/token', id))) && 'token models not extracted (tools/local-extract)' }, async () => {
@@ -155,7 +158,21 @@ describe('the 自选 summon models from the local client are an optional overlay
}); });
describe('client: the official summon model only when data/local-assets.json lists it', () => { describe('client: the official summon model only when data/local-assets.json lists it', () => {
test('assets.js spineEntry: every file listed → the official model; anything missing → none (the diamond); web models unchanged', () => { test('#442: original champagne model takes priority over its skin model; incomplete files keep the web fallback', () => {
const id = 'token_10031_swire2_gdtrap';
const web = MANIFEST.tokens[id].spine;
assert.match(web.skel, /ambienceSynesthesia_4/);
const original = spineEntry(MANIFEST, id, { local: localManifest([id]) });
assert.equal(original.skel, `/assets/local/spine/token/${id}/${id}.skel`);
assert.equal(original.pma, true);
assert.deepEqual(original.anims, COMMITTED[id].anims);
assert.equal(original.fallback, web);
for (const file of [COMMITTED[id].skel, COMMITTED[id].atlas, ...COMMITTED[id].textures]) {
assert.equal(spineEntry(MANIFEST, id, { local: localManifest([id], file) }), web, `${file}: incomplete model`);
}
});
test('assets.js spineEntry: every file listed → the official model; anything missing → the existing fallback', () => {
const local = localManifest(IDS); const local = localManifest(IDS);
for (const id of IDS) { for (const id of IDS) {
const m = COMMITTED[id]; const m = COMMITTED[id];
@@ -166,10 +183,11 @@ describe('client: the official summon model only when data/local-assets.json lis
assert.equal(e.pma, true); assert.equal(e.pma, true);
assert.deepEqual(e.anims, m.anims); assert.deepEqual(e.anims, m.anims);
assert.equal(e.local, true); assert.equal(e.local, true);
assert.equal(e.fallback, null, 'no web model to fall back to'); const fallback = MANIFEST.tokens[id].spine ?? null;
assert.equal(e.fallback, fallback, 'preserves the existing web model, if any');
assert.equal(spineEntry(MANIFEST, id, { local }), e, 'one entry object per manifest pair'); assert.equal(spineEntry(MANIFEST, id, { local }), e, 'one entry object per manifest pair');
assert.equal(spineEntry(MANIFEST, id, { local: localManifest([]) }), null, 'not extracted → no model'); assert.equal(spineEntry(MANIFEST, id, { local: localManifest([]) }), fallback, 'not extracted → existing fallback');
assert.equal(spineEntry(MANIFEST, id, { local: localManifest([id], m.textures[0]) }), null, 'a page missing → no model'); assert.equal(spineEntry(MANIFEST, id, { local: localManifest([id], m.textures[0]) }), fallback, 'a page missing → existing fallback');
} }
assert.equal(spineEntry(MANIFEST, 'token_10028_vigil_wolf', { local }), MANIFEST.tokens.token_10028_vigil_wolf.spine, 'a web token unchanged'); assert.equal(spineEntry(MANIFEST, 'token_10028_vigil_wolf', { local }), MANIFEST.tokens.token_10028_vigil_wolf.spine, 'a web token unchanged');
const store = createAssets({ manifest: MANIFEST, localManifest: local }); const store = createAssets({ manifest: MANIFEST, localManifest: local });
+51
View File
@@ -959,6 +959,57 @@
"height": 241 "height": 241
} }
}, },
"token_10031_swire2_gdtrap": {
"skel": "token_10031_swire2_gdtrap.skel",
"atlas": "token_10031_swire2_gdtrap.atlas",
"textures": [
"token_10031_swire2_gdtrap.png"
],
"pma": true,
"anims": {
"idle": "Idle",
"deploy": "Start",
"attack": {
"begin": null,
"loop": "Attack",
"end": null
},
"attackDown": null,
"skill": {
"begin": null,
"loop": "Attack",
"end": null,
"via": "attack",
"index": 0,
"idle": null
},
"die": "Die",
"move": null,
"stun": null
},
"animations": {
"Attack": 0.2,
"Default": 0,
"Die": 0.467,
"Idle": 2,
"Start": 0.667
},
"events": [
"OnAttack",
"OnStart"
],
"hits": {
"Attack": [
0.2
]
},
"bounds": {
"x": -75.631,
"y": -8.563,
"width": 152,
"height": 225.923
}
},
"token_10032_jesca2_jckshd": { "token_10032_jesca2_jckshd": {
"skel": "token_10032_jesca2_jckshd.skel", "skel": "token_10032_jesca2_jckshd.skel",
"atlas": "token_10032_jesca2_jckshd.atlas", "atlas": "token_10032_jesca2_jckshd.atlas",
+1 -1
View File
@@ -137,7 +137,7 @@ TOKEN_SPINES = [
'token_10005_mgllan_drone3', 'token_10007_phatom_twin', 'token_10008_cqbw_box', 'token_10009_weedy_cannon', 'token_10005_mgllan_drone3', 'token_10007_phatom_twin', 'token_10008_cqbw_box', 'token_10009_weedy_cannon',
'token_10020_ling_soul1', 'token_10020_ling_soul2', 'token_10020_ling_soul3', 'token_10024_ebnhlz_rcube', 'token_10020_ling_soul1', 'token_10020_ling_soul2', 'token_10020_ling_soul3', 'token_10024_ebnhlz_rcube',
'token_10025_doroth_recttp', 'token_10026_bgsnow_subbow', 'token_10027_ironmn_pile1', 'token_10027_ironmn_pile2', 'token_10025_doroth_recttp', 'token_10026_bgsnow_subbow', 'token_10027_ironmn_pile1', 'token_10027_ironmn_pile2',
'token_10027_ironmn_pile3', 'token_10029_slent2_protrb', 'token_10032_jesca2_jckshd', 'token_10034_ray_sndbst', 'token_10027_ironmn_pile3', 'token_10029_slent2_protrb', 'token_10031_swire2_gdtrap', 'token_10032_jesca2_jckshd', 'token_10034_ray_sndbst',
'token_10035_wisdel_wward', 'token_10041_cathy_catsld', 'token_10043_necras_skeltn', 'token_10050_monstr_prosts', 'token_10035_wisdel_wward', 'token_10041_cathy_catsld', 'token_10043_necras_skeltn', 'token_10050_monstr_prosts',
'token_10051_radian_tower1', 'token_10052_radian_tower2', 'token_10053_radian_tower3', 'token_10054_phatm2_encdool', 'token_10051_radian_tower1', 'token_10052_radian_tower2', 'token_10053_radian_tower3', 'token_10054_phatm2_encdool',
'token_10057_svash2_eagle1', 'token_10057_svash2_eagle2', 'token_10057_svash2_eagle3', 'token_10059_nasti_nstdef', 'token_10057_svash2_eagle1', 'token_10057_svash2_eagle2', 'token_10057_svash2_eagle3', 'token_10059_nasti_nstdef',