docs(10-11): 按 D-49 改类示例与改内置定位(查询/钩子)

This commit is contained in:
unanmed 2026-10-19 16:47:31 +08:00
parent 3aae27a939
commit 9c57673e0a
3 changed files with 265 additions and 122 deletions

View File

@ -463,3 +463,24 @@
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0(侧边栏 `/intermediate/tile-store` 链接可解析)。
- 边界:仅新增 1 篇文档 + 1 条侧边栏;未改任何生产代码;未引入指向 `docs/advanced/` 的真实链接(D-47)。
---
## 用户循环 · batch 7 记录(D-49 示例与「改内置优先」)
**范围:** 用户修订循环第 1 轮 batch 7(D-49)——战斗组两篇 `enemy-query.md`、`hooks.md`。按 `10-CONTEXT.md` D-49:①示例代码贴合引擎现状,定义 `class` 再 `new`,不再用 `const obj = {...}`;②主体讲**改内置类**,末尾再简述「自己写类」;③明确写出「不懂代码建议直接改内置,而不是自己写新类」。保留 D-48 六步实操与 D-47 高级教程预留。
### 逐条处置
| 文档 | 处置 |
| --- | --- |
| `enemy-query.md` | ①三个示例(抱团 / 同化 / 号角)由 `const effect: IEnemyCommonQueryEffect<...> = {...}` / `const effect: IEnemySpecialQueryEffect<...> = {...}` 改为 `class ... implements ...`,注册改为 `new ...()`;同化示例把 `for` 返回的修饰器也抽成 `AssimilationModifier` 类,与「定义类再 `new`」保持一致。②按源码核对接口签名:`priority`、`apply(handler, special, query)`、`for(ctx)`、修饰器 `shouldQuery` / `add` / `delete` / `modify`,`query` / `ctx` 按既有 U-15 口径标注为 `IReadonlyEnemyContext`。③主体新增「接进内置流程」:说明查询效果**在 `src/` 中无内置实现**、需在 `src/data/enemy/` 新建类并在 `initializeEnemy` 注册;同时指出能由内置光环(`aura.ts`)或最终效果(`final.ts`)表达的需求应优先改内置。④末尾新增「自己写查询类时」+ 明确 D-49 caveat「不懂代码建议直接改内置类」。⑤「注册在哪里」示例改为 `new` 实例并注明注册的是实例不是类本身。 |
| `hooks.md` | ①开篇 `const hook = {...}` 改为 `class MyHeroAttributeHooks implements IHeroAttributeHooks<IHeroAttr>`、构造函数保存 `state`。②原「注册与移除钩子」改为「改内置钩子(推荐)」:先给内置钩子类 → 文件对照表(`combat` / `event` / `hero` / `loader` / `path`),再按 D-48 六步给出「改 `MainHeroAttributeHooks.onUpdateAttribute` 方法体」的做法,并说明内置类已在 `initializeHooks` 注册、不要重复 `addHook`。③末尾新增「自己写钩子类」:`class` + `addHook(new ...(state))`、保留实例/控制器移除,并写明 D-49 caveat「不懂代码建议直接改内置钩子」。 |
### 校验
- 源码核对:`packages-user/data-system/src/combat/types.ts`(`IEnemyCommonQueryEffect.priority` / `apply(handler, special, query)`、`IEnemySpecialQueryEffect.priority` / `for(ctx)`、`IEnemySpecialQueryModifier.shouldQuery`、`IEnemySpecialModifier.add` / `delete` / `modify`、`registerCommonQueryEffect(code, effect)` / `registerSpecialQueryEffect(effect)`)、`src/data/enemy/initialize.ts`(无查询效果注册;光环 / 最终效果 / 脚本 / 钩子已注册)、`src/data/hook/initialize.ts`(内置钩子统一 `addHook`)、`src/data/hook/*.ts`(`Main*Hooks` 类与文件对应)、`packages-user/data-base/src/hero/types.ts`(`IHeroAttributeHooks.onUpdateAttribute<K>`)、`packages-user/data-common/src/types.ts`(`IHeroAttr`、`emitTip`)。
- 结构门禁:两篇均 CRLF、单 H1(`STRUCT-OK`)。
- 格式门禁:`node node_modules/prettier/bin/prettier.cjs --write docs/intermediate/enemy-query.md docs/intermediate/hooks.md`(D-45,`endOfLine: crlf`)已跑。
- 构建门禁:`cmd /c "pnpm exec tsx docs/.vitepress/init.ts && pnpm docs:build"` → **green**,输出 `build complete`,dead link = 0。
- 边界:仅改两篇文档与本记录;未改 `config.ts` 与生产代码;未新增文件;未引入指向 `docs/advanced/` 的真实链接(D-47 预留仍为代码 span + HTML 注释);未创建 `10-11-SUMMARY.md`,未改 `STATE.md` / `ROADMAP.md`(按本次派发要求)。

View File

@ -5,17 +5,19 @@
查询在构建计算后怪物时执行:读取周围有哪些怪物、它们带什么特殊属性、勇士在哪里,然后修改怪物自身的基础属性,或增删、修改怪物的特殊属性。基础概念见[战斗总览](./combat-overview.md)。
:::warning
查询效果是**引擎未内置、需要自己注册的新增内容**。当前 `src/data/enemy/initialize.ts` 只注册了光环转换器、最终效果、战斗脚本与钩子,没有注册任何查询效果。下面的接口与示例在你自己调用注册方法之前不会生效。
查询效果是**引擎未内置、需要自己注册的新增内容**。当前 `src/data/enemy/initialize.ts` 只注册了光环转换器、最终效果、战斗脚本与钩子,没有注册任何查询效果。下文示例都是新增的类,在你自己写类并调用注册方法之前不会生效。
:::
## 常规查询(CommonQuery)
如果一条特殊属性只需要「读外部状态,然后改自己的基础属性」,用常规查询就够了。注册方式:
如果一条特殊属性只需要「读外部状态,然后改自己的基础属性」,用常规查询就够了。它的效果是一个实现了 `IEnemyCommonQueryEffect` 的类实例,注册方式:
```ts
state.enemyContext.registerCommonQueryEffect(code, effect);
state.enemyContext.registerCommonQueryEffect(code, new GroupQueryEffect());
```
(`GroupQueryEffect` 是下文「抱团」示例里的类;换成你自己写的类名即可。)
`IEnemyCommonQueryEffect` 只有两个成员:
- `priority`:优先级;
@ -28,10 +30,12 @@ state.enemyContext.registerCommonQueryEffect(code, effect);
如果查询要动的是目标怪物**拥有的特殊属性**——添加、删除或改写——就用特殊查询。例如给范围内的怪物补上一条属性,或把它们身上的数值属性叠加起来。注册方式:
```ts
state.enemyContext.registerSpecialQueryEffect(effect);
state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
```
常规查询的效果绑定在某个 `code` 上(引擎要按 code 找到会触发它的那条特殊属性),所以注册时还要传 code;特殊查询面向整只怪物,由修饰器里的 `shouldQuery` 自行筛选目标、可以一次改动多个属性,不绑定单个 code,因此只传 effect。
(`AssimilationQueryEffect` 是下文「同化」示例里的类。)
常规查询的效果绑定在某个 `code` 上(引擎要按 code 找到会触发它的那条特殊属性),所以注册时还要传 code;特殊查询面向整只怪物,由修饰器里的 `shouldQuery` 自行筛选目标、可以一次改动多个属性,不绑定单个 code,因此只传效果实例。
`IEnemySpecialQueryEffect` 有:
@ -63,12 +67,21 @@ state.enemyContext.registerSpecialQueryEffect(effect);
这里的**怪物视图**是 `IEnemyView`:用 `view.getComputedEnemy()` 取到该怪物的计算后对象,就能读它的特殊属性。上下文还有其他成员(如按坐标取怪物、取绑定的勇士与地图伤害管理器),需要时再查 API 参考。
## 改内置,还是自己写
## 接进内置流程
查询是**引擎未内置、必须自己写并注册的新增内容**(见上方 warning),没有「只改内置一处」的捷径:`initialize.ts` 里目前没有注册任何查询效果。
查询效果在引擎里**没有可直接修改的内置实现**:`initializeEnemy` 中只注册了光环转换器、最终效果、战斗脚本与钩子,没有注册任何查询效果。要新增一条查询,需要做两件事:
- **常规查询**:写一个 `IEnemyCommonQueryEffect` 对象,用 `registerCommonQueryEffect(code, effect)` 注册;
- **特殊查询**:写一个 `IEnemySpecialQueryEffect` 对象,用 `registerSpecialQueryEffect(effect)` 注册。
1. 在 `src/data/enemy/` 下新建一个文件,按下面的示例写一个**类**,让它实现对应接口;
2. 在 `initializeEnemy` 里 `new` 出这个类并注册(见文末「注册在哪里」)。
三个示例都是这种写法:先定义类,再在注册时 `new` 一次。查询效果只需要一个实例,注册一次即可。
查询没有内置类可改,但**不是所有需求都要新写查询类**。如果需求只是「影响周围怪物」或「只看自己与勇士做收尾」,优先改已有的内置类:
- 影响周围怪物 → 改内置光环 `src/data/enemy/aura.ts`(见[怪物光环](./enemy-aura.md));
- 只按自身与勇士收尾 → 改内置最终效果 `src/data/enemy/final.ts`(见[最终属性效果](./enemy-final.md))。
这些内置类已经在 `initializeEnemy` 里注册好,直接改方法体即可,不需要新建类。只有「必须读取周围怪物、再修改自己」的逻辑,才需要按下面的示例自己写查询类。
下面代码用 `[模板]` 标出可原样照抄的行(接口签名、遍历骨架),用 `[需改]` 标出必须按需求改写的行(code、优先级、统计与修改逻辑)。
@ -78,17 +91,25 @@ state.enemyContext.registerSpecialQueryEffect(effect);
**目标**:带「抱团」(示例 code 40)的怪物,范围内每多一个同种特殊属性的怪物,自身攻击加固定值。
**改哪里**:另建文件写这个效果对象;`src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/` 下另建一个文件(如 `query.ts`),写下面这个类;再到 `src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [需改] 变量名:这是一个常规查询效果对象
const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
// [需改] 类名:这是一个常规查询效果
export class GroupQueryEffect implements IEnemyCommonQueryEffect<
IEnemyAttr,
IHeroAttr
> {
// [需改] priority:与光环共用一套优先级,越大越先执行
priority: 10,
readonly priority = 10;
// [模板] apply 签名固定:handler 是当前怪物,special 是触发查询的属性,query() 取上下文
apply(handler, special, query) {
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>,
query: () => IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): void {
// [需改] 读出本属性登记的参数(示例:半径与每只同类提供的攻击加成)
const { radius, atk } = special.value as {
radius: number;
@ -120,10 +141,10 @@ const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
// [需改] 修改自身:每有一个同类,攻击加 atk 点
handler.enemy.addAttribute('atk', count * atk);
}
};
}
// [需改] 注册:code 是绑定本效果的属性编号(示例自选 40)
state.enemyContext.registerCommonQueryEffect(40, effect);
state.enemyContext.registerCommonQueryEffect(40, new GroupQueryEffect());
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
@ -142,74 +163,99 @@ state.enemyContext.registerCommonQueryEffect(40, effect);
**目标**:带「同化」(示例 code 41)的怪物,把其他怪物的特殊属性搬到自己身上(没有的照抄,数值型的叠加)。
**改哪里**:另建文件写这个效果对象;`src/data/enemy/initialize.ts` 里用 `registerSpecialQueryEffect` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/` 下另建一个文件(如 `query.ts`),写下面这个类;再到 `src/data/enemy/initialize.ts` 里用 `registerSpecialQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [需改] 变量名:特殊查询效果对象
const effect: IEnemySpecialQueryEffect<IEnemyAttr, IHeroAttr> = {
priority: 15, // [需改] 优先级
// [模板] for(ctx) 签名固定:返回一个修饰器;ctx 是只读查询上下文
for(ctx) {
return {
// [需改] shouldQuery:本效果是否作用于这只怪物(示例:带 code 41 的怪物)
shouldQuery: handler => handler.enemy.hasSpecial(41),
// [需改] add:返回要添加给怪物的特殊属性
add(handler) {
const result: ISpecial<any>[] = [];
// [模板] 取全场其他怪物,并按位置索引从小到大排序
const others = [...ctx.iterateEnemy()]
.filter(
([, view]) => view.getComputedEnemy() !== handler.enemy
)
.sort(
([a], [b]) =>
ctx.indexer.index(a.x, a.y) -
ctx.indexer.index(b.x, b.y)
);
// [需改] 把别人有、自己没有的属性照抄过来(跳过同化属性自身)
for (const [, view] of others) {
for (const special of view
.getComputedEnemy()
.iterateSpecials()) {
if (special.code === 41) continue;
if (handler.enemy.hasSpecial(special.code)) continue;
result.push(special.clone());
}
}
return result;
},
// [模板] 本效果不删除属性,返回空数组
delete() {
return [];
},
// [需改] modify:自己已有、需要叠加数值的属性在这里处理
modify(handler, special) {
// [模板] 只处理数值型属性
if (typeof special.value !== 'number') return false;
let sum = special.value;
let found = false;
// [模板] 遍历全场,把同 code 的数值加算进来
for (const [, view] of ctx.iterateEnemy()) {
const other = view
.getComputedEnemy()
.getSpecial<number>(special.code);
if (!other || typeof other.value !== 'number') continue;
sum += other.value;
found = true;
}
if (!found) return false;
// [模板] 写回并返回 true,表示确实改动了
special.setValue(sum);
return true;
}
};
}
};
// [需改] 类名:特殊查询的修饰器,负责实际改动目标怪物的特殊属性
class AssimilationModifier implements IEnemySpecialQueryModifier<
IEnemyAttr,
IHeroAttr
> {
// [模板] 构造时保存只读查询上下文
constructor(
private readonly ctx: IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
) {}
// [需改] 注册(特殊查询不绑定单个 code,只传 effect)
state.enemyContext.registerSpecialQueryEffect(effect);
// [需改] shouldQuery:本效果是否作用于这只怪物(示例:带 code 41 的怪物)
shouldQuery(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): boolean {
return handler.enemy.hasSpecial(41);
}
// [需改] add:返回要添加给怪物的特殊属性
add(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): ISpecial<any>[] {
const result: ISpecial<any>[] = [];
// [模板] 取全场其他怪物,并按位置索引从小到大排序
const others = [...this.ctx.iterateEnemy()]
.filter(([, view]) => view.getComputedEnemy() !== handler.enemy)
.sort(
([a], [b]) =>
this.ctx.indexer.index(a.x, a.y) -
this.ctx.indexer.index(b.x, b.y)
);
// [需改] 把别人有、自己没有的属性照抄过来(跳过同化属性自身)
for (const [, view] of others) {
for (const special of view.getComputedEnemy().iterateSpecials()) {
if (special.code === 41) continue;
if (handler.enemy.hasSpecial(special.code)) continue;
result.push(special.clone());
}
}
return result;
}
// [模板] 本效果不删除属性,返回空数组
delete(): ISpecial<any>[] {
return [];
}
// [需改] modify:自己已有、需要叠加数值的属性在这里处理
modify(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>
): boolean {
// [模板] 只处理数值型属性
if (typeof special.value !== 'number') return false;
let sum = special.value;
let found = false;
// [模板] 遍历全场,把同 code 的数值加算进来
for (const [, view] of this.ctx.iterateEnemy()) {
const other = view
.getComputedEnemy()
.getSpecial<number>(special.code);
if (!other || typeof other.value !== 'number') continue;
sum += other.value;
found = true;
}
if (!found) return false;
// [模板] 写回并返回 true,表示确实改动了
special.setValue(sum);
return true;
}
}
// [需改] 类名:特殊查询效果
export class AssimilationQueryEffect implements IEnemySpecialQueryEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority = 15; // [需改] 优先级
// [模板] for(ctx) 签名固定:返回一个修饰器;ctx 是只读查询上下文
for(
ctx: IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): IEnemySpecialQueryModifier<IEnemyAttr, IHeroAttr> {
return new AssimilationModifier(ctx);
}
}
// [需改] 注册(特殊查询不绑定单个 code,只传实例)
state.enemyContext.registerSpecialQueryEffect(new AssimilationQueryEffect());
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
@ -218,7 +264,7 @@ state.enemyContext.registerSpecialQueryEffect(effect);
**怎么验证**:给一只怪物挂上「同化」,让它周围有其他带特殊属性的怪物,看它是否照抄了别人的属性、数值型是否叠加。
**注意**:`add` 与 `delete` 不要同时返回非空结果(见上文);`add` 负责处理「自己还没有的属性」,`modify` 负责处理「自己已经有、需要叠加数值」的属性。两者配合,就能把范围内怪物的特殊属性合并到自身。
**注意**:`add` 与 `delete` 不要同时返回非空结果(见上文);`add` 负责处理「自己还没有的属性」,`modify` 负责处理「自己已经有、需要叠加数值」的属性。两者配合,就能把场上其他怪物的特殊属性合并到自身。
## 示例:号角
@ -226,16 +272,24 @@ state.enemyContext.registerSpecialQueryEffect(effect);
**目标**:带「号角」(示例 code 42)的怪物,场上每有一只怪物就给自身加一份攻击。
**改哪里**:另建文件写这个效果对象;`src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**改哪里**:在 `src/data/enemy/` 下另建一个文件(如 `query.ts`),写下面这个类;再到 `src/data/enemy/initialize.ts` 里用 `registerCommonQueryEffect` 注册(见文末「注册在哪里」)。
**复制什么**:
```ts
// [需改] 变量名:同样是常规查询效果对象
const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
priority: 5, // [需改] 优先级
// [需改] 类名:同样是常规查询效果
export class HornQueryEffect implements IEnemyCommonQueryEffect<
IEnemyAttr,
IHeroAttr
> {
readonly priority = 5; // [需改] 优先级
// [模板] apply 签名固定
apply(handler, special, query) {
apply(
handler: IEnemyHandler<IEnemyAttr, IHeroAttr>,
special: ISpecial<any>,
query: () => IReadonlyEnemyContext<IEnemyAttr, IHeroAttr>
): void {
// [模板] 必须用 query() 取上下文
const ctx = query();
let count = 0;
@ -246,10 +300,10 @@ const effect: IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr> = {
// [需改] 修改自身:每有一只怪物,攻击加 special.value 点
handler.enemy.addAttribute('atk', count * (special.value as number));
}
};
}
// [需改] 注册:示例自选 code 42
state.enemyContext.registerCommonQueryEffect(42, effect);
state.enemyContext.registerCommonQueryEffect(42, new HornQueryEffect());
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
@ -276,15 +330,24 @@ export function initializeEnemy(
// ...省略:光环转换器与最终效果的注册...
// [需改] 查询效果目前没有内置,需要自己在这里注册:
// ctx.registerCommonQueryEffect(40, effect);
// ctx.registerSpecialQueryEffect(effect);
// [需改] 查询效果目前没有内置;先 import 你写的类,再在这里注册:
// ctx.registerCommonQueryEffect(40, new GroupQueryEffect());
// ctx.registerSpecialQueryEffect(new AssimilationQueryEffect());
// ...省略:脚本与钩子的注册...
}
```
把注册调用放在这里,查询就会参与构建计算后怪物。
把注册调用放在这里,查询就会参与构建计算后怪物。注册的是 `new` 出来的实例,不是类本身。
## 自己写查询类时
只有在「必须读取周围怪物、再修改自己」时才需要自己写查询类。写法就是上面三个示例:`class` + `implements`,注册时 `new` 一次。
- **常规查询**:`class ... implements IEnemyCommonQueryEffect<IEnemyAttr, IHeroAttr>`,用 `registerCommonQueryEffect(code, new ...())` 注册;
- **特殊查询**:`class ... implements IEnemySpecialQueryEffect<IEnemyAttr, IHeroAttr>`,用 `registerSpecialQueryEffect(new ...())` 注册。
**如果你不怎么懂代码,建议直接改内置类(如 `src/data/enemy/aura.ts`、`src/data/enemy/final.ts`),而不是自己写新的类。** 自己写类要同时处理接口签名、注册时机与查询上下文,改动面比改内置方法体大得多;能由光环或最终效果表达的需求,优先改对应的内置类。
> 查询机制的设计原理(为什么常规查询要绑定 code、查询上下文为何是只读的、它与光环如何共用优先级)会在**高级教程**中讲解(预留标记:`高级教程 · 查询设计原理`);待高级教程就位后,此处会替换为链接。

View File

@ -4,18 +4,28 @@
## 钩子是什么
在数据端,勇士、地图、战斗流程等主对象在发生关键动作时,会依次调用自己身上每个钩子的对应方法。于是你只需要写一个钩子对象、把它注册到关心的主对象上,就能在这些动作发生时收到通知。
在数据端,勇士、地图、战斗流程等主对象在发生关键动作时,会依次调用自己身上每个钩子的对应方法。于是你只需要写一个钩子类、把它注册到关心的主对象上,就能在这些动作发生时收到通知。
一个钩子对象就是一组方法的集合,**「什么时候」由方法名决定,「做什么」由方法体决定**。例如想在勇士属性变化时做点什么,就实现 `onUpdateAttribute`:
一个钩子就是一个**类**:类里实现某个钩子族接口的方法,**「什么时候」由方法名决定,「做什么」由方法体决定**。例如想在勇士属性变化时做点什么,就实现 `IHeroAttributeHooks` 的 `onUpdateAttribute`:
```ts
const hook = {
onUpdateAttribute(name, value) {
// [模板] 钩子类:implements 后面写钩子族接口
export class MyHeroAttributeHooks implements IHeroAttributeHooks<IHeroAttr> {
// [模板] 构造函数里保存 state,方法体里才能取到数据端主对象
constructor(readonly state: ICoreState) {}
// [需改] 方法体换成你要做的事
onUpdateAttribute<K extends keyof IHeroAttr>(
name: K,
value: IHeroAttr[K]
): void {
// 当勇士的某个属性变化时执行
}
};
}
```
引擎里的内置钩子也是这样的类,例如 `src/data/hook/hero.ts` 的 `MainHeroAttributeHooks`;它们的实例在 `src/data/hook/initialize.ts` 里统一注册。日常改内置钩子时,不需要自己写上面这个类,改内置类的方法体即可(见下文「改内置钩子」)。
## 常用钩子族一览
「钩子族」是同一类钩子方法的集合(一个 `interface`)。常用的几族如下,方法名以源码为准:
@ -62,45 +72,56 @@ const hook = {
若某类需求暂未在表中找到,可以在对应主对象的 `IHookable` 接口处查看它支持的全部钩子方法(例如 `hero-attribute`、`enemy-status-hooks` 等篇会进一步说明相关钩子的用法)。
## 注册与移除钩子
## 改内置钩子(推荐)
写好了钩子对象,还要挂到目标主对象上。用 `addHook` 完成注册,它会返回一个控制器。
日常需求优先改内置钩子文件(`src/data/hook/*`):内置钩子已经在 `initializeHooks`(`src/data/hook/initialize.ts`)里注册好,你只要改对应类的方法体,不需要自己写类,也不需要再调用 `addHook`。
内置钩子类与所在文件:
| 钩子族 | 内置类 | 文件 |
| ------------------------- | ---------------------------- | ------------------------- |
| `ICombatFlowHooks` | `MainCombatFlowHooks` | `src/data/hook/combat.ts` |
| `IGameEventExecutorHooks` | `MainGameEventExecutorHooks` | `src/data/hook/event.ts` |
| `IHeroStateHooks` | `MainHeroStateHooks` | `src/data/hook/hero.ts` |
| `IHeroAttributeHooks` | `MainHeroAttributeHooks` | `src/data/hook/hero.ts` |
| `IHeroLocationHooks` | `MainHeroLocationHooks` | `src/data/hook/hero.ts` |
| `IObjectMoverHooks` | `MainHeroMoverHooks` | `src/data/hook/hero.ts` |
| `IMotaDataLoaderHooks` | `MainMotaDataLoaderHooks` | `src/data/hook/loader.ts` |
| `IPathfindingSystemHooks` | `MainPathfindingSystemHooks` | `src/data/hook/path.ts` |
**目标**:在勇士某个属性变化时,立刻执行你写好的处理。
**改哪里**:希望效果生效的地方——事件函数、道具效果,或 `src/data/hook/` 下你新增的钩子文件都可以。
**改哪里**:`src/data/hook/hero.ts` 的 `MainHeroAttributeHooks.onUpdateAttribute` 方法体。
**复制什么**:
**复制什么**:下面的类本来就在文件里,复制时保留类与构造函数,只把方法体里的占位行换成你的处理:
```ts
const hook = {
onUpdateAttribute(name, value) {
// 属性变化时要做的事
export class MainHeroAttributeHooks implements IHeroAttributeHooks<IHeroAttr> {
constructor(readonly state: ICoreState) {}
onUpdateAttribute<K extends keyof IHeroAttr>(
name: K,
value: IHeroAttr[K]
): void {
// [需改] 删掉下面两行占位,换成你的处理
void name;
void value;
// 示例:攻击变化时给出提示
if (name === 'atk') {
this.state.emitTip(`攻击变为 ${value}`);
}
}
};
const controller = state.hero.attribute.addHook(hook);
}
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
> 复制说明:这段就是内置类的现有代码,不需要新增文件;若编辑器提示缺少某个名字,再在文件顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:把 `onUpdateAttribute` 换成你要用的钩子方法名(对照上两张表),把方法体换成你的处理;把 `state.hero.attribute` 换成要挂接的目标主对象。
**改什么**:把 `void name;` / `void value;` 换成你的处理;示例只是提示,也可以改成加属性、记 flag、触发事件等。
**怎么验证**:手动触发一次对应动作(例如改一次攻击力),看方法体里的效果有没有执行。
**怎么验证**:手动改一次勇士攻击力(例如使用道具或调试命令),看提示有没有出现。
**注意**:一个钩子方法会对该主对象的**所有**同类动作触发——例如 `onUpdateAttribute` 对每个属性变化都触发,需要时要在方法体里判断 `name`,别误以为只对某一个属性生效。
需要移除时,有两种方式:把**原来的钩子对象**传给 `removeHook`,或调用 `addHook` 返回的控制器的 `unload` 方法。
```ts
// 方式一:传回原来的钩子对象
state.hero.attribute.removeHook(hook);
// 方式二:用注册时返回的控制器卸载
controller.unload();
```
数据端的内置钩子在 `src/data/hook/initialize.ts` 中统一注册(涵盖战斗、事件、勇士、加载、寻路等),它是了解内置钩子如何挂载的入口。
**注意**:一个钩子方法会对该主对象的**所有**同类动作触发——例如 `onUpdateAttribute` 对每个属性变化都触发,需要时要在方法体里判断 `name`,别误以为只对某一个属性生效。内置类已注册,改完方法体就会生效,不要重复 `addHook`。
## 异步钩子
@ -119,3 +140,41 @@ controller.unload();
钩子由所属主对象触发,但同一个主对象可能对应多个游戏对象(例如场上有多只怪物)。因此钩子被触发时,不一定是针对你关心的那一只;当场上存在多个同类主对象时,需要在回调里根据传入的参数判断是否为你的目标对象。一般情况下不会串。
钩子建立在数据端的主对象之上,两端分工见[数据端与渲染端](./data-vs-render.md);编写钩子所需的语法见[代码基础](./code-basics.md)。
## 自己写钩子类
如果不想改内置文件,或需要给同一个主对象挂多个互不影响的钩子,可以自己写一个钩子类,再用 `addHook(new ...(state))` 注册:
```ts
// [需改] 类名与处理逻辑
export class MyHeroAttributeHooks implements IHeroAttributeHooks<IHeroAttr> {
constructor(readonly state: ICoreState) {}
onUpdateAttribute<K extends keyof IHeroAttr>(
name: K,
value: IHeroAttr[K]
): void {
// 属性变化时要做的事
}
}
// [模板] 注册:实例先存进变量,之后移除时要用
const hook = new MyHeroAttributeHooks(state);
const controller = state.hero.attribute.addHook(hook);
```
> 复制说明:片段已省略 `import`;文件顶部通常已有,不要再粘。若编辑器提示缺少某个名字,再在顶部补对应的 `import`(详见 [代码基础](./code-basics.md) 的「照抄教程里的代码时,怎么处理 import」)。
**改什么**:类名、`implements` 后面的钩子族接口、方法名(对照上两张表),以及 `addHook` 前面的目标主对象。
**注意**:一个主对象可以挂多个钩子,它们会依次被调用。需要移除时,保留注册时的实例或控制器:把**原来的实例**传给 `removeHook`,或调用控制器的 `unload` 方法。
```ts
// 方式一:传回原来的钩子实例
state.hero.attribute.removeHook(hook);
// 方式二:用注册时返回的控制器卸载
controller.unload();
```
**如果你不怎么懂代码,建议直接改内置钩子(`src/data/hook/*` 里已有的类),而不是自己写新的类。** 改内置只需要替换方法体,注册也已经完成;自己写类还要处理接口签名、构造函数与注册/移除时机。