Files
template/docs/intermediate/enemy-calculator.md
T
AncTe 5c77676bca docs(10-11): 用户修订循环 batch2(代码讲解/修正)
按用户领域视角修订循环 batch 2(U-12/U-13/U-16/U-17/U-18/U-19)修正战斗各篇:

- U-12/U-16:转换器 / 视图 / 效果 / 计算器 / 脚本 / 钩子代码块补逐行中文注释,并用 [模板] / [需改] 标出可照抄与必须修改的部分
- U-13:新增「注册在哪里」,给出 initializeEnemy 函数体(无关行省略)与注册调用位置
- U-17:每篇明确「改内置实现」vs「写自己的类」的区别与各自效果
- U-18:enemy-map-damage 的「受地形遮挡的激光」示例替换为更简单的「八方向激光」(FaceGroup.Dir8)
- U-19:enemy-calculator 补「计算器必须无副作用」;enemy-status-hooks 的「下次战斗伤害乘二」改为钩子挂状态 + 计算器只读 + 战斗脚本消费

docs 8 篇;10-11-READER-REVIEW.md 追加 batch 2 记录。
2026-10-18 14:30:51 +08:00

143 lines
8.1 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.
# 伤害计算
属性算准之后,战斗还差最后一步换算:这些 `hp`、`atk`、`def` 到底意味着勇士会掉多少血、要打几回合?负责这件事的就是伤害计算器。
它实现的是 `f(enemy, hero)`:输入计算后的怪物与勇士属性,输出 `{ damage, turn }`。本篇导读内置公式,并说明要改公式时应该动哪里。整体流程见[战斗总览](./combat-overview.md)。
## 接口
`IDamageCalculator` 有两个方法:
- `calculate(handler)`:返回 `IEnemyDamageInfoBase`,即 `{ damage, turn }`;
- `getCriticalLimit(handler, attribute)`:返回指定勇士属性进行临界计算时的上界。
`handler` 是 `IReadonlyDamageHandler`,包含:
- `enemy`:计算后的怪物对象;
- `hero`:勇士属性对象;
- `context`:怪物上下文;
- `locator`:怪物位置;
- `state`:数据端状态对象。
注册方式:
```ts
state.enemyContext.getDamageSystem()?.useCalculator(new MainDamageCalculator());
```
`initializeEnemy` 在创建 `DamageSystem` 后就调用了 `useCalculator(new MainDamageCalculator())`,因此默认使用内置公式。
:::warning 计算器必须无副作用
计算器在一次战斗里会被**反复调用**:算临界值要二分十几次,渲染伤害数值、图鉴与提示等也可能在真正开打前各调用一次。因此它**必须只读输入、算出结果,不能改动任何外部状态**——不能写 flag、不能删 flag,也不能改怪物或勇士对象。只要存在副作用,同一只怪物在不同时刻就会算出不同结果,临界与显示都会错乱。需要「一次性」生效的状态,请放到别处去改(战斗脚本、钩子等),计算器只负责读取。
:::
### 改内置,还是自己写
- **改内置公式**:直接在 `src/data/enemy/calculator.ts` 的 `MainDamageCalculator.calculate` 里改公式或加分支。多数「调参类」属性(先攻、连击、破甲等)都实现在这里。
- **写自己的计算器**:实现 `IDamageCalculator` 接口,再用 `useCalculator` 替换。适合整条公式都要换、或不想改动内置文件时。
两者只能二选一:`useCalculator` 是**替换**当前计算器,选了自己的就会把内置的整个换掉,内置的那些分支需要你在自己的 `calculate` 里重新保留。
## 内置公式逐段导读
与其一次背下整条公式,不如按顺序看每一段在算什么。`MainDamageCalculator.calculate`(`src/data/enemy/calculator.ts`)按以下顺序计算:
1. **读取属性**:从 `hero` 读 `hp`、`atk`、`def`、`mdef`,从 `enemy` 读 `atk`、`def`、`hp`。
2. **无敌分支**:满足条件时直接返回 `{ damage: Infinity, turn: 0 }`,表示无法击败。
3. **勇士每回合伤害**:`atk - monDef`。如果小于等于 0,同样返回 `Infinity`。
4. **吸血**:按勇士生命值比例增加伤害,必要时把伤害加到怪物生命上。
5. **怪物每回合伤害**:`monAtk - def`;魔攻时忽略勇士防御;连击则乘以攻击次数。
6. **回合数**:`turn = ceil(monHp / heroPerDamage)`。
7. **支援**:怪物带有支援者时,递归计算每个支援者的伤害与回合数,并把它们累加到当前怪物上。
8. **先攻、破甲、反击、净化**:按各自规则增加伤害或每回合伤害。
9. **总伤害**:`damage += (turn - 1) * enemyPerDamage`,再减去魔防;未开启负伤时,负伤害归零。
10. **固伤、仇恨、自爆**:固伤与仇恨不受魔防影响;自爆把伤害抬到「勇士生命 - 1」。
11. **返回**:`damage` 向下取整,连同 `turn` 一起返回。
「加自己的属性加在哪一步」取决于你想改什么:
- 只是让怪物某项属性更高:不要改计算器,放进光环、查询或最终效果,让 `enemy.getAttribute(...)` 读到新值即可;
- 要让属性按回合数累积、按条件改变倍率、或增加新的伤害分支:在 `calculate` 的对应段落里改公式;
- 只是给勇士一侧加临时数值:用[勇士属性](./hero-attribute.md)里的修饰器,计算器会自动读到最终属性。
## 支援怪
支援怪部分的递归计算较绕:计算当前怪物时,会把每个支援者的回合数与伤害也加进来。初次阅读可以暂时跳过这一段,不影响自定义普通怪物的公式;等需要处理支援组合时再回来看。
## 示例:等差数列属性
有时公式本身要随回合变化,而光环、查询、最终效果都做不到这一点——它们改的是一次计算里的固定属性。这时就该动计算器。
「等差数列属性」指怪物攻击每回合按固定值递增。属性本身在一次计算中是常量,所以不能靠给 `atk` 加值实现;要在 `calculate` 里把「每回合伤害」按等差数列求和:
示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。下面用 `[模板]` 标出可原样照抄的行,用 `[需改]` 标出必须按需求修改的行。
```ts
// [模板] 依赖导入:类型分别来自 @user/data-common 与 @user/data-system
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
CriticalableHeroStatus,
IDamageCalculator,
IEnemyDamageInfoBase,
IReadonlyEnemyHandler
} from '@user/data-system';
// [需改] 类名:换成你自己的计算器名
export class ArithmeticCalculator implements IDamageCalculator<
IEnemyAttr,
IHeroAttr
> {
// [模板] calculate 签名固定:读 handler 里的 enemy 与 hero,返回 { damage, turn }
calculate(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): IEnemyDamageInfoBase {
// [模板] 取出只读的怪物与勇士对象(只读,供读取,不可改写)
const { enemy, hero } = handler;
// [模板] 勇士每回合对怪物造成的伤害 = 勇士攻击 - 怪物防御
const heroPerDamage =
hero.getFinalAttribute('atk') - enemy.getAttribute('def');
// [模板] 打不动(≤ 0)时返回 Infinity,表示无法击败
if (heroPerDamage <= 0) {
return { damage: Infinity, turn: 0 };
}
// [模板] 回合数 = 怪物血量 / 每回合伤害,向上取整
const turn = Math.ceil(enemy.getAttribute('hp') / heroPerDamage);
// [需改] 怪物每回合的基础伤害:这里按「怪物攻击 - 勇士防御」并最低取 0
const base = Math.max(
0,
enemy.getAttribute('atk') - hero.getFinalAttribute('def')
);
// [需改] 公差:每回合递增的伤害(本示例固定为 5)
const step = 5;
// [需改] 等差数列求和:首项 base,公差 step,共 turn 项
const damage = turn * base + (step * turn * (turn - 1)) / 2;
// [模板] 返回结果,damage 向下取整
return { damage: Math.floor(damage), turn };
}
// [模板] getCriticalLimit 的签名同样是固定的:这里直接返回勇士当前最终值
getCriticalLimit(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
): number {
return handler.hero.getFinalAttribute(attribute);
}
}
```
这是只保留本示例所需分支的最小计算器;内置公式里的无敌、吸血、支援等分支需要你按上文顺序自行保留。
## 临界
临界计算回答「勇士某项属性提高到多少时,战斗结果会发生变化」。相关接口:
- `getCriticalLimit(handler, attribute)`:返回该属性的计算上界;
- `calculateViewCritical(enemy, attribute, precision?)` 与 `calculateEnemyCritical(...)`:伤害系统上的临界计算方法,返回 `IEnemyCritical` 的生成器;
- `CriticalableHeroStatus<THero>`:只允许数值类型的勇士属性,例如 `atk`、`def`、`hp`。
内置 `getCriticalLimit` 对 `atk` 的处理是:怪物有坚固时返回 `Infinity`,否则返回 `def + hp`;其他属性直接返回勇士当前最终值。
现有约定是**伤害值随属性提高单调递减**。临界值唯一依赖「最后的伤害值」,因此公式不单调也能运行,只是算出的临界点不准。写自定义公式时尽量保持单调,临界结果才有意义。