# 伤害修正与精力消耗：原生函数证据

## 适用版本与结论范围

- Steam Build：`24447756`
- 二进制：`BBQ/Binaries/Win64/BBQ-Win64-Shipping.exe`
- SHA-256：`717AC583A968A464E464403EBCEA3B5DD2EA593373E25CF2B1D846F2BE537D94`
- 首选映像基址：`0x140000000`；下列地址均为此映像的 VA，不能跨版本套用。

已确认的内容是若干伤害修正的**写入、累加、移除和条件处理**，以及一次精力消耗请求的原生计算顺序。尚未找到完整的最终生命伤害公式；不能据此把所有增伤归入统一加算区或独立乘区，也不能把精力消耗中的 `0.0001` 推广为所有伤害字段的分母。

证据分为三层：反射元数据确定名称与偏移；机器指令确定实际操作；资产导出提供具体配置。三层没有闭合的部分保留为未知。本文不把字段名称本身当成公式证明，也没有经过实机固定样本验证。

## 复现入口

现有脚本可定位反射名称、名称—函数指针对与引用指令：

```powershell
python scripts/recon/find_pe_string_xrefs.py `
  --exe "V:\SteamLibrary\steamapps\common\The First Berserker Khazan\BBQ\Binaries\Win64\BBQ-Win64-Shipping.exe" `
  --text AddComputedSkillDamageRate
```

可把 `--text` 替换为下文的函数名或类名继续复查。实际函数边界由 PE exception directory（数据目录索引 3）校验；此文件的 section 名称经过改写，不能依赖 `.pdata` 名称。部分函数在异常表中分成多个连续区段，只读取首个区段会遗漏后续计算。下列调用链与关键 VA 也可直接交给 x64 反汇编器复核。

## 技能伤害修正结构确实按字段累加

`AddComputedSkillDamageRate` 的 UTF-16 字符串位于 `0x146E476A0`，名称指针槽 `0x146E48B50` 指向 exec 包装器 `0x1418CE810`；延迟 UFunction 初始化入口为 `0x1418CEC10`。包装器解析整数参数后执行：

```asm
0x1418CE87B  add dword ptr [rsi + 0x178], eax
```

该操作内联在包装器中，确认传入值被加到现有整数值上，没有在此处乘算。`CurrentComputedSkillDamageInfo` 反射描述符 `0x146E48880` 指明其在 DamageInstance 中位于 `+0x174`。结构反射 getter 为 `0x141891660`，元数据 `0x1479E9610`，大小 `0x24` 字节；四个公开字段如下。

| 字段 | 结构内偏移 | DamageInstance 内偏移 | 属性描述符 |
| --- | --- | --- | --- |
| `CustomSkillPowerBonusAdd` | `0x00` | `0x174` | `0x146DCB800` |
| `CustomSkillPowerBonusRate` | `0x04` | `0x178` | `0x146DCB828` |
| `CustomSkillStaminaAttackPowerAdd` | `0x08` | `0x17C` | `0x146DCB870` |
| `CustomSkillStaminaAttackPowerRate` | `0x0C` | `0x180` | `0x146DCB898` |

原生复制和运算还覆盖后续内部字段，但目前只给上述四个字段建立了反射名称映射；不为其余偏移猜测名称。

资产回调 `xxDamageInfoFuncAdditionalDamage` 通过另一条路径执行相同的结构累加：

| 定位层 | VA |
| --- | --- |
| 类名字符串 | `0x146E46592` |
| static class 初始化入口 | `0x1418CB850` |
| 构造器 | `0x1418CF2F0` |
| vtable | `0x146E50460` |
| vtable `+0x288` 实现 | `0x1415AC770` |

该实现取得 DamageInstance 后，把本对象 `+0x28…+0x44` 的八个整数分别加到实例 `+0x174…+0x190`。例如 `0x1415AC7A5` 取得 `instance+0x174`，`0x1415AC7AC` 对首字段执行 `add`，后续字段依次重复。因此这一类回调与 `AddComputedSkillDamageRate` 的**同字段输入可确认累加**；但这些累加结果在最终公式中与基础攻击、技能原始威力、抗性等怎样组合，仍未确认。

`GetDamage` 的 exec 包装器 `0x1418CE7F0` 在 `0x1418CE805` 读取 `instance+0xC8` 并返回，属于读取已经保存的结果。它不是最终伤害计算入口。DamageInstance 构造器为 `0x1418CFDD0`，重置路径 `0x1414CC550` 清零结果及修正结构。`CreateDamageInstance` 包装器 `0x1418CE9E0` 调用 `0x1414CC320` 进行创建/设置，并调用 `0x1414D28E0` 处理反应分类；这些发现同样不足以证明最终伤害公式。

## Buff 的加法聚合与技能等级分支

`xxSpellFunc_AddSkillDamageBuff` 的确定调用链如下。注意精确类名，另一条相邻字符串属于 `xxSpellFunc_AddSkillDamageBuffDamager`。

| 定位层 | VA |
| --- | --- |
| 类名字符串 | `0x1470B18B2` |
| static class 初始化入口 | `0x141A5FEB0` |
| 构造器 | `0x141A629E0` |
| vtable | `0x1470B8E30` |
| 应用：vtable `+0x288` | `0x14170EE60` |
| 移除：vtable `+0x290` | `0x14170F330` |
| 重叠变化相关更新：vtable `+0x298` | `0x14170F0A0` |

`OnAttackerSkillAdditionalDamage` 位于函数对象 `+0x28`（描述符 `0x1470BA710`）；`bUseOverlapChangeTime` 的 setter 写入 `+0x4C`，`LimitOverlapCount` 位于 `+0x50`；`bApplySkillLevel` 的 setter `0x140AED920` 写入 `+0x54`（描述符 `0x1470BA770`）。

应用函数的操作：

1. 当 `bApplySkillLevel=false`，把配置结构复制到 Spell 实例缓存 `+0x48C`。
2. 当 `bApplySkillLevel=true`，前四个值先经过 helper `0x1416A26E0`；使用的类型码依次为 `1、3、2、4`，转换为整数后再进入缓存。目前没有确定该 helper 的完整含义。
3. 将缓存的八个整数分别加到拥有者聚合字段 `+0x7F8、+0x7FC、+0x800、+0x804…+0x814`。
4. 移除函数减去对应字段。重叠变化分支还调用 `0x1417395B0` 传递层数，不能仅凭一次 `add` 指令推定整个 Buff 生命周期中调用次数或层数。

例如 `SP_Kazan_DualAxeSword_Yaksha_SpinningBeast_HitStack.json` 配置 `CustomSkillPowerBonusRate=500`、`OverlapLimit=4`、`OverlapLimitPolicy=NoLimit`。网站可以展示这些原始配置，但“每层直接等于固定最终伤害百分比”“最多实际生效四次”都需要补充生命周期和最终公式证据。

难度 Buff 中存在 `bApplySkillLevel=true` 的案例。因此 Easy/Normal/Extreme 等难度资产中的原始 attacker/receiver 修正值不能直接当作无条件最终倍率；应保留原值、技能等级标记和证据状态。

## 按 Spell 剩余时间生成伤害修正

`xxDmgInfoFuncAdditionalDamageByRemainSpellTime` 调用链：

| 定位层 | VA |
| --- | --- |
| 类名字符串 | `0x146E45E32` |
| static class 初始化入口 | `0x1418CC640` |
| 构造器 | `0x1418CF830` |
| vtable | `0x146E53310` |
| vtable `+0x288` 实现 | `0x1415ADE00` |

找到目标 Spell 后，函数读取两个原生时间值并计算：

```text
t = float32((int64[spell+0x4E8] - int64[spell+0x3F8]) × 1e-7)
```

乘数为 `0x147124CA8` 处的 double `1e-7`，减法和缩放指令位于 `0x1415ADEB4…0x1415ADED2`。名称指向剩余时间用途；本次证据没有另行证明两个时间字段的具体命名。

每个通道仅在 `PerSeconds > 0` 时处理：

| 配置字段 | 本对象偏移 | 累加目标 |
| --- | --- | --- |
| `CustomSkillPowerBonusAddPerSeconds` | `0x30` | `instance+0x174` |
| `MaxCustomSkillPowerBonusAdd` | `0x34` | 上一通道的有符号上限 |
| `CustomSkillPowerBonusRatePerSeconds` | `0x38` | `instance+0x178` |
| `MaxCustomSkillPowerBonusRate` | `0x3C` | 上一通道的有符号上限 |

原生顺序为 float32 乘 `PerSeconds × t`，随后执行：

```asm
addss    xmm1, xmm1
addss    xmm1, [float32 0.5]
cvtss2si eax, xmm1
sar      eax, 1
```

接着与上限作有符号比较、取较小整数，最后 `add` 到实例对应字段。这里保留实际取整指令，不在未检查 MXCSR 舍入模式及边界行为前称为普通四舍五入。构造器默认上限为 Add `100000`、Rate `10000`，具体资产可以覆盖。

样本 `SD_Kazan_Spear_Com_PhantomCross.json` 的 `OnAttackBeforeServer` 事件带 SelfSpell 条件，配置 RatePerSeconds `67`、MaxRate `2000`、MaxAdd `0`；同一资产还有 `SkillPowerBonus=474126`、`StaminaAttackRate=20800`。这些是不同字段，不应相互替换或合并为一个“技能倍率”。

## 技能精力消耗来自动作事件，并经过原生修正

技能 SI 资料中的 `StaminaConsumeValue` 不是所有动作分支最终扣费的充分来源。SB 动作状态的事件函数可以携带独立消耗。

已核对样本：

```text
BBQ/Content/_Kazan_/Design/Kazan/Skill/DualAxeSword/Style/Flow/BreakSlash/
  SB_Kazan_DualAxeSword_Flow_BreakSlash.json
```

其 Step1 使用 `AC_…BreakSlash_Charge`，事件引用 `xxConsumeStaminaRegainFunc_1`，原始消耗 `249`；Step2 使用 `AC_…BreakSlash_Normal`，事件引用 `xxConsumeStaminaRegainFunc_0`，原始消耗 `149`。对应 SI 中的原始值为 `1`。展示应保留动作/事件关联，不应把 SI 的 `1` 标成最终扣费，也不应未经状态流分析就把所有分支相加。

`xxConsumeStaminaRegainFunc` 的原生调用链：

| 定位层 | VA |
| --- | --- |
| 类名字符串 | `0x14702B25A` |
| static class 初始化入口 | `0x141A152A0` |
| 构造器 | `0x141A1C500` |
| vtable | `0x147036DB8` |
| vtable `+0x2B0` 实现 | `0x1415BF6E0` |
| 消耗核心函数 | `0x141013250`（后续区段至 `0x1410135D5`） |

`StaminaConsumeRegainInfo` 位于函数对象 `+0x30`，配置值在 `+0x34`。事件实现只在值为正时走此消耗路径，复制结构，并用类型码 `5` 调用 helper `0x1416A26E0` 获取技能修正参数，再传入核心函数。

### 核心函数已确认的请求量

为避免替未知偏移命名，定义：

- `V`：`int32[info+4]`，原始配置消耗。
- `R`：manager `+0x3D8` 的 float 加上由武器/动作类别索引取得的 float；类别 map 在 manager `+0x388` 附近。
- `S`：调用者传入的 float 技能修正。
- `H`：manager `+0x63C` 的 float，业务名称尚未映射。

核心计算可写为以下局部公式，但保留原生 float32/double 转换顺序：

```text
r1 = max(-1, R - S)

满足 info+9、角色标志及类别检查时：
    C  = float32(status(0x19) × float32(0.0001))
    r2 = max(-1, r1 - C)
其他情况：
    r2 = r1

requestedAmount = trunc_toward_zero(
    double(V) × (1 + r2) × (1 + double(H) × double(0.0001))
)
```

条件分支还要求 actor `+0x9A8` 的 bit 0，并检查类别为 `4` 或 `7`；在没有完成枚举映射前不将它们命名为特定动作。两次 `max(-1, …)` 是顺序执行的，不能任意合并后再截断。

| 关键操作 | VA |
| --- | --- |
| 减技能修正、第一次下限 | `0x1410133E8`、`0x1410133EC` |
| 读取状态 `0x19` | `0x141013407…0x14101340C` |
| 条件减项、第二次下限 | `0x141013423`、`0x141013427` |
| 加 `1`、乘原值 | `0x14101343E`、`0x141013455` |
| `H × 0.0001`、加 `1`、乘入 | `0x14101344D`、`0x141013459`、`0x141013461` |
| `cvttsd2si`，向零截断为整数请求 | `0x141013475` |
| 对状态码 `6` 调用 modifier `0x141013020` | `0x14101347D` |

常数地址：float32 `0.0001` 在 `0x147124B60`，double `0.0001` 在 `0x147124D18`，double `1` 在 `0x147124E30`，double `-1` 在 `0x147125200`。最终函数返回扣除前后状态差值，实际扣费还可能受零下限和状态规则约束，不能把请求量恒等于实际扣费。界面显示单位也必须单独核实；`UIConstants.DamageDisplayMultiplier=0.1` 只说明命名对应的伤害显示配置，不能据此推定精力单位。

其他消耗函数如 `xxConsumeStaminaTimeFunc` 具有每秒值和时间上限，应展示为持续时间相关配置；只有确定事件持续时间、状态路径与修正后，才适合计算总消耗。

## 网站可采用的结论与仍待确认的机制

可提升为 `code_derived` 的范围：上述确定的字段映射、同字段累加/移除、剩余时间修正的计算顺序，以及精力消耗核心的局部请求量公式。资产中具体配置及事件关联应继续标记为数据提取证据，而非实测最终伤害。

目前不能据此发布为确定公式的内容：

- `SkillPowerBonus`、`StaminaAttackRate` 到最终生命/精力伤害的完整换算、分母和取整顺序。
- 武器攻击、技能威力、面板属性和各类 Buff 的最终组合顺序；哪些不同修正来源共享加算区、哪些属于独立乘区。
- 暴击、背击、破防、招架及敌人防御/元素抗性的完整作用方式。敌人抗性原值如 `-400/0/500/666/990` 不能直接按字段外观转成减伤百分比。
- helper `0x1416A26E0` 对技能等级的具体转换；开启 `bApplySkillLevel` 后原始难度 Buff 值的最终效果。
- SB 事件的运行次数、互斥分支和持续时长，以及实际扣费的显示单位。

`SkillPowerBonusRate` 的其他 double 反射属性曾出现在 `xxBalanceDamageInfo`/调试平衡显示结构中；这些属性不是已确认的全局换算常数，不能用来补齐上述未知公式。完整伤害模拟器应继续把未确认的量分开显示，避免以看似精确的最终数字掩盖缺失证据。
