Files
ark-stronghold-protocol/docs/research/07-assets.md
T
sganggs 01a44cd288 卫戍协议:盟约 web remake — initial import
Online co-op browser remake of Arknights' Stronghold Protocol: Alliance
(client-simulated combat, official waves/bonds/items/strategies, 3D board,
facing wheel, operator loadout, official emotes, deployment scripts).
2026-09-29 16:21:31 +08:00

479 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 07 — Art and audio assets: sources, feasibility and the final plan
Scope: every art and audio asset the fan remake of **卫戍协议:盟约 (act2autochess)** needs, with **URL patterns that have been checked**, measured hit rates, file sizes and fallbacks.
Machine-readable companion: **`docs/research/07-assets.json`**. It maps every id to resolved URLs and byte sizes: 138 operators, 20 tokens, 200 enemies, 23 bonds, 59 shop items, 40 bands, the UI sprite groups and the audio banks.
**All 4,451 unique URLs in that JSON were HEAD-checked on 2026-09-27 and returned `200`.**
Probe artifacts are in the scratchpad at `…/scratchpad/gd/extra/assets/`:
- Git-tree listings: `y_*.txt`, `fx_spine.txt`, `am_enemies.txt`, `aa2_*.txt`, `aa2v_*.txt`.
- Id lists: `charids.txt`, `enemyids.txt`, `tokenids.txt`, `trapids.txt`.
- Spine sample: `spine_sample/`.
- Browser render proof: `spine_test.png`, produced from `spine_test.html`.
- Generator script: `gen_assets_json.py`.
---
## 0. TL;DR: the final plan
| Class | Source (primary) | Pattern | Hit rate (pool) | Typical file | Fallback |
|---|---|---|---|---|---|
| a) Avatar (square) | yuanyan3060/ArknightsGameResource `avatar/` | `{charId}.png`, E2: `{charId}_2.png` | **138/138**; E2 122/138 | 180×180 PNG, ~58 KB | E2 missing → base `{charId}.png` (the 16 misses are the 预备干员/Sharp/Pith/… `char_60x/61x` ids that have no E2 art) |
| b) Half-body portrait | same repo `portrait/` | `{charId}_1.png`, E2 `_2.png` | **138/138**; E2 122/138 | 180×360 PNG, ~117 KB | `_1` |
| b') Full art (detail panel only) | same repo `skin/` | `{charId}_1b.png`, E2 `_2b.png` | 138/138; E2 122/138 | 681×1317, ~0.8 MB (E2 ~3.2 MB) | portrait `_1`; lazy-load only |
| c) Skill icon | same repo `skill/` | `skill_icon_{skill.iconId ?? skillId}.png` (percent-encode `[` `]`) | **317/317** (all skills of pool operators; 144 are default skills) | 128×128, ~19 KB | `arts/ui/[uc]charcommon/skills/empty_skill.png` |
| d) Profession / sub-profession | ArknightsAssets/ArknightsAssets2 `@cn` `assets/dyn/arts/…` | `profession_hub/icon_{prof}.png`, `ui/subprofessionicon/sub_{subProfessionId}_icon.png` | 8/8, **58/58** | ~12 KB / ~2 KB | battlecard `icon_profession_*.png` |
| e) Enemy icon | yuanyan `enemy/` | `{enemyId}.png` | **200/200** | ~158×158, ~44 KB | none needed |
| f) **Bond icons: the real autochess ones** | AA2 `ui/autochess/[uc]autochesscommon/arts/bondicon/` | `{iconId.toLowerCase()}.png`, e.g. `icon_yanship.png` | **23/23** | ~110×98 white glyph, ~4 KB | `arts/camplogo/logo_{nation}.png` (see §4) |
| g) **Item / trap icons: the real autochess ones** | AA2 `…/arts/shopitemicon/` | `{trapId}.png` | **59/59** (56 EQUIP + 3 MAGIC) | ~137×157, ~19 KB | none needed |
| g') Band (strategy) icons | AA2 `…/arts/bandicon/` | `icon_{bandId minus "band_"}.png` | **40/40** | 180×180, ~69 KB | — |
| h) Map tiles | **none usable**: the game map is a 3D Unity scene | — | — | — | **Draw tiles procedurally** (§7). Use real autochess backdrops for the lobby and prep screens. |
| i) UI: rarity, elite, chess-level, shop, HUD | AA2 `ui/autochess/…` (537 PNGs) + `arts/rarity_hub`, `elite_hub` | see JSON `autochessUi`, `arts` | 100% | 0.3–250 KB | CSS-drawn panels (Unity 9-slice borders are not in the dump) |
| j) Fonts | Google Fonts + self-hosted Bender / Novecento | §8 | — | — | system CJK |
| k) BGM + SFX: **the real game audio** | AA2 `@voice` branch `assets/dyn/audio/sound_beta_2/…` | mapped by `audio_data.json` banks | BGM 3/3; per-operator SFX **138/138**; enemies 185/200; autochess UI SFX 47 banks | mp3; BGM ~2 MB, SFX 5–60 KB | WebAudio-synthesized blips (§6.4) |
| l) **Battle Spine (in-battle chibi)** | operators: **fexli/ArknightsResource `spine/`**; enemies: **isHarryh/Ark-Models `models_enemies/`** | `spine/{id}/{id}/{Front\|Back}/{id}.{skel,atlas,png}`; enemies via `models_data.json` `assetList` | ops Front **138/138**, Back 135/138; enemies **198/200** | median op front+back ≈ 690 KB; enemy ≈ 134 KB | missing Back → mirrored Front; missing enemy → static icon sprite (§5.6) |
| Spine runtime | **PixiJS 7.4.2 + pixi-spine 4.0.6** (UMD from jsDelivr) | — | render checked in headless Chrome | 456 KB + 362 KB | — |
**Size budget, all classes, only what the pool needs**
| | Size |
|---|---|
| Raw | ≈ 200 MB (≈ 700 MB if full art is included, so leave full art out of the bundle) |
| After build-time optimization | **≈ 65–75 MB**: WebP for images, gzip/brotli for skel |
| Initial page load (lazy-loading Spine per board) | **< 6 MB** |
---
## 1. Id inventory (from `act2autochess`)
| Set | Count | Notes |
|---|---|---|
| Operators (`charShopChessDatas[*].charId ∪ backupCharId`) | **138 unique charIds** over 133 chess | 74 PRESET, 55 NORMAL, 4 DIY. DIY chess have `charId=null`, so the operator is chosen at runtime and uses the chosen operator's assets. Chess levels 1–6: 20/19/21/26/25/22. |
| Most chess are `evolvePhase=PHASE_2` | 94 PHASE_2 (+ 39 PHASE_1) non-golden; every golden chess is PHASE_2 | → Prefer **E2 avatar/portrait** (`_2`) and fall back to `_1`. |
| Tokens/summons referenced by pool operators | 20 | `displayTokenDict` and `overrideTokenKey`. Bond summon "炎佑" (Yan 6/9) has no token id in the data, see §5.6. |
| Enemies | **200** | Union of `enemyInfoDict`, `specialEnemyInfoDict`, `bossInfoDict`-linked ids and level files. All 200 are in `enemy_handbook_table`. |
| Bonds | 23 | `bondInfoDict[*].iconId`: `icon_yanShip` … `icon_suntShip` |
| Shop items | 59 | `trapShopChessDatas[*].trapId` = `trap_10xx_acarmNNN` and friends |
| Bands (strategies) | 40 | `bandDataListDict` keys `band_*` |
| Sub-professions in pool | 58 | e.g. `fastshot`, `protector`, `ritualist`, `primprotector`, `counsellor`, `hunter`… |
| Nations in pool | 18 + None | bolivar, columbia, egir, higashi, iberia, kazimierz, kjerag, laterano, leithanien, lungmen, rhodes, rim, sami, sargon, siracusa, ursus, victoria, yan |
Newest operators in the pool, all verified present in every class: `char_4211_snhunt` 雪猎, `char_4207_branch` 折桠, `char_4196_reckpr` 录武官, `char_4194_rmixer` 信仰搅拌机, `char_4193_lemuen` 蕾缪安, `char_4191_tippi` 蒂比, `char_1047_halo2` 溯光星源, `char_1046_sbell2` 圣聆初雪, `char_1045_svash2` 凛御银灰, and the activity-only `char_616_pithst` 盟约·辅助干员 and `char_617_sharp2` 领主·Sharp.
---
## 2. Sources evaluated
| Repo | Branch | Updated | What it has | Verdict |
|---|---|---|---|---|
| `yuanyan3060/ArknightsGameResource` | `main` | 2026-09-24 | avatar (incl. `token_*`, non-autochess `trap_*`), portrait, skin (full art `b`), skill, enemy, item, item_rarity_img, map (not autochess) | **Primary for 2D portraits and icons.** AGPL-3.0 applies to its code; the art is © Hypergryph. |
| `ArknightsAssets/ArknightsAssets2` | `cn` (art), `voice` (all audio) | 2026-09-23 | Full ArknightsStudio dump: `assets/dyn/ui/autochess/**` (real autochess UI), `activity/[uc]act2autochess/**`, `arts/**` (camplogo, profession, rarity, elite, subprofession, loading illustrations), `battle/[pack]common/**`, and all audio under `@voice` | **Primary for autochess-specific icons, UI and ALL audio.** Battle spine here is incomplete: atlas/skel without PNGs. |
| `fexli/ArknightsResource` | `main` | 2026-09-20 | `spine/{charId}/{charId}/{Front,Back}/` battle models + base (`build_*`) models + skins; `camplogo`, `charpor`, `skills`… | **Primary for operator battle Spine** |
| `isHarryh/Ark-Models` | `main` | 2026-08-02 | `models_enemies/` enemy battle Spine (PC build, PMA forced), `models/` base (基建) chibis only, `models_illust/` dynamic illustrations; `models_data.json` index | **Primary for enemy battle Spine.** Its operator models are base chibis, not battle chibis. |
| `Aceship/Arknight-Images` | `main` | **2024-05** (stale) | avatars etc. | Hit rate **89/138**. It lacks every 2024+ operator. Not recommended. |
| PRTS `media.prts.wiki/{md5[0]}/{md5[0:2]}/{文件名}` | — | live | e.g. `头像_德克萨斯.png` returns 200 | Last-resort fallback only: Chinese-name keyed, and hotlinking is discouraged. `torappu.prts.wiki` timed out from this host. |
| `web.hycdn.cn` (official CDN) | — | — | guessed avatar path → 404 | not usable |
| `TimWangZi/The-font-of-Arknights` | `master` | — | Bender, Novecento Wide, 思源宋体 | font source (§8) |
**CDN behaviour, all checked**
- `raw.githubusercontent.com` returns `access-control-allow-origin: *` with `cache-control: max-age=300`.
- `cdn.jsdelivr.net/gh/<repo>@<branch>/…` works with CORS `*` and a 7-day cache for yuanyan, fexli, Ark-Models and AA2 `@cn`.
- **jsDelivr returns 404 for the AA2 `@voice` branch.** Use raw for audio.
- Do **not** hotlink at runtime (GitHub rate limits, availability, and it is impolite). **Mirror at build time** (§9).
**Path gotchas**
- `[uc]` → `%5Buc%5D` and `[pack]` → `%5Bpack%5D`.
- Skill icons such as `skill_icon_skcom_atk_up[1].png` need `%5B1%5D`. curl's globbing also breaks on unencoded brackets.
- Skin ids contain `#` → `%23`. Skins are not needed.
- Bond `iconId` is camelCase in data (`icon_yanShip`) but **lower-case on disk** (`icon_yanship.png`).
- Band icon = `icon_` + bandId minus `band_`.
- Liskarm's default battle model lives at `spine/char_107_liskam/char_107_liskarm/…`, a dump-side typo.
- `char_1012_skadi2` has a single `Spine/` folder instead of `Front/` and `Back/`.
- Enemy spine filenames can differ from the folder name: `1043_zomsbr/enemy_1043_zomsabr.*`, `1169_duphlx/enemy_1169_duphx.*`. **Always use `models_data.json → data[key].assetList`.** `07-assets.json` already resolves this.
---
## 3. 2D portraits and icons (classes a–e, i)
### a) Avatars
- URL: `https://raw.githubusercontent.com/yuanyan3060/ArknightsGameResource/main/avatar/{charId}.png`
- E2 variant: `{charId}_2.png`.
- Mirror: `https://cdn.jsdelivr.net/gh/yuanyan3060/ArknightsGameResource@main/avatar/…`.
- Checked by curling all 138 charIds (xargs -P16): **138/138** 200. E2 variant: 122/138.
- The 16 without E2 are `char_600_cpione, 601_cguard, 602_cdfend, 603_csnipe, 604_ccast, 605_cmedic, 606_csuppo, 607_cspec, 608_acpion, 609_acguad, 610_acfend, 611_acnipe, 612_accast, 613_acmedc, 614_acsupo, 615_acspec`. They have no E2 art in-game, so use the base image.
- Size 180×180 RGBA, avg 58 KB (8.4 MB for the set, preferring E2). WebP q88 comes to about 15 KB each.
- Tokens: `avatar/{tokenId}.png` hits **15/20**.
- Missing: `token_10012_rosmon_shield`, `token_10039_ulpia_block`, `token_10040_siege2_vlion`, `token_10057_svash2_eagle`, `token_10058_sbell2_icetgt`.
- Fallback: owner operator avatar with a small "召唤物" badge, or the battlecard `icon_profession_token.png`.
### b) Portraits
- `…/portrait/{charId}_1.png` (E2 `_2.png`), 180×360 半身像 (the squad-card crop used on shop cards). **138/138**; E2 122/138. Avg 117 KB, about 31 KB as WebP q88.
- Full art `…/skin/{charId}_1b.png` is 681×1317, avg 787 KB; E2 `_2b` averages 3.2 MB. Use it only in the operator detail modal, lazy-loaded. Keep it out of the bundle; fetch on demand from jsDelivr.
### c) Skill icons
- `…/skill/skill_icon_{iconId}.png`, where `iconId = skill_table[skillId].iconId ?? skillId`. **317/317** (every skill of every pool operator, after percent-encoding brackets). 128×128, avg 19 KB.
- The shop only shows the chess's default skill (`defaultSkillIndex`), which is 144 distinct icons (≈ 2.7 MB raw).
### d) Profession and sub-profession
- `https://raw.githubusercontent.com/ArknightsAssets/ArknightsAssets2/cn/assets/dyn/arts/profession_hub/icon_{caster|medic|pioneer|sniper|special|support|tank|warrior}.png`
- Large white variant: `profession_large_hub/icon_profession_{p}_large_white.png`.
- Small battle-card variant: `arts/ui/[uc]battlecommon/ui_battle_new/battlecard/icon_profession_{p}.png`, which also includes `…_token.png`.
- Sub-profession: `…/arts/ui/subprofessionicon/sub_{subProfessionId}_icon.png`. **58/58**, ~2 KB white glyphs.
### e) Enemies
- `…yuanyan…/enemy/{enemyId}.png`, **200/200**, avg 44 KB.
- The boss HP-bar portrait style exists as `arts/ui/[uc]battlecommon/ui_battle_new/enemybossinfo/…` for only a few bosses. Use the enemy icon inside `sprite_enemy_boss_avatar_bg.png`.
### i) Rarity, elite and misc UI (all 200)
- Rarity stars: `arts/rarity_hub/rarity_{0..5}.png`, plus `rarity_yellow_{i}`, `rarity_black_{i}` and `rarity_left_{i}`.
- Autochess-specific: `ui/autochess/[uc]autochessouter/arts/charraritysprite/rarity_{0..5}.png`.
- Elite icons: `arts/elite_hub/elite_{0..3}.png`, `_large`, `_card`.
- Autochess `eliteIconId` values (`shopCharChessInfoData[*].eliteIconId`) map to `…/[uc]autochessouter/arts/charelitesprite/{char_elite_base_1|char_elite_base_2|char_elite_gold_2}.png`. Also available: `char_elite_detail_*`.
- Chess tier (等阶 1–6): `…/chesslevelsprite/chess_level_icon_{0..6}.png`, `chess_level_detail_{0..6}.png`.
- Shop card: `…/autochess_shop_card_item/img_chess_level_{1..6}.png`, `frame_lv{1..3}.png`, `bg_common.png`, `ice_matte.png` (frozen), `icon_ice.png`.
- Shop level (调度中心): `…/shoplevelsprite/shop_level_icon_{1..6}.png`, `shop_level_tag_{1..6}.png` (150×595 vertical banners). Tag bg colours in data: `#434343, #626654, #445760, #615B74, #6C5E41, #5d341e`.
- Garrison (特质) type icons: `…/garrisontypeicon/{icon_battle|icon_bond|icon_gold|icon_support}.png`, plus `s_` small versions. These match `garrisonDataDict[*].eventTypeIcon`.
- Mode icons `mode_{normal|hard|abyss|funny|training}_icon.png`. Buff icons `icon_{boss_debuff|enemy_debuff|player_buff|stage_buff|team_buff}.png`, matching `effectDecoIconId`.
- Title icons `comment_icon_{1..6}.png` (playerTitle `picId`). Trophies `trophy_level{1..5}_icon.png`. Enemy-type icons `{fly|times|dot|element|invisible|reflection|special}_icon.png`, matching `enemyInfoDict` keys.
- Bond board pips: `…/[uc]autochessbattle/arts/bondboardicon/{n}_{k}.png` (1_0 … 4_4).
- HUD: `…/hud/autochess_hud_panel/*.png` (55 files: `icon_hp`, `icon_battle`, `icon_boss`, `icon_coop`, `icon_rest`, `hp_bar`, `bg_*`…).
- Shop: `…/shop/autochess_shop/*.png` (`refresh_icon`, `frozen_icon`, `upgrade_max`, `bg_money`, `cost_free`…).
- Equip slot: `…/act2autochess_equip_slot_panel/*.png`. Round banner: `…/dialog/round_start_dialog/*.png`.
- Loading illustrations (1920-class, ~0.9 MB each): `arts/loadingillusts/loading_ac_{core|normal|hard|abyss|funny|prototype|train|training}.png`.
- `modeDataDict.loadingPicId` values such as `loading_AC_core` → lower-case the id.
- Lobby / backdrop art:
- `activity/[uc]act2autochess/prefabs/act2autochess_activity_stage/{entry_bkg_01,entry_bkg_02,bkg_01,bkg_02,bg_mountains_tiled,season_logo,img_coin}.png`.
- Mode select: `ui/autochess/[uc]autochessouter/modechoice/auto_chess_mode_choice_state/{normal,hard,abyss,funny}_rhodes_island.png`.
- Battle ready: `…/battleready/auto_chess_battle_ready_state/{bg_mountain1,bg_mountain2,bg_terrain,rhodes}.png`.
- Battle common sprites: `battle/[pack]common/sprites/{sprite_shadow,sprite_direction_arrow,sprite_direction_ring,sprite_box_shadow}.png` and `…/projectiles/projectile_arrow.png`.
- Battle UI: `arts/ui/[uc]battlecommon/ui_battle_new/{btn_speed_1x,btn_speed_2x,btn_pause,slider_hp_back,slider_hp_fill,sprite_skill_ready…}.png`.
- Caveat: Unity sprite 9-slice borders are not in the dump. Use the art for icons, badges and backdrops, and draw frames and panels in CSS or canvas.
---
## 4. Bond icons (f): the real ones are available
`https://raw.githubusercontent.com/ArknightsAssets/ArknightsAssets2/cn/assets/dyn/ui/autochess/%5Buc%5Dautochesscommon/arts/bondicon/{iconId lowercased}.png`
All 23 are verified: white glyphs on transparent, so tint via CSS `filter` or canvas `globalCompositeOperation='source-in'`. Nation fallbacks come from `arts/camplogo/logo_{id}.png` (44 logos, 20–110 KB). The non-nation fallbacks below are [ASSUMED] suggestions.
| bondId | Name | Real icon file | Bytes | Fallback |
|---|---|---|---|---|
| yanShip | 炎 | `icon_yanship.png` | 5466 | camplogo/logo_yan.png |
| sargonShip | 萨尔贡 | `icon_sargonship.png` | 2845 | camplogo/logo_sargon.png |
| victoriaShip | 维多利亚 | `icon_victoriaship.png` | 4267 | camplogo/logo_victoria.png |
| kjeragShip | 谢拉格 | `icon_kjeragship.png` | 5255 | camplogo/logo_kjerag.png |
| lateranoShip | 拉特兰 | `icon_lateranoship.png` | 4725 | camplogo/logo_laterano.png |
| egirShip | 阿戈尔 | `icon_egirship.png` | 4715 | camplogo/logo_egir.png |
| siracusaShip | 叙拉古 | `icon_siracusaship.png` | 6956 | camplogo/logo_siracusa.png |
| kazimierzShip | 卡西米尔 | `icon_kazimierzship.png` | 4288 | camplogo/logo_kazimierz.png |
| preciShip | 精准 | `icon_preciship.png` | 4872 | sub_fastshot_icon |
| swiftShip | 迅捷 | `icon_swiftship.png` | 3675 | icon_profession_pioneer |
| skillfulShip | 灵巧 | `icon_skillfulship.png` | 4166 | sub_executor_icon |
| arcaneShip | 奥术 | `icon_arcaneship.png` | 4114 | icon_profession_caster |
| steadShip | 坚守 | `icon_steadship.png` | 2702 | icon_profession_tank |
| deputShip | 助力 | `icon_deputship.png` | 3726 | icon_profession_support |
| visiShip | 远见 | `icon_visiship.png` | 3451 | garrisontypeicon/icon_gold |
| miraShip | 奇迹 | `icon_miraship.png` | 4945 | garrisontypeicon/icon_gold (tinted) |
| investShip | 投资人 | `icon_investship.png` | 2058 | garrisontypeicon/icon_gold |
| raidShip | 突袭 | `icon_raidship.png` | 2982 | sub_executor_icon |
| indomShip | 不屈 | `icon_indomship.png` | 3439 | sub_unyield_icon |
| maniShip | 调和 | `icon_maniship.png` | 3124 | garrisontypeicon/icon_bond |
| emptyShip | 协防干员 | `icon_emptyship.png` | 2762 | battlecard icon_profession_token |
| soloShip | 独行 | `icon_soloship.png` | 2169 | sub_lord_icon |
| suntShip | 绝技 | `icon_suntship.png` | 3366 | elite_hub/elite_2.png |
Band / strategy icons (g'): `…/[uc]autochesscommon/arts/bandicon/icon_{bldsk|amiya|duyaoy|sarkazb|orchid|justin|ermengard|lmlee|kirara|pepe|harold|sciurus|paganini|clementia|emperor|mberry|humus|quintus|yu|doberm|cathy|malkie|qalaisa|chen|damaztic|pith|dusk|cannot|ducklord|lisa|vodfox|ioleta|jesica|mlyss|makiri|fang|mlynar|chiave|narant|amedic}.png`. All **40/40** are 180×180 "STRATEGY" cards, ~69 KB each.
## 5. Items / traps (g)
`…/[uc]autochesscommon/arts/shopitemicon/{trapId}.png`, **59/59**, ~137×157 on a transparent background, avg 19 KB, e.g. `trap_1041_acarm041` 维式重锤.
- The 3 non-shop special traps are effect ids with no icons: `trap_copy_front_char`, `trap_create_self_choice`, `trap_disney_special`. Use the icon of the item that grants them.
- Rarity backgrounds for item frames: yuanyan `item_rarity_img/sprite_item_r{1..6}.png`.
- Level-placed traps (`trap_1104_aclasert`, `trap_1105_accrate`, `trap_1112_acblzd`, `trap_098_mire`, `trap_013_blower`, `trap_042_tidectrl`) have no autochess icon. Draw them as tile overlays.
---
## 5b. Battle Spine (l): the real in-battle animated chibis
### 5.1 Sources and verification
- **Operators (fexli):** `https://raw.githubusercontent.com/fexli/ArknightsResource/main/spine/{charId}/{charId}/{Front|Back}/{charId}.{skel|atlas|png}`
- jsDelivr: `https://cdn.jsdelivr.net/gh/fexli/ArknightsResource@main/spine/…`.
- **Front 138/138. Back 135/138**: missing for `char_4134_cetsyr` 魔王, `char_291_aglina` 安洁莉娜 and `char_1012_skadi2` 浊心斯卡蒂. Skadi2 has one model in a `Spine/` folder. These are supporters that are often shown only from the front.
- One texture page per model.
- **Enemies (Ark-Models):** `https://raw.githubusercontent.com/isHarryh/Ark-Models/main/models_enemies/{enemyId minus "enemy_"}/{assetList[.skel|.atlas|.png]}`. **198/200.**
- Missing: `enemy_1305_mhslim(_2)`. AA2 has its atlas and skel but not its PNG.
- **Sample downloaded:** `char_102_texas` Front and Back plus `enemy_1007_slime` → `…/scratchpad/gd/extra/assets/spine_sample/`.
- Texas Front: skel 187,826 B, atlas 4,903 B, png 65,126 B (512×512).
- Texas Back: 73,065 / 3,346 / 47,290 B (256×256).
- Slime: 16,567 / 552 / 11,659 B (128×128).
- **Skel header:** read with a varint-string parser, and again with the pixi-spine 3.8 `SkeletonBinary` in Node. Every sampled file reports **version `3.8.99`**: 11 operators and 5 enemies.
- **Browser proof:** headless Chrome rendered Texas Front (Attack_Loop), Texas Back, 雪猎 Front and enemy “自在” (`enemy_1517_xi`, boss) with PixiJS 7.4.2 and pixi-spine 4.0.6 loaded **directly from jsDelivr**, with no errors. Screenshot: `…/scratchpad/gd/extra/assets/spine_test.png`.
### 5.2 Sizes
| Set | Raw | Optimized (skel+atlas gzip ≈ 24%, PNG → lossless WebP ≈ 50%) |
|---|---|---|
| Operators Front (138) | skel 44.3 + png 20.6 + atlas 1.2 = 66 MB | — |
| Operators Back (135) | skel 24.6 + png 16.7 + atlas 0.8 = 42 MB | — |
| **Operators total** | **108 MB**; median op 690 KB; max `char_427_vigil` 3.1 MB | **≈ 36 MB** |
| **Enemies (198)** | skel 14.9 + png 11.1 + atlas 0.5 = **26.6 MB**; avg 134 KB | **≈ 9 MB** |
Gzip measurements: texas skel 188 KB → 46 KB; snhunt 659 KB → 122 KB; ghost2 933 KB → 184 KB. A texas 512² PNG goes from 65 KB to 31 KB as lossless WebP.
**Estimate for 133 ops + 60 enemies on one server: about 110 MB raw, about 38 MB served.** A client only downloads the models on its board or in its shop, typically 10–25 models = 5–15 MB.
### 5.3 Runtime: use pixi-spine, not official spine-ts 3.8
- CDN:
- `https://cdn.jsdelivr.net/npm/pixi.js@7.4.2/dist/pixi.min.js` (456 KB)
- `https://cdn.jsdelivr.net/npm/pixi-spine@4.0.6/dist/pixi-spine.js` (362 KB, UMD → `PIXI.spine`, auto-detects 3.7/3.8/4.0/4.1)
- Or `@pixi-spine/all-3.8@4.0.6/dist/pixi-spine-3.8.js` (148 KB) for a 3.8-only build.
- npm: `pixi.js@7`, `pixi-spine@4.0.6`. pixi-spine 4 needs PixiJS **7**, not 8.
- Load with `PIXI.Assets.load(url.skel)`. The `.atlas` and `.png` must sit next to it with the same basename. Then `new PIXI.spine.Spine(res.spineData)` and `spine.state.setAnimation(0, name, loop)`.
- **Why not official spine-ts 3.8** (`cdn.jsdelivr.net/gh/EsotericSoftware/spine-runtimes@3.8/spine-ts/build/spine-webgl.js`, reachable):
1. Its atlas parser only treats `rotate: true|90` as rotated. **Arknights atlases use `rotate: 270`** (seen in ghost2 and ulpia), so those regions render wrong.
2. The fexli atlases **omit the `size:` line**. pixi-spine falls back to the real texture size; spine-ts 3.8 divides by page width 0.
- Atlas quirks to normalize at build time:
- Insert `size: W,H` after the page name, read from the PNG.
- If PNGs are converted to WebP, rename the page line.
- **Alpha:**
- fexli operator textures are **straight alpha**: a 512² texas texture has 193 pixels with RGB > A, up to 108 over. Load them normally.
- Ark-Models enemy textures are **premultiplied** (README: "强制启用 PMA"; slime has 1 violation of ≤2). Add a `pma: true` page line to those atlases (pixi-spine honours it and sets `ALPHA_MODES.PMA`), or set `baseTexture.alphaMode = PIXI.ALPHA_MODES.PMA`. Otherwise edges come out dark.
- Licensing note: pixi-spine is MIT, but it embeds Spine Runtime code under the Spine Runtimes License, which technically expects a Spine Editor licence. This is commonly tolerated for non-commercial fan tools, and should be noted in the credits.
### 5.4 Animation names (parsed; the naming varies, so build a resolver)
| Unit | Animations |
|---|---|
| char_103_angel (F) | Attack, Default, Die, Idle, Start |
| char_102_texas (F) | Attack_Start/Loop/End, Default, Die, Idle, Skill, Start. **Back model has no Die, no Skill.** |
| char_172_svrash (F) | Attack, Combat, Combat_Down, Default, Die, Idle, Skill, Start |
| char_350_surtr (F) | Attack, Die, Idle, Skill_2, Skill_2_Down, Skill_3_Begin/Idle/Loop, Start |
| char_2015_dusk (F) | Attack, Die, Idle, Skill2_Begin/Loop/End, Skill3_Attack, Start. No underscore after "Skill". |
| char_4211_snhunt (F) | Attack_Begin/Loop/End, Attack_Down_*, Reload_*, Skill_1, Skill_2_Begin/Loop/End, Skill_Down_*, Die, Idle, Start. Events: `BeastOnAttack`, `OnAttack`, `OnStart`. |
| char_1023_ghost2 (F) | Attack, Attack_Down, Die, Die_B, Idle, Idle_B, Skill_1, Skill_3_*, Start, Start_B… |
| char_4134_cetsyr (F) | no Attack; Skill_{1,2,3}_Begin/Loop/End, Idle, Die, Stun, Stun_Begin, Start |
| char_1012_skadi2 (Spine) | no Attack; Skill_{n}_Begin/Loop, Idle, Die, Stun, Start |
| char_600_cpione, char_616_pithst | Attack, Default, Die, Idle, Start |
| enemy_1007_slime | Attack, Default, Die, Idle, Move_Begin/Loop/End |
| enemy_1000_gopro_2 | … + Run_Begin/Loop/End |
| enemy_1502_crowns | Appear, Attack, Die, Disappear, Idle, Move |
| enemy_1517_xi (boss) | Attack_01, Attack_02, Die, Idle, Move, Revive_01, Skill_01, Skill_01_02, Skill_02_Begin/Loop/End. Events: OnAttack, OnPlayAudio. |
| enemy_9009_acfort | Attack, Die, Idle, Move, Skill_1_Begin/Loop/End, Skill_2 |
**Resolver.** Precompute it at build time by parsing every skel in Node with `@pixi-spine/runtime-3.8`; see `spinetest/parse.cjs` in the scratchpad. Emit `anims.json`, then resolve each role in order:
- `idle`: `Idle` → `Default`.
- `deploy`: `Start` (fall back to `Idle`).
- `attack`:
- `Attack`;
- else `Attack_Start|Attack_Begin` → `Attack_Loop` → `Attack_End`;
- else `Combat`;
- else (pure supporter) `Skill_1_Loop`, or just `Idle` with a flash.
- Prefer non-`_Down` variants. Use `_Down` when the target is below the unit on screen.
- `skill`, for the chess's default skill index n (1-based):
- `Skill_{n}_Begin` → `Skill_{n}_Loop` (while active) → `Skill_{n}_End`;
- else `Skill{n}_Begin/Loop/End`;
- else `Skill_{n}`;
- else `Skill`;
- else fall back to `attack`.
- During a skill, `Skill_{n}_Idle` replaces `idle` when present.
- `die`: `Die` (if the Back model lacks it, swap to the Front model for the death animation), else fade out over 0.5 s.
- `move` (enemies): `Move_Begin` → `Move_Loop` → `Move_End`; else `Move`; else `Run_Loop`.
- `stun`: `Stun_Begin` → `Stun`, else freeze the current track at `timeScale = 0`.
- **Hit timing:** apply damage and spawn projectiles on the Spine event `OnAttack` (operators and enemies). If no event fires, use 50% of the attack animation. Scale the attack animation's `timeScale` to `animDuration / attackInterval` so it matches the simulated attack speed.
### 5.5 Direction and scale
- **Operators:**
- Front model when facing right or down.
- Front mirrored (`scale.x = -1`) when facing left.
- Back model when facing up; if there is no Back model, use Front.
- **Enemies:** single model; flip x by the sign of horizontal velocity. The model faces right by default. [ASSUMED from standard Arknights behaviour; verify by eye]
- **Scale [ASSUMED]:** skeleton height is ~360–450 units for a standard chibi. Start with `scale = tilePx / 300` so a chibi stands about 1.2 tiles tall, and tune by eye. Anchor at the feet (Spine origin 0,0 is at the feet).
- Put `battle/[pack]common/sprites/sprite_shadow.png` under each unit.
### 5.6 Gaps and fallbacks
- **Tokens/summons:** fexli has the **default** battle Spine for only 1/20 tokens (`token_10000_silent_healrb`); the rest exist only as skin variants (e.g. `…vodfox_doll_witch_2`). AA2 has their atlas and skel but no PNGs.
- Options, in order of preference:
1. Use the first skin-variant Spine, listed in `tokens[*].battleSpineSkinVariantsOnly` in the JSON. It looks slightly off-model, but it animates.
2. Draw the token avatar (15/20) as a circular sprite with a bob tween.
3. Draw a procedural glyph.
- **"炎佑" (Yan-bond summon), [ASSUMED]:** no token id is in the data. Render it with the `token_10015_dusk_drgn` skin-variant Spine tinted orange-red, or with the Yan bond glyph plus fire particles.
- **enemy_1305_mhslim:** reuse `enemy_1007_slime` Spine with a hue shift, or fall back to the icon sprite.
- **General policy:** if any Spine load fails, render the square avatar or enemy icon in a rarity-coloured diamond with a tween (idle bob, lunge on attack, flash on hit). The game must never block on Spine.
---
## 6. Audio (k): the real audio is available in AA2 `@voice`
Base: `https://raw.githubusercontent.com/ArknightsAssets/ArknightsAssets2/voice/assets/dyn/audio/sound_beta_2/` (CORS `*`; jsDelivr 404s on this branch). The mapping comes from `excel/audio_data.json`, downloaded to `…/extra/assets/audio_data.json`. For an `asset` like `Audio/Sound_Beta_2/CustomSE/act1autochess/act1autochess_b_ui_roundstart`, strip the prefix, lower-case it and append `.mp3`.
### 6.1 BGM (verified)
| Level bgmEvent / context | Files | Size |
|---|---|---|
| act2autochess `m01–m04` use `bgmEvent=act1autochess_shop` → bank `battle.ON_GAME_READY.act1autochess_shop` | `music/act1autochess/m_bat_act1autochess_loop.mp3` | 2.0 MB |
| Lobby: `sys.ON_ACTIVITY_LOADED.act1autochess` | `music/act1autochess/m_sys_act1autochess_intro.mp3`, then loop `m_sys_act1autochess_loop.mp3` | 1.1 + 1.1 MB |
| `h07_05` levels use `bgmEvent=rglk1phantomcastle` | `music/rogue_1/m_bat_rglk1phantomcastle_intro.mp3` + `_loop.mp3` | 1.0 + 1.5 MB |
Play the intro, then crossfade to a looping `AudioBufferSourceNode` (`crossfade: 1.0` s in the bank).
### 6.2 Autochess UI and battle SFX (47 banks, all resolved, in `07-assets.json → audio.autochessSfxBanks`)
Key bank names, which make good event names for our code:
- **Shop:** `ui.ON_ACT1AUTOCHESS_SHOP_UPGRADE`, `…SHOP_LOCK`, `…GETMONEY`.
- **Equipment and bonds:** `battle.ON_ACT1AUTOCHESS_EQUIP_DONE`, `…CHAR_BONUS` (3-merge upgrade), `…EQUIP_BONUS`, `…ADD_BOND` (buffup).
- **Round flow:** `ui.…ROUNDSTART`, `…BATTLESTART`, `…BATTLESTART_BOSS`, `…REST`, `…YOURTURN`, `…YOURTURN_CIRCLE`, `…COUNTDOWN`.
- **Boss rounds:** `…BOSSROUND_{TEAM,SINGLE,SECRET}`, `…KILLBOSS{,_ALL,_NORMAL}`, `…DEFENCE_{START,UNITE}`.
- **End of battle and settlement:** `…BATTLEOVER_{REDUCE,NOREDUCE,NORMAL}`, `…SETTLEMENT_{SUCCEED,FAIL,TEAM,BOSSSIGN}`.
- **Matchmaking and lobby:** `…MATCH_{SUCCEED,FAIL,CANCEL}`, `…PLAYER_{JOINROOM,READY}`, `…STRATEGY`, `…LOAD`, `…START`, `…GOFIRST`, `…DISCONNECT`, `…BROADCASTHINT`, `…ENTER_DANGER`.
- **Other:** `battle.ON_CUSTOM_TRIGGER.autochess_kill_gain_coin`, `battle.ON_ACT1AUTOCHESS_MAGIC_PLACE_BATTLE`.
- Files are under `customse/act1autochess/*.mp3` (46 files, 5–75 KB). Generic sounds: `battle/b_char/b_char_set.mp3` (deploy), `battle/b_enemy/b_enemy_dead_{n,h}.mp3`, `battle/b_ui/b_ui_{win,lose}.mp3`, `general/g_ui/g_ui_btn_{h,u}.mp3`, `g_ui_confirm_h.mp3`.
### 6.3 Per-unit combat SFX (exact, from the data)
Banks are keyed by unit id:
- `battle.ON_ABILITY_START.{charId}.attack` (swing or fire)
- `battle.ON_ABILITY_HIT.{charId}.attack` (impact)
- `battle.ON_UNIT_BORN.{id}`, `battle.ON_UNIT_DEAD.{id}`
- `battle.ON_SKILL_START.{skillId}` (skill activation)
Coverage: **138/138 operators** and **185/200 enemies** have at least one bank; the remaining 15 enemies use a generic `e_atk`/`e_imp` sound. `07-assets.json` lists the resolved URLs per unit (`operators[*].sfx`, `operators[*].skills[*].sfx`, `enemies[*].sfx`).
The pool's needed set is **691 mp3 = 8.7 MB**:
- `player/p_atk`, `p_imp`, `p_skill`: 452 files, 6.8 MB
- `enemy/e_*`: 223 files, 2.6 MB
- `battle/`: 0.9 MB
- `customse/`: 1.3 MB
Pitch-randomize using the bank's `minPitch`/`maxPitch` (typically 0.8–1.2). Respect `maxSoundAllowed`, which caps concurrent instances per bank at 1–2 (`popOldest`). This matters in a 20-unit fight.
### 6.4 Optional and fallback audio
- Voice (optional, off by default): `voice_cn/{charId}/cn_{NNN}.mp3`, about 30 lines and ~1 MB per operator.
- Useful lines, by the standard charword ids [ASSUMED mapping, confirm with `charword_table.json`]: `cn_021`/`cn_022` 选中干员, `cn_023`/`cn_024` 部署, `cn_025`–`cn_028` 作战中.
- Japanese voice is under `voice/`.
- **WebAudio synthesis fallback**, used if the fetch fails or the user mutes "game audio":
- UI click: 1.2 kHz square, 30 ms, exponential decay.
- Coin: two sines, 1.5→2.2 kHz, 80 ms.
- Hit: white-noise burst through a band-pass (1–3 kHz), 60 ms.
- Deploy: low 110 Hz sine thump plus a noise click.
- BGM fallback: silence or an ambient pad. Do not try to imitate the OST.
- Monster Siren (official streaming, `monster-siren.hypergryph.com/api/songs`) returns JSON but **no CORS headers**, so the browser cannot use it directly. Not recommended.
---
## 7. Map tiles and battle background (h)
- The game data has **no 2D map texture**. Autochess maps are 19×21 tile grids rendered as a 3D Unity scene.
- The AA2 root PNGs `assets/tile_floor.png`, `tile_road.png` and similar are 64×64 **editor debug placeholders** (green, wood planks). Do not use them.
- yuanyan `map/` previews do not include autochess stages.
- act2autochess level tile keys: `tile_road`, `tile_floor`, `tile_wall`, `tile_forbidden`, `tile_fence_bound`, `tile_start`, `tile_end`, `tile_telin`, `tile_telout`, **`tile_achand`** (autochess bench/hand area), `tile_mire` (m02), `tile_smog` (m03), `tile_deepsea` (m04).
- `heightType` is LOWLAND or HIGHLAND; `buildableType` is ALL, MELEE, RANGED or NONE.
- **Procedural tile renderer, in the Arknights look [ASSUMED palette, tune by eye]:**
- Oblique top-down view; each tile is an inset square with a 1 px lighter top edge and a darker bottom bevel.
- LOWLAND road `#3b4048` with subtle 45° noise. Deployable lowland gets a faint blue-grey outline `#6f8aa3` at 40% when a melee chess is dragged.
- HIGHLAND (`tile_wall`) is a raised block: top `#6b7280`, side face `#3a3f47` 10 px tall, with a white 1 px top rim.
- `tile_forbidden`: `#22262c` with a diagonal hatch. `tile_fence_bound`: dark plus a yellow-black edge stripe.
- `tile_start` (enemy spawn): red `#d0453b` pulsing frame with an arrow glyph. `tile_end` (protection point): blue `#2d7fd6` with a house glyph.
- `tile_telin`/`telout`: purple ring. `tile_mire`: murky green overlay. `tile_smog`: animated grey fog alpha. `tile_deepsea`: teal wave overlay.
- `tile_achand`: bench slots with a dashed outline.
- Attack-range preview: `arts/ui/[uc]battlecommon/ui_battle_new/attack_range_attack.png` and `attack_range_stand.png`, or draw orange `#ff9c33` squares at 35% alpha.
- **Backdrop:** a dark gradient `#15171b → #262a31`, with `bg_mountains_tiled.png` (act2 entry, 112 KB) parallaxed at 20% opacity. Lobby: `entry_bkg_01.png` (0.99 MB) and `season_logo.png`.
---
## 8. Fonts (j)
| Use | Font | Source | Size |
|---|---|---|---|
| Chinese UI and body | **Noto Sans SC** 400/500/700/900 (思源黑体) | Google Fonts, `fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap` (200); npm `@fontsource/noto-sans-sc@5` | sliced unicode-range, ~50–100 KB per used slice |
| Chinese titles (formal, serif as in-game) | Noto Serif SC 600/900 (思源宋体) | Google Fonts (200) | same |
| Numbers, HUD, costs, timers (in-game **Bender**) | **Bender** (Jovanny Lemonad, free incl. commercial) | `https://raw.githubusercontent.com/TimWangZi/The-font-of-Arknights/master/font/Bender/BENDER.OTF` (200, 52 KB); also Light and Thin. Convert to woff2 and self-host. | 52 KB |
| English headers (in-game **Novecento Wide**) | Novecento Sans Wide Normal | same repo, `font/Novecento-Wide-Normal-2.otf` (200, 45 KB); self-host | 45 KB |
| Google-only fallbacks for Bender / Novecento | **Rajdhani** 600/700; **Saira Condensed** 500/700; Barlow Condensed; Teko; Oswald; Chakra Petch | Google Fonts, all 200 | — |
Recommended CSS stacks:
- Numerals and English UI: `font-family: Bender, Rajdhani, 'Noto Sans SC', sans-serif`.
- Headlines: `'Novecento Wide', 'Saira Condensed', 'Noto Sans SC'`.
Avoid redistributing Georgia and Times New Roman (Microsoft fonts) from that repo. The 11 MB 思源宋体 OTFs are unnecessary because Google Fonts serves them subset.
---
## 9. Build and delivery plan (recommended)
1. **Keep assets out of git.** Add `client/public/assets/` to `.gitignore`.
2. Write `scripts/fetch-assets.mjs`. It reads `docs/research/07-assets.json` and downloads, with concurrency of about 16, retry ×3 and a jsDelivr mirror when raw fails:
- `assets/char/avatar/{charId}.webp`, choosing E2 if present, else base.
- `assets/char/portrait/{charId}.webp`.
- `assets/skill/{iconId}.webp`.
- `assets/enemy/icon/{enemyId}.webp`.
- `assets/ui/…`, `assets/bond/{bondId}.png`, `assets/item/{trapId}.webp`, `assets/band/{bandId}.webp`.
- `assets/spine/op/{charId}/{front|back}/{charId}.{skel,atlas,webp}` and `assets/spine/enemy/{enemyId}/…`.
- `assets/audio/{bgm|sfx}/…mp3`.
- Full art is not downloaded; fetch it lazily from jsDelivr at runtime.
3. **Post-process:**
- Icons and portraits → WebP q88. Spine textures → **lossless WebP**, then rewrite the atlas page line.
- Inject `size:` into fexli atlases and `pma: true` into Ark-Models atlases.
- Normalize every filename to lowercase ASCII.
- Precompute `anims.json` (animation list + resolver result per model) and `sfx.json` (per unit: attack start and hit URLs, skill start, die).
- Serve `.skel` and `.atlas` with gzip or brotli via Node `compression`.
4. **Runtime loading:**
- Preload UI, icons, avatars and portraits: about 6 MB.
- Load Spine on demand when a chess enters the shop, bench or board, or when an enemy wave is announced. Cache with `PIXI.Assets` and an LRU of about 60 models.
- Show the avatar-sprite fallback until the Spine loads.
5. **Budgets after optimization:**
| Asset group | Size |
|---|---|
| Avatars | ~2 MB |
| Portraits | ~4 MB |
| Default-skill icons | ~0.8 MB |
| Enemy icons | ~2 MB |
| Bonds, items, bands, UI subset | ~4 MB |
| Loading illustrations | ~2 MB |
| Op Spine | ~36 MB |
| Enemy Spine | ~9 MB |
| Audio (BGM + SFX) | ~15 MB |
| **Total on the server** | **≈ 75 MB** |
| Per client (typical session) | ≈ 20–30 MB |
## 10. Licensing note
- All images, Spine models and audio are **© Hypergryph (上海鹰角网络); overseas publisher Yostar**.
- Every mirror repo states this: fexli "All files … copyrighted by Hypergryph … solely for educational and research purposes", and Ark-Models "不得用于商业用途".
- No official permissive fan-game policy was found. Hypergryph and Yostar do enforce copyright.
- Therefore:
- Non-commercial only; no ads, donations or paywall.
- Show a credits screen: "Arknights © Hypergryph / Yostar. This is an unofficial fan project; all game assets belong to their owners."
- Keep it private or small-group, and remove on request.
- Do not redistribute bulk asset archives; fetch at deploy time.
- Also credit the asset dumpers and tooling: yuanyan3060, fexli/ArkResourceAutoUpdateBot, isHarryh/ArkUnpacker, ArknightsAssets/ArknightsStudio, Kengxxiao/ArknightsGameData, pixi-spine (MIT) and the Spine Runtimes License.
- Fonts: Noto (SIL OFL); Bender (free by author); Novecento Sans (free licence by Synthview).
## 11. Open questions and [ASSUMED] items
1. **Token spines:** only 1 of 20 defaults is available (§5.6). Approve the skin-variant or avatar fallback.
2. **炎佑 summon visual** [ASSUMED]: the data has no dedicated model.
3. **Tile palette and 3D look** [ASSUMED]: procedural. Tune against in-game screenshots (PRTS map pages or bwiki screenshots of 卫戍协议).
4. **Spine scale** `tilePx/300` and enemy default facing [ASSUMED]: verify visually during build.
5. **PMA** for Ark-Models enemy textures: add `pma: true`, then check visually for dark halos. The test render looked acceptable even without it.
6. Voice line index mapping (`cn_021`–`cn_028`) [ASSUMED]: confirm against `charword_table.json` if voices get enabled.
7. The Unity 9-slice metadata for UI panels is not in the dump, so panels are drawn in CSS. Only icons and backdrops use the real sprites.
---
## Addendum (critic)
Written 2026-09-27 by the completeness critic.
- **Spot check.** 40 URLs were sampled at random from the 4,469 unique URLs in `07-assets.json`: avatars, enemy skel, audio on `@voice`, and others. Each got a `curl -I` on 2026-09-27. **Result: 40/40 returned `200`.** The URL patterns in §0 hold.
- **UI reference captured** for the designer. It is not to be shipped: it is official art and layout.
- The official gameplay-intro long image, 800×13685, sliced into `scratchpad/gd/extra/critic/intro_00..09.jpg`.
- 11 in-game screenshots (`*.jpg` in the same folder): prep with shop at R1/R2/R3, briefing, strategy draft, the three 机变 families, 联防, Final Assault, and the banned-operator dropdown.
- They confirm these layout facts:
- bottom shop bar: LEVEL card | operator cards | item card | funds card;
- freeze and refresh buttons above the bar;
- "剩余可放置角色" label;
- 3×2 机变 grid;
- bond strip with layer numbers under the discs;
- `‹ 自己 ›` / `‹ 全景 ›` view switcher.
- **Icons that are also needed:**
- Disabled-bond badge: the person-✕ glyph on greyed bond discs.
- "层数叠加已禁用" state of the bond strip.
- Price-0 reward card.
- Candidates are in the AA2 `ui/autochess/…` groups already listed in §3 i). If none match, draw them in CSS or SVG.