feedback4: merge master — PR #73 (operator battle voices); golden regenerated: every moved roster / bonds / fields scenario differs only in events.engage (the sim's new 'engage' event for the voice slot; no gameplay change), matches unchanged

This commit is contained in:
sganggs committed 2026-10-06 01:55:26 +08:00
commit beb0296a52
25 files changed
+940 -166

No files matched your search

+1 -1
View File
@@ -1 +1 @@
{"version":1,"hash":"8cb555456b2f","generator":"tools/fetch-assets.mjs","stats":{"files":4019,"bytes":289568237,"chars":138,"charsWithBack":135,"enemies":252,"enemiesWithSpine":251,"tokens":20,"tokensWithSpine":13,"spineModels":529,"bonds":23,"items":59,"bands":40,"skills":317,"ui":601,"sfxUnits":382},"chars":{"char_1012_skadi2":{"avatar":"/assets/char/avatar/char_1012_skadi2.png","avatarE2":"/assets/char/avatar/char_1012_skadi2_2.png","portrait":"/assets/char/portrait/char_1012_skadi2_1.png","portraitE2":"/assets/char/portrait/char_1012_skadi2_2.png","spine":{"front":{"skel":"/assets/spine/op/char_1012_skadi2/front/char_1012_skadi2.skel","atlas":"/assets/spine/op/char_1012_skadi2/front/char_1012_skadi2.atlas","textures":["/assets/spine/op/char_1012_skadi2/front/char_1012_skadi2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Skill_1_Loop","end":null,"via":"skill"},"attackDown":null,"skill":{"begin":"Skill_3_Begin","loop":"Skill_3_Loop","end":null,"index":2,"idle":null},"die":"Die","move":null,"stun":{"begin":"Stun_Begin","loop":"Stun","end":null},"skills":{"0":{"begin":"Skill_1_Begin","loop":"Skill_1_Loop","end":null,"index":0,"idle":null},"1":{"begin":"Skill_2_Begin","loop":"Skill_2_Loop","end":null,"index":1,"idle":null},"2":{"begin":"Skill_3_Begin","loop":"Skill_3_Loop","end":null,"index":2,"idle":null}}},"animations":{"Default":0,"Die":1,"Idle":8.767,"Skill_1_Begin":3.2,"Skill_1_Loop":8.767,"Skill_2_Begin":3.933,"Skill_2_Loop":3.333,"Skill_3_Begin":4.4,"Skill_3_Loop":8.767,"Start":1,"Stun":2,"Stun_Begin":1},"events":["OnAttack","OnStart"],"hits":{},"bounds":{"x":-190.586,"y":-25.785,"width":393.516,"height":443.426}}}},"char_1014_nearl2":{"avatar":"/assets/char/avatar/char_1014_nearl2.png","avatarE2":"/assets/char/avatar/char_1014_nearl2_2.png","portrait":"/assets/char/portrait/char_1014_nearl2_1.png","portraitE2":"/assets/char/portrait/char_1014_nearl2_2.png","spine":{"front":{"skel":"/assets/spine/op/char_1014_nearl2/front/char_1014_nearl2.skel","atlas":"/assets/spine/op/char_1014_nearl2/front/char_1014_nearl2.atlas","textures":["/assets/spine/op/char_1014_nearl2/front/char_1014_nearl2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":"Attack_Begin","loop":"Attack_Loop","end":"Attack_End"},"attackDown":null,"skill":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"},"die":"Die","move":null,"stun":null,"skills":{"0":{"begin":"Skill_1_Begin","loop":"Skill_1_Loop","end":null,"index":0,"idle":"Skill_1_Idle"},"1":{"begin":null,"loop":"Skill_2_Die","end":null,"index":1,"idle":null},"2":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"}}},"animations":{"Attack_Begin":0.1,"Attack_End":0.167,"Attack_Loop":1.5,"Default":0,"Die":1,"Idle":2.667,"Skill_3":1.5,"Skill_1_Begin":0.1,"Skill_1_Idle":1,"Skill_1_Loop":1.5,"Skill_1_Loop_Down":1.5,"Skill_2_Die":1.167,"Skill_3_Down":1.5,"Skill_3_Idle":1.333,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack_Loop":[0.433],"Skill_3":[0.5],"Skill_1_Loop":[0.467],"Skill_1_Loop_Down":[0.467],"Skill_3_Down":[0.5]},"bounds":{"x":-400.77,"y":-461.007,"width":720.545,"height":872.242}},"back":{"skel":"/assets/spine/op/char_1014_nearl2/back/char_1014_nearl2.skel","atlas":"/assets/spine/op/char_1014_nearl2/back/char_1014_nearl2.atlas","textures":["/assets/spine/op/char_1014_nearl2/back/char_1014_nearl2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":"Attack_Begin","loop":"Attack_Loop","end":"Attack_End"},"attackDown":null,"skill":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"},"die":null,"move":null,"stun":null,"skills":{"0":{"begin":"Skill_1_Begin","loop":"Skill_1_Loop","end":null,"index":0,"idle":"Skill_1_Idle"},"1":{"begin":"Attack_Begin","loop":"Attack_Loop","end":"Attack_End","via":"attack","index":1,"idle":null},"2":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"}}},"animations":{"Attack_Begin":0.1,"Attack_End":0.167,"Attack_Loop":1.5,"Default":0,"Idle":2.667,"Skill_1_Begin":0.1,"Skill_1_Idle":1,"Skill_1_Loop":1.5,"Skill_3":1.5,"Skill_3_Idle":1.333,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack_Loop":[0.433],"Skill_1_Loop":[0.467],"Skill_3":[0.5]},"bounds":null}}},"char_1016_agoat2":{"avatar":"/assets/char/avatar/char_1016_agoat2.png","avatarE2":"/assets/char/avatar/char_1016_agoat2_2.png","portrait":"/assets/char/portrait/char_1016_agoat2_1.png","portraitE2":"/assets/char/portrait/char_1016_agoat2_2.png","spine":{"front":{"skel":"/assets/spine/op/char_1016_agoat2/front/char_1016_agoat2.skel","atlas":"/assets/spine/op/char_1016_agoat2/front/char_1016_agoat2.atlas","textures":["/assets/spine/op/char_1016_agoat2/front/char_1016_agoat2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":"Skill_3_Begin","loop":"Skill_3_Loop","end":"Skill_3_End","index":2,"idle":"Skill_3_Idle"},"diLine truncated {"version":1,"hash":"9e240a72da75","generator":"tools/fetch-assets.mjs","stats":{"files":5699,"bytes":331609355,"chars":138,"charsWithBack":135,"enemies":252,"enemiesWithSpine":251,"tokens":20,"tokensWithSpine":13,"spineModels":529,"bonds":23,"items":59,"bands":40,"skills":317,"ui":601,"sfxUnits":382,"voiceChars":120},"chars":{"char_1012_skadi2":{"avatar":"/assets/char/avatar/char_1012_skadi2.png","avatarE2":"/assets/char/avatar/char_1012_skadi2_2.png","portrait":"/assets/char/portrait/char_1012_skadi2_1.png","portraitE2":"/assets/char/portrait/char_1012_skadi2_2.png","spine":{"front":{"skel":"/assets/spine/op/char_1012_skadi2/front/char_1012_skadi2.skel","atlas":"/assets/spine/op/char_1012_skadi2/front/char_1012_skadi2.atlas","textures":["/assets/spine/op/char_1012_skadi2/front/char_1012_skadi2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Skill_1_Loop","end":null,"via":"skill"},"attackDown":null,"skill":{"begin":"Skill_3_Begin","loop":"Skill_3_Loop","end":null,"index":2,"idle":null},"die":"Die","move":null,"stun":{"begin":"Stun_Begin","loop":"Stun","end":null},"skills":{"0":{"begin":"Skill_1_Begin","loop":"Skill_1_Loop","end":null,"index":0,"idle":null},"1":{"begin":"Skill_2_Begin","loop":"Skill_2_Loop","end":null,"index":1,"idle":null},"2":{"begin":"Skill_3_Begin","loop":"Skill_3_Loop","end":null,"index":2,"idle":null}}},"animations":{"Default":0,"Die":1,"Idle":8.767,"Skill_1_Begin":3.2,"Skill_1_Loop":8.767,"Skill_2_Begin":3.933,"Skill_2_Loop":3.333,"Skill_3_Begin":4.4,"Skill_3_Loop":8.767,"Start":1,"Stun":2,"Stun_Begin":1},"events":["OnAttack","OnStart"],"hits":{},"bounds":{"x":-190.586,"y":-25.785,"width":393.516,"height":443.426}}}},"char_1014_nearl2":{"avatar":"/assets/char/avatar/char_1014_nearl2.png","avatarE2":"/assets/char/avatar/char_1014_nearl2_2.png","portrait":"/assets/char/portrait/char_1014_nearl2_1.png","portraitE2":"/assets/char/portrait/char_1014_nearl2_2.png","spine":{"front":{"skel":"/assets/spine/op/char_1014_nearl2/front/char_1014_nearl2.skel","atlas":"/assets/spine/op/char_1014_nearl2/front/char_1014_nearl2.atlas","textures":["/assets/spine/op/char_1014_nearl2/front/char_1014_nearl2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":"Attack_Begin","loop":"Attack_Loop","end":"Attack_End"},"attackDown":null,"skill":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"},"die":"Die","move":null,"stun":null,"skills":{"0":{"begin":"Skill_1_Begin","loop":"Skill_1_Loop","end":null,"index":0,"idle":"Skill_1_Idle"},"1":{"begin":null,"loop":"Skill_2_Die","end":null,"index":1,"idle":null},"2":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"}}},"animations":{"Attack_Begin":0.1,"Attack_End":0.167,"Attack_Loop":1.5,"Default":0,"Die":1,"Idle":2.667,"Skill_3":1.5,"Skill_1_Begin":0.1,"Skill_1_Idle":1,"Skill_1_Loop":1.5,"Skill_1_Loop_Down":1.5,"Skill_2_Die":1.167,"Skill_3_Down":1.5,"Skill_3_Idle":1.333,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack_Loop":[0.433],"Skill_3":[0.5],"Skill_1_Loop":[0.467],"Skill_1_Loop_Down":[0.467],"Skill_3_Down":[0.5]},"bounds":{"x":-400.77,"y":-461.007,"width":720.545,"height":872.242}},"back":{"skel":"/assets/spine/op/char_1014_nearl2/back/char_1014_nearl2.skel","atlas":"/assets/spine/op/char_1014_nearl2/back/char_1014_nearl2.atlas","textures":["/assets/spine/op/char_1014_nearl2/back/char_1014_nearl2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":"Attack_Begin","loop":"Attack_Loop","end":"Attack_End"},"attackDown":null,"skill":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"},"die":null,"move":null,"stun":null,"skills":{"0":{"begin":"Skill_1_Begin","loop":"Skill_1_Loop","end":null,"index":0,"idle":"Skill_1_Idle"},"1":{"begin":"Attack_Begin","loop":"Attack_Loop","end":"Attack_End","via":"attack","index":1,"idle":null},"2":{"begin":null,"loop":"Skill_3","end":null,"index":2,"idle":"Skill_3_Idle"}}},"animations":{"Attack_Begin":0.1,"Attack_End":0.167,"Attack_Loop":1.5,"Default":0,"Idle":2.667,"Skill_1_Begin":0.1,"Skill_1_Idle":1,"Skill_1_Loop":1.5,"Skill_3":1.5,"Skill_3_Idle":1.333,"Start":1},"events":["OnAttack","OnStart"],"hits":{"Attack_Loop":[0.433],"Skill_1_Loop":[0.467],"Skill_3":[0.5]},"bounds":null}}},"char_1016_agoat2":{"avatar":"/assets/char/avatar/char_1016_agoat2.png","avatarE2":"/assets/char/avatar/char_1016_agoat2_2.png","portrait":"/assets/char/portrait/char_1016_agoat2_1.png","portraitE2":"/assets/char/portrait/char_1016_agoat2_2.png","spine":{"front":{"skel":"/assets/spine/op/char_1016_agoat2/front/char_1016_agoat2.skel","atlas":"/assets/spine/op/char_1016_agoat2/front/char_1016_agoat2.atlas","textures":["/assets/spine/op/char_1016_agoat2/front/char_1016_agoat2.png"],"pma":false,"anims":{"idle":"Idle","deploy":"Start","attack":{"begin":null,"loop":"Attack","end":null},"attackDown":null,"skill":{"begin":"Skill_3_Begin","loop":"Skill_3_Loop","end":"Skill_3_End","index":2,"idle":"SLine truncated
+17 -4
View File
@@ -19,7 +19,9 @@ npm run assets # = node tools/vendor.mjs && node tools/fetch-assets.mjs
| `--force` | Re-download everything. | | `--force` | Re-download everything. |
| `--offline` | No network. Re-runs post-processing (atlas fixes, skeleton parsing, WOFF2) on what is already on disk, then rebuilds `data/assets.json`. | | `--offline` | No network. Re-runs post-processing (atlas fixes, skeleton parsing, WOFF2) on what is already on disk, then rebuilds `data/assets.json`. |
| `--dry-run` | Print the plan (file and model counts, alias notes) and exit. | | `--dry-run` | Print the plan (file and model counts, alias notes) and exit. |
| `--refresh-index` | Re-download the two upstream indexes: `audio_data.json` and `models_data.json`. | | `--refresh-index` | Re-download the upstream indexes: `audio_data.json`, `charword_table.json` (the 干员战斗语音 slots) and `models_data.json`. |
| `--voice-lang=cn` | 干员战斗语音 language: `cn` (default) | `jp` | `en` | `kr` — the same file names under `voice_cn/`, `voice/`, `voice_en/`, `voice_kr/`. |
| `--voice-all` | Plan every official voice slot, including the prep-only lines no battle plays (干员报到 / 编入队伍 / 任命队长 — 360 files, one per operator and slot). Off by default: nothing requests them, so planning them only makes every run download more. |
| `--prune` | Delete files under `public/assets/` that the manifest no longer references, for example after a mapping change. Without this flag they are only listed in the report. `public/assets/local/` (written by `tools/local-extract`) is never pruned. Implies `--allow-shrink`. | | `--prune` | Delete files under `public/assets/` that the manifest no longer references, for example after a mapping change. Without this flag they are only listed in the report. `public/assets/local/` (written by `tools/local-extract`) is never pruned. Implies `--allow-shrink`. |
| `--allow-shrink` | Write `data/assets.json` even when it loses entries the current one has (see "The manifest never shrinks by accident" below). | | `--allow-shrink` | Write `data/assets.json` even when it loses entries the current one has (see "The manifest never shrinks by accident" below). |
| `--local-spines` | Rewrite `tools/assets/local-enemy-spines.json` (the metadata of the enemy models only the local client has, see "Enemy aliases") from the models `tools/local-extract/extract.py` extracted to `public/assets/local/spine/enemy/`. Run it after a game update changed them; without it the committed file is used and a differing extraction only gets a warning. | | `--local-spines` | Rewrite `tools/assets/local-enemy-spines.json` (the metadata of the enemy models only the local client has, see "Enemy aliases") from the models `tools/local-extract/extract.py` extracted to `public/assets/local/spine/enemy/`. Run it after a game update changed them; without it the committed file is used and a differing extraction only gets a warning. |
@@ -31,7 +33,9 @@ audio entries short — all 42 still resolve upstream; PR #7). When the rebuilt
one, the run keeps the current file, prints the entries it would drop (also in the report: `droppedEntries`, one, the run keeps the current file, prints the entries it would drop (also in the report: `droppedEntries`,
`manifestWritten: false`) and exits 1. Re-run to retry the downloads, or pass `--allow-shrink` (or `--prune`) when the `manifestWritten: false`) and exits 1. Re-run to retry the downloads, or pass `--allow-shrink` (or `--prune`) when the
smaller manifest is intended, for example after a mapping change. Build fields (`version`, `hash`, `generator`, smaller manifest is intended, for example after a mapping change. Build fields (`version`, `hash`, `generator`,
`stats`), new entries and a changed value are never a drop (`tools/assets/manifest.mjs droppedEntries`). `stats`), new entries and a changed value are never a drop (`tools/assets/manifest.mjs droppedEntries`). A run whose
plan legitimately narrows — like the 干员战斗语音 default, which no longer plans the three prep-only slots (360 entries,
DESIGN §21.30) — reports exactly those entries and needs `--allow-shrink` once; the list it prints is the check.
The script is **idempotent**. A file on disk is kept, not re-downloaded, when any one of these holds: The script is **idempotent**. A file on disk is kept, not re-downloaded, when any one of these holds:
- its size matches the ledger entry from a previous download (`.cache/assets-ledger.json`); - its size matches the ledger entry from a previous download (`.cache/assets-ledger.json`);
@@ -48,7 +52,7 @@ How downloads are fetched:
- A manifest entry with fallbacks (for example an enemy icon that falls back to its base enemy's icon) only moves on to the next alternative after a **definitive 404**. When the primary fails transiently (network error, 5xx or an invalid payload after all retries), no fallback is fetched. The path is listed under `downloadErrors` in the report, and the next run retries the primary. - A manifest entry with fallbacks (for example an enemy icon that falls back to its base enemy's icon) only moves on to the next alternative after a **definitive 404**. When the primary fails transiently (network error, 5xx or an invalid payload after all retries), no fallback is fetched. The path is listed under `downloadErrors` in the report, and the next run retries the primary.
- A skeleton that fails to parse is deleted and removed from the ledger, so the next online run downloads it again. - A skeleton that fails to parse is deleted and removed from the ledger, so the next online run downloads it again.
The first run downloads about **269 MiB in about 4,000 files** (it took 134 s on a ~3 MB/s link before the 55 emote and 玩法说明 files, 21.3 MiB, were added). A re-run takes about 1 s. The first run downloads about **309 MiB in about 5,690 files** (it took 134 s on a ~3 MB/s link before the 55 emote and 玩法说明 files, 21.3 MiB, and the 1,680 干员战斗语音 files, 40.1 MiB, were added). A re-run takes about 1 s. The voice count is the twelve slots a battle plays; the three prep-only slots the official client uses elsewhere (干员报到 / 编入队伍 / 任命队长, 360 more files, 18.4 MiB) are left out unless `--voice-all` is passed.
Outputs: Outputs:
- `data/assets.json`: the manifest (committed). - `data/assets.json`: the manifest (committed).
@@ -86,6 +90,7 @@ The research JSONs in `docs/research/` (03, 05, 07) define **which** ids are nee
| 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`) |
| 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` — the lines `charword_table.json` lists (`placeType` = when the game plays one, `voiceAsset` = the path); `--voice-lang=jp|en|kr` takes the same file names from `voice/`, `voice_en/`, `voice_kr/` | `audio/voice/{lang}/{charId}/{cn_nn}.mp3` |
| Fonts: Bender Regular and Light, Novecento Wide | TimWangZi/The-font-of-Arknights | `public/fonts/*.{otf,ttf,woff2}`, `public/fonts/fonts.css` | | Fonts: Bender Regular and Light, Novecento Wide | TimWangZi/The-font-of-Arknights | `public/fonts/*.{otf,ttf,woff2}`, `public/fonts/fonts.css` |
The `stem` of a Spine model is the upstream file name. Two examples: `char_107_liskam` has the stem `char_107_liskarm`, and `enemy_9032_aclionk` uses `enemy_1559_vtlionk`. The skel and atlas of a model always share one stem. pixi-spine locates the atlas by swapping the extension, so this matters. The `stem` of a Spine model is the upstream file name. Two examples: `char_107_liskam` has the stem `char_107_liskarm`, and `enemy_9032_aclionk` uses `enemy_1559_vtlionk`. The skel and atlas of a model always share one stem. pixi-spine locates the atlas by swapping the extension, so this matters.
@@ -136,7 +141,7 @@ All paths are URL paths relative to the site root, for example `/assets/char/ava
hash: 'a1b2c3d4e5f6', // content hash (cache busting) hash: 'a1b2c3d4e5f6', // content hash (cache busting)
generator: 'tools/fetch-assets.mjs', generator: 'tools/fetch-assets.mjs',
stats: { files, bytes, chars, charsWithBack, enemies, enemiesWithSpine, tokens, tokensWithSpine, stats: { files, bytes, chars, charsWithBack, enemies, enemiesWithSpine, tokens, tokensWithSpine,
spineModels, bonds, items, bands, skills, ui, sfxUnits }, spineModels, bonds, items, bands, skills, ui, sfxUnits, voiceChars },
chars: { [charId]: { avatar, avatarE2?, portrait, portraitE2?, spine: { front: Spine, back?: Spine } } }, chars: { [charId]: { avatar, avatarE2?, portrait, portraitE2?, spine: { front: Spine, back?: Spine } } },
enemies: { [enemyId]: { icon, spine?: Spine, spineAliasOf?: enemyId, enemies: { [enemyId]: { icon, spine?: Spine, spineAliasOf?: enemyId,
spineLocal?: { group, skel, atlas, textures, …Spine } } }, spineLocal?: { group, skel, atlas, textures, …Spine } } },
@@ -161,6 +166,14 @@ All paths are URL paths relative to the site root, for example `/assets/char/ava
// `bgmEvent = corrosion` (卡西米尔 act13d5d0), so the rescue phase does not reuse the 作战's track // `bgmEvent = corrosion` (卡西米尔 act13d5d0), so the rescue phase does not reuse the 作战's track
// (audio.js bgmKeyFor 'unite', falling back to `bgm.combat` for a manifest that lacks it) // (audio.js bgmKeyFor 'unite', falling back to `bgm.combat` for a manifest that lacks it)
bossBgm: { [bossId]: { intro?, loop } }, // per-boss track of its R14/R15 level bossBgm: { [bossId]: { intro?, loop } }, // per-boss track of its R14/R15 level
voice: { [charId]: { start, faceEnemy, select, place, skill1…skill4,
resultFour, resultThree, resultTwo, resultLose } },
// 干员战斗语音: an operator's official battle lines (charword_table.json placeType → slot,
// tools/assets/audio.mjs VOICE_SLOTS); a slot with several lines is an array and the client
// draws one (public/js/audio.js voice). Only these twelve ever play (DESIGN §21.30): the
// prep-only slots 干员报到 / 编入队伍 / 任命队长 are left out of the plan by default —
// nothing requests them and they cost 360 files (19.3 MB) per run — and `--voice-all` adds
// them (audio.mjs VOICE_PREP_SLOTS) for the complete official set
sfx: { sfx: {
ui: { click, back, confirm, tab, pick, drop, error, buy, sell, income, refresh, freeze, levelup, ui: { click, back, confirm, tab, pick, drop, error, buy, sell, income, refresh, freeze, levelup,
merge, equip, itemMerge, bondUp, artPlace, ready, timer, draft, yourTurn, yourTurnCircle, merge, equip, itemMerge, bondUp, artPlace, ready, timer, draft, yourTurn, yourTurnCircle,
+14
View File
@@ -1476,6 +1476,20 @@ Two public GitHub issues after 0.1.0, both client-only. Normative lines rewritte
- Tests: `test/sim/feedback1-tank-triggers.test.js` (the data, kit = data, the real battle for the six and for every other MANUAL 重装 record), `test/data.test.js`, `kits_t1t2` / `kits_alt_t1` (PR #12's), `kits_alt_t2` (灰毫 S2), `kits_t5` / `kits_alt_t5` (号角), `test/docs-consistency.test.js`. - Tests: `test/sim/feedback1-tank-triggers.test.js` (the data, kit = data, the real battle for the six and for every other MANUAL 重装 record), `test/data.test.js`, `kits_t1t2` / `kits_alt_t1` (PR #12's), `kits_alt_t2` (灰毫 S2), `kits_t5` / `kits_alt_t5` (号角), `test/docs-consistency.test.js`.
- Extended 2026-10-04: 余 S2 (SKILL_RANGE on its x-1, §22.10); 14 records deviate now. - Extended 2026-10-04: 余 S2 (SKILL_RANGE on its x-1, §22.10); 14 records deviate now.
### 21.30 干员战斗语音: an operator's official lines, in battle only (user request, 2026-10-03) — `tools/assets/plan.mjs` (`voiceSlots`), `tools/assets/audio.mjs` (`indexVoice` / `VOICE_SLOTS` / `VOICE_BATTLE_SLOTS` / `VOICE_PREP_SLOTS` / `VOICE_DIRS`), `tools/assets/cache.mjs`, `tools/fetch-assets.mjs --voice-lang / --voice-all`, `public/js/audio.js` (`VOICE_PRIORITY` / `VOICE_COOLDOWN_MS` / `VoiceGate` / `resultVoiceSlot` / `resultSpeaker` / `voice()`), `server/sim/ai.js performAttack 'engage'`, `battle/runner.js emit('result')`, `screens/game.js onResult`, `ui/detailPanel.js voice`, `ui/settings.js`, `ui/gameLogic.js sanitizeSettings`
User: 「给每个卫戍干员都加语音」, then, after the first build, 「整备阶段不需要干员语音,战斗开始后才需要语音,也就是只需要作战中语音」.
**Where the lines come from.** Every operator the mode can field already has official battle lines in the same voice dump the BGM and SFX come from — `voice_cn/{charId}/cn_nn.mp3` (JP/EN/KR: `voice/`, `voice_en/`, `voice_kr/`, the same file names, `tools/fetch-assets.mjs --voice-lang=jp|en|kr`). `charword_table.json`'s `placeType` says when each line is used, and `indexVoice()` turns it into the manifest's `audio.voice[charId]`: 行动出发 `start` (BATTLE_START), 行动开始 `faceEnemy` (BATTLE_FACE_ENEMY), 选中干员 `select`, 部署 `place`, 作战中1-4 `skill1..skill4` (BATTLE_SKILL_N), 编入队伍 `squad`, 任命队长 `squadFirst`, 结算 `resultFour/Three/Two/Lose` (完成高难行动 / 3星结束行动 / 非3星结束行动 / 行动失败) and 干员报到 `gacha`. A slot with several lines stays an array and the client draws one. Only the base word key is planned (`wordKey === charId`): the dump has no folder for a skin variant's word key (`char_x_ita`, `char_x_epoque#28`), so those lines 404. **Only the slots a battle can play are planned** (`audio.mjs VOICE_BATTLE_SLOTS`): 干员报到 / 编入队伍 / 任命队长 are lines of the official client's own 养成 / 编队 UI, nothing in this mode ever requests them (`test/docs-consistency.test.js` checks the client's `voice(...)` call sites), and downloading them cost 360 files (19.3 MB) on every run — `--voice-all` adds them back for the complete official set. Measured: 120 of the 121 in-mode charIds have a full set — the 17 预备干员 and the mode's own 盟约·辅助干员 have no battle voice at all, so they simply get no entry — 1680 lines of the 2040 upstream, in a manifest of 5694 files / 324.2 MB (files and `stats.voiceChars` in `data/assets.json`).
**Where each line plays — battle only.** Every line comes from a running battle's own event stream or from its settlement, so no hook needs a phase check and the 休整期 stays silent: the shop does not say 干员报到 (a merge-completing buy neither says 任命队长), a retreat does not say 编入队伍 and dragging a piece onto the board in prep says nothing. In battle: 开战 — the first operator deployed says 行动出发 and the others 部署 (the sim's `deploy` events; `setFieldUnits` resets the flag per field — in this mode every piece enters at once, which is why the gate below exists, and a knocked-out operator's redeploy says 部署 again); 接敌 — the sim's new `['engage', id]` (a unit's first attack that hits an enemy, official ENCOUNTER_ENEMY); 技能 — `skillN` from the unit's `skillIndex` (作战中1-4); 选中 — the detail panel opening on an operator **while a battle runs** (`DetailPanel voice` = the game screen's `combat`); 结算 — `resultVoiceSlot()`: 完美作战 ⇒ 3星结束行动 (绝境 / 终极 ⇒ 完成高难行动), 有漏怪 ⇒ 非3星结束行动, 一个没杀 ⇒ 行动失败, spoken by a drawn operator **of that battle's own field** (`resultSpeaker`, from the result's `perPlayer.unitsEnd`: the operators of that field, survivors first, a summon piece never), driven by the runner's new `emit('result', …)` — never the field on screen, so watching a teammate cannot put their operator's voice on the viewer's settlement line (review on #73).
**Why a channel, not SFX.** Voices are 1–4 s long and a battle deploys eight operators, then fires dozens of skills; playing them like SFX would flood the mix. `VoiceGate` copies the official scheduling data (`audio_data.json battleVoice.voiceTypeOptions`): one line at a time, a 1.2 s global gap, a per-unit per-slot cooldown (10 s for 作战中, 3 s for 接敌, 1.5 s for 选中) and takeover when a line's priority beats the playing one by a margin (the official 0.1 s `crossfade`) — the priorities are the official ones (BATTLE_START 100, ENCOUNTER_ENEMY 90, SKILL_ACTIVE 70, PLACE_CHAR 20, FOCUS_CHAR 10) and the settlement lines get 85, above 作战中 but below 接敌. Every deferred step of a line — its decode, its `onended`, the safety timer — carries its own token and returns without touching the gate once it is stale, so a line taken over while it was still loading can never free the channel its replacement just took (review on #73). The channel has its own gain and its own slider (设置 干员语音 / VOICE; `sanitizeSettings` adds `voice`, so a saved profile without it keeps the default and players can turn voices down without losing SFX). The buffer cache also gained a decoded-PCM budget (64 MB) beside its 180-entry cap: a voice decodes to ~0.4–1.3 MB, and 180 of them would be ~100 MB.
**[ASSUMED].** 接敌 as "the unit's first attack on an enemy" (the official trigger is the first enemy it encounters, not necessarily one it hits) and the settlement mapping (漏怪 ⇒ 非3星, 完美作战 in 绝境 / 终极 ⇒ 完成高难行动) are our readings. Lines follow the field on screen: watching a teammate's battle (前往查看) plays *their* operators' lines, while the settlement line fires for the player's own battle only (`e.own`, `battle/runner.js`) and is spoken by an operator of that battle's own field (`resultSpeaker`). 部署 and 选中干员 stay in the plan although the official groups them with the prep lines: a battle *does* play them (the first operator says 行动出发 and the others 部署, and the detail panel says 选中 while a battle runs), so only the three slots no call site ever asks for are dropped — `VOICE_PRIORITY` / `VOICE_COOLDOWN_MS` keep every level, so `--voice-all` restores 编入队伍 / 任命队长 / 干员报到 for the day the 休整期 speaks again.
- Tests: `test/ui/audio.test.js` (the priority table, the settlement slot and its speaker, the gate's global gap / per-unit cooldown / takeover, the stale-callback regression, and the battle events that drive the lines against a fake Web Audio), `test/ui/gameLogic.test.js` (the `voice` setting), `test/sim/battle.test.js` (the wire format: every emitted tuple is a known `EV` — the new `engage` included); the manifest's 1680 voice paths are covered by `test/assets.test.js`.
## 22. GitHub issues after 0.1.1 (v0.1.2) ## 22. GitHub issues after 0.1.1 (v0.1.2)
Public GitHub issues filed after the 0.1.1 release (2026-10-03), each triaged against 0.1.1 first; the real ones were fixed for 0.1.2 in fix branches WA–WL, each independently reviewed before it was merged into `feedback2`. Where each is handled: #25 back-facing operators keep their attack pose when knocked out → §22.1; #51 突袭's idle redeploy → §22.2; #33 items 1–2 阿戈尔's devour and 乌尔比安 S3 → §22.3; #33 item 6 活性源石, with the 沼泽 and 深水区 terrain → §22.4; #42 emotes and guide pages without a local client, and the English title (#38) → §22.5; #32 item 4 钩索师 / 推击手 on 高台 → §22.6; #52 史尔特尔's 余烬 → §22.7; #43 an enemy's 隐匿 after a block, and the 深池逐火 ember window → §22.8; #32 item 7 荒芜拉普兰德 S3's drones → §22.9; #32 item 1 余 S2 → §22.10; #44 归溟幽灵鲨's 替身 → §22.11; #32 item 6 enemy splash against a 隐匿 operator → §22.12; #35 / #16 (PR #48) 要塞 against air units and 莫斯提马's slow icon → §22.13; #58 / #60 / #64 follow in §22.14–§22.16. Decisions of the owner, 2026-10-04: 荒芜拉普兰德's drones measure the distance to an enemy's position, a huge enemy's centre (§22.9); 余 S2 casts with an enemy on its x-1 — a deliberate deviation from the official TAKE_DAMAGE row, added to §21.29's six (§22.10); 乌尔比安 keeps the devour gains through his 【移动】 — the deviation stays (§22.3). Public GitHub issues filed after the 0.1.1 release (2026-10-03), each triaged against 0.1.1 first; the real ones were fixed for 0.1.2 in fix branches WA–WL, each independently reviewed before it was merged into `feedback2`. Where each is handled: #25 back-facing operators keep their attack pose when knocked out → §22.1; #51 突袭's idle redeploy → §22.2; #33 items 1–2 阿戈尔's devour and 乌尔比安 S3 → §22.3; #33 item 6 活性源石, with the 沼泽 and 深水区 terrain → §22.4; #42 emotes and guide pages without a local client, and the English title (#38) → §22.5; #32 item 4 钩索师 / 推击手 on 高台 → §22.6; #52 史尔特尔's 余烬 → §22.7; #43 an enemy's 隐匿 after a block, and the 深池逐火 ember window → §22.8; #32 item 7 荒芜拉普兰德 S3's drones → §22.9; #32 item 1 余 S2 → §22.10; #44 归溟幽灵鲨's 替身 → §22.11; #32 item 6 enemy splash against a 隐匿 operator → §22.12; #35 / #16 (PR #48) 要塞 against air units and 莫斯提马's slow icon → §22.13; #58 / #60 / #64 follow in §22.14–§22.16. Decisions of the owner, 2026-10-04: 荒芜拉普兰德's drones measure the distance to an enemy's position, a huge enemy's centre (§22.9); 余 S2 casts with an enemy on its x-1 — a deliberate deviation from the official TAKE_DAMAGE row, added to §21.29's six (§22.10); 乌尔比安 keeps the devour gains through his 【移动】 — the deviation stays (§22.3).
+2 -1
View File
@@ -1196,7 +1196,8 @@ Unknown subprofessions fall back to the profession default (test `professions.te
(`none|arrow|bolt|bomb|lob|orb|drone|enemy|boomerang|droneBomb|chain|chainHeal`; a boomerang's way back has no event — the (`none|arrow|bolt|bomb|lob|orb|drone|enemy|boomerang|droneBomb|chain|chainHeal`; a boomerang's way back has no event — the
renderer flies it back to the thrower at `BOOMERANG_RETURN_SPEED`; an enemy's `profile.shot` may name another kind, renderer flies it back to the thrower at `BOOMERANG_RETURN_SPEED`; an enemy's `profile.shot` may name another kind,
e.g. `mortar` for 帝国炮火先兆者, which the renderer does not draw — its fx `bombardShell` is the shell), `['dmg', tgt, amount, type]` (`phys|arts|true|burn|neural|necrosis|apoptosis`), e.g. `mortar` for 帝国炮火先兆者, which the renderer does not draw — its fx `bombardShell` is the shell), `['dmg', tgt, amount, type]` (`phys|arts|true|burn|neural|necrosis|apoptosis`),
`['heal', tgt, amount]`, `['skill', id, 1|0]`, `['die', id, reason]`, `['leak', id]`, `['status', id, key, 1|0]`, `['heal', tgt, amount]`, `['skill', id, 1|0]`, `['engage', id]` (an ally's first attack that hits an enemy — the client's
行动开始 voice, DESIGN §21.30), `['die', id, reason]`, `['leak', id]`, `['status', id, key, 1|0]`,
`['fx', kind, x, y, extra]` (`hitCap` `{ id, n }`: a leader's hit cancelled by 限伤 — the renderer draws nothing; `['fx', kind, x, y, extra]` (`hitCap` `{ id, n }`: a leader's hit cancelled by 限伤 — the renderer draws nothing;
`extra.form` = the unit's model form from then on — an enemy's `content/enemies.js setForm`, a 傀儡师's 替身 — `shared/protocol.js fxForm`), `extra.form` = the unit's model form from then on — an enemy's `content/enemies.js setForm`, a 傀儡师's 替身 — `shared/protocol.js fxForm`),
`['layer', playerId, bondId, n]` (n = the layers actually added, capped at 999), `['bounty', playerId, coins]`. `['layer', playerId, bondId, n]` (n = the layers actually added, capped at 999), `['bounty', playerId, coins]`.
+288 -13
View File
@@ -1,21 +1,28 @@
// Audio manager (Web Audio): BGM per phase, UI SFX, per-unit battle SFX. Never throws. // Audio manager (Web Audio): BGM per phase, UI SFX, per-unit battle SFX, operator battle voice. Never throws.
// //
// Sources: data/assets.json → audio (docs/ASSETS.md): // Sources: data/assets.json → audio (docs/ASSETS.md):
// bgm { lobby, prep, combat, combatAlts?: [ {intro?, loop}, … ], boss: { intro?, loop } }, // bgm { lobby, prep, combat, combatAlts?: [ {intro?, loop}, … ], boss: { intro?, loop } },
// bossBgm { [bossId]: { intro?, loop } }, // bossBgm { [bossId]: { intro?, loop } },
// voice { [charId]: { start, faceEnemy, select, place, skill1…skill4, squad, squadFirst, result*, gacha } },
// sfx.ui { click, buy, sell, refresh, freeze, levelup, merge, equip, ready, timer, yourTurn, … }, // sfx.ui { click, buy, sell, refresh, freeze, levelup, merge, equip, ready, timer, yourTurn, … },
// sfx.battle { deploy, tokenDeploy, charDie, tokenDie?, enemyDie, enemyHit, heal, killCoin, … }, // sfx.battle { deploy, tokenDeploy, charDie, tokenDie?, enemyDie, enemyHit, heal, killCoin, … },
// sfx.units { [charId|tokenId|enemyId]: { attack?, hit?, skill?, die?, born?, mix?: { [role]: { p?, vol? } } } }. // sfx.units { [charId|tokenId|enemyId]: { attack?, hit?, skill?, die?, born?, mix?: { [role]: { p?, vol? } } } }.
// //
// - The AudioContext is created on the first user gesture (pointerdown/keydown/touchend), so browsers // - The AudioContext is created on the first user gesture (pointerdown/keydown/touchend), so browsers
// never block or warn; everything requested before that is remembered (BGM) or dropped (SFX). // never block or warn; everything requested before that is remembered (BGM) or dropped (SFX).
// - Channels: master → { bgm, sfx } gains; volumes from settings (0..1) + mute. Tab hidden ⇒ suspend. // - Channels: master → { bgm, sfx, voice } gains; volumes from settings (0..1) + mute. Tab hidden ⇒ suspend.
// - BGM: `intro` then `loop` (1 s crossfade); switching tracks fades out/in (0.8 s). The same loop URL // - BGM: `intro` then `loop` (1 s crossfade); switching tracks fades out/in (0.8 s). The same loop URL
// keeps playing across phases (prep and combat share a track). // keeps playing across phases (prep and combat share a track).
// - 开战 BGM: `bgm.combatAlts` are the mode's own battle tracks (塞壬唱片 骑士之日 / 无畏者). The track is fixed per // - 开战 BGM: `bgm.combatAlts` are the mode's own battle tracks (塞壬唱片 骑士之日 / 无畏者). The track is fixed per
// round, not drawn: 无畏者 through rounds 1–7 and 骑士之日 from round 8 on (`combatTrackFor`, the official // round, not drawn: 无畏者 through rounds 1–7 and 骑士之日 from round 8 on (`combatTrackFor`, the official
// schedule), so every client of a match hears the same one, a fight never switches track halfway through and the // schedule), so every client of a match hears the same one, a fight never switches track halfway through and the
// 联防 that follows a 作战 keeps its round's track. // 联防 that follows a 作战 keeps its round's track.
// - 干员战斗语音 (`audio.voice`, user request): an operator says the official line of the moment it is in —
// 行动出发 start, 行动开始 faceEnemy, 选中干员 select, 部署 place, 作战中1-4 skillN and the settlement's
// 结算 result* (all battle-only; the 休整期 is silent). Voices are seconds long and a battle deploys eight
// operators, then fires dozens of skills, so they run on their own channel (own gain, settings 干员语音) through
// VoiceGate: one line at a time, a global gap, a per-unit per-slot cooldown, and a higher-priority line taking the
// channel over — the official scheduling of `audio_data.json battleVoice.voiceTypeOptions`.
// - Battle SFX from `b.ev` tuples (`handleBattleEvents`): at most MAX_VOICES concurrent unit sounds, at most // - Battle SFX from `b.ev` tuples (`handleBattleEvents`): at most MAX_VOICES concurrent unit sounds, at most
// MAX_PER_URL overlapping copies of one sound (the official banks' maxSoundAllowed 2), a per-unit cooldown and a // MAX_PER_URL overlapping copies of one sound (the official banks' maxSoundAllowed 2), a per-unit cooldown and a
// per-URL minimum gap (SfxLimiter), so a 60-unit fight stays listenable. // per-URL minimum gap (SfxLimiter), so a 60-unit fight stays listenable.
@@ -47,8 +54,13 @@ const UNIT_COOLDOWN_MS = 160;
const URL_GAP_MS = 45; const URL_GAP_MS = 45;
const MAX_PER_URL = 2; const MAX_PER_URL = 2;
const BUFFER_CACHE = 180; const BUFFER_CACHE = 180;
/** Decoded-PCM budget of the buffer cache beside its entry count: a voice line decodes to 0.4–1.3 MB (see _buffer). */
const BUFFER_BYTES = 64 * 1024 * 1024;
const XFADE_S = 1; const XFADE_S = 1;
const FADE_S = 0.8; const FADE_S = 0.8;
/** Voice: shortest gap between two lines, and the crossfade of a higher-priority line taking the channel (official 0.1 s). */
const VOICE_GAP_MS = 1200;
const VOICE_XFADE_S = 0.1;
/** 'atk' projectile kinds whose first id is the previous bounce target (sim ai.js), not the attacker. */ /** 'atk' projectile kinds whose first id is the previous bounce target (sim ai.js), not the attacker. */
const CHAIN_KINDS = new Set(['chain', 'chainHeal']); const CHAIN_KINDS = new Set(['chain', 'chainHeal']);
/** 'dmg' types that are an attack's impact (element gauge fills / 元素伤害 carry the element's name instead). */ /** 'dmg' types that are an attack's impact (element gauge fills / 元素伤害 carry the element's name instead). */
@@ -224,6 +236,67 @@ export function unitSoundPlays(mix, roll) {
return !(Number.isFinite(p) && p >= 0 && p < 1) || roll < p; return !(Number.isFinite(p) && p >= 0 && p < 1) || roll < p;
} }
/**
* Voice priorities — the official battle voice types (`audio_data.json battleVoice.voiceTypeOptions`) mapped onto the
* manifest's slots: BATTLE_START 100, BATTLE_FACE_ENEMY 90, SKILL_ACTIVE 70, PASSIVE_IMP 60, PASSIVE_NOR 50,
* PLACE_CHAR 20, FOCUS_CHAR 10. The settlement lines are no battle voice of the official scheduler: they sit at 85,
* above 作战中 (70) but below 接敌 (90), so a battle's last word is never cut off by an ordinary line. The four prep
* slots (部署 / 编入队伍 / 任命队长 / 干员报到) keep their levels although the 休整期 is silent (see the header).
*/
export const VOICE_PRIORITY = Object.freeze({
start: 100, faceEnemy: 90,
skill1: 70, skill2: 70, skill3: 70, skill4: 70,
resultFour: 85, resultThree: 85, resultTwo: 85, resultLose: 85,
gacha: 60, squadFirst: 45, squad: 30, place: 20, select: 10,
});
/** Per-unit per-slot cooldowns (ms): the official 10 s of the 作战中 (passive skill) lines, 3 s between 接敌 lines. */
export const VOICE_COOLDOWN_MS = Object.freeze({
start: 0, faceEnemy: 3000,
skill1: 10000, skill2: 10000, skill3: 10000, skill4: 10000,
resultFour: 0, resultThree: 0, resultTwo: 0, resultLose: 0,
gacha: 0, squadFirst: 0, squad: 0, place: 0, select: 1500,
});
/**
* The settlement slot of a finished 作战: 完美作战 ⇒ 3星结束行动 (绝境 / 终极 ⇒ 完成高难行动 instead), a leaked enemy
* ⇒ 非3星结束行动, nothing killed at all ⇒ 行动失败.
* @param {{perfect?:boolean, leaked?:number, killed?:number, total?:number, hard?:boolean}} [o]
* @returns {'resultFour'|'resultThree'|'resultTwo'|'resultLose'}
*/
export function resultVoiceSlot(o = {}) {
const leaked = Number.isFinite(o.leaked) ? o.leaked : 0;
const killed = Number.isFinite(o.killed) ? o.killed : 0;
const total = Number.isFinite(o.total) ? o.total : 0;
if (o.perfect) return o.hard ? 'resultFour' : 'resultThree';
if (total > 0 && killed <= 0) return 'resultLose';
if (leaked > 0) return 'resultTwo';
return o.hard ? 'resultFour' : 'resultThree';
}
/**
* Who says a battle's **result** line (结算): an operator of THAT battle's own field. Never the field the player happens
* to be looking at (review on #73): reading the tracked units of the field on screen made a teammate's operator say the
* viewer's 作战结束 line while the viewer was watching them.
* `pp` is that battle's own `perPlayer` entry (BattleResult, sim/Battle.js): `unitsEnd` lists what stood on its field
* when the battle ended, `defId` being an operator (`char_*`) or a summon piece (`token_*`, which does not talk).
* Survivors speak first — the line reports how the battle went, and a wiped-out squad is the only case where a fallen
* operator ends up saying it. Ties are drawn like every other unit sound.
* @param {{ unitsEnd?: Array<{ defId?: string|null, alive?: boolean }> } | null | undefined} pp that battle's perPlayer
* @param {() => number} [random]
* @returns {string|null} charId, or null when that battle fielded no operator at all
*/
export function resultSpeaker(pp, random = Math.random) {
const ops = [];
for (const u of Array.isArray(pp?.unitsEnd) ? pp.unitsEnd : []) {
if (u && typeof u.defId === 'string' && u.defId.startsWith('char_')) ops.push({ id: u.defId, alive: !!u.alive });
}
const standing = ops.filter((o) => o.alive);
const pool = standing.length ? standing : ops; // only a wiped-out squad is spoken for by a fallen operator
if (!pool.length) return null;
return pool[Math.min(pool.length - 1, Math.floor(random() * pool.length))].id;
}
/** Concurrency + cooldown gate for battle SFX. Pure (time is passed in). */ /** Concurrency + cooldown gate for battle SFX. Pure (time is passed in). */
/** Gestures that may unlock audio: iOS Safari only accepts touchend / click / keydown; pointerdown covers the rest. */ /** Gestures that may unlock audio: iOS Safari only accepts touchend / click / keydown; pointerdown covers the rest. */
const UNLOCK_EVENTS = ['pointerdown', 'touchend', 'click', 'keydown']; const UNLOCK_EVENTS = ['pointerdown', 'touchend', 'click', 'keydown'];
@@ -275,6 +348,63 @@ export class SfxLimiter {
} }
} }
/**
* Voice gate: one line at a time, a global gap between two lines, a per-unit per-slot cooldown, and takeover by a
* clearly more important line (the caller fades the playing one out first). Pure — the clock is passed in.
*/
export class VoiceGate {
/** @param {{ gapMs?: number, preemptMargin?: number, maxUnits?: number }} [o] */
constructor(o = {}) {
this.gapMs = Number.isFinite(o.gapMs) ? o.gapMs : VOICE_GAP_MS;
// a line only takes the channel over when its priority beats the playing one by this margin: an equal-priority
// line (two 作战中 of different operators) waits for its turn instead of cutting the other off
this.preemptMargin = Number.isFinite(o.preemptMargin) ? o.preemptMargin : 10;
this.maxUnits = Number.isFinite(o.maxUnits) ? o.maxUnits : 400;
this.playing = null; // { slot, pri } of the line on air
this.lastAt = -Infinity;
this.unitUntil = new Map();
}
/** Drop all state (a new field must not inherit the previous battle's cooldowns). */
reset() {
this.playing = null;
this.lastAt = -Infinity;
this.unitUntil.clear();
}
/**
* May a `slot` line from `unitKey` start now?
* @param {string} slot
* @param {string|number|null} unitKey the cooldown key (a battle unit id; null = no per-unit cooldown)
* @param {number} now ms
* @returns {'play'|'preempt'|'drop'}
*/
request(slot, unitKey, now) {
const pri = VOICE_PRIORITY[slot] ?? 0;
if (this.playing) {
if (pri < this.playing.pri + this.preemptMargin) return 'drop';
return 'preempt'; // a clearly more important line takes the channel
}
if (now - this.lastAt < this.gapMs) return 'drop';
if (unitKey != null && now < (this.unitUntil.get(`${unitKey}:${slot}`) || 0)) return 'drop';
return 'play';
}
/** Record a line that started (call right after request() answered play / preempt). */
start(slot, unitKey, now) {
this.playing = { slot, pri: VOICE_PRIORITY[slot] ?? 0 };
this.lastAt = now;
const cd = VOICE_COOLDOWN_MS[slot] ?? 0;
if (unitKey != null && cd > 0) {
if (this.unitUntil.size > this.maxUnits) this.unitUntil.clear();
this.unitUntil.set(`${unitKey}:${slot}`, now + cd);
}
}
/** The line ended (naturally, by takeover or by a stop). */
release() { this.playing = null; }
}
// ---- manager ----------------------------------------------------------------------------------------------- // ---- manager -----------------------------------------------------------------------------------------------
/** /**
@@ -303,10 +433,16 @@ export class AudioManager {
this.master = null; this.master = null;
this.bgmGain = null; this.bgmGain = null;
this.sfxGain = null; this.sfxGain = null;
this.volumes = { bgm: 0.6, sfx: 0.8, muted: false }; this.voiceGain = null;
this.volumes = { bgm: 0.6, sfx: 0.8, voice: 0.8, muted: false };
this.buffers = new Map(); // url → Promise<AudioBuffer|null> (insertion order = LRU) this.buffers = new Map(); // url → Promise<AudioBuffer|null> (insertion order = LRU)
this.bufBytes = new Map(); // url → decoded PCM bytes (the byte budget of the LRU, see _buffer)
this.warned = new Set(); this.warned = new Set();
this.limiter = new SfxLimiter(); this.limiter = new SfxLimiter();
this.voiceGate = new VoiceGate();
this.voiceNode = null; // { src, gain, url, token } of the line on air
this.voiceToken = 0;
this.startVoiceDone = false; // 行动出发 of this field (the first operator deployed says it)
this.uiVoices = 0; this.uiVoices = 0;
this.wantBgm = null; // desired key (kept while locked) this.wantBgm = null; // desired key (kept while locked)
this.bgm = null; // { key, loopUrl, nodes: [{src, gain}], gain } this.bgm = null; // { key, loopUrl, nodes: [{src, gain}], gain }
@@ -359,8 +495,10 @@ export class AudioManager {
this.master = this.ctx.createGain(); this.master = this.ctx.createGain();
this.bgmGain = this.ctx.createGain(); this.bgmGain = this.ctx.createGain();
this.sfxGain = this.ctx.createGain(); this.sfxGain = this.ctx.createGain();
this.voiceGain = this.ctx.createGain();
this.bgmGain.connect(this.master); this.bgmGain.connect(this.master);
this.sfxGain.connect(this.master); this.sfxGain.connect(this.master);
this.voiceGain.connect(this.master);
this.master.connect(this.ctx.destination); this.master.connect(this.ctx.destination);
// iOS / iPadOS: a call, Siri or another app's audio moves a running context to 'interrupted' (or 'suspended'); // iOS / iPadOS: a call, Siri or another app's audio moves a running context to 'interrupted' (or 'suspended');
// a resume without a gesture may then be refused — listen for the next gesture again (dropped once it runs) // a resume without a gesture may then be refused — listen for the next gesture again (dropped once it runs)
@@ -430,11 +568,16 @@ export class AudioManager {
/** /**
* Set channel volumes (0..1) and mute. * Set channel volumes (0..1) and mute.
* @param {{ bgm?: number, sfx?: number, muted?: boolean }} v * @param {{ bgm?: number, sfx?: number, voice?: number, muted?: boolean }} v
*/ */
setVolumes(v) { setVolumes(v) {
const n = (x, d) => (Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : d); const n = (x, d) => (Number.isFinite(x) ? Math.max(0, Math.min(1, x)) : d);
this.volumes = { bgm: n(v?.bgm, this.volumes.bgm), sfx: n(v?.sfx, this.volumes.sfx), muted: typeof v?.muted === 'boolean' ? v.muted : this.volumes.muted }; this.volumes = {
bgm: n(v?.bgm, this.volumes.bgm),
sfx: n(v?.sfx, this.volumes.sfx),
voice: n(v?.voice, this.volumes.voice),
muted: typeof v?.muted === 'boolean' ? v.muted : this.volumes.muted,
};
this._applyVolumes(); this._applyVolumes();
} }
@@ -446,6 +589,8 @@ export class AudioManager {
// perceptual curve // perceptual curve
this.bgmGain.gain.setTargetAtTime(this.volumes.bgm ** 2 * 0.55, t, 0.05); this.bgmGain.gain.setTargetAtTime(this.volumes.bgm ** 2 * 0.55, t, 0.05);
this.sfxGain.gain.setTargetAtTime(this.volumes.sfx ** 2 * 0.9, t, 0.03); this.sfxGain.gain.setTargetAtTime(this.volumes.sfx ** 2 * 0.9, t, 0.03);
// no 0.9: a voice line is already mastered as loud as the rest of the official mix (settings 干员语音 tunes it)
this.voiceGain.gain.setTargetAtTime(this.volumes.voice ** 2, t, 0.03);
} catch { /* ignore */ } } catch { /* ignore */ }
} }
@@ -482,15 +627,35 @@ export class AudioManager {
} }
})(); })();
this.buffers.set(url, p); this.buffers.set(url, p);
while (this.buffers.size > BUFFER_CACHE) { p.then((buf) => {
const first = this.buffers.keys().next().value; if (!buf) return;
// never evict the playing BGM try {
if (this.bgm && first === this.bgm.loopUrl) { const v = this.buffers.get(first); this.buffers.delete(first); this.buffers.set(first, v); break; } this.bufBytes.set(url, (buf.length || 0) * (buf.numberOfChannels || 1) * 4);
this.buffers.delete(first); this._trimBuffers();
} } catch { /* ignore */ }
}, () => {});
this._trimBuffers();
return p; return p;
} }
/**
* Evict least-recently-used buffers until both the entry count and the decoded-PCM budget hold. The count alone is
* not enough once voice lines are in the cache: 180 of them are ~100 MB of PCM (a voice decodes to 0.4–1.3 MB).
*/
_trimBuffers() {
let bytes = 0;
for (const n of this.bufBytes.values()) bytes += n;
if (this.buffers.size <= BUFFER_CACHE && bytes <= BUFFER_BYTES) return;
for (const url of [...this.buffers.keys()]) {
if (this.buffers.size <= BUFFER_CACHE && bytes <= BUFFER_BYTES) break;
// never evict the playing BGM (a voice keeps its own reference to its buffer)
if (this.bgm && url === this.bgm.loopUrl) continue;
bytes -= this.bufBytes.get(url) || 0;
this.buffers.delete(url);
this.bufBytes.delete(url);
}
}
/** Preload a list of URLs (e.g. UI SFX) once unlocked. */ /** Preload a list of URLs (e.g. UI SFX) once unlocked. */
preload(urls) { preload(urls) {
if (!this.ctx) return; if (!this.ctx) return;
@@ -640,6 +805,93 @@ export class AudioManager {
} catch { return false; } } catch { return false; }
} }
// ---- operator battle voice ----------------------------------------------------------------------------------
/**
* Play an operator's battle line (`audio.voice[charId][slot]`; a slot with several lines draws one at random).
* Only in battle: every caller is a running battle's own event stream or its settlement (user request — the 休整期
* is silent). The line must pass VoiceGate: one at a time, a global gap, a per-unit cooldown, higher priority wins.
* @param {string} charId e.g. 'char_263_skadi'
* @param {'start'|'faceEnemy'|'select'|'place'|'skill1'|'skill2'|'skill3'|'skill4'|'squad'|'squadFirst'
* |'resultFour'|'resultThree'|'resultTwo'|'resultLose'|'gacha'} slot
* @param {{ unitKey?: string|number|null, volume?: number }} [o] `unitKey` = the cooldown key (a battle unit id)
* @returns {boolean} whether such a line exists and started
*/
voice(charId, slot, o = {}) {
try {
if (!this.ctx || !this.voiceGain || this.volumes.muted || this.volumes.voice <= 0) return false;
if (typeof charId !== 'string' || typeof slot !== 'string') return false;
const line = this.getManifest()?.audio?.voice?.[charId]?.[slot];
const url = Array.isArray(line) ? line[Math.floor(Math.random() * line.length)] : line;
if (typeof url !== 'string' || !url) return false;
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
const verdict = this.voiceGate.request(slot, o.unitKey ?? null, now);
if (verdict === 'drop') return false;
if (verdict === 'preempt') this._stopVoice();
this.voiceGate.start(slot, o.unitKey ?? null, now);
const token = ++this.voiceToken;
this._playVoice(url, token, o.volume);
return true;
} catch (err) { this._warn('voice', err); return false; }
}
/** Fetch/decode and start one voice line through the voice channel. */
_playVoice(url, token, volume) {
// `token` is the line's own `voiceToken`. Every deferred step below — the decode, a failed fetch, `onended` and the
// safety timer — can land AFTER this line was taken over or stopped: `voiceToken` has moved on and the channel then
// belongs to the line that replaced it. So each step re-checks its token and, when it is stale, touches NOTHING:
// `_stopVoice` (takeover / stop) and `setFieldUnits` released the gate themselves. An unconditional release here let
// a stale callback free the channel the NEW line had just taken, and the next line walked in on top of it (review
// on #73).
this._buffer(url).then((buf) => {
if (token !== this.voiceToken) return; // taken over / stopped while it decoded: not ours to release
if (!buf || !this.ctx || !this.voiceGain) { this.voiceGate.release(); return; }
try {
const src = this.ctx.createBufferSource();
src.buffer = buf;
const gain = this.ctx.createGain();
gain.gain.value = Math.max(0, Math.min(1.5, Number.isFinite(volume) ? volume : 1));
src.connect(gain); gain.connect(this.voiceGain);
const node = { src, gain, url, token };
let done = false;
const end = () => {
if (done) return;
done = true;
// this line's own end (natural, or the safety timer): only the line that still owns the channel may free it.
// A stale end is the takeover's leftovers — `_stopVoice` already faded it out and released the gate.
if (token === this.voiceToken) {
if (this.voiceNode === node) this.voiceNode = null;
this.voiceGate.release();
}
try { gain.disconnect(); } catch { /* ignore */ }
};
src.onended = end;
setTimeout(end, (buf.duration + 0.3) * 1000); // safety if onended never fires
src.start();
this.voiceNode = node;
} catch (err) {
this._warn('voice-play', err);
if (token === this.voiceToken) this.voiceGate.release();
}
}, () => { if (token === this.voiceToken) this.voiceGate.release(); });
}
/** Fade the line on air out (a higher priority line is taking the channel over). */
_stopVoice() {
const cur = this.voiceNode;
this.voiceNode = null;
this.voiceToken += 1; // a line still decoding must not start afterwards
this.voiceGate.release();
if (!cur || !this.ctx) return;
try {
const t = this.ctx.currentTime;
cur.gain.gain.cancelScheduledValues(t);
cur.gain.gain.setValueAtTime(cur.gain.gain.value, t);
cur.gain.gain.linearRampToValueAtTime(0, t + VOICE_XFADE_S);
cur.src.stop(t + VOICE_XFADE_S + 0.02);
} catch { /* ignore */ }
}
// ---- battle events ------------------------------------------------------------------------------------------ // ---- battle events ------------------------------------------------------------------------------------------
/** Reset the unit map for a new field (m.field.units = UnitInfo[]). */ /** Reset the unit map for a new field (m.field.units = UnitInfo[]). */
@@ -647,6 +899,9 @@ export class AudioManager {
this.units.clear(); this.units.clear();
this.lastAttacker.clear(); this.lastAttacker.clear();
this.consumed.clear(); this.consumed.clear();
// a new field is a new battle: the first operator deployed says 行动出发 again and no cooldown carries over
this.startVoiceDone = false;
try { this.voiceGate.reset(); this._stopVoice(); } catch { /* ignore */ }
for (const u of Array.isArray(units) ? units : []) this._track(u); for (const u of Array.isArray(units) ? units : []) this._track(u);
} }
@@ -693,7 +948,18 @@ export class AudioManager {
this.battle('heal', { unitKey: `heal:${e[1]}`, volume: 0.35 }); this.battle('heal', { unitKey: `heal:${e[1]}`, volume: 0.35 });
} else if (kind === 'skill' && e[2]) { } else if (kind === 'skill' && e[2]) {
const u = this.units.get(e[1]); const u = this.units.get(e[1]);
if (u) this.unit(u.def, 'skill', e[1], u.skillIndex ?? undefined); if (u) {
this.unit(u.def, 'skill', e[1], u.skillIndex ?? undefined);
// 作战中N: the equipped skill's own slot (0-based; 作战中4 is the fallback of a 4th slot)
if (unitSoundClass(u) === 'char') {
const n = Number.isInteger(u.skillIndex) ? Math.min(4, u.skillIndex + 1) : 1;
this.voice(u.def, `skill${n}`, { unitKey: e[1] });
}
}
} else if (kind === 'engage') {
// 行动开始: the first attack a unit makes on an enemy (the sim's ENGAGE, official ENCOUNTER_ENEMY, 3 s apart)
const u = this.units.get(e[1]);
if (u && unitSoundClass(u) === 'char') this.voice(u.def, 'faceEnemy', { unitKey: e[1] });
} else if (kind === 'die') { } else if (kind === 'die') {
const u = this.units.get(e[1]); const u = this.units.get(e[1]);
if (!u) continue; if (!u) continue;
@@ -710,6 +976,15 @@ export class AudioManager {
if (!u || u.side === 'enemy') continue; if (!u || u.side === 'enemy') continue;
const m = this.getManifest(); const m = this.getManifest();
const url = deploySfxUrl(m, u); const url = deploySfxUrl(m, u);
// 行动出发 / 部署: the first operator of the battle says the battle-start line, the others their deploy line
// (a knocked-out operator redeploying in the same battle is one of the others; a summon says nothing).
// Kept ahead of the deploy-SFX guards below: the voice channel is independent of the unit sound's roll.
if (unitSoundClass(u) === 'char') {
if (!this.startVoiceDone) {
this.startVoiceDone = true;
if (!this.voice(u.def, 'start')) this.voice(u.def, 'place', { unitKey: e[1] });
} else this.voice(u.def, 'place', { unitKey: e[1] });
}
if (!url) continue; if (!url) continue;
const own = url === m?.audio?.sfx?.units?.[u.def]?.born; const own = url === m?.audio?.sfx?.units?.[u.def]?.born;
const mix = own ? m.audio.sfx.units[u.def].mix?.born : null; const mix = own ? m.audio.sfx.units[u.def].mix?.born : null;
@@ -739,7 +1014,7 @@ export const audio = new AudioManager({ getManifest: () => manifestGetter() });
/** /**
* Wire the singleton to the app (called once by main.js): manifest source, settings and store-driven BGM. * Wire the singleton to the app (called once by main.js): manifest source, settings and store-driven BGM.
* @param {{ getManifest: () => any, subscribe: (fn: (s:any, prev:any) => void) => () => void, getState: () => any, * @param {{ getManifest: () => any, subscribe: (fn: (s:any, prev:any) => void) => () => void, getState: () => any,
* selectRoute: (s:any) => string, settings?: { bgm:number, sfx:number, muted:boolean } }} deps * selectRoute: (s:any) => string, settings?: { bgm:number, sfx:number, voice:number, muted:boolean } }} deps
*/ */
export function installAudio(deps) { export function installAudio(deps) {
try { try {
+3
View File
@@ -452,6 +452,9 @@ export function createBattleRunner(deps) {
} catch (err) { console.warn('[runner] result failed', err); } } catch (err) { console.warn('[runner] result failed', err); }
if (result) { if (result) {
e.result = result; e.result = result;
// the view answers with the settlement voice of this battle (screens/game.js → audio.voice result*): the
// compact result carries the leaks and the kill count the slot is picked from
emit('result', { fieldId: e.fieldId, battleId: e.battleId, own: !!e.own, result });
deliver(e); deliver(e);
} }
} }
+25 -3
View File
@@ -108,7 +108,7 @@ import { battleRunner } from '../battle/runner.js';
import { isClientCombat, observeTarget, teammateProgress, cameraLayers, layerCamera, sidesOf, resumedWatch } from '../battle/observe.js'; import { isClientCombat, observeTarget, teammateProgress, cameraLayers, layerCamera, sidesOf, resumedWatch } from '../battle/observe.js';
import { screenStrip, playerBonds, playerLayer, detailBondOwner, toggleBond, popupView } from '../ui/watchBonds.js'; import { screenStrip, playerBonds, playerLayer, detailBondOwner, toggleBond, popupView } from '../ui/watchBonds.js';
import { data, localAsset, getMode } from '../data.js'; import { data, localAsset, getMode } from '../data.js';
import { audio } from '../audio.js'; import { audio, resultSpeaker, resultVoiceSlot } from '../audio.js';
import { useDocClass, FullscreenButton } from '../ui/device.js'; import { useDocClass, FullscreenButton } from '../ui/device.js';
const cx = (...p) => p.flat().filter(Boolean).join(' '); const cx = (...p) => p.flat().filter(Boolean).join(' ');
@@ -493,8 +493,30 @@ function MatchScreen() {
view?.pushEvents(msg); view?.pushEvents(msg);
audio.handleBattleEvents(msg.ev); audio.handleBattleEvents(msg.ev);
}; };
// 干员语音 (结算): the own battle's result just came in — the operator's line depends on how it went
// (完美作战 ⇒ 3星结束行动, 绝境 / 终极 ⇒ 完成高难行动, 有漏怪 ⇒ 非3星结束行动, 一个没杀 ⇒ 行动失败)
const onResult = (msg) => {
try {
if (!msg || !msg.result) return;
const st = store.get();
const pid = st.me?.playerId;
const mine = (pid && msg.result.perPlayer && msg.result.perPlayer[pid]) || null;
const diff = st.match?.public?.difficulty;
// the speaker comes from THIS battle's own field (`mine.unitsEnd`), not from the field on screen: watching a
// teammate used to make THEIR operator say the viewer's line (review on #73)
const charId = resultSpeaker(mine);
if (!charId) return;
audio.voice(charId, resultVoiceSlot({
perfect: !!(mine?.perfect),
leaked: Array.isArray(mine?.leaked) ? mine.leaked.length : 0,
killed: mine?.killed ?? msg.result.killed,
total: mine?.total ?? msg.result.total,
hard: diff === 'HARD' || diff === 'ABYSS',
}));
} catch { /* ignore */ }
};
const offs = [net.on('m.field', onFieldMeta), net.on('b.snap', onSnap), net.on('b.ev', onEv)]; const offs = [net.on('m.field', onFieldMeta), net.on('b.snap', onSnap), net.on('b.ev', onEv)];
if (battleRunner) offs.push(battleRunner.on('field', onFieldMeta), battleRunner.on('snap', onSnap), battleRunner.on('ev', onEv)); if (battleRunner) offs.push(battleRunner.on('field', onFieldMeta), battleRunner.on('snap', onSnap), battleRunner.on('ev', onEv), battleRunner.on('result', onResult));
return () => { for (const off of offs) { try { off(); } catch { /* ignore */ } } clearTimeout(pending); }; return () => { for (const off of offs) { try { off(); } catch { /* ignore */ } } clearTimeout(pending); };
}, [view]); }, [view]);
@@ -1311,7 +1333,7 @@ function MatchScreen() {
onClose=${() => setBondOpen(null)} onMember=${(id, items) => setDetail({ kind: 'chess', id, owner: bondPop.ownerId, items: items || null })} />` : null} onClose=${() => setBondOpen(null)} onMember=${(id, items) => setDetail({ kind: 'chess', id, owner: bondPop.ownerId, items: items || null })} />` : null}
${resolved ? html`<${DetailPanel} detail=${resolved} snapHp=${snapHp} onClose=${() => { setDetail(null); setSel(null); }} ${resolved ? html`<${DetailPanel} detail=${resolved} snapHp=${snapHp} onClose=${() => { setDetail(null); setSel(null); }}
bonds=${detailBonds} offBonds=${offBonds} loadout=${detailLoadout} side=${dSide} shopOpen=${shopOpen} live=${liveStats} bonds=${detailBonds} offBonds=${offBonds} loadout=${detailLoadout} side=${dSide} shopOpen=${shopOpen} live=${liveStats} voice=${combat}
onBond=${(id) => openBond(id, detailOwner, 'detail')} />` : null} onBond=${(id) => openBond(id, detailOwner, 'detail')} />` : null}
${selEntry && editable && !facing && !drag && showPrep ? html`<${Underframe} key=${sel.uid} view=${view} uid=${sel.uid} ${selEntry && editable && !facing && !drag && showPrep ? html`<${Underframe} key=${sel.uid} view=${view} uid=${sel.uid}
+1
View File
@@ -66,6 +66,7 @@ export const ICONS = {
info: { d: 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm-1 8v7h2v-7zm0-3v2h2V7z', eo: true }, info: { d: 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm-1 8v7h2v-7zm0-3v2h2V7z', eo: true },
warn: { d: 'M12 2 1 21h22zm-1 7v6h2V9zm0 7.5v2h2v-2z', eo: true }, warn: { d: 'M12 2 1 21h22zm-1 7v6h2V9zm0 7.5v2h2v-2z', eo: true },
play: { d: 'M7 4v16l13-8z' }, play: { d: 'M7 4v16l13-8z' },
mic: { d: 'M12 14a3 3 0 0 0 3-3V5a3 3 0 0 0-6 0v6a3 3 0 0 0 3 3zm5-3a5 5 0 0 1-10 0H5a7 7 0 0 0 6 6.9V21H8v2h8v-2h-3v-3.1A7 7 0 0 0 19 11z' },
edit: { d: 'M3 17.2V21h3.8l11-11-3.8-3.8zM20.7 7.1a1 1 0 0 0 0-1.4l-2.4-2.4a1 1 0 0 0-1.4 0l-1.8 1.8 3.8 3.8z' }, edit: { d: 'M3 17.2V21h3.8l11-11-3.8-3.8zM20.7 7.1a1 1 0 0 0 0-1.4l-2.4-2.4a1 1 0 0 0-1.4 0l-1.8 1.8 3.8 3.8z' },
sword: { d: 'M20 3h1v4L10.4 17.6l1.4 1.4-1.4 1.4-2.1-2.1-3.5 3.5-1.4-1.4 3.5-3.5L4.8 14.8l1.4-1.4 1.4 1.4L18 4z' }, sword: { d: 'M20 3h1v4L10.4 17.6l1.4 1.4-1.4 1.4-2.1-2.1-3.5 3.5-1.4-1.4 3.5-3.5L4.8 14.8l1.4-1.4 1.4 1.4L18 4z' },
shield: { d: 'M12 2 4 5v6c0 5 3.4 9.3 8 11 4.6-1.7 8-6 8-11V5z' }, shield: { d: 'M12 2 4 5v6c0 5 3.4 9.3 8 11 4.6-1.7 8-6 8-11V5z' },
+12 -1
View File
@@ -31,6 +31,7 @@
// The stats block (chessStatsBlock), the 特性 text (traitText) and the talent list (chessTalents) are exported: the 干员调配 // The stats block (chessStatsBlock), the 特性 text (traitText) and the talent list (chessTalents) are exported: the 干员调配
// screen's 局内数值 section draws the same ones for the chosen skill / module, without a live entry (GitHub issue #64). // screen's 局内数值 section draws the same ones for the chosen skill / module, without a live entry (GitHub issue #64).
import { useEffect } from '../../vendor/hooks.module.js';
import { html, Icon, TierChip, MicroLabel, Button, confirmDialog, useTicker } from './components.js'; import { html, Icon, TierChip, MicroLabel, Button, confirmDialog, useTicker } from './components.js';
import { Img, RichText, UnitThumb, BondGlyph, GIcon } from './gameComponents.js'; import { Img, RichText, UnitThumb, BondGlyph, GIcon } from './gameComponents.js';
import { attackInterval, rangeGridBox, fmtNum, tileKey, chessLoadout, nextThreshold, bondTier, briefingBondTip, pieceBondIds, grantedBonds, morphPairings } from './gameLogic.js'; import { attackInterval, rangeGridBox, fmtNum, tileKey, chessLoadout, nextThreshold, bondTier, briefingBondTip, pieceBondIds, grantedBonds, morphPairings } from './gameLogic.js';
@@ -40,6 +41,7 @@ import { data } from '../data.js';
import { attackRangeGrid } from '../../../shared/loadoutRecord.js'; import { attackRangeGrid } from '../../../shared/loadoutRecord.js';
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js'; import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
import { moduleBadge } from './loadoutModel.js'; import { moduleBadge } from './loadoutModel.js';
import { audio } from '../audio.js';
const cx = (...p) => p.flat().filter(Boolean).join(' '); const cx = (...p) => p.flat().filter(Boolean).join(' ');
@@ -651,10 +653,19 @@ export function resolveDetail(target, pieces) {
* = the defaults * = the defaults
* live: the unit's live stats (unitStatsEntry + src 'battle' | 'prep') — an object, or a getter the panel re-reads 4× * live: the unit's live stats (unitStatsEntry + src 'battle' | 'prep') — an object, or a getter the panel re-reads 4×
* a second (the battle's own sim, battle/runner.js unitStats); null ⇒ the record's numbers * a second (the battle's own sim, battle/runner.js unitStats); null ⇒ the record's numbers
* voice: whether the panel may speak — 选中干员 (audio.voice 'select') plays only while a battle runs (user request:
* 整备期不播干员语音), so the game screen passes its combat flag
*/ */
export function DetailPanel({ detail, editable, snapHp, onClose, onSell, onDestroy, bonds = [], offBonds = null, loadout = null, onBond = null, side = 'left', shopOpen = false, live = null }) { export function DetailPanel({ detail, editable, snapHp, onClose, onSell, onDestroy, bonds = [], offBonds = null, loadout = null, onBond = null, side = 'left', shopOpen = false, live = null, voice = false }) {
const getter = typeof live === 'function' ? live : null; const getter = typeof live === 'function' ? live : null;
useTicker(detail && getter ? 250 : 0); useTicker(detail && getter ? 250 : 0);
// 选中干员 voice (audio.voice 'select'): once per opened operator — the panel stays mounted while the target changes,
// so the key carries what identifies it (its chess record and its piece / battle unit id)
const selectKey = voice && detail?.type === 'chess' ? `${detail.chess?.chessId || ''}:${detail.unitId ?? detail.piece?.uid ?? ''}` : null;
const selectChar = voice && detail?.type === 'chess' ? detail.chess?.charId || null : null;
useEffect(() => {
if (selectKey && selectChar) audio.voice(selectChar, 'select');
}, [selectKey, selectChar]);
if (!detail) return null; if (!detail) return null;
let liveNow = null; let liveNow = null;
try { liveNow = getter ? getter() : live && typeof live === 'object' ? live : null; } catch { liveNow = null; } try { liveNow = getter ? getter() : live && typeof live === 'object' ? live : null; } catch { liveNow = null; }
+3 -2
View File
@@ -1580,13 +1580,13 @@ export function shortcutBlocked(act, { modal = false, drawer = false } = {}) {
// ---- settings ------------------------------------------------------------------------------------------------------ // ---- settings ------------------------------------------------------------------------------------------------------
export const DEFAULT_SETTINGS = Object.freeze({ bgm: 0.6, sfx: 0.8, muted: false, damageNumbers: true, quality: 'high' }); export const DEFAULT_SETTINGS = Object.freeze({ bgm: 0.6, sfx: 0.8, voice: 0.8, muted: false, damageNumbers: true, quality: 'high' });
const QUALITIES = ['high', 'medium', 'low']; const QUALITIES = ['high', 'medium', 'low'];
/** /**
* Sanitize persisted settings. * Sanitize persisted settings.
* @param {any} raw * @param {any} raw
* @returns {{ bgm: number, sfx: number, muted: boolean, damageNumbers: boolean, quality: 'high'|'medium'|'low' }} * @returns {{ bgm: number, sfx: number, voice: number, muted: boolean, damageNumbers: boolean, quality: 'high'|'medium'|'low' }}
*/ */
export function sanitizeSettings(raw) { export function sanitizeSettings(raw) {
const r = isObj(raw) ? raw : {}; const r = isObj(raw) ? raw : {};
@@ -1594,6 +1594,7 @@ export function sanitizeSettings(raw) {
return { return {
bgm: vol(r.bgm, DEFAULT_SETTINGS.bgm), bgm: vol(r.bgm, DEFAULT_SETTINGS.bgm),
sfx: vol(r.sfx, DEFAULT_SETTINGS.sfx), sfx: vol(r.sfx, DEFAULT_SETTINGS.sfx),
voice: vol(r.voice, DEFAULT_SETTINGS.voice),
muted: typeof r.muted === 'boolean' ? r.muted : DEFAULT_SETTINGS.muted, muted: typeof r.muted === 'boolean' ? r.muted : DEFAULT_SETTINGS.muted,
damageNumbers: typeof r.damageNumbers === 'boolean' ? r.damageNumbers : DEFAULT_SETTINGS.damageNumbers, damageNumbers: typeof r.damageNumbers === 'boolean' ? r.damageNumbers : DEFAULT_SETTINGS.damageNumbers,
quality: QUALITIES.includes(r.quality) ? r.quality : DEFAULT_SETTINGS.quality, quality: QUALITIES.includes(r.quality) ? r.quality : DEFAULT_SETTINGS.quality,
+3 -2
View File
@@ -1,4 +1,4 @@
// Player settings (BGM/SFX volume, mute, damage numbers, render quality): a tiny observable store // Player settings (BGM/SFX/voice volume, mute, damage numbers, render quality): a tiny observable store
// persisted in localStorage (`sp.pref.settings`), applied to the audio manager on every change, plus // persisted in localStorage (`sp.pref.settings`), applied to the audio manager on every change, plus
// the settings modal. // the settings modal.
@@ -10,7 +10,7 @@ import { audio } from '../audio.js';
import { openGuide } from './guide.js'; import { openGuide } from './guide.js';
import { detectFeatures } from './device.js'; import { detectFeatures } from './device.js';
/** Settings store: { bgm, sfx, muted, damageNumbers, quality }. */ /** Settings store: { bgm, sfx, voice, muted, damageNumbers, quality }. */
export const settingsStore = createStore(sanitizeSettings(loadPref('settings', null))); export const settingsStore = createStore(sanitizeSettings(loadPref('settings', null)));
settingsStore.subscribe((s) => { settingsStore.subscribe((s) => {
@@ -60,6 +60,7 @@ export function SettingsModal({ open, onClose }) {
<${Button} variant="primary" icon="check" onClick=${onClose}>完成<//>`}> <${Button} variant="primary" icon="check" onClick=${onClose}>完成<//>`}>
<div class="set-list"> <div class="set-list">
<${Slider} label="背景音乐" micro="BGM" icon="play" value=${s.bgm} onInput=${(v) => updateSettings({ bgm: v })} /> <${Slider} label="背景音乐" micro="BGM" icon="play" value=${s.bgm} onInput=${(v) => updateSettings({ bgm: v })} />
<${Slider} label="干员语音" micro="VOICE" icon="mic" value=${s.voice} onInput=${(v) => updateSettings({ voice: v })} />
<${Slider} label="音效" micro="SFX" icon="signal" value=${s.sfx} <${Slider} label="音效" micro="SFX" icon="signal" value=${s.sfx}
onInput=${(v) => { updateSettings({ sfx: v }); if (!tested) { setTested(true); setTimeout(() => setTested(false), 400); audio.sfx('click'); } }} /> onInput=${(v) => { updateSettings({ sfx: v }); if (!tested) { setTested(true); setTimeout(() => setTested(false), 400); audio.sfx('click'); } }} />
<${Toggle} label="静音" micro="MUTE" value=${s.muted} onChange=${(v) => updateSettings({ muted: v })} /> <${Toggle} label="静音" micro="MUTE" value=${s.muted} onChange=${(v) => updateSettings({ muted: v })} />
+6
View File
@@ -155,6 +155,12 @@ export function performAttack(b, u, prof, targets, opts = null) {
u.stats.attacks++; u.stats.attacks++;
const attackId = ++b._attackSeq; // every damage instance of this attack (all targets, splash, chain) carries it const attackId = ++b._attackSeq; // every damage instance of this attack (all targets, splash, chain) carries it
const isHeal = !!(prof.heal && prof.dmgType === 'heal'); const isHeal = !!(prof.heal && prof.dmgType === 'heal');
// 首次接敌 (official voice type ENCOUNTER_ENEMY, ≥ 3 s between two such lines): one event the first time a unit
// attacks an enemy, whatever the attack is — the client answers with that operator's 行动开始 line (audio.js voice).
if (!isHeal && u.side === 'ally' && !u.mem.engaged && targets.some((t) => t && t.side === 'enemy')) {
u.mem.engaged = true;
b._ev(['engage', u.id]);
}
const ranged = !prof._fortressMelee && prof.attack === 'ranged' && prof.projectile && prof.projectile !== 'none' && prof.projectile !== 'beam'; const ranged = !prof._fortressMelee && prof.attack === 'ranged' && prof.projectile && prof.projectile !== 'none' && prof.projectile !== 'beam';
const vis = prof._fortressMelee ? 'none' : (prof.projectile || 'none'); const vis = prof._fortressMelee ? 'none' : (prof.projectile || 'none');
for (let i = 0; i < targets.length; i++) { for (let i = 0; i < targets.length; i++) {
+1 -1
View File
@@ -341,7 +341,7 @@ export function validateC2S(msg) {
// Event tuple kinds inside `b.ev` (DESIGN §8.2). // Event tuple kinds inside `b.ev` (DESIGN §8.2).
export const EV = Object.freeze({ export const EV = Object.freeze({
SPAWN: 'spawn', ATK: 'atk', DMG: 'dmg', HEAL: 'heal', SKILL: 'skill', DIE: 'die', LEAK: 'leak', SPAWN: 'spawn', ATK: 'atk', DMG: 'dmg', HEAL: 'heal', SKILL: 'skill', ENGAGE: 'engage', DIE: 'die', LEAK: 'leak',
STATUS: 'status', FX: 'fx', LAYER: 'layer', BOUNTY: 'bounty', DEPLOY: 'deploy', STATUS: 'status', FX: 'fx', LAYER: 'layer', BOUNTY: 'bounty', DEPLOY: 'deploy',
}); });
+57 -2
View File
@@ -19,8 +19,9 @@
// feedback after 0.1.0 (DESIGN §21, v0.1.1) including batch 6 (§21.21–§21.25: 坚固维式重锤 once per deployment, 起飞, // feedback after 0.1.0 (DESIGN §21, v0.1.1) including batch 6 (§21.21–§21.25: 坚固维式重锤 once per deployment, 起飞,
// fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7) and // fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7) and
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2), // the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2),
// the closing additions §21.26–§21.28 (GitHub issues #1 / #5 / #8) and the owner's deliberate trigger deviation for six // the closing additions §21.26–§21.28 (GitHub issues #1 / #5 / #8), the owner's deliberate trigger deviation for six
// 重装 skills (§21.29, GitHub issue #4 / PR #12). // 重装 skills (§21.29, GitHub issue #4 / PR #12) and the operator battle voice the user asked for the same day (§21.30,
// battle only — the 休整期 is silent).
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
@@ -845,3 +846,57 @@ test('the deliberate trigger deviation (DESIGN §21.29): six 重装 skills DEFAU
assert.match(PLAYING, /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放/); assert.match(PLAYING, /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放/);
assert.match(doc('CHANGELOG.md'), /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能改为攻击范围内有敌人时就释放/); assert.match(doc('CHANGELOG.md'), /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能改为攻击范围内有敌人时就释放/);
}); });
test('干员战斗语音 (DESIGN §21.30): the manifest data, the official priorities, and the 休整期 stays silent', async () => {
const { VOICE_PRIORITY, VOICE_COOLDOWN_MS, resultVoiceSlot } = await import('../public/js/audio.js');
const manifest = JSON.parse(readFileSync(join(ROOT, 'data/assets.json'), 'utf8'));
const voice = manifest.audio?.voice ?? {};
const charIds = Object.keys(voice);
assert.ok(charIds.length >= 100, `${charIds.length} operators carry official battle voice`);
assert.equal(manifest.stats.voiceChars, charIds.length, 'stats.voiceChars counts them');
// one operator carries every slot a battle can play — and none of the prep-only ones (plan.mjs VOICE_BATTLE_SLOTS):
// 干员报到 / 编入队伍 / 任命队长 are never requested by the client, so planning them only made every `npm run assets`
// download 360 files (19.3 MB, one per operator and slot) nobody hears. `--voice-all` brings the complete set back.
const slots = ['start', 'faceEnemy', 'select', 'place', 'skill1', 'skill2', 'skill3', 'skill4', 'resultFour', 'resultThree', 'resultTwo', 'resultLose'];
const prepOnly = ['gacha', 'squad', 'squadFirst'];
const lines = [];
const walk = (x) => {
if (typeof x === 'string') lines.push(x);
else if (Array.isArray(x)) x.forEach(walk);
else if (x && typeof x === 'object') Object.values(x).forEach(walk);
};
for (const id of charIds) {
for (const s of slots) assert.ok(voice[id][s], `${id}.${s}`);
for (const s of prepOnly) assert.equal(voice[id][s], undefined, `${id}.${s} is not planned by default`);
walk(voice[id]);
}
// a battle slot usually carries several lines (选中干员 / 部署 have two), so the battle set alone stays well above 10 each
assert.ok(lines.length >= charIds.length * 10, `${lines.length} voice lines for ${charIds.length} operators`);
for (const u of lines) assert.match(u, /^\/assets\/audio\/voice\/cn\/char_[^/]+\/cn_\d+\.mp3$/);
// the official scheduling numbers (audio_data.json battleVoice.voiceTypeOptions)
assert.equal(VOICE_PRIORITY.start, 100);
assert.equal(VOICE_PRIORITY.faceEnemy, 90);
assert.equal(VOICE_PRIORITY.skill1, 70);
assert.equal(VOICE_PRIORITY.place, 20);
assert.equal(VOICE_PRIORITY.select, 10);
assert.equal(VOICE_COOLDOWN_MS.skill1, 10000);
assert.equal(VOICE_COOLDOWN_MS.faceEnemy, 3000);
assert.equal(VOICE_COOLDOWN_MS.select, 1500);
assert.equal(resultVoiceSlot({ perfect: true }), 'resultThree');
// the docs
assert.match(DESIGN, /### 21\.30 /);
assert.match(DESIGN, /\*\*Where each line plays — battle only\.\*\*/);
assert.match(doc('docs/ASSETS.md'), /\| 干员战斗语音 \|/);
assert.match(doc('docs/ASSETS.md'), /voiceChars/);
assert.match(SIM, /\['engage', id\]/);
// the code: every slot the client asks for comes from a running battle's own stream — the three prep-only lines
// (干员报到 / 编入队伍 / 任命队长) are never requested, and 选中干员 sits behind the panel's combat flag
const panel = doc('public/js/ui/detailPanel.js');
const game = doc('public/js/screens/game.js');
for (const [name, src] of [['audio.js', doc('public/js/audio.js')], ['game.js', game], ['detailPanel.js', panel]]) {
assert.ok(!/voice\([^)]*'(gacha|squad|squadFirst)'/.test(src), `${name}: no prep slot is played`);
}
assert.match(panel, /const selectKey = voice && detail\?\.type === 'chess'/);
assert.match(game, /voice=\$\{combat\}/);
assert.match(game, /audio\.voice\(charId, resultVoiceSlot\(/);
});
+4
View File
@@ -68,6 +68,10 @@ test('fetch-assets shrink guard: the committed manifest minus some audio entries
assert.deepEqual(shrinkGuard(null, next, parseArgs([])), { dropped: [], write: true }, 'no current manifest: written'); assert.deepEqual(shrinkGuard(null, next, parseArgs([])), { dropped: [], write: true }, 'no current manifest: written');
assert.equal(parseArgs(['--allow-shrink']).allowShrink, true); assert.equal(parseArgs(['--allow-shrink']).allowShrink, true);
assert.throws(() => parseArgs(['--allow-shrinks']), /unknown option/); assert.throws(() => parseArgs(['--allow-shrinks']), /unknown option/);
// 干员战斗语音: the prep-only slots are opt-in (DESIGN §21.30 — no battle requests them)
assert.equal(parseArgs([]).voiceAll, false, 'the battle slots are what a plain run plans');
assert.equal(parseArgs(['--voice-all']).voiceAll, true);
assert.equal(parseArgs(['--voice-lang=jp']).voiceLang, 'jp');
assert.equal(statSync(MANIFEST).mtimeMs, before, 'importing the tool runs nothing'); assert.equal(statSync(MANIFEST).mtimeMs, before, 'importing the tool runs nothing');
}); });
+46 -46
View File
@@ -42,7 +42,7 @@
], ],
"enemies": {"enemy_1019_jshoot":[5,5,0,513,10368],"enemy_1043_zomsbr":[18,18,0,351,54677],"enemy_1299_ymkilr":[1,1,0,2251,9792]}, "enemies": {"enemy_1019_jshoot":[5,5,0,513,10368],"enemy_1043_zomsbr":[18,18,0,351,54677],"enemy_1299_ymkilr":[1,1,0,2251,9792]},
"hooks": [1,35,1535,152,152,200,200,1064,24,24,3,1,0,6482,0,19,24,0,1,24,0,0,0], "hooks": [1,35,1535,152,152,200,200,1064,24,24,3,1,0,6482,0,19,24,0,1,24,0,0,0],
"events": {"atk":152,"deploy":8,"die":24,"dmg":200,"fx":4,"skill":6,"spawn":35,"status":1}, "events": {"atk":152,"deploy":8,"die":24,"dmg":200,"engage":5,"fx":4,"skill":6,"spawn":35,"status":1},
"fx": {"buff":1,"coin":3}, "fx": {"buff":1,"coin":3},
"snaps": "6a513536 c150215b 81084fd8 9870355a f707b691 df63fdcd" "snaps": "6a513536 c150215b 81084fd8 9870355a f707b691 df63fdcd"
}, },
@@ -80,7 +80,7 @@
], ],
"enemies": {"enemy_1381_winman":[9,9,0,0,129890],"enemy_1381_winman_2":[5,5,0,0,97044],"enemy_1387_winshd":[4,4,0,0,119439]}, "enemies": {"enemy_1381_winman":[9,9,0,0,129890],"enemy_1381_winman_2":[5,5,0,0,97044],"enemy_1387_winshd":[4,4,0,0,119439]},
"hooks": [1,29,2013,67,67,100,100,0,18,18,5,3,0,2605,44,0,18,0,1,18,0,0,0], "hooks": [1,29,2013,67,67,100,100,0,18,18,5,3,0,2605,44,0,18,0,1,18,0,0,0],
"events": {"atk":79,"deploy":8,"die":18,"dmg":100,"fx":4,"skill":8,"spawn":29,"status":29}, "events": {"atk":79,"deploy":8,"die":18,"dmg":100,"engage":4,"fx":4,"skill":8,"spawn":29,"status":29},
"fx": {"crit":2,"dilemma":1,"shield":1}, "fx": {"crit":2,"dilemma":1,"shield":1},
"snaps": "db18b5ac a7e81526 897c5e72 065cee09 e78d2988 e1d0e7f9 cab65b67" "snaps": "db18b5ac a7e81526 897c5e72 065cee09 e78d2988 e1d0e7f9 cab65b67"
}, },
@@ -117,7 +117,7 @@
], ],
"enemies": {"enemy_1249_lysdb_2":[5,3,2,346,24167],"enemy_1251_lysyta":[15,13,2,48,35396],"enemy_1328_cbjedi":[1,1,0,1940,9000]}, "enemies": {"enemy_1249_lysdb_2":[5,3,2,346,24167],"enemy_1251_lysyta":[15,13,2,48,35396],"enemy_1328_cbjedi":[1,1,0,1940,9000]},
"hooks": [1,31,1739,175,175,223,218,4,18,22,15,15,0,9423,15,9,21,4,1,18,0,0,0], "hooks": [1,31,1739,175,175,223,218,4,18,22,15,15,0,9423,15,9,21,4,1,18,0,0,0],
"events": {"atk":192,"deploy":8,"die":18,"dmg":218,"fx":36,"leak":4,"skill":30,"spawn":31,"status":25}, "events": {"atk":192,"deploy":8,"die":18,"dmg":218,"engage":8,"fx":36,"leak":4,"skill":30,"spawn":31,"status":25},
"fx": {"aoe":1,"displace":11,"pull":11,"shieldBreak":5,"strike":3,"taunt":1,"volley":3,"zone":1}, "fx": {"aoe":1,"displace":11,"pull":11,"shieldBreak":5,"strike":3,"taunt":1,"volley":3,"zone":1},
"snaps": "b677776a 05df3ce3 3f4ea1ef 7c86ba28 c343548f 5e5789b8" "snaps": "b677776a 05df3ce3 3f4ea1ef 7c86ba28 c343548f 5e5789b8"
}, },
@@ -157,7 +157,7 @@
], ],
"enemies": {"enemy_1005_yokai_2":[4,4,0,96,5357],"enemy_1005_yokai_3":[2,2,0,862,69120],"enemy_9009_acfort":[2,2,0,685,55296]}, "enemies": {"enemy_1005_yokai_2":[4,4,0,96,5357],"enemy_1005_yokai_3":[2,2,0,862,69120],"enemy_9009_acfort":[2,2,0,685,55296]},
"hooks": [1,21,2948,217,217,357,357,231,8,8,13,11,0,11234,107,0,8,0,1,7,0,0,0], "hooks": [1,21,2948,217,217,357,357,231,8,8,13,11,0,11234,107,0,8,0,1,7,0,0,0],
"events": {"atk":217,"deploy":10,"die":8,"dmg":357,"fx":36,"heal":189,"skill":24,"spawn":21,"status":80}, "events": {"atk":217,"deploy":10,"die":8,"dmg":357,"engage":5,"fx":36,"heal":189,"skill":24,"spawn":21,"status":80},
"fx": {"beam":2,"counter":16,"pull":1,"slippery":2,"taunt":4,"tornado":1,"tornadoPulse":5,"zone":5}, "fx": {"beam":2,"counter":16,"pull":1,"slippery":2,"taunt":4,"tornado":1,"tornadoPulse":5,"zone":5},
"snaps": "f66ffc26 0aedc293 48dbf768 e46fb28f af06619a 9bc3f9d6 ea76c807 fc688b81 22b52f32 978323eb" "snaps": "f66ffc26 0aedc293 48dbf768 e46fb28f af06619a 9bc3f9d6 ea76c807 fc688b81 22b52f32 978323eb"
}, },
@@ -207,7 +207,7 @@
], ],
"enemies": {"enemy_1166_dusbr":[15,15,0,761,51840],"enemy_1169_duphlx":[5,5,0,659,23040],"enemy_1170_dushld":[2,2,0,3376,16128]}, "enemies": {"enemy_1166_dusbr":[15,15,0,761,51840],"enemy_1169_duphlx":[5,5,0,659,23040],"enemy_1170_dushld":[2,2,0,3376,16128]},
"hooks": [1,32,2870,344,344,593,594,0,23,23,10,7,0,10052,41,13,22,0,1,24,7,0,0], "hooks": [1,32,2870,344,344,593,594,0,23,23,10,7,0,10052,41,13,22,0,1,24,7,0,0],
"events": {"atk":395,"deploy":10,"die":23,"dmg":594,"fx":11,"layer":7,"skill":17,"spawn":31,"status":46}, "events": {"atk":395,"deploy":10,"die":23,"dmg":594,"engage":6,"fx":11,"layer":7,"skill":17,"spawn":31,"status":46},
"fx": {"devour":1,"dp":1,"garrison":3,"volley":3,"wolfShadow":2,"wolfShadowLost":1}, "fx": {"devour":1,"dp":1,"garrison":3,"volley":3,"wolfShadow":2,"wolfShadowLost":1},
"snaps": "d0724fb0 dc731840 afc7b763 32af0154 58ec6e8c ad015606 93516fa3 8d4b0184 6309839b fcfc5b4e" "snaps": "d0724fb0 dc731840 afc7b763 32af0154 58ec6e8c ad015606 93516fa3 8d4b0184 6309839b fcfc5b4e"
}, },
@@ -244,7 +244,7 @@
], ],
"enemies": {"enemy_1229_darmy":[10,10,0,4638,104509],"enemy_1229_darmy_2":[7,7,0,18088,95800],"enemy_9007_acelem":[2,2,0,4609,179159]}, "enemies": {"enemy_1229_darmy":[10,10,0,4638,104509],"enemy_1229_darmy_2":[7,7,0,18088,95800],"enemy_9007_acelem":[2,2,0,4609,179159]},
"hooks": [1,29,2221,148,148,256,276,251,20,21,21,20,0,4154,485,13,19,0,1,20,5,3,0], "hooks": [1,29,2221,148,148,256,276,251,20,21,21,20,0,4154,485,13,19,0,1,20,5,3,0],
"events": {"atk":167,"deploy":10,"die":21,"dmg":276,"fx":34,"layer":5,"skill":41,"spawn":27,"status":132}, "events": {"atk":167,"deploy":10,"die":21,"dmg":276,"engage":4,"fx":34,"layer":5,"skill":41,"spawn":27,"status":132},
"fx": {"beam":2,"blink":1,"burst":3,"devour":4,"dilemma":2,"frostNova":15,"garrison":5,"swordStorm":2}, "fx": {"beam":2,"blink":1,"burst":3,"devour":4,"dilemma":2,"frostNova":15,"garrison":5,"swordStorm":2},
"snaps": "066fec91 aec41fca b75071e0 478fe370 a68e442a 1b0b0907 cc527129 08c4150f" "snaps": "066fec91 aec41fca b75071e0 478fe370 a68e442a 1b0b0907 cc527129 08c4150f"
}, },
@@ -279,7 +279,7 @@
], ],
"enemies": {"enemy_10042_prtrop":[4,4,0,2248,21600],"enemy_10042_prtrop_2":[2,2,0,0,14400],"enemy_10043_sailor":[11,11,0,556,54450]}, "enemies": {"enemy_10042_prtrop":[4,4,0,2248,21600],"enemy_10042_prtrop_2":[2,2,0,0,14400],"enemy_10043_sailor":[11,11,0,556,54450]},
"hooks": [1,25,1676,144,144,252,252,34,18,18,5,3,0,6015,8,7,17,0,1,18,0,0,0], "hooks": [1,25,1676,144,144,252,252,34,18,18,5,3,0,6015,8,7,17,0,1,18,0,0,0],
"events": {"atk":182,"deploy":8,"die":18,"dmg":251,"fx":26,"heal":34,"skill":8,"spawn":25,"status":39}, "events": {"atk":182,"deploy":8,"die":18,"dmg":251,"engage":6,"fx":26,"heal":34,"skill":8,"spawn":25,"status":39},
"fx": {"crit":6,"displace":8,"ripple":6,"shadowWeave":2,"shield":2,"shieldBreak":1,"stealth":1}, "fx": {"crit":6,"displace":8,"ripple":6,"shadowWeave":2,"shield":2,"shieldBreak":1,"stealth":1},
"snaps": "190730ef 965c573a 3f2abe1e 5c577114 4a917fdf 5586f99d" "snaps": "190730ef 965c573a 3f2abe1e 5c577114 4a917fdf 5586f99d"
}, },
@@ -316,7 +316,7 @@
], ],
"enemies": {"enemy_1045_hammer_2":[2,0,2,7798,28315],"enemy_1061_zomshd":[11,11,0,983,179484]}, "enemies": {"enemy_1045_hammer_2":[2,0,2,7798,28315],"enemy_1061_zomshd":[11,11,0,983,179484]},
"hooks": [1,23,3551,281,281,569,560,7994,12,14,14,13,0,5016,14,31,13,2,1,14,0,0,2], "hooks": [1,23,3551,281,281,569,560,7994,12,14,14,13,0,5016,14,31,13,2,1,14,0,0,2],
"events": {"atk":376,"deploy":8,"die":12,"dmg":552,"fx":86,"heal":58,"leak":2,"skill":29,"spawn":23,"status":38}, "events": {"atk":376,"deploy":8,"die":12,"dmg":552,"engage":6,"fx":86,"heal":58,"leak":2,"skill":29,"spawn":23,"status":38},
"fx": {"block":7,"dodge":2,"flame":1,"garrisonGrant":1,"rockslide":6,"shieldBreak":8,"slash":26,"thorns":33,"truesilver":2}, "fx": {"block":7,"dodge":2,"flame":1,"garrisonGrant":1,"rockslide":6,"shieldBreak":8,"slash":26,"thorns":33,"truesilver":2},
"snaps": "48d2a0bc 7a5c72a3 804d5560 8c6ca634 c9bf651b 26081476 486ac1ff 38cf6fca 1c52bb36 3480567c b20bd7bb 65dfc07e" "snaps": "48d2a0bc 7a5c72a3 804d5560 8c6ca634 c9bf651b 26081476 486ac1ff 38cf6fca 1c52bb36 3480567c b20bd7bb 65dfc07e"
}, },
@@ -358,7 +358,7 @@
], ],
"enemies": {"enemy_1158_divman":[15,15,0,2085,57024],"enemy_1160_hvyslr":[2,2,0,4543,23040],"enemy_1161_tidmag":[1,1,0,2691,6912]}, "enemies": {"enemy_1158_divman":[15,15,0,2085,57024],"enemy_1160_hvyslr":[2,2,0,4543,23040],"enemy_1161_tidmag":[1,1,0,2691,6912]},
"hooks": [1,32,1887,159,159,249,257,1001,21,21,11,11,0,5499,102,7,18,0,1,21,1,1,3], "hooks": [1,32,1887,159,159,249,257,1001,21,21,11,11,0,5499,102,7,18,0,1,21,1,1,3],
"events": {"atk":207,"deploy":11,"die":21,"dmg":257,"fx":25,"layer":1,"skill":22,"spawn":31,"status":80}, "events": {"atk":207,"deploy":11,"die":21,"dmg":257,"engage":6,"fx":25,"layer":1,"skill":22,"spawn":31,"status":80},
"fx": {"burst":1,"dilemma":1,"dodge":3,"explosion":4,"frostNova":2,"garrison":1,"levitate":7,"overclock":1,"shield":2,"slippery":1,"splash":2}, "fx": {"burst":1,"dilemma":1,"dodge":3,"explosion":4,"frostNova":2,"garrison":1,"levitate":7,"overclock":1,"shield":2,"slippery":1,"splash":2},
"snaps": "910f43b9 691d4675 c5aa756a 98b2fa4d ce14416b c2b3dc9f f49377cb" "snaps": "910f43b9 691d4675 c5aa756a 98b2fa4d ce14416b c2b3dc9f f49377cb"
}, },
@@ -393,7 +393,7 @@
], ],
"enemies": {"enemy_1166_dusbr_2":[14,11,3,27453,136496],"enemy_1170_dushld_2":[4,1,3,36563,52797],"enemy_9011_acrefr":[4,1,2,57576,114879]}, "enemies": {"enemy_1166_dusbr_2":[14,11,3,27453,136496],"enemy_1170_dushld_2":[4,1,3,36563,52797],"enemy_9011_acrefr":[4,1,2,57576,114879]},
"hooks": [1,52,5700,834,834,1143,1124,403,35,43,29,28,102,4003,202,60,22,8,1,35,0,0,19], "hooks": [1,52,5700,834,834,1143,1124,403,35,43,29,28,102,4003,202,60,22,8,1,35,0,0,19],
"events": {"atk":1176,"deploy":30,"die":35,"dmg":1121,"fx":177,"heal":356,"leak":8,"skill":57,"spawn":30,"status":530}, "events": {"atk":1176,"deploy":30,"die":35,"dmg":1121,"engage":4,"fx":177,"heal":356,"leak":8,"skill":57,"spawn":30,"status":530},
"fx": {"bandage":4,"dodge":19,"extraAttack":54,"garrisonGrant":1,"mote":40,"redistribute":30,"revive":19,"reweave":2,"sandChains":3,"shield":4,"spGift":1}, "fx": {"bandage":4,"dodge":19,"extraAttack":54,"garrisonGrant":1,"mote":40,"redistribute":30,"revive":19,"reweave":2,"sandChains":3,"shield":4,"spGift":1},
"snaps": "a5613700 74b53427 dd5cc97e 0a644be4 18999580 50032d82 eacff749 082c6c93 279f01a1 f348cb6e 5e51d837 27e023a3 3cdea049 dadd297d 9129f525 42525d2f 77ff9862 aa26c61a 0b186db9" "snaps": "a5613700 74b53427 dd5cc97e 0a644be4 18999580 50032d82 eacff749 082c6c93 279f01a1 f348cb6e 5e51d837 27e023a3 3cdea049 dadd297d 9129f525 42525d2f 77ff9862 aa26c61a 0b186db9"
}, },
@@ -430,7 +430,7 @@
], ],
"enemies": {"enemy_1158_divman":[15,15,0,1502,44550],"enemy_1160_hvyslr":[2,2,0,4278,18000],"enemy_1161_tidmag":[1,1,0,3141,5400]}, "enemies": {"enemy_1158_divman":[15,15,0,1502,44550],"enemy_1160_hvyslr":[2,2,0,4278,18000],"enemy_1161_tidmag":[1,1,0,3141,5400]},
"hooks": [1,29,2220,218,218,320,333,873,22,20,14,14,0,6407,56,13,18,0,1,24,0,1,4], "hooks": [1,29,2220,218,218,320,333,873,22,20,14,14,0,6407,56,13,18,0,1,24,0,1,4],
"events": {"atk":230,"deploy":9,"die":20,"dmg":330,"fx":56,"heal":15,"skill":32,"spawn":28,"status":91}, "events": {"atk":230,"deploy":9,"die":20,"dmg":330,"engage":7,"fx":56,"heal":15,"skill":32,"spawn":28,"status":91},
"fx": {"aoe":6,"burst":1,"coin":3,"dodge":4,"featherArrow":1,"flame":1,"garrisonGrant":1,"revive":2,"riposte":2,"rockslide":1,"shieldBreak":3,"sp":1,"swordRain":24,"swordStorm":4,"zone":2}, "fx": {"aoe":6,"burst":1,"coin":3,"dodge":4,"featherArrow":1,"flame":1,"garrisonGrant":1,"revive":2,"riposte":2,"rockslide":1,"shieldBreak":3,"sp":1,"swordRain":24,"swordStorm":4,"zone":2},
"snaps": "d6fd923e 40279e79 d9530f36 e7c8fb9d c7652603 66aea5b2 e03bbfae a99a96e2" "snaps": "d6fd923e 40279e79 d9530f36 e7c8fb9d c7652603 66aea5b2 e03bbfae a99a96e2"
}, },
@@ -468,7 +468,7 @@
], ],
"enemies": {"enemy_1229_darmy":[10,10,0,418,48384],"enemy_1229_darmy_2":[7,7,0,662,44352],"enemy_1275_dwlock_2":[3,3,0,4287,48384]}, "enemies": {"enemy_1229_darmy":[10,10,0,418,48384],"enemy_1229_darmy_2":[7,7,0,662,44352],"enemy_1275_dwlock_2":[3,3,0,4287,48384]},
"hooks": [1,31,2291,130,130,170,354,1,20,21,7,5,47,3964,0,9,20,0,1,64,0,1,0], "hooks": [1,31,2291,130,130,170,354,1,20,21,7,5,47,3964,0,9,20,0,1,64,0,1,0],
"events": {"atk":132,"deploy":8,"die":21,"dmg":203,"fx":62,"skill":14,"spawn":31,"status":3}, "events": {"atk":132,"deploy":8,"die":21,"dmg":203,"engage":3,"fx":62,"skill":14,"spawn":31,"status":3},
"fx": {"anchor":2,"aoe":2,"beam":2,"burst":1,"coin":4,"ember":1,"explode":1,"revive":2,"splash":47}, "fx": {"anchor":2,"aoe":2,"beam":2,"burst":1,"coin":4,"ember":1,"explode":1,"revive":2,"splash":47},
"snaps": "a56e679b 7e159567 a5350d42 7ffa012b 9ceba6f2 a8bde46f 89918fc2 d5e5149a" "snaps": "a56e679b 7e159567 a5350d42 7ffa012b 9ceba6f2 a8bde46f 89918fc2 d5e5149a"
}, },
@@ -508,7 +508,7 @@
], ],
"enemies": {"enemy_1116_liprr":[11,11,0,3450,69696],"enemy_1116_liprr_2":[4,4,0,394,34560],"enemy_1121_lifbos":[1,1,0,7192,23040]}, "enemies": {"enemy_1116_liprr":[11,11,0,3450,69696],"enemy_1116_liprr_2":[4,4,0,394,34560],"enemy_1121_lifbos":[1,1,0,7192,23040]},
"hooks": [1,28,1921,209,209,376,376,95,18,18,8,7,0,6440,13,21,16,0,1,18,2,0,0], "hooks": [1,28,1921,209,209,376,376,95,18,18,8,7,0,6440,13,21,16,0,1,18,2,0,0],
"events": {"atk":246,"deploy":9,"die":18,"dmg":356,"fx":58,"heal":94,"layer":2,"skill":15,"spawn":27,"status":50}, "events": {"atk":246,"deploy":9,"die":18,"dmg":356,"engage":5,"fx":58,"heal":94,"layer":2,"skill":15,"spawn":27,"status":50},
"fx": {"displace":15,"garrison":2,"healField":1,"liberate":3,"redistribute":14,"reweave":1,"sandChains":1,"shield":2,"slash":18,"truesilver":1}, "fx": {"displace":15,"garrison":2,"healField":1,"liberate":3,"redistribute":14,"reweave":1,"sandChains":1,"shield":2,"slash":18,"truesilver":1},
"snaps": "c0ba6ff7 bba0d7f8 f42e9c18 f6f0af84 b51edddf 58a55b0c 987b2ee8" "snaps": "c0ba6ff7 bba0d7f8 f42e9c18 f6f0af84 b51edddf 58a55b0c 987b2ee8"
}, },
@@ -545,7 +545,7 @@
], ],
"enemies": {"enemy_10124_uashld_2":[2,2,0,4508,124416],"enemy_1043_zomsbr":[23,23,0,20,145812],"enemy_1061_zomshd":[5,5,0,4181,132243]}, "enemies": {"enemy_10124_uashld_2":[2,2,0,4508,124416],"enemy_1043_zomsbr":[23,23,0,20,145812],"enemy_1061_zomshd":[5,5,0,4181,132243]},
"hooks": [1,41,2701,239,239,406,518,2221,33,33,13,12,0,5249,874,19,30,0,1,44,0,0,8], "hooks": [1,41,2701,239,239,406,518,2221,33,33,13,12,0,5249,874,19,30,0,1,44,0,0,8],
"events": {"atk":239,"deploy":9,"die":33,"dmg":397,"fx":65,"heal":22,"skill":25,"spawn":40,"status":128}, "events": {"atk":239,"deploy":9,"die":33,"dmg":397,"engage":7,"fx":65,"heal":22,"skill":25,"spawn":40,"status":128},
"fx": {"aoe":28,"displace":17,"dodge":8,"flame":2,"overload":2,"riposte":3,"shield":2,"volley":3}, "fx": {"aoe":28,"displace":17,"dodge":8,"flame":2,"overload":2,"riposte":3,"shield":2,"volley":3},
"snaps": "1f83614e bead82e1 cf903f9e dcc2392a 63ec0242 18d85986 45dac5d7 c55ace88 05326cdb" "snaps": "1f83614e bead82e1 cf903f9e dcc2392a 63ec0242 18d85986 45dac5d7 c55ace88 05326cdb"
}, },
@@ -588,7 +588,7 @@
], ],
"enemies": {"enemy_1000_gopro_2":[19,19,0,0,29070],"enemy_1325_cbgpro":[5,5,0,0,17100],"enemy_1325_cbgpro_2":[3,3,0,0,12420]}, "enemies": {"enemy_1000_gopro_2":[19,19,0,0,29070],"enemy_1325_cbgpro":[5,5,0,0,17100],"enemy_1325_cbgpro_2":[3,3,0,0,12420]},
"hooks": [1,43,1318,64,64,120,120,0,27,31,3,3,0,5234,1,0,27,0,1,27,1,0,0], "hooks": [1,43,1318,64,64,120,120,0,27,31,3,3,0,5234,1,0,27,0,1,27,1,0,0],
"events": {"atk":64,"deploy":13,"die":31,"dmg":120,"fx":37,"layer":1,"skill":8,"spawn":43,"status":2}, "events": {"atk":64,"deploy":13,"die":31,"dmg":120,"engage":4,"fx":37,"layer":1,"skill":8,"spawn":43,"status":2},
"fx": {"buff":1,"coin":8,"explode":4,"garrison":1,"garrisonGrant":1,"spGift":13,"strike":2,"summon":5,"volley":2}, "fx": {"buff":1,"coin":8,"explode":4,"garrison":1,"garrisonGrant":1,"spGift":13,"strike":2,"summon":5,"volley":2},
"snaps": "75591eda 13ef5faa 3682393e a23e5fa6 5d4daa5d" "snaps": "75591eda 13ef5faa 3682393e a23e5fa6 5d4daa5d"
}, },
@@ -624,7 +624,7 @@
], ],
"enemies": {"enemy_1267_nhpbr_2":[11,11,0,2752,69696],"enemy_1270_nhstlk_2":[4,4,0,0,73728],"enemy_1272_nhtank_2":[2,2,0,2359,34560]}, "enemies": {"enemy_1267_nhpbr_2":[11,11,0,2752,69696],"enemy_1270_nhstlk_2":[4,4,0,0,73728],"enemy_1272_nhtank_2":[2,2,0,2359,34560]},
"hooks": [1,26,2174,74,74,125,125,16,18,18,29,29,0,7659,1586,4,17,0,1,18,0,0,0], "hooks": [1,26,2174,74,74,125,125,16,18,18,29,29,0,7659,1586,4,17,0,1,18,0,0,0],
"events": {"atk":78,"deploy":9,"die":18,"dmg":125,"fx":41,"heal":16,"skill":58,"spawn":26,"status":23}, "events": {"atk":78,"deploy":9,"die":18,"dmg":125,"engage":6,"fx":41,"heal":16,"skill":58,"spawn":26,"status":23},
"fx": {"coldWind":2,"frostNova":3,"healAoe":4,"strike":9,"summon":1,"volley":9,"zone":13}, "fx": {"coldWind":2,"frostNova":3,"healAoe":4,"strike":9,"summon":1,"volley":9,"zone":13},
"snaps": "89cecaf9 38614dcd 7761e0b8 9aeebba4 1f07677c 0a06c2c1 3c72c25c f68984bb" "snaps": "89cecaf9 38614dcd 7761e0b8 9aeebba4 1f07677c 0a06c2c1 3c72c25c f68984bb"
}, },
@@ -661,7 +661,7 @@
], ],
"enemies": {"enemy_1267_nhpbr":[11,11,0,4618,57024],"enemy_1267_nhpbr_2":[5,5,0,1141,31680],"enemy_1272_nhtank":[1,1,0,1689,13824]}, "enemies": {"enemy_1267_nhpbr":[11,11,0,4618,57024],"enemy_1267_nhpbr_2":[5,5,0,1141,31680],"enemy_1272_nhtank":[1,1,0,1689,13824]},
"hooks": [1,25,1853,134,134,486,486,0,20,20,7,4,15,7704,0,2,17,0,1,20,1,0,0], "hooks": [1,25,1853,134,134,486,486,0,20,20,7,4,15,7704,0,2,17,0,1,20,1,0,0],
"events": {"atk":173,"deploy":8,"die":20,"dmg":486,"fx":24,"layer":1,"skill":11,"spawn":25,"status":1}, "events": {"atk":173,"deploy":8,"die":20,"dmg":486,"engage":7,"fx":24,"layer":1,"skill":11,"spawn":25,"status":1},
"fx": {"buff":1,"displace":2,"garrison":1,"pull":2,"zone":18}, "fx": {"buff":1,"displace":2,"garrison":1,"pull":2,"zone":18},
"snaps": "c9571e9b 447301c4 210539b8 00f65df0 cca396c8 6e1a7a83 293704b2" "snaps": "c9571e9b 447301c4 210539b8 00f65df0 cca396c8 6e1a7a83 293704b2"
}, },
@@ -696,7 +696,7 @@
], ],
"enemies": {"enemy_1006_shield_3":[3,3,0,3179,298598],"enemy_1043_zomsbr":[23,23,0,0,156273],"enemy_1402_tgshd_2":[4,4,0,4580,129393]}, "enemies": {"enemy_1006_shield_3":[3,3,0,3179,298598],"enemy_1043_zomsbr":[23,23,0,0,156273],"enemy_1402_tgshd_2":[4,4,0,4580,129393]},
"hooks": [1,38,2418,432,432,755,755,5077,32,32,14,11,243,4675,28,10,30,0,1,39,0,0,0], "hooks": [1,38,2418,432,432,755,755,5077,32,32,14,11,243,4675,28,10,30,0,1,39,0,0,0],
"events": {"atk":524,"deploy":8,"die":32,"dmg":749,"fx":354,"skill":25,"spawn":38,"status":119}, "events": {"atk":524,"deploy":8,"die":32,"dmg":749,"engage":8,"fx":354,"skill":25,"spawn":38,"status":119},
"fx": {"aoe":5,"buff":2,"counter":6,"displace":40,"expose":286,"reload":1,"ripple":9,"sermon":1,"shield":3,"substitute":1}, "fx": {"aoe":5,"buff":2,"counter":6,"displace":40,"expose":286,"reload":1,"ripple":9,"sermon":1,"shield":3,"substitute":1},
"snaps": "86f46177 30356dbb 497a95c9 3fa2d7cd 201ae571 cff27ab7 d0813121 a9f6dc45" "snaps": "86f46177 30356dbb 497a95c9 3fa2d7cd 201ae571 cff27ab7 d0813121 a9f6dc45"
}, },
@@ -734,7 +734,7 @@
], ],
"enemies": {"enemy_1195_sfyin":[11,11,0,0,19800],"enemy_1196_msfyin":[22,22,0,0,44],"enemy_1197_sfshu":[5,5,0,0,11250],"enemy_1198_msfshu":[15,15,0,0,45]}, "enemies": {"enemy_1195_sfyin":[11,11,0,0,19800],"enemy_1196_msfyin":[22,22,0,0,44],"enemy_1197_sfshu":[5,5,0,0,11250],"enemy_1198_msfshu":[15,15,0,0,45]},
"hooks": [1,64,1377,123,123,188,188,0,53,53,13,10,0,7888,30,0,53,0,1,53,0,0,0], "hooks": [1,64,1377,123,123,188,188,0,53,53,13,10,0,7888,30,0,53,0,1,53,0,0,0],
"events": {"atk":159,"deploy":9,"die":53,"dmg":188,"fx":25,"skill":23,"spawn":64,"status":47}, "events": {"atk":159,"deploy":9,"die":53,"dmg":188,"engage":7,"fx":25,"skill":23,"spawn":64,"status":47},
"fx": {"copy":1,"dp":1,"sleep":3,"strike":2,"summon":16,"volley":2}, "fx": {"copy":1,"dp":1,"sleep":3,"strike":2,"summon":16,"volley":2},
"snaps": "5711acf3 3aaf6dc4 ebdfdc7e a54fcb41 1044a950" "snaps": "5711acf3 3aaf6dc4 ebdfdc7e a54fcb41 1044a950"
}, },
@@ -776,7 +776,7 @@
], ],
"enemies": {"enemy_1288_duskls":[11,11,0,210,63415],"enemy_1288_duskls_2":[7,7,0,0,48419],"enemy_1292_duskld":[5,5,0,1224,57650]}, "enemies": {"enemy_1288_duskls":[11,11,0,210,63415],"enemy_1288_duskls_2":[7,7,0,0,48419],"enemy_1292_duskld":[5,5,0,1224,57650]},
"hooks": [1,38,2506,288,330,531,531,411,23,25,19,16,0,11219,246,29,23,0,1,53,0,0,0], "hooks": [1,38,2506,288,330,531,531,411,23,25,19,16,0,11219,246,29,23,0,1,53,0,0,0],
"events": {"atk":346,"deploy":12,"die":25,"dmg":531,"fx":200,"skill":35,"spawn":38,"status":422}, "events": {"atk":346,"deploy":12,"die":25,"dmg":531,"engage":12,"fx":200,"skill":35,"spawn":38,"status":422},
"fx": {"aoe":2,"copy":1,"crit":6,"dp":2,"ember":23,"iceSpike":66,"palsy":3,"sleep":3,"split":3,"weightless":2,"zone":89}, "fx": {"aoe":2,"copy":1,"crit":6,"dp":2,"ember":23,"iceSpike":66,"palsy":3,"sleep":3,"split":3,"weightless":2,"zone":89},
"snaps": "936990ce cac0c6e5 0c5910e5 94cfd566 931c176f 4042d89c fadbeae3 2767137e 57fe0fc3" "snaps": "936990ce cac0c6e5 0c5910e5 94cfd566 931c176f 4042d89c fadbeae3 2767137e 57fe0fc3"
}, },
@@ -811,7 +811,7 @@
], ],
"enemies": {"enemy_1116_liprr":[11,11,0,1505,69696],"enemy_1116_liprr_2":[4,4,0,774,34560],"enemy_1118_lidbox_2":[2,2,0,965,21888]}, "enemies": {"enemy_1116_liprr":[11,11,0,1505,69696],"enemy_1116_liprr_2":[4,4,0,774,34560],"enemy_1118_lidbox_2":[2,2,0,965,21888]},
"hooks": [1,25,1466,165,165,364,364,0,18,18,7,5,0,4660,11,12,17,0,1,18,0,0,0], "hooks": [1,25,1466,165,165,364,364,0,18,18,7,5,0,4660,11,12,17,0,1,18,0,0,0],
"events": {"atk":206,"deploy":8,"die":18,"dmg":363,"fx":12,"skill":12,"spawn":25,"status":73}, "events": {"atk":206,"deploy":8,"die":18,"dmg":363,"engage":5,"fx":12,"skill":12,"spawn":25,"status":73},
"fx": {"buff":1,"displace":4,"ripple":1,"shield":2,"shieldBreak":1,"volley":3}, "fx": {"buff":1,"displace":4,"ripple":1,"shield":2,"shieldBreak":1,"volley":3},
"snaps": "303551a0 a1b61e4e 3d1fabc6 7f04a31d 9df08e32" "snaps": "303551a0 a1b61e4e 3d1fabc6 7f04a31d 9df08e32"
}, },
@@ -850,7 +850,7 @@
], ],
"enemies": {"enemy_1381_winman":[9,9,0,283,129890],"enemy_1381_winman_2":[5,5,0,2524,97044],"enemy_1387_winshd":[4,4,0,2683,119439]}, "enemies": {"enemy_1381_winman":[9,9,0,283,129890],"enemy_1381_winman_2":[5,5,0,2524,97044],"enemy_1387_winshd":[4,4,0,2683,119439]},
"hooks": [1,32,4532,473,473,816,829,0,20,21,37,35,12,11061,79,9,18,0,1,21,3,3,0], "hooks": [1,32,4532,473,473,816,829,0,20,21,37,35,12,11061,79,9,18,0,1,21,3,3,0],
"events": {"atk":506,"deploy":12,"die":21,"dmg":816,"fx":168,"layer":3,"skill":72,"spawn":30,"status":152}, "events": {"atk":506,"deploy":12,"die":21,"dmg":816,"engage":10,"fx":168,"layer":3,"skill":72,"spawn":30,"status":152},
"fx": {"aoe":10,"buff":1,"burst":3,"charge":18,"counter":12,"crit":61,"displace":24,"garrison":3,"quadShot":2,"ripple":9,"sentry":1,"sermon":1,"shadowWeave":12,"shield":1,"soul":3,"stealth":3,"substitute":1,"summon":2,"swap":1}, "fx": {"aoe":10,"buff":1,"burst":3,"charge":18,"counter":12,"crit":61,"displace":24,"garrison":3,"quadShot":2,"ripple":9,"sentry":1,"sermon":1,"shadowWeave":12,"shield":1,"soul":3,"stealth":3,"substitute":1,"summon":2,"swap":1},
"snaps": "d2b18fc5 bd98a396 2815c1da cd98bca5 f9430f26 13b06e1e c8764716 073aa206 ddb6d29d c8d7eba3 26ac5044 a58c61f6 2c76342d ae7f4a4d 301020be c76c6184" "snaps": "d2b18fc5 bd98a396 2815c1da cd98bca5 f9430f26 13b06e1e c8764716 073aa206 ddb6d29d c8d7eba3 26ac5044 a58c61f6 2c76342d ae7f4a4d 301020be c76c6184"
}, },
@@ -890,7 +890,7 @@
], ],
"enemies": {"enemy_1249_lysdb_2":[5,5,0,0,27000],"enemy_1251_lysyta":[15,15,0,0,37125],"enemy_1328_cbjedi":[1,1,0,898,9000]}, "enemies": {"enemy_1249_lysdb_2":[5,5,0,0,27000],"enemy_1251_lysyta":[15,15,0,0,37125],"enemy_1328_cbjedi":[1,1,0,898,9000]},
"hooks": [1,34,1704,101,101,161,156,0,21,21,10,8,0,6803,29,1,21,0,1,21,3,0,0], "hooks": [1,34,1704,101,101,161,156,0,21,21,10,8,0,6803,29,1,21,0,1,21,3,0,0],
"events": {"atk":125,"deploy":10,"die":21,"dmg":156,"fx":59,"layer":3,"skill":18,"spawn":34,"status":46}, "events": {"atk":125,"deploy":10,"die":21,"dmg":156,"engage":5,"fx":59,"layer":3,"skill":18,"spawn":34,"status":46},
"fx": {"charge":5,"dilemma":2,"displace":14,"garrison":3,"levitate":11,"pull":1,"shadowWeave":8,"shieldBreak":5,"slippery":1,"splash":2,"stealth":1,"tornado":1,"tornadoPulse":5}, "fx": {"charge":5,"dilemma":2,"displace":14,"garrison":3,"levitate":11,"pull":1,"shadowWeave":8,"shieldBreak":5,"slippery":1,"splash":2,"stealth":1,"tornado":1,"tornadoPulse":5},
"snaps": "c27c0600 6b7bfecf d505aebd b3d60eec b1e621c0 52cc3598" "snaps": "c27c0600 6b7bfecf d505aebd b3d60eec b1e621c0 52cc3598"
}, },
@@ -930,7 +930,7 @@
], ],
"enemies": {"enemy_1019_jshoot_2":[7,7,0,3373,21773],"enemy_1299_ymkilr_2":[3,3,0,1837,38016],"enemy_1422_lrsldr_2":[14,14,0,0,88704]}, "enemies": {"enemy_1019_jshoot_2":[7,7,0,3373,21773],"enemy_1299_ymkilr_2":[3,3,0,1837,38016],"enemy_1422_lrsldr_2":[14,14,0,0,88704]},
"hooks": [1,37,2071,84,84,166,166,663,26,26,7,6,0,4290,103,10,24,0,1,26,0,0,0], "hooks": [1,37,2071,84,84,166,166,663,26,26,7,6,0,4290,103,10,24,0,1,26,0,0,0],
"events": {"atk":84,"deploy":10,"die":26,"dmg":166,"fx":21,"skill":13,"spawn":37,"status":10}, "events": {"atk":84,"deploy":10,"die":26,"dmg":166,"engage":5,"fx":21,"skill":13,"spawn":37,"status":10},
"fx": {"crit":2,"frostNova":1,"levitate":7,"overclock":1,"pull":1,"slippery":1,"strike":2,"tornado":1,"tornadoPulse":5}, "fx": {"crit":2,"frostNova":1,"levitate":7,"overclock":1,"pull":1,"slippery":1,"strike":2,"tornado":1,"tornadoPulse":5},
"snaps": "1c35197a b5a550c2 449f2d84 3e5d7032 3c466668 15903287 c2362cab" "snaps": "1c35197a b5a550c2 449f2d84 3e5d7032 3c466668 15903287 c2362cab"
}, },
@@ -981,7 +981,7 @@
], ],
"enemies": {"enemy_1116_liprr":[11,11,0,27,69696],"enemy_1116_liprr_2":[4,4,0,324,34560],"enemy_1118_lidbox_2":[2,2,0,6654,21888]}, "enemies": {"enemy_1116_liprr":[11,11,0,27,69696],"enemy_1116_liprr_2":[4,4,0,324,34560],"enemy_1118_lidbox_2":[2,2,0,6654,21888]},
"hooks": [1,28,1632,185,185,252,250,65,20,21,13,13,0,5675,255,14,17,0,1,20,4,0,3], "hooks": [1,28,1632,185,185,252,250,65,20,21,13,13,0,5675,255,14,17,0,1,20,4,0,3],
"events": {"atk":239,"deploy":9,"die":21,"dmg":225,"fx":30,"heal":38,"layer":4,"skill":28,"spawn":27,"status":64}, "events": {"atk":239,"deploy":9,"die":21,"dmg":225,"engage":7,"fx":30,"heal":38,"layer":4,"skill":28,"spawn":27,"status":64},
"fx": {"aoe":1,"blink":1,"dodge":3,"explosion":3,"flame":1,"frostNova":6,"garrison":4,"garrisonGrant":1,"liberate":5,"riposte":3,"rockslide":1,"shieldBreak":1}, "fx": {"aoe":1,"blink":1,"dodge":3,"explosion":3,"flame":1,"frostNova":6,"garrison":4,"garrisonGrant":1,"liberate":5,"riposte":3,"rockslide":1,"shieldBreak":1},
"snaps": "d63c71d9 d887e4cb 55b00fa0 66fcfa30 9df5bcce 63545fa4" "snaps": "d63c71d9 d887e4cb 55b00fa0 66fcfa30 9df5bcce 63545fa4"
}, },
@@ -1021,7 +1021,7 @@
], ],
"enemies": {"enemy_1010_demon":[7,7,0,3616,130637],"enemy_1422_lrsldr_2":[14,14,0,259,191601],"enemy_1425_lrcmra_2":[3,3,0,5726,186624]}, "enemies": {"enemy_1010_demon":[7,7,0,3616,130637],"enemy_1422_lrsldr_2":[14,14,0,259,191601],"enemy_1425_lrcmra_2":[3,3,0,5726,186624]},
"hooks": [1,38,1911,99,99,109,112,68,24,27,6,5,0,1925,0,14,24,0,1,24,1,0,0], "hooks": [1,38,1911,99,99,109,112,68,24,27,6,5,0,1925,0,14,24,0,1,24,1,0,0],
"events": {"atk":107,"deploy":11,"die":27,"dmg":111,"fx":9,"heal":62,"layer":1,"skill":13,"spawn":35,"status":8}, "events": {"atk":107,"deploy":11,"die":27,"dmg":111,"engage":4,"fx":9,"heal":62,"layer":1,"skill":13,"spawn":35,"status":8},
"fx": {"aoe":3,"blink":3,"explosion":1,"garrison":1,"garrisonGrant":1}, "fx": {"aoe":3,"blink":3,"explosion":1,"garrison":1,"garrisonGrant":1},
"snaps": "c2c35b01 bcc47598 01104ea6 bc1ad03d c117d4a0 c04f551c 785ecf20" "snaps": "c2c35b01 bcc47598 01104ea6 bc1ad03d c117d4a0 c04f551c 785ecf20"
}, },
@@ -1058,7 +1058,7 @@
], ],
"enemies": {"enemy_1267_nhpbr":[11,10,0,4196,43172],"enemy_1267_nhpbr_2":[5,5,0,1614,24750],"enemy_1272_nhtank":[1,0,0,11294,2493]}, "enemies": {"enemy_1267_nhpbr":[11,10,0,4196,43172],"enemy_1267_nhpbr_2":[5,5,0,1614,24750],"enemy_1272_nhtank":[1,0,0,11294,2493]},
"hooks": [1,25,3300,239,239,525,525,337,15,15,20,20,0,14425,0,5,17,0,1,15,1,0,0], "hooks": [1,25,3300,239,239,525,525,337,15,15,20,20,0,14425,0,5,17,0,1,15,1,0,0],
"events": {"atk":354,"deploy":8,"die":15,"dmg":486,"fx":112,"heal":337,"layer":1,"skill":40,"spawn":25,"status":247}, "events": {"atk":354,"deploy":8,"die":15,"dmg":486,"engage":3,"fx":112,"heal":337,"layer":1,"skill":40,"spawn":25,"status":247},
"fx": {"buff":1,"counter":15,"garrison":1,"healField":1,"mote":38,"redistribute":15,"reweave":1,"slash":17,"spGift":1,"truesilver":1,"zone":21}, "fx": {"buff":1,"counter":15,"garrison":1,"healField":1,"mote":38,"redistribute":15,"reweave":1,"slash":17,"spGift":1,"truesilver":1,"zone":21},
"snaps": "0bdd9351 b24b323e 7000bcb0 603642af 64df0f27 665eb936 b4c5e8d2 9fde297d b69942c5 7af576c0 dc317609" "snaps": "0bdd9351 b24b323e 7000bcb0 603642af 64df0f27 665eb936 b4c5e8d2 9fde297d b69942c5 7af576c0 dc317609"
}, },
@@ -1093,7 +1093,7 @@
], ],
"enemies": {"enemy_1006_shield_3":[3,3,0,5125,138240],"enemy_1043_zomsbr":[23,23,0,48,70867],"enemy_1402_tgshd_2":[4,4,0,1710,59904]}, "enemies": {"enemy_1006_shield_3":[3,3,0,5125,138240],"enemy_1043_zomsbr":[23,23,0,48,70867],"enemy_1402_tgshd_2":[4,4,0,1710,59904]},
"hooks": [1,39,3442,191,191,497,653,1788,32,32,16,14,0,6394,0,15,30,0,1,46,0,1,0], "hooks": [1,39,3442,191,191,497,653,1788,32,32,16,14,0,6394,0,15,30,0,1,46,0,1,0],
"events": {"atk":225,"deploy":9,"die":32,"dmg":653,"fx":138,"heal":13,"skill":30,"spawn":38,"status":56}, "events": {"atk":225,"deploy":9,"die":32,"dmg":653,"engage":4,"fx":138,"heal":13,"skill":30,"spawn":38,"status":56},
"fx": {"aoe":5,"burst":1,"expose":63,"jungleSoul":1,"mote":20,"redistribute":25,"reweave":2,"slash":20,"truesilver":1}, "fx": {"aoe":5,"burst":1,"expose":63,"jungleSoul":1,"mote":20,"redistribute":25,"reweave":2,"slash":20,"truesilver":1},
"snaps": "2c6321b4 cd26a52a 90107827 1e040533 bf55c7a2 587efe24 04e3a230 2316a1be 704aab56 56e8ca2f 9b645a1a 7f429b8f" "snaps": "2c6321b4 cd26a52a 90107827 1e040533 bf55c7a2 587efe24 04e3a230 2316a1be 704aab56 56e8ca2f 9b645a1a 7f429b8f"
}, },
@@ -1128,7 +1128,7 @@
], ],
"enemies": {"enemy_1010_demon":[3,3,0,7576,25920],"enemy_1010_demon_2":[1,1,0,5601,17280],"enemy_1071_dftman":[15,15,0,880,48384]}, "enemies": {"enemy_1010_demon":[3,3,0,7576,25920],"enemy_1010_demon_2":[1,1,0,5601,17280],"enemy_1071_dftman":[15,15,0,880,48384]},
"hooks": [1,27,2214,233,233,292,318,0,24,24,4,3,0,4409,27,28,19,0,1,24,0,0,0], "hooks": [1,27,2214,233,233,292,318,0,24,24,4,3,0,4409,27,28,19,0,1,24,0,0,0],
"events": {"atk":233,"deploy":8,"die":24,"dmg":318,"fx":50,"skill":11,"spawn":27,"status":53}, "events": {"atk":233,"deploy":8,"die":24,"dmg":318,"engage":8,"fx":50,"skill":11,"spawn":27,"status":53},
"fx": {"aoe":8,"coin":3,"crit":7,"dp":1,"strike":31}, "fx": {"aoe":8,"coin":3,"crit":7,"dp":1,"strike":31},
"snaps": "a194c978 d501eda8 b671f9fb e2590e39 2ab25c9c 6714c2bf ff63f649 9794310b" "snaps": "a194c978 d501eda8 b671f9fb e2590e39 2ab25c9c 6714c2bf ff63f649 9794310b"
}, },
@@ -1166,7 +1166,7 @@
], ],
"enemies": {"enemy_1158_divman":[20,20,0,7809,164229],"enemy_1160_hvyslr_2":[3,3,0,0,104509],"enemy_1161_tidmag_2":[3,3,0,1341,59720]}, "enemies": {"enemy_1158_divman":[20,20,0,7809,164229],"enemy_1160_hvyslr_2":[3,3,0,0,104509],"enemy_1161_tidmag_2":[3,3,0,1341,59720]},
"hooks": [1,39,2292,311,311,481,484,2,30,30,6,5,0,3813,40,4,26,0,1,35,3,0,0], "hooks": [1,39,2292,311,311,481,484,2,30,30,6,5,0,3813,40,4,26,0,1,35,3,0,0],
"events": {"atk":312,"deploy":11,"die":30,"dmg":484,"fx":26,"heal":2,"layer":3,"skill":13,"spawn":37,"status":73}, "events": {"atk":312,"deploy":11,"die":30,"dmg":484,"engage":5,"fx":26,"heal":2,"layer":3,"skill":13,"spawn":37,"status":73},
"fx": {"aoe":3,"bondProc":8,"crit":5,"dp":4,"garrison":3,"wolfShadowLost":3}, "fx": {"aoe":3,"bondProc":8,"crit":5,"dp":4,"garrison":3,"wolfShadowLost":3},
"snaps": "d1ead0d3 cda7c00c 8843b429 bd54e255 1de8d432 e6f225a5 7cba25d9 d80a750d" "snaps": "d1ead0d3 cda7c00c 8843b429 bd54e255 1de8d432 e6f225a5 7cba25d9 d80a750d"
}, },
@@ -1204,7 +1204,7 @@
], ],
"enemies": {"enemy_1010_demon":[3,3,0,208,20250],"enemy_1010_demon_2":[1,1,0,202,13500],"enemy_1071_dftman":[15,15,0,0,37800]}, "enemies": {"enemy_1010_demon":[3,3,0,208,20250],"enemy_1010_demon_2":[1,1,0,202,13500],"enemy_1071_dftman":[15,15,0,0,37800]},
"hooks": [1,30,1601,160,160,234,234,0,19,19,8,7,0,8098,44,3,19,0,1,19,0,0,0], "hooks": [1,30,1601,160,160,234,234,0,19,19,8,7,0,8098,44,3,19,0,1,19,0,0,0],
"events": {"atk":217,"deploy":8,"die":19,"dmg":234,"fx":45,"skill":15,"spawn":30,"status":70}, "events": {"atk":217,"deploy":8,"die":19,"dmg":234,"engage":8,"fx":45,"skill":15,"spawn":30,"status":70},
"fx": {"aoe":1,"displace":19,"pull":19,"sleep":2,"strike":2,"volley":2}, "fx": {"aoe":1,"displace":19,"pull":19,"sleep":2,"strike":2,"volley":2},
"snaps": "ccacd2b6 245aa361 460d6c7e 6f4413db 6fc4040a f85f49f4" "snaps": "ccacd2b6 245aa361 460d6c7e 6f4413db 6fc4040a f85f49f4"
}, },
@@ -1239,7 +1239,7 @@
], ],
"enemies": {"enemy_1116_liprr_2":[10,10,0,0,86400],"enemy_1119_vofsd":[5,5,0,1379,63360],"enemy_1121_lifbos_2":[3,3,0,859,86400]}, "enemies": {"enemy_1116_liprr_2":[10,10,0,0,86400],"enemy_1119_vofsd":[5,5,0,1379,63360],"enemy_1121_lifbos_2":[3,3,0,859,86400]},
"hooks": [1,26,2146,166,166,326,386,8,19,19,7,5,0,4669,550,4,18,0,1,24,0,0,0], "hooks": [1,26,2146,166,166,326,386,8,19,19,7,5,0,4669,550,4,18,0,1,24,0,0,0],
"events": {"atk":166,"deploy":8,"die":19,"dmg":326,"fx":9,"heal":8,"skill":12,"spawn":26,"status":122}, "events": {"atk":166,"deploy":8,"die":19,"dmg":326,"engage":4,"fx":9,"heal":8,"skill":12,"spawn":26,"status":122},
"fx": {"anchor":2,"overload":1,"sleep":1,"wake":5}, "fx": {"anchor":2,"overload":1,"sleep":1,"wake":5},
"snaps": "48e98491 573c930f b71c322f cfbb72a5 8df950de d67595c9 37d059c7 e5c9270e" "snaps": "48e98491 573c930f b71c322f cfbb72a5 8df950de d67595c9 37d059c7 e5c9270e"
}, },
@@ -1291,7 +1291,7 @@
], ],
"enemies": {"enemy_1116_liprr":[11,11,0,81,69696],"enemy_1116_liprr_2":[4,4,0,977,34560],"enemy_1118_lidbox_2":[2,2,0,7396,21888]}, "enemies": {"enemy_1116_liprr":[11,11,0,81,69696],"enemy_1116_liprr_2":[4,4,0,977,34560],"enemy_1118_lidbox_2":[2,2,0,7396,21888]},
"hooks": [1,28,1536,202,202,317,317,0,19,19,7,5,9,6814,23,25,17,0,1,23,4,0,0], "hooks": [1,28,1536,202,202,317,317,0,19,19,7,5,9,6814,23,25,17,0,1,23,4,0,0],
"events": {"atk":237,"deploy":9,"die":19,"dmg":313,"fx":43,"layer":4,"skill":12,"spawn":28,"status":81}, "events": {"atk":237,"deploy":9,"die":19,"dmg":313,"engage":8,"fx":43,"layer":4,"skill":12,"spawn":28,"status":81},
"fx": {"aoe":1,"camouflage":1,"counter":9,"displace":14,"dp":3,"garrison":4,"liberate":3,"ripple":4,"sermon":1,"shield":2,"wolfShadow":1}, "fx": {"aoe":1,"camouflage":1,"counter":9,"displace":14,"dp":3,"garrison":4,"liberate":3,"ripple":4,"sermon":1,"shield":2,"wolfShadow":1},
"snaps": "d8f99986 9338e549 3d97bb3e bfa3b539 5cca309f b0d6a324" "snaps": "d8f99986 9338e549 3d97bb3e bfa3b539 5cca309f b0d6a324"
}, },
@@ -1336,7 +1336,7 @@
"enemy_1208_msfji": [1,1,0,0,25] "enemy_1208_msfji": [1,1,0,0,25]
}, },
"hooks": [1,78,2587,146,146,298,297,414,67,68,23,23,0,7567,60,14,65,1,1,79,0,0,1], "hooks": [1,78,2587,146,146,298,297,414,67,68,23,23,0,7567,60,14,65,1,1,79,0,0,1],
"events": {"atk":192,"deploy":10,"die":67,"dmg":297,"fx":86,"heal":2,"leak":1,"skill":46,"spawn":76,"status":142}, "events": {"atk":192,"deploy":10,"die":67,"dmg":297,"engage":8,"fx":86,"heal":2,"leak":1,"skill":46,"spawn":76,"status":142},
"fx": {"aoe":9,"crit":7,"dodge":1,"dp":3,"sp":2,"substitute":2,"summon":20,"swap":1,"weightless":2,"zone":39}, "fx": {"aoe":9,"crit":7,"dodge":1,"dp":3,"sp":2,"substitute":2,"summon":20,"swap":1,"weightless":2,"zone":39},
"snaps": "f2b40c86 cb23fc22 5b0fad69 cc0ea8d5 97d90aeb 8b83ef35 d4ddce39 5be90841 2f3ae2d2" "snaps": "f2b40c86 cb23fc22 5b0fad69 cc0ea8d5 97d90aeb 8b83ef35 d4ddce39 5be90841 2f3ae2d2"
}, },
@@ -1376,7 +1376,7 @@
], ],
"enemies": {"enemy_1267_nhpbr":[11,11,0,1834,44550],"enemy_1267_nhpbr_2":[5,5,0,1600,24750],"enemy_1270_nhstlk":[2,2,0,1537,21600]}, "enemies": {"enemy_1267_nhpbr":[11,11,0,1834,44550],"enemy_1267_nhpbr_2":[5,5,0,1600,24750],"enemy_1270_nhstlk":[2,2,0,1537,21600]},
"hooks": [1,31,1508,78,78,266,266,2,20,20,8,5,0,6237,34,2,18,0,1,20,1,0,0], "hooks": [1,31,1508,78,78,266,266,2,20,20,8,5,0,6237,34,2,18,0,1,20,1,0,0],
"events": {"atk":120,"deploy":10,"die":20,"dmg":266,"fx":52,"heal":2,"layer":1,"skill":13,"spawn":31,"status":62}, "events": {"atk":120,"deploy":10,"die":20,"dmg":266,"engage":5,"fx":52,"heal":2,"layer":1,"skill":13,"spawn":31,"status":62},
"fx": {"charge":1,"crit":3,"dilemma":1,"garrison":1,"heal":2,"levitate":13,"sentry":1,"shadowWeave":10,"slippery":1,"splash":2,"stealth":1,"zone":16}, "fx": {"charge":1,"crit":3,"dilemma":1,"garrison":1,"heal":2,"levitate":13,"sentry":1,"shadowWeave":10,"slippery":1,"splash":2,"stealth":1,"zone":16},
"snaps": "22a08fb1 8e30016a 0f02e464 8332af62 cf150c69" "snaps": "22a08fb1 8e30016a 0f02e464 8332af62 cf150c69"
}, },
@@ -1413,7 +1413,7 @@
], ],
"enemies": {"enemy_1251_lysyta_2":[19,19,0,133,70042],"enemy_1252_lysytb_2":[7,7,0,1898,72576],"enemy_1329_cbshld_2":[4,4,0,4619,59904]}, "enemies": {"enemy_1251_lysyta_2":[19,19,0,133,70042],"enemy_1252_lysytb_2":[7,7,0,1898,72576],"enemy_1329_cbshld_2":[4,4,0,4619,59904]},
"hooks": [1,40,3837,261,268,401,416,59,30,30,67,66,0,15797,44,5,30,0,1,30,0,2,0], "hooks": [1,40,3837,261,268,401,416,59,30,30,67,66,0,15797,44,5,30,0,1,30,0,2,0],
"events": {"atk":299,"deploy":8,"die":30,"dmg":416,"fx":143,"heal":59,"skill":133,"spawn":40,"status":96}, "events": {"atk":299,"deploy":8,"die":30,"dmg":416,"engage":3,"fx":143,"heal":59,"skill":133,"spawn":40,"status":96},
"fx": {"aoe":9,"burst":2,"counter":10,"displace":30,"heal":45,"iceSpike":11,"ripple":9,"shadowWeave":20,"soul":2,"stealth":5}, "fx": {"aoe":9,"burst":2,"counter":10,"displace":30,"heal":45,"iceSpike":11,"ripple":9,"shadowWeave":20,"soul":2,"stealth":5},
"snaps": "d1a4c271 3f59c254 aa4de239 40a5e684 b35896d7 c3fa7c5d 3d19b457 305a5a12 0cbbe4fe 220ff09a d4060b93 f4a07a32 b6167539" "snaps": "d1a4c271 3f59c254 aa4de239 40a5e684 b35896d7 c3fa7c5d 3d19b457 305a5a12 0cbbe4fe 220ff09a d4060b93 f4a07a32 b6167539"
}, },
@@ -1451,7 +1451,7 @@
], ],
"enemies": {"enemy_1249_lysdb_2":[5,5,0,453,34560],"enemy_1251_lysyta":[15,15,0,229,47520],"enemy_1328_cbjedi":[1,1,0,1182,11520]}, "enemies": {"enemy_1249_lysdb_2":[5,5,0,453,34560],"enemy_1251_lysyta":[15,15,0,229,47520],"enemy_1328_cbjedi":[1,1,0,1182,11520]},
"hooks": [1,32,1604,173,173,234,225,9,23,22,15,14,21,5887,502,14,21,0,1,23,0,0,0], "hooks": [1,32,1604,173,173,234,225,9,23,22,15,14,21,5887,502,14,21,0,1,23,0,0,0],
"events": {"atk":173,"deploy":8,"die":22,"dmg":219,"fx":50,"heal":2,"skill":31,"spawn":32,"status":98}, "events": {"atk":173,"deploy":8,"die":22,"dmg":219,"engage":6,"fx":50,"heal":2,"skill":31,"spawn":32,"status":98},
"fx": {"block":4,"explosion":3,"frostNova":6,"rockslide":2,"shieldBreak":11,"swordRain":12,"swordStorm":2,"thorns":10}, "fx": {"block":4,"explosion":3,"frostNova":6,"rockslide":2,"shieldBreak":11,"swordRain":12,"swordStorm":2,"thorns":10},
"snaps": "241cb7e7 8164a048 e60d878b 54b007e8 2a842e99 9f62b1be" "snaps": "241cb7e7 8164a048 e60d878b 54b007e8 2a842e99 9f62b1be"
}, },
@@ -1486,7 +1486,7 @@
], ],
"enemies": {"enemy_1166_dusbr_2":[14,8,6,3587,114489],"enemy_1169_duphlx_2":[7,0,7,3841,27585],"enemy_1172_dugago":[3,0,3,10832,66787]}, "enemies": {"enemy_1166_dusbr_2":[14,8,6,3587,114489],"enemy_1169_duphlx_2":[7,0,7,3841,27585],"enemy_1172_dugago":[3,0,3,10832,66787]},
"hooks": [1,35,4146,667,667,877,877,37,14,30,19,19,82,14466,301,11,24,16,1,16,0,0,0], "hooks": [1,35,4146,667,667,877,877,37,14,30,19,19,82,14466,301,11,24,16,1,16,0,0,0],
"events": {"atk":762,"deploy":11,"die":14,"dmg":877,"fx":13,"heal":14,"leak":16,"skill":38,"spawn":32,"status":246}, "events": {"atk":762,"deploy":11,"die":14,"dmg":877,"engage":7,"fx":13,"heal":14,"leak":16,"skill":38,"spawn":32,"status":246},
"fx": {"featherArrow":2,"flame":3,"garrisonGrant":1,"revive":2,"stone":2,"taunt":3}, "fx": {"featherArrow":2,"flame":3,"garrisonGrant":1,"revive":2,"stone":2,"taunt":3},
"snaps": "e8a58cae e45c0ada 1d0104f3 5a4162ce f459b0c6 6078554c 0da631c2 89a53382 35812639 be18c73e 09fd2d12 0c74c047 b302eee8 1b6c5142" "snaps": "e8a58cae e45c0ada 1d0104f3 5a4162ce f459b0c6 6078554c 0da631c2 89a53382 35812639 be18c73e 09fd2d12 0c74c047 b302eee8 1b6c5142"
}, },
@@ -1521,7 +1521,7 @@
], ],
"enemies": {"enemy_1249_lysdb_2":[5,5,0,0,27000],"enemy_1251_lysyta":[15,15,0,0,37125],"enemy_1328_cbjedi":[1,1,0,0,9000]}, "enemies": {"enemy_1249_lysdb_2":[5,5,0,0,27000],"enemy_1251_lysyta":[15,15,0,0,37125],"enemy_1328_cbjedi":[1,1,0,0,9000]},
"hooks": [1,29,1341,115,115,277,272,1,21,21,5,3,14,5402,27,0,21,0,1,21,1,0,0], "hooks": [1,29,1341,115,115,277,272,1,21,21,5,3,14,5402,27,0,21,0,1,21,1,0,0],
"events": {"atk":135,"deploy":8,"die":21,"dmg":272,"fx":54,"layer":1,"skill":8,"spawn":29,"status":81}, "events": {"atk":135,"deploy":8,"die":21,"dmg":272,"engage":5,"fx":54,"layer":1,"skill":8,"spawn":29,"status":81},
"fx": {"buff":1,"displace":14,"garrison":1,"garrisonGrant":1,"mote":16,"sandChains":1,"shieldBreak":5,"slash":7,"spGift":7,"truesilver":1}, "fx": {"buff":1,"displace":14,"garrison":1,"garrisonGrant":1,"mote":16,"sandChains":1,"shieldBreak":5,"slash":7,"spGift":7,"truesilver":1},
"snaps": "5fab4324 df1c9981 5d0b2fb3 40f389ee beecf492" "snaps": "5fab4324 df1c9981 5d0b2fb3 40f389ee beecf492"
}, },
@@ -1558,7 +1558,7 @@
], ],
"enemies": {"enemy_1010_demon":[7,7,0,1205,60480],"enemy_1422_lrsldr_2":[14,14,0,74,88704],"enemy_1425_lrcmra_2":[3,2,1,10147,70389]}, "enemies": {"enemy_1010_demon":[7,7,0,1205,60480],"enemy_1422_lrsldr_2":[14,14,0,74,88704],"enemy_1425_lrcmra_2":[3,2,1,10147,70389]},
"hooks": [1,34,3251,301,301,557,557,400,27,28,27,25,76,3567,117,10,24,1,1,27,0,0,0], "hooks": [1,34,3251,301,301,557,557,400,27,28,27,25,76,3567,117,10,24,1,1,27,0,0,0],
"events": {"atk":349,"deploy":10,"die":27,"dmg":557,"fx":146,"leak":1,"skill":52,"spawn":34,"status":196}, "events": {"atk":349,"deploy":10,"die":27,"dmg":557,"engage":7,"fx":146,"leak":1,"skill":52,"spawn":34,"status":196},
"fx": {"charge":10,"crit":53,"dilemma":3,"displace":16,"garrisonGrant":1,"levitate":21,"overclock":1,"pull":1,"quadShot":2,"slippery":2,"sp":23,"splash":7,"tornado":1,"tornadoPulse":5}, "fx": {"charge":10,"crit":53,"dilemma":3,"displace":16,"garrisonGrant":1,"levitate":21,"overclock":1,"pull":1,"quadShot":2,"slippery":2,"sp":23,"splash":7,"tornado":1,"tornadoPulse":5},
"snaps": "52481b26 a970d314 2696635b 9c15cc8e 501817d0 a8faf661 7e05b7cf ffceee93 c505d863 f99ccd20 1c4895d8" "snaps": "52481b26 a970d314 2696635b 9c15cc8e 501817d0 a8faf661 7e05b7cf ffceee93 c505d863 f99ccd20 1c4895d8"
}, },
@@ -1595,7 +1595,7 @@
], ],
"enemies": {"enemy_1116_liprr":[11,11,0,389,69696],"enemy_1116_liprr_2":[4,4,0,319,34560],"enemy_1118_lidbox_2":[2,0,2,13211,18290]}, "enemies": {"enemy_1116_liprr":[11,11,0,389,69696],"enemy_1116_liprr_2":[4,4,0,319,34560],"enemy_1118_lidbox_2":[2,0,2,13211,18290]},
"hooks": [1,27,1907,236,236,388,417,42,19,21,6,5,0,6837,0,20,17,2,1,20,0,0,0], "hooks": [1,27,1907,236,236,388,417,42,19,21,6,5,0,6837,0,20,17,2,1,20,0,0,0],
"events": {"atk":236,"deploy":8,"die":19,"dmg":417,"fx":57,"heal":32,"leak":2,"skill":12,"spawn":27,"status":30}, "events": {"atk":236,"deploy":8,"die":19,"dmg":417,"engage":7,"fx":57,"heal":32,"leak":2,"skill":12,"spawn":27,"status":30},
"fx": {"aoe":7,"coin":3,"counter":3,"liberate":5,"overload":1,"revive":1,"strike":36,"taunt":1}, "fx": {"aoe":7,"coin":3,"counter":3,"liberate":5,"overload":1,"revive":1,"strike":36,"taunt":1},
"snaps": "0e312a6e 45b64065 23e7a18c 93f3311a 14f12d67 80b532e0 88f0c499" "snaps": "0e312a6e 45b64065 23e7a18c 93f3311a 14f12d67 80b532e0 88f0c499"
}, },
@@ -1635,7 +1635,7 @@
], ],
"enemies": {"enemy_1005_yokai_2":[4,4,0,1618,15428],"enemy_1041_lazerd_2":[3,3,0,5218,44790],"enemy_1042_frostd":[5,5,0,0,80870]}, "enemies": {"enemy_1005_yokai_2":[4,4,0,1618,15428],"enemy_1041_lazerd_2":[3,3,0,5218,44790],"enemy_1042_frostd":[5,5,0,0,80870]},
"hooks": [1,25,1933,76,76,78,78,17,12,12,10,9,0,5082,274,0,12,0,1,12,0,0,0], "hooks": [1,25,1933,76,76,78,78,17,12,12,10,9,0,5082,274,0,12,0,1,12,0,0,0],
"events": {"atk":76,"deploy":10,"die":12,"dmg":78,"fx":12,"heal":12,"skill":19,"spawn":25,"status":19}, "events": {"atk":76,"deploy":10,"die":12,"dmg":78,"engage":3,"fx":12,"heal":12,"skill":19,"spawn":25,"status":19},
"fx": {"counter":3,"explosion":6,"overload":1,"taunt":2}, "fx": {"counter":3,"explosion":6,"overload":1,"taunt":2},
"snaps": "aa5b6d09 e8b3aed4 d166ba60 3b476f55 5f0dfd72 520958c0 1d7f9e77" "snaps": "aa5b6d09 e8b3aed4 d166ba60 3b476f55 5f0dfd72 520958c0 1d7f9e77"
}, },
@@ -1671,7 +1671,7 @@
], ],
"enemies": {"enemy_1116_liprr":[11,11,0,2113,54450],"enemy_1116_liprr_2":[4,4,0,932,27000],"enemy_1118_lidbox_2":[2,1,1,5973,16462]}, "enemies": {"enemy_1116_liprr":[11,11,0,2113,54450],"enemy_1116_liprr_2":[4,4,0,932,27000],"enemy_1118_lidbox_2":[2,1,1,5973,16462]},
"hooks": [1,28,1811,193,193,210,210,32,18,21,16,16,0,8197,3,27,17,1,1,18,0,0,0], "hooks": [1,28,1811,193,193,210,210,32,18,21,16,16,0,8197,3,27,17,1,1,18,0,0,0],
"events": {"atk":228,"deploy":11,"die":20,"dmg":210,"fx":50,"heal":32,"leak":1,"skill":32,"spawn":26,"status":26}, "events": {"atk":228,"deploy":11,"die":20,"dmg":210,"engage":6,"fx":50,"heal":32,"leak":1,"skill":32,"spawn":26,"status":26},
"fx": {"aoe":1,"displace":19,"liberate":3,"pull":19,"strike":3,"summon":2,"volley":3}, "fx": {"aoe":1,"displace":19,"liberate":3,"pull":19,"strike":3,"summon":2,"volley":3},
"snaps": "4f49e250 3e34819c 722978d2 a4f896f1 12fedc68 96a398c2" "snaps": "4f49e250 3e34819c 722978d2 a4f896f1 12fedc68 96a398c2"
}, },
@@ -1709,7 +1709,7 @@
], ],
"enemies": {"enemy_1234_dsubrl_2":[2,2,0,9847,32256],"enemy_1433_dsbasi_2":[16,16,0,700,105062]}, "enemies": {"enemy_1234_dsubrl_2":[2,2,0,9847,32256],"enemy_1433_dsbasi_2":[16,16,0,700,105062]},
"hooks": [1,32,2169,163,163,249,300,41,22,23,12,11,0,2963,17,18,18,0,1,22,0,0,12], "hooks": [1,32,2169,163,163,249,300,41,22,23,12,11,0,2963,17,18,18,0,1,22,0,0,12],
"events": {"atk":167,"deploy":12,"die":23,"dmg":300,"fx":94,"heal":24,"skill":25,"spawn":29,"status":31}, "events": {"atk":167,"deploy":12,"die":23,"dmg":300,"engage":4,"fx":94,"heal":24,"skill":25,"spawn":29,"status":31},
"fx": {"dodge":12,"featherArrow":1,"flame":3,"garrisonGrant":1,"healField":1,"pulse":43,"riposte":10,"summon":2,"swordRain":17,"swordStorm":4}, "fx": {"dodge":12,"featherArrow":1,"flame":3,"garrisonGrant":1,"healField":1,"pulse":43,"riposte":10,"summon":2,"swordRain":17,"swordStorm":4},
"snaps": "849e61b3 b054e595 7736283f c3ff5997 f8df75cd d3a1062a ce887350 42803555" "snaps": "849e61b3 b054e595 7736283f c3ff5997 f8df75cd d3a1062a ce887350 42803555"
}, },
@@ -1748,7 +1748,7 @@
], ],
"enemies": {"enemy_1166_dusbr":[15,15,0,203,51840],"enemy_1169_duphlx":[5,5,0,102,23040],"enemy_1170_dushld":[2,2,0,2736,16128]}, "enemies": {"enemy_1166_dusbr":[15,15,0,203,51840],"enemy_1169_duphlx":[5,5,0,102,23040],"enemy_1170_dushld":[2,2,0,2736,16128]},
"hooks": [1,34,2252,218,218,308,308,127,24,24,10,9,0,7847,60,19,22,0,1,26,0,0,0], "hooks": [1,34,2252,218,218,308,308,127,24,24,10,9,0,7847,60,19,22,0,1,26,0,0,0],
"events": {"atk":270,"deploy":9,"die":24,"dmg":308,"fx":9,"skill":19,"spawn":34,"status":142}, "events": {"atk":270,"deploy":9,"die":24,"dmg":308,"engage":7,"fx":9,"skill":19,"spawn":34,"status":142},
"fx": {"camouflage":1,"dp":3,"sleep":1,"volley":3,"wolfShadow":1}, "fx": {"camouflage":1,"dp":3,"sleep":1,"volley":3,"wolfShadow":1},
"snaps": "597235cf c1cf1b3d d9739806 4aa90bb7 00b32341 db0c0cef 65594803 f479bb7a" "snaps": "597235cf c1cf1b3d d9739806 4aa90bb7 00b32341 db0c0cef 65594803 f479bb7a"
}, },
@@ -1794,7 +1794,7 @@
], ],
"enemies": {"enemy_1019_jshoot_2":[7,7,0,1544,47029],"enemy_1381_winman":[9,9,0,0,129890],"enemy_1389_winbab_2":[5,5,0,5384,87091]}, "enemies": {"enemy_1019_jshoot_2":[7,7,0,1544,47029],"enemy_1381_winman":[9,9,0,0,129890],"enemy_1389_winbab_2":[5,5,0,5384,87091]},
"hooks": [1,40,2378,128,128,168,212,1108,22,26,11,11,6,5334,1,8,21,0,1,23,0,0,0], "hooks": [1,40,2378,128,128,168,212,1108,22,26,11,11,6,5334,1,8,21,0,1,23,0,0,0],
"events": {"atk":128,"deploy":16,"die":26,"dmg":209,"fx":31,"heal":8,"skill":26,"spawn":40,"status":3}, "events": {"atk":128,"deploy":16,"die":26,"dmg":209,"engage":7,"fx":31,"heal":8,"skill":26,"spawn":40,"status":3},
"fx": {"coin":8,"downed":1,"explode":4,"firewall":1,"revive":1,"splash":6,"summon":5,"yanyouFlame":3,"yanyouSummon":2}, "fx": {"coin":8,"downed":1,"explode":4,"firewall":1,"revive":1,"splash":6,"summon":5,"yanyouFlame":3,"yanyouSummon":2},
"snaps": "9de1a377 006ae259 36d64b47 07539063 05e6c9d6 a572b9f9 d780cb71 6f89109e" "snaps": "9de1a377 006ae259 36d64b47 07539063 05e6c9d6 a572b9f9 d780cb71 6f89109e"
} }
+22 -22
View File
@@ -78,7 +78,7 @@
"enemy_9016_acstmr": [7,7,0,0,56] "enemy_9016_acstmr": [7,7,0,0,56]
}, },
"hooks": [1,70,5249,1013,1013,2478,2830,1935,34,41,83,79,18,33653,2658,14,28,0,1,33,0,20,0], "hooks": [1,70,5249,1013,1013,2478,2830,1935,34,41,83,79,18,33653,2658,14,28,0,1,33,0,20,0],
"events": {"atk":1288,"deploy":36,"die":41,"dmg":2551,"fx":569,"heal":569,"skill":162,"spawn":66,"status":654}, "events": {"atk":1288,"deploy":36,"die":41,"dmg":2551,"engage":16,"fx":569,"heal":569,"skill":162,"spawn":66,"status":654},
"fx": {"airstrike":5,"beam":48,"blink":1,"bombard":18,"bombardShell":18,"burst":20,"copy":3,"dp":3,"drone":252,"droneLock":22,"drones":3,"firewall":1,"firewallBlock":1,"garrisonGrant":1,"lightning":101,"link":3,"lock":18,"phase":1,"radiantSword":3,"shell":7,"snow":5,"split":2,"storm":9,"summon":7,"talent":4,"tide":4,"wanted":9}, "fx": {"airstrike":5,"beam":48,"blink":1,"bombard":18,"bombardShell":18,"burst":20,"copy":3,"dp":3,"drone":252,"droneLock":22,"drones":3,"firewall":1,"firewallBlock":1,"garrisonGrant":1,"lightning":101,"link":3,"lock":18,"phase":1,"radiantSword":3,"shell":7,"snow":5,"split":2,"storm":9,"summon":7,"talent":4,"tide":4,"wanted":9},
"snaps": "894f44af 313e3a72 a1ca1bcc 538c8df7 836414dd 44afd7b8 7ae02b33 acea2d90 d875735a b27b203d 8f33b14c 1286669e 3b6def40 8e947c0d b1a852d9 3b9ffa7e 6d96d8b1 28aa20b4" "snaps": "894f44af 313e3a72 a1ca1bcc 538c8df7 836414dd 44afd7b8 7ae02b33 acea2d90 d875735a b27b203d 8f33b14c 1286669e 3b6def40 8e947c0d b1a852d9 3b9ffa7e 6d96d8b1 28aa20b4"
}, },
@@ -122,7 +122,7 @@
"enemy_9016_acstmr": [8,7,0,0,56] "enemy_9016_acstmr": [8,7,0,0,56]
}, },
"hooks": [1,39,6000,574,574,827,921,186,27,27,58,56,168,18902,2365,0,28,0,1,25,0,0,0], "hooks": [1,39,6000,574,574,827,921,186,27,27,58,56,168,18902,2365,0,28,0,1,25,0,0,0],
"events": {"atk":595,"deploy":11,"die":27,"dmg":861,"fx":285,"heal":172,"skill":114,"spawn":38,"status":277}, "events": {"atk":595,"deploy":11,"die":27,"dmg":861,"engage":6,"fx":285,"heal":172,"skill":114,"spawn":38,"status":277},
"fx": {"anchor":4,"beam":54,"camouflage":1,"candle":2,"healAoe":36,"overload":1,"phase":1,"reborn":1,"revive":1,"shell":8,"splash":168,"substitute":2,"summon":4,"swap":2}, "fx": {"anchor":4,"beam":54,"camouflage":1,"candle":2,"healAoe":36,"overload":1,"phase":1,"reborn":1,"revive":1,"shell":8,"splash":168,"substitute":2,"summon":4,"swap":2},
"snaps": "a5dad180 d125af54 48cee8b5 3b07616c bc0eda5a e532e5f3 d3b187a2 2c98a5f8 559544bf 798c9fad 22a3c133 b3cbd348 1d516e17 4fa0428c b96b4cc9 c98f2306 628c0fee dd8030f8 3266c412 97ddd90c" "snaps": "a5dad180 d125af54 48cee8b5 3b07616c bc0eda5a e532e5f3 d3b187a2 2c98a5f8 559544bf 798c9fad 22a3c133 b3cbd348 1d516e17 4fa0428c b96b4cc9 c98f2306 628c0fee dd8030f8 3266c412 97ddd90c"
}, },
@@ -174,7 +174,7 @@
], ],
"enemies": {"enemy_1005_yokai_2":[6,6,0,4575,62190],"enemy_1430_lrrook":[2,2,0,0,802449],"enemy_9017_achunt":[2,0,0,30041,599859]}, "enemies": {"enemy_1005_yokai_2":[6,6,0,4575,62190],"enemy_1430_lrrook":[2,2,0,0,802449],"enemy_9017_achunt":[2,0,0,30041,599859]},
"hooks": [1,48,6000,1302,1334,2676,2707,5199,12,24,114,108,24,37654,903,0,10,0,1,12,0,7,0], "hooks": [1,48,6000,1302,1334,2676,2707,5199,12,24,114,108,24,37654,903,0,10,0,1,12,0,7,0],
"events": {"atk":1357,"deploy":38,"die":24,"dmg":2701,"fx":654,"heal":54,"skill":222,"spawn":32,"status":441}, "events": {"atk":1357,"deploy":38,"die":24,"dmg":2701,"engage":18,"fx":654,"heal":54,"skill":222,"spawn":32,"status":441},
"fx": {"aoe":27,"appear":2,"blink":13,"burst":7,"catShield":1,"charge":7,"counter":24,"crit":112,"dilemma":4,"disappear":2,"explosion":14,"frostNova":13,"garrisonGrant":1,"iceSpike":91,"overclock":2,"pull":3,"quadShot":3,"ripple":25,"sentry":7,"sermon":1,"shadowWeave":13,"shield":6,"soul":1,"sp":7,"stealth":9,"swordRain":59,"swordStorm":9,"tornado":4,"tornadoPulse":17,"weightless":3,"zone":167}, "fx": {"aoe":27,"appear":2,"blink":13,"burst":7,"catShield":1,"charge":7,"counter":24,"crit":112,"dilemma":4,"disappear":2,"explosion":14,"frostNova":13,"garrisonGrant":1,"iceSpike":91,"overclock":2,"pull":3,"quadShot":3,"ripple":25,"sentry":7,"sermon":1,"shadowWeave":13,"shield":6,"soul":1,"sp":7,"stealth":9,"swordRain":59,"swordStorm":9,"tornado":4,"tornadoPulse":17,"weightless":3,"zone":167},
"snaps": "3cd698b1 c4c10bef a30f10e2 16900e85 a9a35856 dbfa73dc 81650d2d f76c2446 eac77ab1 120c61fe 7df80570 8eeabc2d 0de7de10 f0f4eb24 cca3fbad 2faf03af 37cb63f6 ec50b27e e5066532 6987cb61" "snaps": "3cd698b1 c4c10bef a30f10e2 16900e85 a9a35856 dbfa73dc 81650d2d f76c2446 eac77ab1 120c61fe 7df80570 8eeabc2d 0de7de10 f0f4eb24 cca3fbad 2faf03af 37cb63f6 ec50b27e e5066532 6987cb61"
}, },
@@ -211,7 +211,7 @@
], ],
"enemies": {"enemy_1249_lysdb_2":[5,5,0,7713,107495],"enemy_1430_lrrook":[2,0,2,0,69826],"enemy_9017_achunt":[1,0,0,20369,130032]}, "enemies": {"enemy_1249_lysdb_2":[5,5,0,7713,107495],"enemy_1430_lrrook":[2,0,2,0,69826],"enemy_9017_achunt":[1,0,0,20369,130032]},
"hooks": [1,21,6000,384,384,992,1392,437,8,10,26,25,0,11439,66,5,8,2,1,12,0,2,2], "hooks": [1,21,6000,384,384,992,1392,437,8,10,26,25,0,11439,66,5,8,2,1,12,0,2,2],
"events": {"atk":407,"deploy":13,"die":8,"dmg":1388,"fx":261,"heal":427,"leak":2,"skill":54,"spawn":18,"status":408}, "events": {"atk":407,"deploy":13,"die":8,"dmg":1388,"engage":7,"fx":261,"heal":427,"leak":2,"skill":54,"spawn":18,"status":408},
"fx": {"bandage":3,"block":4,"burst":2,"charge":7,"dodge":2,"featherArrow":1,"flame":5,"garrisonGrant":1,"healField":1,"jungleSoul":3,"mote":102,"redistribute":45,"reweave":3,"riposte":2,"rockslide":2,"sandChains":3,"sandChainsCharged":2,"shieldBreak":9,"slash":50,"spGift":5,"thorns":6,"truesilver":3}, "fx": {"bandage":3,"block":4,"burst":2,"charge":7,"dodge":2,"featherArrow":1,"flame":5,"garrisonGrant":1,"healField":1,"jungleSoul":3,"mote":102,"redistribute":45,"reweave":3,"riposte":2,"rockslide":2,"sandChains":3,"sandChainsCharged":2,"shieldBreak":9,"slash":50,"spGift":5,"thorns":6,"truesilver":3},
"snaps": "0f880931 8c81facc 290a0f9b 2bb4b1a9 8e556ed9 f2194a8a d52e0a62 9b379622 a978b6ed e31e004a 73d5361b 01a346ea ff508697 13a33137 5e17b2f9 f47e3e5d 9d93e6fc ebf591d1 49f516d3 62bd96ec" "snaps": "0f880931 8c81facc 290a0f9b 2bb4b1a9 8e556ed9 f2194a8a d52e0a62 9b379622 a978b6ed e31e004a 73d5361b 01a346ea ff508697 13a33137 5e17b2f9 f47e3e5d 9d93e6fc ebf591d1 49f516d3 62bd96ec"
}, },
@@ -279,7 +279,7 @@
"enemy_9023_acdums": [6,5,0,10453,56] "enemy_9023_acdums": [6,5,0,10453,56]
}, },
"hooks": [1,52,3217,835,835,1876,2153,587,18,26,39,31,178,24531,946,3,19,0,1,17,0,2,0], "hooks": [1,52,3217,835,835,1876,2153,587,18,26,39,31,178,24531,946,3,19,0,1,17,0,2,0],
"events": {"atk":950,"deploy":29,"die":26,"dmg":1987,"fx":591,"heal":214,"skill":70,"spawn":50,"status":337}, "events": {"atk":950,"deploy":29,"die":26,"dmg":1987,"engage":14,"fx":591,"heal":214,"skill":70,"spawn":50,"status":337},
"fx": {"airstrike":82,"beam":6,"blink":2,"bombard":29,"bombardShell":29,"burst":2,"copy":1,"displace":1,"dp":3,"drone":173,"droneLock":16,"drones":2,"explode":96,"finale":1,"garrisonGrant":1,"lightning":44,"link":2,"lock":29,"lure":2,"phase":12,"radiantSword":2,"slash":11,"snow":11,"split":2,"steal":4,"storm":4,"summon":10,"sunBurst":1,"talent":4,"telegraph":3,"tide":3,"wanted":3}, "fx": {"airstrike":82,"beam":6,"blink":2,"bombard":29,"bombardShell":29,"burst":2,"copy":1,"displace":1,"dp":3,"drone":173,"droneLock":16,"drones":2,"explode":96,"finale":1,"garrisonGrant":1,"lightning":44,"link":2,"lock":29,"lure":2,"phase":12,"radiantSword":2,"slash":11,"snow":11,"split":2,"steal":4,"storm":4,"summon":10,"sunBurst":1,"talent":4,"telegraph":3,"tide":3,"wanted":3},
"snaps": "0e2e327e 9068cc6a ced48e18 fdd1b2ba 7bbf69fc 61492769 1b5e7d15 aa2c6113 9484b207 1ca0f38b efde51f4" "snaps": "0e2e327e 9068cc6a ced48e18 fdd1b2ba 7bbf69fc 61492769 1b5e7d15 aa2c6113 9484b207 1ca0f38b efde51f4"
}, },
@@ -330,7 +330,7 @@
"enemy_9023_acdums": [9,9,0,4276,90] "enemy_9023_acdums": [9,9,0,4276,90]
}, },
"hooks": [1,42,6000,359,359,677,1340,255,21,24,62,61,21,14628,1263,3,20,0,1,102,0,1,0], "hooks": [1,42,6000,359,359,677,1340,255,21,24,62,61,21,14628,1263,3,20,0,1,102,0,1,0],
"events": {"atk":383,"deploy":16,"die":24,"dmg":627,"fx":243,"heal":56,"skill":123,"spawn":37,"status":192}, "events": {"atk":383,"deploy":16,"die":24,"dmg":627,"engage":8,"fx":243,"heal":56,"skill":123,"spawn":37,"status":192},
"fx": {"anchor":1,"aoe":2,"beam":9,"burst":1,"candle":1,"ember":2,"explode":151,"healAoe":18,"overload":4,"phase":17,"splash":21,"substitute":2,"summon":9,"swap":2,"telegraph":1,"teleport":2}, "fx": {"anchor":1,"aoe":2,"beam":9,"burst":1,"candle":1,"ember":2,"explode":151,"healAoe":18,"overload":4,"phase":17,"splash":21,"substitute":2,"summon":9,"swap":2,"telegraph":1,"teleport":2},
"snaps": "350d2037 5eb324a9 93890c27 cfd3cc69 9d47cb8d 97a81071 03889d40 e843828d 92884942 e6498744 15391702 f00b712f fcb3da1c 0b44c227 2802904f c8eaf7b3 a142e278 b3656951 bd850e24 0b8940d4" "snaps": "350d2037 5eb324a9 93890c27 cfd3cc69 9d47cb8d 97a81071 03889d40 e843828d 92884942 e6498744 15391702 f00b712f fcb3da1c 0b44c227 2802904f c8eaf7b3 a142e278 b3656951 bd850e24 0b8940d4"
}, },
@@ -382,7 +382,7 @@
], ],
"enemies": {"enemy_1010_demon":[4,4,0,2035,200612],"enemy_1438_dspred":[10,8,2,18391,859975],"enemy_1521_dslily":[1,0,0,89880,1019291]}, "enemies": {"enemy_1010_demon":[4,4,0,2035,200612],"enemy_1438_dspred":[10,8,2,18391,859975],"enemy_1521_dslily":[1,0,0,89880,1019291]},
"hooks": [1,64,6000,1264,1327,3121,3243,5401,46,50,131,130,9,32811,1211,12,15,2,1,49,0,28,6], "hooks": [1,64,6000,1264,1327,3121,3243,5401,46,50,131,130,9,32811,1211,12,15,2,1,49,0,28,6],
"events": {"atk":1527,"deploy":49,"die":48,"dmg":3236,"fx":809,"heal":191,"leak":2,"skill":261,"spawn":37,"status":830}, "events": {"atk":1527,"deploy":49,"die":48,"dmg":3236,"engage":18,"fx":809,"heal":191,"leak":2,"skill":261,"spawn":37,"status":830},
"fx": { "fx": {
"aoe": 30, "aoe": 30,
"appear": 8, "appear": 8,
@@ -457,7 +457,7 @@
], ],
"enemies": {"enemy_1010_demon_2":[2,2,0,7219,107495],"enemy_1438_dspred":[5,2,3,28087,137265],"enemy_1521_dslily":[1,0,0,68045,411797]}, "enemies": {"enemy_1010_demon_2":[2,2,0,7219,107495],"enemy_1438_dspred":[5,2,3,28087,137265],"enemy_1521_dslily":[1,0,0,68045,411797]},
"hooks": [1,23,6000,567,567,959,1067,335,8,11,25,24,0,13355,129,10,8,3,1,8,0,30,0], "hooks": [1,23,6000,567,567,959,1067,335,8,11,25,24,0,13355,129,10,8,3,1,8,0,30,0],
"events": {"atk":762,"deploy":13,"die":8,"dmg":1049,"fx":335,"heal":329,"leak":3,"skill":51,"spawn":20,"status":513}, "events": {"atk":762,"deploy":13,"die":8,"dmg":1049,"engage":6,"fx":335,"heal":329,"leak":3,"skill":51,"spawn":20,"status":513},
"fx": {"bandage":1,"block":18,"burst":30,"camouflage":1,"featherArrow":5,"garrisonGrant":1,"grow":1,"healField":2,"jungleSoul":1,"mote":68,"redistribute":45,"reweave":3,"rockfall":18,"rockslide":4,"sandChains":6,"shieldBreak":18,"slash":46,"spGift":3,"tentacle":6,"thorns":50,"tide":5,"truesilver":3}, "fx": {"bandage":1,"block":18,"burst":30,"camouflage":1,"featherArrow":5,"garrisonGrant":1,"grow":1,"healField":2,"jungleSoul":1,"mote":68,"redistribute":45,"reweave":3,"rockfall":18,"rockslide":4,"sandChains":6,"shieldBreak":18,"slash":46,"spGift":3,"tentacle":6,"thorns":50,"tide":5,"truesilver":3},
"snaps": "bdb41e6b d33270c9 b699515f 4c778ec3 7b987465 1dc86ced aa1b5647 d974b80c 1d71a5f3 0398ba1c cb1b4c96 23b4948c 4e179090 70afc8aa 855ad8e7 80f3d475 405e2c17 3b9e93ac 2b80137e cf30df9d" "snaps": "bdb41e6b d33270c9 b699515f 4c778ec3 7b987465 1dc86ced aa1b5647 d974b80c 1d71a5f3 0398ba1c cb1b4c96 23b4948c 4e179090 70afc8aa 855ad8e7 80f3d475 405e2c17 3b9e93ac 2b80137e cf30df9d"
}, },
@@ -520,7 +520,7 @@
"enemy_2017_csphts": [6,6,0,67,60000] "enemy_2017_csphts": [6,6,0,67,60000]
}, },
"hooks": [1,48,2041,383,383,1035,1245,794,12,23,45,33,24,14908,784,19,14,0,1,10,0,10,44], "hooks": [1,48,2041,383,383,1035,1245,794,12,23,45,33,24,14908,784,19,14,0,1,10,0,10,44],
"events": {"atk":517,"deploy":34,"die":23,"dmg":1136,"fx":333,"heal":286,"skill":78,"spawn":41,"status":368}, "events": {"atk":517,"deploy":34,"die":23,"dmg":1136,"engage":20,"fx":333,"heal":286,"skill":78,"spawn":41,"status":368},
"fx": { "fx": {
"airstrike": 9, "airstrike": 9,
"blink": 13, "blink": 13,
@@ -601,7 +601,7 @@
"enemy_5601_entlec": [2,2,0,0,2905] "enemy_5601_entlec": [2,2,0,0,2905]
}, },
"hooks": [1,25,4545,286,286,539,734,344,13,15,49,47,84,13386,970,10,11,0,1,16,0,3,41], "hooks": [1,25,4545,286,286,539,734,344,13,15,49,47,84,13386,970,10,11,0,1,16,0,3,41],
"events": {"atk":290,"deploy":14,"die":15,"dmg":614,"fx":184,"heal":316,"skill":96,"spawn":23,"status":146}, "events": {"atk":290,"deploy":14,"die":15,"dmg":614,"engage":7,"fx":184,"heal":316,"skill":96,"spawn":23,"status":146},
"fx": {"anchor":1,"blink":3,"bloodBattle":1,"burst":3,"candle":2,"dodge":41,"ember":1,"explode":18,"healAoe":19,"overload":2,"shieldBreak":2,"splash":84,"summon":4,"telegraph":1,"teleport":2}, "fx": {"anchor":1,"blink":3,"bloodBattle":1,"burst":3,"candle":2,"dodge":41,"ember":1,"explode":18,"healAoe":19,"overload":2,"shieldBreak":2,"splash":84,"summon":4,"telegraph":1,"teleport":2},
"snaps": "1b2c287a eaa9179b 630f3c60 4d66a11e 1bb71808 3f98dccb 22cb18a1 217a35ef 09de2906 ba0f42cc 81fbd1d0 398611ec 0abd9c6f 252f015d fe9c3866 f9bb937f" "snaps": "1b2c287a eaa9179b 630f3c60 4d66a11e 1bb71808 3f98dccb 22cb18a1 217a35ef 09de2906 ba0f42cc 81fbd1d0 398611ec 0abd9c6f 252f015d fe9c3866 f9bb937f"
}, },
@@ -659,7 +659,7 @@
"enemy_9032_aclionk": [1,0,0,34912,1157397] "enemy_9032_aclionk": [1,0,0,34912,1157397]
}, },
"hooks": [1,50,6000,1272,1302,2358,2376,1878,26,30,122,120,0,38644,1437,4,15,2,1,21,0,5,1], "hooks": [1,50,6000,1272,1302,2358,2376,1878,26,30,122,120,0,38644,1437,4,15,2,1,21,0,5,1],
"events": {"atk":1371,"deploy":35,"die":28,"dmg":2328,"fx":600,"heal":36,"leak":2,"skill":242,"spawn":37,"status":955}, "events": {"atk":1371,"deploy":35,"die":28,"dmg":2328,"engage":14,"fx":600,"heal":36,"leak":2,"skill":242,"spawn":37,"status":955},
"fx": { "fx": {
"aoe": 43, "aoe": 43,
"appear": 4, "appear": 4,
@@ -744,7 +744,7 @@
"enemy_9032_aclionk": [1,0,0,40419,102806] "enemy_9032_aclionk": [1,0,0,40419,102806]
}, },
"hooks": [1,47,6000,190,190,281,479,239,32,34,21,21,0,7024,68,2,20,2,1,23,0,0,2], "hooks": [1,47,6000,190,190,281,479,239,32,34,21,21,0,7024,68,2,20,2,1,23,0,0,2],
"events": {"atk":220,"deploy":21,"die":32,"dmg":208,"fx":90,"heal":34,"leak":2,"skill":44,"spawn":36,"status":187}, "events": {"atk":220,"deploy":21,"die":32,"dmg":208,"engage":7,"fx":90,"heal":34,"leak":2,"skill":44,"spawn":36,"status":187},
"fx": {"dodge":2,"equip":12,"featherArrow":7,"flame":3,"garrisonGrant":1,"jungleSoul":2,"mote":9,"riposte":2,"rockslide":1,"sandChains":4,"slash":24,"summon":2,"sword":1,"telegraph":11,"truesilver":4,"vest":5}, "fx": {"dodge":2,"equip":12,"featherArrow":7,"flame":3,"garrisonGrant":1,"jungleSoul":2,"mote":9,"riposte":2,"rockslide":1,"sandChains":4,"slash":24,"summon":2,"sword":1,"telegraph":11,"truesilver":4,"vest":5},
"snaps": "8056ac8d bae7d7dc 775827b4 0d832bb0 b68eb0ff 07afed85 063dbd3e 1ffe3d7a b60db00b cc64833f 2776a8b9 a8e2de77 5a30391e 46573a52 04e74ed7 41e6b272 5ea737fb bb532ee9 6cc70b97 f363c643" "snaps": "8056ac8d bae7d7dc 775827b4 0d832bb0 b68eb0ff 07afed85 063dbd3e 1ffe3d7a b60db00b cc64833f 2776a8b9 a8e2de77 5a30391e 46573a52 04e74ed7 41e6b272 5ea737fb bb532ee9 6cc70b97 f363c643"
}, },
@@ -809,7 +809,7 @@
"enemy_9033_acdeer": [1,1,0,35494,2000000] "enemy_9033_acdeer": [1,1,0,35494,2000000]
}, },
"hooks": [1,49,4851,1130,1130,2879,3181,1003,21,27,76,72,200,28482,993,4,17,0,1,20,0,14,0], "hooks": [1,49,4851,1130,1130,2879,3181,1003,21,27,76,72,200,28482,993,4,17,0,1,20,0,14,0],
"events": {"atk":1335,"deploy":32,"die":27,"dmg":2899,"fx":431,"heal":473,"skill":148,"spawn":47,"status":688}, "events": {"atk":1335,"deploy":32,"die":27,"dmg":2899,"engage":16,"fx":431,"heal":473,"skill":148,"spawn":47,"status":688},
"fx": {"airstrike":68,"beam":5,"blink":2,"burst":14,"cleanse":1,"column":40,"copy":4,"dp":4,"drone":234,"droneLock":10,"drones":2,"emergency":1,"explode":6,"garrisonGrant":1,"link":1,"phase":1,"scorchBurst":6,"shieldBreak":4,"snow":5,"split":3,"steal":4,"sunBurst":4,"talent":4,"tide":4,"wanted":3}, "fx": {"airstrike":68,"beam":5,"blink":2,"burst":14,"cleanse":1,"column":40,"copy":4,"dp":4,"drone":234,"droneLock":10,"drones":2,"emergency":1,"explode":6,"garrisonGrant":1,"link":1,"phase":1,"scorchBurst":6,"shieldBreak":4,"snow":5,"split":3,"steal":4,"sunBurst":4,"talent":4,"tide":4,"wanted":3},
"snaps": "d456f8d7 76ad512f fc04bcfe e6e90350 4d81715e b3d0e9d2 9f9304f4 9ea01822 e8064996 57183074 78e21334 5efb2070 1a9f6c3d b3ca158c e6ef5780 ea45e2c1 32576c19" "snaps": "d456f8d7 76ad512f fc04bcfe e6e90350 4d81715e b3d0e9d2 9f9304f4 9ea01822 e8064996 57183074 78e21334 5efb2070 1a9f6c3d b3ca158c e6ef5780 ea45e2c1 32576c19"
}, },
@@ -849,7 +849,7 @@
], ],
"enemies": {"enemy_1061_zomshd":[2,2,0,703,35884],"enemy_1067_snslime":[5,5,0,2179,26957],"enemy_1081_sotisd":[4,4,0,1845,99533],"enemy_9033_acdeer":[1,0,0,63952,345536]}, "enemies": {"enemy_1061_zomshd":[2,2,0,703,35884],"enemy_1067_snslime":[5,5,0,2179,26957],"enemy_1081_sotisd":[4,4,0,1845,99533],"enemy_9033_acdeer":[1,0,0,63952,345536]},
"hooks": [1,32,6000,407,407,581,828,564,12,15,100,99,21,19541,1165,3,12,0,1,12,0,0,0], "hooks": [1,32,6000,407,407,581,828,564,12,15,100,99,21,19541,1165,3,12,0,1,12,0,0,0],
"events": {"atk":407,"deploy":20,"die":15,"dmg":588,"fx":138,"heal":158,"skill":199,"spawn":25,"status":194}, "events": {"atk":407,"deploy":20,"die":15,"dmg":588,"engage":5,"fx":138,"heal":158,"skill":199,"spawn":25,"status":194},
"fx": {"anchor":3,"beam":5,"camouflage":1,"column":52,"explode":5,"healAoe":40,"overload":4,"phase":1,"splash":21,"teleport":6}, "fx": {"anchor":3,"beam":5,"camouflage":1,"column":52,"explode":5,"healAoe":40,"overload":4,"phase":1,"splash":21,"teleport":6},
"snaps": "4de66f1a 63c08f30 825591c9 d0ce31a1 7aef05fb 1ec56c35 f5134898 a3987ee3 f826ddd0 8536abb7 e61cb2af eb4dc819 ccfdf14f 98da64f1 27310d84 6f8f4bac 8f4f0950 bd66a2ba e033ff9d 33a4d212" "snaps": "4de66f1a 63c08f30 825591c9 d0ce31a1 7aef05fb 1ec56c35 f5134898 a3987ee3 f826ddd0 8536abb7 e61cb2af eb4dc819 ccfdf14f 98da64f1 27310d84 6f8f4bac 8f4f0950 bd66a2ba e033ff9d 33a4d212"
}, },
@@ -909,7 +909,7 @@
"enemy_9016_acstmr": [8,7,0,0,42] "enemy_9016_acstmr": [8,7,0,0,42]
}, },
"hooks": [1,87,6000,808,809,2627,2815,4682,68,71,81,79,9,21642,625,21,35,2,1,69,0,8,3], "hooks": [1,87,6000,808,809,2627,2815,4682,68,71,81,79,9,21642,625,21,35,2,1,69,0,8,3],
"events": {"atk":940,"deploy":52,"die":69,"dmg":2811,"fx":550,"heal":130,"leak":2,"skill":160,"spawn":57,"status":569}, "events": {"atk":940,"deploy":52,"die":69,"dmg":2811,"engage":18,"fx":550,"heal":130,"leak":2,"skill":160,"spawn":57,"status":569},
"fx": { "fx": {
"aoe": 31, "aoe": 31,
"appear": 7, "appear": 7,
@@ -999,7 +999,7 @@
"enemy_9016_acstmr": [7,7,0,0,31] "enemy_9016_acstmr": [7,7,0,0,31]
}, },
"hooks": [1,53,6000,301,301,538,573,110,37,45,17,17,0,5624,46,4,27,8,1,33,0,0,0], "hooks": [1,53,6000,301,301,538,573,110,37,45,17,17,0,5624,46,4,27,8,1,33,0,0,0],
"events": {"atk":354,"deploy":22,"die":37,"dmg":567,"fx":174,"heal":104,"leak":8,"skill":35,"spawn":41,"status":138}, "events": {"atk":354,"deploy":22,"die":37,"dmg":567,"engage":5,"fx":174,"heal":104,"leak":8,"skill":35,"spawn":41,"status":138},
"fx": {"bandage":1,"beam":32,"blink":10,"block":3,"charge":6,"explode":7,"featherArrow":1,"garrisonGrant":1,"healField":4,"mote":5,"phase":1,"redistribute":7,"reweave":1,"rockslide":6,"sandChains":1,"shell":7,"shieldBreak":6,"slash":56,"spGift":3,"summon":5,"telegraph":3,"thorns":5,"truesilver":3}, "fx": {"bandage":1,"beam":32,"blink":10,"block":3,"charge":6,"explode":7,"featherArrow":1,"garrisonGrant":1,"healField":4,"mote":5,"phase":1,"redistribute":7,"reweave":1,"rockslide":6,"sandChains":1,"shell":7,"shieldBreak":6,"slash":56,"spGift":3,"summon":5,"telegraph":3,"thorns":5,"truesilver":3},
"snaps": "32c4a923 512591e1 e9c907a6 e3107633 ba7c0969 3a59d469 b8cecc85 46968da9 2129d863 19b4ada6 9afd8bfd ede8ed1f 6d35ee92 820ce8ef 4981d928 d404a14b 089b661f 39f4bf14 8620255e 7016887c" "snaps": "32c4a923 512591e1 e9c907a6 e3107633 ba7c0969 3a59d469 b8cecc85 46968da9 2129d863 19b4ada6 9afd8bfd ede8ed1f 6d35ee92 820ce8ef 4981d928 d404a14b 089b661f 39f4bf14 8620255e 7016887c"
}, },
@@ -1072,7 +1072,7 @@
"enemy_9020_actrpc": [1,0,0,27400,1046182] "enemy_9020_actrpc": [1,0,0,27400,1046182]
}, },
"hooks": [1,67,6000,1512,1512,6012,11389,1913,32,41,99,94,327,40903,1953,18,13,0,1,42,0,22,0], "hooks": [1,67,6000,1512,1512,6012,11389,1913,32,41,99,94,327,40903,1953,18,13,0,1,42,0,22,0],
"events": {"atk":1799,"deploy":48,"die":41,"dmg":10991,"fx":1980,"heal":654,"skill":193,"spawn":49,"status":765}, "events": {"atk":1799,"deploy":48,"die":41,"dmg":10991,"engage":24,"fx":1980,"heal":654,"skill":193,"spawn":49,"status":765},
"fx": { "fx": {
"airstrike": 113, "airstrike": 113,
"beam": 18, "beam": 18,
@@ -1160,7 +1160,7 @@
"enemy_9020_actrpc": [1,0,0,14284,517482] "enemy_9020_actrpc": [1,0,0,14284,517482]
}, },
"hooks": [1,40,4862,758,758,3025,6628,849,17,20,87,85,103,19739,851,11,15,0,1,52,0,2,0], "hooks": [1,40,4862,758,758,3025,6628,849,17,20,87,85,103,19739,851,11,15,0,1,52,0,2,0],
"events": {"atk":1148,"deploy":25,"die":20,"dmg":6285,"fx":704,"heal":533,"skill":172,"spawn":28,"status":271}, "events": {"atk":1148,"deploy":25,"die":20,"dmg":6285,"engage":10,"fx":704,"heal":533,"skill":172,"spawn":28,"status":271},
"fx": {"anchor":4,"aoe":1,"beam":19,"burst":2,"candle":8,"charge":6,"dash":12,"downed":1,"ember":1,"explode":1,"healAoe":28,"link":483,"overload":3,"palsy":3,"phase":10,"reborn":1,"shieldBreak":9,"splash":103,"substitute":2,"summon":1,"swap":2,"teleport":4}, "fx": {"anchor":4,"aoe":1,"beam":19,"burst":2,"candle":8,"charge":6,"dash":12,"downed":1,"ember":1,"explode":1,"healAoe":28,"link":483,"overload":3,"palsy":3,"phase":10,"reborn":1,"shieldBreak":9,"splash":103,"substitute":2,"summon":1,"swap":2,"teleport":4},
"snaps": "d71f569c a47a1d66 df4b4783 0854c875 846091e0 9890e33e 5fdb72ff fb639060 fcd20f12 04a361dc d58cf1d0 1482a16a 606d5129 d394ce04 39d5d7f5 6adac8ac 09d55362" "snaps": "d71f569c a47a1d66 df4b4783 0854c875 846091e0 9890e33e 5fdb72ff fb639060 fcd20f12 04a361dc d58cf1d0 1482a16a 606d5129 d394ce04 39d5d7f5 6adac8ac 09d55362"
}, },
@@ -1220,7 +1220,7 @@
"enemy_9023_acdums": [12,12,0,17083,120] "enemy_9023_acdums": [12,12,0,17083,120]
}, },
"hooks": [1,51,6000,1128,1262,2491,2638,5656,23,25,69,65,0,29939,1498,5,18,0,1,25,0,5,0], "hooks": [1,51,6000,1128,1262,2491,2638,5656,23,25,69,65,0,29939,1498,5,18,0,1,25,0,5,0],
"events": {"atk":1227,"deploy":31,"die":25,"dmg":2627,"fx":988,"heal":28,"skill":134,"spawn":42,"status":587}, "events": {"atk":1227,"deploy":31,"die":25,"dmg":2627,"engage":14,"fx":988,"heal":28,"skill":134,"spawn":42,"status":587},
"fx": { "fx": {
"aoe": 6, "aoe": 6,
"beam": 12, "beam": 12,
@@ -1300,7 +1300,7 @@
"enemy_9023_acdums": [10,10,0,38449,100] "enemy_9023_acdums": [10,10,0,38449,100]
}, },
"hooks": [1,26,6000,606,606,1242,1839,452,13,14,27,24,0,13867,59,10,15,1,1,17,0,11,0], "hooks": [1,26,6000,606,606,1242,1839,452,13,14,27,24,0,13867,59,10,15,1,1,17,0,11,0],
"events": {"atk":723,"deploy":11,"die":13,"dmg":1826,"fx":463,"heal":441,"leak":1,"skill":53,"spawn":25,"status":257}, "events": {"atk":723,"deploy":11,"die":13,"dmg":1826,"engage":6,"fx":463,"heal":441,"leak":1,"skill":53,"spawn":25,"status":257},
"fx": {"bandage":4,"beam":15,"burst":11,"ember":2,"explode":189,"featherArrow":7,"garrisonGrant":1,"healField":2,"jungleSoul":5,"mote":61,"phase":30,"redistribute":43,"reweave":3,"rockslide":2,"sandChainsCharged":1,"shieldBreak":15,"slash":52,"spGift":7,"summon":8,"telegraph":2,"truesilver":3}, "fx": {"bandage":4,"beam":15,"burst":11,"ember":2,"explode":189,"featherArrow":7,"garrisonGrant":1,"healField":2,"jungleSoul":5,"mote":61,"phase":30,"redistribute":43,"reweave":3,"rockslide":2,"sandChainsCharged":1,"shieldBreak":15,"slash":52,"spGift":7,"summon":8,"telegraph":2,"truesilver":3},
"snaps": "39af4287 c1c868de fadf8f46 c55be858 66f9b2a4 3ff37c1b 45adfa96 4967058e cea41925 489c48a7 bccea42b 898a4b3c 6df25753 873fc61f 1619fcb8 7afad5a1 763db01d 46db0a43 f918b4af 75a431db" "snaps": "39af4287 c1c868de fadf8f46 c55be858 66f9b2a4 3ff37c1b 45adfa96 4967058e cea41925 489c48a7 bccea42b 898a4b3c 6df25753 873fc61f 1619fcb8 7afad5a1 763db01d 46db0a43 f918b4af 75a431db"
}, },
@@ -1339,7 +1339,7 @@
], ],
"enemies": {"enemy_10028_vtswd":[1,0,0,0,0],"enemy_1158_divman":[4,4,0,0,15206],"enemy_1160_hvyslr":[3,3,0,0,34560],"enemy_1161_tidmag":[3,3,0,0,20736]}, "enemies": {"enemy_10028_vtswd":[1,0,0,0,0],"enemy_1158_divman":[4,4,0,0,15206],"enemy_1160_hvyslr":[3,3,0,0,34560],"enemy_1161_tidmag":[3,3,0,0,20736]},
"hooks": [1,24,5100,13,13,48,136,293,10,14,8,8,7,11668,21,0,11,0,1,10,0,1,0], "hooks": [1,24,5100,13,13,48,136,293,10,14,8,8,7,11668,21,0,11,0,1,10,0,1,0],
"events": {"atk":15,"bounty":1,"deploy":13,"die":14,"dmg":60,"fx":58,"heal":124,"skill":16,"spawn":23,"status":65}, "events": {"atk":15,"bounty":1,"deploy":13,"die":14,"dmg":60,"engage":3,"fx":58,"heal":124,"skill":16,"spawn":23,"status":65},
"fx": {"bombard":7,"bombardShell":7,"burst":1,"lightning":15,"lock":7,"lure":1,"scorchBurst":4,"snow":10,"storm":1,"talent":1,"tide":4}, "fx": {"bombard":7,"bombardShell":7,"burst":1,"lightning":15,"lock":7,"lure":1,"scorchBurst":4,"snow":10,"storm":1,"talent":1,"tide":4},
"snaps": "d0a32c2d 14dc64f8 d9ffb8be 0e32f941 f5f88a89 f3e84df2 97783db6 49fb05e7 d960234b ef9c8339 304bd9e6 d7e7110a 8a0636b9 840cbf99 e365c522 1d462726 178ff4dc" "snaps": "d0a32c2d 14dc64f8 d9ffb8be 0e32f941 f5f88a89 f3e84df2 97783db6 49fb05e7 d960234b ef9c8339 304bd9e6 d7e7110a 8a0636b9 840cbf99 e365c522 1d462726 178ff4dc"
}, },
@@ -1391,7 +1391,7 @@
], ],
"enemies": {"enemy_10029_vtshld":[1,0,0,0,0],"enemy_1249_lysdb_2":[3,3,0,859,53748],"enemy_1251_lysyta":[4,4,0,0,32846],"enemy_1329_cbshld":[3,3,0,8987,89580]}, "enemies": {"enemy_10029_vtshld":[1,0,0,0,0],"enemy_1249_lysdb_2":[3,3,0,859,53748],"enemy_1251_lysyta":[4,4,0,0,32846],"enemy_1329_cbshld":[3,3,0,8987,89580]},
"hooks": [1,38,6900,174,174,313,539,506,11,16,9,7,0,19291,439,5,11,0,1,11,0,0,0], "hooks": [1,38,6900,174,174,313,539,506,11,16,9,7,0,19291,439,5,11,0,1,11,0,0,0],
"events": {"atk":274,"bounty":1,"deploy":23,"die":16,"dmg":310,"fx":164,"heal":199,"skill":16,"spawn":33,"status":89}, "events": {"atk":274,"bounty":1,"deploy":23,"die":16,"dmg":310,"engage":5,"fx":164,"heal":199,"skill":16,"spawn":33,"status":89},
"fx": {"blink":2,"copy":1,"drone":139,"droneLock":9,"drones":2,"garrisonGrant":1,"link":1,"radiantSword":1,"shieldBreak":3,"summon":1,"sunBurst":1,"talent":3}, "fx": {"blink":2,"copy":1,"drone":139,"droneLock":9,"drones":2,"garrisonGrant":1,"link":1,"radiantSword":1,"shieldBreak":3,"summon":1,"sunBurst":1,"talent":3},
"snaps": "c4705ca6 86063ce9 ea0c2b19 3be206f3 f6616193 bc8068bc b7787872 ccb11abe 5ee8f5fb d45e1507 a906e140 0eab9169 826d1e3c 03748c0a 656a7fc4 2b22fcba ee86d812 c238e284 cabebf47 115310da 7d546bc6 e010aac4 c2093c82" "snaps": "c4705ca6 86063ce9 ea0c2b19 3be206f3 f6616193 bc8068bc b7787872 ccb11abe 5ee8f5fb d45e1507 a906e140 0eab9169 826d1e3c 03748c0a 656a7fc4 2b22fcba ee86d812 c238e284 cabebf47 115310da 7d546bc6 e010aac4 c2093c82"
} }
+49 -49
View File
@@ -62,7 +62,7 @@
"enemy_9010_acpupp": [1,0,0,0,959] "enemy_9010_acpupp": [1,0,0,0,959]
}, },
"hooks": [1,50,2700,404,404,632,636,325,34,34,25,21,19,14421,65,36,31,0,1,35,0,0,0], "hooks": [1,50,2700,404,404,632,636,325,34,34,25,21,19,14421,65,36,31,0,1,35,0,0,0],
"events": {"atk":426,"bounty":4,"deploy":16,"die":34,"dmg":636,"fx":65,"heal":319,"skill":49,"spawn":47,"status":126}, "events": {"atk":426,"bounty":4,"deploy":16,"die":34,"dmg":636,"engage":12,"fx":65,"heal":319,"skill":49,"spawn":47,"status":126},
"fx": {"aoe":1,"bombard":9,"bombardShell":10,"counter":6,"crit":18,"dp":1,"ember":1,"explode":3,"heal":11,"liberate":1,"summon":1,"taunt":1,"telegraph":1,"zone":1}, "fx": {"aoe":1,"bombard":9,"bombardShell":10,"counter":6,"crit":18,"dp":1,"ember":1,"explode":3,"heal":11,"liberate":1,"summon":1,"taunt":1,"telegraph":1,"zone":1},
"snaps": "15544384 8f8ab114 83735bc4 1672a1a1 e9e7ffdb 9b9d0193 15df767d 792390ec 43783e3b" "snaps": "15544384 8f8ab114 83735bc4 1672a1a1 e9e7ffdb 9b9d0193 15df767d 792390ec 43783e3b"
}, },
@@ -119,7 +119,7 @@
"enemy_9011_acrefr": [1,0,0,0,2338] "enemy_9011_acrefr": [1,0,0,0,2338]
}, },
"hooks": [1,62,3300,343,343,389,401,3347,45,56,13,16,31,7389,39,11,44,11,1,46,0,0,0], "hooks": [1,62,3300,343,343,389,401,3347,45,56,13,16,31,7389,39,11,44,11,1,46,0,0,0],
"events": {"atk":361,"bounty":4,"deploy":18,"die":45,"dmg":401,"fx":46,"heal":15,"leak":11,"skill":32,"spawn":56,"status":69}, "events": {"atk":361,"bounty":4,"deploy":18,"die":45,"dmg":401,"engage":10,"fx":46,"heal":15,"leak":11,"skill":32,"spawn":56,"status":69},
"fx": {"aoe":4,"crit":20,"displace":8,"dp":2,"ember":1,"pulse":7,"revive":1,"summon":1,"taunt":2}, "fx": {"aoe":4,"crit":20,"displace":8,"dp":2,"ember":1,"pulse":7,"revive":1,"summon":1,"taunt":2},
"snaps": "7504f6da cf86e400 f495d65e e1e866ef 88f9cabd d57a8164 b1e49710 28ad6226 f40f3c34 62a776b0 2bec217d" "snaps": "7504f6da cf86e400 f495d65e e1e866ef 88f9cabd d57a8164 b1e49710 28ad6226 f40f3c34 62a776b0 2bec217d"
}, },
@@ -182,7 +182,7 @@
"enemy_9016_acstmr": [1,1,0,0,8] "enemy_9016_acstmr": [1,1,0,0,8]
}, },
"hooks": [1,104,3300,602,602,708,826,235,87,91,24,22,0,15820,206,28,85,0,1,94,1,0,0], "hooks": [1,104,3300,602,602,708,826,235,87,91,24,22,0,15820,206,28,85,0,1,94,1,0,0],
"events": {"atk":685,"bounty":7,"deploy":16,"die":91,"dmg":805,"fx":108,"heal":207,"layer":1,"skill":46,"spawn":101,"status":219}, "events": {"atk":685,"bounty":7,"deploy":16,"die":91,"dmg":805,"engage":10,"fx":108,"heal":207,"layer":1,"skill":46,"spawn":101,"status":219},
"fx": {"aoe":1,"blink":1,"crit":1,"displace":6,"ember":1,"explode":10,"garrison":1,"heal":6,"pulse":45,"reveal":1,"sleep":8,"summon":24,"undying":1,"zone":2}, "fx": {"aoe":1,"blink":1,"crit":1,"displace":6,"ember":1,"explode":10,"garrison":1,"heal":6,"pulse":45,"reveal":1,"sleep":8,"summon":24,"undying":1,"zone":2},
"snaps": "554dbc21 dca8028a bfe874b1 c3ea8fde b234e0fc e2e35296 0e5e8cfd 66160212 74b71698 3535e79e 56ba7f72" "snaps": "554dbc21 dca8028a bfe874b1 c3ea8fde b234e0fc e2e35296 0e5e8cfd 66160212 74b71698 3535e79e 56ba7f72"
}, },
@@ -241,7 +241,7 @@
"enemy_2009_csaudc": [1,0,1,0,490] "enemy_2009_csaudc": [1,0,1,0,490]
}, },
"hooks": [1,56,3296,431,431,1432,1451,182,23,50,17,17,0,10435,29,27,36,27,1,31,0,2,0], "hooks": [1,56,3296,431,431,1432,1451,182,23,50,17,17,0,10435,29,27,36,27,1,31,0,2,0],
"events": {"atk":493,"bounty":1,"deploy":17,"die":23,"dmg":1384,"fx":20,"heal":182,"leak":27,"skill":34,"spawn":52,"status":161}, "events": {"atk":493,"bounty":1,"deploy":17,"die":23,"dmg":1384,"engage":11,"fx":20,"heal":182,"leak":27,"skill":34,"spawn":52,"status":161},
"fx": {"aoe":4,"burst":2,"camouflage":2,"dodge":3,"dollEnd":1,"liberate":1,"overload":1,"shield":2,"shieldBreak":1,"substitute":1,"summon":1,"takeoff":1}, "fx": {"aoe":4,"burst":2,"camouflage":2,"dodge":3,"dollEnd":1,"liberate":1,"overload":1,"shield":2,"shieldBreak":1,"substitute":1,"summon":1,"takeoff":1},
"snaps": "f188aea7 a6afe0e2 586473b0 64a602b4 a28a4c01 c2e6f731 0fc56c2d 9e21f4c3 795ab8db 9dbb7b5b 6f022b9b" "snaps": "f188aea7 a6afe0e2 586473b0 64a602b4 a28a4c01 c2e6f731 0fc56c2d 9e21f4c3 795ab8db 9dbb7b5b 6f022b9b"
}, },
@@ -296,7 +296,7 @@
"enemy_2010_csdcr": [1,1,0,1414,9600] "enemy_2010_csdcr": [1,1,0,1414,9600]
}, },
"hooks": [1,102,4200,712,712,1693,1684,767,82,89,37,36,0,28924,152,52,84,1,1,82,0,0,0], "hooks": [1,102,4200,712,712,1693,1684,767,82,89,37,36,0,28924,152,52,84,1,1,82,0,0,0],
"events": {"atk":893,"bounty":5,"deploy":18,"die":88,"dmg":1628,"fx":69,"heal":143,"leak":1,"skill":73,"spawn":97,"status":437}, "events": {"atk":893,"bounty":5,"deploy":18,"die":88,"dmg":1628,"engage":7,"fx":69,"heal":143,"leak":1,"skill":73,"spawn":97,"status":437},
"fx": {"counter":1,"explode":3,"expose":14,"overload":2,"phase":1,"shield":8,"shieldBreak":16,"sleep":7,"summon":5,"telegraph":5,"zone":7}, "fx": {"counter":1,"explode":3,"expose":14,"overload":2,"phase":1,"shield":8,"shieldBreak":16,"sleep":7,"summon":5,"telegraph":5,"zone":7},
"snaps": "de0d6fce 6d6804e9 d7011be0 b72154cf 325314c7 efa5c8d0 ae45ceac e177f8b9 c1acb458 e7de2cbb de6e5396 c820655d de465eb1 281894a7" "snaps": "de0d6fce 6d6804e9 d7011be0 b72154cf 325314c7 efa5c8d0 ae45ceac e177f8b9 c1acb458 e7de2cbb de6e5396 c820655d de465eb1 281894a7"
}, },
@@ -353,7 +353,7 @@
"enemy_2017_csphts": [1,1,0,1838,9600] "enemy_2017_csphts": [1,1,0,1838,9600]
}, },
"hooks": [1,90,4200,372,372,658,640,177,77,77,22,22,0,20005,55,36,78,0,1,77,1,0,4], "hooks": [1,90,4200,372,372,658,640,177,77,77,22,22,0,20005,55,36,78,0,1,77,1,0,4],
"events": {"atk":383,"bounty":5,"deploy":12,"die":77,"dmg":640,"fx":33,"heal":115,"layer":1,"skill":46,"spawn":90,"status":35}, "events": {"atk":383,"bounty":5,"deploy":12,"die":77,"dmg":640,"engage":8,"fx":33,"heal":115,"layer":1,"skill":46,"spawn":90,"status":35},
"fx": {"aoe":2,"buff":1,"coin":3,"dodge":9,"explode":2,"garrison":1,"phase":2,"shieldBreak":1,"sp":2,"takeoff":3,"zone":7}, "fx": {"aoe":2,"buff":1,"coin":3,"dodge":9,"explode":2,"garrison":1,"phase":2,"shieldBreak":1,"sp":2,"takeoff":3,"zone":7},
"snaps": "7e2ad48a 6bd6d5ab b9d597a4 5d6f961e 1e4c04e2 525f85c6 9bf96317 42a8fa26 f1af39d5 66b9f6f3 bca33a7f 56cc754e 896ae641 1111820a" "snaps": "7e2ad48a 6bd6d5ab b9d597a4 5d6f961e 1e4c04e2 525f85c6 9bf96317 42a8fa26 f1af39d5 66b9f6f3 bca33a7f 56cc754e 896ae641 1111820a"
}, },
@@ -415,7 +415,7 @@
"enemy_2021_syfish": [1,0,1,0,0] "enemy_2021_syfish": [1,0,1,0,0]
}, },
"hooks": [1,195,4200,666,666,927,939,427,131,183,30,30,0,17741,21,47,171,52,1,141,0,0,0], "hooks": [1,195,4200,666,666,927,939,427,131,183,30,30,0,17741,21,47,171,52,1,141,0,0,0],
"events": {"atk":778,"bounty":1,"deploy":22,"die":131,"dmg":926,"fx":141,"heal":1,"leak":52,"skill":60,"spawn":186,"status":73}, "events": {"atk":778,"bounty":1,"deploy":22,"die":131,"dmg":926,"engage":13,"fx":141,"heal":1,"leak":52,"skill":60,"spawn":186,"status":73},
"fx": {"aoe":2,"camouflage":1,"displace":25,"dp":3,"explode":19,"expose":1,"pull":25,"sp":1,"strike":3,"summon":46,"volley":7,"wolfShadow":3,"wolfShadowLost":5}, "fx": {"aoe":2,"camouflage":1,"displace":25,"dp":3,"explode":19,"expose":1,"pull":25,"sp":1,"strike":3,"summon":46,"volley":7,"wolfShadow":3,"wolfShadowLost":5},
"snaps": "1e865453 8da2b91c 8ffcf382 5f3baf36 c084da7c c4a17fcb d60c9513 b462beff 05a1582e 8764853d 1fa503d0 eebfaa4c b2311536 b68273bd" "snaps": "1e865453 8da2b91c 8ffcf382 5f3baf36 c084da7c c4a17fcb d60c9513 b462beff 05a1582e 8764853d 1fa503d0 eebfaa4c b2311536 b68273bd"
}, },
@@ -483,7 +483,7 @@
"enemy_2025_syufo": [13,1,9,22069,61781] "enemy_2025_syufo": [13,1,9,22069,61781]
}, },
"hooks": [1,109,5100,476,476,825,898,341,54,104,18,22,0,6770,54,40,88,47,1,58,11,8,0], "hooks": [1,109,5100,476,476,825,898,341,54,104,18,22,0,6770,54,40,88,47,1,58,11,8,0],
"events": {"atk":526,"bounty":4,"deploy":21,"die":57,"dmg":897,"fx":98,"heal":340,"layer":11,"leak":47,"skill":44,"spawn":100,"status":84}, "events": {"atk":526,"bounty":4,"deploy":21,"die":57,"dmg":897,"engage":12,"fx":98,"heal":340,"layer":11,"leak":47,"skill":44,"spawn":100,"status":84},
"fx": {"blink":2,"buff":1,"burst":8,"coin":15,"displace":11,"explode":16,"garrison":6,"layer":5,"pull":11,"revive":4,"shield":1,"shieldBreak":1,"strike":8,"summon":1,"volley":8}, "fx": {"blink":2,"buff":1,"burst":8,"coin":15,"displace":11,"explode":16,"garrison":6,"layer":5,"pull":11,"revive":4,"shield":1,"shieldBreak":1,"strike":8,"summon":1,"volley":8},
"snaps": "a90ffeac d57612e4 e8d10948 88e75de1 e4c4862e 99ffec22 42d6df71 787805d3 6d700ba6 662099ea 641c7acd 8c28fc25 17c9a531 45c3a979 675372ca 8171c584 4bb0fa0e" "snaps": "a90ffeac d57612e4 e8d10948 88e75de1 e4c4862e 99ffec22 42d6df71 787805d3 6d700ba6 662099ea 641c7acd 8c28fc25 17c9a531 45c3a979 675372ca 8171c584 4bb0fa0e"
}, },
@@ -544,7 +544,7 @@
"enemy_2034_sythef": [1,1,0,0,8640] "enemy_2034_sythef": [1,1,0,0,8640]
}, },
"hooks": [1,187,4617,720,720,1321,1305,0,172,172,62,59,0,20248,103,11,171,0,1,175,0,0,0], "hooks": [1,187,4617,720,720,1321,1305,0,172,172,62,59,0,20248,103,11,171,0,1,175,0,0,0],
"events": {"atk":899,"bounty":6,"deploy":14,"die":172,"dmg":1304,"fx":171,"skill":121,"spawn":186,"status":209}, "events": {"atk":899,"bounty":6,"deploy":14,"die":172,"dmg":1304,"engage":10,"fx":171,"skill":121,"spawn":186,"status":209},
"fx": {"crit":20,"displace":30,"dp":3,"explode":3,"phase":1,"ripple":9,"shadowWeave":28,"shield":2,"shieldBreak":1,"stealth":3,"summon":51,"telegraph":3,"volley":9,"wolfShadow":4,"wolfShadowLost":3,"zone":1}, "fx": {"crit":20,"displace":30,"dp":3,"explode":3,"phase":1,"ripple":9,"shadowWeave":28,"shield":2,"shieldBreak":1,"stealth":3,"summon":51,"telegraph":3,"volley":9,"wolfShadow":4,"wolfShadowLost":3,"zone":1},
"snaps": "74efae3f f0ba5ed6 7f080293 ce089018 72fd5b40 aa7cfc03 9cd4911e 59794743 22e30ffb a647f35c 1fac8532 af13d758 f00ae8e0 3abb265c d6a41faf b70c8a95" "snaps": "74efae3f f0ba5ed6 7f080293 ce089018 72fd5b40 aa7cfc03 9cd4911e 59794743 22e30ffb a647f35c 1fac8532 af13d758 f00ae8e0 3abb265c d6a41faf b70c8a95"
}, },
@@ -607,7 +607,7 @@
"enemy_2034_sythef_2": [1,0,1,0,0] "enemy_2034_sythef_2": [1,0,1,0,0]
}, },
"hooks": [1,130,5100,419,419,1069,1051,388,57,119,20,21,0,7357,426,12,102,62,1,207,3,0,10], "hooks": [1,130,5100,419,419,1069,1051,388,57,119,20,21,0,7357,426,12,102,62,1,207,3,0,10],
"events": {"atk":485,"bounty":2,"deploy":25,"die":57,"dmg":959,"fx":147,"heal":4,"layer":3,"leak":62,"skill":42,"spawn":119,"status":274}, "events": {"atk":485,"bounty":2,"deploy":25,"die":57,"dmg":959,"engage":12,"fx":147,"heal":4,"layer":3,"leak":62,"skill":42,"spawn":119,"status":274},
"fx": {"block":8,"catShield":1,"charge":1,"dilemma":1,"dodge":10,"explode":18,"explosion":2,"featherArrow":1,"flame":1,"frostNova":4,"garrison":3,"garrisonGrant":1,"overclock":1,"pull":1,"riposte":2,"rockslide":2,"shieldBreak":5,"slippery":1,"summon":11,"swordRain":48,"swordStorm":5,"telegraph":1,"thorns":6,"tornado":1,"tornadoPulse":5,"undying":7}, "fx": {"block":8,"catShield":1,"charge":1,"dilemma":1,"dodge":10,"explode":18,"explosion":2,"featherArrow":1,"flame":1,"frostNova":4,"garrison":3,"garrisonGrant":1,"overclock":1,"pull":1,"riposte":2,"rockslide":2,"shieldBreak":5,"slippery":1,"summon":11,"swordRain":48,"swordStorm":5,"telegraph":1,"thorns":6,"tornado":1,"tornadoPulse":5,"undying":7},
"snaps": "57fe2e2f 4a53b7b8 2e0d662f c2473d0c d61f8291 0f879595 34e652a4 ea9b995f 19d258e5 019f5b39 2e2a3d4d 8b38907c 2927da2c 3ad7a87f ce8d9d20 0b381f42 180cd6d8" "snaps": "57fe2e2f 4a53b7b8 2e0d662f c2473d0c d61f8291 0f879595 34e652a4 ea9b995f 19d258e5 019f5b39 2e2a3d4d 8b38907c 2927da2c 3ad7a87f ce8d9d20 0b381f42 180cd6d8"
}, },
@@ -667,7 +667,7 @@
"enemy_9011_acrefr": [12,8,0,12883,318178] "enemy_9011_acrefr": [12,8,0,12883,318178]
}, },
"hooks": [1,101,5700,996,996,2496,2844,832,83,83,47,43,30,26684,566,37,80,0,1,84,2,0,0], "hooks": [1,101,5700,996,996,2496,2844,832,83,83,47,43,30,26684,566,37,80,0,1,84,2,0,0],
"events": {"atk":1888,"bounty":6,"deploy":19,"die":83,"dmg":2609,"fx":288,"heal":149,"layer":2,"skill":90,"spawn":94,"status":1332}, "events": {"atk":1888,"bounty":6,"deploy":19,"die":83,"dmg":2609,"engage":9,"fx":288,"heal":149,"layer":2,"skill":90,"spawn":94,"status":1332},
"fx": {"camouflage":2,"charge":19,"counter":30,"crit":32,"dilemma":4,"displace":33,"explode":1,"garrison":2,"healField":2,"jungleSoul":3,"mote":24,"quadShot":1,"redistribute":30,"revive":1,"reweave":2,"ripple":18,"sandChains":5,"sentry":1,"sermon":1,"shadowWeave":27,"shield":6,"slash":30,"stealth":5,"summon":1,"telegraph":6,"truesilver":2}, "fx": {"camouflage":2,"charge":19,"counter":30,"crit":32,"dilemma":4,"displace":33,"explode":1,"garrison":2,"healField":2,"jungleSoul":3,"mote":24,"quadShot":1,"redistribute":30,"revive":1,"reweave":2,"ripple":18,"sandChains":5,"sentry":1,"sermon":1,"shadowWeave":27,"shield":6,"slash":30,"stealth":5,"summon":1,"telegraph":6,"truesilver":2},
"snaps": "c002e613 10a048e4 83882719 48595aaf c4331395 cfece443 01fb830e 82f827f8 d791c555 fec11375 d25c1e59 b986924f 415cc8f6 cc8bf2ac 1aec718d d2106326 d7cd1fea 2093bc04 facf1e90" "snaps": "c002e613 10a048e4 83882719 48595aaf c4331395 cfece443 01fb830e 82f827f8 d791c555 fec11375 d25c1e59 b986924f 415cc8f6 cc8bf2ac 1aec718d d2106326 d7cd1fea 2093bc04 facf1e90"
}, },
@@ -730,7 +730,7 @@
"enemy_2050_smsha": [1,1,0,2185,46080] "enemy_2050_smsha": [1,1,0,2185,46080]
}, },
"hooks": [1,121,4854,470,470,1388,1387,830,102,108,85,83,0,16257,1775,20,100,3,1,112,1,0,3], "hooks": [1,121,4854,470,470,1388,1387,830,102,108,85,83,0,16257,1775,20,100,3,1,112,1,0,3],
"events": {"atk":628,"bounty":5,"deploy":18,"die":105,"dmg":1379,"fx":236,"heal":222,"layer":1,"leak":3,"skill":170,"spawn":117,"status":878}, "events": {"atk":628,"bounty":5,"deploy":18,"die":105,"dmg":1379,"engage":10,"fx":236,"heal":222,"layer":1,"leak":3,"skill":170,"spawn":117,"status":878},
"fx": {"beam":6,"blink":2,"block":1,"displace":10,"dodge":3,"explode":4,"explosion":23,"featherArrow":6,"flame":3,"frostNova":37,"garrison":1,"garrisonGrant":1,"healField":2,"overclock":2,"palsy":1,"pull":2,"revive":1,"riposte":3,"rockslide":6,"shieldBreak":8,"slash":19,"summon":1,"swordStorm":1,"telegraph":3,"thorns":6,"tornado":2,"tornadoPulse":10,"truesilver":3,"zone":69}, "fx": {"beam":6,"blink":2,"block":1,"displace":10,"dodge":3,"explode":4,"explosion":23,"featherArrow":6,"flame":3,"frostNova":37,"garrison":1,"garrisonGrant":1,"healField":2,"overclock":2,"palsy":1,"pull":2,"revive":1,"riposte":3,"rockslide":6,"shieldBreak":8,"slash":19,"summon":1,"swordStorm":1,"telegraph":3,"thorns":6,"tornado":2,"tornadoPulse":10,"truesilver":3,"zone":69},
"snaps": "273dcc8e c323f708 7c1d7d8a 0f1c8a5d fa1faa7c 5973c4b4 1abb6f51 fc254e01 5e27fbd7 4c75c9e8 3d3d371b d21abedf 1215afdd c03925aa 45aadc00 eecaaba5 03eee058" "snaps": "273dcc8e c323f708 7c1d7d8a 0f1c8a5d fa1faa7c 5973c4b4 1abb6f51 fc254e01 5e27fbd7 4c75c9e8 3d3d371b d21abedf 1215afdd c03925aa 45aadc00 eecaaba5 03eee058"
}, },
@@ -801,7 +801,7 @@
"enemy_5601_entlec": [7,7,0,0,81403] "enemy_5601_entlec": [7,7,0,0,81403]
}, },
"hooks": [1,126,5700,593,593,1848,2658,17845,100,114,25,21,16,24262,248,23,111,12,1,185,29,2,0], "hooks": [1,126,5700,593,593,1848,2658,17845,100,114,25,21,16,24262,248,23,111,12,1,185,29,2,0],
"events": {"atk":1052,"bounty":3,"deploy":15,"die":102,"dmg":2093,"fx":286,"heal":267,"layer":29,"leak":12,"skill":46,"spawn":123,"status":721}, "events": {"atk":1052,"bounty":3,"deploy":15,"die":102,"dmg":2093,"engage":8,"fx":286,"heal":267,"layer":29,"leak":12,"skill":46,"spawn":123,"status":721},
"fx": {"anchor":3,"aoe":2,"bandage":2,"burst":2,"candle":7,"downed":2,"ember":2,"explode":1,"garrison":29,"garrisonGrant":1,"jungleSoul":2,"mote":96,"overload":2,"redistribute":38,"revive":1,"reweave":3,"sandChains":2,"sandChainsCharged":2,"spGift":35,"splash":16,"summon":1,"wake":37}, "fx": {"anchor":3,"aoe":2,"bandage":2,"burst":2,"candle":7,"downed":2,"ember":2,"explode":1,"garrison":29,"garrisonGrant":1,"jungleSoul":2,"mote":96,"overload":2,"redistribute":38,"revive":1,"reweave":3,"sandChains":2,"sandChainsCharged":2,"spGift":35,"splash":16,"summon":1,"wake":37},
"snaps": "6c317ca9 7f45f40d dad79f3d e90c8987 002195fc 2da215da 1d494876 e7b6c5b2 d1226386 4ee87a1d fa35734a 5d6d473f e88eabc4 99f28a48 c9d792a9 4eb8e136 5529c074 daac4c5a 05eacc7a" "snaps": "6c317ca9 7f45f40d dad79f3d e90c8987 002195fc 2da215da 1d494876 e7b6c5b2 d1226386 4ee87a1d fa35734a 5d6d473f e88eabc4 99f28a48 c9d792a9 4eb8e136 5529c074 daac4c5a 05eacc7a"
}, },
@@ -862,7 +862,7 @@
"enemy_2085_skzjxd": [1,0,1,0,0] "enemy_2085_skzjxd": [1,0,1,0,0]
}, },
"hooks": [1,216,6054,531,556,1900,1900,2691,50,212,62,62,11,13131,404,65,183,161,1,66,1,5,22], "hooks": [1,216,6054,531,556,1900,1900,2691,50,212,62,62,11,13131,404,65,183,161,1,66,1,5,22],
"events": {"atk":815,"bounty":1,"deploy":30,"die":51,"dmg":1898,"fx":182,"heal":70,"layer":1,"leak":161,"skill":124,"spawn":199,"status":686}, "events": {"atk":815,"bounty":1,"deploy":30,"die":51,"dmg":1898,"engage":12,"fx":182,"heal":70,"layer":1,"leak":161,"skill":124,"spawn":199,"status":686},
"fx": {"aoe":13,"burst":5,"charge":1,"crit":18,"dodge":22,"dollEnd":1,"extraAttack":6,"garrison":1,"healAoe":2,"iceSpike":31,"shieldBreak":42,"sp":17,"substitute":2,"summon":1,"swap":1,"weightless":1,"zone":18}, "fx": {"aoe":13,"burst":5,"charge":1,"crit":18,"dodge":22,"dollEnd":1,"extraAttack":6,"garrison":1,"healAoe":2,"iceSpike":31,"shieldBreak":42,"sp":17,"substitute":2,"summon":1,"swap":1,"weightless":1,"zone":18},
"snaps": "68e6dd3e 63da606a 50932fd2 c9d08aea 1b0529cf 4ee3f028 9481f888 e5c1490f 3b723a57 4d1edeed 22285b31 ebd626d7 b4e23d9d f84895d6 9d224ac1 2fcc6d1c 5124a24a 483d6958 d5a7245d f8f40b5f 3c97ac78" "snaps": "68e6dd3e 63da606a 50932fd2 c9d08aea 1b0529cf 4ee3f028 9481f888 e5c1490f 3b723a57 4d1edeed 22285b31 ebd626d7 b4e23d9d f84895d6 9d224ac1 2fcc6d1c 5124a24a 483d6958 d5a7245d f8f40b5f 3c97ac78"
}, },
@@ -942,7 +942,7 @@
"enemy_9006_actoxi": [30,30,0,8926,699840] "enemy_9006_actoxi": [30,30,0,8926,699840]
}, },
"hooks": [1,139,5504,359,359,2190,3119,2138,109,119,154,152,0,15398,4871,48,108,0,1,301,2,0,0], "hooks": [1,139,5504,359,359,2190,3119,2138,109,119,154,152,0,15398,4871,48,108,0,1,301,2,0,0],
"events": {"atk":407,"bounty":6,"deploy":28,"die":119,"dmg":2200,"fx":441,"heal":1928,"layer":2,"skill":306,"spawn":128,"status":684}, "events": {"atk":407,"bounty":6,"deploy":28,"die":119,"dmg":2200,"engage":11,"fx":441,"heal":1928,"layer":2,"skill":306,"spawn":128,"status":684},
"fx": {"anchor":5,"aoe":49,"candle":1,"ember":4,"garrison":2,"healAoe":78,"overload":3,"revive":4,"sp":101,"substitute":1,"summon":3,"swap":1,"telegraph":1,"teleport":7,"zone":181}, "fx": {"anchor":5,"aoe":49,"candle":1,"ember":4,"garrison":2,"healAoe":78,"overload":3,"revive":4,"sp":101,"substitute":1,"summon":3,"swap":1,"telegraph":1,"teleport":7,"zone":181},
"snaps": "798d3b9e 2b34ab7d 8be8f821 c3698188 979eb9f1 903039ba ed4b9fbc 40a31c8a ebd507cf b18857df 989c8b31 002c7544 b6985bf3 d204fd2e 7dd78575 a543964b 84af0388 363f0d65 7410a619" "snaps": "798d3b9e 2b34ab7d 8be8f821 c3698188 979eb9f1 903039ba ed4b9fbc 40a31c8a ebd507cf b18857df 989c8b31 002c7544 b6985bf3 d204fd2e 7dd78575 a543964b 84af0388 363f0d65 7410a619"
}, },
@@ -1004,7 +1004,7 @@
"enemy_9006_actoxi": [1,0,1,0,1334] "enemy_9006_actoxi": [1,0,1,0,1334]
}, },
"hooks": [1,151,6900,1039,1098,8702,9061,75616,84,136,42,37,17,37922,1075,49,126,48,1,98,1,19,6], "hooks": [1,151,6900,1039,1098,8702,9061,75616,84,136,42,37,17,37922,1075,49,126,48,1,98,1,19,6],
"events": {"atk":2514,"bounty":3,"deploy":25,"die":88,"dmg":9001,"fx":539,"heal":215,"layer":1,"leak":48,"skill":79,"spawn":143,"status":2462}, "events": {"atk":2514,"bounty":3,"deploy":25,"die":88,"dmg":9001,"engage":10,"fx":539,"heal":215,"layer":1,"leak":48,"skill":79,"spawn":143,"status":2462},
"fx": {"aoe":8,"blessing":1,"bombard":17,"bombardShell":17,"burst":19,"crit":46,"dodge":6,"explode":20,"firewall":3,"garrison":1,"iceSpike":90,"lightning":145,"lock":17,"lure":3,"revive":1,"snow":82,"soul":1,"sp":3,"storm":5,"talent":2,"telegraph":6,"tide":3,"wanted":39,"weightless":4}, "fx": {"aoe":8,"blessing":1,"bombard":17,"bombardShell":17,"burst":19,"crit":46,"dodge":6,"explode":20,"firewall":3,"garrison":1,"iceSpike":90,"lightning":145,"lock":17,"lure":3,"revive":1,"snow":82,"soul":1,"sp":3,"storm":5,"talent":2,"telegraph":6,"tide":3,"wanted":39,"weightless":4},
"snaps": "ee6304b1 a0a8db7b 068a3ee5 8ee7bd29 88542758 3332f494 ae7dd280 3f1096b4 1941c90e 8e911459 33fb0edd da20bfa6 72ea363d 84663ce3 d02717dc 820cc8be d04c4865 6b92a4d6 c868bded 468439dd e830528e 160601bb cc9b19e4" "snaps": "ee6304b1 a0a8db7b 068a3ee5 8ee7bd29 88542758 3332f494 ae7dd280 3f1096b4 1941c90e 8e911459 33fb0edd da20bfa6 72ea363d 84663ce3 d02717dc 820cc8be d04c4865 6b92a4d6 c868bded 468439dd e830528e 160601bb cc9b19e4"
}, },
@@ -1075,7 +1075,7 @@
"enemy_9007_acelem": [1,1,0,150,34560] "enemy_9007_acelem": [1,1,0,150,34560]
}, },
"hooks": [1,196,6067,801,801,1567,1882,330,136,195,29,29,80,25530,1195,134,174,58,1,182,3,2,0], "hooks": [1,196,6067,801,801,1567,1882,330,136,195,29,29,80,25530,1195,134,174,58,1,182,3,2,0],
"events": {"atk":856,"bounty":6,"deploy":22,"die":137,"dmg":1743,"fx":278,"heal":149,"layer":3,"leak":58,"skill":58,"spawn":187,"status":844}, "events": {"atk":856,"bounty":6,"deploy":22,"die":137,"dmg":1743,"engage":11,"fx":278,"heal":149,"layer":3,"leak":58,"skill":58,"spawn":187,"status":844},
"fx": {"airstrike":20,"beam":2,"burst":2,"displace":5,"dp":2,"drone":111,"droneLock":50,"drones":2,"emergency":3,"explode":16,"finale":2,"garrison":3,"link":2,"scorchBurst":34,"slash":12,"steal":2,"summon":2,"sunBurst":2,"talent":3,"undying":3}, "fx": {"airstrike":20,"beam":2,"burst":2,"displace":5,"dp":2,"drone":111,"droneLock":50,"drones":2,"emergency":3,"explode":16,"finale":2,"garrison":3,"link":2,"scorchBurst":34,"slash":12,"steal":2,"summon":2,"sunBurst":2,"talent":3,"undying":3},
"snaps": "8cc6173f 78a2a3f2 980bca21 b3288370 06ae8f2a 8bad204c 84b97b8f 7f51ac7a 26ee6099 7389cc55 a87ec8ab 4186a251 c48ea3af 787853e8 5e7df826 9196a07e b960104e 24a29bb9 6f13a14b 44d34e3e c29968e9" "snaps": "8cc6173f 78a2a3f2 980bca21 b3288370 06ae8f2a 8bad204c 84b97b8f 7f51ac7a 26ee6099 7389cc55 a87ec8ab 4186a251 c48ea3af 787853e8 5e7df826 9196a07e b960104e 24a29bb9 6f13a14b 44d34e3e c29968e9"
}, },
@@ -1158,7 +1158,7 @@
"enemy_9008_acbunn": [1,1,0,2434,19907] "enemy_9008_acbunn": [1,1,0,2434,19907]
}, },
"hooks": [1,146,5863,534,534,1311,1464,508,125,132,39,34,35,25836,360,5,122,0,1,125,11,3,0], "hooks": [1,146,5863,534,534,1311,1464,508,125,132,39,34,35,25836,360,5,122,0,1,125,11,3,0],
"events": {"atk":625,"bounty":6,"deploy":22,"die":132,"dmg":1383,"fx":266,"heal":235,"layer":11,"skill":73,"spawn":146,"status":411}, "events": {"atk":625,"bounty":6,"deploy":22,"die":132,"dmg":1383,"engage":13,"fx":266,"heal":235,"layer":11,"skill":73,"spawn":146,"status":411},
"fx": {"beam":4,"bombard":35,"bombardShell":35,"burst":3,"camouflage":2,"copy":1,"dp":5,"garrison":8,"lightning":84,"link":1,"lock":35,"lure":2,"scorchBurst":22,"snow":7,"split":5,"storm":7,"summon":2,"talent":1,"tide":5,"wanted":2}, "fx": {"beam":4,"bombard":35,"bombardShell":35,"burst":3,"camouflage":2,"copy":1,"dp":5,"garrison":8,"lightning":84,"link":1,"lock":35,"lure":2,"scorchBurst":22,"snow":7,"split":5,"storm":7,"summon":2,"talent":1,"tide":5,"wanted":2},
"snaps": "5acdc7df c55424c0 c6c7faaa 141f61c9 87b42903 b9f9f4bb bfe3c39f ff01f7c3 88dde5ed e1cffdea 1e6c86b5 b346636f 3411307f 72468689 58b5c601 2a20a3fa 91883493 3699c0bc 8385ea32 05aedbfa" "snaps": "5acdc7df c55424c0 c6c7faaa 141f61c9 87b42903 b9f9f4bb bfe3c39f ff01f7c3 88dde5ed e1cffdea 1e6c86b5 b346636f 3411307f 72468689 58b5c601 2a20a3fa 91883493 3699c0bc 8385ea32 05aedbfa"
}, },
@@ -1229,7 +1229,7 @@
"enemy_9009_acfort": [1,1,0,4299,68797] "enemy_9009_acfort": [1,1,0,4299,68797]
}, },
"hooks": [1,125,5969,895,895,2816,3200,677,109,114,29,26,90,21712,972,84,108,0,1,109,2,5,0], "hooks": [1,125,5969,895,895,2816,3200,677,109,114,29,26,90,21712,972,84,108,0,1,109,2,5,0],
"events": {"atk":1265,"bounty":6,"deploy":17,"die":114,"dmg":2997,"fx":541,"heal":512,"layer":2,"skill":55,"spawn":123,"status":429}, "events": {"atk":1265,"bounty":6,"deploy":17,"die":114,"dmg":2997,"engage":10,"fx":541,"heal":512,"layer":2,"skill":55,"spawn":123,"status":429},
"fx": {"airstrike":25,"aoe":7,"beam":23,"blink":2,"bombard":8,"bombardShell":8,"burst":5,"cleanse":3,"displace":9,"drone":332,"droneLock":57,"drones":3,"emergency":1,"extraAttack":4,"finale":5,"garrison":2,"garrisonGrant":1,"palsy":1,"radiantSword":3,"slash":32,"steal":3,"summon":3,"sunBurst":1,"talent":3}, "fx": {"airstrike":25,"aoe":7,"beam":23,"blink":2,"bombard":8,"bombardShell":8,"burst":5,"cleanse":3,"displace":9,"drone":332,"droneLock":57,"drones":3,"emergency":1,"extraAttack":4,"finale":5,"garrison":2,"garrisonGrant":1,"palsy":1,"radiantSword":3,"slash":32,"steal":3,"summon":3,"sunBurst":1,"talent":3},
"snaps": "381e890f 760dfa5d 93a0f72e 50b58e62 8e3a14af 0f2d2d9c 74612304 48bb381d ffbdca65 03dd2d3c d57b56b8 672398fc 57b8e194 600fba22 465c1e97 492e16d5 0bf0b101 14c19f15 612bfa83 828005d2" "snaps": "381e890f 760dfa5d 93a0f72e 50b58e62 8e3a14af 0f2d2d9c 74612304 48bb381d ffbdca65 03dd2d3c d57b56b8 672398fc 57b8e194 600fba22 465c1e97 492e16d5 0bf0b101 14c19f15 612bfa83 828005d2"
}, },
@@ -1282,7 +1282,7 @@
"enemy_2005_axetro": [1,0,0,11649,0] "enemy_2005_axetro": [1,0,0,11649,0]
}, },
"hooks": [1,57,2700,557,556,691,741,5576,34,37,26,24,6,12712,51,37,43,3,1,34,0,2,0], "hooks": [1,57,2700,557,556,691,741,5576,34,37,26,24,6,12712,51,37,43,3,1,34,0,2,0],
"events": {"atk":620,"bounty":3,"deploy":12,"die":34,"dmg":725,"fx":44,"heal":55,"leak":3,"skill":50,"spawn":57,"status":146}, "events": {"atk":620,"bounty":3,"deploy":12,"die":34,"dmg":725,"engage":12,"fx":44,"heal":55,"leak":3,"skill":50,"spawn":57,"status":146},
"fx": {"buff":1,"burst":2,"crit":25,"dp":2,"explode":10,"summon":2,"telegraph":2}, "fx": {"buff":1,"burst":2,"crit":25,"dp":2,"explode":10,"summon":2,"telegraph":2},
"snaps": "07710787 1b3ca690 e36a30ad 9a49b76a b9d22a50 0c289ee8 092e59ea 62398dd1 226b2339" "snaps": "07710787 1b3ca690 e36a30ad 9a49b76a b9d22a50 0c289ee8 092e59ea 62398dd1 226b2339"
}, },
@@ -1336,7 +1336,7 @@
"enemy_2085_skzjxd_2": [1,1,0,0,11700] "enemy_2085_skzjxd_2": [1,1,0,0,11700]
}, },
"hooks": [1,45,2942,615,615,727,734,928,32,33,49,47,32,9649,110,27,29,0,1,32,0,0,0], "hooks": [1,45,2942,615,615,727,734,928,32,33,49,47,32,9649,110,27,29,0,1,32,0,0,0],
"events": {"atk":688,"bounty":4,"deploy":13,"die":33,"dmg":726,"fx":41,"heal":32,"skill":96,"spawn":44,"status":256}, "events": {"atk":688,"bounty":4,"deploy":13,"die":33,"dmg":726,"engage":12,"fx":41,"heal":32,"skill":96,"spawn":44,"status":256},
"fx": {"crit":26,"dp":3,"explode":1,"liberate":6,"shield":4,"telegraph":1}, "fx": {"crit":26,"dp":3,"explode":1,"liberate":6,"shield":4,"telegraph":1},
"snaps": "08475251 060e1404 a77d3fa6 74ab94fb 39ec40a8 4a899aaf 8a49b1ba b7ea4461 d110804c df9fa85e" "snaps": "08475251 060e1404 a77d3fa6 74ab94fb 39ec40a8 4a899aaf 8a49b1ba b7ea4461 d110804c df9fa85e"
}, },
@@ -1388,7 +1388,7 @@
"enemy_2002_bearmi": [1,0,1,9048,2918] "enemy_2002_bearmi": [1,0,1,9048,2918]
}, },
"hooks": [1,43,3300,363,363,888,880,767,22,33,31,31,0,12925,98,30,28,11,1,22,0,0,0], "hooks": [1,43,3300,363,363,888,880,767,22,33,31,31,0,12925,98,30,28,11,1,22,0,0,0],
"events": {"atk":382,"bounty":2,"deploy":13,"die":22,"dmg":880,"fx":26,"heal":463,"leak":11,"skill":62,"spawn":42,"status":202}, "events": {"atk":382,"bounty":2,"deploy":13,"die":22,"dmg":880,"engage":10,"fx":26,"heal":463,"leak":11,"skill":62,"spawn":42,"status":202},
"fx": {"buff":1,"crit":1,"liberate":4,"shield":16,"zone":4}, "fx": {"buff":1,"crit":1,"liberate":4,"shield":16,"zone":4},
"snaps": "9001a773 d682bb23 8fbcd651 5df53986 07e3ac0f 0f7aa6b9 5acdf417 61e10070 400a77ca 652541a4 8bc87622" "snaps": "9001a773 d682bb23 8fbcd651 5df53986 07e3ac0f 0f7aa6b9 5acdf417 61e10070 400a77ca 652541a4 8bc87622"
}, },
@@ -1443,7 +1443,7 @@
"enemy_2048_smgrd": [1,0,0,8461,2244] "enemy_2048_smgrd": [1,0,0,8461,2244]
}, },
"hooks": [1,52,3300,419,419,917,1086,360,32,32,20,17,0,11570,67,46,36,0,1,32,0,4,0], "hooks": [1,52,3300,419,419,917,1086,360,32,32,20,17,0,11570,67,46,36,0,1,32,0,4,0],
"events": {"atk":590,"bounty":3,"deploy":13,"die":32,"dmg":969,"fx":58,"heal":342,"skill":37,"spawn":51,"status":290}, "events": {"atk":590,"bounty":3,"deploy":13,"die":32,"dmg":969,"engage":10,"fx":58,"heal":342,"skill":37,"spawn":51,"status":290},
"fx": {"beam":44,"buff":3,"burst":4,"explode":1,"summon":1,"takeoff":1,"telegraph":4}, "fx": {"beam":44,"buff":3,"burst":4,"explode":1,"summon":1,"takeoff":1,"telegraph":4},
"snaps": "dd24c30b 998fef35 660771ba c69dbe74 3ffd6844 5f694b3c aac3f1c1 1533a52a ac7cca8b ccd35e50 5b498d3e" "snaps": "dd24c30b 998fef35 660771ba c69dbe74 3ffd6844 5f694b3c aac3f1c1 1533a52a ac7cca8b ccd35e50 5b498d3e"
}, },
@@ -1497,7 +1497,7 @@
"enemy_1535_wlfmster": [1,0,1,8843,18291] "enemy_1535_wlfmster": [1,0,1,8843,18291]
}, },
"hooks": [1,200,4200,794,794,1045,1112,2413,152,187,68,66,0,13905,1117,53,188,35,1,161,0,0,0], "hooks": [1,200,4200,794,794,1045,1112,2413,152,187,68,66,0,13905,1117,53,188,35,1,161,0,0,0],
"events": {"atk":928,"bounty":3,"deploy":12,"die":152,"dmg":1066,"fx":77,"heal":172,"leak":35,"skill":134,"spawn":200,"status":544}, "events": {"atk":928,"bounty":3,"deploy":12,"die":152,"dmg":1066,"engage":8,"fx":77,"heal":172,"leak":35,"skill":134,"spawn":200,"status":544},
"fx": {"beam":3,"buff":5,"liberate":1,"summon":56,"undying":1,"zone":11}, "fx": {"beam":3,"buff":5,"liberate":1,"summon":56,"undying":1,"zone":11},
"snaps": "07ba544c 84f4ccbf 38166938 b925fca7 945f4b86 2aa6642d 96e01f04 a3185a51 b77953b3 fa7afb72 6d5be7ad a030c96f 78dbd4b6 8d3af5e8" "snaps": "07ba544c 84f4ccbf 38166938 b925fca7 945f4b86 2aa6642d 96e01f04 a3185a51 b77953b3 fa7afb72 6d5be7ad a030c96f 78dbd4b6 8d3af5e8"
}, },
@@ -1553,7 +1553,7 @@
"enemy_2021_syfish": [1,1,0,0,4608] "enemy_2021_syfish": [1,1,0,0,4608]
}, },
"hooks": [1,81,4200,610,610,981,984,390,67,67,98,95,0,5824,750,71,66,0,1,124,1,0,0], "hooks": [1,81,4200,610,610,981,984,390,67,67,98,95,0,5824,750,71,66,0,1,124,1,0,0],
"events": {"atk":626,"bounty":3,"deploy":12,"die":67,"dmg":984,"fx":172,"heal":331,"layer":1,"skill":195,"spawn":81,"status":233}, "events": {"atk":626,"bounty":3,"deploy":12,"die":67,"dmg":984,"engage":9,"fx":172,"heal":331,"layer":1,"skill":195,"spawn":81,"status":233},
"fx": {"block":1,"buff":1,"coin":2,"ember":57,"garrison":1,"sp":71,"summon":1,"takeoff":3,"zone":35}, "fx": {"block":1,"buff":1,"coin":2,"ember":57,"garrison":1,"sp":71,"summon":1,"takeoff":3,"zone":35},
"snaps": "a16b87b8 7f2721ce a438ac49 e52df11e 882d7026 bc1e5811 1dfd72d4 90a6dd65 80ab29f4 1b3f315f c5e5d061 a8e8097d ffc02f6e 481fc792" "snaps": "a16b87b8 7f2721ce a438ac49 e52df11e 882d7026 bc1e5811 1dfd72d4 90a6dd65 80ab29f4 1b3f315f c5e5d061 a8e8097d ffc02f6e 481fc792"
}, },
@@ -1610,7 +1610,7 @@
"enemy_9010_acpupp": [1,0,1,0,1540] "enemy_9010_acpupp": [1,0,1,0,1540]
}, },
"hooks": [1,98,4200,342,342,1398,1472,9,37,91,26,26,0,8063,24,39,73,53,1,42,0,0,0], "hooks": [1,98,4200,342,342,1398,1472,9,37,91,26,26,0,8063,24,39,73,53,1,42,0,0,0],
"events": {"atk":418,"bounty":3,"deploy":22,"die":38,"dmg":1471,"fx":19,"heal":9,"leak":53,"skill":52,"spawn":90,"status":54}, "events": {"atk":418,"bounty":3,"deploy":22,"die":38,"dmg":1471,"engage":14,"fx":19,"heal":9,"leak":53,"skill":52,"spawn":90,"status":54},
"fx": {"dp":3,"explode":3,"liberate":1,"palsy":1,"revive":4,"shield":1,"sp":1,"strike":1,"summon":2,"telegraph":2}, "fx": {"dp":3,"explode":3,"liberate":1,"palsy":1,"revive":4,"shield":1,"sp":1,"strike":1,"summon":2,"telegraph":2},
"snaps": "b8ad25ab 03f51213 d7dcd813 5906dc4f 89b1c824 3e67ab1e f003cf8b dc2caccb 6a54c87b 77160756 042f6532 9bdc15a6 3da4b3fe 5314774a" "snaps": "b8ad25ab 03f51213 d7dcd813 5906dc4f 89b1c824 3e67ab1e f003cf8b dc2caccb 6a54c87b 77160756 042f6532 9bdc15a6 3da4b3fe 5314774a"
}, },
@@ -1663,7 +1663,7 @@
"enemy_9006_actoxi": [12,5,5,5143,135278] "enemy_9006_actoxi": [12,5,5,5143,135278]
}, },
"hooks": [1,67,5100,540,540,1934,1934,30,38,61,108,110,0,7239,40,30,53,21,1,40,3,0,0], "hooks": [1,67,5100,540,540,1934,1934,30,38,61,108,110,0,7239,40,30,53,21,1,40,3,0,0],
"events": {"atk":747,"bounty":2,"deploy":14,"die":40,"dmg":1863,"fx":108,"heal":30,"layer":3,"leak":21,"skill":220,"spawn":65,"status":187}, "events": {"atk":747,"bounty":2,"deploy":14,"die":40,"dmg":1863,"engage":12,"fx":108,"heal":30,"layer":3,"leak":21,"skill":220,"spawn":65,"status":187},
"fx": {"blink":1,"buff":1,"coin":18,"explode":26,"garrison":3,"revive":2,"shield":19,"shieldBreak":1,"strike":10,"volley":7,"zone":20}, "fx": {"blink":1,"buff":1,"coin":18,"explode":26,"garrison":3,"revive":2,"shield":19,"shieldBreak":1,"strike":10,"volley":7,"zone":20},
"snaps": "757da7ba 23c3e301 44393780 f617c160 eeedb2b3 6f6317df 3ad26b55 5bd556aa a29f3948 771a5093 f9fd3ae6 9b2e6d4b 873809bc 8d2b5f1a 506cb112 f0f0a521 c25d712d" "snaps": "757da7ba 23c3e301 44393780 f617c160 eeedb2b3 6f6317df 3ad26b55 5bd556aa a29f3948 771a5093 f9fd3ae6 9b2e6d4b 873809bc 8d2b5f1a 506cb112 f0f0a521 c25d712d"
}, },
@@ -1715,7 +1715,7 @@
"enemy_9006_actoxi": [1,0,1,554,712] "enemy_9006_actoxi": [1,0,1,554,712]
}, },
"hooks": [1,89,5100,401,401,506,522,30,30,86,28,28,0,4426,16,26,62,56,1,33,0,0,18], "hooks": [1,89,5100,401,401,506,522,30,30,86,28,28,0,4426,16,26,62,56,1,33,0,0,18],
"events": {"atk":427,"deploy":27,"die":30,"dmg":498,"fx":84,"leak":56,"skill":56,"spawn":75,"status":77}, "events": {"atk":427,"deploy":27,"die":30,"dmg":498,"engage":11,"fx":84,"leak":56,"skill":56,"spawn":75,"status":77},
"fx": {"camouflage":1,"crit":6,"dodge":18,"dp":4,"phase":1,"pulse":17,"sentry":9,"shadowWeave":9,"shield":4,"shieldBreak":2,"summon":2,"telegraph":1,"volley":6,"wolfShadowLost":3,"zone":1}, "fx": {"camouflage":1,"crit":6,"dodge":18,"dp":4,"phase":1,"pulse":17,"sentry":9,"shadowWeave":9,"shield":4,"shieldBreak":2,"summon":2,"telegraph":1,"volley":6,"wolfShadowLost":3,"zone":1},
"snaps": "7c092213 7eeed0b1 ac8f2ed3 3858c149 e8ea91a7 8453351a 4fc7b210 32f63df8 4e74f555 5bfbe308 7746a517 c9e84d55 480b39df 046d3213 b82f6b7a f22d156a 686086b5" "snaps": "7c092213 7eeed0b1 ac8f2ed3 3858c149 e8ea91a7 8453351a 4fc7b210 32f63df8 4e74f555 5bfbe308 7746a517 c9e84d55 480b39df 046d3213 b82f6b7a f22d156a 686086b5"
}, },
@@ -1771,7 +1771,7 @@
"enemy_2002_bearmi_2": [1,1,0,1311,8800] "enemy_2002_bearmi_2": [1,1,0,1311,8800]
}, },
"hooks": [1,106,4641,470,470,920,1638,1314,82,91,48,48,0,22203,805,35,85,9,1,82,1,4,1], "hooks": [1,106,4641,470,470,920,1638,1314,82,91,48,48,0,22203,805,35,85,9,1,82,1,4,1],
"events": {"atk":508,"bounty":2,"deploy":19,"die":82,"dmg":1628,"fx":45,"heal":418,"layer":1,"leak":9,"skill":99,"spawn":101,"status":149}, "events": {"atk":508,"bounty":2,"deploy":19,"die":82,"dmg":1628,"engage":10,"fx":45,"heal":418,"layer":1,"leak":9,"skill":99,"spawn":101,"status":149},
"fx": {"block":11,"burst":4,"dodge":1,"dp":4,"explode":1,"expose":1,"garrison":1,"garrisonGrant":1,"phase":1,"riposte":1,"shieldBreak":10,"swordRain":5,"zone":4}, "fx": {"block":11,"burst":4,"dodge":1,"dp":4,"explode":1,"expose":1,"garrison":1,"garrisonGrant":1,"phase":1,"riposte":1,"shieldBreak":10,"swordRain":5,"zone":4},
"snaps": "d1a66fbb 1e4c3412 cfa655f4 33ae5aea be858e5a e390032f a14b2f14 f815866a e4173185 0a72a5c9 f546a0e1 e7e58926 f223a279 820a4de9 842392b8 3e71a88c" "snaps": "d1a66fbb 1e4c3412 cfa655f4 33ae5aea be858e5a e390032f a14b2f14 f815866a e4173185 0a72a5c9 f546a0e1 e7e58926 f223a279 820a4de9 842392b8 3e71a88c"
}, },
@@ -1823,7 +1823,7 @@
"enemy_2050_smsha": [1,1,0,4186,46080] "enemy_2050_smsha": [1,1,0,4186,46080]
}, },
"hooks": [1,66,5700,1218,1218,1660,1731,18992,51,51,149,147,0,18598,114,50,47,0,1,51,3,0,0], "hooks": [1,66,5700,1218,1218,1660,1731,18992,51,51,149,147,0,18598,114,50,47,0,1,51,3,0,0],
"events": {"atk":1455,"bounty":4,"deploy":19,"die":51,"dmg":1730,"fx":220,"heal":171,"layer":3,"skill":296,"spawn":59,"status":489}, "events": {"atk":1455,"bounty":4,"deploy":19,"die":51,"dmg":1730,"engage":9,"fx":220,"heal":171,"layer":3,"skill":296,"spawn":59,"status":489},
"fx": {"beam":3,"bloodBattle":3,"camouflage":7,"crit":63,"explode":6,"garrison":3,"mote":97,"shadowWeave":31,"shell":4,"shield":1,"spGift":1,"telegraph":1}, "fx": {"beam":3,"bloodBattle":3,"camouflage":7,"crit":63,"explode":6,"garrison":3,"mote":97,"shadowWeave":31,"shell":4,"shield":1,"spGift":1,"telegraph":1},
"snaps": "9e38c03f 5a781a2c 1b1806a8 a01c69a5 478275c7 3fdc19c1 88200e04 cf7c3703 b237f22b d89d10cf 927487a1 34b0c585 dc21e468 f291619d 6275831b f92d22ce 96ce1774 e6fcb005 f68dc57b" "snaps": "9e38c03f 5a781a2c 1b1806a8 a01c69a5 478275c7 3fdc19c1 88200e04 cf7c3703 b237f22b d89d10cf 927487a1 34b0c585 dc21e468 f291619d 6275831b f92d22ce 96ce1774 e6fcb005 f68dc57b"
}, },
@@ -1879,7 +1879,7 @@
"enemy_1539_reid": [1,0,1,7794,22227] "enemy_1539_reid": [1,0,1,7794,22227]
}, },
"hooks": [1,96,5384,1057,1057,1143,1129,145,68,81,196,193,0,16010,2319,60,76,13,1,74,1,0,10], "hooks": [1,96,5384,1057,1057,1143,1129,145,68,81,196,193,0,16010,2319,60,76,13,1,74,1,0,10],
"events": {"atk":1129,"bounty":3,"deploy":18,"die":68,"dmg":1111,"fx":87,"heal":137,"layer":1,"leak":13,"skill":392,"spawn":92,"status":423}, "events": {"atk":1129,"bounty":3,"deploy":18,"die":68,"dmg":1111,"engage":10,"fx":87,"heal":137,"layer":1,"leak":13,"skill":392,"spawn":92,"status":423},
"fx": {"block":5,"charge":4,"displace":19,"dodge":10,"dp":11,"explode":5,"garrison":1,"garrisonGrant":1,"riposte":10,"shieldBreak":18,"swordRain":2,"undying":1}, "fx": {"block":5,"charge":4,"displace":19,"dodge":10,"dp":11,"explode":5,"garrison":1,"garrisonGrant":1,"riposte":10,"shieldBreak":18,"swordRain":2,"undying":1},
"snaps": "70957ea7 12980bbe eb40080d c6a9d8b0 9fbf288c ce758920 f04d0223 eeb062ad 767c47e0 9f0398d9 f2c0ec15 dda8a114 61112f0b 78f7d894 0bcdbfa3 2ea72ee7 7c313f29 5ee2d05a" "snaps": "70957ea7 12980bbe eb40080d c6a9d8b0 9fbf288c ce758920 f04d0223 eeb062ad 767c47e0 9f0398d9 f2c0ec15 dda8a114 61112f0b 78f7d894 0bcdbfa3 2ea72ee7 7c313f29 5ee2d05a"
}, },
@@ -1946,7 +1946,7 @@
"enemy_2025_syufo": [1,0,1,6212,5995] "enemy_2025_syufo": [1,0,1,6212,5995]
}, },
"hooks": [1,98,5700,904,904,3483,3735,1662,76,84,93,92,18,27936,433,79,80,8,1,80,14,3,0], "hooks": [1,98,5700,904,904,3483,3735,1662,76,84,93,92,18,27936,433,79,80,8,1,80,14,3,0],
"events": {"atk":1408,"bounty":3,"deploy":15,"die":76,"dmg":3735,"fx":220,"heal":1410,"layer":14,"leak":8,"skill":185,"spawn":95,"status":410}, "events": {"atk":1408,"bounty":3,"deploy":15,"die":76,"dmg":3735,"engage":9,"fx":220,"heal":1410,"layer":14,"leak":8,"skill":185,"spawn":95,"status":410},
"fx": {"anchor":20,"aoe":6,"bloodBattle":7,"burst":3,"camouflage":2,"displace":23,"explode":8,"extraAttack":14,"garrison":14,"garrisonGrant":1,"mote":58,"reborn":1,"shell":4,"spGift":56,"zone":3}, "fx": {"anchor":20,"aoe":6,"bloodBattle":7,"burst":3,"camouflage":2,"displace":23,"explode":8,"extraAttack":14,"garrison":14,"garrisonGrant":1,"mote":58,"reborn":1,"shell":4,"spGift":56,"zone":3},
"snaps": "0c53a603 05d5c701 ee9f4dd1 513e9602 3c21e902 78c6425e e7c025d9 93b89490 8d6a0515 df9c19cd ecab44df 4ae9e6ce 185ecf73 739fe00a 3325d0b1 85dff7e4 fdc8bbd8 f9295138 ca8fd5c7" "snaps": "0c53a603 05d5c701 ee9f4dd1 513e9602 3c21e902 78c6425e e7c025d9 93b89490 8d6a0515 df9c19cd ecab44df 4ae9e6ce 185ecf73 739fe00a 3325d0b1 85dff7e4 fdc8bbd8 f9295138 ca8fd5c7"
}, },
@@ -2014,7 +2014,7 @@
"enemy_9011_acrefr": [1,0,1,0,941] "enemy_9011_acrefr": [1,0,1,0,941]
}, },
"hooks": [1,198,6784,807,807,2836,2852,1721,62,192,45,45,0,17712,150,84,167,129,1,67,4,0,7], "hooks": [1,198,6784,807,807,2836,2852,1721,62,192,45,45,0,17712,150,84,167,129,1,67,4,0,7],
"events": {"atk":890,"bounty":1,"deploy":29,"die":63,"dmg":2826,"fx":80,"heal":218,"layer":4,"leak":129,"skill":90,"spawn":182,"status":178}, "events": {"atk":890,"bounty":1,"deploy":29,"die":63,"dmg":2826,"engage":12,"fx":80,"heal":218,"layer":4,"leak":129,"skill":90,"spawn":182,"status":178},
"fx": {"aoe":3,"crit":4,"dodge":7,"dollEnd":2,"garrison":4,"healAoe":4,"hpShare":1,"palsy":1,"reborn":2,"revive":5,"shield":23,"sp":4,"substitute":2,"summon":1,"zone":17}, "fx": {"aoe":3,"crit":4,"dodge":7,"dollEnd":2,"garrison":4,"healAoe":4,"hpShare":1,"palsy":1,"reborn":2,"revive":5,"shield":23,"sp":4,"substitute":2,"summon":1,"zone":17},
"snaps": "91b58e42 37d31be2 91cd7722 7db19787 65b82336 0cfa4a88 a0321fa7 0ce2a8db 1d18b08a 9a9ab67c 288bd388 700c526a ab107dd2 c08ac737 58eb67ce 6785a665 f9b21aa2 dbd90927 5e5e89f5 6efdbfe0 b320de5e c14c84bc 64c7b44f" "snaps": "91b58e42 37d31be2 91cd7722 7db19787 65b82336 0cfa4a88 a0321fa7 0ce2a8db 1d18b08a 9a9ab67c 288bd388 700c526a ab107dd2 c08ac737 58eb67ce 6785a665 f9b21aa2 dbd90927 5e5e89f5 6efdbfe0 b320de5e c14c84bc 64c7b44f"
}, },
@@ -2068,7 +2068,7 @@
"enemy_2009_csaudc": [1,0,1,3146,22495] "enemy_2009_csaudc": [1,0,1,3146,22495]
}, },
"hooks": [1,121,6900,2598,2598,3968,3976,7692,74,103,357,354,0,34035,4163,91,104,27,1,78,0,2,5], "hooks": [1,121,6900,2598,2598,3968,3976,7692,74,103,357,354,0,34035,4163,91,104,27,1,78,0,2,5],
"events": {"atk":3045,"bounty":3,"deploy":14,"die":76,"dmg":3966,"fx":364,"heal":1226,"leak":27,"skill":711,"spawn":119,"status":863}, "events": {"atk":3045,"bounty":3,"deploy":14,"die":76,"dmg":3966,"engage":12,"fx":364,"heal":1226,"leak":27,"skill":711,"spawn":119,"status":863},
"fx": {"aoe":13,"burst":2,"crit":26,"dodge":5,"ember":3,"healAoe":33,"hpShare":5,"revive":1,"sp":137,"zone":139}, "fx": {"aoe":13,"burst":2,"crit":26,"dodge":5,"ember":3,"healAoe":33,"hpShare":5,"revive":1,"sp":137,"zone":139},
"snaps": "9a2f8fd8 b37c8c61 f0df4454 24e23a91 980293de cea62154 49bfebd4 fa776cbc 392a23b0 a7f5a5a9 b99c5268 81c2acf0 946acd80 2ef3c97f b5eaa4e5 7483ff45 7a64ad89 8c4728f0 dc665440 391ad73d 3dda4b2c b893d994 2f892f77" "snaps": "9a2f8fd8 b37c8c61 f0df4454 24e23a91 980293de cea62154 49bfebd4 fa776cbc 392a23b0 a7f5a5a9 b99c5268 81c2acf0 946acd80 2ef3c97f b5eaa4e5 7483ff45 7a64ad89 8c4728f0 dc665440 391ad73d 3dda4b2c b893d994 2f892f77"
}, },
@@ -2126,7 +2126,7 @@
"enemy_9007_acelem": [1,1,0,0,34560] "enemy_9007_acelem": [1,1,0,0,34560]
}, },
"hooks": [1,143,5571,319,319,517,777,828,126,129,73,70,0,13486,398,4,125,0,1,126,2,11,0], "hooks": [1,143,5571,319,319,517,777,828,126,129,73,70,0,13486,398,4,125,0,1,126,2,11,0],
"events": {"atk":515,"bounty":4,"deploy":18,"die":129,"dmg":777,"fx":219,"heal":13,"layer":2,"skill":143,"spawn":142,"status":585}, "events": {"atk":515,"bounty":4,"deploy":18,"die":129,"dmg":777,"engage":6,"fx":219,"heal":13,"layer":2,"skill":143,"spawn":142,"status":585},
"fx": {"bombard":3,"bombardShell":3,"burst":11,"copy":1,"expose":116,"frostNova":1,"garrison":2,"heal":3,"phase":1,"shieldBreak":1,"snow":14,"sp":59,"talent":1,"wanted":3}, "fx": {"bombard":3,"bombardShell":3,"burst":11,"copy":1,"expose":116,"frostNova":1,"garrison":2,"heal":3,"phase":1,"shieldBreak":1,"snow":14,"sp":59,"talent":1,"wanted":3},
"snaps": "378db087 da011039 a8cc5431 34ca6f87 443dbbf3 e75588cb 6ebcbad2 8bfa9f27 72a27ef6 b55125f5 2a70aeb3 b7367592 f5c90049 20a5319d c7c52b3e eb8af929 a8659bf6 edfbb948 42af11b8" "snaps": "378db087 da011039 a8cc5431 34ca6f87 443dbbf3 e75588cb 6ebcbad2 8bfa9f27 72a27ef6 b55125f5 2a70aeb3 b7367592 f5c90049 20a5319d c7c52b3e eb8af929 a8659bf6 edfbb948 42af11b8"
}, },
@@ -2179,7 +2179,7 @@
"enemy_2003_rockman": [1,0,1,2448,9451] "enemy_2003_rockman": [1,0,1,2448,9451]
}, },
"hooks": [1,158,6900,477,477,931,1150,8737,44,148,27,26,23,15163,2600,22,130,104,1,45,0,5,0], "hooks": [1,158,6900,477,477,931,1150,8737,44,148,27,26,23,15163,2600,22,130,104,1,45,0,5,0],
"events": {"atk":494,"bounty":2,"deploy":25,"die":44,"dmg":971,"fx":378,"heal":414,"leak":104,"skill":53,"spawn":145,"status":525}, "events": {"atk":494,"bounty":2,"deploy":25,"die":44,"dmg":971,"engage":9,"fx":378,"heal":414,"leak":104,"skill":53,"spawn":145,"status":525},
"fx": {"airstrike":2,"aoe":3,"blessing":1,"burst":5,"displace":1,"explode":17,"expose":313,"frostNova":4,"healField":2,"link":1,"phase":1,"snow":11,"sunBurst":2,"talent":4,"telegraph":2,"wanted":9}, "fx": {"airstrike":2,"aoe":3,"blessing":1,"burst":5,"displace":1,"explode":17,"expose":313,"frostNova":4,"healField":2,"link":1,"phase":1,"snow":11,"sunBurst":2,"talent":4,"telegraph":2,"wanted":9},
"snaps": "be0b8d25 30c6d0d5 05ec9606 7be7f8fd dd4716ec 9b935dce e36e5e43 8f814ab9 4d9a1660 248c489e 50d850ed 29c9b215 cad03322 1c438ab5 8546c260 4b9eadf7 a5cf0d6d 923927c5 24300fdb 0d39a3a4 0273b8b7 4d641969 c7abfcb8" "snaps": "be0b8d25 30c6d0d5 05ec9606 7be7f8fd dd4716ec 9b935dce e36e5e43 8f814ab9 4d9a1660 248c489e 50d850ed 29c9b215 cad03322 1c438ab5 8546c260 4b9eadf7 a5cf0d6d 923927c5 24300fdb 0d39a3a4 0273b8b7 4d641969 c7abfcb8"
}, },
@@ -2254,7 +2254,7 @@
"enemy_2052_smgia": [1,0,1,1324,54640] "enemy_2052_smgia": [1,0,1,1324,54640]
}, },
"hooks": [1,170,6516,385,385,2331,2489,21704,69,163,60,57,27,9028,797,23,131,93,1,45,1,4,0], "hooks": [1,170,6516,385,385,2331,2489,21704,69,163,60,57,27,9028,797,23,131,93,1,45,1,4,0],
"events": {"atk":507,"bounty":2,"deploy":36,"die":70,"dmg":2410,"fx":77,"heal":100,"layer":1,"leak":93,"skill":117,"spawn":153,"status":584}, "events": {"atk":507,"bounty":2,"deploy":36,"die":70,"dmg":2410,"engage":15,"fx":77,"heal":100,"layer":1,"leak":93,"skill":117,"spawn":153,"status":584},
"fx": {"airstrike":6,"aoe":9,"burst":4,"copy":6,"explode":25,"garrison":1,"heal":3,"healField":4,"link":2,"revive":1,"stone":1,"summon":15}, "fx": {"airstrike":6,"aoe":9,"burst":4,"copy":6,"explode":25,"garrison":1,"heal":3,"healField":4,"link":2,"revive":1,"stone":1,"summon":15},
"snaps": "2c514938 bcd55a92 bc0ef3cd 177f0ddc 094ad4d4 3c998e39 91164767 626c1814 ca4a4e11 7d041c72 6049bc05 f1dc407b 018f68f8 8022d1ff ecd0be38 2c92eb60 512c05fd 92c8410f 0f24013e 783f911c c0ed7cca 359704f3" "snaps": "2c514938 bcd55a92 bc0ef3cd 177f0ddc 094ad4d4 3c998e39 91164767 626c1814 ca4a4e11 7d041c72 6049bc05 f1dc407b 018f68f8 8022d1ff ecd0be38 2c92eb60 512c05fd 92c8410f 0f24013e 783f911c c0ed7cca 359704f3"
}, },
@@ -2306,7 +2306,7 @@
"enemy_2001_duckmi": [1,1,0,0,64497] "enemy_2001_duckmi": [1,1,0,0,64497]
}, },
"hooks": [1,171,5918,881,880,4623,4636,545,160,160,28,22,58,16483,2343,92,157,0,1,163,2,1,0], "hooks": [1,171,5918,881,880,4623,4636,545,160,160,28,22,58,16483,2343,92,157,0,1,163,2,1,0],
"events": {"atk":1469,"bounty":4,"deploy":14,"die":160,"dmg":4636,"fx":108,"heal":472,"layer":2,"skill":50,"spawn":169,"status":467}, "events": {"atk":1469,"bounty":4,"deploy":14,"die":160,"dmg":4636,"engage":11,"fx":108,"heal":472,"layer":2,"skill":50,"spawn":169,"status":467},
"fx": {"aoe":13,"burst":1,"explode":1,"extraAttack":8,"garrison":2,"garrisonGrant":4,"meltdown":1,"overload":4,"reborn":2,"shieldBreak":60,"sleepGuard":3,"sunBurst":3,"talent":3,"undying":3}, "fx": {"aoe":13,"burst":1,"explode":1,"extraAttack":8,"garrison":2,"garrisonGrant":4,"meltdown":1,"overload":4,"reborn":2,"shieldBreak":60,"sleepGuard":3,"sunBurst":3,"talent":3,"undying":3},
"snaps": "1ac9ec64 7b9548db f09bce85 8fdfbf4c b7b5cfb1 1ffd5f45 b65ed6f3 0dd60492 e327aa8a 368e4eb0 a9b2c7ac 1e89bd0e ea4b335c 50cf09b8 376cd09d e793a857 4e0fc531 33a5eca2 6e3b0cd5 7ea9fd76" "snaps": "1ac9ec64 7b9548db f09bce85 8fdfbf4c b7b5cfb1 1ffd5f45 b65ed6f3 0dd60492 e327aa8a 368e4eb0 a9b2c7ac 1e89bd0e ea4b335c 50cf09b8 376cd09d e793a857 4e0fc531 33a5eca2 6e3b0cd5 7ea9fd76"
}, },
@@ -2372,7 +2372,7 @@
"enemy_2034_sythef": [1,1,0,139,8640] "enemy_2034_sythef": [1,1,0,139,8640]
}, },
"hooks": [1,73,5100,1445,1445,1980,1980,2325,48,54,76,73,0,16134,93,54,53,0,1,50,8,0,0], "hooks": [1,73,5100,1445,1445,1980,1980,2325,48,54,76,73,0,16134,93,54,53,0,1,50,8,0,0],
"events": {"atk":1445,"bounty":3,"deploy":20,"die":54,"dmg":1980,"fx":176,"heal":188,"layer":8,"skill":149,"spawn":66,"status":147}, "events": {"atk":1445,"bounty":3,"deploy":20,"die":54,"dmg":1980,"engage":12,"fx":176,"heal":188,"layer":8,"skill":149,"spawn":66,"status":147},
"fx": {"aoe":32,"blink":3,"bondMilestone":1,"buff":14,"coin":63,"displace":6,"dp":28,"garrison":8,"shield":1,"volley":19,"wolfShadow":1}, "fx": {"aoe":32,"blink":3,"bondMilestone":1,"buff":14,"coin":63,"displace":6,"dp":28,"garrison":8,"shield":1,"volley":19,"wolfShadow":1},
"snaps": "7039516f 1f992cb5 0f5e59af c4b1bbc5 07a0826f 56c348a1 323da105 91eb067a 068e4335 66187a93 1732df53 b1fbe31f f69a53e8 250909f9 25729cd3 5c78fdcd 2b29dd1f" "snaps": "7039516f 1f992cb5 0f5e59af c4b1bbc5 07a0826f 56c348a1 323da105 91eb067a 068e4335 66187a93 1732df53 b1fbe31f f69a53e8 250909f9 25729cd3 5c78fdcd 2b29dd1f"
}, },
@@ -2426,7 +2426,7 @@
"enemy_9016_acstmr": [1,1,0,0,8] "enemy_9016_acstmr": [1,1,0,0,8]
}, },
"hooks": [1,96,5100,650,650,1044,1076,731,55,87,38,37,13,10088,270,14,65,33,1,66,0,2,19], "hooks": [1,96,5100,650,650,1044,1076,731,55,87,38,37,13,10088,270,14,65,33,1,66,0,2,19],
"events": {"atk":742,"bounty":1,"deploy":29,"die":54,"dmg":1064,"fx":117,"heal":3,"leak":33,"skill":75,"spawn":80,"status":169}, "events": {"atk":742,"bounty":1,"deploy":29,"die":54,"dmg":1064,"engage":12,"fx":117,"heal":3,"leak":33,"skill":75,"spawn":80,"status":169},
"fx": {"beam":5,"block":4,"buff":4,"burst":2,"coldWind":1,"dodge":19,"dp":5,"explode":1,"liberate":1,"overclock":1,"overload":1,"palsy":3,"revive":3,"ripple":1,"rockfall":1,"sentry":6,"shadowWeave":14,"shield":7,"shieldBreak":7,"stone":1,"swordStorm":9,"telegraph":3,"volley":11,"wake":1,"wolfShadow":2,"wolfShadowLost":3,"zone":1}, "fx": {"beam":5,"block":4,"buff":4,"burst":2,"coldWind":1,"dodge":19,"dp":5,"explode":1,"liberate":1,"overclock":1,"overload":1,"palsy":3,"revive":3,"ripple":1,"rockfall":1,"sentry":6,"shadowWeave":14,"shield":7,"shieldBreak":7,"stone":1,"swordStorm":9,"telegraph":3,"volley":11,"wake":1,"wolfShadow":2,"wolfShadowLost":3,"zone":1},
"snaps": "96990ab6 47d9fcbb b6682854 8a9c5942 60e5c9ed 6b9c44fe a5ca7551 1568032d 1be5ada7 b119dbd8 85869e7c 3af299cb d618f1e8 5d28b358 1cf9df99 ca9b5cd1 9233013f" "snaps": "96990ab6 47d9fcbb b6682854 8a9c5942 60e5c9ed 6b9c44fe a5ca7551 1568032d 1be5ada7 b119dbd8 85869e7c 3af299cb d618f1e8 5d28b358 1cf9df99 ca9b5cd1 9233013f"
}, },
@@ -2477,7 +2477,7 @@
"enemy_2010_csdcr": [1,1,0,960,9600] "enemy_2010_csdcr": [1,1,0,960,9600]
}, },
"hooks": [1,67,5700,642,642,1055,1059,20714,53,54,28,23,62,26339,94,58,47,0,1,55,0,0,6], "hooks": [1,67,5700,642,642,1055,1059,20714,53,54,28,23,62,26339,94,58,47,0,1,55,0,0,6],
"events": {"atk":946,"bounty":4,"deploy":20,"die":54,"dmg":1015,"fx":168,"heal":48,"skill":51,"spawn":60,"status":343}, "events": {"atk":946,"bounty":4,"deploy":20,"die":54,"dmg":1015,"engage":10,"fx":168,"heal":48,"skill":51,"spawn":60,"status":343},
"fx": {"aoe":2,"beam":4,"buff":2,"devour":3,"displace":29,"dodge":6,"ember":2,"garrisonGrant":1,"link":3,"mote":71,"overload":3,"revive":1,"reweave":3,"riposte":2,"ripple":2,"sentry":5,"shadowWeave":19,"shield":4,"snow":1,"summon":1,"wake":4}, "fx": {"aoe":2,"beam":4,"buff":2,"devour":3,"displace":29,"dodge":6,"ember":2,"garrisonGrant":1,"link":3,"mote":71,"overload":3,"revive":1,"reweave":3,"riposte":2,"ripple":2,"sentry":5,"shadowWeave":19,"shield":4,"snow":1,"summon":1,"wake":4},
"snaps": "10b0fc57 1879e30f 44354394 dddd1d0a 627a31ff cf3c19d7 47582a23 87c2e2e7 2d0f5158 d9b586e6 18b235f7 fdb8d063 12d39f7e f01978fd f7e457a8 eb5e327f fccdb560 79c7c5d0 4f791ba7" "snaps": "10b0fc57 1879e30f 44354394 dddd1d0a 627a31ff cf3c19d7 47582a23 87c2e2e7 2d0f5158 d9b586e6 18b235f7 fdb8d063 12d39f7e f01978fd f7e457a8 eb5e327f fccdb560 79c7c5d0 4f791ba7"
}, },
@@ -2544,7 +2544,7 @@
"enemy_9008_acbunn": [1,1,0,1338,13824] "enemy_9008_acbunn": [1,1,0,1338,13824]
}, },
"hooks": [1,79,5461,554,554,1344,1361,1606,69,66,32,28,0,17722,2148,41,59,0,1,76,50,1,6], "hooks": [1,79,5461,554,554,1344,1361,1606,69,66,32,28,0,17722,2148,41,59,0,1,76,50,1,6],
"events": {"atk":674,"bounty":4,"deploy":18,"die":66,"dmg":1181,"fx":205,"heal":174,"layer":50,"skill":60,"spawn":73,"status":314}, "events": {"atk":674,"bounty":4,"deploy":18,"die":66,"dmg":1181,"engage":10,"fx":205,"heal":174,"layer":50,"skill":60,"spawn":73,"status":314},
"fx": {"aoe":5,"bombard":5,"bombardShell":5,"burst":1,"coldWind":3,"devour":4,"dodge":6,"explode":26,"extraAttack":12,"garrison":26,"garrisonGrant":1,"iceSpike":3,"link":3,"mote":40,"overload":1,"revive":1,"reweave":3,"riposte":2,"rockfall":1,"shieldBreak":2,"sleepGuard":4,"snow":1,"stone":1,"swordStorm":7,"telegraph":2,"zone":40}, "fx": {"aoe":5,"bombard":5,"bombardShell":5,"burst":1,"coldWind":3,"devour":4,"dodge":6,"explode":26,"extraAttack":12,"garrison":26,"garrisonGrant":1,"iceSpike":3,"link":3,"mote":40,"overload":1,"revive":1,"reweave":3,"riposte":2,"rockfall":1,"shieldBreak":2,"sleepGuard":4,"snow":1,"stone":1,"swordStorm":7,"telegraph":2,"zone":40},
"snaps": "875d082b 85a4bc0b 9625d9f6 7e68be43 ddfb3f30 824ba052 277e00a7 05c18f12 c69c93d5 804b666f d57f06c7 1c059a92 2e2f05e5 8a408670 bcd9bab3 325c0f56 9eeb4763 8b918a67 86550ca0" "snaps": "875d082b 85a4bc0b 9625d9f6 7e68be43 ddfb3f30 824ba052 277e00a7 05c18f12 c69c93d5 804b666f d57f06c7 1c059a92 2e2f05e5 8a408670 bcd9bab3 325c0f56 9eeb4763 8b918a67 86550ca0"
}, },
@@ -2600,7 +2600,7 @@
"enemy_9006_actoxi": [12,7,5,15242,193791] "enemy_9006_actoxi": [12,7,5,15242,193791]
}, },
"hooks": [1,96,5274,684,684,2293,2404,3868,69,84,18,14,17,26981,327,76,64,12,1,104,11,6,1], "hooks": [1,96,5274,684,684,2293,2404,3868,69,84,18,14,17,26981,327,76,64,12,1,104,11,6,1],
"events": {"atk":783,"bounty":2,"deploy":29,"die":72,"dmg":2378,"fx":135,"heal":1025,"layer":11,"leak":12,"skill":32,"spawn":80,"status":452}, "events": {"atk":783,"bounty":2,"deploy":29,"die":72,"dmg":2378,"engage":13,"fx":135,"heal":1025,"layer":11,"leak":12,"skill":32,"spawn":80,"status":452},
"fx": {"aoe":6,"beam":1,"bloodBattle":2,"burst":6,"crit":9,"displace":19,"dodge":1,"dollEnd":1,"downed":2,"ember":2,"explode":1,"garrison":11,"meltdown":4,"overload":2,"reborn":2,"revive":8,"steal":1,"substitute":6,"summon":1,"swap":4,"telegraph":2,"zone":44}, "fx": {"aoe":6,"beam":1,"bloodBattle":2,"burst":6,"crit":9,"displace":19,"dodge":1,"dollEnd":1,"downed":2,"ember":2,"explode":1,"garrison":11,"meltdown":4,"overload":2,"reborn":2,"revive":8,"steal":1,"substitute":6,"summon":1,"swap":4,"telegraph":2,"zone":44},
"snaps": "841229ba 78357365 0f64454f 0905a7a5 adb1558a 12265ef9 86732a88 1d688602 fccecf7b 75a68be4 ae48bfb4 a73a37e1 623579a6 7e4ca3de f0fed966 3ff2868e 0e35991d 9bdcb440" "snaps": "841229ba 78357365 0f64454f 0905a7a5 adb1558a 12265ef9 86732a88 1d688602 fccecf7b 75a68be4 ae48bfb4 a73a37e1 623579a6 7e4ca3de f0fed966 3ff2868e 0e35991d 9bdcb440"
}, },
@@ -2672,7 +2672,7 @@
"enemy_2085_skzjxd": [1,0,1,0,44931] "enemy_2085_skzjxd": [1,0,1,0,44931]
}, },
"hooks": [1,166,6296,1642,1642,4394,4403,6834,123,155,45,40,0,24838,2741,133,119,32,1,127,0,0,2], "hooks": [1,166,6296,1642,1642,4394,4403,6834,123,155,45,40,0,24838,2741,133,119,32,1,127,0,0,2],
"events": {"atk":2063,"bounty":4,"deploy":45,"die":123,"dmg":4403,"fx":135,"heal":178,"leak":32,"skill":85,"spawn":152,"status":957}, "events": {"atk":2063,"bounty":4,"deploy":45,"die":123,"dmg":4403,"engage":31,"fx":135,"heal":178,"leak":32,"skill":85,"spawn":152,"status":957},
"fx": {"blink":1,"crit":24,"displace":39,"dodge":2,"explode":5,"sp":6,"substitute":4,"summon":19,"swap":3,"teleport":9,"zone":23}, "fx": {"blink":1,"crit":24,"displace":39,"dodge":2,"explode":5,"sp":6,"substitute":4,"summon":19,"swap":3,"teleport":9,"zone":23},
"snaps": "a39ea9a8 894ae924 7bac6648 7d42567b 3eb2ab4e 3ab164c7 95348b36 0f17357c d58a493f fb05401c 709ec076 b9072b05 be569992 81553045 441b0a88 5c38623c 7ed8e02b 628e26a1 52aeca40 3789a121 f5ea4e29" "snaps": "a39ea9a8 894ae924 7bac6648 7d42567b 3eb2ab4e 3ab164c7 95348b36 0f17357c d58a493f fb05401c 709ec076 b9072b05 be569992 81553045 441b0a88 5c38623c 7ed8e02b 628e26a1 52aeca40 3789a121 f5ea4e29"
}, },
@@ -2734,7 +2734,7 @@
"enemy_2001_duckmi_2": [1,1,0,0,9331] "enemy_2001_duckmi_2": [1,1,0,0,9331]
}, },
"hooks": [1,91,5571,429,416,885,1371,9297,69,74,11,8,10,21975,747,16,68,0,1,69,2,49,0], "hooks": [1,91,5571,429,416,885,1371,9297,69,74,11,8,10,21975,747,16,68,0,1,69,2,49,0],
"events": {"atk":525,"bounty":4,"deploy":20,"die":74,"dmg":1186,"fx":122,"heal":239,"layer":2,"skill":19,"spawn":90,"status":1442}, "events": {"atk":525,"bounty":4,"deploy":20,"die":74,"dmg":1186,"engage":7,"fx":122,"heal":239,"layer":2,"skill":19,"spawn":90,"status":1442},
"fx": {"airstrike":4,"anchor":1,"burst":49,"dp":3,"expose":6,"garrison":2,"link":2,"snow":5,"summon":2,"summonStun":2,"talent":1,"wanted":45}, "fx": {"airstrike":4,"anchor":1,"burst":49,"dp":3,"expose":6,"garrison":2,"link":2,"snow":5,"summon":2,"summonStun":2,"talent":1,"wanted":45},
"snaps": "9faf7d66 a9734f34 a9f988b1 95d9e572 07f9937f 828c97ea 0414f9f5 d8ef07d8 3d72f489 2c55520d c28e71dc e5dc9e40 b395c094 75a018c5 1f188deb 4ffca9a0 93cc70bf 4169aca2 d23979c7" "snaps": "9faf7d66 a9734f34 a9f988b1 95d9e572 07f9937f 828c97ea 0414f9f5 d8ef07d8 3d72f489 2c55520d c28e71dc e5dc9e40 b395c094 75a018c5 1f188deb 4ffca9a0 93cc70bf 4169aca2 d23979c7"
}, },
@@ -2789,7 +2789,7 @@
"enemy_2034_sythef_2": [1,0,1,0,0] "enemy_2034_sythef_2": [1,0,1,0,0]
}, },
"hooks": [1,165,6669,205,205,496,500,100,26,162,9,9,4,3662,276,15,134,132,1,27,1,1,0], "hooks": [1,165,6669,205,205,496,500,100,26,162,9,9,4,3662,276,15,134,132,1,27,1,1,0],
"events": {"atk":291,"bounty":1,"deploy":31,"die":30,"dmg":488,"fx":170,"heal":58,"layer":1,"leak":132,"skill":18,"spawn":149,"status":387}, "events": {"atk":291,"bounty":1,"deploy":31,"die":30,"dmg":488,"engage":8,"fx":170,"heal":58,"layer":1,"leak":132,"skill":18,"spawn":149,"status":387},
"fx": {"blessing":1,"blink":2,"burst":1,"camouflage":2,"displace":1,"explode":72,"garrison":1,"garrisonGrant":2,"link":1,"snow":7,"sunBurst":3,"talent":2,"telegraph":2,"zone":73}, "fx": {"blessing":1,"blink":2,"burst":1,"camouflage":2,"displace":1,"explode":72,"garrison":1,"garrisonGrant":2,"link":1,"snow":7,"sunBurst":3,"talent":2,"telegraph":2,"zone":73},
"snaps": "6605881c 99d27c6c d2d45669 7e4fbc70 bcf82881 4959c1cf 07251748 47e1ea9c ef3fabd2 d150c862 3083e599 7e3dcbbd 7c7253a3 191cba19 fb761362 7766cc81 56e7cf0e 85fb9079 929affb4 6525eb41 e68dc459 0c416386 3305aebb" "snaps": "6605881c 99d27c6c d2d45669 7e4fbc70 bcf82881 4959c1cf 07251748 47e1ea9c ef3fabd2 d150c862 3083e599 7e3dcbbd 7c7253a3 191cba19 fb761362 7766cc81 56e7cf0e 85fb9079 929affb4 6525eb41 e68dc459 0c416386 3305aebb"
}, },
@@ -2848,7 +2848,7 @@
"enemy_1525_blkswb": [1,0,1,4227,26880] "enemy_1525_blkswb": [1,0,1,4227,26880]
}, },
"hooks": [1,177,6417,702,694,819,922,115,77,172,15,15,80,9761,1199,38,142,91,1,81,0,0,0], "hooks": [1,177,6417,702,694,819,922,115,77,172,15,15,80,9761,1199,38,142,91,1,81,0,0,0],
"events": {"atk":704,"bounty":3,"deploy":32,"die":81,"dmg":837,"fx":59,"heal":26,"leak":91,"skill":30,"spawn":163,"status":180}, "events": {"atk":704,"bounty":3,"deploy":32,"die":81,"dmg":837,"engage":12,"fx":59,"heal":26,"leak":91,"skill":30,"spawn":163,"status":180},
"fx": {"airstrike":26,"anchor":1,"appear":1,"blink":2,"copy":1,"disappear":1,"ember":1,"explode":1,"garrisonGrant":2,"heal":2,"link":1,"revive":4,"strike":6,"summon":2,"summonStun":2,"sunBurst":1,"sword":3,"telegraph":1,"zone":1}, "fx": {"airstrike":26,"anchor":1,"appear":1,"blink":2,"copy":1,"disappear":1,"ember":1,"explode":1,"garrisonGrant":2,"heal":2,"link":1,"revive":4,"strike":6,"summon":2,"summonStun":2,"sunBurst":1,"sword":3,"telegraph":1,"zone":1},
"snaps": "019a26cc 168013a9 838d9fe2 d6e37c3b 03ecc5c8 af794d9a bab801d7 d430ce64 545aa402 627f3cca 90dea475 f6fbcb03 eac8545b d32ae905 60789830 dc25a684 972cf188 1da714fe 79b82ec1 826d0e8e 866ff2a7 c4064e28" "snaps": "019a26cc 168013a9 838d9fe2 d6e37c3b 03ecc5c8 af794d9a bab801d7 d430ce64 545aa402 627f3cca 90dea475 f6fbcb03 eac8545b d32ae905 60789830 dc25a684 972cf188 1da714fe 79b82ec1 826d0e8e 866ff2a7 c4064e28"
}, },
@@ -2908,7 +2908,7 @@
"enemy_2017_csphts": [1,0,1,782,1260] "enemy_2017_csphts": [1,0,1,782,1260]
}, },
"hooks": [1,100,6410,284,284,2293,2300,22249,54,92,94,94,0,10841,608,45,86,38,1,69,11,0,0], "hooks": [1,100,6410,284,284,2293,2300,22249,54,92,94,94,0,10841,608,45,86,38,1,69,11,0,0],
"events": {"atk":526,"deploy":11,"die":54,"dmg":2300,"fx":182,"heal":62,"layer":11,"leak":38,"skill":188,"spawn":95,"status":875}, "events": {"atk":526,"deploy":11,"die":54,"dmg":2300,"engage":5,"fx":182,"heal":62,"layer":11,"leak":38,"skill":188,"spawn":95,"status":875},
"fx": {"dollEnd":1,"explode":1,"garrison":10,"layer":1,"lightning":68,"shield":8,"slash":32,"sp":50,"storm":4,"substitute":1,"telegraph":1,"weightless":5}, "fx": {"dollEnd":1,"explode":1,"garrison":10,"layer":1,"lightning":68,"shield":8,"slash":32,"sp":50,"storm":4,"substitute":1,"telegraph":1,"weightless":5},
"snaps": "94f68fc0 699b5957 74b1283d 3b83834f a3210271 89392766 01647ce4 ef4832d2 50b1d3df 464d3e8b 3b0659d2 ff5572c0 d04959d6 1a5f4ee2 aa11dd73 b2b4d570 01351b36 6d94c52d 6e684127 cbf7b7ff 35bcef0d b1048253" "snaps": "94f68fc0 699b5957 74b1283d 3b83834f a3210271 89392766 01647ce4 ef4832d2 50b1d3df 464d3e8b 3b0659d2 ff5572c0 d04959d6 1a5f4ee2 aa11dd73 b2b4d570 01351b36 6d94c52d 6e684127 cbf7b7ff 35bcef0d b1048253"
}, },
@@ -2953,7 +2953,7 @@
"enemy_9009_acfort": [1,0,1,7851,19348] "enemy_9009_acfort": [1,0,1,7851,19348]
}, },
"hooks": [1,71,5700,366,366,1824,1824,0,31,65,30,31,0,3900,792,3,64,34,1,33,0,0,0], "hooks": [1,71,5700,366,366,1824,1824,0,31,65,30,31,0,3900,792,3,64,34,1,33,0,0,0],
"events": {"atk":422,"bounty":3,"deploy":7,"die":31,"dmg":1823,"fx":45,"leak":34,"skill":64,"spawn":69,"status":304}, "events": {"atk":422,"bounty":3,"deploy":7,"die":31,"dmg":1823,"engage":4,"fx":45,"leak":34,"skill":64,"spawn":69,"status":304},
"fx": {"coin":8,"dilemma":2,"explode":2,"frostNova":25,"revive":2,"shield":1,"shieldBreak":1,"summon":1,"telegraph":3}, "fx": {"coin":8,"dilemma":2,"explode":2,"frostNova":25,"revive":2,"shield":1,"shieldBreak":1,"summon":1,"telegraph":3},
"snaps": "3dfc169e 33981103 b9e7dc10 e5c76cae 6c52ad96 4e2ae444 84eaae26 656ad352 8930f4ed cb10ab9f 8d62f572 2928e41b 928d6080 4b392947 cad850f5 88878813 87bd227a 00e4cae4 a630d784" "snaps": "3dfc169e 33981103 b9e7dc10 e5c76cae 6c52ad96 4e2ae444 84eaae26 656ad352 8930f4ed cb10ab9f 8d62f572 2928e41b 928d6080 4b392947 cad850f5 88878813 87bd227a 00e4cae4 a630d784"
} }
+18
View File
@@ -392,6 +392,24 @@ test('snapshot / event wire format (DESIGN §8.2)', () => {
JSON.stringify(snap); // serialisable JSON.stringify(snap); // serialisable
}); });
test('one ENGAGE per unit, on its first attack that hits an enemy (the client\'s 行动开始 voice)', () => {
const h = makeBattle({
defs: { chess: { t_guard: guard({ stats: { atk: 100, maxHp: 5000 } }) }, enemies: { enemy_walker: walker({ atk: 100, hp: 3000 }) } },
units: [{ chessId: 't_guard', row: 9, col: 5 }],
enemies: [{ key: 'enemy_walker' }], content: 'generic', fieldId: 'n:p1',
});
h.run(60); // the walker (0.5 tiles/s) reaches the guard at ~9 s and is attacked from then on
const id = h.unit('t_guard').id;
const engage = h.eventsOf(EV.ENGAGE);
assert.equal(engage.length, 1, 'one event for the whole battle');
assert.deepEqual(engage[0], ['engage', id]);
// it is emitted at the unit's first attack on an enemy — an earlier 命中的攻击 is impossible
const first = h.events.findIndex((e) => e[0] === 'engage');
const earlierAtk = h.events.slice(0, first).findIndex((e) => e[0] === 'atk' && e[1] === id);
assert.equal(earlierAtk, -1, 'no attack of that unit precedes it');
assert.ok(h.events.slice(first).some((e) => e[0] === 'atk' && e[1] === id), 'the attack it belongs to');
});
test('determinism: same seed ⇒ identical result and event stream; different seed may differ', () => { test('determinism: same seed ⇒ identical result and event stream; different seed may differ', () => {
const ds = getDefaultSource(); const ds = getDefaultSource();
const tpl = ds.getWave('act1autochess_05'); const tpl = ds.getWave('act1autochess_05');
+192 -2
View File
@@ -6,7 +6,7 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { bgmKeyFor, resolveBgm, SfxLimiter, AudioManager, normalAttackSfx, installAudio, audio, combatTrackFor, COMBAT_TRACK_SWITCH_ROUND } from '../../public/js/audio.js'; import { bgmKeyFor, resolveBgm, SfxLimiter, AudioManager, normalAttackSfx, installAudio, audio, combatTrackFor, COMBAT_TRACK_SWITCH_ROUND, VoiceGate, resultSpeaker, resultVoiceSlot, VOICE_PRIORITY, VOICE_COOLDOWN_MS } from '../../public/js/audio.js';
import { mediaUrl } from '../../public/js/media.js'; import { mediaUrl } from '../../public/js/media.js';
import { PHASE } from '../../shared/constants.js'; import { PHASE } from '../../shared/constants.js';
@@ -170,6 +170,196 @@ describe('SfxLimiter', () => {
}); });
}); });
// ---- operator battle voice (manifest audio.voice; official priorities, audio_data.json battleVoice) ---------
describe('operator battle voice', () => {
test('resultVoiceSlot: 完美作战 / 高难 / 漏怪 / 全灭', () => {
assert.equal(resultVoiceSlot({ perfect: true }), 'resultThree');
assert.equal(resultVoiceSlot({ perfect: true, hard: true }), 'resultFour', '绝境 / 终极 报 完成高难行动');
assert.equal(resultVoiceSlot({ perfect: false, leaked: 2, killed: 10, total: 12 }), 'resultTwo');
assert.equal(resultVoiceSlot({ perfect: false, leaked: 0, killed: 0, total: 12 }), 'resultLose');
assert.equal(resultVoiceSlot({ perfect: false, leaked: 0, killed: 5, total: 5 }), 'resultThree', 'no leak ⇒ the 完美 line');
assert.equal(resultVoiceSlot(), 'resultThree');
});
test('resultSpeaker: the operators of THAT battle (survivors first), never the field on screen', () => {
// `mine.unitsEnd` of the finished own battle: operators (char_*) and the board's summon pieces (token_*, no voice)
const mine = { unitsEnd: [
{ defId: 'char_fallen', alive: false },
{ defId: 'token_1001', alive: true },
{ defId: 'char_a', alive: true },
{ defId: 'char_b', alive: true },
] };
assert.equal(resultSpeaker(mine, () => 0), 'char_a');
assert.equal(resultSpeaker(mine, () => 0.999), 'char_b', 'a drawn operator, still only the ones standing');
assert.equal(resultSpeaker(mine, () => 0.4), 'char_a');
const drawn = new Set([0, 0.2, 0.4, 0.6, 0.8, 1].map((r) => resultSpeaker(mine, () => r)));
assert.deepEqual([...drawn].sort(), ['char_a', 'char_b'], 'a fallen operator or a summon never speaks');
// the whole squad fell: only then does a fallen operator report the result
const wiped = { unitsEnd: [{ defId: 'char_fallen', alive: false }, { defId: 'char_also', alive: false }] };
assert.equal(resultSpeaker(wiped, () => 0), 'char_fallen');
assert.equal(resultSpeaker(wiped, () => 0.999), 'char_also');
// no operator on that field at all — summons only, an empty list, or a payload without `unitsEnd` (an older
// server): no line. There is deliberately NO fallback to the operators currently tracked for the field on screen —
// that fallback is what made a watched team-mate's operator say the viewer's line (review on #73).
assert.equal(resultSpeaker({ unitsEnd: [{ defId: 'token_1', alive: true }] }, () => 0), null);
assert.equal(resultSpeaker({ unitsEnd: [] }), null);
assert.equal(resultSpeaker({}), null);
assert.equal(resultSpeaker(null), null);
assert.equal(resultSpeaker(undefined), null);
assert.equal(resultSpeaker({ unitsEnd: [{ alive: true }] }, () => 0), null, 'a unit without a defId');
assert.equal(resultSpeaker({ unitsEnd: [{ defId: 'enemy_1007_slime', alive: true }] }, () => 0), null, 'an enemy');
});
test('VoiceGate: one line at a time, a global gap, per-unit cooldowns, higher priority takes over', () => {
assert.ok(VOICE_PRIORITY.start > VOICE_PRIORITY.skill1 && VOICE_PRIORITY.skill1 > VOICE_PRIORITY.place, 'the official order');
assert.ok(VOICE_PRIORITY.resultThree > VOICE_PRIORITY.skill1 && VOICE_PRIORITY.resultThree < VOICE_PRIORITY.faceEnemy);
assert.equal(VOICE_COOLDOWN_MS.skill1, 10000, 'official SKILL_ACTIVE 10 s');
const g = new VoiceGate({ gapMs: 1000 });
assert.equal(g.request('start', 1, 0), 'play');
g.start('start', 1, 0);
assert.equal(g.request('skill1', 2, 500), 'drop', 'one voice at a time (a lower priority waits)');
assert.equal(g.request('start', 3, 500), 'drop', 'the same priority does not interrupt');
assert.equal(g.request('faceEnemy', 3, 500), 'drop', '90 < 100 + margin');
g.release();
assert.equal(g.request('place', 1, 900), 'drop', 'the global gap is not over');
assert.equal(g.request('place', 1, 1000), 'play');
g.release();
const g2 = new VoiceGate({ gapMs: 0 });
assert.equal(g2.request('skill1', 'u1', 0), 'play');
g2.start('skill1', 'u1', 0);
g2.release();
assert.equal(g2.request('skill1', 'u1', 5000), 'drop', 'the unit cooldown');
assert.equal(g2.request('skill1', 'u2', 5000), 'play', 'another unit is unaffected');
g2.release();
assert.equal(g2.request('skill1', 'u1', 10000), 'play', 'cooldown over');
g2.release();
g2.reset();
assert.equal(g2.request('skill1', 'u1', 10001), 'play', 'a new battle inherits no cooldown');
});
test('AudioManager.voice: manifest slots (a drawn array), the gate, and the battle events that drive them', async () => {
const fw = fakeWindow();
const origFetch = globalThis.fetch;
const urls = [];
globalThis.fetch = async (u) => { urls.push(u); return { ok: true, arrayBuffer: async () => new ArrayBuffer(8) }; };
try {
const vm = { audio: { sfx: { ui: {}, battle: {}, units: {} }, voice: {
char_a: { start: '/v/a_start.mp3', place: ['/v/a_p1.mp3', '/v/a_p2.mp3'], skill3: '/v/a_s3.mp3', faceEnemy: '/v/a_face.mp3' },
char_b: { place: '/v/b_p1.mp3', skill1: '/v/b_s1.mp3' },
} } };
const a = new AudioManager({ win: fw.win, getManifest: () => vm });
a.voiceGate = new VoiceGate({ gapMs: 0 }); // the gap itself is covered above
a.install();
assert.equal(a.voice('char_a', 'place'), false, 'locked: nothing is requested');
assert.equal(urls.length, 0);
fw.fire('pointerdown');
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voice('char_a', 'start', { unitKey: 1 }), true);
await new Promise((r) => setTimeout(r, 10));
assert.ok(asked(urls, '/v/a_start.mp3'));
assert.equal(a.voice('char_b', 'place', { unitKey: 2 }), false, '部署 cannot interrupt 行动出发');
assert.equal(a.voice('char_a', 'faceEnemy', { unitKey: 3 }), false, '接敌 cannot either');
a._stopVoice();
assert.equal(a.voice('char_a', 'place', { unitKey: 1 }), true, 'the channel is free again');
await new Promise((r) => setTimeout(r, 10));
assert.ok(asked(urls, '/v/a_p1.mp3') || asked(urls, '/v/a_p2.mp3'), 'a drawn 部署 line');
assert.equal(a.voice('char_a', 'nope', { unitKey: 1 }), false, 'unknown slot');
assert.equal(a.voice('char_zz', 'place', { unitKey: 1 }), false, 'unknown operator');
a._stopVoice(); a.voiceGate.reset();
// the battle events drive the lines: 行动出发 (the first deploy), 部署 (the rest), 行动开始, 作战中N
a.setFieldUnits([{ id: 11, side: 'ally', spine: 'char_a', kind: 'op', skillIndex: 2 }, { id: 12, side: 'ally', spine: 'char_b', kind: 'op' }]);
a.handleBattleEvents([['deploy', 11]]);
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voiceNode?.url, '/v/a_start.mp3', 'the first operator deployed says 行动出发');
a.handleBattleEvents([['deploy', 12]]);
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voiceNode?.url, '/v/a_start.mp3', 'a 部署 line never interrupts it (lower priority)');
a._stopVoice(); a.voiceGate.reset();
a.handleBattleEvents([['engage', 11], ['skill', 12, 1]]);
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voiceNode?.url, '/v/a_face.mp3', '首次接敌 ⇒ 行动开始 (a 作战中 line never interrupts it)');
a._stopVoice(); a.voiceGate.reset();
a.handleBattleEvents([['skill', 11, 1]]);
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voiceNode?.url, '/v/a_s3.mp3', '作战中N follows the unit skillIndex (2 ⇒ 作战中3)');
a._stopVoice(); a.voiceGate.reset();
a.handleBattleEvents([['skill', 12, 1]]);
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voiceNode?.url, '/v/b_s1.mp3', 'a unit without skillIndex falls back to 作战中1');
a._stopVoice(); a.voiceGate.reset();
a.setFieldUnits([{ id: 21, side: 'ally', spine: 'char_a', kind: 'op' }]);
a.handleBattleEvents([['deploy', 21]]);
await new Promise((r) => setTimeout(r, 10));
assert.equal(a.voiceNode?.url, '/v/a_start.mp3', 'a new battle starts with 行动出发 again');
} finally {
globalThis.fetch = origFetch;
}
});
test('voice: a stale callback never frees the channel the newest line holds (review on #73)', async () => {
const fw = fakeWindow();
const origFetch = globalThis.fetch;
// holds named URLs open: the first request for one waits on the promise in `held`, later ones (the manager caches a
// decoded URL) do not. `fail` answers 404, i.e. `_buffer` resolves null.
const held = new Map();
const fail = new Set();
globalThis.fetch = async (u) => {
if (fail.has(u)) return { ok: false, status: 404 };
const h = held.get(u);
if (h) { held.delete(u); await h; } // the manager caches per URL, so only the first call waits
return { ok: true, arrayBuffer: async () => new ArrayBuffer(8) };
};
const tick = () => new Promise((r) => setTimeout(r, 10));
try {
const vm = { audio: { sfx: { ui: {}, battle: {}, units: {} }, voice: {
char_a: { place: '/v/a_p1.mp3', start: '/v/a_start.mp3', faceEnemy: '/v/a_face.mp3', select: '/v/a_sel.mp3' },
} } };
const a = new AudioManager({ win: fw.win, getManifest: () => vm });
a.voiceGate = new VoiceGate({ gapMs: 0 }); // the gap itself is covered above
a.install();
fw.fire('pointerdown');
await tick();
// 部署 (20) starts with its LOAD HELD OPEN, so 行动出发 (100) takes the channel over while it is still loading
let openA;
held.set('/v/a_p1.mp3', new Promise((r) => { openA = r; }));
assert.equal(a.voice('char_a', 'place', { unitKey: 1 }), true);
assert.equal(a.voice('char_a', 'start', { unitKey: 2 }), true, 'a higher priority line takes the channel over');
await tick();
assert.equal(a.voiceNode?.url, '/v/a_start.mp3', 'the replacement is on air');
openA(); // the preempted line's load finally finishes
await tick();
assert.equal(a.voiceNode?.url, '/v/a_start.mp3', 'a stale callback never starts its own line');
assert.equal(a.voice('char_a', 'place', { unitKey: 3 }), false, 'and never freed the channel it lost');
// the safety timer / onended of a line that was STOPPED must not free the line that replaced it either
const ringing = a.voiceNode; // 行动出发 on air
a._stopVoice(); // the stop path releases the gate itself and moves the token on
assert.equal(a.voice('char_a', 'place', { unitKey: 4 }), true, 'the channel is free again');
await tick();
const now = a.voiceNode;
assert.equal(now?.url, '/v/a_p1.mp3');
ringing.src.onended?.(); // the stopped line's own end lands late (or its safety timer fires)
ringing.src.onended?.(); // …and a second time, like timer + onended both firing
await tick();
assert.equal(a.voiceNode, now, 'the line on air is untouched');
assert.equal(a.voice('char_a', 'place', { unitKey: 5 }), false, 'and its channel is still held');
// a load that FAILS after the takeover (404 ⇒ null buffer): same rule
fail.add('/v/a_sel.mp3');
a._stopVoice();
assert.equal(a.voice('char_a', 'select', { unitKey: 6 }), true);
assert.equal(a.voice('char_a', 'place', { unitKey: 7 }), true, 'a higher priority line takes over while it loads');
await tick();
assert.equal(a.voiceNode?.url, '/v/a_p1.mp3');
await tick();
assert.equal(a.voice('char_a', 'place', { unitKey: 8 }), false, 'the failed load kept out of the newest line\'s way');
} finally {
globalThis.fetch = origFetch;
}
});
});
// ---- fake Web Audio ------------------------------------------------------------------------------------- // ---- fake Web Audio -------------------------------------------------------------------------------------
function fakeWindow() { function fakeWindow() {
@@ -209,7 +399,7 @@ describe('AudioManager', () => {
assert.equal(a.unit('char_x', 'attack', 1), false); assert.equal(a.unit('char_x', 'attack', 1), false);
a.handleBattleEvents([['atk', 1, 2, 'arrow'], 'junk', null]); a.handleBattleEvents([['atk', 1, 2, 'arrow'], 'junk', null]);
a.setVolumes({ bgm: 5, sfx: -1, muted: true }); a.setVolumes({ bgm: 5, sfx: -1, muted: true });
assert.deepEqual(a.volumes, { bgm: 1, sfx: 0, muted: true }); assert.deepEqual(a.volumes, { bgm: 1, sfx: 0, voice: 0.8, muted: true });
assert.equal(a.unlocked, false); assert.equal(a.unlocked, false);
}); });
test('unlocks on the first gesture, then plays BGM and SFX from the manifest', async () => { test('unlocks on the first gesture, then plays BGM and SFX from the manifest', async () => {
+3 -2
View File
@@ -515,8 +515,9 @@ describe('keyboard & settings', () => {
}); });
test('sanitizeSettings', () => { test('sanitizeSettings', () => {
assert.deepEqual(sanitizeSettings(null), { ...DEFAULT_SETTINGS }); assert.deepEqual(sanitizeSettings(null), { ...DEFAULT_SETTINGS });
assert.deepEqual(sanitizeSettings({ bgm: 3, sfx: -1, muted: 'yes', damageNumbers: false, quality: 'ultra' }), assert.deepEqual(sanitizeSettings({ bgm: 3, sfx: -1, voice: 2, muted: 'yes', damageNumbers: false, quality: 'ultra' }),
{ bgm: 1, sfx: 0, muted: false, damageNumbers: false, quality: 'high' }); { bgm: 1, sfx: 0, voice: 1, muted: false, damageNumbers: false, quality: 'high' });
assert.equal(sanitizeSettings({ bgm: 0.5 }).voice, DEFAULT_SETTINGS.voice, 'a saved profile without `voice` gets the default');
assert.equal(sanitizeSettings({ bgm: 0.333 }).bgm, 0.33); assert.equal(sanitizeSettings({ bgm: 0.333 }).bgm, 0.33);
assert.equal(sanitizeSettings({ quality: 'low' }).quality, 'low'); assert.equal(sanitizeSettings({ quality: 'low' }).quality, 'low');
}); });
+88
View File
@@ -268,3 +268,91 @@ export function resolveSpec(spec, bank) {
if (spec?.bank) return bank(spec.bank); if (spec?.bank) return bank(spec.bank);
return []; return [];
} }
// ---- operator battle voice (excel/charword_table.json) -------------------------------------------------
//
// Every playable operator has official battle lines (行动出发 / 行动开始 / 选中干员 / 部署 / 作战中 / 编入队伍 /
// 任命队长 / 结算 / 干员报到) in `voice_cn/<charId>/cn_<n>.mp3` — JP: `voice/`, EN: `voice_en/`, KR: `voice_kr/`,
// the same file names in every dump, only the folder differs. charword_table.json's `placeType` says when the game
// plays each line; its `voiceAsset` is the path under the dump.
/** Voice dump folder per language (under sound_beta_2). */
export const VOICE_DIRS = Object.freeze({ cn: 'voice_cn', jp: 'voice', en: 'voice_en', kr: 'voice_kr' });
/** Official `placeType` → the manifest's voice slot (public/js/audio.js VOICE_PRIORITY / VOICE_COOLDOWN_MS). */
export const VOICE_SLOTS = Object.freeze({
BATTLE_START: 'start', // 行动出发: 开战
BATTLE_FACE_ENEMY: 'faceEnemy', // 行动开始: 首次接敌
BATTLE_SELECT: 'select', // 选中干员1/2
BATTLE_PLACE: 'place', // 部署1/2
BATTLE_SKILL_1: 'skill1', // 作战中1-4: the equipped skill's own slot
BATTLE_SKILL_2: 'skill2',
BATTLE_SKILL_3: 'skill3',
BATTLE_SKILL_4: 'skill4',
SQUAD: 'squad', // 编入队伍
SQUAD_FIRST: 'squadFirst', // 任命队长
FOUR_STAR: 'resultFour', // 完成高难行动
THREE_STAR: 'resultThree', // 3星结束行动 (完美作战)
TWO_STAR: 'resultTwo', // 非3星结束行动
LOSE: 'resultLose', // 行动失败
GACHA: 'gacha', // 干员报到
});
/**
* The slots a running battle can actually request — the only ones `public/js/audio.js` ever asks for (the 休整期 is
* silent, so nothing else is played): 行动出发 start, 首次接敌 faceEnemy, 作战中1-4 skillN, 部署 place (the deploy
* events) and 选中干员 select (the detail panel, behind its combat flag), plus the settlement lines
* resultFour / resultThree / resultTwo / resultLose (public/js/screens/game.js onResult).
* `buildPlan` plans these by default; `--voice-all` widens it to every slot of VOICE_SLOTS.
*/
export const VOICE_BATTLE_SLOTS = Object.freeze(['start', 'faceEnemy', 'select', 'place',
'skill1', 'skill2', 'skill3', 'skill4', 'resultFour', 'resultThree', 'resultTwo', 'resultLose']);
/**
* Slots no battle plays: the lines the official client uses in its own 养成 / 编队 UI (干员报到 gacha, 编入队伍 squad,
* 任命队长 squadFirst). `test/docs-consistency.test.js` proves the client never asks for one, so planning them only
* makes every `npm run assets` download 360 files (19.3 MB, CN dub) that no player will ever hear — they are left out
* unless `--voice-all` is passed. 部署 `place` and 选中干员 `select` deliberately stay in: the official client groups
* them with the prep lines, but a battle does play them (the deploy events; the detail panel behind its combat flag).
*/
export const VOICE_PREP_SLOTS = Object.freeze(['gacha', 'squad', 'squadFirst']);
/**
* Index charword_table.json into per-character voice slots, in voiceIndex order (one slot may have several lines).
* Only the base word key (`wordKey === charId`) is used: the dump has no folder for a skin variant's word key
* (`char_x_ita`, `char_x_epoque#28`, …) — those files simply do not exist upstream.
* @param {any} charword parsed excel/charword_table.json
* @param {string} [lang] voiceId prefix — the zh_CN table carries the `CN_*` lines; the other dubs share the numbering
* @param {Iterable<string>|null} [only] slot names to keep; null/omitted keeps every slot (VOICE_BATTLE_SLOTS is what
* the plan uses by default, so a battle's own lines are planned and the prep-only ones are not)
* @returns {Map<string, Record<string, string[]>>} charId → slot → voiceAsset ('char_263_skadi/CN_023')
*/
export function indexVoice(charword, lang = 'CN', only = null) {
const keep = only ? new Set(only) : null;
const out = new Map();
const words = charword?.charWords;
if (!words || typeof words !== 'object') return out;
/** @type {Map<string, Map<string, Map<string, {index:number, asset:string}>>>} */
const seen = new Map();
for (const e of Object.values(words)) {
if (!e || typeof e !== 'object') continue;
const charId = e.charId, slot = VOICE_SLOTS[e.placeType], vid = e.voiceId;
if (!charId || !slot || (keep && !keep.has(slot)) || typeof vid !== 'string' || !vid.startsWith(`${lang}_`)) continue;
if (e.wordKey !== charId) continue;
if (typeof e.voiceAsset !== 'string' || !e.voiceAsset) continue;
if (!seen.has(charId)) seen.set(charId, new Map());
const slots = seen.get(charId);
if (!slots.has(slot)) slots.set(slot, new Map());
// the same line can be listed twice (an operator's 升变 / alt records): keep its lowest voiceIndex, once
const m = slots.get(slot);
const index = Number.isFinite(e.voiceIndex) ? e.voiceIndex : 0;
const prev = m.get(vid);
if (!prev || index < prev.index) m.set(vid, { index, asset: e.voiceAsset });
}
for (const [charId, slots] of seen) {
const rec = {};
for (const [slot, m] of slots) rec[slot] = [...m.values()].sort((a, b) => a.index - b.index).map((x) => x.asset);
out.set(charId, rec);
}
return out;
}
+15 -5
View File
@@ -1,6 +1,7 @@
// Cached upstream JSON indexes needed by the asset pipeline: // Cached upstream JSON indexes needed by the asset pipeline:
// .cache/gamedata/excel/audio_data.json (Kengxxiao/ArknightsGameData, zh_CN) // .cache/gamedata/excel/audio_data.json (Kengxxiao/ArknightsGameData, zh_CN)
// .cache/ark-models/models_data.json (isHarryh/Ark-Models enemy Spine index) // .cache/gamedata/excel/charword_table.json (same repo: the operators' voice slots and voice assets)
// .cache/ark-models/models_data.json (isHarryh/Ark-Models enemy Spine index)
// Downloaded once when missing (or with --refresh-index), then reused. // Downloaded once when missing (or with --refresh-index), then reused.
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
@@ -46,10 +47,10 @@ export async function cachedJson({ cacheFile, url, refresh = false, offline = fa
} }
/** /**
* Load audio_data.json (official) and Ark-Models models_data.json. * Load audio_data.json (official), charword_table.json (official voice slots) and Ark-Models models_data.json.
* @param {string} root project root * @param {string} root project root
* @param {{refresh?:boolean, offline?:boolean, log?:(m:string)=>void}} [opts] * @param {{refresh?:boolean, offline?:boolean, log?:(m:string)=>void}} [opts]
* @returns {Promise<{ audioData: any, modelsData: any }>} * @returns {Promise<{ audioData: any, modelsData: any, charword: any }>}
*/ */
export async function loadIndexes(root, opts = {}) { export async function loadIndexes(root, opts = {}) {
const audioData = await cachedJson({ const audioData = await cachedJson({
@@ -59,6 +60,15 @@ export async function loadIndexes(root, opts = {}) {
offline: opts.offline, offline: opts.offline,
log: opts.log, log: opts.log,
}); });
// 11 MB, and raw.githubusercontent stalls on it often: the jsDelivr mirror cachedJson() falls back to is the
// reliable path (the operator battle voice needs it, plan.mjs indexVoice).
const charword = await cachedJson({
cacheFile: join(root, '.cache', 'gamedata', 'excel', 'charword_table.json'),
url: RAW.gamedata + 'excel/charword_table.json',
refresh: opts.refresh,
offline: opts.offline,
log: opts.log,
});
const modelsData = await cachedJson({ const modelsData = await cachedJson({
cacheFile: join(root, '.cache', 'ark-models', 'models_data.json'), cacheFile: join(root, '.cache', 'ark-models', 'models_data.json'),
url: RAW.arkModels + 'models_data.json', url: RAW.arkModels + 'models_data.json',
@@ -66,5 +76,5 @@ export async function loadIndexes(root, opts = {}) {
offline: opts.offline, offline: opts.offline,
log: opts.log, log: opts.log,
}); });
return { audioData, modelsData }; return { audioData, modelsData, charword };
} }
+53 -3
View File
@@ -20,10 +20,16 @@
import { RAW, joinUrl, safeName, urlBase, urlDir } from './sources.mjs'; import { RAW, joinUrl, safeName, urlBase, urlDir } from './sources.mjs';
import { kindOf } from './formats.mjs'; import { kindOf } from './formats.mjs';
import { pickUnitSfx, UI_SFX, BATTLE_SFX, resolveSpec } from './audio.mjs'; import { pickUnitSfx, UI_SFX, BATTLE_SFX, resolveSpec, indexVoice, VOICE_DIRS, VOICE_BATTLE_SLOTS } from './audio.mjs';
import { literal } from './manifest.mjs'; import { literal } from './manifest.mjs';
import { EMOTE_CATALOG } from '../../shared/constants.js'; import { EMOTE_CATALOG } from '../../shared/constants.js';
/**
* The voice slots are read from the zh_CN charword table, whose voiceIds are always `CN_*`: the other dubs
* (`--voice-lang=jp|en|kr`) share the very same slot numbering and file names, only the dump folder differs.
*/
const VOICE_ID_LANG = 'CN';
/** /**
* Enemies whose Spine no community dump carries: the web model is another enemy's (research 07 §5.6). Their official * Enemies whose Spine no community dump carries: the web model is another enemy's (research 07 §5.6). Their official
* models come from the local client only (tools/local-extract ENEMY_SPINES): `localEnemySpines` adds them as the * models come from the local client only (tools/local-extract ENEMY_SPINES): `localEnemySpines` adds them as the
@@ -127,6 +133,14 @@ function leaf(...alts) {
return list.length ? { alts: list } : null; return list.length ? { alts: list } : null;
} }
/** One operator voice line: charword voiceAsset ('char_263_skadi/CN_023') → `audio/voice/<lang>/<charId>/cn_023.mp3`. */
function voiceAlt(asset, lang) {
const [charId, voiceId] = String(asset).split('/');
if (!charId || !voiceId || !/^[a-z0-9_]+$/i.test(charId) || !/^[a-z]{2}_\d+$/i.test(voiceId)) return null;
const file = `${charId}/${voiceId.toLowerCase()}.mp3`;
return alt(`audio/voice/${lang}/${file}`, joinUrl(RAW.aa2voice, `${VOICE_DIRS[lang]}/${file}`));
}
/** Sound path under sound_beta_2 → alternative under public/assets/audio/<sub>. */ /** Sound path under sound_beta_2 → alternative under public/assets/audio/<sub>. */
function soundAlt(path, sub = 'sfx') { function soundAlt(path, sub = 'sfx') {
const rel = `audio/${sub}/` + path.split('/').map(safeName).join('/'); const rel = `audio/${sub}/` + path.split('/').map(safeName).join('/');
@@ -251,6 +265,11 @@ export function collectEnemyIds({ assets07, enemies05, maps05, ops03 }) {
* @param {any} p.maps05 docs/research/05-maps.json * @param {any} p.maps05 docs/research/05-maps.json
* @param {ReturnType<import('./audio.mjs').indexAudio>} p.audio indexed audio_data.json * @param {ReturnType<import('./audio.mjs').indexAudio>} p.audio indexed audio_data.json
* @param {any} p.modelsData Ark-Models models_data.json * @param {any} p.modelsData Ark-Models models_data.json
* @param {any} [p.charword] parsed excel/charword_table.json — the operators' official voice slots (voice)
* @param {string} [p.voiceLang] voice dump to plan: cn (default) | jp | en | kr
* @param {Iterable<string>|null} [p.voiceSlots] which voice slots to plan: VOICE_BATTLE_SLOTS (default) plans only the
* lines a battle can play, null plans every slot of audio.mjs VOICE_SLOTS (`--voice-all`). The prep-only slots
* (干员报到 / 编入队伍 / 任命队长) are never requested by the client and cost 360 files / 19.3 MB of downloads.
* @param {string[]} [p.extraEnemyIds] more enemy ids that can spawn (e.g. keys of data/enemies.json) * @param {string[]} [p.extraEnemyIds] more enemy ids that can spawn (e.g. keys of data/enemies.json)
* @param {string[]} [p.extraTokenIds] more token ids (e.g. token_* keys of data/tokens.json) * @param {string[]} [p.extraTokenIds] more token ids (e.g. token_* keys of data/tokens.json)
* @param {Record<string,string>} [p.extraHandbook] enemyId → handbook/model id (e.g. from data/bosses.json) * @param {Record<string,string>} [p.extraHandbook] enemyId → handbook/model id (e.g. from data/bosses.json)
@@ -259,7 +278,9 @@ export function collectEnemyIds({ assets07, enemies05, maps05, ops03 }) {
* gets `spineLocal` = { group: 'spine/enemy/<id>', ...meta } beside its web `spine` * gets `spineLocal` = { group: 'spine/enemy/<id>', ...meta } beside its web `spine`
* @returns {{ template: any, models: Map<string, any>, notes: string[] }} * @returns {{ template: any, models: Map<string, any>, notes: string[] }}
*/ */
export function buildPlan({ assets07, ops03, enemies05, maps05, audio, modelsData, extraEnemyIds = [], extraTokenIds = [], extraHandbook = {}, localEnemySpines = {} }) { export function buildPlan({ assets07, ops03, enemies05, maps05, audio, modelsData, charword = null, voiceLang = 'cn',
voiceSlots = VOICE_BATTLE_SLOTS,
extraEnemyIds = [], extraTokenIds = [], extraHandbook = {}, localEnemySpines = {} }) {
const notes = []; const notes = [];
/** @type {Map<string, any>} */ /** @type {Map<string, any>} */
const models = new Map(); const models = new Map();
@@ -533,9 +554,38 @@ export function buildPlan({ assets07, ops03, enemies05, maps05, audio, modelsDat
const sfxBattle = {}; const sfxBattle = {};
for (const [name, spec] of Object.entries(BATTLE_SFX)) { const l = soundLeaf(resolveSpec(spec, audio.bank)); if (l) sfxBattle[name] = l; else notes.push(`battle SFX ${name}: no sound`); } for (const [name, spec] of Object.entries(BATTLE_SFX)) { const l = soundLeaf(resolveSpec(spec, audio.bank)); if (l) sfxBattle[name] = l; else notes.push(`battle SFX ${name}: no sound`); }
// --- 干员战斗语音 (excel/charword_table.json → audio.voice) ---------------------------------------------
// The official lines of every operator the mode can field, for the slots a battle can actually play: 行动出发 start /
// 行动开始 faceEnemy / 选中干员 select / 部署 place / 作战中1-4 skillN / 结算 result* (charword `placeType`,
// audio.mjs VOICE_BATTLE_SLOTS). A slot with several lines stays an array — the client draws one at random
// (public/js/audio.js voice). Operators without official battle voice keep no entry at all: the 17 预备干员
// (char_60x_c*, char_617_sharp2) and the mode's own 盟约·辅助干员 (char_616_pithst).
// The three prep-only slots (干员报到 gacha / 编入队伍 squad / 任命队长 squadFirst) are NOT planned by default: the
// client never requests them, and downloading them adds 360 files / 19.3 MB to every `npm run assets` — pass --voice-all for
// the complete official set (`voiceSlots: null`, reviewer note on the voice PR).
const voice = {};
for (const [charId, slots] of indexVoice(charword, VOICE_ID_LANG, voiceSlots)) {
if (!chars[charId]) continue; // only the operators this game can field (the 138-pool charIds)
const v = {};
for (const [slot, assets] of Object.entries(slots)) {
// one leaf per line (部署1 / 部署2 …): an array stays an array so the client can draw one — chaining them as
// alternatives of a single leaf would keep only the first line that landed on disk.
const lines = assets.map((a) => leaf(voiceAlt(a, voiceLang))).filter(Boolean);
if (!lines.length) continue;
v[slot] = lines.length === 1 ? lines[0] : lines;
}
if (Object.keys(v).length) voice[charId] = v;
}
if (!Object.keys(voice).length) notes.push('battle voice: charword_table.json has no slots (index missing?)');
const template = { const template = {
chars, enemies, tokens, bonds, items, bands, skills, skillsById, ui, prof, chars, enemies, tokens, bonds, items, bands, skills, skillsById, ui, prof,
audio: { bgm, bossBgm: Object.fromEntries(Object.entries(bossBgm).sort(([a], [b]) => a.localeCompare(b, 'en', { numeric: true }))), sfx: { ui: sfxUi, battle: sfxBattle, units: unitsSfx } }, audio: {
bgm,
bossBgm: Object.fromEntries(Object.entries(bossBgm).sort(([a], [b]) => a.localeCompare(b, 'en', { numeric: true }))),
voice,
sfx: { ui: sfxUi, battle: sfxBattle, units: unitsSfx },
},
}; };
return { template, models, notes }; return { template, models, notes };
} }
+17 -7
View File
@@ -34,7 +34,7 @@ import { dirname, join, relative, sep } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { Downloader } from './assets/downloader.mjs'; import { Downloader } from './assets/downloader.mjs';
import { loadIndexes } from './assets/cache.mjs'; import { loadIndexes } from './assets/cache.mjs';
import { indexAudio } from './assets/audio.mjs'; import { indexAudio, VOICE_DIRS } from './assets/audio.mjs';
import { buildPlan } from './assets/plan.mjs'; import { buildPlan } from './assets/plan.mjs';
import { processModels, findLocalEnemyModels, localEnemySpineMeta, loadLocalEnemySpines, LOCAL_ENEMY_SPINES_FILE } from './assets/spine.mjs'; import { processModels, findLocalEnemyModels, localEnemySpineMeta, loadLocalEnemySpines, LOCAL_ENEMY_SPINES_FILE } from './assets/spine.mjs';
import { collectLeaves, downloadLeaves, resolveTemplate, totalBytes, contentHash, droppedEntries, MANIFEST_VERSION } from './assets/manifest.mjs'; import { collectLeaves, downloadLeaves, resolveTemplate, totalBytes, contentHash, droppedEntries, MANIFEST_VERSION } from './assets/manifest.mjs';
@@ -54,7 +54,10 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
--force re-download files even when present --force re-download files even when present
--offline no network: post-process what is on disk and rebuild data/assets.json --offline no network: post-process what is on disk and rebuild data/assets.json
--dry-run print the plan and exit --dry-run print the plan and exit
--refresh-index re-download audio_data.json / models_data.json indexes --refresh-index re-download the audio_data.json / charword_table.json / models_data.json indexes
--voice-lang=cn operator battle voice language: cn (default) | jp | en | kr
--voice-all plan every official voice slot, including the prep-only lines no battle plays
(干员报到 / 编入队伍 / 任命队长; 360 files / 19.3 MB more per run — off by default)
--prune delete files under public/assets that the manifest no longer references --prune delete files under public/assets that the manifest no longer references
(public/assets/local/** of tools/local-extract is never deleted); implies --allow-shrink (public/assets/local/** of tools/local-extract is never deleted); implies --allow-shrink
--allow-shrink write data/assets.json even when it loses entries the current one has --allow-shrink write data/assets.json even when it loses entries the current one has
@@ -66,10 +69,10 @@ const HELP = `Usage: node tools/fetch-assets.mjs [options]
/** /**
* Parse CLI flags. * Parse CLI flags.
* @param {string[]} argv * @param {string[]} argv
* @returns {{concurrency:number, force:boolean, offline:boolean, dryRun:boolean, refreshIndex:boolean, prune:boolean, allowShrink:boolean, localSpines:boolean, help:boolean}} * @returns {{concurrency:number, force:boolean, offline:boolean, dryRun:boolean, refreshIndex:boolean, prune:boolean, allowShrink:boolean, localSpines:boolean, voiceLang:string, voiceAll:boolean, help:boolean}}
*/ */
export function parseArgs(argv) { export function parseArgs(argv) {
const o = { concurrency: 16, force: false, offline: false, dryRun: false, refreshIndex: false, prune: false, allowShrink: false, localSpines: false, help: false }; const o = { concurrency: 16, force: false, offline: false, dryRun: false, refreshIndex: false, prune: false, allowShrink: false, localSpines: false, voiceLang: 'cn', voiceAll: false, help: false };
for (const a of argv) { for (const a of argv) {
const [k, v] = a.split('='); const [k, v] = a.split('=');
if (k === '--concurrency') o.concurrency = Math.max(1, Math.min(64, parseInt(v, 10) || 16)); if (k === '--concurrency') o.concurrency = Math.max(1, Math.min(64, parseInt(v, 10) || 16));
@@ -80,6 +83,8 @@ export function parseArgs(argv) {
else if (k === '--prune') o.prune = true; else if (k === '--prune') o.prune = true;
else if (k === '--allow-shrink') o.allowShrink = true; else if (k === '--allow-shrink') o.allowShrink = true;
else if (k === '--local-spines') o.localSpines = true; else if (k === '--local-spines') o.localSpines = true;
else if (k === '--voice-lang') { if (!VOICE_DIRS[v]) throw new Error(`unknown --voice-lang ${v} (cn | jp | en | kr)`); o.voiceLang = v; }
else if (k === '--voice-all') o.voiceAll = true;
else if (k === '--help' || k === '-h') o.help = true; else if (k === '--help' || k === '-h') o.help = true;
else throw new Error(`unknown option ${a}\n${HELP}`); else throw new Error(`unknown option ${a}\n${HELP}`);
} }
@@ -158,6 +163,7 @@ function countStats(m, bytes, files) {
skills: Object.keys(m.skills || {}).length, skills: Object.keys(m.skills || {}).length,
ui: Object.keys(m.ui || {}).length, ui: Object.keys(m.ui || {}).length,
sfxUnits: Object.keys(m.audio?.sfx?.units || {}).length, sfxUnits: Object.keys(m.audio?.sfx?.units || {}).length,
voiceChars: Object.keys(m.audio?.voice || {}).length,
}; };
} }
@@ -218,7 +224,7 @@ async function main() {
readJson('docs/research/05-enemies.json'), readJson('docs/research/05-enemies.json'),
readJson('docs/research/05-maps.json'), readJson('docs/research/05-maps.json'),
]); ]);
const { audioData, modelsData } = await loadIndexes(ROOT, { refresh: opts.refreshIndex && !opts.offline, offline: opts.offline, log }); const { audioData, modelsData, charword } = await loadIndexes(ROOT, { refresh: opts.refreshIndex && !opts.offline, offline: opts.offline, log });
const audio = indexAudio(audioData); const audio = indexAudio(audioData);
// The game data built by tools/build-data.mjs (when present) may reference more // The game data built by tools/build-data.mjs (when present) may reference more
// spawnable enemies/tokens than research lists (e.g. 机变 enemy swaps): cover them too. // spawnable enemies/tokens than research lists (e.g. 机变 enemy swaps): cover them too.
@@ -228,7 +234,9 @@ async function main() {
for (const b of Object.values(dataBosses || {})) if (b?.enemyKey && typeof b.handbookId === 'string') extraHandbook[b.enemyKey] = b.handbookId; for (const b of Object.values(dataBosses || {})) if (b?.enemyKey && typeof b.handbookId === 'string') extraHandbook[b.enemyKey] = b.handbookId;
const localEnemySpines = await syncLocalEnemySpines(opts); const localEnemySpines = await syncLocalEnemySpines(opts);
const plan = buildPlan({ const plan = buildPlan({
assets07, ops03, enemies05, maps05, audio, modelsData, assets07, ops03, enemies05, maps05, audio, modelsData, charword, voiceLang: opts.voiceLang,
// default: only the slots a battle can play (plan.mjs VOICE_BATTLE_SLOTS); --voice-all takes the whole official set
voiceSlots: opts.voiceAll ? null : undefined,
extraEnemyIds: Object.keys(dataEnemies || {}), extraEnemyIds: Object.keys(dataEnemies || {}),
extraTokenIds: Object.keys(dataTokens || {}), extraTokenIds: Object.keys(dataTokens || {}),
extraHandbook, extraHandbook,
@@ -238,7 +246,8 @@ async function main() {
log(`[plan] ${leaves.length} files + ${plan.models.size} Spine models ` + log(`[plan] ${leaves.length} files + ${plan.models.size} Spine models ` +
`(${Object.keys(plan.template.chars).length} chars, ${Object.keys(plan.template.enemies).length} enemies, ` + `(${Object.keys(plan.template.chars).length} chars, ${Object.keys(plan.template.enemies).length} enemies, ` +
`${Object.keys(plan.template.tokens).length} tokens, ${Object.keys(plan.template.ui).length} UI sprites, ` + `${Object.keys(plan.template.tokens).length} tokens, ${Object.keys(plan.template.ui).length} UI sprites, ` +
`${Object.keys(plan.template.audio.sfx.units).length} units with SFX)`); `${Object.keys(plan.template.audio.sfx.units).length} units with SFX, ` +
`${Object.keys(plan.template.audio.voice).length} operators with ${opts.voiceLang.toUpperCase()} voice)`);
if (opts.dryRun) { if (opts.dryRun) {
for (const n of plan.notes) log(` note: ${n}`); for (const n of plan.notes) log(` note: ${n}`);
return 0; return 0;
@@ -324,6 +333,7 @@ async function main() {
log(`on disk (manifest) : ${mb(s.bytes)} in ${s.files} files`); log(`on disk (manifest) : ${mb(s.bytes)} in ${s.files} files`);
log(`chars ${s.chars} (Back model ${s.charsWithBack}) · enemies ${s.enemies} (Spine ${s.enemiesWithSpine}) · tokens ${s.tokens} (Spine ${s.tokensWithSpine}) · Spine models ${s.spineModels}`); log(`chars ${s.chars} (Back model ${s.charsWithBack}) · enemies ${s.enemies} (Spine ${s.enemiesWithSpine}) · tokens ${s.tokens} (Spine ${s.tokensWithSpine}) · Spine models ${s.spineModels}`);
log(`bonds ${s.bonds} · items ${s.items} · bands ${s.bands} · skill icons ${s.skills} · UI ${s.ui} · units with SFX ${s.sfxUnits}`); log(`bonds ${s.bonds} · items ${s.items} · bands ${s.bands} · skill icons ${s.skills} · UI ${s.ui} · units with SFX ${s.sfxUnits}`);
log(`operator battle voice: ${s.voiceChars} charIds (--voice-lang=${opts.voiceLang})`);
log(`fonts: ${Object.values(fonts.files).map((f) => f.woff2 || f.original).join(', ') || 'none'}`); log(`fonts: ${Object.values(fonts.files).map((f) => f.woff2 || f.original).join(', ') || 'none'}`);
if (resolved.fallbacks.length) { log(`fallbacks used (${resolved.fallbacks.length}):`); for (const f of resolved.fallbacks.slice(0, 20)) log(` ${f}`); } if (resolved.fallbacks.length) { log(`fallbacks used (${resolved.fallbacks.length}):`); for (const f of resolved.fallbacks.slice(0, 20)) log(` ${f}`); }
if (downloadErrors.length) log(`download errors (${downloadErrors.length}, re-run to retry): ${downloadErrors.slice(0, 10).join(', ')}`); if (downloadErrors.length) log(`download errors (${downloadErrors.length}, re-run to retry): ${downloadErrors.slice(0, 10).join(', ')}`);