docs(10-11): 按 D-51 微调特殊属性落点与讲解方式

- 特殊属性实现写进已有文件(aura.ts 等),仅查询属性新建 query.ts
- aura.ts / query.ts 建议用 //#region {code}-{name} 包裹
- 通俗解释「副作用」;地图伤害按计算后怪物计算,光环加的特殊属性会真实生效
- 依赖并修改外部环境应拆成查询 + 光环、用新怪物属性桥接(指向高级教程)
- getCriticalLimit 提一句并指向高级教程;接口讲解改为「为了实现 X,改 Y 接口」
- 追加 batch 10 读者审阅记录
This commit is contained in:
AncTe committed 2026-10-19 19:28:18 +08:00
1 parent a50210aec2
commit e64167b7c5
9 files changed
+85 -42

No files matched your search

+14 -4
View File
@@ -4,7 +4,7 @@
光环的效果作用在范围内的其他怪物身上(也可以包括自己)。它要回答两个问题:先算谁、后算谁?改基础属性还是改特殊属性?下面先从优先级讲起,再看内置光环、怎么改它们,最后才是自己写类。基础概念见[战斗总览](./combat-overview.md)。
光环属于「不读取外部环境、但会修改外部」的那一类:它只按范围命中目标,不会先观察环境再决定自己。(为什么没有「既读取外部、又修改外部」的能力,见[最终属性效果](./enemy-final.md)。)
光环属于「不读取外部环境、但会修改外部」的那一类:它只按范围命中目标,不会先观察环境再决定自己。(如果某种属性确实既要读取外部、又要修改外部,应拆成查询与光环两个效果,见下文「光环、计算后怪物与『读写外部』」。)
## 优先级
@@ -72,6 +72,14 @@
下方示例里的 `@motajs/*`、`@user/*` 是包名,含义见 [src 导览](./src-tour.md) 的「包从哪来」。
## 光环、计算后怪物与「读写外部」
光环的 `applySpecial` 改的是**计算后怪物**(`getComputedEnemy()`):构建完成后,地图伤害等系统读到的就是这份结果。所以光环给目标新增的特殊属性会真实存在——例如光环给一只怪物加上「领域」(code 13),这只怪物就确实拥有领域效果,而不只是显示上多了一行。旧样板无法动态给怪物添加特殊属性,这一点是现版本才有的能力。
也正因为光环负责「改外部」,它不应再去读外部环境。如果一种属性既要依赖外部环境、又要修改外部环境,直接写就会陷入「先算谁」的循环依赖;正确做法是拆成两个效果:先用**查询**读取外部、改自己身上的一个新增标记属性,再用**光环**根据这个标记属性去影响其他怪物。两者靠这个新怪物属性桥接起来。具体写法会在**高级教程**中讲解(预留标记:`高级教程 · 依赖并修改外部环境的属性`),待高级教程就位后替换为链接。
<!-- TODO(advanced): 依赖并修改外部环境的属性 -->
## 改内置:调整内置光环的行为
内置三类光环都写在 `src/data/enemy/aura.ts`,而且已经在 `initializeEnemy` 里注册好了。所以**改内置就是在这些类的现有方法里加分支、改参数**:不用新建类,也不用再调用 `registerAuraConverter`。这是本篇的主线做法,绝大多数需求都能这样完成。
@@ -183,7 +191,9 @@ apply(
### 模板:复制什么、改什么
这是光环机制本身的骨架,不含任何具体效果。把它复制到 `src/data/enemy/` 下新建的文件里,再按 `[需改]` 改成你的效果。
这是光环机制本身的骨架,不含任何具体效果。把它复制到 `src/data/enemy/aura.ts` 末尾,再按 `[需改]` 改成你的效果。**不要另建文件**:光环的实现集中在这个文件里,便于统一管理,也方便后续编辑器识别。
新增的每条实现建议用 `//#region {code}-{name}` 包起来,以 `//#endregion` 结束,例如 `//#region 30-削弱光环`。内置的 23 / 24 / 26 就是这样组织的。
先写转换器——它只负责「认领 code」和「造出视图」:
@@ -279,7 +289,7 @@ export class XxxAura implements IEnemyAuraView<
**目标**:站在带「削弱光环」怪物周围的怪物,金币与经验按参数下降。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写转换器与视图;`src/data/enemy/initialize.ts` 注册(见文末「注册在哪里」)。
**改哪里**:`src/data/enemy/special.ts` 登记属性;转换器与视图写进 `src/data/enemy/aura.ts`(用 `//#region 30-削弱光环` 包住);`src/data/enemy/initialize.ts` 注册(见文末「注册在哪里」)。
**复制什么**:先在 `special.ts` 登记属性——
@@ -400,7 +410,7 @@ state.enemyContext.registerAuraConverter(new WeakAuraConverter());
**目标**:站在带「传染光环」怪物附近的怪物,若自己没有普通光环,就获得一层 code 23 的普通光环。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写转换器与视图;`src/data/enemy/initialize.ts` 注册(见文末「注册在哪里」)。
**改哪里**:`src/data/enemy/special.ts` 登记属性;转换器与视图写进 `src/data/enemy/aura.ts`(用 `//#region 31-传染光环` 包住);`src/data/enemy/initialize.ts` 注册(见文末「注册在哪里」)。
**复制什么**:先在 `special.ts` 登记属性——
+12 -15
View File
@@ -4,12 +4,11 @@
它实现的是 `f(enemy, hero)`:输入计算后的怪物与勇士属性,输出 `{ damage, turn }`。本篇导读内置公式,并说明要改公式时应该动哪里。整体流程见[战斗总览](./combat-overview.md)。
## 接口
## 伤害计算器接口
`IDamageCalculator` 有两个方法:
为了把计算后的怪物与勇士属性换算成「勇士会掉多少血、要打几回合」,我们需要实现 `IDamageCalculator` 接口。日常只需要关心它的 `calculate(handler)`:返回 `IEnemyDamageInfoBase`,即 `{ damage, turn }`。
- `calculate(handler)`:返回 `IEnemyDamageInfoBase`,即 `{ damage, turn }`;
- `getCriticalLimit(handler, attribute)`:返回指定勇士属性进行临界计算时的上界。
接口里还有一个 `getCriticalLimit(handler, attribute)`,只有临界计算会用到。大多数需求用不到它,先知道有这么个方法即可,细节留到高级教程。
`handler` 是 `IReadonlyDamageHandler`,包含:
@@ -28,6 +27,8 @@ state.enemyContext.getDamageSystem()?.useCalculator(new MainDamageCalculator());
`initializeEnemy` 在创建 `DamageSystem` 后就调用了 `useCalculator(new MainDamageCalculator())`,因此默认使用内置公式。
:::warning 计算器必须无副作用
所谓「副作用」,就是除了算出结果之外,还顺手改动了别的东西:写了一个 flag、删了一条记录、改了怪物或勇士的属性,这些都属于副作用。
计算器在一次战斗里会被**反复调用**:算临界值要二分十几次,渲染伤害数值、图鉴与提示等也可能在真正开打前各调用一次。因此它**必须只读输入、算出结果,不能改动任何外部状态**——不能写 flag、不能删 flag,也不能改怪物或勇士对象。只要存在副作用,同一只怪物在不同时刻就会算出不同结果,临界与显示都会错乱。需要「一次性」生效的状态,请放到别处去改(战斗脚本、钩子等),计算器只负责读取。
:::
@@ -121,7 +122,7 @@ calculate(
### 模板:复制什么、改什么
下面是计算器机制本身的骨架,不含任何具体公式。复制到新文件后按 `[需改]` 修改:
下面是计算器机制本身的骨架,不含任何具体公式。复制到 `src/data/enemy/calculator.ts` 末尾后按 `[需改]` 修改(不要另建文件,计算器的实现集中在 `calculator.ts`):
```ts
// [需改] 类名:换成你自己的计算器名
@@ -141,7 +142,7 @@ export class XxxCalculator implements IDamageCalculator<IEnemyAttr, IHeroAttr> {
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
): number {
// [需改] 需要自定义临界上界时改这里;否则返回勇士当前最终值即可
// [模板] 一般不用改:返回勇士当前最终值即可;高级教程会细讲临界
return handler.hero.getFinalAttribute(attribute);
}
}
@@ -197,7 +198,7 @@ export class ArithmeticCalculator implements IDamageCalculator<
return { damage: Math.floor(damage), turn };
}
// [模板] getCriticalLimit 的签名同样是固定的:这里直接返回勇士当前最终值
// [模板] getCriticalLimit 一般不用改:这里直接返回勇士当前最终值
getCriticalLimit(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
@@ -241,14 +242,10 @@ export function initializeEnemy(
把 `useCalculator(new ArithmeticCalculator())` 放在内置那一行之后,你的公式就会随 `initializeEnemy` 一起生效。
## 临界
## 临界(了解即可)
临界计算回答「勇士某项属性提高到多少时,战斗结果会发生变化」。相关接口:
临界计算回答「勇士某项属性提高到多少时,战斗结果会发生变化」,它会用到 `getCriticalLimit`。多数需求不必改这个方法:内置实现已经处理了 `atk`(怪物有坚固时返回 `Infinity`,否则返回 `def + hp`)与其他属性的默认上界。
- `getCriticalLimit(handler, attribute)`:返回该属性的计算上界;
- `calculateViewCritical(enemy, attribute, precision?)` 与 `calculateEnemyCritical(...)`:伤害系统上的临界计算方法,返回 `IEnemyCritical` 的生成器;
- `CriticalableHeroStatus<THero>`:只允许数值类型的勇士属性,例如 `atk`、`def`、`hp`。
现有约定是**伤害值随属性提高单调递减**。公式不单调也能运行,只是算出的临界点不准。更完整的临界算法与二分过程会在**高级教程**中讲解(预留标记:`高级教程 · 临界计算`),待高级教程就位后替换为链接。
内置 `getCriticalLimit` 对 `atk` 的处理是:怪物有坚固时返回 `Infinity`,否则返回 `def + hp`;其他属性直接返回勇士当前最终值。
现有约定是**伤害值随属性提高单调递减**。临界值唯一依赖「最后的伤害值」,因此公式不单调也能运行,只是算出的临界点不准。写自定义公式时尽量保持单调,临界结果才有意义。
<!-- TODO(advanced): 临界计算 -->
+5 -5
View File
@@ -4,9 +4,9 @@
战前脚本可以决定「这场战斗是否发生」,战后脚本负责结算奖励或修改状态。脚本与钩子的分工见下方选型表,整体流程见[战斗总览](./combat-overview.md)。
## 接口
## 战斗脚本接口
`ICombatScript` 有三个成员:
为了让一段逻辑在战斗开始前决定「打不打」、在战斗结束后做结算,我们需要实现 `ICombatScript` 接口。它有三个成员:
- `priority`:优先级,越高越先执行;
- `before(info, handler)`:战前执行,返回 `Promise<boolean>`。返回 `false` 会立刻停止后续战前脚本,并放弃此次战斗;
@@ -143,7 +143,7 @@ async after(
### 模板:复制什么、改什么
下面是战斗脚本机制本身的骨架,不含任何具体规则。复制到新文件后按 `[需改]` 修改:
下面是战斗脚本机制本身的骨架,不含任何具体规则。复制到 `src/data/enemy/script.ts` 末尾后按 `[需改]` 修改(不要另建文件,脚本的实现集中在这个文件里):
```ts
// [需改] 类名:换成你的脚本名
@@ -184,7 +184,7 @@ state.combat.addCombatScript(new XxxCombatScript());
**目标**:带「无敌」(code 18)的怪物不能被打,点它时直接放弃战斗并给出提示。
**改哪里**:另建文件写这个脚本类;`src/data/enemy/initialize.ts` 用 `addCombatScript` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/script.ts` 里写这个脚本类;`src/data/enemy/initialize.ts` 用 `addCombatScript` 注册(见文末「注册在哪里」)。
**复制什么**:
@@ -232,7 +232,7 @@ state.combat.addCombatScript(new SealCombatScript());
**目标**:每场战斗结束后,给勇士回复固定量生命(本示例 20 点)。
**改哪里**:另建文件写这个脚本类;`src/data/enemy/initialize.ts` 用 `addCombatScript` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/script.ts` 里写这个脚本类;`src/data/enemy/initialize.ts` 用 `addCombatScript` 注册(见文末「注册在哪里」)。
**复制什么**:
+7 -3
View File
@@ -4,9 +4,9 @@
它适合实现「只看自己(和勇士)就能决定」的转换,例如坚固、模仿。整体流程见[战斗总览](./combat-overview.md)。
## 接口
## 最终效果接口
`IEnemyFinalEffect` 只有两个成员:
为了在外部影响都算完后对怪物自己做一次收尾,我们需要实现 `IEnemyFinalEffect` 接口。它只有两个成员:
- `priority`:优先级,越高越先执行;
- `apply(handler)`:读取 `handler.enemy` 与 `handler.hero`,修改 `handler.enemy` 的基础属性。
@@ -100,6 +100,10 @@ apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
所以上表里「读取外部」与「影响外部」同时为「是」的那一格被刻意留空:需要「读外部之后改变自己的结果」,就用查询;需要「影响其他怪物」,就用光环。两者分开、按固定阶段执行,循环依赖就不会出现。
如果一种属性确实同时需要这两件事,不要试图做一个混合效果,而是把它拆成**查询效果 + 光环效果**:查询读取外部、改自己身上的一个新增标记属性,光环再根据这个标记属性去影响其他怪物。两者靠这个新怪物属性桥接。具体写法会在**高级教程**中讲解(预留标记:`高级教程 · 依赖并修改外部环境的属性`),待高级教程就位后替换为链接。
<!-- TODO(advanced): 依赖并修改外部环境的属性 -->
## 自己写最终效果类
只有两种情况需要自己写类:一是效果要按自己的节奏独立执行(比如想单独控制 `priority`、让它在内置效果之前跑完),二是你不打算改动内置文件。
@@ -112,7 +116,7 @@ apply(handler: IEnemyHandler<IEnemyAttr, IHeroAttr>): void {
### 模板:复制什么、改什么
下面是最终效果机制本身的骨架,不含任何具体效果。复制到新文件后按 `[需改]` 修改:
下面是最终效果机制本身的骨架,不含任何具体效果。复制到 `src/data/enemy/final.ts` 末尾后按 `[需改]` 修改(不要另建文件,最终效果的实现集中在这个文件里):
```ts
// [需改] 类名:换成你自己的效果名
+8 -2
View File
@@ -18,6 +18,12 @@
伤害信息 `IMapDamageInfo` 包含 `damage`(伤害值)、`type`(伤害类型)与 `extra`(额外信息)。`extra.catch` 记录需要触发强制战斗的怪物位置,`extra.repulse` 记录需要后退的阻击怪物位置。
## 按「计算后怪物」计算
转换器拿到的 `handler.enemy` 是**计算后怪物**(`getComputedEnemy()`),也就是光环、查询、最终效果都算完后的结果。因此光环通过 `applySpecial` 给怪物新增的特殊属性会真实生效:例如某个光环给一只怪物加了「领域」(code 13),这只怪物就确实拥有领域地图伤害,勇士踩进范围会掉血。
旧样板做不到动态给怪物添加特殊属性,这一点是现版本才有的能力。反过来说,如果一种属性既要读外部环境、又要修改外部环境,不要硬塞进地图伤害或单个光环里,而应拆成「查询效果 + 光环效果」,再用一个新怪物属性桥接(见[怪物光环](./enemy-aura.md)的「光环、计算后怪物与『读写外部』」)。
## 注册
地图伤害管理器由 `state.enemyContext.getMapDamage()` 取得。注册方式:
@@ -125,7 +131,7 @@ convert(
### 模板:复制什么、改什么
自己写地图伤害永远是「一个视图 + 一个转换器」两个类。下面是机制本身的骨架,不含任何具体伤害;复制到新文件后按 `[需改]` 修改。
自己写地图伤害永远是「一个视图 + 一个转换器」两个类。下面是机制本身的骨架,不含任何具体伤害;复制到 `src/data/enemy/mapDamage.ts` 末尾后按 `[需改]` 修改(不要另建文件,地图伤害的实现集中在这个文件里)。
先写视图——它负责「范围、某点的伤害与类型」:
@@ -211,7 +217,7 @@ mapDamage?.useConverter(new XxxConverter());
**目标**:新增一个「八方向激光」特殊属性(示例 code 43),沿上下左右加四个斜向共八个方向发射,且不影响内置的四方向激光。
**改哪里**:`src/data/enemy/special.ts` 登记属性;另建文件写伤害视图与转换器;`initializeEnemy` 里用 `useConverter` 替换(见下方「注册在哪里」)。
**改哪里**:`src/data/enemy/special.ts` 登记属性;伤害视图与转换器写进 `src/data/enemy/mapDamage.ts`;`initializeEnemy` 里用 `useConverter` 替换(见下方「注册在哪里」)。
**复制什么**:先在 `src/data/enemy/special.ts` 登记这条属性——
+8 -8
View File
@@ -10,7 +10,7 @@
## 常规查询(CommonQuery)
如果一条特殊属性只需要「读外部状态,然后改自己的基础属性」,用常规查询就够了。它的效果是一个实现了 `IEnemyCommonQueryEffect` 的类实例,注册方式:
如果一条特殊属性只需要「读外部状态,然后改自己的基础属性」,用常规查询就够了。为了实现它,我们需要实现 `IEnemyCommonQueryEffect` 接口:写一个类,让引擎在构建计算后怪物时调用它的 `apply`。写好后这样注册:
```ts
state.enemyContext.registerCommonQueryEffect(code, new GroupQueryEffect());
@@ -27,7 +27,7 @@ state.enemyContext.registerCommonQueryEffect(code, new GroupQueryEffect());
## 特殊查询(SpecialQuery)
如果查询要动的是目标怪物**拥有的特殊属性**——添加、删除或改写——就用特殊查询。例如给范围内的怪物补上一条属性,或把它们身上的数值属性叠加起来。注册方式:
如果查询要动的是目标怪物**拥有的特殊属性**——添加、删除或改写——就用特殊查询。例如给范围内的怪物补上一条属性,或把它们身上的数值属性叠加起来。为了实现它,我们需要实现 `IEnemySpecialQueryEffect` 接口,并配一个修饰器类来处理增删改。注册方式:
```ts
state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
@@ -55,7 +55,7 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
常规查询与特殊查询的 `priority` 和光环共用同一套优先级,从高到低处理。高优先级的效果先执行,低优先级能看到它留下的结果;反过来不行。查询新增的光环只能影响之后的阶段,规则与[怪物光环](./enemy-aura.md)一致。
查询属于「依赖外部、但只改自己」的那一类;「既依赖外部、又修改外部」的能力是刻意不提供的,原因见[最终属性效果](./enemy-final.md)。
查询属于「依赖外部、但只改自己」的那一类。如果一种属性确实同时需要「依赖外部」与「修改外部」,不要把它硬做成一种查询或一种光环,而应拆成查询 + 光环两个效果,再用一个新怪物属性桥接(见[怪物光环](./enemy-aura.md)的「光环、计算后怪物与『读写外部』」)。
## 查询上下文 ctx
@@ -71,7 +71,7 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
查询效果在引擎里**没有可直接修改的内置实现**:`initializeEnemy` 中只注册了光环转换器、最终效果、战斗脚本与钩子,没有注册任何查询效果。要新增一条查询,需要做两件事:
1. 在 `src/data/enemy/` 下新建一个文件,按下面的示例写一个**类**,让它实现对应接口;
1. 在 `src/data/enemy/query.ts` 里按下面的示例写一个**类**,让它实现对应接口。查询属性是**唯一**需要新建文件的一类:光环、最终效果、地图伤害等都写进各自的现成文件,不要另建;
2. 在 `initializeEnemy` 里 `new` 出这个类并注册(见文末「注册在哪里」)。
三个示例都是这种写法:先定义类,再在注册时 `new` 一次。查询效果只需要一个实例,注册一次即可。
@@ -85,7 +85,7 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
## 模板:复制什么、改什么
查询分两种,模板也分两种。下面是机制本身的骨架,不含任何具体效果;复制到 `src/data/enemy/` 下的新文件后,按 `[需改]` 改成你的查询。
查询分两种,模板也分两种。下面是机制本身的骨架,不含任何具体效果;复制到 `src/data/enemy/query.ts` 后,按 `[需改]` 改成你的查询。每条查询建议用 `//#region {code}-{name}` 包起来,以 `//#endregion` 结束,例如 `//#region 40-抱团`。
### 常规查询模板
@@ -203,7 +203,7 @@ state.enemyContext.registerSpecialQueryEffect(new XxxSpecialQueryEffect());
**目标**:带「抱团」(示例 code 40)的怪物,范围内每多一个同种特殊属性的怪物,自身攻击加固定值。
**改哪里**:在 `src/data/enemy/` 下另建一个文件(如 `query.ts`),写下面这个类;再到 `src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/query.ts` 里写下面这个类(用 `//#region 40-抱团` 包住);再到 `src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
@@ -268,7 +268,7 @@ state.enemyContext.registerCommonQueryEffect(40, new GroupQueryEffect());
**目标**:带「同化」(示例 code 41)的怪物,把其他怪物的特殊属性搬到自己身上(没有的照抄,数值型的叠加)。
**改哪里**:在 `src/data/enemy/` 下另建一个文件(如 `query.ts`),写下面这个类;再到 `src/data/enemy/initialize.ts` 里用 `registerSpecialQueryEffect` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/query.ts` 里写下面这个类(用 `//#region 41-同化` 包住);再到 `src/data/enemy/initialize.ts` 里用 `registerSpecialQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
@@ -367,7 +367,7 @@ state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
**目标**:带「号角」(示例 code 42)的怪物,场上每有一只怪物就给自身加一份攻击。
**改哪里**:在 `src/data/enemy/` 下另建一个文件(如 `query.ts`),写下面这个类;再到 `src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/query.ts` 里写下面这个类(用 `//#region 42-号角` 包住);再到 `src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
+3 -3
View File
@@ -46,7 +46,7 @@
## 在数据里的样子
数据端的怪物对象(`IReadonlyEnemy`)提供三个读取接口:
为了读取怪物身上的特殊属性,我们需要用到数据端怪物对象(`IReadonlyEnemy`)上的三个接口:
- `getSpecial<T>(code)`:取指定 code 的特殊属性对象,没有则返回 `null`;
- `hasSpecial(code)`:判断是否拥有指定特殊属性;
@@ -148,7 +148,7 @@ map.set(
## 改内置:往属性表里加一条属性
本篇的主线是**改内置**。特殊属性没有「自己写一个类」的做法:这类数据本就登记在内置文件 `src/data/enemy/special.ts` 里,新增一条属性的方式就是**往这个文件里加一段 `map.set(...)`**(即下面六步)。真正让它生效的代码,则按类型写在各内置实现文件里(见下方「属性实现位置」表)——这也是本篇与光环 / 查询那两篇最大的不同:那两边需要新建类,这里只需要改内置文件。
本篇的主线是**改内置**。特殊属性没有「自己写一个类」的做法:这类数据本就登记在内置文件 `src/data/enemy/special.ts` 里,新增一条属性的方式就是**往这个文件里加一段 `map.set(...)`**(即下面六步)。真正让它生效的代码,则按类型写在各内置实现文件里(见下方「属性实现位置」表)——这也是本篇与光环 / 查询那两篇最大的不同:那两边需要新增类(光环写进 `aura.ts`,查询写进新建的 `query.ts`),这里只需要改内置文件。
需要新机制时,先看它能不能归入已有的分类(调参、坚固 / 模仿、地图伤害、光环、状态钩子)。能归入,就照下面六步加一条属性,再在对应的内置实现里加一个分支;这是绝大多数需求的做法。
@@ -264,6 +264,6 @@ map.set(
## 如果内置实现里实在找不到落点
上面六步加上「属性实现位置」表,覆盖了内置特殊属性的全部实现分工。只有当你的机制与这八类都不同构、且无法拆成「属性 + 已有实现」时,才需要自己写实现代码:在 `src/data/enemy/` 下新建一个文件,按对应接口写一个类,再在 `initializeEnemy` 里注册。这时你仍然要回到本篇的 `special.ts` 登记这条属性,区别只是生效逻辑换了地方。
上面六步加上「属性实现位置」表,覆盖了内置特殊属性的全部实现分工。只有当你的机制与这八类都不同构、且无法拆成「属性 + 已有实现」时,才需要自己写实现代码:按类型写进现有的实现文件(光环 → `src/data/enemy/aura.ts`,地图伤害 → `mapDamage.ts`,最终效果 → `final.ts`,脚本 / 钩子 → `script.ts` / `hook.ts`),再在 `initializeEnemy` 里注册。**不要为每条属性另建文件**;只有查询属性需要新建 `src/data/enemy/query.ts`。这时你仍然要回到本篇的 `special.ts` 登记这条属性,区别只是生效逻辑换了地方。
**如果你不怎么懂代码,建议直接改内置实现(`special.ts` 的属性定义 + 上表中的实现文件),而不是自己写新的类。** 自己写类要同时处理接口签名、注册时机与上下文刷新,改动面比在已有方法里加一个分支大得多,出错也更难排查。
+2 -2
View File
@@ -167,7 +167,7 @@ if (double) {
**如果你不怎么懂代码,建议直接改内置钩子(`src/data/enemy/hook.ts` 的四个 `Enemy*Hooks` 类),而不是自己写新的类。** 上面「改内置」一节已经覆盖了移动、瞬移、战后、切层四个时机;自写钩子意味着你要自己确认挂载点、优先级与状态校验。
做法是实现对应接口的类,构造时保存 `state`,再 `addHook` 到对应挂载点:
为了实现某个时机上的效果,我们需要实现对应钩子接口的类:构造时保存 `state`,再 `addHook` 到对应挂载点。四个时机对应的接口与挂载点是:
- 移动:`state.hero.location.mover.addHook(new ...Hooks(state))`,实现 `IObjectMoverHooks`;
- 寻路:`state.pathfinding.addHook(new ...Hooks(state))`,实现 `IPathfindingSystemHooks`;
@@ -176,7 +176,7 @@ if (double) {
### 模板:复制什么、改什么
下面是状态钩子机制本身的骨架(以战斗钩子为例),不含任何具体状态。复制到新文件后按 `[需改]` 修改:
下面是状态钩子机制本身的骨架(以战斗钩子为例),不含任何具体状态。复制到 `src/data/enemy/hook.ts` 末尾后按 `[需改]` 修改(不要另建文件,内置状态的钩子类集中在这个文件里):
```ts
// [需改] 类名与接口:按上表换成你要挂的钩子接口