Files
template/docs/intermediate/combat-overview.md
T
AncTe 8c5e8979e1 docs(10-11): 用户修订循环 batch1(战斗概念/说明)
- combat-overview/enemy-specials: 说明上下文重建时机(惰性、一般无需手动)
- enemy-final/aura/query: 补「既依赖又改外部」不提供的原因(循环依赖)
- enemy-specials: getSpecial<T> 泛型说明 + 特殊属性定义逐行注释与动态文案
- enemy-aura: 转换器/视图大白话,每条属性可对应不同转换器
- enemy-query: query() 必须调用 + 查询上下文改为只读 IReadonlyEnemyContext
- 追加 10-11-READER-REVIEW.md 用户循环 batch1 记录
2026-10-18 14:07:25 +08:00

100 lines
6.8 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` 等),加上若干特殊属性(先攻、连击、光环……)。它描述的是「这只怪物本来是什么样」。
可战斗不能直接拿模板来算。光环会改周围怪物的属性,查询会根据周围情况调整自己,最终效果还会在收尾时再改一次。如果直接拿模板去算伤害,这些影响就全丢了。
所以进入战斗前,怪物上下文会把每只怪物**重建**成一份属性已经算准的副本,这就是「计算后怪物」。重建的入口是:
```ts
state.enemyContext.buildup();
```
`buildup()` 会按固定顺序处理每只怪物,把所有特殊属性的作用真正写进属性里。之后伤害计算器读到的就是最终结果,而不是模板上的原始数字。
### 什么时候重建
重建的时机由怪物上下文自己掌握,而且是**惰性**的:真正读取某只怪物的计算后属性时,上下文才会检查它需不需要重算。因此日常流程里,你一般**不需要**手动重建:
- 放置或移除怪物、注册光环转换器 / 查询效果 / 最终效果、绑定勇士,都会自动把上下文标记为「需要更新」;
- `buildup()` 是全量重建的入口,会把所有怪物的光环、查询与最终效果重新算一遍;若上下文没有变化,它会直接跳过;
- `markDirty(view)` 只把某一只怪物标记为待重算,下次读取它时做局部刷新。
也就是说,「什么时候重建」大多由样板与引擎内部处理好了。只有当你绕过正常流程、直接改动了怪物对象(例如用可写接口拿到怪物后改属性),才需要自己对那只怪物调用 `markDirty(view)`,让改动生效。
### 为什么要重建
一句话:**为了拿到准确的怪物属性**。
模板上的 `atk` 只是怪物「自己写的」攻击力。实际战斗中,它可能被旁边的光环加成,也可能因为查询看到周围有多少同类而提高,还可能被最终效果改写。只有把这些作用全部算进去,伤害计算才可能准确。
重建还有一个好处:每次重建都从模板的原始值出发,所以反复 `buildup()` 不会让加成越叠越多。光环、查询与最终效果改的始终是那份计算后副本,模板本身保持不变。
### 为什么分四步
重建不是一次算完的,而是分成四个阶段。原因很简单:**不同阶段要处理的事情不一样**。有的阶段要先决定「怪物拥有哪些特殊属性」,有的阶段才能去改基础属性,还有的阶段需要看周围的怪物。
1. **光环与特殊查询**:先把特殊属性转换成光环视图,按优先级执行会修改特殊属性的效果,确定每只怪物最终拥有哪些特殊属性。
2. **普通光环**:执行修改基础属性的光环效果。
3. **常规查询**:允许读取上下文(周围怪物、位置等),并修改自身基础属性。
4. **最终效果**:不再查询上下文,只对自身基础属性做收尾调整。
可以把它想成装配一件装备:先确认要装哪些零件(第 1 步),再装主体(第 2 步),接着根据使用环境微调(第 3 步),最后做一次收尾检查(第 4 步)。顺序不能随便换,因为后面的步骤要建立在前面步骤的结果上。
前三步都可能受优先级影响;光环与查询共用同一套优先级,见[怪物光环](./enemy-aura.md)与[怪物查询](./enemy-query.md)。最终效果见[最终属性效果](./enemy-final.md)。
## 伤害大致怎么算
属性准备完之后,伤害计算器把它们换算成两个数字。一次战斗的伤害可以写成 `f(enemy, hero)`:`enemy` 是计算后的怪物对象,`hero` 是勇士属性对象。计算器读取双方的 `hp`、`atk`、`def`、`mdef` 等属性,返回:
- `damage`:本次战斗勇士会损失的生命值;
- `turn`:战斗持续多少回合。
数据端不逐帧模拟战斗,只根据公式给出这两个值。公式的实现与替换见[伤害计算](./enemy-calculator.md)。
## 战斗流程
把上面几步连起来,就是一次完整的战斗:
```mermaid
graph TD
A[怪物基础数据] --> B[光环 / 查询 / 最终]
B --> C[伤害计算 f(enemy, hero)]
C --> D[战前脚本 before]
D -->|返回 false| X[放弃战斗]
D -->|返回 true| E[战斗结算 damage / turn]
E --> F[战后脚本 after]
F --> G[战后钩子与状态钩子]
```
- **基础数据**:怪物模板的属性与特殊属性,来自怪物数据;
- **光环 / 查询 / 最终**:由 `enemyContext.buildup()` 生成计算后怪物;
- **伤害计算**:`IDamageCalculator.calculate` 返回 `{ damage, turn }`;
- **战前脚本**:`ICombatScript.before` 返回 `false` 时放弃战斗;
- **战斗结算**:按 `damage` 扣血,按 `turn` 记录回合数;
- **战后脚本**:`ICombatScript.after` 结算经验、金币等内容;
- **战后钩子与状态钩子**:处理中毒、衰弱、重生等持续状态。
战斗流程本身由 `state.combat`(`ICombatFlow`)驱动,`battle(view)` 返回本次战斗的伤害信息。脚本见[战斗脚本](./enemy-combat-script.md),状态钩子见[状态钩子](./enemy-status-hooks.md)。
## 流程步骤与对应文档
| 步骤 | 做什么 | 对应文档 |
| --------------- | -------------------------------- | ------------------------------------ |
| 特殊属性 | 定义怪物的机制与参数 | [特殊属性](./enemy-specials.md) |
| 光环 | 影响周围怪物的基础属性或特殊属性 | [怪物光环](./enemy-aura.md) |
| 查询 | 读取上下文后调整自身 | [怪物查询](./enemy-query.md) |
| 最终效果 | 只调整自身基础属性 | [最终属性效果](./enemy-final.md) |
| 伤害计算 | 实现 `f(enemy, hero)` | [伤害计算](./enemy-calculator.md) |
| 地图伤害 | 勇士移动时按位置结算伤害 | [地图伤害](./enemy-map-damage.md) |
| 战前 / 战后脚本 | 拦截战斗、结算奖励 | [战斗脚本](./enemy-combat-script.md) |
| 状态钩子 | 处理中毒、衰弱、重生等状态 | [状态钩子](./enemy-status-hooks.md) |
勇士一侧的属性读写见[勇士属性](./hero-attribute.md)。