fix: 优先使用香槟炸弹原皮的本地模型覆盖 (#442)
This commit is contained in:
1 parent
1db8e51023
commit
7ecc80d537
10 files changed
+111
-23
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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),不需要客户端;有本地素材时优先显示本地的。
|
||||||
|
|
||||||
|
|||||||
@@ -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`, …).
|
||||||
|
|||||||
@@ -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 加载器显示原皮。黄金结果不变。
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in new issue
Block a user