- B-03: enemy-specials.md 新增「内置属性编号表」(0..26,逐条抄自当前 special.ts),表下 warning 诚实标注地图伤害转换器编号未对齐(以 special.ts 为准,不臆造对齐表) - B-03: enemy-map-damage.md 明确点出 code 18 在内置表=无敌、转换器=阻击的双重含义 - B-03: enemy-combat-script.md 的 code 18 示例注明 18=无敌,并说明转换器未对齐与本篇无关 - R2-01: 解释 @motajs/anon-tokyo 的来历(events.md + src-tour.md「包从哪来」) - R2-02: enemy-aura.md 补 ISpecial23Value 字段说明表 - R2-03: 补 initializeEnemy 一句话介绍(集中注册入口,内置/自定义同一流程) - R2-04: events.md 说明参数接口实际放入 src/data/event/types.ts - R2-05: hero-equipment.md 接口清单补 compareEquip - R2-06: enemy-map-damage.md code-43 示例补关键类型注解 - 追加第 2 轮复审与修正记录到 10-11-READER-REVIEW.md
4.9 KiB
战斗脚本
战斗前后常有一些规则要跑:有的怪物「不许打」,有的战斗要「打完回血」,还有的要在结算奖励前调整状态。这些规则不适合塞进伤害公式,而是在战斗流程上挂一段逻辑——这就是战斗脚本(combat script)。
战前脚本可以决定「这场战斗是否发生」,战后脚本负责结算奖励或修改状态。脚本与钩子的分工见下方选型表,整体流程见战斗总览。
接口
ICombatScript 有三个成员:
priority:优先级,越高越先执行;before(info, handler):战前执行,返回Promise<boolean>。返回false会立刻停止后续战前脚本,并放弃此次战斗;after(info, handler):战后执行,返回Promise<void>。
info 是 IEnemyDamageInfo,包含 damage、turn 与 handler;handler 是 ICombatFlowHandler,包含:
onMap:怪物是否在地图上;force:是否为强制战斗;hero:可修改的勇士属性对象;enemy:怪物对象;context:怪物上下文;locator:怪物位置。
注册方式:
state.combat.addCombatScript(new MainCombatScript());
同优先级的脚本不能重复注册:addCombatScript 遇到相同 priority 会告警并忽略后一个脚本。内置的 MainCombatScript 优先级为 0。
内置脚本
MainCombatScript(src/data/enemy/script.ts)做两件事:
- 战前:非强制战斗时,如果勇士当前生命小于本次伤害,返回
false,阻止战斗; - 战后:把怪物与支援者的经验、金币加到勇士身上;
curseflag 为true时不加;最后发出一次全局提示。
脚本 vs 钩子
战斗前后能做事的除了脚本,还有钩子。两者的区别主要在「能不能改变战斗的走向」和「多个监听者如何共存」:
| 需求 | 用脚本 | 用钩子 |
|---|---|---|
| 决定这场战斗是否发生 | 是,before 返回 false |
否,onBeforeCombat 不能阻止战斗 |
| 按固定顺序结算奖励或回血 | 是,用 priority 排序 |
顺序不由优先级控制 |
| 只响应战斗结果做副作用 | 可以,但不是必须 | 是,onAfterCombat 更直接 |
| 多个互不影响的监听者 | 不适合,同优先级会冲突 | 适合,多个钩子可以并存 |
| 需要和内置脚本一样参与战前拦截 | 是 | 否 |
简单记:要拦截或按顺序结算,用脚本;只想在战斗前后做附加动作,用钩子。
战前示例:拥有某特殊属性时无法战斗
下面的脚本检查怪物是否拥有 code 18 的特殊属性(无敌,编号见特殊属性的「内置属性编号表」),有则直接放弃战斗(示例里的 @motajs/*、@user/* 是包名,含义见 src 导览 的「包从哪来」):
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
ICombatFlowHandler,
ICombatScript,
IEnemyDamageInfo
} from '@user/data-system';
export class SealCombatScript implements ICombatScript<IEnemyAttr, IHeroAttr> {
readonly priority = 100;
async before(
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>,
handler: ICombatFlowHandler<IEnemyAttr, IHeroAttr>
): Promise<boolean> {
if (handler.enemy.hasSpecial(18)) {
handler.state.emitTip('这个怪物无法被击败。');
return false;
}
return true;
}
async after(): Promise<void> {}
}
state.combat.addCombatScript(new SealCombatScript());
priority = 100 让它先于内置脚本执行;返回 false 后,后续战前脚本、战后脚本与 onAfterCombat 钩子都不会执行。
这里的 18 号在内置属性编号表里就是「无敌」。地图伤害转换器把 18 读作「阻击」是它自身的编号未对齐(见地图伤害),与本篇的判定无关。
战后示例:战后玩家回复固定量生命值
下面的脚本在每场战斗后给勇士回复 20 点生命:
import { IEnemyAttr, IHeroAttr } from '@user/data-common';
import {
ICombatFlowHandler,
ICombatScript,
IEnemyDamageInfo
} from '@user/data-system';
export class HealAfterCombatScript implements ICombatScript<
IEnemyAttr,
IHeroAttr
> {
readonly priority = -10;
async before(): Promise<boolean> {
return true;
}
async after(
info: IEnemyDamageInfo<IEnemyAttr, IHeroAttr>,
handler: ICombatFlowHandler<IEnemyAttr, IHeroAttr>
): Promise<void> {
handler.hero.add('hp', 20);
}
}
state.combat.addCombatScript(new HealAfterCombatScript());
priority = -10 让它在内置脚本之后执行,避免在奖励结算之前改动勇士状态。handler.hero 是可修改对象,直接 add 即可。
priority 只需要记住一句:数值越大越先执行;战前与战后各自按这个顺序遍历脚本列表。