- 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
244 lines
18 KiB
Markdown
244 lines
18 KiB
Markdown
# 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`, 【死亡集群】).
|