Files
HEROLL/HEROLL_RULES_v0.2.8/03-战斗/2026-08-28_逆向-HEROLL战斗规则-report.md

183 lines
14 KiB
Markdown
Raw Permalink 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.
# HEROLL 2.0.1 战斗规则与 Double 影响逆向报告
> 分析日期:2026-08-28
> 目标:HEROLL : Dice Roguelike 2.0.1(Android / Unity IL2CPP / ARM64)
> 结论用途:HEROLL 本地玩法验证 MVP
> 证据标签:`NATIVE_CONFIRMED` 原生指令闭合;`CONFIG_CONFIRMED` 源表确认;`VIDEO_CONFIRMED` 录像确认;`COMMUNITY_CROSSCHECK` 社区交叉验证;`PARTIAL` 仍有未闭合分支。
## 1. 执行摘要
本次从解密后 `libil2cpp.runtime-decrypted.so` 的真实 ARM64 方法体、IL2CPP 方法地址表、CSV 配置和原作录像中重建了主线战斗核心。可以直接用于 MVP 的闭合结论是:
1. 玩家基础攻击取最终攻击力;敌人攻击在自身攻击区间上叠加 `EnemyDamgeRange=15` 的浮动。
2. 命中、暴击和反击是三次独立判定,不能共用一个随机结果。旧报告把 `0x549B934` 误当暴击 RNG;该点实际是命中/闪避 RNG。
3. 暴击率判定为 `CR > RandomInt(0,101)`;基础暴伤为 150%,暴击伤害为 `damage * criticalDamagePercent / 100`。
4. 防御是线性减伤:`max(0, 防御前伤害 - 最终防御)`,不是比例减伤。
5. 攻速换算为 `攻击间隔秒 = 100 / 最终攻速`;裸装攻速 100 对应 1.0 秒基础间隔。
6. 吸血为 `ceil(本次最终伤害 * 吸血% / 100)`,只要计算值为正,最低恢复 1 HP。
7. Double 的基础含义只是“两骰相等”并广播 Double 事件;移动仍为 `diceA + diceB`,不会自动再×2,也没有无条件伤害倍率。
8. Double 对战斗的影响来自已获得的遗物、Joker、宠物或武器封印等条件消费者,不是全局基础规则。
## 2. 目标与范围
- 授权与边界:[`work/heroll-stage-2-5/scope.md`](../../work/heroll-stage-2-5/scope.md)
- 主样本:`work/heroll-stage-2-5/libil2cpp.runtime-decrypted.so`
- SHA-256:`AC357FBA5BF8A68BA02829593020F29DA220EAC8D5ECA91212C1A6744B0100DA`
- 工具:radare2 6.2.0;IDA 启动脚本已实际执行,但本机未配置 `IDADIR` 且未发现 IDA Pro,因此未伪造 IDA 输出。
- 网络:本地证据不足以恢复遗物的用户文案时,使用 Heroll Wiki 和玩家实测帖交叉验证;互联网证据不替代原生证据。
## 3. 可实现的战斗公式
| 环节 | 原作规则 / 公式 | 边界与取整 | 证据 | 置信度 |
|---|---|---|---|---|
| 裸装初始值 | ATK 20,HP 500,AIM 1.5,DEF 10,EVA 1,Counter 10,Attack Speed 100,Crit Rate 10,Life Steal 0 | 浮点状态 | `battleplayerdefaultstat.csv` | 高 |
| 最终属性 | 常规路径为 `independentBonus + flatSum * (100 + percentBonus) / 100`,再经特殊模式修正 | 某些调用可请求“仅 flat+independent”或扣除特定层 | `CharacterStatus.GetFinalStatus @ 0x5409B54` | 中高 |
| 玩家基础伤害 | `baseDamage = GetFinalStatus(ATTACK_POWER)` | 此阶段不做随机浮动 | `BaseActor.GetBaseDamageAndConvertWithPlayerDamage @ 0x5494CD8` | 高 |
| 敌人基础伤害 | 根据攻击状态上/下界做浮点随机;配置浮动为 ±15% | 随机边界由浮点 Range 实现 | 同上 + `EnemyDamgeRange=15` | 高 |
| 命中 | `hit = hitPercent > RandomInt(0,101)` | RNG 为 0..100;`hitPercent<=roll` 失败;负的 hitPercent 先归 0 | `BaseMonoCharacter.HitAndGetResult @ 0x549B2CC` | 高(判定)/中(hitPercent 合成) |
| 暴击 | `critical = finalCritRate > RandomInt(0,101)` | 0..100,严格大于;10 对应 10/101≈9.901% | `BaseMonoPlayer.OnBeforeAttack @ 0x55A95EC` | 高 |
| 暴击伤害 | `damage = damage * GetCriticalDamagePercent() * 0.01` | `BaseCritical=150`,即裸配 1.5×;常数 `0x1EB7214=0.01f` | `0x55A9704..0x55A9728` | 高 |
| 防御减伤 | `finalDamage = max(0, preDefenseDamage - finalDefense)` | 伤害保留 float;字体/日志可单独取整 | `0x549BB14..0x549BD9C` | 高 |
| HP 扣除 | `targetHP = targetHP - finalDamage` | 证据中未见在扣 HP 前对伤害做整数化 | `0x549BE7C..0x549BE94` | 高 |
| 攻速 | `interval = 1 / (finalAttackSpeed * 0.01) = 100/finalAttackSpeed` | 协程再减去已经过的时间;剩余时间≤0 则立即继续 | `<WaitAttackCooldown>d__45.MoveNext @ 0x5497280` | 高 |
| 吸血 | `heal = ceil(finalDamage * finalLifeSteal * 0.01)` | `heal>0` 时最低 1;调用 `CharacterStatus.Heal` | `BaseMonoPlayer.OnAfterAttack @ 0x55A9908`, `0x55A9E48..0x55A9EC0` | 高 |
| 反击触发 | `counter = finalCounterRate >= RandomInt(1,101)` | RNG 为 1..100,因此 10 就是精确 10% | `BaseMonoPlayer.OnCounterAttack @ 0x55ABEE0` | 高 |
| 反击伤害 | 以本次敌方攻击/受到伤害链构建反击 `DamageMeta`,再走命中、暴击和对方防御 | 玩家仍承受原伤害;无伤害护盾可不触发;玩家实测表明反击不触发吸血 | 原生部分闭合 + 社区实测 | 中高 |
### 3.1 概率边界必须照原作实现
原作没有统一使用“0..99 小于百分比”。三种判定的边界不同:
```text
命中: hitPercent > random(0..100)
暴击: critRate > random(0..100)
反击: counterRate >= random(1..100)
```
因此 MVP 若用 `Math.random()*100 < rate` 同时替代三者,会产生边界差异。特别是暴击率 100 在原作中仍会在 roll=100 时失败;只有大于 100 才能在该路径保证暴击。
### 3.2 伤害数据流
```mermaid
flowchart LR
AP["最终攻击力 / 敌人浮动攻击"] --> META["DamageMeta.damage"]
META --> HIT{"命中判定"}
HIT -- "miss" --> ZERO["0 伤害"]
HIT -- "hit" --> CRIT{"暴击判定"}
CRIT -- "yes" --> CMUL["× 暴伤% / 100"]
CRIT -- "no" --> HOOK["攻击/受击钩子"]
CMUL --> HOOK
HOOK --> DEF["max(0, damage - 最终DEF)"]
DEF --> HP["HP -= finalDamage"]
HP --> LS["ceil(finalDamage × 吸血% / 100)"]
```
注:`TOTAL_DAMAGE`、`TRUE_DAMAGE`、装备/遗物钩子会在链中插入修正,但不应在没有闭合每个消费者时压成一条伪精确的总公式。
## 4. Double 对战斗的真实影响
### 4.1 Double 基础规则
1. `dicerate.csv` 包含 36 个有序 D6 组合,APK 内置权重全为 10。原生代码按 `weight/sum(weight)` 抽取。在没有远程覆盖时,每个有序结果为 1/36,自然 Double 概率为 6/36=1/6。
2. `isDouble = (diceA == diceB) OR forcedDoubleBranch`。原作存在独立的强制 Double 分支,但当前启用内容中 `Potion_GuaranteedDouble` 为禁用。
3. `moveCount = diceA + diceB`。录像中 1+1 显示总点数 2,不是 4。
4. Double 发生后,第一枚骰子的值传入 `BoardController.TryAddDoubleStack(diceA)`;录像中 1+1 后显示“Double 点数 +1”。这是“累计骰面点数”,不应实现成每次 Double 固定 +1。
5. `EventManager.TriggerOnDiceDouble` 会广播事件,各个已装备内容自行订阅;基础战斗公式中没有“Double 直接伤害×2”。
### 4.2 条件性战斗效果
| 来源 | 配置技能 | Double 效果 | 数值 | 证据等级 |
|---|---|---|---:|---|
| Scissors(剪刀)遗物 | `DoubleAttack` | Double 时对敌人发动一次基于 ATK 的攻击 | 100% ATK | 配置 + Wiki |
| Scissors 传奇效果 | `DoubleAttackEnforce` | Double 时按骰面点数增加额外攻击次数 | 次数与 dot count 相关 | Wiki;精确次数还需动态验证 |
| Chilled Tome(寒冰书) | `DoubleFreeze` | Double 时冻结敌人 | 1 s | 配置 + Wiki |
| Chilled Tome 传奇效果 | `DoubleFreezeEnforce` | 增加 Double 冻结时长 | +1 s | 配置 + Wiki |
| Battleship Horn(号角) | `DoubleAspdUp` | Double 时攻速提高 | +30% | 配置 + Wiki;持续时间未静态闭合 |
| Gas Mask(防毒面具) | `DoubleShield` | Double 时有概率获得护盾 | 50% 触发概率 | 配置 + Wiki;护盾量未闭合 |
| Rejuvenate Tea(恢复茶) | `DoubleBonusStat` | Double 时永久提升一项随机基础属性 | +10% | 配置 + Wiki |
| Monster Dice(怪物骰子) | `DoubleExecute` | Double 后抵达敌人格时,概率处决 1 个敌人 | 35% | 配置 + Wiki |
| Joker | `DoubleRollPoints` | Double 时获得额外点数,可间接与 `PointToAttackPower` 构筑联动 | Normal +2 / Hero +5 / Legend +10 | `joker.csv` + `OnDiceDoubleHandler` |
| 宠物 | `BasePet.CheckDoubleDice` / `Pet_DoubleAttack` | 部分宠物在 Double 后进入自身攻击链 | 取决于宠物 | 元数据方法链,数值未闭合 |
| 武器封印负面事件 | `OnWeaponSealDoubleHandler` | Double 增加解封计数 | 取决于事件配置 | 元数据方法链 |
### 4.3 不应加入 MVP 的伪规则
- 不要把 Double 做成移动点数×2。
- 不要把 Double 做成无条件伤害×2。
- 不要把“随机基础属性 +10%”与“下次战斗先攻”做成全局 Double 规则。前者是 Rejuvenate Tea 遗物的稀有效果;当前证据不支持后者是原作基础规则。
- 不要将 `DoubleHeal` 名称直接解读为“骰子 Double 回血”。该 Augment 的现有调用链没有证明其订阅 `OnDiceDouble`;更可能是“治疗事件二次结算概率”,在未闭合前不纳入 Double 基础链。
## 5. 对现有 GDD / MVP 的覆盖修正
| 位置 | 现有内容 | 覆盖结论 |
|---|---|---|
| `Heroll_Gameplay_Validation_GDD.md` Double 段 | 对子统一触发“机缘”、随机属性 +10%和下次先攻 | 与原作不符。删除全局规则;将 +10% 放回 Rejuvenate Tea 遗物条件。 |
| `formula-fit.csv` 吸血 | `floor(finalDamage*lifeStealPct/100)` | 改为 `ceil(finalDamage*lifeStealPct/100)`,正数最低 1。 |
| `formula-fit.csv` 攻速 | 候选 `max(0.15,1/(attackSpeed/100))` | 原生主干已证明 `100/attackSpeed`;未见全局 0.15 s 下限,不应擅自加 clamp。 |
| 暴击 RNG 旧结论 | `0x549B934` | 该地址是命中 RNG;暴击在 `0x55A96C4..0x55A98C0`。 |
## 6. Evidence → Finding → Path
### 6.1 Evidence
| E-id | source_ref | 可复现内容 | content_hash |
|---|---|---|---|
| E-COMBAT-001 | `libil2cpp.runtime-decrypted.so` + `all_method_addresses.csv` | 用 radare2 按 RVA 反汇编 `GetFinalStatus` / `HitAndGetResult` / `OnBeforeAttack` / `OnAfterAttack` / `OnCounterAttack` | SO SHA-256 见第 2 节 |
| E-COMBAT-002 | `analysis/combat-rules/native-slices/` | 独立 ARM64 切片;例:`radare2 -q -a arm -b 64 -m 0x5412454 -c "pd 253" CharacterStatus_GetPlayerEvasionPercent.bin` | 各切片 SHA-256 见目录中文件固定值 |
| E-COMBAT-003 | `battleplayerdefaultstat.csv`, `gameconfig.csv` | 裸装数值、`BaseCritical=150`、`EnemyDamgeRange=15` | `35DAC0A6...32D8`, `A57510DE...E3D` |
| E-DOUBLE-001 | `dicerate.csv` + `<CheckLastTile>d__130.MoveNext` + 原作录像 00:03:35 | 等权 36 对、相等判 Double、移动取和、1+1→2且累计 +1 | 已有 Stage 2.5 证据包固定值 |
| E-DOUBLE-002 | `relic.csv`, `equipskill.csv`, `joker.csv` | Double 消费者名称与 100/1/30/50/10/35、2/5/10 数值 | `relic.csv E396B478...BE9`; `equipskill.csv EB8A8674...75C2` |
| E-WEB-001 | Heroll Wiki Relics | 遗物用户可见语义,用于解释本地 localization key | n/a |
| E-WEB-002 | Reddit 玩家反击实测 | 反击基于敌方攻击,可暴击/未命中,仍受伤,不吸血 | n/a |
### 6.2 Findings
| F-id | 结论 | evidence_ids | confidence | location | status |
|---|---|---|---|---|---|
| F-COMBAT-001 | 暴击与命中 RNG 已分离,旧地址归属错误 | E-COMBAT-001, E-COMBAT-002 | high | `0x549B934`, `0x55A96C4` | validated |
| F-COMBAT-002 | 防御线性减伤、攻速取倒数、吸血向上取整 | E-COMBAT-001, E-COMBAT-003 | high | 见第 3 节 | validated |
| F-DOUBLE-001 | Double 不倍增移动,不内建伤害倍率 | E-DOUBLE-001 | high | `<CheckLastTile>d__130.MoveNext` | validated |
| F-DOUBLE-002 | Double 的战斗价值由遗物/Joker/宠物/事件订阅者提供 | E-DOUBLE-001, E-DOUBLE-002, E-WEB-001 | high | `TriggerOnDiceDouble` 消费链 | validated |
| F-DOUBLE-003 | Double Stack 的完整阈值、清空时机和终点奖励仍未闭合 | E-DOUBLE-001 | medium | `TryAddDoubleStack` async state machine | candidate |
### 6.3 Path
**P-COMBAT-001 — 一次普通攻击的原生调用流**
1. `RunAttackSequence` 调用攻击者 `OnBeforeAttack`,建立伤害与暴击标志 — E-COMBAT-001 — F-COMBAT-001。
2. `HitAndGetResult` 获得 AIM/目标命中百分比并执行 0..100 判定 — E-COMBAT-001。
3. 命中后进入受击钩子,执行 `max(0, damage-DEF)` 并扣 HP — E-COMBAT-001 — F-COMBAT-002。
4. `OnAfterAttack` 以最终伤害计算吸血 — E-COMBAT-001 — F-COMBAT-002。
5. `WaitAttackCooldown` 用 `100/AttackSpeed` 计算下一次攻击时间 — E-COMBAT-001 — F-COMBAT-002。
**P-DOUBLE-001 — Double 到战斗效果的事件流**
1. `DiceRate` 加权抽取有序骰子对 — E-DOUBLE-001。
2. 两骰相等或命中强制分支后置 `isDouble` — E-DOUBLE-001 — F-DOUBLE-001。
3. 仍以 `A+B` 移动,同时把 A 送入 Double Stack — E-DOUBLE-001 — F-DOUBLE-001。
4. `TriggerOnDiceDouble` 广播,已装备遗物/Joker/宠物/事件按各自规则结算 — E-DOUBLE-002 — F-DOUBLE-002。
## 7. 未闭合与建议最小实测
下列内容不影响核心 MVP 公式,但如要 1:1 实现全部构筑,需补录:
1. 命中百分比的完整 `AIM/EVA/宠物/遗物` 合成顺序:同一敌人下使用 3 组已知 AIM,每组至少 200 次攻击。
2. Battleship Horn +30% 攻速的持续时间和叠加方式:录制 Double 前后至少 10 次攻击时间戳。
3. Gas Mask 护盾量和刷新规则:连续触发两次 Double,记录护盾 UI 数值。
4. Scissors 传奇追加攻击次数:分别强制 1+1..6+6,记录攻击段数。
5. Double Stack:连续强制不同骰面 Double,记录累计值、阈值、奖励与清空时机。
## 8. Timeline 摘要
- 2026-08-19:建立授权范围,重建 IL2CPP 地址映射和解密 SO。
- 2026-08-19:提取 93 个有边界的原生方法体,封存 Stage 2.5.1 证据。
- 2026-08-28:复核主线战斗、纠正暴击地址,闭合攻速/吸血/反击边界,完成 Double 战斗消费者矩阵。
## 9. 互联网交叉资料
- [Heroll Wiki — Relics](https://heroll.fandom.com/wiki/Relics)
- [Reddit r/heroll — Counter attack?](https://www.reddit.com/r/heroll/comments/1l20mkh/counter_attack/)
- [Reddit r/heroll — Slightly in-depth mechanics and interactions](https://www.reddit.com/r/heroll/comments/1l3sn2n/)
- [Google Play — Heroll : Dice Roguelike](https://play.google.com/store/apps/details?id=com.percent.aos.rollinghero)