Playtest #5 fixes: hovering enemies, target order, blocking hand-over, 联防 knocked-out, elements, huge-boss hit area, maps, gear icon, phone HUD, 至简 price

- #1 近地悬浮 enemies count as air units (Unit.isFlying): ground-only attacks, splash, terrain and traps skip them;
  PRTS 不可对空 skill effects made ground-only; 掠海漂移体 / 吉兆飞鳞 drop modes; the bot plans them as air
- #2 an operator knocked out before 联防 is fielded and force-exited (PRTS 强制退场): it lies on its tile with the
  redeploy ring and redeploys (full timer [ASSUMED]); partners, observers and the server fallback included
- #3 element audit of every operator, item and enemy: 海嗣 numbers are official; bursts are 无来源, 元素脆弱 only
  raises 元素伤害, 元素抗性 applied, 盟约·辅助干员 fills all three gauges, 菲莱 / 哈洛德 / 纯烬 / enemy kits fixed
- #4 blocking by the official contact radius (0.7071): another operator in contact takes over at once when the
  blocker dies, withdraws or is stunned; melee blockers attack what they block
- #5 enemy target order per PRTS 索敌: blocker, special priority, taunt, last deployed; initial deploy 从上到下>从左到右,
  operators before summons; special priorities break ties by taunt then deploy order
- #6 act2 m01's normal-field blower machines (row 13) are drawn in the normal / 联防 views
- #7 Final Assault prep legality reads the player's half of the boss field (fence tiles deployable); all 11 stages
  audited against the official level files
- #8 regular settings gear; #9 the prep camera keeps the bench and temp rows clear of the shop bar on phones
- #10 huge bosses have the official 4.95 × 2.95 hit area (sim/body.js) and are 自缚 / unblockable
- #11 至简 costs 1: 购买价格为N's bb.price is a discount off the tier price, applied before other price effects
- DESIGN §19 and the normative §3 / §5 / §6 / §7 lines, SIM / META / DATA / PLAYING updated; 2595 unit tests and
  every browser suite green; dev recordings regenerated
This commit is contained in:
sganggs committed 2026-09-30 19:12:36 +08:00
1 parent d4f868cd19
commit 15291c2293
104 files changed
+5539 -662

No files matched your search

+6 -3
View File
@@ -38,9 +38,10 @@ input, textarea, select { -webkit-user-select: text; user-select: text; }
layer: step back over the HUD's safe-area offset so it stays on its unit on a notched phone */
.gm__hud > .uframe { margin-left: calc(-1 * var(--sa-l)); margin-top: calc(-1 * var(--sa-t)); }
/* the shop bar keeps its place at the bottom edge (it may reach into the home-indicator strip): raised by the bottom
inset it covered ~3/4 of the bench pads 2–9 on a notched phone (844×390 with the iPhone insets: 23 % of those tiles
left free, 71 % now — as without insets), and a bench piece is pressed / dropped on by its tile (user playtest #4
item 1). Its right edge stays inside the inset (the notch / rounded corner). */
inset it covered ~3/4 of the bench pads 2–9 on a notched phone (user playtest #4 item 1), and a bench piece is
pressed / dropped on by its tile. The prep camera keeps the bench clear of the bar (ui/fieldHost.js hudBands →
render/projection.js clearHud, user playtest #5 item 9). Its right edge stays inside the inset (the notch / rounded
corner). */
.gm__hud > .shopbar { bottom: calc(.2rem - var(--sa-b)); }
.gm > .spov { left: calc(2.8rem + var(--sa-l)); right: var(--sa-r); }
/* every other screen is laid out inside the safe area (the app backdrop fills the notch strip) */
@@ -84,6 +85,8 @@ html:not(.sp-in-match) .toast-host { margin-top: var(--sa-t); }
.readybtn { height: max(.56rem, 34px); }
.toolbtn { min-height: max(.5rem, 30px); }
.gm__gear { width: max(.56rem, 34px); height: max(.56rem, 34px); }
/* ⚙ / 📖 / ⛶ keep their desktop proportion (half the button) instead of an 11 px glyph in the grown button */
.gm__corner .gm__gear .icon { width: max(.28rem, 17px); height: max(.28rem, 17px); }
.gtop__exit { min-width: max(.56rem, 34px); min-height: max(.56rem, 34px); }
.gtop__iconbtn.has-sprite, .enemybtn.has-sprite { height: max(.46rem, 26px); width: calc(max(.46rem, 26px) * 35 / 29); }
.gtop__iconbtn.has-sprite.is-wide, .enemybtn.has-sprite.is-wide { width: calc(max(.46rem, 26px) * 61 / 29); }
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"boss-m02","title":"最终攻势 · 卢西恩,“猩红血钻” · R14","kind":"boss","stageId":"act2autochess_m02","modeId":"mode_multi_normal","round":14,"wave":"act2autochess_h07_05","tick":0.03333333333333333,"snapEvery":3,"duration":75,"recordedAt":"2026-09-27","field":{"fieldId":"b1","kind":"boss","rect":{"r0":0,"r1":5,"c0":0,"c1":20},"stageId":"act2autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":2,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_demkni"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_b","name":"山","tier":5,"golden":true,"spine":"char_264_f12yin","avatar":"char_264_f12yin_2","x":8,"y":2,"facing":1,"dir":"RIGHT","maxHp":2738,"uid":2,"skillIndex":1,"moduleId":"uniequip_002_f12yin"},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_b","name":"史尔特尔","tier":5,"golden":true,"spine":"char_350_surtr","avatar":"char_350_surtr_2","x":7,"y":2,"facing":1,"dir":"RIGHT","maxHp":2680,"uid":3,"skillIndex":2,"moduleId":"uniequip_002_surtr"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_b","name":"夕","tier":5,"golden":true,"spine":"char_2015_dusk","avatar":"char_2015_dusk_2","x":4,"y":3,"facing":1,"dir":"RIGHT","maxHp":1787,"uid":4,"skillIndex":0,"moduleId":"uniequip_002_dusk"},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":4,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5,"skillIndex":2},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_b","name":"烛煌","tier":5,"golden":true,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2_2","x":6,"y":5,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":6,"skillIndex":2,"moduleId":"uniequip_002_blaze2"},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":5,"facing":1,"dir":"RIGHT","maxHp":1184,"uid":7,"skillIndex":2},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_6_05_a","name":"异客","tier":6,"golden":false,"spine":"char_472_pasngr","avatar":"char_472_pasngr","x":8,"y":5,"facing":1,"dir":"RIGHT","maxHp":1215,"uid":8,"skillIndex":2},{"id":9,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_03_b","name":"余","tier":6,"golden":true,"spine":"char_2026_yu","avatar":"char_2026_yu_2","x":11,"y":2,"facing":-1,"dir":"LEFT","maxHp":3430,"uid":101,"skillIndex":2,"moduleId":"uniequip_002_yu"},{"id":10,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_17_a","name":"耀骑士临光","tier":6,"golden":false,"spine":"char_1014_nearl2","avatar":"char_1014_nearl2","x":12,"y":2,"facing":-1,"dir":"LEFT","maxHp":2698,"uid":102,"skillIndex":2},{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_19_b","name":"锏","tier":6,"golden":true,"spine":"char_4116_blkkgt","avatar":"char_4116_blkkgt_2","x":13,"y":2,"facing":-1,"dir":"LEFT","maxHp":2767,"uid":103,"skillIndex":2,"moduleId":"uniequip_002_blkkgt"},{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_01_b","name":"蕾缪安","tier":6,"golden":true,"spine":"char_4193_lemuen","avatar":"char_4193_lemuen_2","x":16,"y":3,"facing":-1,"dir":"LEFT","maxHp":1470,"uid":104,"skillIndex":2,"moduleId":"uniequip_002_lemuen"},{"id":13,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_08_a","name":"焰影苇草","tier":6,"golden":false,"spine":"char_1020_reed2","avatar":"char_1020_reed2","x":15,"y":4,"facing":-1,"dir":"LEFT","maxHp":1361,"uid":105,"skillIndex":2},{"id":14,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_12_b","name":"迷迭香","tier":6,"golden":true,"spine":"char_391_rosmon","avatar":"char_391_rosmon_2","x":14,"y":5,"facing":-1,"dir":"LEFT","maxHp":2887,"uid":106,"skillIndex":1,"moduleId":"uniequip_002_rosmon"},{"id":15,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_14_a","name":"流明","tier":6,"golden":false,"spine":"char_4042_lumen","avatar":"char_4042_lumen","x":13,"y":5,"facing":-1,"dir":"LEFT","maxHp":1569,"uid":107,"skillIndex":2},{"id":16,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_20_a","name":"纯烬艾雅法拉","tier":6,"golden":false,"spine":"char_1016_agoat2","avatar":"char_1016_agoat2","x":12,"y":5,"facing":-1,"dir":"LEFT","maxHp":1237,"uid":108,"skillIndex":2},{"id":17,"kind":"enemy","side":"enemy","ownerId":"P1","defId":"enemy_2016_csphtm","name":"卢西恩,“猩红血钻”","tier":3,"golden":false,"spine":"enemy_2016_csphtm","avatar":"enemy_2016_csphtm","x":9.99,"y":4,"facing":1,"dir":"RIGHT","maxHp":195000,"boss":true}]},"frames":[{"snap":{"fieldId":"b1","t":0.1,"units":[[1,9,2,3003,Line truncated
{"version":1,"name":"boss-m02","title":"最终攻势 · 卢西恩,“猩红血钻” · R14","kind":"boss","stageId":"act2autochess_m02","modeId":"mode_multi_normal","round":14,"wave":"act2autochess_h07_05","tick":0.03333333333333333,"snapEvery":3,"duration":75,"recordedAt":"2026-09-27","field":{"fieldId":"b1","kind":"boss","rect":{"r0":0,"r1":5,"c0":0,"c1":20},"stageId":"act2autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":2,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_demkni"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_b","name":"山","tier":5,"golden":true,"spine":"char_264_f12yin","avatar":"char_264_f12yin_2","x":8,"y":2,"facing":1,"dir":"RIGHT","maxHp":2738,"uid":2,"skillIndex":1,"moduleId":"uniequip_002_f12yin"},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_b","name":"史尔特尔","tier":5,"golden":true,"spine":"char_350_surtr","avatar":"char_350_surtr_2","x":7,"y":2,"facing":1,"dir":"RIGHT","maxHp":2680,"uid":3,"skillIndex":2,"moduleId":"uniequip_002_surtr"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_b","name":"夕","tier":5,"golden":true,"spine":"char_2015_dusk","avatar":"char_2015_dusk_2","x":4,"y":3,"facing":1,"dir":"RIGHT","maxHp":1787,"uid":4,"skillIndex":0,"moduleId":"uniequip_002_dusk"},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":4,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5,"skillIndex":2},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_b","name":"烛煌","tier":5,"golden":true,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2_2","x":6,"y":5,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":6,"skillIndex":2,"moduleId":"uniequip_002_blaze2"},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":5,"facing":1,"dir":"RIGHT","maxHp":1184,"uid":7,"skillIndex":2},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_6_05_a","name":"异客","tier":6,"golden":false,"spine":"char_472_pasngr","avatar":"char_472_pasngr","x":8,"y":5,"facing":1,"dir":"RIGHT","maxHp":1215,"uid":8,"skillIndex":2},{"id":9,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_03_b","name":"余","tier":6,"golden":true,"spine":"char_2026_yu","avatar":"char_2026_yu_2","x":11,"y":2,"facing":-1,"dir":"LEFT","maxHp":3430,"uid":101,"skillIndex":2,"moduleId":"uniequip_002_yu"},{"id":10,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_17_a","name":"耀骑士临光","tier":6,"golden":false,"spine":"char_1014_nearl2","avatar":"char_1014_nearl2","x":12,"y":2,"facing":-1,"dir":"LEFT","maxHp":2698,"uid":102,"skillIndex":2},{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_19_b","name":"锏","tier":6,"golden":true,"spine":"char_4116_blkkgt","avatar":"char_4116_blkkgt_2","x":13,"y":2,"facing":-1,"dir":"LEFT","maxHp":2767,"uid":103,"skillIndex":2,"moduleId":"uniequip_002_blkkgt"},{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_01_b","name":"蕾缪安","tier":6,"golden":true,"spine":"char_4193_lemuen","avatar":"char_4193_lemuen_2","x":16,"y":3,"facing":-1,"dir":"LEFT","maxHp":1470,"uid":104,"skillIndex":2,"moduleId":"uniequip_002_lemuen"},{"id":13,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_08_a","name":"焰影苇草","tier":6,"golden":false,"spine":"char_1020_reed2","avatar":"char_1020_reed2","x":15,"y":4,"facing":-1,"dir":"LEFT","maxHp":1361,"uid":105,"skillIndex":2},{"id":14,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_12_b","name":"迷迭香","tier":6,"golden":true,"spine":"char_391_rosmon","avatar":"char_391_rosmon_2","x":14,"y":5,"facing":-1,"dir":"LEFT","maxHp":2887,"uid":106,"skillIndex":1,"moduleId":"uniequip_002_rosmon"},{"id":15,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_14_a","name":"流明","tier":6,"golden":false,"spine":"char_4042_lumen","avatar":"char_4042_lumen","x":13,"y":5,"facing":-1,"dir":"LEFT","maxHp":1569,"uid":107,"skillIndex":2},{"id":16,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_6_20_a","name":"纯烬艾雅法拉","tier":6,"golden":false,"spine":"char_1016_agoat2","avatar":"char_1016_agoat2","x":12,"y":5,"facing":-1,"dir":"LEFT","maxHp":1237,"uid":108,"skillIndex":2},{"id":17,"kind":"enemy","side":"enemy","ownerId":"P1","defId":"enemy_2016_csphtm","name":"卢西恩,“猩红血钻”","tier":3,"golden":false,"spine":"enemy_2016_csphtm","avatar":"enemy_2016_csphtm","x":9.99,"y":4,"facing":1,"dir":"RIGHT","maxHp":195000,"boss":true}]},"frames":[{"snap":{"fieldId":"b1","t":0.1,"units":[[1,9,2,3003,Line truncated
+20 -20
View File
@@ -6,12 +6,12 @@
"stageId": "act2autochess_m01",
"duration": 55,
"frames": 550,
"maxUnits": 30,
"maxUnits": 29,
"file": "normal-m01.json",
"bytes": 433457,
"bytes": 424452,
"result": {
"reason": "timeout",
"killed": 12,
"killed": 13,
"total": 32
}
},
@@ -22,12 +22,12 @@
"stageId": "act2autochess_m02",
"duration": 85,
"frames": 850,
"maxUnits": 26,
"maxUnits": 27,
"file": "normal-m02.json",
"bytes": 618614,
"bytes": 634795,
"result": {
"reason": "timeout",
"killed": 16,
"killed": 15,
"total": 35
}
},
@@ -38,12 +38,12 @@
"stageId": "act2autochess_m03",
"duration": 90,
"frames": 900,
"maxUnits": 33,
"maxUnits": 35,
"file": "normal-m03.json",
"bytes": 791453,
"bytes": 809504,
"result": {
"reason": "timeout",
"killed": 15,
"killed": 12,
"total": 46
}
},
@@ -54,12 +54,12 @@
"stageId": "act2autochess_m04",
"duration": 70,
"frames": 700,
"maxUnits": 35,
"maxUnits": 37,
"file": "normal-m04.json",
"bytes": 597582,
"bytes": 626583,
"result": {
"reason": "timeout",
"killed": 23,
"killed": 21,
"total": 39
}
},
@@ -72,7 +72,7 @@
"frames": 843,
"maxUnits": 30,
"file": "late-m03-r12.json",
"bytes": 638858,
"bytes": 632912,
"result": {
"reason": "cleared",
"killed": 12,
@@ -86,9 +86,9 @@
"stageId": "act2autochess_m01",
"duration": 110,
"frames": 1100,
"maxUnits": 33,
"maxUnits": 34,
"file": "late-m01-r13.json",
"bytes": 904761,
"bytes": 902188,
"result": {
"reason": "timeout",
"killed": 6,
@@ -104,10 +104,10 @@
"frames": 600,
"maxUnits": 36,
"file": "unite-m01.json",
"bytes": 636264,
"bytes": 636396,
"result": {
"reason": "timeout",
"killed": 17,
"killed": 19,
"total": 40
}
},
@@ -118,12 +118,12 @@
"stageId": "act2autochess_m02",
"duration": 75,
"frames": 750,
"maxUnits": 24,
"maxUnits": 25,
"file": "boss-m02.json",
"bytes": 714082,
"bytes": 703728,
"result": {
"reason": "forced",
"killed": 12,
"killed": 13,
"total": 18
}
},
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"late-m01-r13","title":"常规作战 · 源石流发生装置 · R13(官方波次)","kind":"normal","stageId":"act2autochess_m01","modeId":"mode_multi_hard","round":13,"wave":"act1autochess_h06","tick":0.03333333333333333,"snapEvery":3,"duration":110,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_b","name":"星熊","tier":4,"golden":true,"spine":"char_136_hsguma","avatar":"char_136_hsguma_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3513,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_hsguma"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_b","name":"银灰","tier":4,"golden":true,"spine":"char_172_svrash","avatar":"char_172_svrash_2","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":2569,"uid":2,"skillIndex":2,"moduleId":"uniequip_002_svrash"},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_b","name":"斯卡蒂","tier":3,"golden":true,"spine":"char_263_skadi","avatar":"char_263_skadi_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":3853,"uid":3,"skillIndex":2,"moduleId":"uniequip_002_skadi"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_b","name":"莫斯提马","tier":4,"golden":true,"spine":"char_213_mostma","avatar":"char_213_mostma_2","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1695,"uid":4,"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_b","name":"寒芒克洛丝","tier":4,"golden":true,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1561,"uid":6,"skillIndex":1,"moduleId":"uniequip_002_kroos2"},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7,"skillIndex":1},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_b","name":"远牙","tier":4,"golden":true,"spine":"char_430_fartth","avatar":"char_430_fartth_2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1419,"uid":8,"skillIndex":2,"moduleId":"uniequip_002_fartth"}]},"preview":[{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":2,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"upper","t":3,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":7,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":1,"gate":"upper","t":7,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":10,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":11,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"upper","t":21,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":32,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":37,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":39,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":55,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":62,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null}],"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3513,3513,0,0,0,6],[2,8,9,2569,2569,60.1,90,0,6],[3,7,9,3853,3853,60.1,90,0,6],[4,4,10,1695,1695,62.1,110,0,6],[5,4,11,1271,1271,70.1,100,0,6],[6,4,12,1561,1561,20.1,40,0,6],[7,5,12,1288,1288,20.1,55,0,6],[8,6,12,1419,1419,7.1,24,0,6]],"dp":10,"killed":0,"total":55},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_b","name":"寒芒克洛丝","tier":4,"golden":true,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1561,"uid":6,"skillIndex":1,"moduleId":"uniequip_002_kroos2"}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"Line truncated
{"version":1,"name":"late-m01-r13","title":"常规作战 · 源石流发生装置 · R13(官方波次)","kind":"normal","stageId":"act2autochess_m01","modeId":"mode_multi_hard","round":13,"wave":"act1autochess_h06","tick":0.03333333333333333,"snapEvery":3,"duration":110,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_b","name":"星熊","tier":4,"golden":true,"spine":"char_136_hsguma","avatar":"char_136_hsguma_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3513,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_hsguma"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_b","name":"银灰","tier":4,"golden":true,"spine":"char_172_svrash","avatar":"char_172_svrash_2","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":2569,"uid":2,"skillIndex":2,"moduleId":"uniequip_002_svrash"},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_b","name":"斯卡蒂","tier":3,"golden":true,"spine":"char_263_skadi","avatar":"char_263_skadi_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":3853,"uid":3,"skillIndex":2,"moduleId":"uniequip_002_skadi"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_b","name":"莫斯提马","tier":4,"golden":true,"spine":"char_213_mostma","avatar":"char_213_mostma_2","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1695,"uid":4,"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_b","name":"寒芒克洛丝","tier":4,"golden":true,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1561,"uid":6,"skillIndex":1,"moduleId":"uniequip_002_kroos2"},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7,"skillIndex":1},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_b","name":"远牙","tier":4,"golden":true,"spine":"char_430_fartth","avatar":"char_430_fartth_2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1419,"uid":8,"skillIndex":2,"moduleId":"uniequip_002_fartth"}]},"preview":[{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":2,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"upper","t":3,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":7,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":1,"gate":"upper","t":7,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":10,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":11,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"upper","t":21,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":32,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":37,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":39,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":55,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":62,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null}],"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3513,3513,0,0,0,6],[2,8,9,2569,2569,60.1,90,0,6],[3,7,9,3853,3853,60.1,90,0,6],[4,4,10,1695,1695,62.1,110,0,6],[5,4,11,1271,1271,70.1,100,0,6],[6,4,12,1561,1561,20.1,40,0,6],[7,5,12,1288,1288,20.1,55,0,6],[8,6,12,1419,1419,7.1,24,0,6]],"dp":10,"killed":0,"total":55},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_b","name":"寒芒克洛丝","tier":4,"golden":true,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1561,"uid":6,"skillIndex":1,"moduleId":"uniequip_002_kroos2"}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"Line truncated
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"late-m03-r12","title":"常规作战 · 排气格栅 · R12(官方波次)","kind":"normal","stageId":"act2autochess_m03","modeId":"mode_multi_hard","round":12,"wave":"act1autochess_h05","tick":0.03333333333333333,"snapEvery":3,"duration":84.23,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_demkni"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_b","name":"星熊","tier":4,"golden":true,"spine":"char_136_hsguma","avatar":"char_136_hsguma_2","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":3513,"uid":2,"skillIndex":1,"moduleId":"uniequip_002_hsguma"},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_b","name":"山","tier":5,"golden":true,"spine":"char_264_f12yin","avatar":"char_264_f12yin_2","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2738,"uid":3,"skillIndex":1,"moduleId":"uniequip_002_f12yin"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_b","name":"史尔特尔","tier":5,"golden":true,"spine":"char_350_surtr","avatar":"char_350_surtr_2","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":2680,"uid":4,"skillIndex":2,"moduleId":"uniequip_002_surtr"},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_b","name":"夕","tier":5,"golden":true,"spine":"char_2015_dusk","avatar":"char_2015_dusk_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1787,"uid":5,"skillIndex":0,"moduleId":"uniequip_002_dusk"},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_b","name":"莫斯提马","tier":4,"golden":true,"spine":"char_213_mostma","avatar":"char_213_mostma_2","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1695,"uid":6,"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_b","name":"铃兰","tier":5,"golden":true,"spine":"char_358_lisa","avatar":"char_358_lisa_2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1480,"uid":7,"skillIndex":2,"moduleId":"uniequip_002_lisa"},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2","x":3,"y":9,"facing":1,"dir":"RIGHT","maxHp":2775,"uid":8,"skillIndex":2,"moduleId":"uniequip_002_horn"},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}]},"preview":[{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":2,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":5,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"upper","t":5,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"lower","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":10,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"upper","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":12,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"upper","t":21,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"lower","t":30,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"lower","t":31,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":35,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":37,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":51,"fly":false,"eLine truncated
{"version":1,"name":"late-m03-r12","title":"常规作战 · 排气格栅 · R12(官方波次)","kind":"normal","stageId":"act2autochess_m03","modeId":"mode_multi_hard","round":12,"wave":"act1autochess_h05","tick":0.03333333333333333,"snapEvery":3,"duration":84.23,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_demkni"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_b","name":"星熊","tier":4,"golden":true,"spine":"char_136_hsguma","avatar":"char_136_hsguma_2","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":3513,"uid":2,"skillIndex":1,"moduleId":"uniequip_002_hsguma"},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_b","name":"山","tier":5,"golden":true,"spine":"char_264_f12yin","avatar":"char_264_f12yin_2","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2738,"uid":3,"skillIndex":1,"moduleId":"uniequip_002_f12yin"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_b","name":"史尔特尔","tier":5,"golden":true,"spine":"char_350_surtr","avatar":"char_350_surtr_2","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":2680,"uid":4,"skillIndex":2,"moduleId":"uniequip_002_surtr"},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_b","name":"夕","tier":5,"golden":true,"spine":"char_2015_dusk","avatar":"char_2015_dusk_2","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1787,"uid":5,"skillIndex":0,"moduleId":"uniequip_002_dusk"},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_b","name":"莫斯提马","tier":4,"golden":true,"spine":"char_213_mostma","avatar":"char_213_mostma_2","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1695,"uid":6,"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_b","name":"铃兰","tier":5,"golden":true,"spine":"char_358_lisa","avatar":"char_358_lisa_2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1480,"uid":7,"skillIndex":2,"moduleId":"uniequip_002_lisa"},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2","x":3,"y":9,"facing":1,"dir":"RIGHT","maxHp":2775,"uid":8,"skillIndex":2,"moduleId":"uniequip_002_horn"},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}]},"preview":[{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":2,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":5,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"upper","t":5,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"lower","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":5,"gate":"lower","t":10,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"upper","t":10,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":12,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"upper","t":21,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":3,"gate":"lower","t":30,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1254_lypa_2","count":3,"gate":"lower","t":31,"fly":false,"elite":true,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":35,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1249_lysdb_2","count":5,"gate":"lower","t":37,"fly":false,"elite":false,"boss":false,"source":"wave","tag":null},{"enemyKey":"enemy_1251_lysyta","count":4,"gate":"lower","t":51,"fly":false,"eLine truncated
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"normal-m01","title":"常规作战 · 源石流发生装置 · R5","kind":"normal","stageId":"act2autochess_m01","modeId":"mode_multi_normal","round":5,"wave":"act1autochess_05","tick":0.03333333333333333,"snapEvery":3,"duration":55,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1285,"uid":2,"skillIndex":1},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_b","name":"艾丝黛尔","tier":1,"golden":true,"spine":"char_127_estell","avatar":"char_127_estell_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2389,"uid":3,"skillIndex":1,"moduleId":"uniequip_002_estell"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1123,"uid":4,"skillIndex":1},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1260,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":6,"skillIndex":1},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":7,"skillIndex":2}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2216,2216,0.1,47,0,6],[2,8,9,1285,1285,23.1,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.1,24,0,6],[5,4,11,1260,1260,0.1,60,0,6],[6,4,12,1400,1400,0.1,26,0,6],[7,5,12,1472,1472,20.1,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":6,"skillIndex":1}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":7,"skillIndex":2}],["deploy",7],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5,"skillIndex":1}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1123,"uid":4,"skillIndex":1}],["deploy",4],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_b","name":"艾丝黛尔","tier":1,"golden":true,"spine":"char_127_estell","avatar":"char_127_estell_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2389,"uid":3,"skillIndex":1,"moduleId":"uniequip_002_estell"}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1285,"uid":2,"skillIndex":1}],["deploy",2],["spawn",{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":1,"skillIndex":1}],["deploy",1],["status",5,"talent:angel_bless_ally",1],["fx","buff",4,11,{"id":5,"src":7,"talent":"angel_bless"}]]},{"snap":{"fieldId":"n:P1","t":0.2,"units":[[1,9,9,2216,2216,0.2,47,0,6],[2,8,9,1285,1285,23.2,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.2,24,0,6],[5,4,11,1260,1260,0.2,60,0,6],[6,4,12,1400,1400,0.2,26,0,6],[7,5,12,1472,1472,20.2,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.3,"units":[[1,9,9,2216,2216,0.3,47,0,6],[2,8,9,1285,1285,23.3,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.3,24,0,6],[5,4,11,1260,1260,0.3,60,0,6],[6,4,12,1400,1400,0.3,26,0,6],[7,5,12,1472,1472,20Line truncated
{"version":1,"name":"normal-m01","title":"常规作战 · 源石流发生装置 · R5","kind":"normal","stageId":"act2autochess_m01","modeId":"mode_multi_normal","round":5,"wave":"act1autochess_05","tick":0.03333333333333333,"snapEvery":3,"duration":55,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1285,"uid":2,"skillIndex":1},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_b","name":"艾丝黛尔","tier":1,"golden":true,"spine":"char_127_estell","avatar":"char_127_estell_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2389,"uid":3,"skillIndex":1,"moduleId":"uniequip_002_estell"},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1123,"uid":4,"skillIndex":1},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1260,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":6,"skillIndex":1},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":7,"skillIndex":2}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2216,2216,0.1,47,0,6],[2,8,9,1285,1285,23.1,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.1,24,0,6],[5,4,11,1260,1260,0.1,60,0,6],[6,4,12,1400,1400,0.1,26,0,6],[7,5,12,1472,1472,20.1,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":6,"skillIndex":1}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":7,"skillIndex":2}],["deploy",7],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5,"skillIndex":1}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_01_a","name":"隐现","tier":1,"golden":false,"spine":"char_498_inside","avatar":"char_498_inside","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1123,"uid":4,"skillIndex":1}],["deploy",4],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_12_b","name":"艾丝黛尔","tier":1,"golden":true,"spine":"char_127_estell","avatar":"char_127_estell_2","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2389,"uid":3,"skillIndex":1,"moduleId":"uniequip_002_estell"}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1285,"uid":2,"skillIndex":1}],["deploy",2],["spawn",{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_02_a","name":"角峰","tier":1,"golden":false,"spine":"char_199_yak","avatar":"char_199_yak","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":1,"skillIndex":1}],["deploy",1],["status",5,"talent:angel_bless_ally",1],["fx","buff",4,11,{"id":5,"src":7,"talent":"angel_bless"}]]},{"snap":{"fieldId":"n:P1","t":0.2,"units":[[1,9,9,2216,2216,0.2,47,0,6],[2,8,9,1285,1285,23.2,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.2,24,0,6],[5,4,11,1260,1260,0.2,60,0,6],[6,4,12,1400,1400,0.2,26,0,6],[7,5,12,1472,1472,20.2,44,0,6]],"dp":11,"killed":0,"total":32},"ev":[]},{"snap":{"fieldId":"n:P1","t":0.3,"units":[[1,9,9,2216,2216,0.3,47,0,6],[2,8,9,1285,1285,23.3,40,0,6],[3,7,9,2389,2389,10,23,0,6],[4,4,10,1123,1123,8.3,24,0,6],[5,4,11,1260,1260,0.3,60,0,6],[6,4,12,1400,1400,0.3,26,0,6],[7,5,12,1472,1472,20Line truncated
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"normal-m02","title":"常规作战 · 沼泽控制 · R8","kind":"normal","stageId":"act2autochess_m02","modeId":"mode_multi_hard","round":8,"wave":"act1autochess_h01","tick":0.03333333333333333,"snapEvery":3,"duration":85,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2849,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":2,"skillIndex":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_a","name":"斯卡蒂","tier":3,"golden":false,"spine":"char_263_skadi","avatar":"char_263_skadi","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2938,"uid":3,"skillIndex":2},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1428,"uid":4,"skillIndex":2},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1246,"uid":6,"skillIndex":1},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":7,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7,"skillIndex":1},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":8,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":8,"skillIndex":2},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,8,9,2022,2022,55.1,90,0,6],[3,7,9,2938,2938,55.1,90,0,6],[4,4,10,1428,1428,56.1,110,0,6],[5,5,11,1271,1271,70.1,100,0,6],[6,6,12,1246,1246,15.1,45,0,6],[7,7,12,1288,1288,20.1,55,0,6],[8,8,12,1217,1217,6.1,27,0,6],[9,9,11,100,100,0,0,0,6],[10,9,10,100,100,0,0,0,6]],"dp":10,"killed":0,"total":35},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1246,"uid":6,"skillIndex":1}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":7,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7,"skillIndex":1}],["deploy",7],["spawn",{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":8,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":8,"skillIndex":2}],["deploy",8],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5,"skillIndex":1}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":4,"y":1Line truncated
{"version":1,"name":"normal-m02","title":"常规作战 · 沼泽控制 · R8","kind":"normal","stageId":"act2autochess_m02","modeId":"mode_multi_hard","round":8,"wave":"act1autochess_h01","tick":0.03333333333333333,"snapEvery":3,"duration":85,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m02","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2849,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":2,"skillIndex":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_05_a","name":"斯卡蒂","tier":3,"golden":false,"spine":"char_263_skadi","avatar":"char_263_skadi","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2938,"uid":3,"skillIndex":2},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1428,"uid":4,"skillIndex":2},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1246,"uid":6,"skillIndex":1},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":7,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7,"skillIndex":1},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":8,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":8,"skillIndex":2},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,8,9,2022,2022,55.1,90,0,6],[3,7,9,2938,2938,55.1,90,0,6],[4,4,10,1428,1428,56.1,110,0,6],[5,5,11,1271,1271,70.1,100,0,6],[6,6,12,1246,1246,15.1,45,0,6],[7,7,12,1288,1288,20.1,55,0,6],[8,8,12,1217,1217,6.1,27,0,6],[9,9,11,100,100,0,0,0,6],[10,9,10,100,100,0,0,0,6]],"dp":10,"killed":0,"total":35},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_06_a","name":"寒芒克洛丝","tier":4,"golden":false,"spine":"char_1021_kroos2","avatar":"char_1021_kroos2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1246,"uid":6,"skillIndex":1}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_10_a","name":"阿罗玛","tier":4,"golden":false,"spine":"char_446_aroma","avatar":"char_446_aroma","x":7,"y":12,"facing":1,"dir":"RIGHT","maxHp":1288,"uid":7,"skillIndex":1}],["deploy",7],["spawn",{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":8,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":8,"skillIndex":2}],["deploy",8],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":5,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":5,"skillIndex":1}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":4,"y":1Line truncated
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"normal-m03","title":"常规作战 · 排气格栅 · R11","kind":"normal","stageId":"act2autochess_m03","modeId":"mode_multi_hard","round":11,"wave":"act1autochess_h04","tick":0.03333333333333333,"snapEvery":3,"duration":90,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_demkni"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_a","name":"山","tier":5,"golden":false,"spine":"char_264_f12yin","avatar":"char_264_f12yin","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":2250,"uid":2,"skillIndex":1},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_a","name":"史尔特尔","tier":5,"golden":false,"spine":"char_350_surtr","avatar":"char_350_surtr","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":3,"skillIndex":2},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":0},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5,"skillIndex":2},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1206,"uid":6,"skillIndex":2},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":12,"facing":1,"dir":"RIGHT","maxHp":1184,"uid":7,"skillIndex":2},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":2775,"uid":8,"skillIndex":2,"moduleId":"uniequip_002_horn"},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3003,3003,0.1,8,0,6],[2,6,9,2250,2250,0.1,9,0,6],[3,5,9,2216,2216,0.1,9,0,6],[4,4,12,1404,1404,0.1,7,0,6],[5,5,12,1208,1208,20.1,45,0,6],[6,6,12,1206,1206,13.1,37,8,6],[7,7,12,1184,1184,43.1,70,0,6],[8,4,9,2775,2775,25.1,40,0,6],[9,3,12,100,100,0,0,0,6],[10,3,11,100,100,0,0,0,6],[11,9,11,100,100,0,0,0,6]],"dp":10,"killed":0,"total":46},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":0}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5,"skillIndex":2}],["deploy",5],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":6,"y":12,"facing":1,"dir":"RILine truncated
{"version":1,"name":"normal-m03","title":"常规作战 · 排气格栅 · R11","kind":"normal","stageId":"act2autochess_m03","modeId":"mode_multi_hard","round":11,"wave":"act1autochess_h04","tick":0.03333333333333333,"snapEvery":3,"duration":90,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m03","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_11_b","name":"塞雷娅","tier":5,"golden":true,"spine":"char_202_demkni","avatar":"char_202_demkni_2","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":3003,"uid":1,"skillIndex":1,"moduleId":"uniequip_002_demkni"},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_17_a","name":"山","tier":5,"golden":false,"spine":"char_264_f12yin","avatar":"char_264_f12yin","x":6,"y":9,"facing":1,"dir":"RIGHT","maxHp":2250,"uid":2,"skillIndex":1},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_07_a","name":"史尔特尔","tier":5,"golden":false,"spine":"char_350_surtr","avatar":"char_350_surtr","x":5,"y":9,"facing":1,"dir":"RIGHT","maxHp":2216,"uid":3,"skillIndex":2},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":0},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5,"skillIndex":2},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1206,"uid":6,"skillIndex":2},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_10_a","name":"铃兰","tier":5,"golden":false,"spine":"char_358_lisa","avatar":"char_358_lisa","x":7,"y":12,"facing":1,"dir":"RIGHT","maxHp":1184,"uid":7,"skillIndex":2},{"id":8,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_08_b","name":"号角","tier":5,"golden":true,"spine":"char_4039_horn","avatar":"char_4039_horn_2","x":4,"y":9,"facing":1,"dir":"RIGHT","maxHp":2775,"uid":8,"skillIndex":2,"moduleId":"uniequip_002_horn"},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100},{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,3003,3003,0.1,8,0,6],[2,6,9,2250,2250,0.1,9,0,6],[3,5,9,2216,2216,0.1,9,0,6],[4,4,12,1404,1404,0.1,7,0,6],[5,5,12,1208,1208,20.1,45,0,6],[6,6,12,1206,1206,13.1,37,8,6],[7,7,12,1184,1184,43.1,70,0,6],[8,4,9,2775,2775,25.1,40,0,6],[9,3,12,100,100,0,0,0,6],[10,3,11,100,100,0,0,0,6],[11,9,11,100,100,0,0,0,6]],"dp":10,"killed":0,"total":46},"ev":[["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":10,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":3,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":11,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_12_a","name":"夕","tier":5,"golden":false,"spine":"char_2015_dusk","avatar":"char_2015_dusk","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":0}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":5,"skillIndex":2}],["deploy",5],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_5_03_a","name":"烛煌","tier":5,"golden":false,"spine":"char_1040_blaze2","avatar":"char_1040_blaze2","x":6,"y":12,"facing":1,"dir":"RILine truncated
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"normal-m04","title":"常规作战 · 涨潮控制 · R6","kind":"normal","stageId":"act2autochess_m04","modeId":"mode_multi_normal","round":6,"wave":"act1autochess_06","tick":0.03333333333333333,"snapEvery":3,"duration":70,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m04","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_16_a","name":"蛇屠箱","tier":3,"golden":false,"spine":"char_150_snakek","avatar":"char_150_snakek","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2173,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulpis","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1481,"uid":2,"skillIndex":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":1911,"uid":3,"skillIndex":1},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":1},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1364,"uid":5,"skillIndex":2},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1232,"uid":6,"skillIndex":1},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1251,"uid":7,"skillIndex":1},{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2173,2173,0.1,40,0,6],[2,8,9,1481,1481,6.1,21,0,6],[3,7,9,1911,1911,18.1,54,0,6],[4,3,12,1404,1404,0,19,0,6],[5,4,12,1364,1364,15,37,32,6],[6,5,12,1232,1232,20.1,60,0,6],[7,6,12,1251,1251,13.1,40,0,6],[8,9,11,100,100,0,0,0,6],[9,9,10,100,100,0,0,0,6]],"dp":10,"killed":0,"total":39},"ev":[["spawn",{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":1}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1364,"uid":5,"skillIndex":2}],["deploy",5],["status",5,"talent:archet_shield",1],["fx","shield",4,12,{"id":5}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1232,"uid":6,"skillIndex":1}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1251,"uid":7,"skillIndex":1}],["deploy",7],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":1911,"uid":3,"skillIndex":1}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulLine truncated
{"version":1,"name":"normal-m04","title":"常规作战 · 涨潮控制 · R6","kind":"normal","stageId":"act2autochess_m04","modeId":"mode_multi_normal","round":6,"wave":"act1autochess_06","tick":0.03333333333333333,"snapEvery":3,"duration":70,"recordedAt":"2026-09-27","field":{"fieldId":"n:P1","kind":"normal","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"stageId":"act2autochess_m04","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_16_a","name":"蛇屠箱","tier":3,"golden":false,"spine":"char_150_snakek","avatar":"char_150_snakek","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2173,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulpis","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1481,"uid":2,"skillIndex":2},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":1911,"uid":3,"skillIndex":1},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":1},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1364,"uid":5,"skillIndex":2},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1232,"uid":6,"skillIndex":1},{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1251,"uid":7,"skillIndex":1},{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100},{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}]},"frames":[{"snap":{"fieldId":"n:P1","t":0.1,"units":[[1,9,9,2173,2173,0.1,40,0,6],[2,8,9,1481,1481,6.1,21,0,6],[3,7,9,1911,1911,18.1,54,0,6],[4,3,12,1404,1404,0,19,0,6],[5,4,12,1364,1364,15,37,32,6],[6,5,12,1232,1232,20.1,60,0,6],[7,6,12,1251,1251,13.1,40,0,6],[8,9,11,100,100,0,0,0,6],[9,9,10,100,100,0,0,0,6]],"dp":10,"killed":0,"total":39},"ev":[["spawn",{"id":8,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":11,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":9,"kind":"device","side":"ally","ownerId":null,"defId":"trap_1105_accrate","name":"阻隔工事","tier":0,"golden":false,"spine":"trap_1105_accrate","avatar":"trap_1105_accrate","x":9,"y":10,"facing":1,"dir":"RIGHT","maxHp":100}],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_17_a","name":"流星","tier":3,"golden":false,"spine":"char_126_shotst","avatar":"char_126_shotst","x":3,"y":12,"facing":1,"dir":"RIGHT","maxHp":1404,"uid":4,"skillIndex":1}],["deploy",4],["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_21_a","name":"空弦","tier":3,"golden":false,"spine":"char_332_archet","avatar":"char_332_archet","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1364,"uid":5,"skillIndex":2}],["deploy",5],["status",5,"talent:archet_shield",1],["fx","shield",4,12,{"id":5}],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_14_a","name":"调香师","tier":2,"golden":false,"spine":"char_181_flower","avatar":"char_181_flower","x":5,"y":12,"facing":1,"dir":"RIGHT","maxHp":1232,"uid":6,"skillIndex":1}],["deploy",6],["spawn",{"id":7,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_14_a","name":"初雪","tier":3,"golden":false,"spine":"char_174_slbell","avatar":"char_174_slbell","x":6,"y":12,"facing":1,"dir":"RIGHT","maxHp":1251,"uid":7,"skillIndex":1}],["deploy",7],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_02_a","name":"断崖","tier":3,"golden":false,"spine":"char_294_ayer","avatar":"char_294_ayer","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":1911,"uid":3,"skillIndex":1}],["deploy",3],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_18_a","name":"忍冬","tier":3,"golden":false,"spine":"char_4026_vulpis","avatar":"char_4026_vulLine truncated
+1 -1
View File
@@ -1 +1 @@
{"version":1,"name":"unite-m01","title":"联防 · 源石流发生装置 · R9","kind":"unite","stageId":"act2autochess_m01","modeId":"mode_multi_normal","round":9,"wave":"act1autochess_escaped_multi","tick":0.03333333333333333,"snapEvery":3,"duration":60,"recordedAt":"2026-09-27","field":{"fieldId":"u","kind":"unite","rect":{"r0":9,"r1":12,"c0":0,"c1":20},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2849,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1414,"uid":2,"skillIndex":1},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":3,"skillIndex":2},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":4,"skillIndex":1},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":6,"skillIndex":2},{"id":7,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_11_a","name":"塞雷娅","tier":5,"golden":false,"spine":"char_202_demkni","avatar":"char_202_demkni","x":17,"y":9,"facing":1,"dir":"RIGHT","maxHp":2268,"uid":101,"skillIndex":1},{"id":8,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_2_16_a","name":"拉普兰德","tier":2,"golden":false,"spine":"char_140_whitew","avatar":"char_140_whitew","x":16,"y":9,"facing":1,"dir":"RIGHT","maxHp":1791,"uid":102,"skillIndex":1},{"id":9,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":12,"y":10,"facing":1,"dir":"RIGHT","maxHp":1428,"uid":103,"skillIndex":2},{"id":10,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":12,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":104,"skillIndex":1},{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":12,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":105,"skillIndex":2},{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":13,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":106,"skillIndex":2}]},"frames":[{"snap":{"fieldId":"u","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,8,9,1414,1414,23.1,40,0,6],[3,4,10,1472,1472,20.1,44,0,6],[4,4,11,1400,1400,0.1,26,0,6],[5,4,12,1145,1145,0.1,60,0,6],[6,7,9,2022,2022,55.1,90,0,6],[7,17,9,2268,2268,0.1,9,0,6],[8,16,9,1791,1791,0,27,0,6],[9,12,10,1428,1428,56.1,110,0,6],[10,12,11,1271,1271,70.1,100,0,6],[11,12,12,1217,1217,6.1,27,0,6],[12,13,12,1208,1208,20.1,45,0,6]],"dp":11,"killed":0,"total":40,"dps":{"P1":11,"P2":10}},"ev":[["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5,"skillIndex":1}],["deploy",5],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":4,"skillIndex":1}],["deploy",4],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":3,"skillIndex":2}],["deploy",3],["spawn",{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":6,"skillIndex":2}],["deploy",6],["spawn",{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"cheLine truncated
{"version":1,"name":"unite-m01","title":"联防 · 源石流发生装置 · R9","kind":"unite","stageId":"act2autochess_m01","modeId":"mode_multi_normal","round":9,"wave":"act1autochess_escaped_multi","tick":0.03333333333333333,"snapEvery":3,"duration":60,"recordedAt":"2026-09-27","field":{"fieldId":"u","kind":"unite","rect":{"r0":9,"r1":12,"c0":0,"c1":20},"stageId":"act2autochess_m01","units":[{"id":1,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_17_a","name":"星熊","tier":4,"golden":false,"spine":"char_136_hsguma","avatar":"char_136_hsguma","x":9,"y":9,"facing":1,"dir":"RIGHT","maxHp":2849,"uid":1,"skillIndex":1},{"id":2,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_08_a","name":"德克萨斯","tier":1,"golden":false,"spine":"char_102_texas","avatar":"char_102_texas","x":8,"y":9,"facing":1,"dir":"RIGHT","maxHp":1414,"uid":2,"skillIndex":1},{"id":3,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_3_01_a","name":"能天使","tier":3,"golden":false,"spine":"char_103_angel","avatar":"char_103_angel","x":4,"y":10,"facing":1,"dir":"RIGHT","maxHp":1472,"uid":3,"skillIndex":2},{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":4,"skillIndex":1},{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5,"skillIndex":1},{"id":6,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_4_22_a","name":"银灰","tier":4,"golden":false,"spine":"char_172_svrash","avatar":"char_172_svrash","x":7,"y":9,"facing":1,"dir":"RIGHT","maxHp":2022,"uid":6,"skillIndex":2},{"id":7,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_11_a","name":"塞雷娅","tier":5,"golden":false,"spine":"char_202_demkni","avatar":"char_202_demkni","x":17,"y":9,"facing":1,"dir":"RIGHT","maxHp":2268,"uid":101,"skillIndex":1},{"id":8,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_2_16_a","name":"拉普兰德","tier":2,"golden":false,"spine":"char_140_whitew","avatar":"char_140_whitew","x":16,"y":9,"facing":1,"dir":"RIGHT","maxHp":1791,"uid":102,"skillIndex":1},{"id":9,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_02_a","name":"莫斯提马","tier":4,"golden":false,"spine":"char_213_mostma","avatar":"char_213_mostma","x":12,"y":10,"facing":1,"dir":"RIGHT","maxHp":1428,"uid":103,"skillIndex":2},{"id":10,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_21_a","name":"白面鸮","tier":4,"golden":false,"spine":"char_128_plosis","avatar":"char_128_plosis","x":12,"y":11,"facing":1,"dir":"RIGHT","maxHp":1271,"uid":104,"skillIndex":1},{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":12,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":105,"skillIndex":2},{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":13,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":106,"skillIndex":2}]},"frames":[{"snap":{"fieldId":"u","t":0.1,"units":[[1,9,9,2849,2849,0,0,0,6],[2,8,9,1414,1414,23.1,40,0,6],[3,4,10,1472,1472,20.1,44,0,6],[4,4,11,1400,1400,0.1,26,0,6],[5,4,12,1145,1145,0.1,60,0,6],[6,7,9,2022,2022,55.1,90,0,6],[7,17,9,2268,2268,0.1,9,0,6],[8,16,9,1791,1791,0,27,0,6],[9,12,10,1428,1428,56.1,110,0,6],[10,12,11,1271,1271,70.1,100,0,6],[11,12,12,1217,1217,6.1,27,0,6],[12,13,12,1208,1208,20.1,45,0,6]],"dp":11,"killed":0,"total":40,"dps":{"P1":11,"P2":10}},"ev":[["spawn",{"id":5,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_1_14_a","name":"格雷伊","tier":1,"golden":false,"spine":"char_253_greyy","avatar":"char_253_greyy","x":4,"y":12,"facing":1,"dir":"RIGHT","maxHp":1145,"uid":5,"skillIndex":1}],["deploy",5],["spawn",{"id":11,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_4_20_a","name":"远牙","tier":4,"golden":false,"spine":"char_430_fartth","avatar":"char_430_fartth","x":12,"y":12,"facing":1,"dir":"RIGHT","maxHp":1217,"uid":105,"skillIndex":2}],["deploy",11],["spawn",{"id":4,"kind":"op","side":"ally","ownerId":"P1","defId":"chess_char_2_02_a","name":"赫默","tier":2,"golden":false,"spine":"char_108_silent","avatar":"char_108_silent","x":4,"y":11,"facing":1,"dir":"RIGHT","maxHp":1400,"uid":4,"skillIndex":1}],["deploy",4],["spawn",{"id":12,"kind":"op","side":"ally","ownerId":"P2","defId":"chess_char_5_02_a","name":"缇缇","tier":5,"golden":false,"spine":"char_4056_titi","avatar":"char_4056_titi","x":13,"y":12,"facing":1,"dir":"RIGHT","maxHp":1208,"uid":106,"skillIndex":2}],["deploy",12],["spawn",{"id":3,"kind":"op","side":"ally","ownerId":"P1","defLine truncated
+72 -25
View File
@@ -1,16 +1,22 @@
// render/app.js — battlefield view (DESIGN §9). PixiJS 7 (global PIXI) + pixi-spine (PIXI.spine), loaded on demand
// from /vendor when the page did not include them as classic <script> tags.
//
// const view = await createFieldView(host, { data, assets, audio, settings, padding })
// const view = await createFieldView(host, { data, assets, audio, settings, padding, hud })
// view.setStage(stage) procedural tiles + devices (data/stages.json entry)
// view.setCamera(kind, { rect, side, padding, instant }) 'prep'|'normal'|'unite'|'boss'('hidden'); animated
// — the prep cameras (own board / Final Assault half) keep the bench
// and the field clear of `hud(kind, size)` = { top, bottom } px of
// DOM HUD along the top / bottom edge (projection.js clearHud; user
// playtest #5 item 9: the shop bar covered the bench on phones)
// view.setPrep(privateState, { editable, canPlace }) hand/temp/board pieces; editable enables drag & drop
// view.enterBattle(fieldMeta) m.field { fieldId, kind, rect, stageId, units: [UnitInfo] }
// view.pushSnapshot(snap); view.pushEvents(ev | { ev, gt }) b.snap / b.ev wire frames as received (game time in
// `gt`; a numeric `t` is accepted for raw Battle snapshots / recordings)
// — 100 ms interpolation buffer; b.snap `down` keeps knocked-out
// operators on the field under a redeploy ring and `elem` draws the
// element gauges (user playtest #4 items 8 / 9, render/units.js)
// element gauges (user playtest #4 items 8 / 9, render/units.js); a
// 'die' with reason FORCED_EXIT (an operator entering 联防 knocked out,
// user playtest #5 item 2) goes straight to the held pose, no burst
// view.setLocalFeed({ on, speed }) frames come from the local sim every frame (client-side combat):
// ~2-frame buffer at the battle's game speed
// view.highlightTiles(tiles, style) [[r,c]] | [{row,col}]; style 'legal'|'illegal'|'range'|'rangeStand'|
@@ -25,9 +31,10 @@
// there): every "which unit is under the pointer" — prep press / click / detail / drag start / hover (pieceAt), battle
// clicks and hover (battleUnitAt), the pen (penUnitAt) — is render/pick.js over the tile under the pointer (groundTile:
// raised tops first); battle enemies, which walk between tiles, by their ground position (flying ones by their drawn
// body). A dragged piece is held under the pointer — a unit with its drawn feet DRAG_HOLD_TILES below it (the pointer
// on its body), an item plate centred on it — and drops on the tile under the pointer (render/drag.js); an item
// dropped on a unit's tile equips that unit.
// body; a huge boss also anywhere on its hit area — data/enemies.json `hitArea`, user playtest #5). A dragged piece
// is held under the pointer — a unit with its drawn feet DRAG_HOLD_TILES below it (the pointer on its body), an item
// plate centred on it — and drops on the tile under the pointer (render/drag.js); an item dropped on a unit's tile
// equips that unit.
// Direction step (ui/facingWheel.js, research 09 §1.2):
// view.tileScreen(row, col) → { x, y, s, poly: [[x,y]×4] } (client px: tile-top centre, px per tile, corners) | null
// view.holdPiece(uid, {row,col} | null) keep a dropped prep piece standing on a tile while its direction is
@@ -93,7 +100,7 @@ import { BoardScene } from './board3d/scene.js';
import { AREAS, areaFor, unionAreas } from './board3d/layout.js';
import { layoutPen, penSignature } from './pen.js';
import { IDENTITY, bossPrepField, tilesToDisp } from './prepfield.js';
import { pickOnTile, pickBattle } from './pick.js';
import { pickOnTile, pickBattle, hitRectAt } from './pick.js';
const VENDOR = { pixi: '/vendor/pixi.min.js', spine: '/vendor/pixi-spine.js' };
const PIECE_DIRS = new Set(['UP', 'RIGHT', 'DOWN', 'LEFT']);
@@ -121,7 +128,7 @@ export const DRAG_HOLD_TILES = 0.45;
* tile it is drawn on, or — `walks` (battle enemies) — by its ground position and its drawn body (feet to head, on
* screen; a flying one by its body alone).
*/
function pickUnitOf(v, walks = false) {
function pickUnitOf(v, walks = false, hitArea = null) {
// a knocked-out operator lying on its tile waiting to redeploy (b.snap `down`, user playtest #4 item 9) is on that tile
// too: a press there selects it; other dead / dying views are gone
if (!v || v.destroyed || (v.alive === false && !v.down)) return null;
@@ -129,7 +136,10 @@ function pickUnitOf(v, walks = false) {
const sc = v.screen;
const body = walks && sc && sc.s > 0 && !v.culled ? { x: sc.x, top: Number.isFinite(sc.top) ? sc.top : sc.y, feet: sc.y, s: sc.s } : null;
const tile = walks ? null : { row: Math.round(v.y), col: Math.round(v.x) };
return { tile, x: v.x, y: v.y, fly: walks && !!v.flying, body, depth: v.root && !v.root.destroyed ? v.root.zIndex : 0, ref: v };
// a huge boss (data `hitArea`): its hit area on the ground and a body box as wide as it are pickable (render/pick.js)
const area = walks && !v.flying ? hitRectAt(v.x, v.y, hitArea) : null;
if (area && body) body.hw = hitArea.w / 2;
return { tile, x: v.x, y: v.y, fly: walks && !!v.flying, body, area, depth: v.root && !v.root.destroyed ? v.root.zIndex : 0, ref: v };
}
let pixiPromise = null;
@@ -176,13 +186,15 @@ export function viewKind(kind, opts) {
/** 3D areas without the enemy preview pen block (the own field / both normal halves); see `boardArea`. */
const AREA_NO_PEN = Object.freeze({
normal: Object.freeze(AREAS.normal.filter((a) => a.r1 <= 12)),
unite: Object.freeze(AREAS.unite.filter((a) => a.r1 <= 12)),
normal: Object.freeze(AREAS.normal.filter((a) => a.r1 <= 13)),
unite: Object.freeze(AREAS.unite.filter((a) => a.r1 <= 13)),
});
/**
* 3D area built for a view kind (viewKind): the enemy preview pen (rows 13–18) only for the 'pen' camera — the prep,
* battle and 联防 cameras show the field alone (user playtest #2 item 6); the boss kinds build the boss field.
* 3D area built for a view kind (viewKind): the enemy preview pen (rows 14–18) only for the 'pen' camera — the prep,
* battle and 联防 cameras show the field alone (user playtest #2 item 6) with its separator rows 6 and 13 (the row-13
* devices blow into the field: act2 m01's blowers, user playtest #5 item 6; the boss field's row-6 devices are drawn
* with the boss field only — board3d/layout.js stageDevices); the boss kinds build the boss field.
*/
export function boardArea(vk) {
if (vk === 'pen') return AREAS.normal;
@@ -200,15 +212,32 @@ export function bandFor(kind) {
return kind === 'pen' ? [6, 18] : [6, 13];
}
/**
* Active field rows [r0, r1] of a view kind for the 2D board (render/tiles.js `setView` field: drawn rows outside it are
* dim scenery without devices): the normal / 联防 / prep fields live between the separator walls (rows 6–13, the
* devices on the row-13 wall included; the row-6 wall's belong to the boss field — tiles.js _stageDevices); the pen
* camera adds the pen (6–18); the boss field 0–6.
*/
export function fieldRows(kind) {
return kind === 'boss' || kind === 'hidden' || kind === 'bossPrep' ? [0, 6] : kind === 'pen' ? [6, 18] : [6, 13];
}
/** Are the pen's figures shown for a view kind (a camera flight shows them when either end is the pen)? */
export const penShown = (vk, prevVk = null) => vk === 'pen' || prevVk === 'pen';
/**
* 'die' reason of an operator that enters the battle already knocked out — a 联防 helper's operator down at the end of
* its own combat, deployed and forced out at once (server/sim/constants.js FORCED_EXIT, user playtest #5 item 2): its
* view goes straight to the held knocked-down pose (UnitView.die(true)) without the death burst; b.snap `down` keeps it.
*/
export const FORCED_EXIT = 'forcedExit';
/**
* Generic death particles for a battle unit's 'die' event: never for stage devices, nor for a summon used up by its
* own effect (user playtest #2 item 4: 香槟炸弹's blast — sim fx `{ consumed: true, id }` before its 'die' — is its end,
* not a knock-out).
* not a knock-out), nor for an operator entering the battle knocked out (FORCED_EXIT).
*/
export const showsDeathFx = (info, consumed = false) => !consumed && info?.kind !== 'device';
export const showsDeathFx = (info, consumed = false, reason = null) => !consumed && reason !== FORCED_EXIT && info?.kind !== 'device';
/** '2d' | '3d' | 'auto' board preference: `?board=` in the page URL (dev), else the view option. */
export function boardPreference(opt) {
@@ -307,7 +336,8 @@ export function releaseGl(renderer) {
* Create the battlefield view inside `host` (an element sized by CSS; the canvas fills it).
* @param {HTMLElement} host
* @param {{ data?: any, assets?: any, audio?: any, settings?: { damageNumbers?: boolean, quality?: string },
* padding?: object|((kind:string, size:{width:number,height:number}) => object) }} [opts]
* padding?: object|((kind:string, size:{width:number,height:number}) => object),
* hud?: {top:number,bottom:number}|((kind:'prep'|'bossPrep', size:{width:number,height:number}) => {top:number,bottom:number}|null) }} [opts]
*/
export async function createFieldView(host, options = {}) {
if (!host || typeof host.appendChild !== 'function') throw new TypeError('createFieldView: host element required');
@@ -392,7 +422,7 @@ export async function createFieldView(host, options = {}) {
let destroyed = false;
let mode = 'idle'; // 'idle' | 'prep' | 'battle'
let stageRec = null;
let cam = presetCamera('prep', { width: s0.width, height: s0.height, padding: defaultPadding('prep', s0) });
let cam = presetCamera('prep', { width: s0.width, height: s0.height, padding: defaultPadding('prep', s0) }, { hud: hudBands('prep', s0) });
let camFrom = null, camTo = null, camT0 = 0, camKind = 'prep', camOpts = {}, camMs = CAMERA_MS;
let pendingView = null; // tile band/focus to apply when the camera transition ends
const views = new Map(); // key → view (prep: 'p:'+uid; battle: unit id)
@@ -463,7 +493,7 @@ export async function createFieldView(host, options = {}) {
ctx.createBox = () => switchableBox({ board: () => board3d, pixi: () => tiles.createBox() });
const impostors = new ImpostorAtlas(app.renderer);
ctx.impostors = impostors;
tiles.setView(bandFor('prep'), camRect(), fieldFor('prep'));
tiles.setView(bandFor('prep'), camRect(), fieldRows('prep'));
// the real board art of the local client (optional): wait briefly so the first frame already uses it; a late
// arrival swaps the atlas in place
const artPromise = loadBoardArt(assets).then((art) => {
@@ -575,6 +605,14 @@ export async function createFieldView(host, options = {}) {
return { top: h * 0.13, bottom: h * 0.12, left: Math.min(170, w * 0.09), right: w * 0.05 };
}
// the HUD bands the prep cameras keep the bench / field clear of (projection.js clearHud; user playtest #5 item 9):
// `opts.hud` = (kind, size) => { top, bottom } | null, or a fixed object; none → the plain official framing
function hudBands(kind, sz) {
if (kind !== 'prep' && kind !== 'bossPrep') return null;
if (typeof opts.hud === 'function') { try { return opts.hud(kind, sz) || null; } catch { return null; } }
return opts.hud && typeof opts.hud === 'object' ? opts.hud : null;
}
function camRect() {
const k = viewKind(camKind, camOpts);
if (k === 'pen') return { r0: 14, r1: 18, c0: 7, c1: 13 };
@@ -590,21 +628,21 @@ export async function createFieldView(host, options = {}) {
let rect = o.rect ? normRect(o.rect) : null;
if (k === 'prep') rect = rect ? { ...rect, r0: Math.min(rect.r0, GEO.HAND_ROW) } : null;
// official configBlackBoard framing (render/projection.js presetCamera); the padding only matters for the
// fitted fallback (custom rects, portrait viewports)
// fitted fallback (custom rects, portrait viewports); a prep camera keeps the bench and the field clear of the HUD
const vk = viewKind(kind, o); // (a 'prep' camera on the boss rows = the Final Assault prep)
return presetCamera(k, { width: sz.width, height: sz.height, padding: o.padding || defaultPadding(k, sz) }, {
rect, side: o.side, half: !!o.half, shop: o.shop, fit: !!o.fit, config: stageRec?.config || null,
hud: hudBands(vk, sz),
});
}
// rows drawn per camera kind (module `bandFor`): the normal/unite/prep fields live between the separator walls
// (rows 6–13); the enemy preview pen behind them (rows 14–18) only for the pen camera; the boss field below (0–6)
function fieldFor(kind) { return kind === 'boss' || kind === 'hidden' || kind === 'bossPrep' ? [0, 6] : kind === 'pen' ? [6, 18] : [6, 13]; }
// rows drawn per camera kind: module `bandFor`; the active field rows: module `fieldRows`
function setCamera(kind, options) {
if (destroyed) return false;
let o = options && typeof options === 'object' ? options : {};
const prevView = viewKind(camKind, camOpts);
const prevBand = bandFor(prevView), prevField = fieldFor(prevView);
const prevBand = bandFor(prevView), prevField = fieldRows(prevView);
const nextKind = typeof kind === 'string' ? kind : 'normal';
// the enemy pen is a detour of the prep camera: remember where it came from; the same kind asked again without
// framing options goes back to exactly that camera (Final Assault half, shop state…)
@@ -622,7 +660,7 @@ export async function createFieldView(host, options = {}) {
else if (vk === 'bossPrep') setPrepField(bossPrepField(camOpts.side === 'R' ? 'R' : 'L'));
const target = targetCamera(camKind, camOpts);
const band = bandFor(vk);
const field = fieldFor(vk);
const field = fieldRows(vk);
const focus = camRect();
board3d?.setFocus(focus);
camMs = Number.isFinite(o.ms) && o.ms >= 0 ? o.ms : (vk === 'pen' || prevView === 'pen' ? PEN_CAMERA_MS : CAMERA_MS);
@@ -1109,7 +1147,9 @@ export async function createFieldView(host, options = {}) {
const units = [];
for (const v of views.values()) {
if (!v.info || v.info.kind === 'device') continue;
const u = pickUnitOf(v, v.info.side !== 'ally');
const enemy = v.info.side !== 'ally';
// a huge boss: its hit area (data/enemies.json `hitArea`, the sim's hit rectangle) is pickable too
const u = pickUnitOf(v, enemy, enemy ? data.enemy(v.info.defId)?.hitArea ?? null : null);
if (u) units.push(u);
}
const hit = pickBattle(units, groundTile(x, y), x, y);
@@ -1323,7 +1363,7 @@ export async function createFieldView(host, options = {}) {
case 'die': {
const v = views.get(e[1]);
const used = consumedIds.delete(e[1]);
if (v && v.alive) { v.die(); if (showsDeathFx(v.info, used)) fx.death(v); }
if (v && v.alive) { v.die(e[2] === FORCED_EXIT); if (showsDeathFx(v.info, used, e[2])) fx.death(v); }
break;
}
case 'leak': {
@@ -1342,6 +1382,13 @@ export async function createFieldView(host, options = {}) {
consumedIds.add(e[4].id);
if (consumedIds.size > 200) consumedIds.delete(consumedIds.values().next().value);
}
// an enemy's mode change (掠海漂移体 → 爬行模式, user playtest #5 item 1): its view switches clip set
// (UnitView.setForm); the info keeps it for a view built later
if (e[1] === 'phase' && e[4] && typeof e[4] === 'object' && e[4].id != null) {
const inf = infos.get(e[4].id);
if (inf) inf.form = typeof e[4].kind === 'string' ? e[4].kind : null;
views.get(e[4].id)?.setForm?.(e[4].kind);
}
fx.simFx(e[1], Number(e[2]), Number(e[3]), e[4]);
break;
case 'layer': {
+30 -6
View File
@@ -2,9 +2,12 @@
// autochess board at runtime (DESIGN §15): pure JS, no three import (tested in Node; render/board3d/scene.js turns
// the buckets into merged BufferGeometries).
//
// const board = buildBoard(stage, { uv: resolveUvTable(tiles), crate?: objMesh })
// const board = buildBoard(stage, { uv: resolveUvTable(tiles), crate?: objMesh, area?: AREAS.normal })
// → { grid, buckets: { board, decal, pipe }, gates: [{ r, c, kind: 'start'|'end' }], devices: [...],
// terrain: { water, mire, smog, infection: [[r,c]…] }, edges: [...cyan field edge strips], bounds, stats }
// `devices` = the active stage devices standing on a built tile — the separator row 13 included (act2 m01's blowers at
// the top edge of the normal / 联防 field); the boss field's (rows ≤ 6: its row-6 blowers) only when the boss field is
// built — user playtest #5 item 6 (stageDevices).
//
// World space = render/projection.js: x = col, y = row, z = height (up); tile (r,c) covers [c±½]×[r±½].
// Heights follow render/style.js TILE_H (the Pixi layers stand units on the same tops: tiles.js heightAt).
@@ -45,10 +48,16 @@ const CONTENT_OF = (g) => g !== '#' && g !== 'X';
* ((6,11),(12,20)), leftBoss ((0,0),(6,10)), rightBoss ((0,11),(6,20)); enemy_place_rect ((14,7),(18,13)) + the
* separator in front of it). The official prep / battle view shows only the player's own field and the preview pen
* (the island ends under the bench row); 联防 joins both normal halves; the Final Assault builds the boss field.
* The normal / 联防 field rects also take the separator row 13 above the field (like leftBoss takes row 6 above the
* boss field): the devices standing on it act on the field — act2 m01's 源石流发生装置 #001/#002 (#101/#102) at (13, 5 /
* 9 / 13 / 17) blow DOWN into rows 12–10 — and the official normal rounds show those machines at the field's top edge
* (user playtest #5 item 6: the wind lanes worked but the blowers were missing; the 2D board always drew row 13). The
* row-6 wall under the bench is built in these views too (in leftNormal), without the boss field's devices on it
* (stageDevices).
*/
export const AREAS = Object.freeze({
normal: Object.freeze([Object.freeze({ r0: 6, r1: 12, c0: 0, c1: 10 }), Object.freeze({ r0: 13, r1: 18, c0: 6, c1: 14 })]),
unite: Object.freeze([Object.freeze({ r0: 6, r1: 12, c0: 0, c1: 20 }), Object.freeze({ r0: 13, r1: 18, c0: 6, c1: 14 })]),
normal: Object.freeze([Object.freeze({ r0: 6, r1: 13, c0: 0, c1: 10 }), Object.freeze({ r0: 13, r1: 18, c0: 6, c1: 14 })]),
unite: Object.freeze([Object.freeze({ r0: 6, r1: 13, c0: 0, c1: 20 }), Object.freeze({ r0: 13, r1: 18, c0: 6, c1: 14 })]),
boss: Object.freeze([Object.freeze({ r0: 0, r1: 6, c0: 0, c1: 20 })]),
all: Object.freeze([Object.freeze({ r0: 0, r1: 18, c0: 0, c1: 20 })]),
});
@@ -431,7 +440,7 @@ export function buildBoard(stage, opts = {}) {
}
}
const devices = opts.devices === false ? [] : stageDevices(stage, grid);
const devices = opts.devices === false ? [] : stageDevices(stage, grid, opts.area || null);
for (const d of devices) {
if (d.kind === 'platform' || d.kind === 'sealedFloor' || d.kind === 'waterPlatform') slabDevice(board, decal, UVT, d, grid);
}
@@ -478,9 +487,23 @@ export function tube(g, a, b, rad, seg = 8, col = [1, 1, 1]) {
const DEVICE_KINDS = new Set(['crate', 'platform', 'mound', 'blower', 'turret', 'waterPlatform', 'bush', 'sealedFloor']);
/** Active stage devices standing on drawn tiles: `{ kind, r, c, dir, z0, key, alias }`. */
export function stageDevices(stage, grid) {
/** The boss field's top wall — the row under the normal field's bench (rows ≤ this one are the boss field's). */
export const BOSS_WALL_ROW = 6;
/**
* Active stage devices standing on drawn tiles: `{ kind, r, c, dir, z0, key, alias, rangeTiles }` — every visible
* predefined token of the level is a scene object, a device on the separator row 13 included (act2 m01's 源石流发生装置
* #001 / #002 blow DOWN into the normal field: the official normal rounds show them at its top edge although row 13
* lies outside every configBlackBoard area rect, user playtest #5 item 6). The boss field's devices (rows ≤ 6, its top
* wall under the bench included: act2 m01's #201 / #202 blow DOWN into rows 5–3) are drawn only when `area` builds the
* boss field (a rect below row 6): the official normal cameras never show that wall — it lies under the shop bar in
* prep and off-screen in battle and in the shop-collapsed prep view — while the remake's prep view showed the boss
* round's machines under the bench ("反而在关底吹风机的贴图又出现了"). `area` null = the whole map. The 2D board
* (render/tiles.js _stageDevices) follows the same rule.
*/
export function stageDevices(stage, grid, area = null) {
const list = Array.isArray(stage?.devices) ? stage.devices : [];
const bossShown = !area || area.some((a) => a.r0 < BOSS_WALL_ROW);
const out = [];
for (const d of list) {
if (!d || !Array.isArray(d.pos) || !Number.isInteger(d.pos[0]) || !Number.isInteger(d.pos[1])) continue;
@@ -488,6 +511,7 @@ export function stageDevices(stage, grid) {
const active = typeof d.active === 'boolean' ? d.active : !d.hidden;
if (!active) continue;
const [r, c] = d.pos;
if (r <= BOSS_WALL_ROW && !bossShown) continue;
const t = grid?.[r]?.[c];
if (!t || !t.drawn) continue;
out.push({ kind: d.role, r, c, dir: ['UP', 'DOWN', 'LEFT', 'RIGHT'].includes(d.dir) ? d.dir : 'UP', z0: t.h, key: d.key || null, alias: d.alias || null, rangeTiles: Array.isArray(d.rangeTiles) ? d.rangeTiles : null });
+34 -4
View File
@@ -12,12 +12,36 @@
// pointer's point on the ground, or its drawn body (on screen: the upright line from its feet to its head, in its px
// per tile), whichever is nearer, so a press on a tall enemy's / a boss's body picks it too; a flying enemy (drawn in
// the air, over whatever tile it crosses) by its drawn body alone.
// * a huge boss (巨型单位, data `hitArea` — the sim's hit rectangle, server/sim/body.js; user playtest #5 item 10) is
// also picked by a press anywhere on its drawn body — its hit area on the ground (the 5 × 3 tiles under the model)
// or the upright box from its feet to its head as wide as that area — counted as AREA_PICK tile away, so an ally
// standing on the pressed tile, or a regular enemy near the pointer, still wins.
// * an ally's tile and an enemy both qualify → the nearer one (distance in tiles to the pointer's point), ties → the
// ally, then the front-most.
// (Replaces the drawn-body shapes and the 1-px render probe of v2.2, DESIGN §17.2: the player aims at the grid.)
/** Battle enemies are picked within this many tiles of the pointer's ground point or of their drawn body on screen. */
export const ENEMY_REACH = 0.6;
/**
* A press on a huge boss's hit area (not near its feet / drawn body line) counts as this far — farther than any ally
* on the pressed tile (≤ √0.5) or any enemy within ENEMY_REACH, so those win [ASSUMED].
*/
export const AREA_PICK = 1;
/**
* World rectangle (tiles: x = col, y = row) of a hit area `{ w, h, dx, dy }` for a unit at (x, y) — the same rectangle
* as server/sim/body.js hitRect: `w` along the columns × `h` along the rows, centred `dx` columns right / `dy` rows up.
* @returns {{ x0: number, x1: number, y0: number, y1: number } | null}
*/
export function hitRectAt(x, y, a) {
if (!a || !(a.w > 0) || !(a.h > 0) || !Number.isFinite(x) || !Number.isFinite(y)) return null;
const cx = x + (Number(a.dx) || 0), cy = y + (Number(a.dy) || 0);
return { x0: cx - a.w / 2, x1: cx + a.w / 2, y0: cy - a.h / 2, y1: cy + a.h / 2 };
}
const inArea = (A, g) => !!A && !!g && g.x >= A.x0 && g.x <= A.x1 && g.y >= A.y0 && g.y <= A.y1;
/** Screen point inside a huge body's upright box: `hw` tiles (its px per tile) either side of its x, feet up to head. */
const inBox = (b, px, py) => !!b && b.hw > 0 && b.s > 0 && Math.abs(px - b.x) <= b.hw * b.s
&& py >= Math.min(b.top, b.feet) && py <= Math.max(b.top, b.feet);
const EPS = 1e-9;
const isTile = (t) => !!t && Number.isInteger(t.row) && Number.isInteger(t.col);
@@ -39,10 +63,13 @@ function bodyDist(b, px, py) {
* @typedef {{ row: number, col: number, x?: number, y?: number }} PickGround the tile under the pointer and the pointer's
* point on its top (world tiles: x = col, y = row), or null off the grid
* @typedef {{ tile?: { row: number, col: number } | null, x?: number, y?: number, depth?: number, fly?: boolean,
* body?: { x: number, top: number, feet: number, s: number } | null }} PickUnit
* body?: { x: number, top: number, feet: number, s: number, hw?: number } | null,
* area?: { x0: number, x1: number, y0: number, y1: number } | null }} PickUnit
* `tile`: the tile it stands on (null: it walks — a battle enemy); `x`, `y`: its ground position (world tiles);
* `body`: an enemy's drawn body on screen (px: x, head `top`, `feet`; `s` px per tile there); `fly`: in the air — its
* ground position does not count; `depth`: draw order (larger = in front)
* `body`: an enemy's drawn body on screen (px: x, head `top`, `feet`; `s` px per tile there; `hw`: a huge boss's half
* width in tiles); `fly`: in the air — its
* ground position does not count; `area`: a huge boss's hit rectangle on the ground (world tiles, hitRectAt);
* `depth`: draw order (larger = in front)
*/
/**
@@ -87,7 +114,10 @@ export function pickBattle(units, ground, px, py) {
else {
d = bodyDist(u.body, px, py);
if (!u.fly && onGround && Number.isFinite(u.x) && Number.isFinite(u.y)) d = Math.min(d, Math.hypot(u.x - ground.x, u.y - ground.y));
if (!(d <= ENEMY_REACH)) continue;
if (!(d <= ENEMY_REACH)) {
if (u.fly || !(inBox(u.body, px, py) || (onGround && inArea(u.area, ground)))) continue;
d = AREA_PICK;
}
}
if (d < Infinity && better(u, d)) { best = u; bd = d; }
}
+1
View File
@@ -10,6 +10,7 @@
// The view keeps every public coordinate in BOARD space (the server's, g.move targets, canPlace, highlightTiles,
// tileScreen, holdPiece, setPieceDir): only what is drawn and picked goes through this transform. Pure functions.
/** Board row → display (boss-field) row: row − 7 (server/match/board.js BOARD_ROWS_ABOVE_BOSS is +7, the inverse). */
export const BOSS_ROW_SHIFT = -7;
export const MAX_COL = 20;
+65 -1
View File
@@ -25,6 +25,18 @@
// an official shop-view screenshot gave col 5.17, row 2.40, height 11.57 at 30°/40°.) Screens narrower than 16:9
// back the camera off like the client (up to 4:3: Δ = (0, −1.4, −2.8)·t). `presetCamera` returns these official
// cameras; `fitCamera` (frame any rect inside a padded viewport) remains for custom rects and portrait viewports.
//
// HUD clearance of the prep views (user playtest #5 item 9, `clearHud`): the official shop camera puts the bench's
// near edge right on the shop bar's top and the field's back row right under the bond strip at 16:9 (both measured
// flush at 1920×1080). The remake's HUD is sized in rem with a 40 px floor (css/theme.css), so on screens shorter
// than 432 CSS px (phones in landscape) it is taller than the official one relative to the screen and the shop bar
// covered the lower part of bench pads 3–9 (29 % at 844×390, 46 % at the user's Android = 756×366, 51 % at 800×360;
// the Final Assault prep alike). With `opts.hud` (the px bands the HUD covers along the top and bottom edge) the
// official prep camera is kept whenever the band from the bench's near edge to the field's back row fits between them
// — every desktop viewport measured (the Final Assault bench, 1.2 px lower, is nudged up < 3 px at 16:9) — and
// otherwise panned and, only when the band is taller than the space, zoomed out (844×390: ×0.90, 756×366: ×0.84,
// 800×360: ×0.83): a 2D pan / zoom of the image (principal point and focal length), so the perspective and the
// three.js camera stay the official ones.
export const DEG = Math.PI / 180;
@@ -57,15 +69,22 @@ export const OFFICIAL_PARAMS = Object.freeze({
/** Default optics of the fitted (non-official) cameras: the official pitch; dist is a moderate lens. */
export const DEFAULT_OPTICS = Object.freeze({ tilt: OFFICIAL.pitch, dist: 16 });
/** Tile-top heights (tiles) of the prep band's edges (render/style.js TILE_H): bench pads, raised high ground. */
const KEEP_Z_BENCH = 0.16, KEEP_Z_HIGH = 0.42;
/**
* Rects framed by each camera kind (inclusive tile bounds), framing parameters of the fitted fallback, and the
* official configBlackBoard param(s) of the kind (`official`: side → param key; `half` for the ‹ › half views).
* `keep` (prep views): the band `clearHud` keeps clear of the HUD — `near` = world row (y) of the bench's near edge
* (hand row 7 / boss row 0) with the pads' tops at `zNear`, `far` = the field's back edge (row 12 / boss row 5) with
* its raised tops at `zFar`.
*/
export const CAMERA_PRESETS = Object.freeze({
prep: Object.freeze({
rect: Object.freeze({ r0: 7, r1: 12, c0: 0, c1: 10 }), margin: 0.35, headroom: 1.25, tilt: 30, dist: 13,
official: Object.freeze({ L: 'left_shop_camera_param', R: 'left_shop_camera_param' }),
officialNoShop: Object.freeze({ L: 'left_prepare_camera_param', R: 'left_prepare_camera_param' }),
keep: Object.freeze({ near: 6.5, zNear: KEEP_Z_BENCH, far: 12.5, zFar: KEEP_Z_HIGH }),
}),
normal: Object.freeze({
rect: Object.freeze({ r0: 9, r1: 12, c0: 0, c1: 10 }), margin: 0.45, headroom: 1.35, tilt: 30, dist: 13,
@@ -85,6 +104,7 @@ export const CAMERA_PRESETS = Object.freeze({
rect: Object.freeze({ r0: 0, r1: 5, c0: 0, c1: 10 }), margin: 0.35, headroom: 1.25, tilt: 30, dist: 13,
official: Object.freeze({ L: 'left_boss_shop_camera_param', R: 'right_boss_shop_camera_param' }),
officialNoShop: Object.freeze({ L: 'left_boss_prepare_camera_param', R: 'right_boss_prepare_camera_param' }),
keep: Object.freeze({ near: -0.5, zNear: KEEP_Z_BENCH, far: 5.5, zFar: KEEP_Z_HIGH }),
}),
pen: Object.freeze({
rect: Object.freeze({ r0: 14, r1: 18, c0: 7, c1: 13 }), margin: 0.3, headroom: 1.2, tilt: 30, dist: 11,
@@ -346,6 +366,47 @@ export function officialCamera(param, viewport, opts) {
return new Camera({ tx: cx, ty: cy + height * Math.tan(t), tz: 0, tilt: pitch, dist, scale: focal / dist, cx: W / 2, cy: H / 2 });
}
/** Overlap (px) between the kept band and a HUD band that still counts as clear (float noise). */
const HUD_TOLERANCE = 0.5;
/** Gap (px) an adjusted camera leaves between the kept band and the HUD when there is room (no seam under an edge). */
const HUD_GAP = 1;
/**
* Keep a band of the board clear of the HUD (see the header). `hud` = { top, bottom }: CSS px the HUD covers along
* the viewport's top edge (top bar + bond strip) and bottom edge (the shop bar); `keep` = { near, zNear?, far, zFar? }:
* world rows (y) of the band's near and far edge, at the heights zNear / zFar. Returns `cam` itself when the band
* fits between the HUD bands; else a new camera whose image is panned vertically by the smallest shift that clears
* the HUD (by up to HUD_GAP where there is room) or — only when the band is taller than the space between — zoomed
* out about the viewport's centre so it fills that space less HUD_GAP at both ends. The screen y of a horizontal
* world line is the same at every x (the camera has no roll), so one point per edge decides.
* @param {Camera} cam
* @param {{ top?: number, bottom?: number }|null} hud
* @param {{ near: number, zNear?: number, far: number, zFar?: number }|null} keep
* @param {{ width: number, height: number }} viewport CSS px
* @returns {Camera}
*/
export function clearHud(cam, hud, keep, viewport) {
if (!cam || !hud || typeof hud !== 'object' || !keep) return cam;
const W = Math.max(1, finite(viewport?.width, 1)), H = Math.max(1, finite(viewport?.height, 1));
const top = Math.max(0, finite(hud.top, 0)), bottom = H - Math.max(0, finite(hud.bottom, 0));
if (!(bottom - top > 16)) return cam; // no usable space: keep the official framing
const yNear = cam.project(cam.tx, keep.near, finite(keep.zNear, 0)).y;
const yFar = cam.project(cam.tx, keep.far, finite(keep.zFar, 0)).y;
if (!Number.isFinite(yNear) || !Number.isFinite(yFar) || !(yNear > yFar)) return cam;
if (yNear <= bottom + HUD_TOLERANCE && yFar >= top - HUD_TOLERANCE) return cam;
const spare = bottom - top - (yNear - yFar);
const gap = spare >= 0 ? Math.min(HUD_GAP, spare / 2) : HUD_GAP;
const lo = top + gap, hi = bottom - gap;
const f = spare >= 0 ? 1 : (hi - lo) / (yNear - yFar);
const out = cam.clone();
// image transform x' = xc + f·(x − xc), y' = lo + f·(y − yFar) (zoom) or y' = y + dy (pan, the smallest shift
// that puts [yFar, yNear] inside [lo, hi]): scale the focal length by f and move the principal point accordingly
out.scale = cam.scale * f;
out.cx = W / 2 + f * (cam.cx - W / 2);
out.cy = f < 1 ? lo + f * (cam.cy - yFar) : cam.cy + Math.min(Math.max(0, lo - yFar), hi - yNear);
return out.update();
}
const sameRect = (a, b) => !!a && !!b && a.r0 === b.r0 && a.r1 === b.r1 && a.c0 === b.c0 && a.c1 === b.c1;
/**
@@ -354,6 +415,8 @@ const sameRect = (a, b) => !!a && !!b && a.r0 === b.r0 && a.r1 === b.r1 && a.c0
* viewport is narrower than 4:3 (portrait), `opts.fit` is set, or `opts.rect` is a custom rect — then the rect is
* fitted into the padded viewport (`fitCamera`). `opts.side` 'L'|'R' picks the side's param (right boss half);
* `opts.half` frames only that half of a unite/boss field; `opts.shop === false` uses the shop-collapsed prep camera.
* `opts.hud` = { top, bottom } (CSS px of HUD along the top / bottom edge): the official prep / Final Assault prep
* camera keeps the bench and the field clear of it (`clearHud`, the preset's `keep` band).
*/
export function presetCamera(kind, viewport, options) {
const opts = options && typeof options === 'object' ? options : {};
@@ -374,7 +437,8 @@ export function presetCamera(kind, viewport, options) {
const key = table[side];
const cfg = opts.config && typeof opts.config === 'object' ? opts.config : null;
const param = (cfg && parseCameraParam(cfg[key])) || parseCameraParam(OFFICIAL_PARAMS[key]);
return officialCamera(param, { width: W, height: H }, opts);
const cam = officialCamera(param, { width: W, height: H }, opts);
return preset.keep && opts.hud ? clearHud(cam, opts.hud, preset.keep, { width: W, height: H }) : cam;
}
if (!rect) {
rect = { ...preset.rect };
+28
View File
@@ -12,6 +12,7 @@
// deploy() 'Start' once, then base
// die() die clip once (callers fade out afterwards)
// stunned (setBase('stun')) stun clip, or the current track frozen at timeScale 0
// setForm(roles, change) another clip set of the skeleton (an enemy's mode), after a change clip
// update(dt) advances the skeleton (autoUpdate is off: one clock for everything)
// Attack mode lasts until ~1.4 attack intervals without a new attack, then the end clip (if any) and base.
@@ -114,6 +115,25 @@ export class SpineActor {
this.roles = clip ? { ...anims, skill: clip } : anims;
}
/**
* Another clip set of the same skeleton — an enemy's mode (render/units.js FORMS: 掠海漂移体's 爬行模式 plays its *_02
* clips): `roles` override the manifest roles (null = back to them); `change` = a transition clip played once first
* (also while stunned: the pose it ends in is the one a stun then holds).
*/
setForm(roles, change = null) {
const anims = this.entry?.anims || {};
this.roles = roles ? { ...anims, ...roles } : anims;
if (this.dead) return;
if (change && this.has(change)) {
this.stunWanted = this.mode === 'stun';
this.frozen = false;
this.mode = 'change';
this._play(change, false, { mix: 0.08 });
this.changeUntil = this.clock + this.dur(change);
} else if (this.mode === 'base') this._play(this._baseName(), true);
else if (this.mode === 'stun' && this.has(this.roles.stun?.loop)) this._play(this.roles.stun.loop, true);
}
has(name) { return !!name && this.names.has(name); }
dur(name) { const d = this.durations[name]; return typeof d === 'number' && d > 0 ? d : this._durFromData(name); }
@@ -159,6 +179,8 @@ export class SpineActor {
setBase(base) {
if (this.dead) return;
const b = base === 'move' || base === 'stun' ? base : 'idle';
// a mode change clip plays out first; the resting state it lands in is remembered
if (this.mode === 'change') { this.stunWanted = b === 'stun'; if (b !== 'stun') this.base = b; return; }
if (b === 'stun') { this._enterStun(); return; }
if (this.mode === 'stun') this._leaveStun();
if (b === this.base && this.mode !== 'stun') return;
@@ -340,6 +362,12 @@ export class SpineActor {
case 'deploy':
if (this.clock >= this.deployUntil) { this.mode = 'base'; this._play(this._baseName(), true); }
break;
case 'change':
if (this.clock >= this.changeUntil) {
this.mode = 'base';
if (this.stunWanted) { this.stunWanted = false; this._enterStun(); } else this._play(this._baseName(), true);
}
break;
default: break;
}
if (!this.frozen) {
+9 -7
View File
@@ -45,6 +45,8 @@ export const ROW_KEY = Object.freeze({ blocks: 0, devices: 0.01, surface: 0.02 }
export const boxDepthKey = (cam, row) => -cam.depthOf(0, row + 0.45, 0) * 100;
const ROWS = 19, COLS = 21;
/** The boss field's top wall under the normal field's bench (= board3d/layout.js BOSS_WALL_ROW). */
const BOSS_WALL_ROW = 6;
const PLAYABLE_DIST = 2; // margin tiles farther than this from a playable tile are not drawn
const CLIFF_DEPTH = 0.62; // how far the island's edge faces go down to the ground plane (tiles)
/** Brightness of the board meshes (vertex colour above 1 lifts the mid-grey board art a little). */
@@ -362,6 +364,9 @@ export class TileField {
_stageDevices() {
const list = Array.isArray(this.stage?.devices) ? this.stage.devices : [];
// the boss field's devices (rows ≤ 6, its top wall under the bench included) only in the views of the boss field
// (board3d/layout.js stageDevices: the official normal cameras never show that wall; user playtest #5 item 6)
const bossShown = this.field[0] < BOSS_WALL_ROW;
const out = [];
for (const d of list) {
if (!d || !Array.isArray(d.pos) || !Number.isInteger(d.pos[0]) || !Number.isInteger(d.pos[1])) continue;
@@ -369,14 +374,11 @@ export class TileField {
const active = typeof d.active === 'boolean' ? d.active : !d.hidden;
if (!active) continue;
if (d.pos[0] < 0 || d.pos[0] >= ROWS || d.pos[1] < 0 || d.pos[1] >= COLS || d.pos[0] >= 14) continue;
if (d.pos[0] <= BOSS_WALL_ROW && !bossShown) continue;
const gt = this.grid[d.pos[0]] && this.grid[d.pos[0]][d.pos[1]];
if (gt && (!gt.drawn || gt.scenery)) continue;
// a device on the field's edge wall only matters when it acts on this field (blowers blowing into it)
const F = this.field;
if (Array.isArray(d.rangeTiles) && d.rangeTiles.length > 1 && (d.pos[0] === F[0] || d.pos[0] === F[1])) {
const into = d.rangeTiles.some(([r, c]) => (r !== d.pos[0] || c !== d.pos[1]) && r > F[0] && r < F[1]);
if (!into) continue;
}
// devices stand on the island (playable tiles + the ring around them, dist ≤ 1 — the 3D board's drawn tiles), not
// on the faded margin; the separator row 13 above the field included (act2 m01's blowers)
if (gt && (!gt.drawn || gt.scenery || gt.dist > 1)) continue;
out.push(d);
}
return out;
+54 -5
View File
@@ -20,6 +20,10 @@
// a dark disc, a mint arc filling as the respawn timer runs and the seconds left; once the timer is done and it still
// waits, a full amber ring with "DP" (not enough DP) or a red ring with "!" (its tile is taken). `onDeploy` (the
// redeploy) plays the deploy clip and restores the normal look; `setDown(null)` on a dead view lets it fade out.
// An operator that enters a battle already knocked out (联防, user playtest #5 item 2: sim 'die' reason 'forcedExit')
// goes down with `die(true)`: straight to the held end of the clip, no fall.
// Enemy modes (sim fx 'phase' { id, kind } → `setForm(kind)`): 掠海漂移体 dropping to 爬行模式 (user playtest #5 item 1)
// plays its skeleton's 'Change' clip once, then the crawl set (*_02) — FORMS; a view built later keeps the mode.
// Element gauges (item 8, b.snap `elem` → sample `el` / `elFill` / `elUntil` / `elDur`): the official element icon
// beside the bars (PRTS 元素: operators the element's disc with its glyph, enemies a smaller plain disc of the
// element's colour) inside a white ring of the remaining 元素值 (1 − fill); during a 爆发冷却 the ring refills over
@@ -74,6 +78,25 @@ export const DOWN_LOOK = Object.freeze({
/** Element ring diameter in tiles (operators; enemies ×0.8) and its pixel clamp. */
const EL_RING = 0.3;
/**
* Enemy modes drawn with another clip set of the same skeleton (sim fx 'phase' kind → UnitView.setForm), per Spine
* id: 掠海漂移体 (PRTS: 受晕眩/沉睡/冻结影响后进入爬行模式 — for good) crawls on its *_02 clips after 'Change'. The mode's
* roles override the manifest's (data/assets.json anims); 吉兆飞鳞's 晕眩模式 is its Stun clip already.
*/
export const FORMS = Object.freeze({
enemy_2025_syufo: Object.freeze({
crawl: Object.freeze({
change: 'Change',
roles: Object.freeze({
idle: 'Idle_02', deploy: 'Idle_02', die: 'Die_02',
move: Object.freeze({ begin: null, loop: 'Move_02', end: null }),
attack: Object.freeze({ begin: null, loop: 'Attack_02', end: null, via: 'attackAny' }),
skill: Object.freeze({ begin: null, loop: 'Attack_02', end: null, via: 'attack', index: 0, idle: null }),
}),
}),
}),
});
/**
* Keep `obj` in the right layer: the surface container of block row round(y) when it lies on a raised top at
* height `z` (drawn after that row's blocks), else `fallback`. Reparents only on change.
@@ -168,6 +191,7 @@ export class UnitView {
this.shake = 0;
this.screen = { x: 0, y: 0, s: 1, top: 0 };
this.destroyed = false;
this.form = typeof info.form === 'string' ? info.form : null; // an enemy's mode (setForm, FORMS)
// --- display objects
this.shadow = new P.Sprite(ctx.shadowTex || shadowTexture());
@@ -275,6 +299,9 @@ export class UnitView {
this.spineReady = true;
this.swapT = swap ? 1 : 0;
this.actor.spine.alpha = swap ? 1 : 0;
// a mode the unit is already in (a model built or rebuilt after the change): its clip set, no change clip
const f = this._formSpec();
if (f) this.actor.setForm(f.roles);
// replay current state (a dead model resumes its Die clip where it would be — a knocked-down one holds its end)
if (!this.alive) {
const d = this.actor.die();
@@ -287,6 +314,26 @@ export class UnitView {
}, () => { this._releaseEntry(entry); /* keep the fallback */ });
}
_formSpec() {
return this.form ? FORMS[this.info.spine || this.info.defId]?.[this.form] || null : null;
}
/**
* The unit changed mode (sim fx 'phase' { id, kind }): the mode's clip set (FORMS) after its change clip; a kind with
* no clip set of its own goes back to the manifest clips. Kept for a model built later.
*/
setForm(kind) {
const k = typeof kind === 'string' ? kind : null;
if (k === this.form) return;
const had = !!this._formSpec();
this.form = k;
this.info.form = k;
const f = this._formSpec();
if (!this.actor) return;
if (f) this.actor.setForm(f.roles, f.change || null);
else if (had) this.actor.setForm(null);
}
// ---- HUD -------------------------------------------------------------------------------------------------
_buildHud() {
@@ -495,7 +542,11 @@ export class UnitView {
if (on) this.statuses.add(key); else this.statuses.delete(key);
}
die() {
/**
* The unit died: its Die clip plays, then it fades (unless it stays down, setDown). `instant`: it starts on the held
* end of the clip — a unit that is already down (a field joined mid-battle, an operator entering 联防 knocked out).
*/
die(instant = false) {
if (!this.alive) return;
this.alive = false;
const d = this.actor ? this.actor.die() : 0;
@@ -503,6 +554,7 @@ export class UnitView {
this.dieT = 0;
this.dying = clamp(d / rate, 0.35, 1.6) + 0.55;
this.dieDur = this.dying;
if (instant) { this.dieT = 30; if (this.actor) this.actor.update(30); }
}
revive() {
@@ -528,10 +580,7 @@ export class UnitView {
if (!this.alive) { this.dying = 0.55; this.dieDur = 0.55; }
return;
}
if (this.alive) {
this.die();
if (instant) { this.dieT = 30; if (this.actor) this.actor.update(30); }
}
if (this.alive) this.die(instant);
if (this.zTarget == null) this.z = this.zTarget = groundZ(this.ctx, this.x, this.y); // never synced: its tile top
this.dying = 0; // no death fade while down
const dn = this.down || (this.down = { until: 0, total: 0, state: DOWN_STATE.COUNTING });
+12 -7
View File
@@ -14,7 +14,8 @@
// it; a refused drop toasts the reason (gameLogic dropFailureReason / canPlace). Tapping an own piece selects it:
// its underframe (ui/underframe.js: 撤退 / 出售 +N / 销毁), its rotated range tiles and its detail card; right-click /
// long-press opens the detail card only. There is no drag-to-sell. The own board is drawn and checked with the
// player's stage overrides (terrain 机变 cards: gameLogic stageOverrides / effectiveStage).
// player's stage overrides (terrain 机变 cards: gameLogic stageOverrides / effectiveStage); in a boss round's prep the
// legality reads the player's half of the boss field (gameLogic deployFieldOf, user playtest #5 item 7).
// Shortcuts: R refresh, F freeze, D level-up, Space ready, Esc closes the topmost popup.
// The UI never mutates match state locally; it re-renders from m.public / m.private / m.field pushes.
// Client-side combat (DESIGN §14, m.public.combatMode 'client'): battles are simulated in this browser by
@@ -74,7 +75,7 @@ import {
phaseMode, phaseBanner, isCombatPhase, showDeadPill, isBossPhase, placementContext, canPlace, boardTargets, dropIntent,
snapHud, activeBubbles, shortcutFor, shortcutBlocked, closesOnFieldPress, phaseTotalSeconds, homeFieldId, ownFieldId, normalizeSp, sortedPlayers,
countdownState, shopBlockReason, stageOverrides, effectiveStage, watchTarget, dropFailureReason,
previewEnemyKey, prepCamera, panelSide, panelSlots, bondPopupPlace, chessLoadout, unitLoadout,
previewEnemyKey, prepCamera, deployFieldOf, panelSide, panelSlots, bondPopupPlace, chessLoadout, unitLoadout,
} from '../ui/gameLogic.js';
import { toast } from '../ui/toasts.js';
import { BriefingScreen } from './briefing.js';
@@ -227,17 +228,21 @@ function MatchScreen() {
// latest values for event handlers bound once
const live = useRef({});
// the field the own pieces are deployed on: the own board, or the player's half of the boss field in a boss round's
// prep (user playtest #5 item 7: legality and the legal-tile highlights read THOSE tiles, like the server)
const deployField = deployFieldOf(pub, myId);
const placeCtx = useMemo(() => placementContext({
priv, stage: gd.stage(pub?.stageId), editable,
priv, stage: gd.stage(pub?.stageId), editable, field: deployField,
getChess: gd.chess, getToken: gd.token, getItem: gd.item, getEffect: gd.effect,
}), [priv, pub?.stageId, editable, gd.ready]);
}), [priv, pub?.stageId, editable, gd.ready, deployField]);
live.current = { pub, priv, field, editable, placeCtx, watching, home, myId, detail, drawer, bondOpen, emoteOpen, settingsOpen, exitOpen, drag, facing, sel, pen, collapsedNow: collapsed, localDone: false, canPause: false, paused };
// ---- camera: every request goes through setCam, which remembers it for the pen's way back -----------------------
// the own prep board: the normal board, or — in the prep of a boss round — the player's half of the boss field
// (research 09 §1.2: the right-hand player's board mirrored; gameLogic prepCamera). The DOM fallback view has no
// boss-field prep: it keeps the own board (every coordinate the UI handles is a board coordinate either way).
const prepCam = viewKind === 'fallback' ? prepCamera(null, myId) : prepCamera(pub, myId);
// (research 09 §1.2: the right-hand player's board mirrored; gameLogic prepCamera). The DOM fallback view keeps the
// own board's layout there (every coordinate the UI handles is a board coordinate either way) and draws the tiles of
// that half (ui/fallbackField.js: the legal fence tiles are floor, not the normal field's walls; user playtest #5 item 7).
const prepCam = prepCamera(pub, myId);
const prepCamKey = `${prepCam.kind}:${prepCam.opts.side}`;
const prepCamSeen = useRef(prepCamKey); // the prep camera last requested
const camRef = useRef({ kind: 'prep', opts: { rect: { ...GEO.NORMAL_RECT }, side: 'L' } });
+27 -17
View File
@@ -13,13 +13,15 @@
// like the official client and exactly where the render engine puts them (gameLogic penPlacement = render/pen.js
// layoutPen: rows 14–18 × cols 7–13, upper gate rows 17–18, lower gate 14–15, row 16 empty, ≤ 50 models, ≤ 3 per tile);
// tapping one emits pieceClick { enemyKey, preview: true, unit: { side: 'enemy', defId } }. setCamera('bossPrep') (the
// Final Assault prep) keeps the own board here — every coordinate the UI exchanges is a board coordinate anyway.
// Final Assault prep) keeps the own board layout here — every coordinate the UI exchanges is a board coordinate anyway —
// but draws the tiles of the player's half of the boss field (gameLogic fieldTile, the legality the highlights use: act2
// m01's fence tiles are floor there, not the normal field's walls; user playtest #5 item 7).
import { render } from '../../vendor/preact.module.js';
import { html, TierChip } from './components.js';
import { GEO } from '../../../shared/constants.js';
import { chessAvatarUrl, itemIconUrl, tokenAvatarUrl, enemyIconUrl } from './assetUrls.js';
import { tileKey, hasFlag, UF, penPlacement, PEN } from './gameLogic.js';
import { tileKey, hasFlag, UF, penPlacement, PEN, fieldTile } from './gameLogic.js';
const DRAG_PX = 6;
const DMG_TTL = 900;
@@ -37,6 +39,24 @@ function unitArt(m, info) {
return null;
}
/** Look of the stage tile (row, col) on the fallback board (stage coordinates). */
export function fallbackTileClass(stage, row, col) {
const rows = stage?.rows;
const glyph = Array.isArray(rows) && typeof rows[row] === 'string' ? rows[row][col] : 'r';
const t = stage?.tiles?.[glyph];
if (glyph === 'S') return 'gate';
if (glyph === 'E') return 'goal';
if (!t) return 'void';
if (t.tileKey === 'tile_forbidden') return 'void';
if (t.height === 'HIGH') return t.buildable === 'NONE' ? 'void' : 'high';
if (t.special === 'mire' || glyph === 'm') return 'mire';
if (glyph === 'g') return 'smog';
if (glyph === 'd') return 'deep';
if (glyph === 'i') return 'infect';
if (t.buildable === 'NONE') return 'lane';
return 'floor';
}
/**
* Create the DOM fallback view.
* @param {HTMLElement} host
@@ -52,7 +72,7 @@ export function createFallbackView(host, opts = {}) {
host.appendChild(root);
const st = {
stage: null, camera: 'prep', rect: { ...GEO.NORMAL_RECT }, side: 'L',
stage: null, camera: 'prep', rect: { ...GEO.NORMAL_RECT }, side: 'L', deployField: 'normal',
mode: 'prep', priv: null, editable: false, canPlace: null,
field: null, units: new Map(), snapUnits: new Map(), highlight: new Map(), hlGroups: new Map(), held: new Map(), dirs: new Map(),
drag: null, hoverTarget: null, floats: [], settings: { damageNumbers: true, ...(opts.settings || {}) },
@@ -196,21 +216,10 @@ export function createFallbackView(host, opts = {}) {
}
// ---- tiles ----------------------------------------------------------------------------------------------
// the stage tile a board tile shows: in a boss round's prep the player's half of the boss field (fieldTile)
function tileClass(row, col) {
const rows = st.stage?.rows;
const glyph = Array.isArray(rows) && typeof rows[row] === 'string' ? rows[row][col] : 'r';
const t = st.stage?.tiles?.[glyph];
if (glyph === 'S') return 'gate';
if (glyph === 'E') return 'goal';
if (!t) return 'void';
if (t.tileKey === 'tile_forbidden') return 'void';
if (t.height === 'HIGH') return t.buildable === 'NONE' ? 'void' : 'high';
if (t.special === 'mire' || glyph === 'm') return 'mire';
if (glyph === 'g') return 'smog';
if (glyph === 'd') return 'deep';
if (glyph === 'i') return 'infect';
if (t.buildable === 'NONE') return 'lane';
return 'floor';
const [r, c] = st.mode === 'prep' ? fieldTile(st.deployField, row, col) : [row, col];
return fallbackTileClass(st.stage, r, c);
}
function dropState(target) {
@@ -369,6 +378,7 @@ export function createFallbackView(host, opts = {}) {
if (kind === 'pen') { schedule(); return; } // the board keeps its rect for the way back
if (o && o.rect && Number.isFinite(o.rect.r0)) st.rect = { ...o.rect };
else if (kind === 'prep' || kind === 'bossPrep' || kind === 'normal') st.rect = { ...GEO.NORMAL_RECT };
st.deployField = kind === 'bossPrep' ? (o?.side === 'R' ? 'bossR' : 'bossL') : 'normal';
if (kind === 'bossPrep') st.camera = 'prep';
else if (kind === 'unite') st.rect = { ...GEO.UNITE_RECT };
else if (kind === 'boss') st.rect = { ...GEO.BOSS_RECT };
+42 -1
View File
@@ -37,6 +37,47 @@ export function hudPadding(kind, size) {
return { top: clampH(rem * 1.95), bottom: clampH(rem * 1.0), left: rem * 2.25, right: rem * 1.05 };
}
/**
* HUD geometry (rem) the prep cameras keep clear (they mirror the CSS; test/ui/playtest5-ui.test.js checks the rules):
* the bond strip's bottom edge (css/screens/game.css .gm__bonds top 1.36rem + a .bslot: disc .52rem + name ≈
* 2.15rem measured) and the shop bar's top edge above the viewport's bottom (css/screens/game-shop.css .shopbar
* bottom .2rem + .shopbar__row padding .1rem ×2 + card height 2.24rem, plus its 2 px + 1 px borders). The shop bar
* sits on the viewport's bottom edge even on a notched phone (css/devices.css, DESIGN §18.1).
*/
export const HUD_REM = Object.freeze({ bondStripBottom: 2.16, shopBarTop: 2.64, shopBarBorderPx: 3 });
/**
* CSS px of HUD along the top edge (top bar + bond strip) and the bottom edge (the shop bar) of the viewport during
* prep — the own board ('prep') or the Final Assault half ('bossPrep'); null for every other camera. The prep camera
* keeps the bench / temp rows and the field's back row clear of them (render/projection.js clearHud; user playtest
* #5 item 9: the rem floor of 40 px makes the HUD relatively taller on phones in landscape and the shop bar covered
* the bench). The prep camera is the shop camera whether or not the bar is collapsed, so the band assumes the bar —
* also for an eliminated player's own board (no shop bar: the band only costs size there, while a camera following
* the bar's presence would have to re-frame whenever it appears, e.g. when the private state arrives after the prep
* camera was set). Scouting a teammate's board uses the 'normal' camera: no band. An armed shop card (two-tap buy,
* css/screens/game-shop.css .scard.is-armed) rises 4 px above the bar's top on a phone and covers the bench pads'
* near corners by ≈ 3 px while it stays armed — less than under the unchanged official camera at 1920×1080 (13 px
* above the bar, ≈ 11 px over the pads).
* @param {string} kind
* @param {{ width: number, height: number }} size
* @returns {{ top: number, bottom: number }|null}
*/
export function hudBands(kind, size) {
if (kind !== 'prep' && kind !== 'bossPrep') return null;
let rem = 100;
let safeTop = 0;
try {
rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 100;
// the HUD layer starts below the top safe-area inset (css/devices.css .gm__hud)
safeTop = Math.max(0, document.querySelector('.gm__hud')?.getBoundingClientRect().top || 0);
} catch { /* ignore */ }
const h = size?.height || 1080;
return {
top: Math.min(h * 0.4, safeTop + rem * HUD_REM.bondStripBottom),
bottom: Math.min(h * 0.4, rem * HUD_REM.shopBarTop + HUD_REM.shopBarBorderPx),
};
}
function renderPref() {
try {
const q = new URLSearchParams(globalThis.location?.search || '').get('render');
@@ -82,7 +123,7 @@ export function guardView(view, kind) {
*/
export async function mountFieldView(host) {
const pref = renderPref();
const opts = { data, assets: data.get('assets'), audio, settings: settingsStore.get(), padding: hudPadding };
const opts = { data, assets: data.get('assets'), audio, settings: settingsStore.get(), padding: hudPadding, hud: hudBands };
if (pref !== 'fallback') {
try {
// the shared asset store (public/js/assets.js) keeps its Spine cache across remounts (next match, reconnect)
+25 -3
View File
@@ -139,9 +139,31 @@ export function PieceThumb({ piece, size = 'md', class: cls, badge }) {
golden=${!!piece.golden} tier=${piece.tier} size=${size} class=${cls} badge=${badge} />`;
}
/**
* A regular gear centred in the 24×24 box (even-odd path): `teeth` trapezoid teeth (width tipW at the tip circle
* rTip, rootW at the root circle rRoot; the first one straight up), arcs along both circles, and a round hole. The
* settings button's gear (user playtest #5 item 8): the hand-written path it replaces had teeth of different sizes
* and spacing on a rim that was not round (it bulged at 9 o'clock and was cut flat elsewhere) — a deformed icon.
* @param {{ teeth?: number, rTip?: number, rRoot?: number, tipW?: number, rootW?: number, rHole?: number }} [o]
*/
export function gearPath({ teeth = 8, rTip = 10, rRoot = 7.4, tipW = 3.2, rootW = 4.4, rHole = 3.3 } = {}) {
const c = 12;
const f = (v) => String(Math.round(v * 100) / 100 + 0);
const pt = (a, r) => `${f(c + r * Math.cos(a))} ${f(c + r * Math.sin(a))}`;
const at = Math.asin(tipW / 2 / rTip), ar = Math.asin(rootW / 2 / rRoot), step = (2 * Math.PI) / teeth;
let d = `M${pt(-Math.PI / 2 - ar, rRoot)}`;
for (let i = 0; i < teeth; i++) {
const t = -Math.PI / 2 + i * step;
d += `L${pt(t - at, rTip)}A${f(rTip)} ${f(rTip)} 0 0 1 ${pt(t + at, rTip)}L${pt(t + ar, rRoot)}`
+ `A${f(rRoot)} ${f(rRoot)} 0 0 1 ${pt(t + step - ar, rRoot)}`;
}
return `${d}ZM${f(c)} ${f(c - rHole)}A${f(rHole)} ${f(rHole)} 0 1 0 ${f(c)} ${f(c + rHole)}`
+ `A${f(rHole)} ${f(rHole)} 0 1 0 ${f(c)} ${f(c - rHole)}Z`;
}
/** Extra 24×24 glyphs used by the in-match HUD (original shapes). */
const GLYPHS = {
gear: 'M10.3 2h3.4l.5 2.6c.6.2 1.2.5 1.7.9l2.5-.9 1.7 2.9-2 1.8c.1.6.1 1.2 0 1.8l2 1.8-1.7 2.9-2.5-.9c-.5.4-1.1.7-1.7.9l-.5 2.6h-3.4l-.5-2.6c-.6-.2-1.2-.5-1.7-.9l-2.5.9L2 14.9l2-1.8a6 6 0 0 1 0-1.8L2 9.5l1.7-2.9 2.5.9c.5-.4 1.1-.7 1.7-.9zM12 8.8a3.2 3.2 0 1 0 0 6.4 3.2 3.2 0 0 0 0-6.4z',
export const GLYPHS = Object.freeze({
gear: gearPath(),
eye: 'M12 5c5 0 9 4.5 10 7-1 2.5-5 7-10 7S3 14.5 2 12c1-2.5 5-7 10-7zm0 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 2a2 2 0 1 1 0 4 2 2 0 0 1 0-4z',
emote: 'M8.2 2h7.6L22 8.2v7.6L15.8 22H8.2L2 15.8V8.2zM8.5 8.5v3h2v-3zm5 0v3h2v-3zM7.8 14.2a5.5 5.5 0 0 0 8.4 0l-1.5-1.3a3.5 3.5 0 0 1-5.4 0z',
dp: 'M13 2 4 13.5h6.2L9 22l11-12.5h-6.4z',
@@ -152,7 +174,7 @@ const GLYPHS = {
back: 'M10 5 3 12l7 7 1.4-1.4L6.8 13H21v-2H6.8l4.6-4.6z',
flag: 'M5 2h2v20H5zm3 1h11l-2.5 4.5L19 12H8z',
target: 'M11 2h2v3.1A7 7 0 0 1 18.9 11H22v2h-3.1A7 7 0 0 1 13 18.9V22h-2v-3.1A7 7 0 0 1 5.1 13H2v-2h3.1A7 7 0 0 1 11 5.1zm1 5a5 5 0 1 0 0 10 5 5 0 0 0 0-10zm0 3a2 2 0 1 1 0 4 2 2 0 0 1 0-4z',
};
});
/** Extra HUD glyph. */
export function GIcon({ name, class: cls, title }) {
+62 -14
View File
@@ -5,7 +5,10 @@
//
// Board = own normal field (GEO.FIELD rows 9–12, cols 2–10). Melee chess stand on `melee` deploy tiles
// (LOW, buildable ALL/MELEE); ranged chess on `melee ∪ rangedOnly` (stages.json → deployTiles.normal,
// derived from the tile legend when missing). Tokens follow their own `position`.
// derived from the tile legend when missing). Tokens follow their own `position`. In the prep of a boss round
// (最终攻势 / 隐秘核心) the tiles are the player's half of the boss field (`deployFieldOf`: 'bossL' / mirrored
// 'bossR', board (r, c) = stage tile (r − 7, c) / (r − 7, 20 − c)) like the server's deploy map (server/match/
// board.js field; user playtest #5 item 7) — board coordinates stay the same.
// Hand = 10 slots (index = col). Temp slots are server-filled only (no move target).
// Dropping onto an occupied tile/slot swaps (both pieces must be legal at their new spots);
// an EQUIP item dropped onto a chess piece equips it (tokens can't carry items); an Arts (MAGIC) item
@@ -18,6 +21,7 @@ import { GEO, PHASE, UF } from '../../../shared/constants.js';
import { resolveLoadout, loadoutOptions, MODULE_NONE } from '../../../shared/protocol.js';
import { resolveRecordLoadout, loadoutRecord } from '../../../shared/loadoutRecord.js';
import { layoutPen } from '../render/pen.js';
import { BOSS_ROW_SHIFT, MAX_COL } from '../render/prepfield.js';
import { bossLevelSeconds } from './matchStatus.js';
// ---- small helpers -------------------------------------------------------------------------------
@@ -83,6 +87,32 @@ export function prepCamera(pub, myId) {
return { kind: 'bossPrep', opts: { side: idx % 2 === 1 ? 'R' : 'L' } };
}
/**
* The field the own pieces are deployed on (server/match/Match.js deployFieldOf): 'bossL' / 'bossR' in the prep of a
* boss round (the same pairing as `prepCamera`), else 'normal'. Placement legality (`placementContext` → `deployMap`
* with `field`) reads that field's tiles.
* @returns {'normal'|'bossL'|'bossR'}
*/
export function deployFieldOf(pub, myId) {
const cam = prepCamera(pub, myId);
return cam.kind === 'bossPrep' ? (cam.opts.side === 'R' ? 'bossR' : 'bossL') : 'normal';
}
/**
* The stage tile [row, col] of board tile (r, c) on deploy field `field` (server/match/board.js fieldTile): the boss
* prep's display transform (render/prepfield.js bossPrepField: row − 7, the right half mirrored col c → 20 − c).
*/
export function fieldTile(field, r, c) {
if (field !== 'bossL' && field !== 'bossR') return [r, c];
return [r + BOSS_ROW_SHIFT, field === 'bossR' ? MAX_COL - c : c];
}
/** The board tile of stage tile (r, c) on deploy field `field` (server/match/board.js boardTileOf). */
export function boardTileOf(field, r, c) {
if (field !== 'bossL' && field !== 'bossR') return [r, c];
return [r - BOSS_ROW_SHIFT, field === 'bossR' ? MAX_COL - c : c];
}
/** Banner shown when a phase starts: { title, sub?, tone } or null. */
export function phaseBanner(phase, pub) {
const r = int(pub?.round, 0);
@@ -515,13 +545,22 @@ export function shopBlockReason(kind, { priv, editable, slot, getChess, getItem
// ---- placement (canPlace mirror) ------------------------------------------------------------------------
/**
* Deployable tiles of the own normal field for a stage record.
* Deployable tiles of the own board for a stage record, in board coordinates: the normal field (default), or — `field`
* 'bossL' / 'bossR' — the player's half of the boss field (always derived from the legend + active devices, like the
* server's deploy map on that field).
* @param {any} stage stages.json record
* @param {'normal'|'bossL'|'bossR'} [field]
* @param {{ deviceOverrides?: Record<string, boolean>, tileOverrides?: Record<string, string> }} [overrides] boss field
* only (the normal field takes them folded into the stage: `effectiveStage`)
* @returns {{ melee: Set<string>, ranged: Set<string> }} ranged = melee ∪ rangedOnly
*/
export function deploySets(stage) {
export function deploySets(stage, field = 'normal', overrides = {}) {
const melee = new Set();
const ranged = new Set();
if (field === 'bossL' || field === 'bossR') {
for (const [k, cls] of deployMap(stage, { ...(isObj(overrides) ? overrides : {}), field })) { ranged.add(k); if (cls === 'melee') melee.add(k); }
return { melee, ranged };
}
const inField = (r, c) => r >= GEO.FIELD.r0 && r <= GEO.FIELD.r1 && c >= GEO.FIELD.c0 && c <= GEO.FIELD.c1;
const dt = stage?.deployTiles?.normal;
if (isObj(dt) && Array.isArray(dt.melee)) {
@@ -585,21 +624,24 @@ export function stageOverrides(priv, getEffect = () => null) {
/**
* Deploy classes of the own board — mirror of server/match/board.js buildDeployMap (legend + active devices +
* overrides): 'melee' (melee and ranged) or 'ranged' (ranged only) per 'r,c'.
* overrides, on deploy field `field`): 'melee' (melee and ranged) or 'ranged' (ranged only) per board 'r,c'.
* @param {any} stage
* @param {{ deviceOverrides?: Record<string, boolean>, tileOverrides?: Record<string, string>, field?: 'normal'|'bossL'|'bossR' }} [o]
* @returns {Map<string, 'melee'|'ranged'>}
*/
export function deployMap(stage, { deviceOverrides = {}, tileOverrides = {} } = {}) {
export function deployMap(stage, { deviceOverrides = {}, tileOverrides = {}, field = 'normal' } = {}) {
const F = GEO.FIELD;
const inField = (r, c) => Number.isInteger(r) && Number.isInteger(c) && r >= F.r0 && r <= F.r1 && c >= F.c0 && c <= F.c1;
const map = new Map();
const rows = Array.isArray(stage?.rows) ? stage.rows : null;
const legend = isObj(stage?.tiles) ? stage.tiles : {};
for (let r = F.r0; r <= F.r1; r++) {
const line = rows && typeof rows[r] === 'string' ? rows[r] : null;
for (let c = F.c0; c <= F.c1; c++) {
const [sr, sc] = fieldTile(field, r, c);
const line = rows && typeof rows[sr] === 'string' ? rows[sr] : null;
let cls = null;
if (line) {
const g = line[c];
const g = line[sc];
const t = g != null && Object.hasOwn(legend, g) ? legend[g] : null;
if (t) {
const b = t.buildable;
@@ -612,7 +654,7 @@ export function deployMap(stage, { deviceOverrides = {}, tileOverrides = {} } =
}
for (const d of Array.isArray(stage?.devices) ? stage.devices : []) {
if (!isObj(d) || !Array.isArray(d.pos)) continue;
const [r, c] = d.pos;
const [r, c] = boardTileOf(field, d.pos[0], d.pos[1]);
if (!inField(r, c)) continue;
let active;
if (d.alias != null && isObj(deviceOverrides) && Object.hasOwn(deviceOverrides, d.alias)) active = !!deviceOverrides[d.alias];
@@ -676,11 +718,12 @@ export function indexPieces(priv) {
/**
* Build the placement context for `canPlace`. The deploy tiles include the player's stage overrides (terrain 机变
* cards, `stageOverrides`), like the server's per-player deploy map.
* @param {{ priv:any, stage:any, editable:boolean, getChess?:(id:string)=>any, getToken?:(id:string)=>any, getItem?:(id:string)=>any,
* getEffect?:(id:string)=>any }} o
* cards, `stageOverrides`), like the server's per-player deploy map, on the field the pieces are deployed on
* (`field` = `deployFieldOf(pub, myId)`: the own board, or the player's half of the boss field in a boss round).
* @param {{ priv:any, stage:any, editable:boolean, field?:'normal'|'bossL'|'bossR', getChess?:(id:string)=>any,
* getToken?:(id:string)=>any, getItem?:(id:string)=>any, getEffect?:(id:string)=>any }} o
*/
export function placementContext({ priv, stage, editable, getChess = () => null, getToken = () => null, getItem = () => null, getEffect = () => null }) {
export function placementContext({ priv, stage, editable, field = 'normal', getChess = () => null, getToken = () => null, getItem = () => null, getEffect = () => null }) {
const pieces = indexPieces(priv);
const boardAt = new Map();
for (const e of pieces.values()) if (e.area === 'board') boardAt.set(tileKey(e.row, e.col), e);
@@ -690,8 +733,13 @@ export function placementContext({ priv, stage, editable, getChess = () => null,
let deployed = 0;
for (const e of pieces.values()) if (e.area === 'board' && e.piece.kind === 'chess') deployed += 1;
const count = Number.isInteger(priv?.deployCount) ? priv.deployCount : deployed;
const deploy = deploySets(effectiveStage(stage, stageOverrides(priv, getEffect)));
return { priv, pieces, boardAt, handAt, deploy, cap, count, editable: !!editable, getChess, getToken, getItem };
const ov = stageOverrides(priv, getEffect);
// the own normal board keeps the data's deploy tiles (with the terrain overrides folded in); the boss field of a
// boss round's prep is read from the legend + its devices under the same overrides (server deploy map, field)
const deploy = field === 'bossL' || field === 'bossR'
? deploySets(stage, field, ov)
: deploySets(effectiveStage(stage, ov));
return { priv, pieces, boardAt, handAt, deploy, cap, count, field, editable: !!editable, getChess, getToken, getItem };
}
/** Deploy position ('MELEE'|'RANGED') of a chess/token piece, or null for items. */