Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit

- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item /
  机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one
  (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call)
- official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in
  boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11
- a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight,
  promotion effect
- 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪
- DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
This commit is contained in:
sganggs committed 2026-10-02 04:19:28 +08:00
1 parent 6464e4b46a
commit 620f88c0d8
81 files changed
+4194 -447

No files matched your search

+149 -3
View File
@@ -13,8 +13,24 @@ kinds / counts and routes must follow the official game; research 08 §6–§7,
`k` copies, no kept fly placeholders; verified against all 429 official entry × round compositions and the official
count distribution (同盟 险境 R3 ≤ 10 enemies, R13 ≈ 37 — the old generator averaged 28 / 81);
* stats = the PRTS per-round `enemyScale` table only (`data/config.json`, 终极 ×1.15 speed from R3);
* leader pool = `bloodPoint[difficulty]` (co-op; one pool for the split leaders, no alive-player factor — solo keeps
×0.25, flagged [ASSUMED] in data);
* leader pool = `bloodPoint[difficulty]` in co-op — one pool for every boss field, whatever the number of alive players
(config `bossHpScale.aliveScaling` would make it × alive / 4; off — the user chose the fixed pool, DESIGN §20.10); solo keeps ×0.25,
flagged [ASSUMED] in data;
* leader parts and drones (DESIGN §20.10): a 剑 / 锤 / 碎铳之簧 passes every damage it takes to its leader 1:1 (PRTS "等量的
无来源生命流失"; until 2026-10-01 half, and a 剑 / 锤 only while grounded — the dive hits [ASSUMED], the handbook says
"被击落时"); a 胄 drone that dies (whoever kills it, DESIGN §20.13) costs 2 % of the leader's shown max HP = the pool (unchanged from v2.5; which "最大生命值" the
official client reads is [ASSUMED] — the review round tried the unit's data 600 000, which made drones 19–78 % of a solo
标准 fight, and went back, `bosses.js DRONE_LINK_BASE`);
* same-named effects on an enemy: one instance, the strongest — 奥术 (the two players of a pair field compete for it;
until 2026-10-01 one per player, multiplied: ×30 arts damage taken on a leader at ~250 layers each), 灵知 坚冰,
莱恩哈特 / 缄默德克萨斯 RES cuts (until 2026-10-01 one per copy of the operator);
* 坚守 thorns: 无来源 but credited to the member hit (until 2026-10-01 nobody's: up to 19 % of a co-op leader pool and
88 % of a solo one missing from the per-player boss damage; kill times unchanged);
* bond layers stop at 999 per bond (`BOND_LAYER_CAP`, official: the client's `MAX_GARRISON_STACK`, §7 / research 11; until
2026-10-01 no cap, so per-layer bonuses kept growing past anything the official game reaches);
* player-side "+X%" ATK / DEF / max HP of bonds, strategies, equipment, 机变 cards and per-layer 特质 = 直接乘算, summed
with each other and with skills (PRTS 盟约记录 / 游戏数据基础; DESIGN §20.10) — until 2026-10-01 each compounded, which
made every measurement below the 2026-10-01 note (§3) too strong for the player side (most in late rounds and leader fights);
* bounties: no extra ×0.7 in solo (the 70 % base is already in `enemyScale`), spawned with the template's first normal
action (research 08 §5);
* routes = the official 4-direction flow field with crates at cost 1000 (research 08 §3), act1 m02 without crates.
@@ -104,7 +120,9 @@ the preview being read by a human in 90 s, and losing LP early (fewer rounds to
| Faction mixing | was: every round a ground entry + a FLY entry | **official: one entry per round from a 15-slot type schedule** |
| `MOVE_SCALE` 0.5 | every level's `moveMultiplier` 0.5 (05 §2.2) | kept |
| **Combat time limit unit** | `maxPlayTime` read as game seconds [ASSUMED] | **wrong — real seconds; fixed** (§2.1) |
| Boss pool | bloodPoint DATA; co-op "敌方领袖的总生命值不变" | **co-op = bloodPoint, no alive scaling**; solo ×0.25 kept [ASSUMED]; the solo 标准 ×0.6 removed |
| Boss pool | bloodPoint DATA = the current game data (activity_table act2autochess bossInfoDict; the local cache equals upstream ArknightsGameData master, checked 2026-10-01). PRTS 盟约记录's leader table is the older 11月18日 revision: it matches 常规 and most of 险境, but differs for 铳 险境 (450 000 vs 400 000) and 胄 / 铳 / 萨米 绝境 (1 600 000 / 870 000 / 1 800 000 vs 1 800 000 / 800 000 / 2 000 000), and has no 终极 column or 卢西恩 row; one pool for every field ("所有人将一起对敌方领袖造成伤害"); "敌方领袖的总生命值不变" = the mirrored copies of a pair field share it (notice 5114) | **co-op = bloodPoint whatever the alive count**; × alive / 4 is a config switch (`aliveScaling`, off: 巴哈姆特 12294 "聯機隊友…變少,最後boss血條也會變少" is one community note without a proportion, awaiting the user's recall); solo ×0.25 kept [ASSUMED]; the solo 标准 ×0.6 removed |
| Leader parts / 胄 drones / same-named debuffs / 坚守 thorns | PRTS 碎铳之簧, “斩胄之剑”, “破胄之锤” "受到伤害时令…受到等量的无来源生命流失"; PRTS 假想敌:胄 "该妖怪死亡时令假想敌:胄受到最大生命值2%的真实伤害" (which max HP: [ASSUMED]); PRTS 作战机制 同名buff默认只表现出一个, 巴哈姆特 12316 "共享型buff會跟對面搶"; 坚守 "伤害来源受到(850+10×L)点法术伤害" | **parts 1:1 (was ½, 剑/锤 only grounded); drone 2 % of the pool (unchanged; the unit-HP reading was tried and reverted); 奥术 / 灵知 坚冰 / 莱恩哈特 & 缄默德克萨斯 RES cut: one instance per target (was one per player / copy); thorns credited to the member (was to nobody)** — 2026-10-01, DESIGN §20.10 |
| Bonus stacking | was: each bond / strategy / item / 机变 / 特质 "+X%" its own ×(1 + x) (research 02 §2.1) | **official 直接乘算: summed with each other and with skill "+X%"** (PRTS 盟约记录 "…属性加成均为直接乘算", 游戏数据基础 D_t = Σtᵢ); 2026-10-01, DESIGN §20.10 |
| Solo bounties ×0.7 | 上半 11/18 note = the global solo base | **removed** (already in enemyScale) |
| Bounty spawn timing | was: the bounty units appended after the host action's own units | **official: inserted among them at floor((i+1)·len/(n+1)), then the list spread over the window** (client `_InsertSpActionToNormal` + `_CalculateActionPredelay`, decoded; `waves.js withBounties`) |
| 联防 spawn timing | was: owner k at +min(0.5·k, 5) s, each owner's units over the whole window [ASSUMED] | **official (decoded `_CalculateActionPredelayConsiderUid`): owner k at +0.5·k s, unit step min(max(W/M, 0.05·W), 5 s), M = largest owner group** — a lone leaker's units now come 5 s apart instead of up to 40 s |
@@ -171,6 +189,14 @@ multipliers, their parts all of them): normal rounds unchanged, leader rounds an
| 同盟 绝境 | pool 1.68M · win 88 % · 99 % · 32.6 s | pool 3.42M · win 88 % · 94 % · 100.9 s |
| 同盟 终极 | pool 3.70M · win 88 % · 95 % · 41.9 s | pool 6.08M · win 75 % · 87 % · 92.6 s |
**2026-10-01 — 假想敌:胄 audit** (content/bosses.js `kitHelm` / `kitBlade`, PRTS): one 刺胄之弹 below 20 % as well
(能力修正), shells only at operators in range (boss_1 too), any drone death costs 2 %, 剑 / 锤 sorties per PRTS (【瘫痪】
10 s instead of 20 s, 100 % of their damage passed to 胄 also during the dive instead of 50 % while grounded, a fresh copy
after every sortie, so 掷 comes back after its initial 25 / 55 s instead of the 80 s cooldown; 锤 hits 150 % ATK).
Same-seed `--boss-only` before → after: boss_1 unchanged (≤ 0.5 s); boss_8 独立 绝境 44.7 → 45.2 s, 独立 终极 win
2/3 → 3/3, 同盟 绝境 23.3 → 23.7 s, 同盟 终极 72.0 → 64.2 s (n = 3–5 per cell). The leader columns above are from an
earlier commit (a current run differs for every leader) and were not refreshed here.
Reading: the early game is easy everywhere (official R1–R3 bring 3–10 enemies); leaks concentrate on the second-half
specials (深池逐火 TIMES embers, stealth 隐形弩手组长 / 重弩突袭者, 疯狂的逐腐兽, 掠海漂移体) and 终极 R6–R11, where co-op
boards still leak 3–6 and 联防 halves the LP cost. Solo leaders (bloodPoint × 0.25 [ASSUMED]) are the hard part of the
@@ -178,6 +204,97 @@ solo modes; the solo pool factor is the one open number here (research 08 §8 #2
Reproduce: `node tools/balance.mjs --mode all --difficulty ALL --tuning off [--bots 5] [--json]`.
**2026-10-01 — 直接乘算 stacking and the leader pool (DESIGN §20.10).** The tables above were measured while every bond /
strategy / item "+X%" compounded. With the official summing, leader rounds of the same model (`--boss-only --boss-samples 8
--rehearsal 0`, mean kill time in **game** seconds; 2× = real): 同盟 终极 35.6 → 59.7 (隐秘核心 104.9 → 143.9, win 100 → 88 %),
同盟 绝境 25.1 → 37.0 (38.5 → 80.8), 同盟 险境 14.0 → 17.8, 独立 终极 91.1 → 128.5, 独立 绝境 42.0 → 73.7. Replaying the
same R14 lineups of 4-AI 终极 matches (seeds 1–3, every leader; the fights dumped with the old code and replayed with
both): median kill 175 → 247 real s at the bots' own layers (Σ 36–361 per player), 65 → 109 s at +100 layers per active
bond, 37 → 61 s at +200, 25 → 40 s at +400; 绝境 97 → 106, 25 → 43, 16 → 28, 10.5 → 19.7 s. Bot matches (`matchrun`, 8
seeds) pass fewer rounds: 同盟 险境 14.0 → 13.6, 绝境 12.0 → 11.4, 终极 10.1 → 9.9; 独立 险境 13.5 → 12.6 (wins 6 → 3 of 8),
绝境 12.9 → 12.6, 终极 9.5 → 9.1.
**2026-10-01 (review rounds) — 奥术 and the other same-named debuffs one instance, leader parts 1:1, fixed pool, 坚守
thorns credited.** Replays of the R14 boards of 4-AI co-op matches (seeds 7–9 × boss_1…7 × 终极 / 绝境 = 21 fights per
row, both pair fields on one pool, +L layers on every active bond; real seconds, ∞ = not killed in 200 s), v2.5 →
直接乘算 → now — measured on the boss-HP workstream's boards, **before** the elite-to-board merge (DESIGN §20.11) changed
what the bots build; the integrated build's numbers are in the 2026-10-02 table below:
| mode | +L | Σ active layers / player | median kill | fastest | kills < 10 s | kills < 20 s |
|---|---|---|---|---|---|---|
| 终极 | 0 | 251 (210–404) | ∞ → ∞ → ∞ (8 → 6 → 6 kills) | 90.7 → 126.5 → 126.5 | 0 | 0 |
| 终极 | 100 | 892 (793–1204) | 61.4 → 85.8 → 102.6 | 20.0 → 22.5 → 54.7 | 0 | 0 |
| 终极 | 200 | 1532 (1343–2004) | 30.9 → 43.4 → 56.0 | 9.2 → 9.7 → 20.1 | 1 → 1 → 0 | 9 → 4 → 0 |
| 终极 | 400 | 2813 (2443–3604) | 16.4 → 23.9 → 23.9 | 2.6 → 2.7 → 9.0 | 7 → 4 → 1 | 13 → 10 → 8 |
| 绝境 | 0 | 257 (224–423) | 66.5 → 81.3 → 81.3 | 21.7 → 41.0 → 41.0 | 0 | 0 |
| 绝境 | 100 | 900 (824–1277) | 16.0 → 23.5 → 23.5 | 10.8 → 11.7 → 11.7 | 0 | 13 → 8 → 8 |
| 绝境 | 200 | 1543 (1424–2177) | 11.7 → 14.2 → 14.2 | 7.4 → 7.8 → 8.8 | 5 → 5 → 5 | 19 → 16 → 16 |
| 绝境 | 400 | 2829 (2624–3977) | 8.4 → 9.2 → 9.2 | 2.1 → 2.0 → 2.7 | 16 → 12 → 12 | 21 |
The 奥术 fix moves the fields where both players run 奥术 (终极 seed 7: leader arts damage taken ×40 → ×7.6 at +400, kills
2.7 → 9.0 s; 9.7 → 20.1 s at +200). 胄's drones stay at 2 % of the pool (v2.5; 12–44 % of the pool damage at the bots'
layers, 2–16 % at +200): the second review round's unit-HP reading (12 000 per drone) is reverted — it made a drone 19 %
of a solo 标准 bar (solo 胄 replays, seeds 13 / 21 / 31: drones 10 → 78 % of the pool, kill 144 → 78 s); with the pool
reading the solo fights are back at v2.5 (drones 4–30 %) and only 直接乘算 slows them (标准 none / +100 / +200: kill
times = v2.5 or up to +4 s; 终极 / 绝境 +100: 16 → 25 s, 69 → 101 s, 90 → 135 s). 坚守 thorns now count for the member's
player: on the second reviewer's 28 co-op boards the unattributed share of the leader damage drops from up to 27 % (58 %
with each player's main bond at 999) to 0, a solo 标准 胄 fight's from 88 % to 0; kill times are unchanged. What still kills a 绝境 leader in
3 s at Σ ≈ 3000 layers per player is official per-layer scaling without caps (炎 +0.9 %/layer ATK, 维多利亚 ×(1.25 +
0.008 L), 精准 +1.2 %/layer, 坚守 thorns 850 + 10 L every 0.2 s) — the community reports the same in the official game
(巴哈姆特 12522: 999 谢拉格 + 301 灵巧 + 122 炎 at R14 "boss一秒死"). Since 2026-10-01 a bond stops at 999 layers
(`BOND_LAYER_CAP`, official: the client's `MAX_GARRISON_STACK`, research 11 §1 and §7 below — the game data has no such
constant, and the community never names a count above 999: 巴哈姆特 12534 "每把都能999层", "沒999層的盟約情況下"; v2.5 kept
growing). On the 42 boards of the table above it changes nothing up to
+400 (no bond passes 568); with each player's top bond at 1500 it holds that bond at 999: 终极 median 82.1 → 101 s
(16–17 of 21 killed), fastest 19.9 → 31.4 s; 绝境 median 12.2 → 19.6 s, fastest 6.7 → 8.6 s (uncapped at 2500: 终极
median 54.9 s, fastest 12.0 s; 绝境 8.2 s / 3.4 s). "限傷" (巴哈姆特 12316 "999謝絕對不要亮奧術…大初雪會被限傷
打不出來") is the official boss-hit limit, not an overflow: the client cancels a single leader hit of ≥ 300000 in boss
battles (`MAX_BATTLE_DAMAGE`, research 11 §2, `BOSS_HIT_LIMIT` — §7). An earlier reading of this note (an engine
fixed-point overflow at 2³¹, not modelled) is superseded by that binary evidence. The largest damage instance on a
leader in all the measurements here is 144 000 (a hidden 胄 drone at 终极; from an operator 141 323 at +400 layers;
245 493 with an uncapped 2500-layer bond), below the line, so the limit leaves these numbers unchanged — for these
profiles (up to +400 per bond, a top bond at 999); with two bonds or every bond at 999 it cancels hits and changes the
kill times (the 2026-10-02 table below). Hidden Core (36 fights, boss_8–10, +30 / +100 at R14): hidden 铳 kills 5 → 3 → 5 of 12
(pool dealt by 200 s 82 → 61 → 80 %; the springs' 1:1 transfer is 55–85 % of its damage); hidden 胄 10 → 6 → 9 of 12
(drones median 16–33 % of the pool damage, v2.5 12–32 %; 剑 / 锤 transfer 10–11 → 22–25 %); hidden 管 12 → 11 → 11 of 12.
**2026-10-02 — the integrated build (QA 6b, `cca11e6`: boss HP + elite to the board + official limits + the 胄 audit).**
The R14 boards of real 4-AI co-op matches, seeds 7 / 8 / 9 / 12 × boss_1…7 (28 Final Assaults per row; Hidden Core
boss_8–10, 12 per row), both pair fields on one pool, the same boards replayed on v2.5 → now. Real seconds (the QA tables
are in game seconds: ÷ 2); "cancelled" = hits stopped by 限伤 on the integrated build. Profiles: own = the bots' layers
(Σ ≈ 230 active per player), +L on every active bond (Σ ≈ 1500 / 2650–2960), top-2 = each player's two highest bonds at
999 (Σ ≈ 2050), all = every active bond at 999 (Σ ≈ 6000–7000).
| fight | profile | median kill | fastest | kills < 5 s | cancelled | biggest landed hit |
|---|---|---|---|---|---|---|
| 终极 最终攻势 | own | 206 → 258 | 88 → 114 | 0 → 0 | 0 | 72k → 72k |
| 终极 最终攻势 | +200 | 24.5 → 42.1 | 8.2 → 18.9 | 0 → 0 | 0 | 180k → 72k |
| 终极 最终攻势 | +400 | 11.3 → 16.8 | 3.7 → 9.2 | 2 → 0 | 0 | 402k → 114k |
| 终极 最终攻势 | top-2 | 28.5 → 47.8 | 4.2 → 7.5 | 2 → 0 | 6 | 828k → 274k |
| 终极 最终攻势 | all | 5.0 → 8.3 | 1.2 → 3.4 | 14 → 9 | 18 | 2.38M → 300k |
| 绝境 最终攻势 | own | 79 → 99 | 19 → 32 | 0 → 0 | 0 | 44k → 36k |
| 绝境 最终攻势 | +200 | 11.6 → 14.8 | 1.9 → 7.3 | 3 → 0 | 0 | 148k → 51k |
| 绝境 最终攻势 | +400 | 6.1 → 8.6 | 0.8 → 3.6 | 7 → 1 | 0 | 347k → 108k |
| 绝境 最终攻势 | top-2 | 9.7 → 10.0 | 1.1 → 2.6 | 9 → 7 | 7 | 398k → 267k |
| 绝境 最终攻势 | all | 2.4 → 3.7 | 0.2 → 1.7 | 26 → 20 | 10 | 1.76M → 266k |
| 终极 隐秘核心 | own | 345 → 352 | 138 → 167 | – | 0 | 144k drone |
| 终极 隐秘核心 | +200 | 49 → 80 | 25 → 42 | – | 0 | 144k drone |
| 终极 隐秘核心 | +400 | 18.6 → 27.3 | 10 → 20 | – | 0 | 144k drone |
| 终极 隐秘核心 | all | 7.2 → 14.7 | 3.7 → 8.0 | – | 77 | – |
| 绝境 隐秘核心 | +200 | 20 → 34 | – | – | 0 | – |
| 绝境 隐秘核心 | +400 | 10 → 15 | – | – | 0 | – |
| 绝境 隐秘核心 | all | 4.3 → 7.8 | 0.3 → 2.1 | – | 14 | – |
Each player's top bond requested at 1500 (held at 999 now): 终极 median 53 → 95 s, 绝境 17.5 → 33 s. Same boards: now is
1.2–2.1× slower in every row. On each version's own bot boards the medians moved only ×1.0–1.2 (终极 own 256 → 258 s,
+200 47 → 54 s; 绝境 own 81 → 99 s, +200 13 → 15 s; defeats at 终极 own 4 → 3 of 28, all boss_4 昆图斯), because the
elite-to-board rule gives the bots slightly stronger R14 boards (终极 2.12 → 2.26 elites per player, 绝境 2.14 → 2.51;
绝境 6.0 → 6.9 active bonds). Pool composition now: Final Assault drone link 3.7 % at own / 0.8 % at +400, the rest
direct, 0 % unattributed; Hidden Core drones 20.7 % / 1.8 %, 剑 / 锤 and spring transfers 16.3 % / 21.1 %. What still
kills in seconds at ≈ 2000+ layers per player is official uncapped per-layer scaling (谢拉格 vs cold ×11.34, 奥术 ×15.7,
精准 +1208.8 % ATK at 999), which 限伤 bounds per hit only. Matchrun (co-op 4 AI, rounds passed v2.5 → now): 标准 14.00 →
14.00, 险境 13.81 → 13.88, 绝境 12.41 → 12.06 (Final Assault wins 8 → 5 of 32), 终极 9.47 → 9.47.
---
## 4. The tuning — removed
@@ -228,3 +345,32 @@ previous pass (old generator), kept for history. \* the bot code of this pass (
* 坚若磐石 = least LP lost (`titles.comment_3 { stat: 'lpLost', rule: 'min' }` in tuning.json; results.js supports
`rule: 'min'` among the players still alive).
* 教鞭 / “神秘顾客” stay a random bounty, documented in docs/META.md §2.5.
---
## 7. Official limits: 999 layers per bond, 限伤 300000 (research 11, DESIGN §20.12)
Two official caps, now in the sim and the match (`shared/constants.js BOND_LAYER_CAP` / `BOSS_HIT_LIMIT`, 0 / Infinity =
off): a bond's layers stop at 999 (`MAX_GARRISON_STACK`); in the Final Assault / Hidden Core a single hit of ≥ 300000
on a leader is cancelled (`MAX_BATTLE_DAMAGE`: 0 damage, nothing to the pool — not a clamp).
- **Effect on the measurements above: none.** The competent board of §1.1 carries ≈ 150 core layers at R14, far below
999, and its biggest leader hits stay far under the line — probe of 2026-10-01 (`runBoss` with every leader hit
recorded, 2 seeds each): 同盟 终极 R14 competent max 37k (2481 hits), 同盟 终极 R15 strong (×1.4) max 72k (4669),
独立 终极 R14 strong max 28k (1201); no hit reached 200k, none was cancelled. §3 stays valid. The co-op replay
profiles of §3 with two bonds or every bond at 999 per player do reach the line (6–77 cancelled hits per row, the
2026-10-02 table).
- **Where they bind**: the late boards the community describes — 999-layer 谢拉格 (×11.34 vs cold / frozen), 奥术
(×11.19 arts taken, a debuff a teammate's board also applies), 炎 (ATK ×10.22) — whose single hits run from 160k to
past 300k. There the limit makes the official co-op leaders as hard to kill as the user asked: a hit past the line
deals nothing, so extra multipliers can lower the damage, the reason players avoid lighting more damage bonds at 999
layers. Pools: 200k (卢西恩 FUNNY) … 7.6M (假想敌:管 hidden ABYSS) — in the easy modes one cancelled hit would have
been the whole pool.
- Bots read the capped `ps.layers`; their boards never come near either line (§5), so their play is unchanged.
- The tools stop at the cap too: `balance.mjs applyBoard` clamps the profile-scaled layer curves (a numeric
`--profile` above ≈ 4.9 would have pushed the 170-layer R15 core curve, +20 % jitter, past 999) and
`matchrun.mjs --layers N` adds at most the room left under 999, so `--check` stays clean on a boosted run.
- **Pool size and the line**: a drone that dies (whoever kills it, DESIGN §20.13) costs 假想敌:胄 0.02 × the pool
max (【死亡集群】, boss_1 / boss_8; `bosses.js DRONE_LINK_BASE 'pool'`). Today that is at most 144000 (boss_8 ABYSS 7.2M) and lands; a pool above 14999950 would make every
drone kill a cancelled hit. Re-check this whenever the pool size changes (research 11 §6).
+9 -8
View File
@@ -66,7 +66,7 @@ Top level: `{ season, seasonName, modes, economy, lpCapPerRound, bossOvertimeAft
| `spRounds` | `[3,9,11]` | rounds whose prep opens with a 机变 draft |
| `combatTimeLimit[r]` | `{"1":45,…,"14":null}` | = `rounds[r].combatTimeLimit`: **real** seconds of the forced-2× battle (DESIGN §4) |
| `enemyScale[r]` | `{"atk":1.1,"hp":1.2,"speed":1,"kAtk":1,"kHp":1}` | non-boss enemy multipliers (research 01 A3): `atk = atkBase·1.1^kAtk`, `hp = hpBase·1.2^kHp·extra`, `speed` 1.15 on ABYSS from R3. Apply to level-0 stats after special-enemy replacement, also to bounty/special enemies. **Leader HP pools excluded.** |
| `bossHpScale` | multi `{"bloodPointKey":"bloodPointAbyss","coop":1,"aliveScaling":false,…}` · solo `{"bloodPointKey":"bloodPoint","solo":0.25,"soloAssumed":true,…}` | co-op pool = `bosses[id].bloodPoint[difficulty]` unscaled (split leaders share one pool, "敌方领袖的总生命值不变", no alive-player factor — research 08 §6 #7); solo = × `solo` [ASSUMED, flagged]. `GameData.bossPoolHp(bossId)` implements it |
| `bossHpScale` | multi `{"bloodPointKey":"bloodPointAbyss","coop":1,"aliveScaling":false,"aliveFull":4,"aliveAssumed":true,…}` · solo `{"bloodPointKey":"bloodPoint","solo":0.25,"soloAssumed":true,…}` | one pool for every boss field: co-op = `bosses[id].bloodPoint[difficulty]` whatever the number of alive players; `aliveScaling: true` would scale it × alive / `aliveFull` (巴哈姆特 12294 "隊友變少,最後boss血條也會變少" — one community note, no proportion: off until confirmed, the proportion [ASSUMED, flagged `aliveAssumed`]); solo = × `solo` [ASSUMED, flagged]. `GameData.bossPoolHp(bossId, aliveCount)` / `bossPoolShare` implement it (DESIGN §20.10) |
| `upgradePrices` | `[5,8,11,12,13]` | base price L1→2 … L5→6 (−1 per round start, floor 0, reset after upgrade) |
| `maxShopLevel` | `6` | |
| `shopSlots[level]` | `{"1":{"chess":3,"item":1},…}` | operator + item slots per shop level |
@@ -114,7 +114,7 @@ Top level: `{ season, seasonName, modes, economy, lpCapPerRound, bossOvertimeAft
|---|---|---|
| `lpCapPerRound` | `10` | max LP lost per player per normal round |
| `bossOvertimeAfter`, `bossOvertimeDrainPerSec` | `150`, `1` | official `bossTurnHpReduceTime`: from 150 **real** s of a boss round the merged team LP loses 1 per whole real second (`gamedata.js bossOvertimeDue`; first point at 151 s) |
| `bossHpScale` | `{"formula":"co-op: bloodPoint[difficulty] …","coop":1,"solo":0.25,"soloAssumed":true,"aliveScaling":false,…}` | global form of the mode rule |
| `bossHpScale` | `{"formula":"co-op: bloodPoint[difficulty] — one pool for every field …","coop":1,"solo":0.25,"soloAssumed":true,"aliveScaling":false,"aliveFull":4,"aliveAssumed":true,…}` | global form of the mode rule |
| `hiddenCore` | `{"single":350,"multi":1200,"minTeamLpExclusive":1,"difficulties":["NORMAL","HARD","ABYSS"],"checkedAfterRound":14}` | Σ activated layers must be **>** threshold and LP **>** 1 |
| `dp` | `{"init":10,"perSec":1,"max":99}` | |
| `unite` | `{"maxHelpers":2,"helperOrder":"unitsOnField>activeBond>undownedUnits; pair: unitsOnField>activeBond>activeLayers>undownedUnits, first = right field (PRTS 帮助)","layerGainsEnabled":false,"templates":{"1":"act1autochess_escaped_single","2":"act1autochess_escaped_multi"},…}` | 联防; `helperOrder` documents the rule `server/match/unite.js helperOrder` implements (research 08 §5; ties → seat) |
@@ -248,7 +248,7 @@ everything needed to resolve a unit for `(chessId, skillIndex, moduleId)` (`simd
| `layerMilestones[]` | `[{"layer":25,"mode":"every","effect":"bond_layer_added_reward_equip"}]` | layer-based powers: `reach` (while L ≥ layer), `every` (each multiple), `first` (latched once) |
| `desc`, `descRaw` | | bond panel text |
| `effectId`, `effectName`, `effectDesc`, `effectDescRaw` | `"bondeffect_deput"`, …, `"所有干员防御力+{0:0%}(受层数影响)…"` | in-battle text with **positional** placeholders |
| `effectDescParams[]` | `[{"index":0,"format":"0%","base":"base_def","perStack":"def_per_stack"}]` | `{i:fmt}` = `bb[base] + bb[perStack] × layers` |
| `effectDescParams[]` | `[{"index":0,"format":"0%","base":"base_def","perStack":"def_per_stack"}]` | `{i:fmt}` = `bb[base] + bb[perStack] × layers` — layers never exceed 999 (the official `MAX_GARRISON_STACK`, not in any table: shared/constants.js `BOND_LAYER_CAP`, research 11) |
| `bb`, `bbStr` | `{"base_def":0.15,"def_per_stack":0.012,"respawn_time":-0.3,"layer":2,"count":3,"more_layer":4}` | all buffs merged (first wins) |
| `buffs[]` | `[{"key":"env_gbuff_new","bb":{…},"bbStr":{"key":"act1autochess_bond_eff_deput"}}, …]` | exact official buffs (e.g. 萨尔贡 has a second buff valid only with `band_narant`) |
| `baseParams`, `perStackParams` | `["base_def"]`, `["def_per_stack"]` | official descParamBaseList / descParamPerStackList |
@@ -434,13 +434,13 @@ Glyph legend (`rows`):
| Field | Example (`boss_5`) | Meaning |
|---|---|---|
| `bossId`, `enemyKey`, `handbookId`, `name`, `sortId` | `"boss_5"`, `"enemy_2016_csphtm"`, …, `"卢西恩,“猩红血钻”"`, `5` | |
| `bossId`, `enemyKey`, `handbookId`, `name`, `sortId` | `"boss_5"`, `"enemy_2016_csphtm"`, …, `"卢西恩,“猩红血钻”"`, `5` | `enemyKey` = activity_table `autoChessData.bossInfoDict[*].enemyId`, the official `IsBossEnemy` list: the units 限伤 applies to (one hit ≥ 300000 in a boss battle deals 0; shared/constants.js `BOSS_HIT_LIMIT`, research 11 §2.3) — the wave templates tag exactly these keys `boss` |
| `weight`, `hidden` | `10`, `false` | pick weight within its round |
| `bloodPoint` | `{"FUNNY":200000,"NORMAL":390000,"HARD":780000,"ABYSS":3000000}` | shared leader HP pool per difficulty |
| `bloodPoint` | `{"FUNNY":200000,"NORMAL":390000,"HARD":780000,"ABYSS":3000000}` | shared leader HP pool per difficulty: bossInfoDict `bloodPoint` / `bloodPointNormal` / `bloodPointHard` / `bloodPointAbyss` of the current data (PRTS 盟约记录's leader table is an older revision: 铳 险境, 胄 / 铳 / 萨米 绝境 differ, no 终极, no 卢西恩); no single hit of ≥ 300000 counts toward it (限伤, `BOSS_HIT_LIMIT`) |
| `lpr` | `30` | LP cost if it leaks |
| `templates[modeId]` | `{"round":14,"template":"act2autochess_h07_05"}` | |
| `escortsByTemplate[templateId]` | `[{"key":"enemy_2009_csaudc","slot":null,"count":4},…]` | non-boss spawns (slot = still a placeholder) |
| `parts[]` | `["enemy_9014_acstma","enemy_9015_acstmb"]` | boss parts (random local groups / unharmful) |
| `parts[]` | `["enemy_9014_acstma","enemy_9015_acstmb"]` | boss parts (random local groups / unharmful); not leaders — 限伤 never applies to them |
| `abilities[]` | handbook texts | |
## 14. `tokens.json` — `{ [tokenId]: Token }` (22)
@@ -503,8 +503,9 @@ Glyph legend (`rows`):
requiresBondId/rangeGrid/flavor`, bond `spec`, E2 art availability (falls back to "char has an E2 phase").
17. [ASSUMED] content (flagged in data): 机变 family schedule and server pools, the solo leader pool factor
(`bossHpScale.solo` 0.25), title criteria, income cap 12, per-turn band-draft timer 30 s (`timers.bandTurn`, the
step's only countdown — user playtest #4), the shop-only item list (`SHOP_EXCLUDED_ITEMS`, from play). The special-enemy
generator, the co-op leader pool (`bloodPoint`, no alive-player scaling) and the 联防 timing are official.
step's only countdown — user playtest #4), the shop-only item list (`SHOP_EXCLUDED_ITEMS`, from play), the alive / 4
proportion of the optional co-op alive scaling (`bossHpScale.aliveAssumed`; `aliveScaling` off). The special-enemy
generator, the co-op leader pool (`bloodPoint`, one pool for every field) and the 联防 timing are official.
18. **Module parts flagged `isToken`** (伺夜, 浊心斯卡蒂, 缪尔赛思, 耀骑士临光 golden) upgrade the summon only; they are
applied to `tokens.json` variants, never to the operator's talents/trait.
19. **Undefined enemy-database fields** (`m_defined:false`): a zero `m_value` means "never set" and falls back to the
+97 -14
View File
@@ -101,7 +101,7 @@ public/
screens/ title.js, lobby.js, room.js, briefing.js, bandDraft.js, game.js, result.js
ui/ hud.js, bondStrip.js, bondPopup.js, shopBar.js, teamPanel.js, detailPanel.js,
tooltip.js, choiceOverlay.js, rewardOverlay.js, ticker.js, emotes.js, settings.js, toasts.js
render/ app.js, projection.js, tiles.js, units.js, spine.js, fx.js, interp.js, drag.js, pick.js (API §9)
render/ app.js, projection.js, tiles.js, units.js, spine.js, fx.js, interp.js, drag.js, pick.js, promote.js (API §9)
vendor/ pixi, pixi-spine, preact, hooks, htm (copied from node_modules by tools/vendor.mjs)
assets/ downloaded art/audio (git-ignored), see docs/ASSETS.md
fonts/ self-hosted fonts
@@ -198,7 +198,8 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
killed, total, // enemies attributed to this player's field/half
leaked: [ { enemyKey, mods, lpr, sourcePlayerId } ], // for unite aggregation & LP
perfect: bool, // no leaks
layerGains: { [bondId]: n }, // IN_BATTLE gains (0 if flags.layerGainsEnabled false)
layerGains: { [bondId]: n }, // IN_BATTLE gains (0 if flags.layerGainsEnabled false); what was added — capped at
// the bond's room under BOND_LAYER_CAP 999 (§6.3, §20.12)
coins: n, // bounty coins earned
damageDealt: n, bossDamage: n, healingDone: n, deaths: n,
unitsEnd: [ { uid, hpPct, sp, alive } ] } },
@@ -212,7 +213,7 @@ b.on(event, fn, { priority=0, owner } ) / b.off(...) // hook bus (§5.4)
Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `bat` (base attack time, s), `blockCnt`, `rangeGrid` (ops) / `rangeRadius` (enemies, 0 = melee), `moveSpeed`, `massLevel`, `weight`, `lpr` (enemies), `dmgType` ('phys'|'arts'|'heal'|'true'), `motion` ('WALK'|'FLY'), `cost`, `respawnTime`, `spRecovery`.
**Aggregation** (`units.js`, recomputed lazily when buffs change):
- `ATK = (base + Σflat) × (1 + Σpct) × Πmul` — same shape for `maxHp`, `def`; `res = clamp((base + Σflat) × Πmul, 0, 100)`; `aspd = clamp(100 + Σaspd + base-100, 20, 600)` (`constants.js ASPD_MIN` 20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).
- `ATK = (base + Σflat) × (1 + Σpct) × Πmul` — same shape for `maxHp`, `def`; the 卫戍 systems' "+X%" ATK / DEF / max HP (盟约, 策略, 装备, 机变 cards, per-layer 特质) are 直接乘算 and go into `Σpct` with the skills' "+X%" (`sim/constants.js DIRECT_BONUS_STACKING 'add'`, content/support `directMods`; PRTS 盟约记录 / 游戏数据基础, §20.10) — "提升至X%" effects (炎佑 ×1.5) and the char_attribute_mul 特质 stay in `Πmul`; `res = clamp((base + Σflat) × Πmul, 0, 100)`; `aspd = clamp(100 + Σaspd + base-100, 20, 600)` (`constants.js ASPD_MIN` 20: PRTS 数值范围 "ATTACK_SPEED 默认下限 20", §20.3).
- `interval = bat × (1 + ΣbatPct) × 100 / aspd` (blackboard `base_attack_time` is treated as `batPct`, e.g. −0.3 → 70%).
- Changing `maxHp` keeps the HP **ratio**.
- Stats are rounded only for display; the sim keeps floats.
@@ -227,6 +228,7 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
- `flags`: `stun, freeze, sleep, silence (no skills), disarm (no attacks), stealth (untargetable by enemies/ops unless blocked or revealed), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile — `fear.js`, §20.4), taunt (+aggro), cold, noHeal (no heals: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally, `Battle.allySelectable`; §20.3)`.
- **Status catalogue** (`buffs.js`, keys used by content): `stun`, `freeze` (stun + res −15), `cold` (aspd −30; a second cold while cold ⇒ freeze 3 s unless immune), `sleep` (stun; ends on damage? no: in AK sleep = untargetable & unable to act; keep AK: untargetable & inactive), `slow` (moveMul), `bind` (moveMul 0), `fragile` (dmgTakenMul), `artsFragile` (artsTakenMul), `silence`, `fear` (the 恐惧 walk, `fear.js`), `burnGauge`/`necrosis`/`neural` element gauges (see damage), `stealth`, `camou` (迷彩, its own keys: 忍冬 S3 `vulpis:camou` until her next cast, 寒芒克洛丝 S1), `reveal`.
- Immunities from enemy data (`stunImmune`, `silenceImmune`, `sleepImmune`, `frozenImmune`, `levitateImmune`) are honoured by `applyStatus`.
- Same-named debuffs on an enemy (奥术's arts damage taken, 灵知 坚冰, the 莱恩哈特 / 缄默德克萨斯 RES cuts): one instance per target whoever applies it, the strongest wins (`Battle.applyStrongest`; PRTS 作战机制 "同名buff…只能表现出一个" — strongest rather than latest [ASSUMED], §20.10).
### 5.4 Hook bus (the only way content plugs in)
@@ -254,7 +256,7 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
DamageInfo: `{ amount (pre-mitigation), type: 'phys'|'arts'|'true'|'element', element?: 'burn'|'necrosis'|'neural'|'apoptosis', atkScale, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, mul (multiplier), canDodge, isSkill, isSplash, tags[] }`.
Engine helpers (content must use these, never touch internals): `battle.dealDamage(source, target, dmgInfo)`, `battle.heal(source, target, amount, {overheal=false})`, `battle.applyStatus(target, key, { duration, source, value })`, `battle.addBuff(unit, buff)`, `battle.removeBuff(unit, key)`, `battle.spawnToken(ownerUnit|playerId, tokenId, row, col, opts)`, `battle.spawnEnemy(enemyKey, { routeIndex, pos, mods, tag })`, `battle.addProjectile({ from, to|target, speed, onHit, visual })`, `battle.push(e, force, { from, dir, fixed, fixedAngle, inward, effect })` / `battle.pull(e, force, { to, center, stop })` / `battle.pullToFront(e, unit, force)` / `battle.forceLevel(e, force)` / `battle.pushDistance(e, force, { effect })` (the official 力度 − 重量 tables, §20.3; `battle.displace(e, dir, distance)` is the raw mover), `battle.allySelectable(a, by)` / `battle.alliesFor(by, ownerId)` (friendly selection, skipping 孤立 units, §20.3), `battle.unitsInGrid(unit, grid)`, `battle.enemiesInRadius(x, y, r)`, `battle.alliesInRadius(x, y, r, ownerId?)`, `battle.addLayers(playerId, bondId, n, reason)` (no-op when gains disabled; records into result), `battle.addCoins(playerId, n)`, `battle.fx(kind, params)` (client VFX event), `battle.rng()` (never `Math.random`), `battle.getPlayer(playerId)` (bonds/band/effects view), `battle.redeploy(unit)`, `battle.after(seconds, fn)` (scheduled callbacks), `battle.every(seconds, fn)`.
Engine helpers (content must use these, never touch internals): `battle.dealDamage(source, target, dmgInfo)`, `battle.heal(source, target, amount, {overheal=false})`, `battle.applyStatus(target, key, { duration, source, value })`, `battle.addBuff(unit, buff)`, `battle.removeBuff(unit, key)`, `battle.spawnToken(ownerUnit|playerId, tokenId, row, col, opts)`, `battle.spawnEnemy(enemyKey, { routeIndex, pos, mods, tag })`, `battle.addProjectile({ from, to|target, speed, onHit, visual })`, `battle.push(e, force, { from, dir, fixed, fixedAngle, inward, effect })` / `battle.pull(e, force, { to, center, stop })` / `battle.pullToFront(e, unit, force)` / `battle.forceLevel(e, force)` / `battle.pushDistance(e, force, { effect })` (the official 力度 − 重量 tables, §20.3; `battle.displace(e, dir, distance)` is the raw mover), `battle.allySelectable(a, by)` / `battle.alliesFor(by, ownerId)` (friendly selection, skipping 孤立 units, §20.3), `battle.unitsInGrid(unit, grid)`, `battle.enemiesInRadius(x, y, r)`, `battle.alliesInRadius(x, y, r, ownerId?)`, `battle.addLayers(playerId, bondId, n, reason)` (no-op when gains disabled; at most the room left under `BOND_LAYER_CAP` 999 — `shared/constants.js layerGainRoom`, §20.12; records into result), `battle.loseHp(target, amount, { source, from, sourceless })` (流失; `sourceless` = 无来源, `source` keeps the credit — leader parts pass damage on 1:1 with it, §20.10), `battle.addCoins(playerId, n)`, `battle.fx(kind, params)` (client VFX event), `battle.rng()` (never `Math.random`), `battle.getPlayer(playerId)` (bonds/band/effects view), `battle.redeploy(unit)`, `battle.after(seconds, fn)` (scheduled callbacks), `battle.every(seconds, fn)`.
### 5.5 Combat rules (engine)
@@ -264,7 +266,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
- **Operator attacks**: every `interval` s if a valid target exists; targets = up to `maxTargets` (default 1) from `unitsInGrid(rangeGrid)`, priority: (1) the enemies it blocks (every blocker, in range or not, §20.3), (2) profession/trait priorities (e.g. fly-first snipers, `trait` text parsed by build-data into `targetPriority`), (3) enemy with the **least remaining path distance** to its goal, ties → earliest spawned. Melee (no ranged trait) and ground-only attacks cannot hit air units (`Unit.isFlying`: FLY, or a 近地悬浮 / 浮空 enemy — PRTS "算作空中单位", §19.1). Stealthed enemies can't be targeted unless blocked or revealed. Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact.
- **Healers** (`dmgType heal`): target the ally in range with the lowest HP% below 100%, never a 禁疗 / 孤立 unit (tokens.json `abnormal`, `noHeal`; §20.3); do nothing otherwise (SP still regenerates).
- **Enemy AI**: follow the route (checkpoints; WALK paths recomputed on the grid when devices change); when blocked stop & fight; ranged enemies (`rangeRadius > 0`) attack ops whose 0.25 collider touches their radius (centre distance ≤ `rangeRadius` + 0.25, §3, §20.4): the enemy's own target rule filters the candidates first (`profile.canTarget`: 只攻击地面单位 …), then priority blocker → special priority → highest taunt → **latest deployed** (`aggroSeq`) — PRTS 索敌 "阻挡→特殊优先级→仇恨值→最早出现"; a stealthed ally is attacked only by the enemy it blocks (§19.1); they pause movement for `ATTACK_PAUSE = 0.35 s` per attack. FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动; `fear.js`); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4).
- **Damage**: phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A)`; arts `max(A × (1 − R'/100), 0.05·A)` with `R' = max(0, R×(1−resIgnorePct) − resIgnoreFlat)`; true = A; 元素伤害 ('elemental') `max(A × (1 − 元素抗性/100), 0.05·A)`. Then × `dmgDealtMul` (source) × `dmgTakenMul` & type-specific taken multipliers (target) × `dmg.mul` — 元素伤害 takes 元素脆弱 (`elementalTakenMul`) alone, not `dmgTakenMul` (脆弱 = "受到的物理、法术、真实伤害提升"); shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) × `elemTakenMul` × max(0.05, 1 − 损伤抵抗/100), 损伤抵抗 = data `epResistance` (PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.js `hasHp`: no burst on a corpse). A full gauge bursts with the side-specific official effects, as **无来源** damage (no damage-dealt multiplier or penetration of the unit that filled the gauge, which keeps the kill and the stats; §19.5) (sim/constants.js `ELEMENT`; operators: 灼燃 1200 arts + RES −20, 神经 stun 10 s then 1000 true, 凋亡 15 s of 阻回 (`noSp`: no SP gain of any kind) + 静默 with −1 SP/s and 100 arts/s, 侵蚀 DEF −100 then 800 phys; enemies: the "·我方" terms). Every burst starts a **爆发冷却** = its duration (灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s) during which NO element of the unit fills or can be recovered; when it ends every gauge of the unit resets (§18.3).
- **Damage**: phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A)`; arts `max(A × (1 − R'/100), 0.05·A)` with `R' = max(0, R×(1−resIgnorePct) − resIgnoreFlat)`; true = A; 元素伤害 ('elemental') `max(A × (1 − 元素抗性/100), 0.05·A)`. Then × `dmgDealtMul` (source) × `dmgTakenMul` & type-specific taken multipliers (target) × `dmg.mul` — 元素伤害 takes 元素脆弱 (`elementalTakenMul`) alone, not `dmgTakenMul` (脆弱 = "受到的物理、法术、真实伤害提升"). Then **限伤**: on a leader (`isBoss` — the 10 bossInfoDict enemyIds and their mirrored copies; never parts, drones or escorts) in a `'boss'` / `'hidden'` battle a hit with `ceil(final) ≥ 300000` is cancelled — no HP or pool loss, no credit, no damage number, no `damaged` / kill (`shared/constants.js BOSS_HIT_LIMIT`, `damage.js leaderHitCancelled`, after every multiplier and the hit-step blocks, before HP shields; `loseHp` too — a part's 传递, a drone's death; the client's `MAX_BATTLE_DAMAGE`, §20.12). Then shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) × `elemTakenMul` × max(0.05, 1 − 损伤抵抗/100), 损伤抵抗 = data `epResistance` (PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.js `hasHp`: no burst on a corpse). A full gauge bursts with the side-specific official effects, as **无来源** damage (no damage-dealt multiplier or penetration of the unit that filled the gauge, which keeps the kill and the stats; §19.5) (sim/constants.js `ELEMENT`; operators: 灼燃 1200 arts + RES −20, 神经 stun 10 s then 1000 true, 凋亡 15 s of 阻回 (`noSp`: no SP gain of any kind) + 静默 with −1 SP/s and 100 arts/s, 侵蚀 DEF −100 then 800 phys; enemies: the "·我方" terms). Every burst starts a **爆发冷却** = its duration (灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s) during which NO element of the unit fills or can be recovered; when it ends every gauge of the unit resets (§18.3).
- **Leak**: an enemy reaching its goal is removed, counted as leaked for its field owner (`sourcePlayerId` for unite). LP accounting is done by the match, not the battle.
- **End**: all spawns done and no enemies alive ⇒ `cleared`; `time ≥ timeLimit` ⇒ `timeout` (remaining enemies = leaked). Boss battles end when the shared boss pool reaches 0 (victory) or the match forces an end (team LP 0). A pool holding less than 1 HP is empty (`constants.js BOSS_POOL_MIN_HP`: the hit, or the per-player credit, that would leave less takes the rest; §20.5).
@@ -352,15 +354,16 @@ Summons (§20.1): an operator on the board sends its manually deployable summons
Equipment (research 04 §2 + addendum, 09 §1.2): 2 slots per chess; a third item on a full carrier replaces the equipped item `replaceUid` names (the replace dialog's pick, official `UseEquipUp.unloadInstId`; absent ⇒ the oldest; a uid not equipped on the target ⇒ `BAD_TARGET`, nothing changes) and the replaced item is destroyed. Equipped items are otherwise **locked**: `destroy` (销毁) accepts hand / temp items only (`BAD_TARGET 'equipped items are locked'`); equipment goes back to the hand only when its carrier merges or is sold (a sale is refused with `HAND_FULL` when there is no room for it).
Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income `min(3+r,12)`; prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; shared pool copies 12/14/18/16/8/5 (缪尔赛思 4) across all players; elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite to hand + reward offer of 3 **different** free chess of tier `min(level+1, 6)` (copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); purchase completing a merge allowed with full hand; full hand blocks buy; overflow to temp; temp blocks ready).
Economy/shop/pool/merge/hand rules: exactly as research 00-INDEX §3–§4 (income `min(3+r,12)`; prices by tier; refresh 1; freeze toggles all; sell +1; items not sellable; upgrade prices per mode with −1/round; shared pool copies 12/14/18/16/8/5 (缪尔赛思 4) across all players; elite = 3 copies; odds = copy-weighted draw over unbanned visible chess with tier ≤ level; merge 3 (风丸 2) → elite onto the consumed deployed copy's tile, else to the hand (§20.11) + reward offer of 3 **different** free chess of tier `min(level+1, 6)` (copy-weighted; a tier short of different chess tops up from the tier below [ASSUMED], §20.6); purchase completing a merge allowed with full hand; full hand blocks buy; overflow to temp; temp blocks ready).
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants, battle-result rewards, equipment returned by a merged / sold carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite always goes to the hand, overflowing into temp (also outside PREP); it takes a freed board tile of a consumed copy only when the hand and temp are both full. `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
**Temp overflow (临时整备区, row 8; user playtest #3 item 3).** Passive gains that find the hand full (merge elites, grants, battle-result rewards, equipment returned by a merged / sold carrier) overflow into the 5 temp slots, and Ready is refused while any is occupied (official "直到溢出情况排除才可开始进行作战"). A temp piece is resolved — a chess sold back to the pool with its summons removed, items destroyed, a summon stack removed (it comes back at the next round start, PRTS "干员所属召唤物会于下一回合返还", §20.1) — only at the deadline of the **first prep in which its player could act on it** (`PlayerState.tempDue(piece)` vs `prepsEnded`; every write into a temp slot goes through `_putTemp`, which records it): a piece that arrived during a prep before Ready is due at that prep's end; one that arrived after Ready or during the `<休整期结束时>` effects, or outside PREP (COMBAT, 联防, SETTLE — battle-result grants, a SETTLE merge's elite, returned equipment —, the next ROUND_START, 机变) is kept, visible and usable (move to a free hand slot, place, equip, use, destroy, sell) through the **next** prep. Un-readying (`setReady(false)`) makes whatever arrived while ready due at the current prep. ROUND_START no longer wipes temp (it used to destroy what SETTLE had put there before the player ever saw it). A merge's elite whose merge consumed a deployed copy takes that copy's tile and facing (§20.11); otherwise it goes to the hand, overflowing into temp (also outside PREP). `server/match/audit.js` (`tools/matchrun.mjs --check`) tracks the arrivals itself and flags any temp piece that outlives its due prep. The UI frames the row on the board with the rule (§17.5).
### 6.3 Bonds & layers (bondsMeta.js)
- `count` = number of **distinct base chess** on the board carrying the bond (`BOARD`); `BOARD_AND_DECK` bonds also count the hand; 绝技 counts elites on board; 调和 adds +1 to each core bond count, etc. (research 02).
- `tier` from thresholds; `active = tier ≥ 1`.
- `layers[bondId]` persist all match; gains come from garrisons/bands/items/choices/bond effects; IN_BATTLE gains are collected from `BattleResult.layerGains` at SETTLE (disabled in unite/boss/hidden).
- A bond holds at most `BOND_LAYER_CAP` = 999 layers (the client's `MAX_GARRISON_STACK`, §20.12): every writer — `PlayerState.addLayers`, the SETTLE merge, `Battle.addLayers`, the dev tools — adds `layerGainRoom(before, n)` = min(n, 999 − before); a gain at the cap adds 0 and dispatches nothing (no onLayers, no `layerGain` hook, no 'layer' event), so the per-N milestones stop with the count.
- Σ activated layers (hidden-core check) = Σ layers over bonds that are active at the moment of the check (end of R14 prep, per research).
### 6.4 Prep-phase effect dispatcher (effectsMeta.js)
@@ -386,7 +389,7 @@ Each domain file `server/sim/content/{tokens,bonds,garrisons,items,bands,enemies
- garrisons: 43 effect keys (IN_BATTLE via hooks; SERVER_* via meta registry).
- items: 56 equipment (+golden) + 3 Arts; 变形同构体 bond grant; VI-tier set/combos.
- bands: 40 strategies (meta + battle).
- enemies: special-type behaviours (FLY, TIMES, ELEMENT, DOT, INVISIBLE, REFLECTION, SPECIAL) and notable individual abilities from `data/enemies.json`; bosses 1–10 scripted — the huge leaders (假想敌:胄 ×2, 假想敌:管 ×2, 盐风主教昆图斯, 阿利斯泰尔, “萨米的意志”) are 自缚 + 无法被阻挡 (PRTS 天赋; `content/bosses.js SELF_BOUND`: a persistent noMove + unblockable buff from spawn) with their hit area (§19.4); devices/terrain.
- enemies: special-type behaviours (FLY, TIMES, ELEMENT, DOT, INVISIBLE, REFLECTION, SPECIAL) and notable individual abilities from `data/enemies.json`; bosses 1–10 scripted — the huge leaders (假想敌:胄 ×2, 假想敌:管 ×2, 盐风主教昆图斯, 阿利斯泰尔, “萨米的意志”) are 自缚 + 无法被阻挡 (PRTS 天赋; `content/bosses.js SELF_BOUND`: a persistent noMove + unblockable buff from spawn) with their hit area (§19.4); leader parts (斩胄之剑 / 破胄之锤 / 碎铳之簧) pass every damage they take to their leader 1:1 as 无来源 HP loss (`PART_TRANSFER` 1, PRTS "等量"), a 假想敌:胄 drone that dies costs it 2 % of the pool (`DRONE_LINK_BASE 'pool'`) (§20.10, §20.13); devices/terrain.
- choices: 机变 families (悬赏决策 bounties, 道具补给, 机密商店, 战术决策 team/personal buffs). 悬赏决策 = cards.bounty `draft` (the PRTS 敌人轮选 table: no 战术特训, no 鸭爵 set; 105 cards), drawn uniformly; bounty cards carry `descRaw`; a multi-round card lasts two battles (`choices.js MULTI_ROUND_BOUNTY_BATTLES`, §20.6). 教鞭 = a random 战术特训 card (3 drawn, 1 taken).
---
@@ -409,7 +412,7 @@ S→C:
- `m.field` — field meta when watching starts: `{ fieldId, kind, rect, stageId, units: [UnitInfo] }` (the units on the field, knocked-out operators waiting to redeploy included — `Battle.fieldMeta`, §18.3)
- NOTE: game time in `b.snap`/`b.ev` is carried as **`gt`** (the frame's `t` is the message type).
- `b.snap` — `{ fieldId, gt, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]] , dp, killed, total, down?, elem? }` 20 Hz. Optional, only when non-empty (§18.3): `down: [[id, respawnAt (game s), respawnTime (s), state]]` — knocked-out operators waiting to redeploy, `state` 0 counting | 1 timer done, waiting for DP | 2 timer done, its tile taken (sim/constants.js `DOWN_STATE`); `elem: [[id, element, fill, cooldownEnd (game s), cooldown (s)]]` — the one gauge a unit shows: the fullest (ties by the official element id 神经 < 侵蚀 < 灼燃 < 凋亡), fill with 2 decimals rounded DOWN and kept at 0.01–0.99 outside a burst (§20.8); during a 爆发冷却 the bursting element with fill 1 and the cooldown numbers (else 0, 0)
- `b.ev` — `{ fieldId, ev: [ ... ] }` event tuples: `['spawn', UnitInfo]`, `['atk', srcId, tgtId, projKind]`, `['dmg', tgtId, amount, type]`, `['heal', tgtId, amount]`, `['skill', id, on:0|1]`, `['die', id, reason]` (reason `'killed'`, `'retreat'`, `'expired'` …; only `'killed'` plays an operator's knock-down sound), `['status', id, key, on]`, `['fx', kind, x, y, extra]`, `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`, `['deploy', id]`
- `b.ev` — `{ fieldId, ev: [ ... ] }` event tuples: `['spawn', UnitInfo]`, `['atk', srcId, tgtId, projKind]`, `['dmg', tgtId, amount, type]`, `['heal', tgtId, amount]`, `['skill', id, on:0|1]`, `['die', id, reason]` (reason `'killed'`, `'retreat'`, `'expired'` …; only `'killed'` plays an operator's knock-down sound), `['status', id, key, on]`, `['fx', kind, x, y, extra]` (`'hitCap' {id, n}` = a cancelled 限伤 hit, drawn as nothing, §20.12), `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`, `['deploy', id]`
- `m.toast {kind, text}` · `m.ticker {text}` · `m.emote {playerId, id}` · `m.result {...}`
- `m.unitStats {seq, round, units: [unitStatsEntry]}` — the answer to `g.unitStats` (`seq` echoed): every own board operator / summon by uid with `{id, uid, defId, hp, alive, maxHp, atk, def, res, interval, blockCnt, moveSpeed, base: {…same}}` (shared/protocol.js `unitStatsEntry`) as its next battle starts (§18.5)
@@ -448,7 +451,9 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
const view = await createFieldView(canvasHost /* HTMLElement */, { data, assets, audio })
view.setStage(stage) // draw tiles (procedural), devices
view.setCamera(kind /* 'prep'|'normal'|'unite'|'boss' */, { rect, side }) // animates camera
view.setPrep(privateState, { editable }) // render hand/temp/board pieces as chibis (spine or fallback); editable enables drag
view.setPrep(privateState, { editable }) // render hand/temp/board pieces as chibis (spine or fallback); editable enables drag;
// a merge's new elite on the board plays fx.promote instead of fx.deploy (render/promote.js, §20.11),
// also after a battle (a merge between two preps: the last prep state is kept, §20.14)
view.enterBattle(fieldMeta) // switch to battle rendering for m.field
view.pushSnapshot(snap) ; view.pushEvents(ev) // interpolated rendering (100 ms buffer)
view.highlightTiles(tiles, style) // legal/illegal placement, range preview
@@ -534,9 +539,9 @@ Audio: autochess BGM per phase, UI SFX (buy/sell/refresh/level/merge/ready/timer
- `m.public.fields[]` gains `progress: { killed, total, done }`; `m.public.combatMode`; `m.public.paused: boolean` (always present; solo pause below).
- `b.snap` / `b.ev` are **not sent for combat** in client mode. `g.watch` rules: see Spectating.
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ total ≤ 4·spawns + 100`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ 60 + 4·round`, none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; `coins ≤ Σ bounty coins of the spawns`; `perfect` consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is **rebuilt** from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is **handed over** instead (below). `SP_VERIFY=all|sample|off` (default `off`): `all` re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); `sample` checks ~1 battle in 8 in a later callback and logs mismatches. `Match.verifyStats = { checked, mismatches, rejected, takeovers }`. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
**Result validation (server, `fields.js validateClientResult`).** A client result is accepted only if structurally valid and plausible for its spec: every spec player reported and nobody else; finite numbers; `killed ≤ total ≤ 4·spawns + 100`; every leaked `enemyKey` is in the spec's spawns (multiset bound) or a key the spawned enemies' data mentions (content summons/splits, bounded by the total) — boss fields excepted (their leaks cost team LP through `b.progress`); `counted: false` only for enemies that never count (data `notCountInTotal`, schedule `countInTotal: false`, boss/part entries, content spawns); 联防: leaks + `unspawned` per (enemy, leaker) ≤ what that leaker sent in (nobody's survivors are billed to a teammate); per-bond `layerGains ≤ min(60 + 4·round, 999 − the bond's starting layers)` (§20.12), none when the spec disables gains, bonds must exist and be named by the player's bond snapshot, band, effects, units or items; `coins ≤ Σ bounty coins of the spawns`; `perfect` consistent with the counted leaks; unit states only for the player's own board uids with hpPct ∈ [0, 1]. The accepted result is **rebuilt** from whitelisted fields (leak mods/tags/sources from the spec — a content-spawned enemy (split, summon) keeps the mods of the schedule entry they equal, i.e. its parent's round multipliers and bounty id, so its 联防 re-entry and bounty match the server run; unit names from server data). Otherwise (or on any exception) the server re-simulates (normal/联防); a boss field is **handed over** instead (below). `SP_VERIFY=all|sample|off` (default `off`): `all` re-simulates every accepted normal/联防 result before accepting it (the server result wins on a digest mismatch); `sample` checks ~1 battle in 8 in a later callback and logs mismatches. `Match.verifyStats = { checked, mismatches, rejected, takeovers }`. Desync between display-only clients and the authority is tolerated: the authoritative outcome is what counts and is broadcast at settlement.
**Boss rounds.** The shared pool (`SharedBossPool`, co-op size = `bloodPoint[difficulty]`, see corrections) lives on the server and is fed by the authorities' `b.progress` damage deltas (attributed per player through `by`, BOSS_HIT tickers as before) and by server-run fields; a server clock (250 ms) applies the overtime drain (−`bossOvertimeDrainPerSec` team LP per whole **real** second past `bossOvertimeAfter` real s, measured on the field clock — `gamedata.js bossOvertimeDue`, §4), the silence watchdog and the end checks: pool 0 → `b.end cleared` (victory), team LP 0 → `b.end forced` (defeat) — the first one the server registers decides the verdict (`Match._finalEnding`) and after a forced end no report (an in-flight `b.progress`, the final `b.result`) credits the pool (§20.5); results are awaited ≤ 6 s (× timerScale) after a forced end. `b.pool` is the live channel for the pool and the team LP: a boss `b.progress` (4 Hz per field) and the team-LP losses never re-mark the whole public state — `m.public` (`bossHp`, `teamLp`, `fields[].progress`) and the players' merged-LP shares (`_syncTeamLp`) refresh at ~1 Hz (`Match._bossPublic`, `BOSS_PUBLIC_MS`), and at once when the round ends. A boss field's `b.result` ends it only when the shared pool is empty or after that `b.end`: any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds — and any result failing validation would stop the pair's fight (and, with every field done, end the Final Assault as a defeat), so the field is **handed over** (`Match._bossHandover`) to the partner's display replica (promoted with `b.start authoritative: true`) or, without one, to the server (fast-forwarded to the field clock); the sender gets `b.end takeover` and is never that field's authority again (`f.demoted`). There is no synthetic zero result for a running boss field: a stats-only stand-in (`fields.js syntheticResult`, carrying the damage already credited, never charging LP) only fills a result that cannot be had otherwise — the Final Assault already ending (no result, or an invalid one, within the grace) or a server-run battle whose `result()` threw.
**Boss rounds.** The shared pool (`SharedBossPool`, co-op size = `bloodPoint[difficulty]`, see corrections) lives on the server and is fed by the authorities' `b.progress` damage deltas (attributed per player through `by`, BOSS_HIT tickers as before) and by server-run fields; a server clock (250 ms) applies the overtime drain (−`bossOvertimeDrainPerSec` team LP per whole **real** second past `bossOvertimeAfter` real s, measured on the field clock — `gamedata.js bossOvertimeDue`, §4), the silence watchdog and the end checks: pool 0 → `b.end cleared` (victory), team LP 0 → `b.end forced` (defeat) — the first one the server registers decides the verdict (`Match._finalEnding`) and after a forced end no report (an in-flight `b.progress`, the final `b.result`) credits the pool (§20.5); results are awaited ≤ 6 s (× timerScale) after a forced end. `b.pool` is the live channel for the pool and the team LP: a boss `b.progress` (4 Hz per field) and the team-LP losses never re-mark the whole public state — `m.public` (`bossHp`, `teamLp`, `fields[].progress`) and the players' merged-LP shares (`_syncTeamLp`) refresh at ~1 Hz (`Match._bossPublic`, `BOSS_PUBLIC_MS`), and at once when the round ends. A boss field's `b.result` ends it only when the shared pool is empty or after that `b.end` — except a `'cleared'` result whose cumulative report covers what the pool still holds (within its 1 HP floor) while the plausibility budget (one field credits ≤ the pool per `BOSS_MIN_CLEAR_GS` 5 game s) has not caught up: it is **held** (`f.heldResult`; 999-layer boards kill a leader in 2–4 game s, §20.14) — the boss clock keeps crediting the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end (`_endFinal`) completes the field with it; a forged report gains nothing a `b.progress` could not already get. Any other result — `'forced'` / `'timeout'` at t = 0, `'cleared'` while the pool still holds more than the report covers — and any result failing validation would stop the pair's fight (and, with every field done, end the Final Assault as a defeat), so the field is **handed over** (`Match._bossHandover`) to the partner's display replica (promoted with `b.start authoritative: true`) or, without one, to the server (fast-forwarded to the field clock); the sender gets `b.end takeover` and is never that field's authority again (`f.demoted`). There is no synthetic zero result for a running boss field: a stats-only stand-in (`fields.js syntheticResult`, carrying the damage already credited, never charging LP) only fills a result that cannot be had otherwise — the Final Assault already ending (no result, or an invalid one, within the grace) or a server-run battle whose `result()` threw.
**Solo pause (official `PauseUp` / `ResumeUp`; `Match.setPause`).** C→S `g.pause { on }`: solo matches only (co-op → `WRONG_PHASE 'co-op battles never pause'`), and `on: true` only while a battle runs (COMBAT / FINAL_ASSAULT / HIDDEN_CORE with a live field, not once the Final Assault is ending — else `WRONG_PHASE 'no battle running'`); `on: false` is always accepted. While `m.public.paused` is true everything of the battle is frozen: the field clock (`b.start elapsed`, the boss plausibility budgets and the overtime drain), the authority deadline and the server-run release timers, the boss clock, the HUD `deadline` / `overtimeAt` (shifted by the paused time on resume) and the server-run pacers (FieldRunner / HeadlessPacer skip their intervals); the browser's runner stops its local clock. In solo battles the top bar shows a 暂停 / 继续作战 button (`ui/hud.js PauseButton`, `ui/matchStatus.js pauseAvailable`; `Space` toggles it), a paused overlay offers 继续作战 / 放弃模拟, and the HUD clocks freeze at the pause moment. A disconnect or leave resumes (the server takes the field over — nobody is left to resume it); the battle phase ending drops the pause.
@@ -808,12 +813,15 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
---
## 20. User playtest #6 (v2.5): summons, skill triggers, blocking, push force, enemies, boss pool, bounties, 机变 cards, element gauge
## 20. User playtest #6 (v2.5): summons, skill triggers, blocking, push force, enemies, boss pool, bounties, 机变 cards, element gauge; leader HP, elite to the board, official limits
Nineteen reports after playing v2.4 (#1–#3 from a friend of the user). Where each is handled: #1 凯瑟琳's device could not be placed by hand → §20.1; #2 赫默's drone could not be placed by hand → §20.1; #3 忍冬's 叙拉古 stealth never ended → §20.2; #4 the bounty enemies kept coming instead of stopping after two battles → §20.6; #5 the Final Assault leader at 0 HP kept fighting until the team LP ran out, and the result still said 胜利 → §20.5; #6 the 道具补给 cards showed no description → §20.7; #7 in 联防 the leaker's count should fall live as teammates kill (it seemed capped at 10) → §20.6; #8 新约能天使 S2's shield on teammates seemed not to work → §20.2; #9 威龙 was drawn far too large → §20.4; #10 on the phone the shop's confirm strip did not buy (only the middle of the card worked) → §20.7; #11 the element gauge seemed to fill at the wrong rate → §20.8; #12 萨卡兹枯朽战车 hit too softly → §20.4; #13 “萨科塔之翼”'s 战栗 (small random flight) was missing → §20.4; #14 enemy weight vs push force (野鬃 pushed too many enemies) → §20.3; #15 skills whose range is wider than the attack range did not fire when an enemy entered it → §20.2; #16 古米's ally heal fired only after she engaged an enemy → §20.2; #17 the front operator that took over an enemy held between two operators never attacked it → §20.3; #18 炎佑 was healed by medics → §20.3; #19 the 3-choose-1 promotion reward often offered two identical operators → §20.6.
Eight workstreams were merged into `playtest6` (WH → WG → WD → WC → WB → WA → WF → WE); the reviews' last rounds and the cross-workstream notes were applied on top: a stocked skill summon deploys as soon as its owner is back (§20.1), the AUTO DP skills 焰尾 S1 / 凛御银灰 S1 fire on full SP (§20.2), 锏 S3's slashes give 无敌 and 晕眩 / 冻结免疫 (§20.3), the `AUTO_OP_COOLDOWN` comment and the SKILL_RANGE count (13 skills / 26 records) were corrected. The user then answered the open questions (2026-10-01); the answers are **settled decisions**, recorded in their sections and in §20.9, each with the one-line change that flips it: the placed skill summons deploy once at the battle start and 凯瑟琳's device stays a hand card (§20.1), every blocker hits what it blocks (§20.3), 萨卡兹枯朽战车 follows the re-research — ranged enemies reach an operator by its 0.25 collider (§20.4) — and multi-round bounties last two battles (§20.6). Normative lines rewritten with it: §2 (`fear.js`), §3 (the enemies' reach), §5.2 (ASPD floor 20), §5.3 (`camou`, `fear`, `noHeal`, `isolated`), §5.4 (push / pull / ally-selection helpers), §5.5 (initial deployment, blocking, operator attacks, healers, enemy AI, end), §5.6 (skill triggers, the operation cooldown), §6.1 / §6.2 (the Final Assault verdict, summon stacks, summons in the hand, the promotion reward), §7 (bounty draft, 教鞭), §8.2 / §8.3 (`uniteLeft`, `descRaw`, `counterText`, the `elem` fill, the stealth bit), §9 (gauge row, model scale), §10 (机变 card, the card tap), §14 (`left`, the pool floor, the verdict, `uniteLeft`); superseded or revised: §17.5 (the 联防 count), §18.3 (the gauge look), §18.6 #11 (skill summons), §19.2 (the melee-only blocked-first rule; 灰毫 S2), §19.5 (the gauge look).
**Follow-up (`playtest6b`, 2026-10-02).** Four more workstreams were merged into `playtest6b` in this order: the elite-to-board rule (§20.11), the 假想敌:胄 kit audit (§20.13), the Final Assault leader HP report (§20.10) and the official limits read from the client binary (§20.12). Reports: co-op 终极 / 绝境 leaders died within seconds although the official leader never dies that fast with such layers (§20.10); "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置" (§20.11, user-confirmed). The boss-HP and the limits workstreams both capped bond layers at 999 — one implementation remains (`shared/constants.js BOND_LAYER_CAP` / `layerGainRoom`, used by every writer), and the boss-HP workstream's reading of 限伤 as an engine fixed-point overflow "not modelled" is superseded by the binary evidence (`MAX_BATTLE_DAMAGE` 300000, the hit is cancelled). Normative lines rewritten with it: §2 (`promote.js`), §5.1 (`layerGains` capped), §5.2 (直接乘算 in `Σpct`), §5.3 (same-named debuffs), §5.4 (`addLayers`, `loseHp`), §5.5 (限伤 between the multipliers and the shields), §6.2 (where a merge's elite goes), §6.3 (the layer cap), §7 (leader parts, drones), §8.2 (`hitCap`), §9 (the promotion cue), §14 (the layer bound of client results); §20.9 records the decisions. Two QA verifiers then checked the integrated build; their residual issues are handled in §20.14.
### 20.1 Summons placed by hand (#1, #2) — `tools/build-data.mjs`, `match/gamedata.js placeableTokens`, `PlayerState`, `match/bot.js`, `sim/Battle.js`, `content/tokens.js`, kits tier2/3/4, `ui/detailPanel.js`, `shared/constants.js SKILL_SUMMON_START_DEPLOY`
- **Seen** (the friend): 凯瑟琳's device and 赫默's drone could not be placed by hand. **Cause**: v2.3 (§18.6 #11) limited hand pieces to talent summons after the user's playtest #4 report on the drone, which dropped 医疗探机 and 诅咒娃娃; 爬行号·防护单元 has no shop-state entry, so 凯瑟琳's kit placed it automatically.
@@ -930,5 +938,80 @@ The user's answers of 2026-10-01 (playtest #6 follow-up):
| Multi-round bounty cards (§20.6) | last two battles like "接下来两场作战", blue text, in the draft | `server/match/choices.js MULTI_ROUND_BOUNTY_BATTLES = null` (official 每场) |
| 萨卡兹枯朽战车's reach (§20.4) | data range 2.2 + the operator's 0.25 collider for every ranged enemy; scales per data (100 % ranged, 200 % blocked melee, skill 100 %) | `server/sim/constants.js ALLY_COLLIDER_RADIUS = 0` |
| Blocking (§20.3) | every blocker hits the enemies it blocks, first, whatever its range or facing | gate `Battle.blockedTargets` / `sortEnemyTargets` blocked-first on a melee position again |
| A merge's elite (§20.11) | takes the consumed deployed copy's tile and facing, else the hand (PRTS 帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"; the user confirmed it, 2026-10-01) | none — official; restoring the v2.5 hand-first rule means reverting `PlayerState._mergeChess` (merge `5f3842f`) |
Still [ASSUMED] (each in its section): the class rows covering every MANUAL skill (§20.2); skill-summon redeploys free and never while the owner is away (§20.1); the 1-HP pool floor and the server-order verdict (§20.5); a short promotion tier topping up from below and “神秘顾客” keeping the band bounties (§20.6); the 弹道 column for unclassified pushes and the other displacement details (§20.3); 枯朽战车's skill firing on its blocker at 100 % and content enemy skills keeping a point radius (§20.4); the gauge's refill colour and placement (§20.8); the 机变 card's phone sizes (§20.7).
Official since the follow-up — read from the client binary (research 11, §20.12) or the official texts (§20.10, §20.13):
| Rule | Now | One-line flip |
|---|---|---|
| Bond layer cap (§20.12) | 999 per bond (`MAX_GARRISON_STACK`; `AddBondCount` stores min(L + n, 999)) — no longer [ASSUMED] | `shared/constants.js BOND_LAYER_CAP = 0` |
| 限伤 (§20.12) | a leader hit with ceil(d) ≥ 300000 in boss / hidden battles is cancelled (`MAX_BATTLE_DAMAGE`), not an overflow | `shared/constants.js BOSS_HIT_LIMIT = 0` |
| 直接乘算 (§20.10) | bond / strategy / item / 机变 / 特质 "+X%" are summed with each other and the skills' | `server/sim/constants.js DIRECT_BONUS_STACKING = 'multiply'` |
| Leader parts (§20.10, §20.13) | every damage a 剑 / 锤 / 碎铳之簧 takes is lost by its leader 1:1, 无来源 | `server/sim/content/bosses.js PART_TRANSFER = 0.5` (v2.5's share; v2.5 also passed a 剑 / 锤's damage only while grounded) |
Settled by the user ("保持固定血量"): the co-op leader pool does not shrink when players are eliminated — `config bossHpScale.aliveScaling` is **false** (pool = bloodPoint whatever the number of alive players; one community note, 巴哈姆特 12294, said otherwise) (§20.10); flip: `aliveScaling: true` (× alive / 4).
Still [ASSUMED] (each in its section): the class rows covering every MANUAL skill (§20.2); skill-summon redeploys free and never while the owner is away (§20.1); the 1-HP pool floor and the server-order verdict (§20.5); a short promotion tier topping up from below and “神秘顾客” keeping the band bounties (§20.6); the 弹道 column for unclassified pushes and the other displacement details (§20.3); 枯朽战车's skill firing on its blocker at 100 % and content enemy skills keeping a point radius (§20.4); the gauge's refill colour and placement (§20.8); the 机变 card's phone sizes (§20.7); the elite's tile among several deployed copies (the first in deploy order), a transformed carrier's tile and the elite's fresh summon stack (§20.11); "strongest" rather than "latest" for same-named debuffs, which "最大生命值" the 胄 drone link reads (the pool), the 剑 / 锤 dive hits passing on, the springs' split and the solo pool × 0.25 (§20.10); the scene server clamping prep gains like the client, a cancelled hit showing nothing, a passed-on loss being one hit and the order blocks → 限伤 → HP shields (§20.12); 胄's unread `max_hp` 0.5, the untargetable 初始模式 and a replacement copy inheriting only the HP (§20.13); talent debuffs keyed per source unit (two copies of one operator each apply theirs, §20.14).
### 20.10 Final Assault leader HP and damage (user report after playtest #6, 2026-10-01) — `server/sim/constants.js DIRECT_BONUS_STACKING`, `content/support directMods`, `sim/Battle.js applyStrongest / loseHp sourceless`, content bonds / bands / items / choices / garrisons / kits tier4, `content/bosses.js PART_TRANSFER / DRONE_LINK_BASE`, `match/gamedata.js bossPoolHp / bossPoolShare`, `match/finalAssault.js`, `data/config.json bossHpScale`
- **Seen**: co-op 终极 / 绝境 leaders died within seconds; with the user's layer counts the official leader never dies that fast.
- **HP and pool — official, unchanged**: `bloodPoint[difficulty]` = activity_table act2autochess bossInfoDict `bloodPoint` / `bloodPointNormal` / `bloodPointHard` / `bloodPointAbyss` of the current data (the cache equals upstream ArknightsGameData master as of 2026-10-01; `test/match/bosshp.test.js` pins all 40 values — e.g. 胄 247 500 / 675 000 / 1 800 000 / 3 600 000, hidden 胄 750 000 / 937 500 / 3 600 000 / 7 200 000). PRTS 盟约记录's leader table is an older (11月18日) revision: 铳 险境 and 胄 / 铳 / 萨米 绝境 differ, no 终极 column, no 卢西恩 row. The data has no per-player or per-field factor; leader HP ignores `enemyScale` ("领袖单位于服务器的生命值加成不受上述加成影响"). One pool for every boss field ("最终攻势中,所有人将一起对敌方领袖造成伤害"); notice 5114's "敌方领袖的总生命值不变" means the two mirrored copies of a pair field share it.
- **Pool size**: co-op = bloodPoint whatever the number of alive players — `bossHpScale.aliveScaling` (× alive / 4, one community note: 巴哈姆特 12294) stays **off** — the user chose the fixed pool (§20.9); solo × 0.25 [ASSUMED].
- **Fixed — why leaders fell too fast**:
1. **直接乘算**: "+X%" ATK / DEF / max HP from bonds, strategies, equipment, 机变 cards and per-layer 特质 are summed with each other and with the skills' "+X%" (PRTS 盟约记录 "…属性加成均为直接乘算"; 游戏数据基础 D_t = Σtᵢ, A = (A₀ + D_p)(1 + D_t)). v2.5 compounded them (×74 instead of ×12 at 600 精准 layers with four more bonuses). `DIRECT_BONUS_STACKING 'add'`, content/support `directMods`; 炎佑 ×1.5 and the char_attribute_mul 特质 stay multipliers (§5.2).
2. **Same-named enemy debuffs**: one instance per target, the strongest (PRTS 作战机制 "同名buff…只能表现出一个"; 巴哈姆特 12316 "共享型buff會跟對面搶") — 奥术, 灵知 坚冰, the 莱恩哈特 / 缄默德克萨斯 RES cuts (`Battle.applyStrongest`, §5.3). v2.5 kept one 奥术 per player, so a pair field multiplied it (×40 arts damage taken at +400 layers; ×7.6 now). "Strongest" rather than "latest" is [ASSUMED].
3. **Leader parts**: 剑 / 锤 / 碎铳之簧 pass every damage they take 1:1 as 无来源 HP loss, credited to the attacker (PRTS "受到等量的无来源生命流失"; `PART_TRANSFER` 1, `loseHp … sourceless`); v2.5 passed ½, a 剑 / 锤 only while grounded. The dive hits counting (PRTS talent "受到伤害时" vs the handbook "被击落时") and the springs' split to the other springs are [ASSUMED] (§20.13 for the 剑 / 锤 kit).
4. **坚守 thorns**: dealt 无来源 by the member who was hit and credited to that member (statistics only; up to 19 % of a co-op pool was nobody's).
5. **Bond layers**: uncapped in v2.5; now 999 per bond — official, §20.12 (this workstream first added the cap as [ASSUMED] from the community; the merge kept the limits workstream's single implementation).
- **Kept**: a 胄 drone costs 2 % of the leader's shown max HP = the pool (`DRONE_LINK_BASE 'pool'`, = v2.5; which "最大生命值" the official client reads is [ASSUMED] — the review tried the unit's data 600 000, which made drones 19–78 % of a solo 标准 fight, and went back). Under 限伤 it is one hit: 144 000 at most today (hidden 胄 终极), cancelled only above a 14 999 950 pool (§20.12).
- **Ruled out by measurement**: double crediting (client / server / replica), multi-hits through the 5×3 hit area, element bursts, %-max-HP damage on leaders (only the drone link).
- **Official, not a bug**: per-layer bonuses have no cap below 999 (谢拉格 vs cold ×(1.35 + 0.01 L) = ×11.34, 奥术 at low HP ×15.7, 精准 +1208.8 % ATK at 999 layers), so very high layers still kill fast — 绝境 with +400 on every active bond: median 8.6 s, fastest 3.6 s; 终极 under 20 s needs ≈ +400 on every bond (≈ 2 800 active layers per player; median 16.8 s); with two bonds per player at 999 (≈ 2 000 layers) 绝境 kills median 10 s, 7 of 28 under 5 s; with every active bond at 999 终极 median 8.3 s, 绝境 3.7 s — and there 限伤 (§20.12) cancels 6–77 hits per 12–28 fights. The community reports the same (巴哈姆特 12522, 999 谢拉格 "boss一秒死").
- **Measured** (real seconds — the fights run at 2×, so half the game seconds of the QA tables). Integrated build (QA 6b on `cca11e6`, after the elite-to-board merge): the R14 boards of real 4-AI co-op matches, seeds 7 / 8 / 9 / 12 × boss_1…7 = 28 Final Assaults per row, both pair fields on one pool, the same boards replayed on v2.5 → now: 终极 own layers median 206 → 258 s, +200 on every active bond 24.5 → 42.1 s (fastest 8.2 → 18.9 s), +400 11.3 → 16.8 s; 绝境 own 79 → 99 s, +200 11.6 → 14.8 s (fastest 1.9 → 7.3 s), +400 6.1 → 8.6 s; each player's top bond requested at 1500 (now held at 999): 终极 53 → 95 s, 绝境 17.5 → 33 s. On each version's own bot boards the medians moved only ×1.0–1.2 (终极 own 256 → 258 s, +200 47 → 54 s; 绝境 own 81 → 99 s, +200 13 → 15 s): the elite-to-board rule (§20.11) gives the bots slightly stronger R14 boards (终极 2.12 → 2.26 elites per player, 绝境 2.14 → 2.51; 绝境 6.0 → 6.9 active bonds), so a player whose board did not change feels the same-board numbers. No landed leader hit reached 300 000 (v2.5 landed up to 18.6 M); the largest at +400 is 144 000 (a hidden 胄 drone; 114 000 from an operator), at 999 layers 299 600. The boss-HP workstream's own measurement (42 pre-merge boards, seeds 7–9: 终极 +200 30.9 → 56.0 s, …) is kept in BALANCE §3, labelled as such; BALANCE §3 has both tables.
- Tests: `test/match/bosshp.test.js` (real Final Assault / Hidden Core matches, the 40 bloodPoint values; the 奥术 pair test runs seed 12 since the elite-to-board merge changed the bots' boards), `test/sim/bosshp_stacking.test.js`, `test/match/layercap.test.js`, `test/sim/battle.test.js` (Battle.addLayers cap), `test/content/bonds_addon.test.js`, `kits_t4.test.js`, `kits_alt_t4.test.js`, `enemies_bosses.test.js`, `items.test.js`, `garrisons_battle.test.js`, `choices.test.js`, `test/match/unitStats.test.js`, `finalAssault.test.js`, `balance.test.js`, `playtest6-bosspool.test.js`; opt-in `SIM_E2E=1 node --test test/sim/bossfield.browser.test.js` (the real boss / hidden specs of a 999-layer bot match give the same digest, pool and fx in headless Chrome as in Node; covers §20.12 and §20.13 too).
### 20.11 A merge's elite takes the deployed copy's tile (user-confirmed) — `match/PlayerState.js _mergeChess / transformChess`, `match/board.js mergeTile`, `match/audit.js`, `ui/gameLogic.js mergeProgress / mergeTarget`, `ui/shopBar.js armedSlotOf / onArm`, `screens/game.js MERGE_HL`, `render/promote.js`, `render/fx.js promote`, `render/app.js setPrep`
- **Seen** (the user, first-hand, after playtest #6): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置".
- **Cause**: research 01 A1 row 7 read PRTS 帮助 as "1 elite goes to the hand, not to a board tile" (marked VERIFIED, repeated in `01-core-data.json` promotion.eliteGoesTo), so `_mergeChess` put every elite in the hand and used a consumed copy's tile only when the hand and temp were both full.
- **Official** (PRTS 卫戍协议/帮助): §干员的获得与精锐化 "…发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; tip "干员晋级后已配发装备会回收至整备区,需要重新配置"; §战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区", deploy order "从上到下>从左到右". The text does not say which tile wins among several deployed copies.
- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — row desc, then col asc, the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A deployed piece transformed into the completing copy (突变细胞, `transformChess` `fromKey` / `fromDir`) counts as deployed with its own tile [ASSUMED]. A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
- **Now (client)**: `gameLogic.mergeTarget(priv, chessId)` mirrors `mergeTile`; a shop or reward card armed for a merge-completing buy lights the target tile in gold (`ShopBar onArm` → `screens/game.js` highlight group `mergeTile`); the 可晋升 tag says 精锐干员将出现在作战区原位置 or 精锐干员将进入整备区; an elite card never merges (`completesChessMerge` refuses `isGolden`, `mergeProgress` reports 0 copies: no pips, tag or tile); `render/promote.js promotionsOf` recognises a merge in setPrep and the elite plays `fx.promote` (gold pillar, shockwave, hex ring, streaks from the other copies) instead of `fx.deploy` (`view.debug.promotions`).
- **Checks**: `audit.js` checks every merge — the tile is `mergeTile` of the deployed copies legal for the elite, with that copy's facing; the deploy count did not grow (a carrier `transformChess` took off the board first still counts); otherwise the elite is not on the board, except the no-room fallback. `tools/matchrun --check` sweeps are clean.
- Normative lines rewritten: §2 (`promote.js`), §6.2 (the economy line and the temp-overflow paragraph), §9 (the promotion cue).
- Tests: `test/match/playtest6-elite-to-board.test.js`, `test/match/merge.test.js`, `hardening.test.js`, `facing.test.js`, `test/ui/playtest6-elite.test.js`, `test/render/playtest6-promote.test.js`, `test/docs-consistency.test.js`; opt-in `SP_E2E=1 node --test test/ui/playtest6-elite.e2e.test.js` (real server and prep: the armed card lights the deployed copy's tile, the elite stands there after the buy, 剩余可放置角色 unchanged).
### 20.12 Official limits: 999 layers per bond, 限伤 300000 on leaders — `shared/constants.js BOND_LAYER_CAP / BOSS_HIT_LIMIT / layerGainRoom`, `match/PlayerState.addLayers`, `Match` SETTLE, `sim/Battle.addLayers / loseHp`, `sim/damage.js leaderHitCancelled`, `match/fields.js validateClientResult`, `match/invariants.js`, `render/fx.js`, `tools/matchrun.mjs`, `tools/balance.mjs`
- **Asked**: the co-op leaders as hard to kill as in the official game. Research 11 (`docs/research/11-limits-official.md`) found two official caps in the client (2.7.71 il2cpp) that the remake lacked.
- **Official** [DATA]: `AutoChessBattleConst.MAX_GARRISON_STACK = 999` — `AddBondCount` stores `min(L + n, 999)`, per bond. `MAX_BATTLE_DAMAGE = 300000` — in step mode (every boss battle outside training) `AutoChessStepModeManager._OnBossEnemyTakeDamage` runs `modifier.Cancel()` when `ceil(value) ≥ 300000` on an `IsBossEnemy` unit; `IsBossEnemy` (disassembled, research 11 §2.3) = enemyId ∈ `autoChessData.bossInfoDict[*].enemyId` = data/bosses.json `enemyKey`. [COMM] bonds stay at 999 while fed; 「一次超过30w会被卡掉」: players avoid extra multipliers at 999 layers. 限伤 is a deliberate constant, **not** an engine fixed-point overflow — the boss-HP workstream's earlier reading (2³¹, "not modelled", §20.10) is superseded.
- **Now — layers**: the single implementation is `shared/constants.js BOND_LAYER_CAP` + `layerGainRoom(before, n)` = min(n, 999 − before), never negative (the boss-HP workstream's duplicate `sim/constants.js BOND_LAYER_CAP / layerRoom` was removed in the merge). Every writer uses it: `PlayerState.addLayers` (特质, items, bands, 机变 cards, bonds), the SETTLE of `BattleResult.layerGains`, `Battle.addLayers` (the live copy, clamped after the `layerGain` hook, like the client), and the dev tools' direct writes (`matchrun --layers N`, `balance.mjs applyBoard`). A gain at the cap adds 0: no onLayers, hook or 'layer' event, so the per-N milestones (远见, 奇迹, 维多利亚 …) stop with the count. Client results: per-bond `layerGains ≤ min(60 + 4·round, 999 − starting layers)`. `invariants.js` flags a bond above 999; the UI shows the server's capped count.
- **Now — 限伤**: kinds `'boss'` / `'hidden'` only; a leader is `isBoss` (the tag-'boss' units: those 10 keys and their mirrored copies — parts, 刺胄之弹, drones and escorts never count). A hit with `ceil(final) ≥ 300000` is cancelled: checked in `dealDamage` after DEF/RES and every multiplier, before HP shields, and in `loseHp` (a part's 传递, a drone's death). A cancelled hit causes no HP or pool loss, no credit or stats, no damage number, no `damaged` / `fatal` / kill; the attack, its SP, the `hit` hook and separate element 损伤 hits stay. Element bursts and DoT ticks are checked, gauge fills are not. The sim emits fx `hitCap {id, n}`, drawn as nothing (deterministic in Node and browsers; SP_VERIFY digests agree).
- **The line vs the pool**: 【死亡集群】 drone deaths cost 0.02 × the pool max (§20.10, §20.13) — at most 144 000 today (boss_8 终极 7.2M), so it lands; a pool above 14 999 950 would cancel every drone death. An alive-scaled pool (§20.9, open) can only shrink, but any larger pool must re-check it; a real-kit test pins the line.
- **[ASSUMED]**: the scene server clamps prep gains the same way; a cancelled hit shows nothing; a loss passed on to a leader is one hit; the order hit-step blocks → 限伤 → HP shields — 阿利斯泰尔's 莫非王土 vest (`boss:vest`, 5000): a hit of 300000–304999 is cancelled and the barrier stays; 假想敌:再生's blocks on 假想敌:管 (hidden h08_03) negate a hit past the line before the limit sees it.
- **Measured**: the competent and strong balance boards never reach the line (biggest leader hits 28k–72k), so BALANCE §3's model numbers stand (BALANCE §7); the co-op replays up to +400 per bond and a top bond at 999 cancel nothing either. Two bonds per player at 999 or every active bond at 999 do reach it: 6–18 cancelled hits per 28 Final Assaults and 14–77 per 12 Hidden Cores (QA 6b), and those kills take longer than with v2.5's uncapped hits (§20.10).
- **Flip**: `BOND_LAYER_CAP = 0` and / or `BOSS_HIT_LIMIT = 0` (or Infinity).
- Tests: `test/sim/playtest6_limits.test.js` (including the real-kit vest, 再生 and 【死亡集群】 cases), `test/match/playtest6-limits.test.js` (a 999-layer board through a real Final Assault + Hidden Core, both tools at the cap), `test/render/playtest6_hitcap.test.js`, `test/sim/playtest6_bosspool.test.js` (adapted), and the boss-HP workstream's `test/match/layercap.test.js` / `test/sim/battle.test.js` cap tests on the single API.
### 20.13 假想敌:胄 kit audit (PRTS) — `content/bosses.js kitHelm / kitBlade`
- **Audit** (a separate session, 2026-10-01), against the PRTS 技能 / 天赋 / 能力修正 texts and the skill blackboards:
- 灭顶之灾 fires **one** 刺胄之弹 below 20 % too (PRTS 能力修正 原因 6 "描述与游戏实际表现不符合": no extra shell; no blackboard holds a second one), at the highest-ATK operator **within attack range** for both copies ("触发索敌和普通攻击相同"; boss_1 used the whole field).
- 死亡集群: **any** drone death costs the leader 2 % max HP (= the pool, §20.10), credited to the killing operator if one killed it; a leak costs nothing (能力修正 "该无人机单位死亡时"). Drone HP: `summon.hp_ratio` (隐秘核心) scales it; skill 2's `max_hp` 0.5 is not read [ASSUMED] — no source defines it.
- 斩胄之剑 / 破胄之锤: 初始模式 hovers invulnerable (untargetable [ASSUMED]) and attacks everything in range (锤 150 % ATK, unblockable). 掷 → 出击模式: blink to the start of the level's `<hand>_blink` route, fly at the lowest-ATK operator, no normal attack. 15 hits → 【瘫痪】: stun 10 s (its end ends 瘫痪, not the 20 s), ground unit, damage taken ×1.3. Landing or 瘫痪 ending: a new 初始模式 copy with the old HP is summoned on the `<hand>_origin` route and the old one is killed (a part: uncounted, no bounty) [ASSUMED: only the HP is inherited, so 掷 comes back after its initial 25 / 55 s instead of the 80 s cooldown].
- Every damage a 剑 / 锤 takes passes 100 % to 胄 (PRTS "等量", also during the dive) — merged with §20.10 into the one `PART_TRANSFER` 1 (`BLADE_TRANSFER` is its alias), 无来源 and credited to the attacker.
- **Measured** (same-seed `balance.mjs --boss-only`, n = 3–5 per cell): boss_1 unchanged (≤ 0.5 s); boss_8 独立 绝境 44.7 → 45.2 s, 独立 终极 win 2/3 → 3/3, 同盟 绝境 23.3 → 23.7 s, 同盟 终极 72.0 → 64.2 s (BALANCE §3).
- Tests: `test/content/enemies_bosses.test.js` (shell count and range, drone deaths and leaks, the pool-based link, the sortie / 瘫痪 / replacement cycle with the 1:1 无来源 transfer and its credit).
### 20.14 QA residuals of the integrated build (2026-10-02) — `match/Match.js _onResult / heldResult`, `render/app.js promoBase`, `ui/shopBar.js mergeHint`, `ui/detailPanel.js`, `screens/game.js dSide`
Two QA verifiers ran the integrated `playtest6b` (real 4-AI and 2-human co-op matches for every leader, Node vs headless Chrome digests, the merge UI on the user's phone profile and on desktop). Every check passed; the residual issues and what was done:
- **Fast boss kills handed over (fixed).** Under client-side combat a pair that empties the pool faster than the plausibility budget (one field credits ≤ the pool per `BOSS_MIN_CLEAR_GS` 5 game s, §14) had its `'cleared'` `b.result` rejected as implausible, the client demoted and the field handed to the partner or the server — seen at 绝境 with every bond at 999 (kills at 2.0–4.0 game s; official 999-layer play reports "boss一秒死"). Now a `'cleared'` result whose cumulative report covers what the pool holds (1 HP slack, the pool floor) is **held** (`f.heldResult`): the boss clock credits the report at the budget's rate, the silence watchdog and handovers skip the field, a re-sent copy is ignored, and the pool's end completes the field with it (`_endFinal`); a team-LP defeat first still credits nothing more. The budget itself is unchanged, so a forged report gains nothing a `b.progress` could not already get. The verdict waits at most until the budget covers the report (≤ 5 game s from the field's start). Re-run of the QA cases (绝境 seed 7 boss_1 / boss_6 with every bond pushed to 999, seed 21 with 990 early layers, 3 more at 999 / 终极): 0 rejections, 0 takeovers, every boss field kept by its client, Σ credited boss damage = the cleared pools; in headless Chrome (2 humans + 2 AI, a scratch server with a 12× slower budget so the path runs) both boss rounds held the clearing client's result and went on to the Hidden Core and the result with no takeover and no console error.
- **No promotion cue for a merge outside the prep (fixed).** A merge completed between two preps (a band / 机变 grant at ROUND_START, a SETTLE merge) placed the elite silently: `enterBattle` emptied the prep state the cue compares with. `render/app.js` keeps the last prep pieces across a battle or scouting board (`promoBase`), so the next `setPrep` plays `fx.promote` on the elite (streaks from the consumed copies' slots). Re-run of the QA case (松桐 grants the third 古米 at R4 ROUND_START): `view.debug.promotions` records the elite on the copy's tile, copies 2.
- **The 可晋升 destination on touch (fixed).** The tag's text was a native `title`, never shown on a phone. The detail card the first tap opens now says it under the header — 「可晋升:精锐干员将出现在作战区原位置」 / 「…将进入整备区」 (`shopBar.js mergeHint`, gold `.dhint--merge`; shop and reward cards).
- **The detail card covered the gold tile (fixed).** At 1920×1080 the left card covered half of a target tile in the leftmost legal column; the card now docks on the side away from the armed merge tile (`screens/game.js dSide`, the underframe's `panelSide` rule), on phones too.
- **Measurements re-taken (docs).** §20.10 / §20.12 and BALANCE §3 / §7 now give the integrated build's numbers (real seconds; the earlier boss-HP numbers are labelled as pre-merge boards) and the 999-layer profiles where 限伤 cancels hits.
- **Not changed — per-source talent debuffs [ASSUMED].** Some talents key their debuff with the source unit (`etlchi:dot:<id>`, `ines:atkSteal:<id>`, `ines:sentry:<id>`, and similar slows / DoTs in other kits), so two copies of one operator (a pair field where both players own it) each apply theirs. PRTS 作战机制 says only that same-named buffs show one instance under the *default* overlap strategy; whether these talents use it across copies is not in the data the project has, and the effect on leader kill times is small (an arts DoT, an ATK steal, a slow). The rule stays applied to the debuffs that move leader kills (奥术, 灵知 坚冰, the RES cuts — §20.10).
- **Not changed — temp items behind a full hand.** With the hand full, items overflowing into the 临时整备区 (row 8) stand behind the bench row's operator models and are hard to see; the row's red frame, the 「临时整备区 N 件道具待处理」 notice and the refused Ready still show them. A layout change of the temp row is left for a later pass.
- Tests: `test/match/clientCombat.test.js` (the held result; a 'cleared' result not covering the pool is still handed over), `test/render/playtest6-promote.test.js`, opt-in `RENDER_E2E=1 node --test test/render/browser.test.js` (a merge between two preps is cued), `test/ui/playtest6-elite.test.js` (the detail hint), opt-in `SP_E2E=1 node --test test/ui/playtest6-elite.e2e.test.js` (the hint in the real detail card).
+58 -24
View File
@@ -35,9 +35,9 @@ server/match/
LOBBY → INFO_CHECK (co-op 25 s; solo and single-human matches untimed; all humans confirmed ⇒ next) → BAND_DRAFT → BATTLE_CHECK (3 s)
→ for r = 1..lastRound (+ hidden):
ROUND_START (2 s) income + pending coins, upgrade price −1 (r > 1, floor 0), temp NOT wiped (what overflowed after
the last prep's deadline — battle-result grants, a SETTLE merge's elite, returned equipment — is
shown and usable in this prep, `PlayerState.tempDue`; reward offers earned after the last prep —
a SETTLE merge — are kept for this prep),
the last prep's deadline — battle-result grants, a SETTLE merge's elite with no copy deployed,
returned equipment — is shown and usable in this prep, `PlayerState.tempDue`; reward offers
earned after the last prep — a SETTLE merge — are kept for this prep),
shop reroll (frozen slots kept in place, then unfrozen), the round's wave generated (preview),
onRoundStart dispatch
[SP_DRAFT] r ∈ modes[m].spRounds (机变)
@@ -130,8 +130,9 @@ A `choice:<effectId>` registry handler overrides the default application (§2.4)
* `g.autoplay { on }` ("AI 托管"): the bot plays the seat (drafts, buying, placement, ready) until turned off.
* `onLeave` (quit / reconnect window expired): 中途退出 counts as elimination (research 00-INDEX §3, 01 §9, 06 §7 /
§10.3): every copy the seat holds returns to the shared pool at once; the seat leaves the round loop and the Final
Assault pairing (re-planned when it quits before the boss fight; the boss pool has no alive-player factor, so it does
not change); its running normal battle is force-ended; a pending band pick
Assault pairing (re-planned when it quits before the boss fight; the boss pool stays bloodPoint — it would shrink to
× alive / 4 only with config `bossHpScale.aliveScaling`, off — the user chose the fixed pool, DESIGN §20.10); its
running normal battle is force-ended; a pending band pick
becomes the default band and a 机变 turn passes on. Status `left`, LP 0, rounds passed = the rounds it had survived.
When no human is left at all the match ends immediately (`reason: 'abandoned'`); when only eliminated spectators are
left it ends as `'eliminated'`.
@@ -230,7 +231,7 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa
| `onRefresh` | manual refresh | `{ slots, free, price }` (slots mutable, or use `ctx.setShopSlot`) |
| `onPrice` | every price query of a shop slot (views + buy) — must be **pure** | `{ slot, kind, id, price }` — write `ev.price` / `ctx.setPrice` / `ctx.modifyPrice` |
| `onBuy` | after a purchase | `{ piece, slot, price, kind }` (`piece` = owned result, elite after a merge) |
| `onMerge` | chess or item merge | `{ kind, piece, baseId|itemId, consumed:[uid] }` |
| `onMerge` | chess or item merge | `{ kind, piece, baseId|itemId, consumed:[uid], area? }` — a chess merge's `area` is where its elite went: 'board' (a consumed copy's tile) \| 'hand' \| 'temp' |
| `onLevelUp` | shop level up | `{ level, price }` |
| `onSpend` | a payment's action is complete (buy / refresh / levelUp / reward / effect) | `{ amount, reason, total }` (`total` = funds spent this match) |
| `onBattleStart` | a battle input is built (normal / unite / boss / hidden); also for the stats preview (`g.unitStats`, DESIGN §18.5) | `{ input: PlayerBattleInput, kind, round, spawns?, preview? }` — mutate/replace `ev.input`. With `ev.preview` true (no `ev.spawns`) a handler must NOT change match state or draw match rng: the preview only reads the input |
@@ -239,7 +240,7 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa
| `onEquip` | `g.equip` (item handler only) | `{ item, target, golden, consumed, keep, error }` — see §2.5 |
| `onArt` | `g.art` (item handler only) | `{ item, row, col, targets:[pieces], error, used }` |
| `onDestroy` | an item was destroyed (player / replaced) | `{ item, holder, reason }` |
| `onLayers` | bond layers were added (prep or battle gains) | `{ bondId, from, to, reason }` (milestones: 维多利亚 25, 远见 10, 奇迹 100 …) |
| `onLayers` | bond layers were added (prep or battle gains) | `{ bondId, from, to, reason }` (milestones: 维多利亚 25, 远见 10, 奇迹 100 …); `to` ≤ 999 (`BOND_LAYER_CAP`) — a gain at the cap dispatches nothing, so the milestones stop with the count |
Dispatch order per player: `global` → `band` → `bond` (data order) → garrisons (board in deployment order, then hand)
→ equipped items → EffectRefs (insertion order). `onPrice` runs the priced chess's own 特质 first (购买价格为N sets the
@@ -287,7 +288,7 @@ Writes (all validated, never throw on bad input, never make funds / pools negati
| helper | effect |
|---|---|
| `addFunds(n, reason?)` / `addPendingFunds(n)` / `spendFunds(n)` | funds now / at the next round start / pay (false when short) |
| `addLayers(bondId, n, { requireActive })` | layer gain (`requireActive` = "使已激活的…"); returns layers added; fires onLayers |
| `addLayers(bondId, n, { requireActive })` | layer gain (`requireActive` = "使已激活的…"); returns layers added — at most the room left under `BOND_LAYER_CAP` (999, shared/constants.js `layerGainRoom`; the battle gains merged at SETTLE too); fires onLayers unless it added 0 |
| `grantChess(id, { toTemp, golden, requirePool=true, fromPool=true })` | acquire a chess (takes pool copies; with `requirePool` a pool chess with no copy left fails → `null`); merges; fires onGain |
| `grantItem(id, { toTemp })` | acquire an item (merges with an identical normal item) |
| `rollChess({ maxTier, tier, bond, filter })` / `rollItem({ pool, tier, maxTier })` | copy-weighted chess id from the shared pool / item id (choices.json pools) |
@@ -386,14 +387,23 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
Ready, during onPrepEnd, or outside PREP (COMBAT, 联防, SETTLE, ROUND_START, 机变) → the end of the NEXT prep, so it
is shown and usable first; `setReady(false)` makes what arrived while ready due at the current prep. The round
start never wipes temp (DESIGN §6.2).
* **Merge**: 3 normal copies (风丸 2) anywhere (board/hand/temp) → elite to the hand (the incoming copy, then temp, hand,
board copies are consumed); equipment returns to the hand; summons of consumed copies are removed; a reward offer of 3
* **Merge**: 3 normal copies (风丸 2) anywhere (board/hand/temp) → 1 elite (the incoming copy, then temp, hand, board
copies are consumed). Where it goes (PRTS 卫戍协议/帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
则发送至作战区对应位置)", the user's playtest #6 follow-up): when a consumed copy stood on the board, onto that copy's
tile with its facing — of several, the one that deploys first (row desc, then col asc; `board.js mergeTile`,
[ASSUMED]); a deployed piece transformed into the completing copy (突变细胞, `transformChess`) counts with its own tile;
a tile the elite may not use (a stale terrain change) is skipped. It replaces a deployed copy, so the deploy count never
grows (no BOARD_FULL), and as a deployment its manually deployable summons join the hand (`grantTokensFor`, the
player's loadout). Otherwise the elite goes to the hand, overflowing into temp. Equipment returns to the hand (overflow
temp; with both full it stays on the elite, up to its 2 equip slots — any further item is destroyed); summons of
consumed copies are removed; a reward offer of 3
**different** free chess of tier min(level+1, 6) (copy-weighted from the shared pool, already-drawn ones excluded; a
short tier tops up from the tier below — user playtest #6 item 19; pick 1, expires at prep end; queued when several
merges happen). A merge
completed after the prep (SETTLE / Final Assault effects such as 突变细胞) keeps its offer for the next prep; its elite
goes to the hand like any merge's, overflowing into temp (kept through the next prep, see Hand) — it takes a freed
board tile of a consumed copy only when the hand and temp are both full (research 01 A1).
merges happen). The same rule holds for every way a merge completes — buy, reward pick, effect / band / choice grants
(`acquireChess`), transformations — and in every phase: a merge completed after the prep (SETTLE / Final Assault
effects such as 突变细胞) keeps its offer for the next prep; its elite takes the deployed copy's tile at once, or goes to
the hand / temp (kept through the next prep, see Hand). In a boss round's prep the tile is read on the player's half of
the boss field (board coordinates unchanged). `onMerge` carries `area` ('board' | 'hand' | 'temp').
* **Board**: rows 9–12 × cols 2–10, legality from `stages[id].tiles` + devices (board.js); deploy cap 8 (+effects);
summons don't use slots; board↔hand swaps always allowed. A terrain change (terrain 机变 cards such as 模拟战场演变·
模式二 "阻隔工事变为射击台", content `setDeviceActive` / `setTileOverride`) is checked at the next `recompute()` (at the
@@ -412,6 +422,14 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
to the deploy limit, `grantTokensFor`). Battle side: SIM.md §1.1 token pieces.
* **Bonds**: bondsMeta.js (BOARD distinct, BOARD_AND_DECK, 绝技 elites, 调和 +1, 独行 downward, 助力 upper tiers,
变形同构体 grants). Σ activated layers for the hidden core = Σ layers of active bonds at the boss round's prep end.
**Layer cap** (research 11 §1; the client's `MAX_GARRISON_STACK` / `AddBondCount` = min(L + n, 999)): each bond's
layers stop at `BOND_LAYER_CAP` = 999 (shared/constants.js; 0 / Infinity = off). Every writer clamps with
`layerGainRoom` — `PlayerState.addLayers` (all prep-side gains: 特质, items, bands, 机变 cards, bonds; `ctx.addLayers`),
the SETTLE of the in-battle gains and the battle's own live copy (`Battle.addLayers`, SIM.md §6); a gain at the cap
adds 0 and dispatches no onLayers. The client-result check bounds a reported gain by the room left from the bond's
starting layers (`fields.js validateClientResult`, 'layer bound'); `invariants.js` flags a bond above the cap. The
bond strip, its popup, the effect text and the detail card show the server's capped count. The dev tools' direct
writes (`tools/matchrun.mjs --layers N`, `tools/balance.mjs applyBoard`) stop at the cap too.
* **Waves**: waves.js header (stage/factions/boss per match, faction replacement per round with `k` copies, scaling by
`enemyScale[r]` × the tuning layer §3.1, bounties, boss templates, 联防 routing). Bounties with battles left (every one
that lasts more than one battle; an official multi-round card lasts two, §1.2) also spawn in the Final Assault /
@@ -422,13 +440,17 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
default 2). Read as game seconds the rounds' own spawn schedules would not fit (R2's last flyer spawns at 43 s of
45 s, R3's at 62 s of 55 s); × 2 every limit ≈ last spawn + one flyer crossing. docs/BALANCE.md §2.1.
* **SETTLE**: LP −min(counted leaks, 10) (after 联防: survivors attributed to their source, same cap); IN_BATTLE layer
gains applied; kill-bounty coins (paid by the Battle to the killer — a 联防 helper included) and perfect-bounty coins
gains applied, each bond up to `BOND_LAYER_CAP` (999, `layerGainRoom`, as `PlayerState.addLayers`); kill-bounty coins (paid by the Battle to the killer — a 联防 helper included) and perfect-bounty coins
(own phase perfect) go to pending funds; bounty rounds decrement; LP ≤ 0 ⇒ eliminated (all copies back to the pool).
* **Final Assault / Hidden Core**: finalAssault.js header. Boards are passed in board coordinates; the sim maps board
rows 9–12 onto boss rows 2–5 (`BOSS_ROW_OFFSET` −7, matching every stage's boss rows) and mirrors the right side.
Pool (`finalAssault.js bossPoolHp`, research 08 §6 #7): co-op = `bloodPoint[difficulty]` as is — one pool shared by
every boss field, no alive-player factor ("敌方领袖的总生命值不变"); solo = × `bossHpScale.solo` (0.25 [ASSUMED]); leaders
are never scaled by `enemyScale`. The merged team LP loses leaks (`lpr`), the overtime drain
Pool (`finalAssault.js bossPoolHp` → `GameData.bossPoolShare`, DESIGN §20.10): one pool shared by every boss field
(official tip "最终攻势中,所有人将一起对敌方领袖造成伤害"); co-op = `bloodPoint[difficulty]` whatever the number of alive
players (notice 5114's "敌方领袖的总生命值不变" is about the mirrored copies of a pair field sharing the pool, not about that
number); `bossHpScale.aliveScaling` true (default false) would scale it × alive / `aliveFull` (4) — 巴哈姆特 12294
"聯機隊友(撤退/死掉)變少,最後boss血條也會變少" is one community note without a proportion, kept off until the user confirms
it (it would shorten the fight after eliminations, the opposite of the playtest report); solo = ×
`bossHpScale.solo` (0.25 = one player of four [ASSUMED]); leaders are never scaled by `enemyScale`. The merged team LP loses leaks (`lpr`), the overtime drain
(`bossTurnHpReduceTime` 150 counts REAL seconds, like the boss level's 120 s maxPlayTime that runs out first — the
battle goes on — so 1 LP per real second from 150 real s = 300 game s on the 2× field clock; `gd.bossOvertimeDue`)
and leader "扣除目标生命" effects (the sim's `lpLoss` hook: boss_7 Doom, 斥退 …); after every change it is written back
@@ -438,9 +460,11 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
credited pool damage of one field ≤ the whole pool per `BOSS_MIN_CLEAR_GS` (5) game s (20 %/s; the balance model's
fastest mean kills run ≈ 5 %/s per field), its LP cost ≤ 10 + 1 per game s; reports are cumulative, so what exceeds
the budget is credited later (the boss clock re-applies the latest report), never lost. A boss field's `b.result` ends
it only when the pool is empty or after the match's `b.end`; any other (a 'forced' result at t = 0, 'cleared' while
the pool holds, a result failing validation) hands the field to the partner's replica or the server, and the sender is
never its authority again (`f.demoted`). Every boss field run by the server (takeover, or nobody connected at the
it only when the pool is empty or after the match's `b.end`; a 'cleared' result whose report covers what the pool
holds waits for the budget instead (`f.heldResult`: no takeover — 999-layer kills take 2–4 game s, DESIGN §20.14);
any other (a 'forced' result at t = 0, 'cleared' while the pool holds more than the report covers, a result failing
validation) hands the field to the partner's replica or the server, and the sender is never its authority again
(`f.demoted`). Every boss field run by the server (takeover, or nobody connected at the
start) publishes its own damage as `b.pool.acked[fieldId]` (its CreditPool total), so the display replica of a
reconnecting / watching human stays in sync.
End and verdict (user playtest #6 item 5): the pool never holds less than 1 HP (`sim/constants.js BOSS_POOL_MIN_HP`:
@@ -450,6 +474,11 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
on until the overtime drain. The first end condition the server registers decides (`Match._finalEnding`): pool 0 →
victory, team LP 0 → defeat (PRTS 卫戍协议:盟约 下半 "…使目标生命值扣除至0,则无视倒计时直接失败"); after a forced end
no report (an in-flight `b.progress`, the final `b.result` with its rounded per-player damage) credits the pool.
**限伤** (research 11 §2; client `AutoChessStepModeManager._OnBossEnemyTakeDamage`): in both boss rounds a single hit
on a leader with ceil(damage) ≥ `BOSS_HIT_LIMIT` = 300000 (shared/constants.js; 0 / Infinity = off) is cancelled in the
sim (`sim/damage.js leaderHitCancelled`, SIM.md §4) — it deals 0 and nothing reaches the shared pool, the per-player
boss damage or the BOSS_HIT tickers; minions, parts, normal rounds and 联防 are unaffected. The server's own runs, the
browsers' runs and the server's verification share the rule, so digests agree.
### 3.1 Balance layer (data/tuning.json)
`data/config.json` is generated and stays research-faithful. There is **no custom balance** any more (DESIGN §14
@@ -562,7 +591,8 @@ receiver only), plus CUSTOM texts (eliminations, 联防, hidden core).
model, layout planner, rehearsal side-effect freedom, economy/bench regression).
`node --test test/match/*.test.js`
* `node tools/matchrun.mjs --mode coop --difficulty HARD --players 4 --seeds 20` — per-round balancing summary / aggregate
(`--check` audit, `--errors` per-source error table, `--lp N` / `--layers N` to reach late rounds, `--rehearsal N`).
(`--check` audit, `--errors` per-source error table, `--lp N` / `--layers N` to reach late rounds — the boost stops at
999 per bond —, `--rehearsal N`).
* `node tools/balance.mjs --mode multi --difficulty NORMAL` — the competent-board difficulty model (docs/BALANCE.md):
per round leaks / LP after 联防 / clear time of representative boards against the real waves, boss damage by 150 s
and kill time; `--tuning off` (research numbers), `--legacy-time`, `--profile weak|strong`, `--bots N`, `--json`.
@@ -579,6 +609,9 @@ receiver only), plus CUSTOM texts (eliminations, 联防, hidden core).
* Chess granted by effects need a free pool copy unless `requirePool: false` (then they hold 0 copies).
* Boss-round `local` pack spawns (boss parts) all spawn; content scripts (bosses.js) decide their behaviour.
* The Final Assault ends as a defeat when every field finished with the boss pool above 0 (boss escaped).
* The 999 layer cap holds for every prep-side gain too (the client only shows the in-battle clamp; the scene server's
code is not in the client — research 11 §1.2). A 限伤-cancelled hit shows no number; a part's damage passed on to its
leader (Battle.loseHp) meets the limit like a hit (research 11 §2.3).
* Combat limits are real seconds (× 2 in game seconds) — see §3; research 00-INDEX §8 #10 assumed game seconds.
* Emotes faster than 1/s answer `RATE`.
* 中途退出 = elimination at once (research); a quitter's operators already fighting in a shared field (联防, boss
@@ -588,5 +621,6 @@ receiver only), plus CUSTOM texts (eliminations, 联防, hidden core).
§4) with HP 0, and redeploys after its full redeploy time: no timer carry (the official setup carries only hp / tech
per operator), forced out before `battleStart` (its timer re-read after it, so 征召's −50 % applies; 征召's row check
does not count it), no knock-out hooks (`kill`, 'killed' deaths) a second time.
* A merge completed after the prep (SETTLE effects) keeps its reward offer for the next prep; its elite goes to the hand,
overflowing into temp like a prep merge's (temp pieces that arrive after the prep wait through the next prep).
* A merge completed after the prep (SETTLE effects) keeps its reward offer for the next prep; its elite goes where a prep
merge's would — onto the tile of a consumed deployed copy (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区
对应位置"), else to the hand, overflowing into temp (temp pieces that arrive after the prep wait through the next prep).
+8 -3
View File
@@ -55,7 +55,7 @@
- **购买点两次**:第一次点商店卡片只选中(卡片抬起并打开详情,底部出现「确认购买 · 再次点击」),再点这张卡片的**任意位置**(包括金色的确认条)才购买;点别处或按 Esc 取消。升级调度中心、晋升奖励同样两次。不能购买时(例如已准备就绪)点卡片只打开详情。
- **公共卡池**(同盟):每名干员的副本数有限,I–VI 阶分别 12 / 14 / 18 / 16 / 8 / 5 份,所有玩家共用;持有的干员(棋盘 + 整备区)占用副本,商店里展示的不占用。一名干员被大家拿光后就刷不出来了;淘汰玩家的干员会还回卡池。
- **位置上限**:棋盘最多部署 **8** 名;整备区 **10** 格;整备区满时新获得的单位(合成的精锐、奖励、退回的装备等)进入**临时整备区**(整备区前面的 5 格)。临时整备区有单位时**不能准备就绪**,棋盘上会用红色虚线框标出这一排并说明规则:把它们放进整备区或战场、配发或使用,或者出售 / 销毁。休整期中进入临时整备区的单位,若到**本休整期结束**时仍留在那里就会被自动处理(干员返还卡池、装备销毁);作战、结算期间或准备就绪之后才进入的(例如战斗奖励、结算时合成的精锐),会保留到**下一个休整期**结束,你一定有机会处理它们。
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。精锐进入整备区(满了进入临时整备区),原先配发的装备退回整备区。
- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——从上到下、从左到右);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
- **晋级奖励**:每次晋升后获得一次特殊招募:从 3 名 **高一阶**(最高 VI)、**互不相同**的随机干员中**免费**挑 1 名(该阶剩余干员不足 3 名时由低一阶补足)。它不能刷新、不能冻结、本回合结束失效。
- 每回合商店会自动刷新未冻结的格子。
@@ -95,14 +95,17 @@
- **核心盟约**(8 个势力:炎、萨尔贡、维多利亚、谢拉格、拉特兰、阿戈尔、叙拉古、卡西米尔):3 名激活,6 名获得强力的额外效果。
- **附加盟约**(精准、迅捷、灵巧、奥术、坚守、助力、远见、奇迹、投资人、突袭、不屈、调和、协防干员、独行、绝技):多为 2 / 3 名激活;「独行」需要场上**恰好 1 名**,「绝技」按场上的精锐数量计算,没有势力盟约的干员算「协防干员」。
- **炎佑**:场上 6 名以上「炎」干员时召唤炎佑(9 名时两只)。它追踪全场仇恨最高的敌人并悬停在其上方,技能「祛恶之焰」锁定 1 个目标**持续喷火**(最长 20 秒,每秒对目标周围的敌人造成法术伤害并附带灼燃,喷火时不普攻,目标死亡或被沉默时结束);场上暂时没有敌人时停在原地等待。它持有官方的「孤立」:医疗干员不会治疗它,友方的增益、光环和天赋也不会选中它(敌人照常可以攻击它)。
- **层数**:盟约通过各种效果叠加层数,层数越高效果越强(例如「炎」攻击力随层数提升、「远见」每 10 层给资金)。**层数整局保留,但只有盟约激活时才生效**,多数情况下也只有激活的盟约能叠层。
- **层数**:盟约通过各种效果叠加层数,层数越高效果越强(例如「炎」攻击力随层数提升、「远见」每 10 层给资金)。**层数整局保留,但只有盟约激活时才生效**,多数情况下也只有激活的盟约能叠层。每个盟约最多 **999 层**(与官方相同,取自官方客户端的常量):到 999 后再获得的层数直接作废,按层数发放的奖励(如「远见」的资金)也随之停止。
- **加成怎么叠**:盟约、策略、装备、机变卡和特质给的「攻击力 / 防御力 / 生命值 +X%」与技能的「攻击力 +X%」**相加**后再乘到属性上(官方:直接乘算),例如精准 +130%、独行 +60%、一件 +40% 的装备合计 +230%,而不是 ×2.3×1.6×1.4。「伤害提升至 X%」「受到的伤害 +X%」(维多利亚、谢拉格、奥术、脆弱等)这类伤害倍率之间仍然相乘;但同名效果只算一个——同一战场两名玩家都开了「奥术」时,敌人身上只挂一层奥术易伤,取两人中较高的那一个(不再相乘);两名「灵知」的坚冰、两名「莱恩哈特」/「缄默德克萨斯」的法抗削减也一样只算最强的一个。「坚守」的反伤计入被打的那名成员(个人对领袖伤害统计里能看到)。
- 每局会随机**禁用**若干盟约(标准模拟较少),它们的干员不会出现在商店;开局的「确认本局信息」会列出。
- 两件同名装备会合成一件更强的装备;特定的装备组合可以为干员赋予额外的盟约效果。
## 7. 最终攻势与隐秘核心
- **最终攻势**(第 14 回合;独立标准为第 9 回合):按座位两两配对,每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**,看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);从开战第 150 秒起(实际秒数)全队每秒扣 1 点 LP。领袖血量归零即胜利(血条显示 0 时领袖已倒下,战斗立即结束),LP 归零即失败——以先归零的一方为准,LP 耗尽后打出的伤害不再计入。此阶段盟约不叠层。
- **最终攻势**(第 14 回合;独立标准为第 9 回合):按座位两两配对,每对共享一个战场,一人守左半边、一人守右半边(右侧玩家的棋盘左右镜像)。所有人**共同削减同一条领袖血条**(血量 = 该难度的官方领袖生命值,取自当前版本的游戏数据,例如终极「假想敌:胄」3 600 000,不论存活几人),看不到另一组的战场。全队 LP 合并成一个池,不再有每回合 10 点上限。顶部的 120 秒倒计时结束后战斗**继续**(不会超时判负);从开战第 150 秒起(实际秒数)全队每秒扣 1 点 LP。领袖血量归零即胜利(血条显示 0 时领袖已倒下,战斗立即结束),LP 归零即失败——以先归零的一方为准,LP 耗尽后打出的伤害不再计入。此阶段盟约不叠层。
- **限伤**(最终攻势与隐秘核心,与官方相同):对领袖的**单次伤害达到 300000 或以上时整次作废**——这一下造成 0 点伤害,不计入领袖血条,也不显示伤害数字;不是截到 300000,而是一点都不算(299999 照常生效)。只针对领袖本体(含两人战场里的另一个领袖),领袖的部件、护卫、无人机和其他小怪不受影响,普通回合和联防也不受影响;部件把伤害等量转给领袖、无人机死亡令领袖失去 2% 生命时,这一下也按一次伤害检查(目前最多 14.4 万,不会触线)。层数极高时领袖仍可能几秒内倒下(例如每名玩家的两个盟约都在 999 层,或所有激活的盟约都在 999 层),这来自官方按层数计算、没有上限的伤害倍率(谢拉格、奥术、精准等),官方社区也有「boss一秒死」的说法。
- 最终攻势 / 隐秘核心的休整期按你在**关底战场上的半场**判定可部署地块(右侧玩家左右镜像),拖动时高亮的就是可放置的格子:例如「源石流发生装置」关底图中被 1×3 橙色管子围起的地块可以部署。在最终攻势放在这些格子上的干员进入隐秘核心时会留在原位。
- **领袖的部件**:「假想敌:胄」的无人机死亡时(不论是谁击落;漏过去不算),胄失去领袖血条总量的 2%(例如终极 3 600 000 → 每架 72 000;官方没写明这个「最大生命值」指哪一个,本作按血条总量处理,属推断);隐秘核心的「斩胄之剑 / 破胄之锤」「碎铳之簧」受到的每一点伤害都会等量扣在领袖血条上(剑 / 锤被击落后一定算;俯冲途中挨的打也算,这一点官方两处文字不一致,属推断)。剑 / 锤平时悬浮在胄身边、无敌;掷出后闪现到固定路线,冲向攻击力最低的干员,途中被打中 15 次就会【瘫痪】(晕眩 10 秒、落地、受到的伤害 ×1.3);落地或瘫痪结束后,会以剩余生命值换成一把新的回到原位。「灭顶之灾」每次只发射 1 枚刺胄之弹(低于 20% 生命也一样),只打攻击范围内攻击力最高的干员。
- **巨型领袖**(假想敌:胄 / 管、盐风主教昆图斯、阿利斯泰尔、“萨米的意志”)的受击判定是一片约 5×3 格的区域(整个管道区块):攻击范围碰到其中任意一格就能打到它;它们**自缚、无法被阻挡**,不会离开原地。溅射按敌人的中心判定,打在旁边小怪身上的溅射碰不到巨型领袖。
- **隐秘核心**(第 15 回合,仅险境及以上):打倒领袖后,若全队激活的盟约层数总和 **> 1200**(独立 > 350)且 LP > 1,进入隐秘核心,挑战更强的领袖。失败不影响本局的通关。
@@ -126,3 +129,5 @@
- LP 只剩 10 左右时(一回合最多扣 10)要优先保完美作战。
- 联防只让完美作战的队友上场,所以稳定清怪的阵容对全队都有价值。
- 进入隐秘核心看的是全队的**激活层数总和**:后期可以针对性地堆层数。
- 层数到 999 就满了,多余的叠层没有用,可以把资源转去别的盟约。
- 领袖战注意**限伤**:单次 30 万以上的伤害会被整次作废。盟约叠到 999 层后,再叠加额外的伤害倍率(例如 999 层谢拉格再点亮奥术)反而可能让大招打出 0 伤害;队友的奥术等减益也会放大你的伤害。
+50 -7
View File
@@ -91,6 +91,9 @@ release hooks/timers of units removed this tick (§1.4) → `time += TICK` → e
`rowOffset` on the player overrides the boss row mapping. Unit input `dir` ∈ `'UP'|'RIGHT'|'DOWN'|'LEFT'` (the board
piece's facing, research 09 §1.2; absent/junk ⇒ RIGHT; with `abs` / `coords:'field'` it is a field direction as given).
A merge's elite is an ordinary board piece to the sim: when the merge consumed a deployed copy it stands on that copy's
tile with its facing (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"; `PlayerState._mergeChess`), with
its loadout's `skillIndex` / `moduleId` and no equipment (the copies' items went back to the hand).
Token pieces: `{ kind:'token', tokenId, ownerUid, row, col, dir? }` — the manually deployable summons the player placed
(data `placeable`, PRTS 卫戍协议/帮助 §战斗部署; user playtest #6). A piece marks the tile its summon deploys on: a talent
summon the owner holds from the start (狼群, 海嗣, 流形, 凯瑟琳's 爬行号·防护单元 — facing the operator it shields) deploys
@@ -139,6 +142,12 @@ and research routes (`{m, s, e, cp:[['MOVE',r,c]…]}`). `spawnsFromTemplate(wav
template into `{ routes, spawns, maxPlayTime, overrides, extraRoutes }` (non-spawn `action` entries are skipped; `unharmful`
and `tag:'part'` spawns don't count in `total`).
Leader parts (`tag:'part'`) pass damage to their leader with `loseHp(leader, share, { source, from, sourceless: true })`
(无来源, credited to the attacker's `bossDamage`): `PART_TRANSFER` 1 for 斩胄之剑 / 破胄之锤 (`BLADE_TRANSFER`, the same
constant) and 碎铳之簧 (PRTS "受到伤害时令假想敌:胄/铳受到等量的无来源生命流失"; DESIGN §20.10, §20.13). Content may replace a part mid-battle: every 剑/锤 sortie (content/bosses.js `kitBlade`)
ends by spawning a new 初始模式 copy on its level branch route (`left_hand_origin` / `right_hand_origin`) with the old
HP, then `kill(old, null)` — uncounted, no bounty; the client sees a `die` and a `spawn`.
WALK legs pathfind on the stage grid inside the rect with the official flow field (grid.js: 4-direction SPFA from the
destination, crates cost 1000, then Bresenham line-of-sight smoothing — research 08 §3.4). Among equal-length routes the
one crossing the fewest non-blockable tiles (floor / gate lanes) wins, and a smoothed segment never cuts across floor
@@ -242,6 +251,8 @@ recorded for the player whose half contains the goal it reached, with `sourcePla
- `leaked[].counted = false` for `notCountInTotal`/`unharmful`/boss parts (LP rules: normal rounds count only `counted`
leaks, cap 10; boss rounds use `lpr`). `perfect = no counted leak`.
- `battleEnd {result}` fires before the result is frozen: handlers may still call `addLayers`/`addCoins`.
- `layerGains` holds what `addLayers` actually added — never more than a bond's room under `BOND_LAYER_CAP` (999) from
its starting layers; `bossDamage` / `damageDealt` never include a leader hit cancelled by 限伤 (§4).
### 1.4 Robustness
@@ -325,6 +336,19 @@ Aggregation: `ATK/DEF/maxHp = (base + Σflat) × (1 + Σpct) × Πmul`; `res = c
`aspd = clamp(base + Σaspd, 20, 600)` (floor 20: PRTS 数值范围 ATTACK_SPEED 默认下限; user playtest #6); `interval = bat × (1 + ΣbatPct) × 100 / aspd`; `moveSpeed = (base + ΣmoveFlat) × ΠmoveMul`;
tiles/s = `moveSpeed × MOVE_SCALE (0.5)`. A maxHp change keeps the HP ratio. Elite stats (module included) come from data.
**Which bucket (PRTS 游戏数据基础 属性基本公式 / 作战机制, DESIGN §20.10).** `Σpct` is the official **直接乘算** class — its
values are summed (`A = (A₀ + D_p)(1 + D_t)`, D_t = t₁ + … + tₙ): a skill's or talent's "攻击力+X%" **and** every "+X%"
ATK / DEF / max HP bonus of the 卫戍 systems — 盟约, 策略 (bands), 装备, 机变 cards, the per-layer 特质 (PRTS
卫戍协议:盟约 下半/PRTS盟约记录 "盟约效果,策略效果,装备效果提供的属性加成均为直接乘算"). Content builds those with
`content/support directMods({ atk, def, hp })` (constants.js `DIRECT_BONUS_STACKING` 'add'; 'multiply' = the v2.5
per-source ×(1 + x), which compounded with layers: user report after playtest #6). `Πmul` is for 最终乘算 / "提升至X%"
effects (炎佑 ×1.5 at 9 炎, 虚弱, 停顿 …) and the char_attribute_mul 特质 ("攻击力和生命值+20%", a rune on the base
attributes). Damage multipliers (`dmgDealtMul`, "伤害提升至X%"; `*TakenMul`, 脆弱 / "受到的伤害+X%") multiply each other
(PRTS 游戏数据基础 "同种倍率间叠乘"), same-named statuses keep the strongest — catalogue statuses (§3) and the content
effects routed through `battle.applyStrongest` (§3: 奥术, 灵知 坚冰, 莱恩哈特 / 缄默德克萨斯 RES cuts), also across the two
players of a pair field or two copies of one operator. A content buff keyed per unit (`key:${unit.id}`) still stacks
per source — DoTs and slows do that on purpose; a damage-taken / DEF / RES modifier on enemies should not.
---
## 3. Buffs, mods, statuses
@@ -361,6 +385,11 @@ and the 浮空 weight rule, then `statusApplied { source, target, status, durati
(`gamedata_const.termDescriptionDict`, `ba.*`). Same-key statuses refresh to the longer duration, except the
"同名效果取最高" ones marked *strongest* below: the strongest value wins, a weaker application never overrides it and,
if it outlasts it, resumes when the strong one expires (pass `refresh` to opt out).
`battle.applyStrongest(target, key, { duration, value, mods: (v) => mods, source })` gives a content effect that is not a
catalogue status the same rule (one invisible buff `key` per target whatever applies it — no immunity, 抵抗, status hooks
or icon): the engine default for two same-named buffs (PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"). 奥术 uses
it, so the two players of a pair field compete for one instance instead of multiplying, and so do 灵知's 坚冰 and the
莱恩哈特 / 缄默德克萨斯 RES cuts, once keyed per unit (DESIGN §20.10). "Strongest" = the largest |value|.
| key | effect | value |
|---|---|---|
@@ -444,7 +473,9 @@ HP left is refused (damage.js `hasHp`: a lethal hit's `damaged` hook runs before
element?, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, mul=1, canDodge (phys/arts), isSkill, isSplash,
isAttack, attackId, ignoreSleep, sourceless, tags[], cancel }` (`sourceless`: 无来源 damage — the source's stats add
nothing and the hooks get `source: null` plus `credit` = the source, which keeps the stats and the kill; a `loseHp` whose
`from` is 无来源 is 无来源 too) (`battle.makeDamage(d)` normalises). `attackId` is the same for every
`from` is 无来源 is 无来源 too; element bursts, leader-part transfers and 坚守 thorns use it — content damage that has a
responsible unit should pass it as `source` with `sourceless: true` rather than `source: null`, which credits nobody)
(`battle.makeDamage(d)` normalises). `attackId` is the same for every
damage instance of one normal attack (all targets, splash, chain, projectile impacts; 0 for non-attack damage) — use it
for "本次攻击" procs that must roll once per attack. **Dodge** from several buffs rolls independently: the unit's
`s.dodgePhys` = 1 − Π(1 − pᵢ) (a single source keeps its exact value).
@@ -452,14 +483,24 @@ Order: invulnerable / asleep? → **`hit`** (mutate `dmg`, set `dmg.cancel`) →
`max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 5 %A)`, arts `max(A×(1 − R′/100), 5 %A)`, elemental
`max(A×(1 − 元素抗性/100), 5 %A)`, true = A; source ignore mods are added) → × source `dmgDealtMul` (× phys/artsDealtMul)
× target `dmgTakenMul` (not for elemental) × type-taken mul × `dmg.mul` (a `sourceless` hit skips every source term) →
shields → HP loss
**限伤** (`leaderHitCancelled`: on a leader — `isBoss`, the tag-'boss' units: data/bosses.json `enemyKey`, the official
`IsBossEnemy` list, and their mirrored copies; never parts, escorts, drones — in a `'boss'` / `'hidden'` battle, a hit
with `ceil(final) ≥ BOSS_HIT_LIMIT` (300000, shared/constants.js) is cancelled whole: returns 0 before shields (阿利斯泰尔's
`boss:vest` barrier stays untouched; a `hit`-step block such as 假想敌:再生's aura acts earlier) [ASSUMED order], no HP /
pool loss, no credit or stats, no `dmg` event, no `damaged` / `fatal` / kill; an fx `hitCap` `{ id, n }` marks it and
draws nothing; research 11) → shields → HP loss
(boss units: routed to `sharedBoss.damage(playerId, amount)`; a pool left under 1 HP is emptied) → if HP ≤ 0: **`fatal`** (`ctx.prevented = true` keeps the
unit at ≥ 1 HP) → **`damaged`** → SP-on-hurt / TAKE_DAMAGE → `kill` + `death`.
`battle.heal(source, target, amount, { overheal=false, self, silent })`: no-op on `noHeal` targets (unless self — 禁疗 /
孤立 summons carry the flag, §3);
× source `healingDealtMul` × target `healingTakenMul`; **`heal`** hook (mutable amount); capped at max HP; `overheal`
turns the excess into an `overheal` shield. `battle.loseHp(target, amount, {source})` = HP loss ignoring DEF/RES/shields/dodge (流失).
turns the excess into an `overheal` shield. `battle.loseHp(target, amount, { source, from, tags, silent, sourceless })` = HP
loss ignoring DEF/RES/shields/dodge (流失); `sourceless: true` makes it 无来源 ("受到等量的无来源生命流失": hooks see no source,
`source` keeps the credit — stats and the per-player shared-pool tally), as does a 无来源 `from`. On a leader in a boss /
hidden battle a loss of ≥ `BOSS_HIT_LIMIT` (a part's 传递, a drone's death) is cancelled like a hit. Every HP-damage kind
meets the limit (phys / arts / true / 元素伤害 incl. element bursts, DoT ticks); element 损伤 (the gauge, `type: 'element'`)
removes no HP and never does.
---
@@ -492,7 +533,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
| `enemySpawn` / `enemyLeak` | `{ enemy }` | |
| `elementBurst` | `{ source, target, element }` | before the burst's lock/effects; same-element fills of `target` are already refused |
| `dodge` | `{ source, target, dmg }` | an attack was dodged |
| `layerGain` | `{ playerId, bondId, n, reason, source, tile }` | mutable `n` before recording (魔王 +1 …); `tile` = `[r, c]` where `source` stands — or was knocked out this very instant ("被击倒时" gains) — else null (`addLayers` opts.tile overrides) |
| `layerGain` | `{ playerId, bondId, n, reason, source, tile }` | mutable `n` before recording (魔王 +1 …), then clamped to the room left under `BOND_LAYER_CAP` (999); not emitted for a bond already at the cap; `tile` = `[r, c]` where `source` stands — or was knocked out this very instant ("被击倒时" gains) — else null (`addLayers` opts.tile overrides) |
| `merchantPay` | `{ unit, cost, cancel }` | a merchant (行商) is about to pay its periodic DP; change `cost` or set `cancel` |
| `battleEnd` | `{ result }` | may still add layer gains / coins |
@@ -509,8 +550,9 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
| helper | notes |
|---|---|
| `dealDamage(src, tgt, dmg)`, `heal(src, tgt, amount, opts)`, `loseHp(tgt, amount, {source})` | §4 |
| `dealDamage(src, tgt, dmg)`, `heal(src, tgt, amount, opts)`, `loseHp(tgt, amount, {source, from, tags, sourceless})` | §4 |
| `applyStatus(tgt, key, {duration, source, value, force, point})`, `removeStatus(tgt, key)`, `resistOf(unit)` | §3 |
| `applyStrongest(tgt, key, {duration, value, mods, source})` | §3 — "同名效果取最高" for a non-catalogue effect |
| `addBuff(unit, buff)`, `removeBuff(unit, key)` | §3 |
| `spawnToken(ownerUnit | playerId, tokenId, row, col, { def, stats, hp, duration, untargetable, dir, kit, force, anySource })` | field tiles; def from data/tokens.json `variants[ownerChessId]` for the owner unit's selected skill / module (`tokenDef`); `dir` defaults to the owner unit's (else the player's: RIGHT, mirrored side LEFT; a legacy `facing` ±1 is still read); returns the token or null (tile busy; or the owner runs a **non-default** skill that does not produce the token — `producesToken` — unless `anySource`: kit install hooks written for the default skill run under every skill). `spawnDevice(key, row, col, { …, dir })` likewise |
| `tokenDef(tokenId, ownerUnit | chessId)`, `producesToken(ownerUnit, tokenId)` | the token def a summon of that owner gets — `getToken(id, owner.defId, owner.def.loadout)`, exact even when two players of one field give the same chess different loadouts (prefer it over an id-only `battle.data.getToken(id, unit.defId)` for summon stats / blackboards); whether the owner's loadout makes the token (DATA.md §14 `sources` has 'skill' or 'talent'; true when the data does not tell: no own variant, player-owned summons) |
@@ -522,7 +564,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
| `unitsInGrid(unit, grid, {side, extend})`, `alliesInGrid(unit)`, `enemiesInRadius(x, y, r, centre?)`, `alliesInRadius(x, y, r, ownerId?)` | grid offsets are relative to facing RIGHT, rotated by `unit.dir`; enemies by their body (§2 hit areas: a huge enemy on every tile it occupies / within `r` of its rectangle; `centre` = splash around a target, a 中点判定 by position) |
| `enemiesInKeys(keys, attacker, profile)`, `blockedTargets(unit, profile)` | targetable enemies whose body is on the tiles (a huge one listed once); the enemies a unit blocks — always selectable by it, a ranged operator on a melee tile included (§1.2 Blocking) |
| `allies(ownerId?)`, `aliveEnemies()`, `unitAt(r, c)`, `unitById(id)`, `tileInfo(r, c)`, `lowestHpAllyInRange(unit)` | |
| `addLayers(playerId, bondId, n, reason, {source})`, `addCoins(playerId, n)` | layers are a no-op when `flags.layerGainsEnabled` is false (unite/boss) |
| `addLayers(playerId, bondId, n, reason, {source})`, `addCoins(playerId, n)` | layers are a no-op when `flags.layerGainsEnabled` is false (unite/boss); a gain adds at most the room left under `BOND_LAYER_CAP` (999, shared/constants.js `layerGainRoom`: the client's `AddBondCount` min(L + n, 999)) on the live copy — or, without one, on the battle's own gains — and returns what it added (0 at the cap: no hook, no event) |
| `getPlayer(playerId)` | `{ playerId, seat, side, colOffset, mirror, dir (default unit direction: RIGHT, mirrored side LEFT), facing (its sign), bonds (live copy, layers updated by addLayers), bandId, playerEffects, lpForBoss, dp, units }` |
| `mapTile(ps, row, col, abs?)` / `mapDir(ps, dir, abs?)` | board → field tile / direction of a player (the FA right-side mirror) |
| `addDp(playerId, n)`, `retreat(unit, {reason, permanent})`, `relocate(unit, r, c)` | |
@@ -913,7 +955,8 @@ Unknown subprofessions fall back to the profession default (test `professions.te
renderer flies it back to the thrower at `BOOMERANG_RETURN_SPEED`; an enemy's `profile.shot` may name another kind,
e.g. `mortar` for 帝国炮火先兆者, which the renderer does not draw — its fx `bombardShell` is the shell), `['dmg', tgt, amount, type]` (`phys|arts|true|burn|neural|necrosis|apoptosis`),
`['heal', tgt, amount]`, `['skill', id, 1|0]`, `['die', id, reason]`, `['leak', id]`, `['status', id, key, 1|0]`,
`['fx', kind, x, y, extra]`, `['layer', playerId, bondId, n]`, `['bounty', playerId, coins]`.
`['fx', kind, x, y, extra]` (`hitCap` `{ id, n }`: a leader's hit cancelled by 限伤 — the renderer draws nothing),
`['layer', playerId, bondId, n]` (n = the layers actually added, capped at 999), `['bounty', playerId, coins]`.
- `UnitInfo = { id, kind, side, ownerId, defId, name, tier, golden, spine, avatar, x, y, facing, dir, maxHp, motion?, boss?, uid?, skillIndex? }` (`skillIndex`: an ally's equipped skill, DESIGN §16)
(`dir` = the unit direction, allies meaningful, enemies 'RIGHT'; `facing` = its horizontal sign for sprite flipping)
(`spine`/`avatar` are asset ids from data).
+4 -2
View File
@@ -17,6 +17,7 @@ Tags: **[DATA]** client game data · **[VERIFIED]** official or wiki rule text,
| `05-enemies-levels.md` / `05-enemies.json` / `05-maps.json` | Stage terrains (8 active), 38 wave templates with routes, 326 enemies, special-enemy pools, generation algorithm, bosses, bounties, LP rules. Addendum: DP, hidden-core thresholds, multi FUNNY rounds, stat scaling. | `05-maps.json: stages, roundLevels`; `05-enemies.json: enemies, specialEntries, bosses, bountyEffects, generation, _criticAddendum` |
| `06-multiplayer-ux.md` | Co-op flow and state machine, 联防, shared vs individual, shared pool, cross-player effects, emotes and broadcasts, settlement, per-screen UI layout, style guide, server notes. Addendum: TL;DR fixes, banned-operator UI. | – |
| `07-assets.md` / `07-assets.json` | Verified URL patterns and resolved URLs for avatars, portraits, skills, bond/item/band icons, UI, Spine (ops + enemies), BGM/SFX, fonts; build plan. Addendum: spot check 40/40 OK. | `operators[*]`, `enemies[*]`, `bonds`, `items`, `bands`, `autochessUi`, `audio` |
| `11-limits-official.md` | Official limits read from the client (2.7.71 il2cpp) and the community: bond layers cap at **999** per bond (`MAX_GARRISON_STACK`, `AddBondCount` = min(L + n, 999)); in boss battles outside training a single hit of **≥ 300000** on a leader (`IsBossEnemy` = an `autoChessData.bossInfoDict` enemyId) is **cancelled** ("限伤", `MAX_BATTLE_DAMAGE`); implemented in DESIGN §20.12. | `shared/constants.js BOND_LAYER_CAP / BOSS_HIT_LIMIT` |
## 2. Match structure
@@ -72,7 +73,7 @@ Boss rounds: the battle continues past the timer. Merged team LP drains **1/s af
The visible chess per tier are 16/17/19/22/19/19. The item slot uses the same tier shares, then picks uniformly within the tier [ASSUMED].
**Merge:** 3 copies of the same chess (board or hand) merge into 1 elite (`_b`), sent to the **hand**. 风丸 needs 2. Elites never merge. The equipment of the merged copies returns to the hand. Reward: the shop temporarily shows 3 operators of tier min(shopLv+1, 6) at price **0**; take 1; no refresh or freeze; gone at round end. A purchase that completes a merge is allowed with a full hand. [VERIFIED]
**Merge:** 3 copies of the same chess (board or hand) merge into 1 elite (`_b`), sent to the **hand** — or, when a consumed copy was deployed, to **that copy's board position** (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"; 01 A1 row 7). 风丸 needs 2. Elites never merge. The equipment of the merged copies returns to the hand. Reward: the shop temporarily shows 3 operators of tier min(shopLv+1, 6) at price **0**; take 1; no refresh or freeze; gone at round end. A purchase that completes a merge is allowed with a full hand. [VERIFIED]
## 4. Board, hand, combat
@@ -86,7 +87,7 @@ The visible chess per tier are 16/17/19/22/19/19. The item slot uses the same ti
| Skill auto-cast | Default: SP ready + about to attack/heal + an enemy in the **initial** range. ~~`skillTriggerDataList` rows with `skillIndex` 0 = skill 1 only (TANK S1 → on damage: only 古米 and 灰毫 this season)~~ — Superseded by user playtest #6 (DESIGN §20): the class rows apply to **every MANUAL skill** of the class and never to an AUTO skill (PRTS 卫戍协议/帮助 names whole classes; the 阵法术师 row must cover 薄绿's default S2, a phalanx that never attacks with its skill off); a MANUAL skill with its own 技能范围 (not an attack-range change) uses SKILL_RANGE; automatic operations have a 3 s cooldown. charId rows per 03. | VERIFIED (class-row reading superseded) |
| Speed | 2× forced, no manual control | VERIFIED |
| Board geometry | 19×21 stage. Player board rows 9–12, cols 2–10. Gates (9,10) and (12,10); R1–3 use only the lower one. Goal (9,2). Col 9 is a non-deployable lane. Hand row 7 cols 0–9. Temp hand row 8 cols 4–8. Boss field rows 1–5. | DATA |
| Bond counting | Distinct operators on board. 远见/奇迹/投资人 also count the hand. 绝技 counts elites. Layers persist all match; **no IN_BATTLE layer gains in 联防 or boss rounds**. | DATA/VERIFIED |
| Bond counting | Distinct operators on board. 远见/奇迹/投资人 also count the hand. 绝技 counts elites. Layers persist all match; **no IN_BATTLE layer gains in 联防 or boss rounds**; each bond's layers stop at **999** (research 11). | DATA/VERIFIED |
## 5. LP, 联防, Final Assault, Hidden Core
@@ -98,6 +99,7 @@ The visible chess per tier are 16/17/19/22/19/19. The item slot uses the same ti
| Elimination | LP ≤ 0: out; copies return to the pool | VERIFIED/COMM |
| Final Assault (R14) | LP of all alive players **merged**, no cap. Players in pairs, and an **odd player alone**. Movable bosses spawn one per alive player's side and share one HP pool. Overtime −1 LP/s after 150 s. Wave leaks cost `lifePointReduce`. | VERIFIED |
| Boss HP pool (`bloodPoint`, F/N/H/A) | boss_1 247.5k/675k/1.8M/3.6M · boss_2 225k/400k/800k/3M · boss_3 285k/708.75k/2M/4M · boss_4 307.5k/708.75k/2.1M/4.2M · boss_5 200k/390k/780k/3M · boss_6 285k/705k/1.99M/3.98M · boss_7 277.5k/787.5k/2M/4M. Pick weights 6/6/6/5/10/5/5. | DATA |
| Boss-hit limit (限伤) | Final Assault / Hidden Core: one hit of ≥ 300000 on a leader deals **0** (cancelled, not clamped); minions, normal rounds and 联防 unaffected (research 11). | DATA/COMM |
| Hidden Core (R15, 险境+) | Solo: activated layers **> 350** and LP > 1. Co-op: team sum **> 1200** and merged LP > 1. Bosses boss_8/9/10 (weights 50/40/40; HP N 937.5k/900k/1.0125M, H 3.6M/2.8M/3.8M, A 7.2M/3.95M/7.6M). Failure doesn't affect the clear. | VERIFIED/DATA |
**Enemy scaling (non-boss)**, per mode and round [VERIFIED via PRTS, user-sourced]:
+5 -1
View File
@@ -9560,7 +9560,11 @@
"source": "official intro '多余的装备无法进行出售,可以销毁'"
},
"promotion": {
"eliteGoesTo": "hand",
"eliteGoesTo": "hand; when a consumed copy was deployed, that copy's board position (PRTS 卫戍协议/帮助 '发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)'; user-confirmed after playtest #6)",
"eliteTileAmongSeveralDeployed": {
"rule": "the consumed deployed copy that deploys first (board row desc, then col asc; mirrored on the right boss half), with its facing",
"assumed": true
},
"equipmentReturnsTo": "hand",
"reward": "shop temporarily shows 3 ops of tier min(shopLevel+1,6) at price 0; take 1; no refresh/freeze; expires at round end",
"verified": true
+5 -1
View File
@@ -338,6 +338,10 @@ The 20-fund 绮良 / 55-fund 帕格尼尼 band thresholds count **funds spent**
- WIKI: "三位同名干员(无论是否部署)会自动合并且晋级为精锐干员".
- `upgradeNum`=3 for 132 chess; **风丸 `chess_char_2_11_a` needs 2**.
- Elites have `upgradeNum`=0: **no further merging**.
- **Where the elite goes:** to the hand, **unless a consumed copy was deployed — then to that copy's board position**.
PRTS 卫戍协议/帮助 §干员的获得与精锐化: "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)";
confirmed first-hand by the user after playtest #6 ("合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置"). See A1 row 7
for the remake's choices where the text is silent.
- **Equipment:** anything the merged units carried returns to the bench. Tip: "干员晋级后已配发装备会回收至整备区".
- **Bond traits** of "<获得时>" type: a triple stacks 1+1+2 (the elite value is double) [COMM].
- **Stats (`shopCharChessInfoData`):**
@@ -838,7 +842,7 @@ Local copies are in `scratchpad/gd/extra/critic/` (`bw_盟约.txt`, `bw_sweep.tx
| new | – | At **combat start, all unfrozen shop contents are cleared** ("进入作战期后,栏上若未处于冻结状态则清空全部物资"). | VERIFIED |
| 4.2 / 8 | bench 10 + temp area | **Hand (手牌区/整备区) = 10 regular + 5 temporary slots**, shared by operators, **items** and hand-deployable **summons**. Several summons of one owner stack into 1 slot; summons of different owners don't stack.<br>When the regular hand is full, every action that would add a card is refused: buy, and withdraw from the board. Exception: a purchase that completes a 3-merge nets −1 card, so allow it.<br>Passive gains (merge result, band/trait/item grants, items returned by promotion) overflow into the temp slots. **While temp is non-empty the player cannot start combat.** Temp is wiped when the next phase starts.<br>A board↔hand swap is allowed even with a full hand. Cards fill hand slots **right→left**. | VERIFIED |
| 5.2 | – | **Items cannot be sold** ("多余的装备无法进行出售,可以销毁"). They can only be destroyed for 0 funds. Operators sell for 1 from board or hand. | VERIFIED |
| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:<br>1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.<br>2. **1 elite goes to the hand, not to a board tile.** Their equipment returns to the hand.<br>3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.<br>4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED |
| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:<br>1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.<br>2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (top→bottom, then left→right — the official deploy order) with its facing; a deployed operator transformed into the completing copy (突变细胞) counts as deployed; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows.<br>3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.<br>4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
| 2 | BAND_CHECK "3 random + reroll" [ASSUMED] | **All unlocked bands are shown.** In co-op, players pick **one at a time in a random order** shown on the left ("以随机顺序进行策略轮选"). Each co-op player may **skip once**. The step lasts 50 s; on timeout the band is 华法琳. **Duplicates are allowed:** the official illustration shows 2 teammates' picks still un-greyed in the grid. [ASSUMED] 12 s per turn inside the 50 s step. | VERIFIED except the per-turn timer |
| 2 | banned bonds "1 core + 1 add-on" [ASSUMED] | Replaced by the verified rule in **A2**. | VERIFIED |
| 1, 12 | one ×0.7/×0.8 multiplier per mode; `aceffect_enemy_*` mapping [ASSUMED] | Replaced by the **per-round table in A3**. | VERIFIED (PRTS, user-sourced) |
+4 -4
View File
@@ -15,7 +15,7 @@
"Each player has one integer layer counter L per bond (23 counters). All start at 0 when the simulation starts, are never reset between rounds and are never reduced (no layer-loss effect exists in the data) [ASSUMED: never reduced].",
"L is kept while the bond is inactive, but bond effects only apply while the bond is active (PRTS 盟约记录).",
"Wording contract used by every source: \"使已激活的【X】层数+N\" -> add only if bond X is active at the moment of the trigger; \"(无需激活盟约)\" -> add even if X is inactive. \"自身所属盟约\" = every bond of that operator; \"自身已激活的盟约\" = only its bonds that are active.",
"No global layer cap. Caps are per source: max_add_count_per_battle (layers a single garrison instance may add in one battle), max_layer (per round, e.g. 安洁莉娜 12/round).",
"No layer cap in the data. Caps are per source: max_add_count_per_battle (layers a single garrison instance may add in one battle), max_layer (per round, e.g. 安洁莉娜 12/round). The official cap is in the client, not the data: AutoChessBattleConst.MAX_GARRISON_STACK = 999, AddBondCount stores min(L + n, 999) per bond (research 11 §1). Since 2026-10-01 (DESIGN §20.12) the remake stops each bond at 999 (shared/constants.js BOND_LAYER_CAP, layerGainRoom); the community agrees (巴哈姆特 12534 \"每把都能999层\", \"沒999層的盟約情況下\"; 12316 \"999謝\").",
"Layers gained in battle persist after the battle and immediately affect formulas that read L [ASSUMED live update: recompute bond buffs whenever L changes; the reference game uses env_gbuff that reads current stacks].",
"IN_BATTLE layer gains are disabled while fighting for a teammate in the 联防 (Unite) phase and in the leader/boss round (巴哈姆特 snA=12294; arknights.wiki.gg: \"Players cannot gain stacks via Combat-related Operator Attributes when assisting other players in the Unite Phase\").",
"魔王 (garrison_59, emptyShip): whenever the operator on the tile in front of 魔王 raises layers through its own trait during battle, that raise gets +1 extra layer (per trigger).",
@@ -36,7 +36,7 @@
"DIY (自选) chess slots chess_char_5_diy1/2 and chess_char_6_diy1/2 have bondIds [] and are filled with a 6-star operator the player owns (diyChessDict = TIER_6). Their bond is derived from the chosen operator's nationId/groupId/teamId matched against autoChessData.bondInfoDict[*].powerIdList (e.g. yan: yan,sui,lungmen,lee,lgd; victoria: victoria,glasgow; kjerag: kjerag,karlan; egir: egir,abyssal; siracusa: siracusa,chiave; kazimierz: kazimierz,pinus; sargon; laterano). No match -> constData.fallbackBondId = emptyShip (协防干员). (bwiki: operators from those nations get the nation alliance, \"others receive the 协防 designation\".)",
"Prototype substitution: a NORMAL chess whose operator the player does not own is fielded as its backupCharId (预备干员 / 原型干员) but keeps the chess's bonds and traits (PRTS: \"烛煌(或对应的原型干员)\").",
"Mode filter: 标准模拟 (mode_single_funny / mode_multi_funny) enables only 13 bonds (炎 萨尔贡 维多利亚 谢拉格 叙拉古 精准 迅捷 坚守 助力 远见 不屈 调和 协防干员); 拉特兰 阿戈尔 卡西米尔 灵巧 奥术 奇迹 投资人 突袭 独行 绝技 are inactive there. Training/险境/绝境/终极 enable all 23.",
"Stat math: attribute bonuses from 盟约, strategies and equipment are \"直接乘算\" - each is a separate multiplier on the operator's already-final stat, independent of the operator's own talents/skills (PRTS 盟约记录). E.g. 炎 +50% ATK and an item +30% ATK -> final = own_final_atk x 1.5 x 1.3.",
"Stat math: attribute bonuses from 盟约, strategies and equipment are \"直接乘算\" (PRTS 盟约记录 \"盟约效果,策略效果,装备效果提供的属性加成均为直接乘算,与自持有干员的属性加成独立\"). Corrected 2026-10-01 (DESIGN §20.10): 直接乘算 is PRTS's modifier class whose values are SUMMED with each other and with the skills' \"+X%\" (游戏数据基础: D_t = t1 + ... + tn, A = (A0 + D_p)(1 + D_t)); \"与自持有干员的属性加成独立\" = separate from the 自持有加成 table (an owned operator +5-10% ATK / DEF / max HP by its real promotion; not modelled). E.g. 炎 +50% ATK and an item +30% ATK -> final = base_atk x (1 + 0.5 + 0.3), not x 1.5 x 1.3 (the reading used until v2.5).",
"Core bonds (bondType SEASON, isPower) = 炎 萨尔贡 维多利亚 谢拉格 拉特兰 阿戈尔 叙拉古 卡西米尔 (8). The other 15 are add-on (REGULAR). bondOrder: 调和 = 1 (listed first in UI), others 2.",
"weight (10 for most, 0 for 投资人/调和/协防干员/绝技): weight used when an effect picks a random bond (e.g. SERVER_ADD_BOND_ACTIVATED_RANDOM, random bond chess) [ASSUMED meaning]."
],
@@ -5721,7 +5721,7 @@
"tiers": [
{
"need": "2 distinct",
"effect": "When an 奥术 member deals magic damage, the target takes magic damage x(1.20 + 0.01*L) (i.e. +(20+L)%) for 3 s. One instance per (source unit, target); reapplying refreshes."
"effect": "When an 奥术 member deals magic damage, the target takes magic damage x(1.20 + 0.01*L) (i.e. +(20+L)%) for 3 s. ONE instance per target whatever applies it: the strongest value wins, a weaker one resumes if it outlasts it; reapplying refreshes (corrected 2026-10-01: PRTS 作战机制 同名buff默认只表现出一个; 巴哈姆特 12316 the two players of a pair field compete for it)."
},
{
"need": "3 distinct",
@@ -5736,7 +5736,7 @@
},
"layerGain": "夕 +2 per operator gained; 阿罗玛 +2 per refresh (<=6); 溯光星源 +2 per 3 gold spent; 海霓 first kill +3; 获得时: 深靛 +2, 协律 +4 (hidden). 机变 \"莫斯提马的盟誓\" +10.",
"assumed": [
"instances from different 奥术 sources stack multiplicatively"
"one instance per target, the strongest (the engine's default same-name rule; the two players of a pair field compete for it) — until v2.5 the remake kept one per player and they multiplied"
]
},
"members": [
+4 -4
View File
@@ -51,7 +51,7 @@ Core (isPower, bondType SEASON): 炎 萨尔贡 维多利亚 谢拉格 拉特兰
- DIY (自选) chess slots chess_char_5_diy1/2 and chess_char_6_diy1/2 have bondIds [] and are filled with a 6-star operator the player owns (diyChessDict = TIER_6). Their bond is derived from the chosen operator's nationId/groupId/teamId matched against autoChessData.bondInfoDict[*].powerIdList (e.g. yan: yan,sui,lungmen,lee,lgd; victoria: victoria,glasgow; kjerag: kjerag,karlan; egir: egir,abyssal; siracusa: siracusa,chiave; kazimierz: kazimierz,pinus; sargon; laterano). No match -> constData.fallbackBondId = emptyShip (协防干员). (bwiki: operators from those nations get the nation alliance, "others receive the 协防 designation".)
- Prototype substitution: a NORMAL chess whose operator the player does not own is fielded as its backupCharId (预备干员 / 原型干员) but keeps the chess's bonds and traits (PRTS: "烛煌(或对应的原型干员)").
- Mode filter: 标准模拟 (mode_single_funny / mode_multi_funny) enables only 13 bonds (炎 萨尔贡 维多利亚 谢拉格 叙拉古 精准 迅捷 坚守 助力 远见 不屈 调和 协防干员); 拉特兰 阿戈尔 卡西米尔 灵巧 奥术 奇迹 投资人 突袭 独行 绝技 are inactive there. Training/险境/绝境/终极 enable all 23.
- Stat math: attribute bonuses from 盟约, strategies and equipment are "直接乘算" - each is a separate multiplier on the operator's already-final stat, independent of the operator's own talents/skills (PRTS 盟约记录). E.g. 炎 +50% ATK and an item +30% ATK -> final = own_final_atk x 1.5 x 1.3.
- Stat math: attribute bonuses from 盟约, strategies and equipment are "直接乘算" (PRTS 盟约记录 "盟约效果,策略效果,装备效果提供的属性加成均为直接乘算,与自持有干员的属性加成独立"). **Corrected 2026-10-01 (DESIGN §20.10):** 直接乘算 is PRTS's technical modifier class (游戏数据基础 属性基本公式: "直接乘算结果 D_t = t₁ + t₂ + … + tₙ", A = (A₀ + D_p)(1 + D_t); 作战机制: X₂ = X₁(1 + b₁% + b₂%), a skill's "攻击力+180%" being the example) — the values are SUMMED with each other and with the skills' "+X%"; "与自持有干员的属性加成独立" means they are separate from the **自持有加成** (PRTS 卫戍协议:盟约 下半, its own table: an operator the player really owns gets +5–10 % ATK / DEF / max HP by its real promotion — 精英1 Lv.1 +5/+5/–, 精英2 Lv.1 +10/+5/+5, 精英2 Lv.60 +10/+10/+10, the highest met applies), which the remake does not model. E.g. 炎 +50% ATK and an item +30% ATK -> final = base_atk x (1 + 0.5 + 0.3) (+ any skill %), not x 1.5 x 1.3 (the reading used until v2.5).
- Core bonds (bondType SEASON, isPower) = 炎 萨尔贡 维多利亚 谢拉格 拉特兰 阿戈尔 叙拉古 卡西米尔 (8). The other 15 are add-on (REGULAR). bondOrder: 调和 = 1 (listed first in UI), others 2.
- weight (10 for most, 0 for 投资人/调和/协防干员/绝技): weight used when an effect picks a random bond (e.g. SERVER_ADD_BOND_ACTIVATED_RANDOM, random bond chess) [ASSUMED meaning].
@@ -60,7 +60,7 @@ Core (isPower, bondType SEASON): 炎 萨尔贡 维多利亚 谢拉格 拉特兰
- Each player has one integer layer counter L per bond (23 counters). All start at 0 when the simulation starts, are never reset between rounds and are never reduced (no layer-loss effect exists in the data) [ASSUMED: never reduced].
- L is kept while the bond is inactive, but bond effects only apply while the bond is active (PRTS 盟约记录).
- Wording contract used by every source: "使已激活的【X】层数+N" -> add only if bond X is active at the moment of the trigger; "(无需激活盟约)" -> add even if X is inactive. "自身所属盟约" = every bond of that operator; "自身已激活的盟约" = only its bonds that are active.
- No global layer cap. Caps are per source: max_add_count_per_battle (layers a single garrison instance may add in one battle), max_layer (per round, e.g. 安洁莉娜 12/round).
- No layer cap in the data. Caps are per source: max_add_count_per_battle (layers a single garrison instance may add in one battle), max_layer (per round, e.g. 安洁莉娜 12/round). The official cap is in the client, not the data: `AutoChessBattleConst.MAX_GARRISON_STACK = 999`, `AddBondCount` stores `min(L + n, 999)` per bond (research 11 §1). Since 2026-10-01 (DESIGN §20.12) the remake stops each bond at 999 (`shared/constants.js BOND_LAYER_CAP`, `layerGainRoom`); the community agrees (巴哈姆特 12534 "每把都能999层", "沒999層的盟約情況下"; 12316 "999謝").
- Layers gained in battle persist after the battle and immediately affect formulas that read L [ASSUMED live update: recompute bond buffs whenever L changes; the reference game uses env_gbuff that reads current stacks].
- IN_BATTLE layer gains are disabled while fighting for a teammate in the 联防 (Unite) phase and in the leader/boss round (巴哈姆特 snA=12294; arknights.wiki.gg: "Players cannot gain stacks via Combat-related Operator Attributes when assisting other players in the Unite Phase").
- 魔王 (garrison_59, emptyShip): whenever the operator on the tile in front of 魔王 raises layers through its own trait during battle, that raise gets +1 extra layer (per trigger).
@@ -655,11 +655,11 @@ Members (12 chess, 11 in current shop pool; by tier in shop: {'1': 1, '2': 2, '3
**Implementable spec**
- **[2 distinct]** When an 奥术 member deals magic damage, the target takes magic damage x(1.20 + 0.01*L) (i.e. +(20+L)%) for 3 s. One instance per (source unit, target); reapplying refreshes.
- **[2 distinct]** When an 奥术 member deals magic damage, the target takes magic damage x(1.20 + 0.01*L) (i.e. +(20+L)%) for 3 s. ONE instance per target whatever applies it — the strongest value wins, a weaker one resumes if it outlasts it; reapplying refreshes (corrected 2026-10-01, DESIGN §20.10: PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"; 巴哈姆特 12316 first-hand "共享型buff會跟對面搶 如果對面層數比你高就不需要再特別激活直接吃他的奧術buff就行" — the two players of a pair field compete for it).
- **[3 distinct]** Against enemies below 50% HP the multiplier is 1.4 x (1.20 + 0.01*L) = 1.68 + 0.014*L (i.e. +(68+1.4L)%).
- Formulas: `magicTakenMul = 1.20 + 0.01*L`; `magicTakenMulLowHp(3) = 1.4*(1.20 + 0.01*L)`; `hpThreshold = 0.5`; `durationSec = 3`
- How layers are gained: 夕 +2 per operator gained; 阿罗玛 +2 per refresh (<=6); 溯光星源 +2 per 3 gold spent; 海霓 first kill +3; 获得时: 深靛 +2, 协律 +4 (hidden). 机变 "莫斯提马的盟誓" +10.
- [ASSUMED] instances from different 奥术 sources stack multiplicatively
- [ASSUMED] one instance per target, the strongest (the engine's default same-name rule; the two players of a pair field compete for it). Until v2.5 this read "instances from different 奥术 sources stack multiplicatively" and the remake kept one instance per player, so two players' instances multiplied.
- Bond item (with 变形同构体 grants this bond): 奥术法阵 `chess_item_3_08_e` (1 gold: 法术抗性+20,攻击使目标失去特殊能力5秒)
- Garrisons that explicitly add layers to this bond: `garrison_119`, `garrison_121`, `garrison_122`, `garrison_131` (see section 4; plus the generic ones)
- Garrisons that scale with this bond's layers: `garrison_05`
+3 -3
View File
@@ -487,7 +487,7 @@ The gate is the start of the action's route. R1–R3 use only the lower gate. No
| 4 | `pickEntries` `inactiveIn`, `pickReplacement` filters attached keys | inactive list also filters N/E replacements | only `specialEnemyKey` is filtered | [DATA], [WIKI] 上半 note | change |
| 5 | `data/tuning.json → modes[*].enemyHpMul / enemyAtkMul` (all 8 modes, e.g. multi 险境 R13 HP ×0.55) | custom per-round multipliers from `tools/balance.mjs` | none; only the PRTS table (`config.enemyScale`) | [WIKI] | **delete** (keep `titles.comment_3`, which is unrelated) |
| 6 | `data/tuning.json → mode_single_funny.bossHpMul 0.6` | leader pool ×0.6 in solo 标准 | none | — | **delete** |
| 7 | `config.bossHpScale` `bloodPoint × alive/4`, solo ×0.25 [ASSUMED] | pool scales with alive players | co-op: split leaders share HP, "敌方领袖的总生命值不变" → pool = `bloodPoint` (no alive scaling found); **solo: unknown** | [WIKI] notice 5114 / PRTS 下半 | co-op: use `bloodPoint` as is; solo: keep a config value, flagged unknown |
| 7 | `config.bossHpScale` `bloodPoint × alive/4`, solo ×0.25 [ASSUMED] | pool scales with alive players | co-op: split leaders share HP, "敌方领袖的总生命值不变" → pool = `bloodPoint` (no alive scaling found); **solo: unknown**. **Re-read 2026-10-01 (DESIGN §20.10):** notice 5114's full sentence is "在联合模拟的最终攻势阶段,部分敌方领袖会同时出现在战场的左右两侧,两侧的敌方领袖共享生命值(敌方领袖的总生命值不变)" — the left / right copies of one pair field share the HP; it says nothing about the number of players. The one note on that is community (巴哈姆特 12294 "聯機隊友(撤退/死掉)變少,最後boss血條也會變少", no proportion); it is kept as the config switch `bossHpScale.aliveScaling` (× alive / 4), **off** until the user confirms it from official play — it would shorten fights after eliminations, the opposite of the playtest report | [WIKI] notice 5114 / PRTS 下半 | co-op: `bloodPoint` whatever the alive count (× alive / 4 behind the switch, [ASSUMED proportion]); solo: keep a config value, flagged unknown |
| 8 | `waves.js bountySpawns` `soloMul 0.7` for "perfect" bounties | bounty HP/ATK/DEF ×0.7 **on top of** the solo 0.7 base | no bounty-specific rule; the "70 %" note is the global solo base, already in `enemyScale` | [WIKI] 上半 11/18 log | remove `soloMul` |
| 9 | `bountySpawns` timing | t = 4 s, +5 s per bounty, interval 3, `routeByMotion` | attached to the template's first N / NF action window (§5) | [DATA] | change |
| 10 | `buildUniteWave` | grouped by slot class N/E/S/…, compressed into 40 % of the time limit | walkers → `lrsldr` action, flyers → `yokai`, tokens → T/TF; per-owner 0.5 s spacing | [DATA] (partial) | change |
@@ -536,7 +536,7 @@ Everything in `docs/BALANCE.md` §4 (the tuned tables) and the `tools/balance.mj
2. **Stat scaling and balance.**
- Delete every `enemyHpMul`, `enemyAtkMul` and `bossHpMul` in `data/tuning.json`. Keep the `titles` block.
- Remove the `soloMul` in `bountySpawns`.
- `config.bossHpScale`: co-op pool = `bloodPoint[difficulty]` with no alive-player factor; solo stays a flagged config value.
- `config.bossHpScale`: co-op pool = `bloodPoint[difficulty]` with no alive-player factor; solo stays a flagged config value. (2026-10-01: × alive / 4 available behind `aliveScaling`, off, see §6 #7.)
- Keep `enemyScale` (PRTS table) and the 终极 speed ×1.15 from R3.
- Re-run `tools/balance.mjs --tuning off` only to report, not to tune.
3. **Pathing** (`server/sim/grid.js`, `server/sim/ai.js`, `server/sim/Battle.js`):
@@ -566,7 +566,7 @@ Everything in `docs/BALANCE.md` §4 (the tuned tables) and the `tools/balance.mj
| # | Question | Default |
|---|---|---|
| 1 | Do 射击台 (act1 m03 (10,3)/(10,4)) and 土石结构 block ground movement? | Block [ASSUMED] |
| 2 | Leader HP pool in solo, and in co-op with fewer than 4 alive players | co-op `bloodPoint`; solo config value (flagged) |
| 2 | Leader HP pool in solo, and in co-op with fewer than 4 alive players | co-op `bloodPoint` (× alive / 4 behind `aliveScaling`, off — ask the user, 2026-10-01); solo config value (flagged) |
| 3 | Unit of `maxPlayTime` | Real seconds (×2 game) [ASSUMED] |
| 4 | Exact 联防 predelay formula (`_CalculateActionPredelayConsiderUid`) | 0.5 s per owner [ASSUMED] |
| 5 | DEF reduction in the 70 % / 80 % bases | HP/ATK only (下半 text) |
+243
View File
@@ -0,0 +1,243 @@
# 11 · Official limits: the 999 bond-layer cap and the 300000 boss-hit limit ("限伤")
Written 2026-10-01 as research (no code changed then). It follows the boss-HP review (DESIGN §20.10), where
community first-hand reports pointed at two official mechanics the remake lacked: bond layers that stop at 999, and a
"限伤" that makes very large hits deal nothing. **Implemented** the same day after the user asked for the co-op leaders to
be as hard to kill as officially — §6 below and DESIGN §20.12; §2.3 (who counts as a leader) was read from the client
while implementing.
**Tags** (as in research 09)
- **[DATA]**: read from the official client. Here that is the local Windows client 2.7.71 (Unity 2021.3.39f1, files dated
2026-08-20): `global-metadata.dat` (il2cpp v29: class, field and method names, constant values) and `GameAssembly.dll`
(x86-64 machine code, disassembled with `llvm-objdump`; method addresses mapped to names through the
`Assembly-CSharp.dll` code-gen module's method-pointer table and the metadata method tokens).
- **[COMM]**: community posts (Bahamut, NGA, bilibili).
- **[ASSUMED]**: our inference or proposal.
---
## 0. TL;DR
| # | Question | Official answer | Tag |
|---|---|---|---|
| 1 | Do bond layers cap? | **Yes, at 999.** `Torappu.Battle.AutoChessBattleConst.MAX_GARRISON_STACK = 999`. The client's bond-layer counter (`AutoChessPlayerDataModel/ScenePlayerData.AddBondCount`) adds the gain and then clamps: `count = min(count + n, 999)`. | DATA + COMM |
| 2 | Is there a damage limit? | **Yes, on boss leaders in boss battles: a single hit of 300000 or more is cancelled (0 damage).** It is not a clamp and not an integer overflow. `AutoChessBattleConst.MAX_BATTLE_DAMAGE = 300000`; `AutoChessStepModeManager._OnBossEnemyTakeDamage` computes `d = ceil(hit)` and calls `modifier.Cancel()` when `d ≥ 300000`. | DATA + COMM |
| 2a | Where does it apply? | Only in **boss battles outside training** (the Final Assault and the Hidden Core: `AutoChessGameStatus.inBossBattleState && !isTrainingMode` starts "step mode"), and only to enemies that `AutoChessBattleUtil.IsBossEnemy(enemyId)` accepts (the leaders). Normal rounds, 联防, minions in boss battles and training are not affected. | DATA |
| 2b | What does a player see? | The hit does nothing: no HP loss, nothing counted toward the shared pool. Bigger multipliers therefore *lower* the damage of hits pushed past the line, which is why the community advice is to *not* light extra damage bonds on a 999-layer board. | DATA + COMM |
---
## 1. The 999 layer cap
### 1.1 Constant
`Torappu.Battle.AutoChessBattleConst` (metadata field default values; ints are compressed in v29):
| Field | Value |
|---|---|
| `MAX_GARRISON_STACK` | **999** |
| `MAX_BATTLE_DAMAGE` | **300000** |
| `MAX_BATTLE_DEPLOY_CNT` | 12 |
| `MAX_WEAR_EQUIP_CNT` | 2 |
| `SPEED_LEVEL_IN_BATTLE` | 2 |
| `BATTLE_WAIT_UI_TIME` / `HELP_BATTLE_WAIT_UI_TIME` | 1.5 / 2.5 (float) |
(`Torappu.UI.AutoChess.AutoChessCountDownView.COUNT_DOWN_MAX_NUM = 999` is an unrelated UI countdown.)
### 1.2 Where it is used
C# constants are inlined, so the use sites were found by scanning the il2cpp code section for the immediate. In every
`AutoChess` method, 999 appears as a clamp only in
`Torappu.Battle.AutoChess.AutoChessPlayerDataModel/ScenePlayerData::AddBondCount(bondId, n)`:
```
TryGetValue(bondId, out bond); if (!bond.valid) return false; // [rcx+0x20]
bond.stack += n; // add edx, edi ; [rcx+0x18]
bond.stack = bond.stack < 999 ? bond.stack : 999; // mov eax,0x3e7 ; cmp ; cmovl
return true;
```
Its only caller is `AutoChessGarrisonManager._TriggerGarrisonStatusRefresh`, the in-battle 特质 / bond layer gain on the
viewed player's data (`AutoChessDataCenter.viewPlayerData`). The prep-side gains are computed by the scene server, whose
code is not in the client; the client clamp, the constant's home (`AutoChessBattleConst`) and the community reports
(§3) say the cap is the same there. [ASSUMED: the server clamps every gain the same way, `min(L + n, 999)`.]
### 1.3 Reading
- A hard gameplay cap, not a display cap: the clamped value is the one stored (and the layer readers — `GetBondStackCount`,
`AutoChessAssignBondStackCntToBB` — read the stored value).
- Per bond: each bond's layer counter stops at 999 on its own.
- The per-battle gain limits of individual 特质 (e.g. 锏 ≤ 24, 凛御银灰 ≤ 200; blackboard `max_add…`,
`AutoChessGarrisonManager.BOND_ADD_MAX_PER_BATTLE`) are separate and unchanged.
## 2. The 300000 boss-hit limit
### 2.1 The code
`Torappu.Battle.AutoChess.AutoChessGameManager.StartBattle`:
```
if (gameStatus.inBossBattleState && !isTrainingMode) { // [this+0x18] = get_isTrainingMode
inStepMode = true;
BattleController.ReqChangeDeltaTimeFPInStepMode();
stepModeManager.Start();
}
```
`AutoChessStepModeManager.OnEnemyRegistered(enemy)`: `if (AutoChessBattleUtil.IsBossEnemy(enemy.id)) bosses[enemy.instId] = enemy`.
`AutoChessGameManager.OnEntityApplyModifier(entity, modifier)`: `if (inStepMode) stepModeManager.OnEntityApplyModifier(...)`,
which forwards to `_OnBossEnemyTakeDamage` when the entity is a registered boss.
`AutoChessStepModeManager._OnBossEnemyTakeDamage(enemy, modifier)`:
```
int d = (int)Math.Ceiling(modifier.value.AsFloat()); // roundsd …, 0xA (toward +∞)
if (enemy == null || modifier.isCancelled || !modifier.isDamage) return;
if (d <= 0) return;
if (d >= 300000) { modifier.Cancel(); return; } // cmp ebp, 0x493e0 ; jge → Modifier::Cancel(0)
bossDamage[localPlayer] += d; // TryGetValue / set
actions.Add(new StepAction { type = 3, d, killedInfo… }); // the step relayed to the server / partner
```
There is no other use of 300000 (or 299999, or the float 300000.0) in the autochess code; the other three hits of the
immediate in the binary are `HttpWebRequest` / `FtpWebRequest` timeouts.
### 2.2 Reading
- **Cancel, not clamp.** `Modifier.Cancel` is the engine's ordinary damage cancel (the same call as for invulnerability):
unless the modifier carries an ignore-cancel mask for reason 0, the hit is dropped. So a hit worth 299999 lands, a hit
worth 300000 deals **0**.
- **The value checked is the hit about to be applied** (`modifier.value` at apply time: after DEF / RES and every
multiplier). [ASSUMED: before HP shields, after hit-negating blocks.] Only two leader defences in our data are
affected by this order:
- 阿利斯泰尔 (boss_6) gets an HP shield, the 5000-HP barrier of 莫非王土's shield equipment (`boss:vest`, skill
`store` / `storeA` `dynamic` 5000 in data/enemies.json). The order decides only its hits within 5000 of the line:
with the limit first, a hit of 300000–304999 is cancelled and the barrier stays; with shields first, 5000 would be
absorbed and 295000–299999 would land. The vest's ×0.2 on hits from its side is a multiplier, so it counts before
the check.
- 假想敌:管 (boss_10, hidden template h08_03) can stand in 假想敌:再生's aura (`Aura.max_damage_block_cnt` 5), which
negates a whole phys / arts hit in the `hit` step, before the limit. So a hit past the line spends one block
instead of being cancelled by the limit; either way it deals 0.
- **Who:** the leaders only (`IsBossEnemy`, §2.3). Minions in the boss battle, every enemy in normal rounds and 联防, and
everything in training are uncapped.
- **Why it exists [ASSUMED]:** in step mode every boss hit is relayed as an action so both players of a pair and the
server keep one shared pool; the limit looks like a sanity guard on those reports. It still decides real fights:
the co-op leader pools (`bloodPoint`, data/bosses.json) run from 200000 (FUNNY) to 7600000 (ABYSS), so in the easy
modes one hit over the line would be a whole pool.
- **Not an overflow.** Nothing wraps at 2³¹; the line is a deliberate constant.
### 2.3 Who is a leader: `AutoChessBattleUtil.IsBossEnemy(enemyId)` [DATA]
Read while implementing (metadata token 0x6013192, method pointer 0x180b058e0 in the same build; `Il2CppCodeGenModule`
method-pointer table of research 08 Appendix B):
```
foreach (var kv in <activity data>.autoChessData.bossInfoDict) // Dictionary<string, AutoChessBossInfoData>
if (string.Equals(kv.Value.enemyId, enemyId)) return true; // [value+0x18]; the class's fields: bossId 0x10,
return false; // enemyId 0x18, handbookEnemyId 0x20
```
So a leader is an enemy whose id is a `bossInfoDict[*].enemyId` — the 10 keys of data/bosses.json `enemyKey`
(enemy_9013_acstmk, enemy_9017_achunt, enemy_9021_acduml, enemy_1521_dslily, enemy_2016_csphtm, enemy_9032_aclionk,
enemy_9033_acdeer and the hidden enemy_9013_acstmk_2 / enemy_9017_achunt_2 / enemy_9021_acduml_2). It matches by id, so
every instance counts — the second copy a pair field spawns (boss_battle_multi_player) included. The hidden leaders'
parts (“斩胄之剑” / “破胄之锤” enemy_9014/9015, “碎铳之簧” enemy_9018–9020, 假想敌:弦 enemy_9022 and “裂管之音” /
“断弦之音” enemy_9023), 假想敌:胄's 刺胄之弹 (enemy_9016_acstmr) and drones (enemy_1005_yokai), escorts and bounty enemies
are not in the list: hits on them are never cancelled — the share a part passes on to its leader ("被击落时受到伤害以一定
比例传递") is a hit on the leader and is checked [ASSUMED: the transfer is an ordinary damage modifier on the leader].
In the remake's wave data the tag-'boss' spawns of the boss / hidden templates are exactly those keys and nothing else
(test/sim/playtest6_limits.test.js checks it).
## 3. Community reports
Paraphrased; only key terms are quoted.
### 3.1 Layers stop at 999 [COMM]
- **Bahamut** [C.php?bsn=33651&snA=12522](https://forum.gamer.com.tw/C.php?bsn=33651&snA=12522) (2026-06-28): the text says
999 layers are usually reached within 2 rounds, and that 精准 and 萨尔贡 were stacked full at 999. Its screenshots:
R12 谢拉格 696 → R13 谢拉格 **999**, 灵巧 301, 炎 122 → R14 谢拉格 still **999** while the author kept feeding it,
灵巧 671, 炎 537 → R15 炎 / 谢拉格 / 灵巧 all **999**, 迅捷 222. Each bond stops on its own.
- **bilibili** [BV16bU7BTEX1](https://www.bilibili.com/video/BV16bU7BTEX1) (2025-11-22): 萨尔贡 filled from 0 in one round, the
surplus wasted; a commenter lists the 「层数上限」 among what holds 瑰盐 back.
- **TapTap** [moment 747932824320870144](https://www.taptap.cn/moment/747932824320870144) (2025-12-10): 谢拉格 from 100 to 999
in one round.
- More videos treating 999 as full: bilibili BV1xVdBBWEuu, BV1vUyJB4Ero ("all bonds 999"), BV1eoCSBzEde (拉特兰 999 by
R11); YouTube thG4eVPnSnM. No source shows a bond above 999.
- Not stated anywhere official: the excel tables (`activity_table` act2autochess / `autoChessData`: no cap field,
`maxInactiveBondCount = -1`), PRTS (卫戍协议:盟约, 下半, 帮助, PRTS盟约记录), BWIKI and the in-game help.
### 3.2 "限伤" [COMM]
- **Bahamut** [C.php?bsn=33651&snA=12316](https://forum.gamer.com.tw/C.php?bsn=33651&snA=12316) (【心得】衛戍協議:盟約上下差異整理,
2026-05-04): some big bond combinations make the output too high, and the 「限傷」 then keeps it from coming out; with
999 谢拉格 do not light 奥术, or 大初雪 (圣聆初雪, tier 6, S3) is limited away; 瑪 (玛恩纳) should not switch to 谢拉格
for the same reason; the same warning for 阿戈尔. No number given.
- **bilibili** 【卫戍攻略#1】维多利亚&谢拉格篇 [BV19UXZBJExQ](https://www.bilibili.com/video/BV19UXZBJExQ) (comments, 2026-04):
a high-layer 圣雪 easily hits the limit and her damage goes to zero; at 999 谢 do not light 奥术, maybe drop 灵知; a
teammate's 奥术 in the boss fight zeroes her too. Asked what 限伤 means, a reply gives the number:
「一次超过30w会被卡掉」 (a single hit over 300k is cut) — the client's `MAX_BATTLE_DAMAGE`.
- **bilibili** 每把都能999层 谢拉格 [BV1xVdBBWEuu](https://www.bilibili.com/video/BV1xVdBBWEuu): bullet comments call an on-screen
hit of 272451 "nearly at the limit" and warn that the damage "overflows to zero"; in the comments 160k hits are said
to be safe, and a player's 0 damage *on the boss* is discussed.
- **bilibili** 【卫戍攻略#4】什么叫叠到999层反而亏输出 [BV1nrdqBLERz](https://www.bilibili.com/video/BV1nrdqBLERz): comments mention
"炎999限伤" and ask whether 仇白 S3 reaches it.
- JP wikiru (堅守協定:盟約 2nd) says the damage limit wastes layers beyond a point.
- Not the main game's overflow: PRTS 定点数与浮点数 / 数值范围 — battle values are a 64-bit fixed point (the client's
`Torappu.FP`), max ≈ 2³¹; a main-game hit past 2.1 billion shows 0 (bilibili BV1bhgz6DEKS, Integrated Strategies).
That is ~7000× the 卫戍 line; PRTS 作战机制 records ordinary hits in the millions, so the 300000 rule is mode-specific.
Why the hits get that big (data blackboards): 谢拉格 ×(1.35 + 0.01·L) vs cold / frozen = ×11.34 at 999; 奥术 arts taken
×(1.2 + 0.01·L) = ×11.19 at 999, a debuff on the target (so the partner's 奥术 boosts your hits); 灵知 +27–29 % taken on
cold targets, doubled on frozen; 圣聆初雪's 特质 +1 % ATK (elite +2 %) per 3 layers of her bonds; 炎 ATK
+(0.23 + 0.009·L).
- NGA [tid=46592301](https://bbs.nga.cn/read.php?tid=46592301) "卫戍协议BUG汇总&神秘机制": every endpoint answers 403
「访客不能直接访问」 (login required); not read.
These reports fit §2 exactly: the advice is to *avoid* extra multipliers, which only makes sense if a hit past the
line is lost rather than clamped.
## 4. What the remake did before the implementation, and what it does now
| | Remake before (master 7094892) | Remake now (§6) | Official |
|---|---|---|---|
| Layers | uncapped: `PlayerState.addLayers` (prep), `Match.js` settle (`ps.layers[b] += layerGains`), `Battle.addLayers` (live in-battle copy) | per bond `min(L + n, 999)` at every writer (`layerGainRoom`) | per bond `min(L + n, 999)` |
| Boss hits | no limit (`sim/damage.js dealDamage` → `applyHpLoss` → `bossPool.damage`) | leader hit in a 'boss' / 'hidden' battle with `ceil(d) ≥ 300000` → cancelled, 0 (`leaderHitCancelled`) | leader hit with `ceil(d) ≥ 300000` → 0 |
## 5. Proposal (as written before the implementation; §6 is what was built)
- `shared/constants.js`: `BOND_LAYER_CAP = 999` and `BOSS_HIT_LIMIT = 300000` (each `0`/`Infinity` = off), the switches.
- Layers: clamp in `PlayerState.addLayers`, the settle in `Match.js`, and `Battle.addLayers` (the live copy, as the
client's `AddBondCount`); `onLayers` reports the clamped `to`; a gain at the cap adds 0.
- Boss hits: in `dealDamage`, after the multipliers and before shields, `if (target.isBoss && (battle.kind === 'boss' ||
battle.kind === 'hidden') && Math.ceil(final) >= BOSS_HIT_LIMIT) final = 0` (with a `capped` event so the client can show it) [ASSUMED: shown as
no number, like the official].
- Tests, docs (META / SIM / BALANCE / PLAYING) and a DESIGN draft.
## 6. Implementation (2026-10-01, DESIGN §20.12)
- `shared/constants.js`: `BOND_LAYER_CAP = 999` and `BOSS_HIT_LIMIT = 300000` (0 / Infinity = off), plus
`layerGainRoom(before, n)` = min(n, cap − before), never negative.
- Layers: every writer clamps with it — `PlayerState.addLayers` (every prep-side gain: 特质, items, bands, 机变 cards,
bonds), the settle of the in-battle gains (`Match.js`), `Battle.addLayers` (the live in-battle copy, after the
`layerGain` hook, as the client's `AddBondCount`); a gain at the cap adds 0 (no onLayers, no 'layer' event, no hook),
so the per-N milestones (远见, 奇迹, 维多利亚 …) stop with the count; the client-result check (`fields.js`) bounds a
reported gain by the room left from the bond's starting layers; `invariants.js` flags a bond above the cap. The dev
tools write through the same helper: `tools/matchrun.mjs --layers N` (a raw boost, no milestones) and
`tools/balance.mjs applyBoard` (any `--profile`) stop at 999 too.
- 限伤: `sim/damage.js leaderHitCancelled` in `dealDamage` (after DEF / RES and every multiplier, before shields) and in
`Battle.loseHp` (losses passed on to a leader): a leader (`isBoss`, the tag-'boss' units = §2.3) in a 'boss' / 'hidden'
battle with `ceil(amount) ≥ 300000` → the hit returns 0 before shields (阿利斯泰尔's 5000 `boss:vest` barrier stays
untouched; 再生's hit blocks act earlier, in the `hit` step, §2.2), HP / pool loss, stats, the damage number, the
`damaged` / `fatal` hooks and the kill; what ran before it (the attack, its SP, the `hit` hook, separate element 损伤
hits) stays. Element bursts (元素伤害) and DoT ticks are checked like any hit; element 损伤 (gauge fill) removes no HP
and is not. The sim emits `['fx', 'hitCap', x, y, { id, n }]`; the client draws nothing for it (no number, like the
official [ASSUMED]).
- Fixed-ratio losses on a leader scale with the pool, so the pool size decides whether they meet the line. The one that
can: 【死亡集群】 (boss_1 / boss_8 skill 2, bb `hp_ratio 0.02`): a drone killed by an operator costs the leader
0.02 × its max HP, and a leader's max HP is the shared pool's max (`Battle` syncs it). Today's largest such pool,
boss_8 ABYSS 7.2M, gives 144000, which lands. A pool above 14999950 (≈ 2.08 × that; ceil(0.02 × max) = 300000)
would turn every drone kill into a cancelled hit [ASSUMED: the official routes this loss through the same
`_OnBossEnemyTakeDamage` check]. Any change to the pool size (an alive-scaled pool, a new difficulty) should keep this
in view; a real-kit test pins the line (`test/sim/playtest6_limits.test.js`, 【死亡集群】).