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:
unanmed 2026-10-19 19:28:18 +08:00
parent a50210aec2
commit e64167b7c5
9 changed files with 85 additions and 42 deletions

View File

@ -541,4 +541,30 @@
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 边界:仅改 `docs/intermediate/enemy-*.md` 与本记录;未改 `config.ts` 与生产代码;未新增文件;未引入指向 `docs/advanced/` 的真实链接(D-47 预留仍为代码 span + HTML 注释);未创建 `10-11-SUMMARY.md`,未改 `STATE.md` / `ROADMAP.md`(按本次派发要求)。
---
## 用户循环 · batch 10 记录(D-51 特殊属性落点与讲解方式)
**范围:** 用户修订循环第 1 轮 batch 10(D-51)——战斗组 8 篇(`enemy-aura`、`enemy-query`、`enemy-calculator`、`enemy-map-damage`、`enemy-specials`、`enemy-final`、`enemy-combat-script`、`enemy-status-hooks`)。按 `10-CONTEXT.md` D-51:①不新建文件,已有实现文件(`aura.ts` 等)就写在里面,只有查询属性新建 `query.ts`;②`aura.ts` / `query.ts` 建议用 `//#region {code}-{name}` … `//#endregion` 包裹;③用通俗语言解释「副作用」;④说明地图伤害按计算后怪物计算、光环加的特殊属性会真实生效,并说明「依赖 + 修改外部环境」应拆成查询 + 光环、用新怪物属性桥接;⑤`getCriticalLimit` 提一句并指向高级教程;⑥接口讲解改为「为了实现 X,需要(实现 / 修改)Y 接口」再讲参数。保留 D-50 模板、D-49(改内置优先)、D-48(六步实操)、D-47 预留。
### 逐条处置
| 文档 | 处置 |
| --- | --- |
| `enemy-aura.md` | ①模板与两个示例的落点由「新建文件 / 另建文件」改为写进 `src/data/enemy/aura.ts`,并新增 `//#region {code}-{name}`(示例 `//#region 30-削弱光环` / `31-传染光环`)建议,说明内置 23 / 24 / 26 即如此组织。②新增「光环、计算后怪物与『读写外部』」:讲明 `applySpecial` 改的是计算后怪物、地图伤害等读到的就是这份结果,光环新增的特殊属性会真实生效(旧样板无法动态添加);并说明「依赖 + 修改外部环境」应拆成查询 + 光环、用新怪物属性桥接,指向高级教程(D-47 预留 `高级教程 · 依赖并修改外部环境的属性` + TODO 注释)。 |
| `enemy-query.md` | ①落点统一为 `src/data/enemy/query.ts`,并明确查询是唯一需要新建文件的一类(光环 / 最终效果 / 地图伤害写各自现成文件)。②新增 `//#region {code}-{name}`(示例 40 / 41 / 42)建议。③「常规查询」「特殊查询」intro 改为「为了实现它,我们需要实现 `IEnemy…QueryEffect` 接口」再讲参数。④「优先级」段的「既依赖外部、又修改外部」改为指向「拆成查询 + 光环、用新怪物属性桥接」。 |
| `enemy-calculator.md` | ①「接口」改为「伤害计算器接口」,先讲 `calculate` 的用途,`getCriticalLimit` 降为一句带过并指向高级教程。②warning 用通俗语言补充「副作用」的定义。③模板 / 示例里 `getCriticalLimit` 注释改为「一般不用改;高级教程会细讲」;模板落点改为 `calculator.ts`。④「临界」章改为「临界(了解即可)」,保留单调递减约定与 `atk` 上界,二分等细节指向高级教程(D-47 预留 `高级教程 · 临界计算` + TODO 注释)。 |
| `enemy-map-damage.md` | ①新增「按『计算后怪物』计算」:转换器拿到的是 `getComputedEnemy()`,光环 `applySpecial` 新增的「领域」等属性会真实生效;再说「依赖 + 修改外部」应拆成查询 + 光环桥接(回指光环篇)。②模板 / 示例落点改为写进 `src/data/enemy/mapDamage.ts`。 |
| `enemy-specials.md` | ①「在数据里的样子」改为「为了读取怪物身上的特殊属性,我们需要用到 … 三个接口」。②「改内置」段补明光环写 `aura.ts`、查询写新建的 `query.ts`。③「如果内置实现里实在找不到落点」改为按类型写进现有实现文件(aura / mapDamage / final / script / hook),不要另建,只有查询新建 `query.ts`。 |
| `enemy-final.md` | ①「接口」改为「最终效果接口」,改为「为了实现收尾调整,我们需要实现 `IEnemyFinalEffect` 接口」。②「为什么没有『既依赖外部、又修改外部』的能力」补:确实需要时拆成查询 + 光环、用新怪物属性桥接,指向高级教程(D-47 预留 + TODO 注释)。③模板落点改为写进 `src/data/enemy/final.ts`。 |
| `enemy-combat-script.md` | ①「接口」改为「战斗脚本接口」,改为「为了实现战前拦截 / 战后结算,我们需要实现 `ICombatScript` 接口」。②模板与两个自写示例的落点改为写进 `src/data/enemy/script.ts`。 |
| `enemy-status-hooks.md` | ①「自己写钩子类」intro 改为「为了实现某个时机上的效果,我们需要实现对应钩子接口的类」并列四时机接口 / 挂载点。②模板落点改为写进 `src/data/enemy/hook.ts`。 |
### 校验
- 源码核对:`src/data/enemy/aura.ts`(`//#region 23-光环` / `24-支援` / `26-特殊光环`,`CommonAuraConverter.convert` / `CommonAura.applySpecial`)、`packages-user/data-system/src/combat/mapDamage.ts`(`createReadonlyHandler` 用 `view.getComputedEnemy()` 作为 `enemy`,故地图伤害按计算后怪物计算)、`packages-user/data-system/src/combat/context.ts`(`buildupSpecials` → `buildupBase` → `buildupQuery` → `buildupFinal` 顺序、`applySpecial` / `refreshSpecialModifier`)、`src/data/enemy/calculator.ts`(`getCriticalLimit` 对 `atk` 的处理、`calculate`)、`src/data/enemy/mapDamage.ts`(`MainMapDamageConverter.convert` 读 `enemy.getSpecial(13/16/22)`)、`src/data/enemy/final.ts`、`src/data/enemy/script.ts`、`src/data/enemy/hook.ts`、`src/data/enemy/special.ts`(`//#region 复合属性值类型` / `属性定义`)。
- 结构门禁:所改 8 篇均 CRLF、单 H1(`STRUCT-OK`)。
- 格式门禁:`node node_modules/prettier/bin/prettier.cjs --write docs/intermediate/enemy-*.md`(D-45,`endOfLine: crlf`)已跑。
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0(新增预留仍为代码 span + HTML 注释,不产生链接)。
- 边界:仅改 `docs/intermediate/enemy-*.md` 与本记录;未改 `config.ts` 与生产代码;未新增文件;未创建 `10-11-SUMMARY.md`,未改 `STATE.md` / `ROADMAP.md`(按本次派发要求)。

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` 登记属性——

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): 临界计算 -->

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` 注册(见文末「注册在哪里」)。
**复制什么**:

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
// [需改] 类名:换成你自己的效果名

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` 登记这条属性——

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` 注册(见文末「注册在哪里」)。
**复制什么**:

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` 的属性定义 + 上表中的实现文件),而不是自己写新的类。** 自己写类要同时处理接口签名、注册时机与上下文刷新,改动面比在已有方法里加一个分支大得多,出错也更难排查。

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
// [需改] 类名与接口:按上表换成你要挂的钩子接口