diff --git a/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md b/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md index 066ce85..9020b3c 100644 --- a/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md +++ b/.planning/phases/10-docs-restructure/10-11-READER-REVIEW.md @@ -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`、构造函数保存 `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`)、`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`(按本次派发要求)。 + diff --git a/docs/intermediate/enemy-query.md b/docs/intermediate/enemy-query.md index 71c7193..6d5d17e 100644 --- a/docs/intermediate/enemy-query.md +++ b/docs/intermediate/enemy-query.md @@ -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 = { +// [需改] 类名:这是一个常规查询效果 +export class GroupQueryEffect implements IEnemyCommonQueryEffect< + IEnemyAttr, + IHeroAttr +> { // [需改] priority:与光环共用一套优先级,越大越先执行 - priority: 10, + readonly priority = 10; + // [模板] apply 签名固定:handler 是当前怪物,special 是触发查询的属性,query() 取上下文 - apply(handler, special, query) { + apply( + handler: IEnemyHandler, + special: ISpecial, + query: () => IReadonlyEnemyContext + ): void { // [需改] 读出本属性登记的参数(示例:半径与每只同类提供的攻击加成) const { radius, atk } = special.value as { radius: number; @@ -120,10 +141,10 @@ const effect: IEnemyCommonQueryEffect = { // [需改] 修改自身:每有一个同类,攻击加 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 = { - priority: 15, // [需改] 优先级 - // [模板] for(ctx) 签名固定:返回一个修饰器;ctx 是只读查询上下文 - for(ctx) { - return { - // [需改] shouldQuery:本效果是否作用于这只怪物(示例:带 code 41 的怪物) - shouldQuery: handler => handler.enemy.hasSpecial(41), - // [需改] add:返回要添加给怪物的特殊属性 - add(handler) { - const result: ISpecial[] = []; - // [模板] 取全场其他怪物,并按位置索引从小到大排序 - 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(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 + ) {} -// [需改] 注册(特殊查询不绑定单个 code,只传 effect) -state.enemyContext.registerSpecialQueryEffect(effect); + // [需改] shouldQuery:本效果是否作用于这只怪物(示例:带 code 41 的怪物) + shouldQuery( + handler: IReadonlyEnemyHandler + ): boolean { + return handler.enemy.hasSpecial(41); + } + + // [需改] add:返回要添加给怪物的特殊属性 + add( + handler: IReadonlyEnemyHandler + ): ISpecial[] { + const result: ISpecial[] = []; + // [模板] 取全场其他怪物,并按位置索引从小到大排序 + 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[] { + return []; + } + + // [需改] modify:自己已有、需要叠加数值的属性在这里处理 + modify( + handler: IEnemyHandler, + special: ISpecial + ): 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(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 + ): IEnemySpecialQueryModifier { + 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 = { - priority: 5, // [需改] 优先级 +// [需改] 类名:同样是常规查询效果 +export class HornQueryEffect implements IEnemyCommonQueryEffect< + IEnemyAttr, + IHeroAttr +> { + readonly priority = 5; // [需改] 优先级 + // [模板] apply 签名固定 - apply(handler, special, query) { + apply( + handler: IEnemyHandler, + special: ISpecial, + query: () => IReadonlyEnemyContext + ): void { // [模板] 必须用 query() 取上下文 const ctx = query(); let count = 0; @@ -246,10 +300,10 @@ const effect: IEnemyCommonQueryEffect = { // [需改] 修改自身:每有一只怪物,攻击加 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`,用 `registerCommonQueryEffect(code, new ...())` 注册; +- **特殊查询**:`class ... implements IEnemySpecialQueryEffect`,用 `registerSpecialQueryEffect(new ...())` 注册。 + +**如果你不怎么懂代码,建议直接改内置类(如 `src/data/enemy/aura.ts`、`src/data/enemy/final.ts`),而不是自己写新的类。** 自己写类要同时处理接口签名、注册时机与查询上下文,改动面比改内置方法体大得多;能由光环或最终效果表达的需求,优先改对应的内置类。 > 查询机制的设计原理(为什么常规查询要绑定 code、查询上下文为何是只读的、它与光环如何共用优先级)会在**高级教程**中讲解(预留标记:`高级教程 · 查询设计原理`);待高级教程就位后,此处会替换为链接。 diff --git a/docs/intermediate/hooks.md b/docs/intermediate/hooks.md index 7fa640b..e33ef87 100644 --- a/docs/intermediate/hooks.md +++ b/docs/intermediate/hooks.md @@ -4,18 +4,28 @@ ## 钩子是什么 -在数据端,勇士、地图、战斗流程等主对象在发生关键动作时,会依次调用自己身上每个钩子的对应方法。于是你只需要写一个钩子对象、把它注册到关心的主对象上,就能在这些动作发生时收到通知。 +在数据端,勇士、地图、战斗流程等主对象在发生关键动作时,会依次调用自己身上每个钩子的对应方法。于是你只需要写一个钩子类、把它注册到关心的主对象上,就能在这些动作发生时收到通知。 -一个钩子对象就是一组方法的集合,**「什么时候」由方法名决定,「做什么」由方法体决定**。例如想在勇士属性变化时做点什么,就实现 `onUpdateAttribute`: +一个钩子就是一个**类**:类里实现某个钩子族接口的方法,**「什么时候」由方法名决定,「做什么」由方法体决定**。例如想在勇士属性变化时做点什么,就实现 `IHeroAttributeHooks` 的 `onUpdateAttribute`: ```ts -const hook = { - onUpdateAttribute(name, value) { +// [模板] 钩子类:implements 后面写钩子族接口 +export class MyHeroAttributeHooks implements IHeroAttributeHooks { + // [模板] 构造函数里保存 state,方法体里才能取到数据端主对象 + constructor(readonly state: ICoreState) {} + + // [需改] 方法体换成你要做的事 + onUpdateAttribute( + 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 { + constructor(readonly state: ICoreState) {} + + onUpdateAttribute( + 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 { + constructor(readonly state: ICoreState) {} + + onUpdateAttribute( + 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/*` 里已有的类),而不是自己写新的类。** 改内置只需要替换方法体,注册也已经完成;自己写类还要处理接口签名、构造函数与注册/移除时机。