Files
ark-stronghold-protocol/docs/research/11-limits-official.md
T

244 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.settle`, `server/match/match/settle.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`, 【死亡集群】).