# 战斗总览 在游戏里点一下怪物,勇士走过去、挥几刀,屏幕上出现伤害和回合数。对玩家来说这只是一瞬间,但在数据端,它背后有一条固定的流水线:先准备一份属性准确的怪物,再把双方的属性换算成伤害,最后跑一遍战前与战后的规则。 本篇沿着这条流水线走一遍,让你知道每一步在做什么、该去哪篇文档看细节。 ## 先准备一份「计算后怪物」 怪物数据里保存的是一份**模板**:基础属性(`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)。