mirror of
https://github.com/motajs/template.git
synced 2026-10-21 06:40:16 +08:00
- enemy-calculator:从「属性换算成伤害」引入,保留公式导读与临界约定 - enemy-combat-script:从「战斗前后的规则」引入,保留脚本/钩子选型表 - enemy-status-hooks:从「状态要反复生效」引入,强调多钩子协同
117 lines
4.9 KiB
Markdown
117 lines
4.9 KiB
Markdown
# 状态钩子
|
||
|
||
中毒、衰弱、重生这类状态有个共同点:它们不会在一次计算里结束,而是要在移动、战斗、切换楼层等时机反复生效。只靠一次属性调整实现不了,必须挂到这些时机上——这就是钩子(hook)。
|
||
|
||
而且这类状态往往不能只挂一个钩子。中毒要同时管正常走路与瞬移,重生要同时管战后与切层。钩子本身的用法见[钩子](./hooks.md),整体流程见[战斗总览](./combat-overview.md)。
|
||
|
||
## 为什么一个状态要用多个钩子
|
||
|
||
以中毒为例,它会从两个时机扣血:
|
||
|
||
- 勇士正常走一步时,在移动钩子的 `onStepEnd` 里扣血;
|
||
- 勇士通过寻路瞬移到目标点时,在寻路钩子的 `onAfterTeleportTo` 里按路径长度扣血。
|
||
|
||
只挂其中一个钩子,另一种移动方式就会漏掉中毒。重生也一样:战后要先记录位置,切层时再把怪物放回去,两个时机分别属于战斗钩子与楼层钩子。
|
||
|
||
## 三类内置状态
|
||
|
||
| 状态 | 使用的钩子 | 实现位置 | 行为 |
|
||
|------|------------|----------|------|
|
||
| 中毒 | `onStepEnd` + `onAfterTeleportTo` | `EnemyMoveHooks` / `EnemyPathfindingHooks` | 读取 `poison` flag,按移动扣血;瞬移时按路径长度处理 |
|
||
| 衰弱 | `onAfterCombat` + 修饰器 | `EnemyCombatHooks` | 战后取最高虚弱值,用 `PercentageModifier` 降低攻防 |
|
||
| 重生 | `onAfterCombat` + `onSetFloor` | `EnemyCombatHooks` / `EnemyChangeFloorHooks` | 战后记录 `rebirth` flag;切层时把记录的事件块放回 |
|
||
|
||
补充说明:
|
||
|
||
- 中毒的每步扣血由 `EnemyMoveHooks.handlePoison` 处理,移动钩子还会顺带调用 `handleMapDamage`;
|
||
- 衰弱在 `EnemyCombatHooks` 构造时向勇士的 `atk` 与 `def` 挂两个 `PercentageModifier`,战后通过 `setWeakValue` 设置负百分比;
|
||
- 重生的战后记录以楼层为键存入 `rebirth` flag,切换楼层时由 `EnemyChangeFloorHooks.onSetFloor` 读取并放回。
|
||
|
||
同一个 `onAfterCombat` 还处理诅咒、仇恨、退化等状态,因此新增状态时要先检查现有钩子是否已经覆盖了你的时机。
|
||
|
||
## 注册
|
||
|
||
这些钩子在 `initializeEnemy` 中注册:
|
||
|
||
```ts
|
||
state.hero.location.mover.addHook(new EnemyMoveHooks(state));
|
||
state.pathfinding.addHook(new EnemyPathfindingHooks(state));
|
||
state.combat.addHook(new EnemyCombatHooks(state));
|
||
state.hero.location.addHook(new EnemyChangeFloorHooks(state));
|
||
```
|
||
|
||
四个注册点分别对应:
|
||
|
||
- 勇士移动器:处理正常移动;
|
||
- 寻路系统:处理瞬移;
|
||
- 战斗流程:处理战前/战后;
|
||
- 勇士位置:处理切换楼层。
|
||
|
||
## 战斗流程中的时序
|
||
|
||
战斗钩子的执行顺序是:
|
||
|
||
1. 战前脚本 `before`;
|
||
2. `onBeforeCombat` 钩子;
|
||
3. 战后脚本 `after`;
|
||
4. `onAfterCombat` 钩子。
|
||
|
||
因此,想在「奖励结算之后」处理状态,应该用 `onAfterCombat`;想在战斗开始前拦截,应该用脚本的 `before`,见[战斗脚本](./enemy-combat-script.md)。
|
||
|
||
## 示例:下次战斗伤害乘二
|
||
|
||
钩子能改状态,但「伤害乘二」最终要由伤害公式来兑现。这个例子正好展示一个状态如何跨两个系统:战斗钩子负责「挂上状态」,伤害计算器负责「消费状态」。先定义一个无参数特殊属性(code 45):
|
||
|
||
```ts
|
||
// special.ts:45 - 蓄力
|
||
map.set(
|
||
45,
|
||
defineNonePropertySpecial(45, {
|
||
name: '蓄力',
|
||
desc: '战斗后,下一次战斗伤害翻倍。',
|
||
color: '#F99'
|
||
})
|
||
);
|
||
```
|
||
|
||
再写一个战斗钩子。它不修改内置的 `EnemyCombatHooks`,而是作为另一个钩子并存:
|
||
|
||
```ts
|
||
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
|
||
import { ICoreState } from '@user/data-state';
|
||
import { ICombatFlowHooks, IEnemyDamageInfo } from '@user/data-system';
|
||
|
||
export class ChargedHooks implements ICombatFlowHooks<IEnemyAttr, IHeroAttr> {
|
||
constructor(readonly state: ICoreState) {}
|
||
|
||
async onAfterCombat(
|
||
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>
|
||
): Promise<void> {
|
||
if (info.handler.enemy.hasSpecial(45)) {
|
||
this.state.flags.setFieldValue('nextBattleDouble', true);
|
||
}
|
||
}
|
||
}
|
||
|
||
state.combat.addHook(new ChargedHooks(state));
|
||
```
|
||
|
||
最后在伤害计算器里消费这个 flag:
|
||
|
||
```ts
|
||
const double = handler.state.flags.getFieldValueDefaults(
|
||
'nextBattleDouble',
|
||
false
|
||
);
|
||
if (double) {
|
||
damage *= 2;
|
||
handler.state.flags.deleteField('nextBattleDouble');
|
||
}
|
||
```
|
||
|
||
把这段逻辑放在 `calculate` 返回之前即可。伤害公式的其余部分见[伤害计算](./enemy-calculator.md)。
|
||
|
||
这个例子体现了「一个状态要用多个钩子/系统」:`onAfterCombat` 只负责记录状态,真正影响伤害的是计算器。如果只在钩子里改 flag 而不改计算器,状态不会有任何效果。
|
||
|
||
状态里用到的临时加减或百分比,优先用修饰器实现,概念与 `save` 参数见[勇士属性](./hero-attribute.md)。
|