From 171740094591a801012753fbb1c4f64294488dec Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 27 Jul 2026 18:04:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=A7=A6=E5=8F=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 + dev.md | 1 + docs/dev/system/trigger-refactor.md | 259 ++++++++++++++++++ packages-user/data-base/src/hero/mover.ts | 60 +++- packages-user/data-base/src/hero/types.ts | 26 +- packages-user/data-base/src/map/mapLayer.ts | 4 +- packages-user/data-base/src/map/mover.ts | 5 + packages-user/data-common/src/common/mover.ts | 18 ++ .../data-state/src/content/triggers.ts | 22 +- .../data-state/src/hero/moverImpl.ts | 48 +++- .../data-system/src/trigger/collection.ts | 40 ++- .../data-system/src/trigger/trigger.ts | 8 +- .../data-system/src/trigger/types.ts | 40 ++- 13 files changed, 503 insertions(+), 30 deletions(-) create mode 100644 docs/dev/system/trigger-refactor.md diff --git a/AGENTS.md b/AGENTS.md index 6859b9f..d17779d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,8 @@ 身为 AI Agent,你可能会被分配不同的任务,不同的任务请参阅不同的文件来查看具体要求。注意,不要查看本次任务之外的提示词文件,以避免上下文与注意力污染。 +所有的任务我都会在对话中提出,项目中的一部分 markdown 文件如 `task.md` 都是给我自己看的,不是给你看的。 + # 编写代码或设计系统 如果要求你进行代码编写,以及系统设计相关的行为,参阅 [code.md](./.agents/code.md)。 diff --git a/dev.md b/dev.md index 2938719..afc79ac 100644 --- a/dev.md +++ b/dev.md @@ -80,6 +80,7 @@ - 单行注释结尾不加句号;较长的多行注释结尾可加句号。 - 一般不建议给接口(`inteface` 本身)、类型别名、枚举或类本身写注释(即需要紧接着一长段缩进的时候,这时候写注释不好看),特殊情况除外。 - 对于 jsDoc 注释,方法注释必须使用换行风格;对于成员 jsDoc 注释,除非注释较长需要换行,否则使用不换行的风格。 +- 代码中的注释必须包含价值,说明下一行为什么会出现,不得出现解释下一行代码等类型的无价值注释,如 `// 清空 Xxx` 就属于无价值注释。 ### 类型规范 diff --git a/docs/dev/system/trigger-refactor.md b/docs/dev/system/trigger-refactor.md new file mode 100644 index 0000000..759f99b --- /dev/null +++ b/docs/dev/system/trigger-refactor.md @@ -0,0 +1,259 @@ +# 需求综述 + +当前触发器系统中,进入图块与撞击图块被合并为同一个触发器,通过 `eventPass` 标志位区分。这种合并设计导致两个触发条件无法拥有各自独立的触发器,同时也缺失了离开图块与无法进入图块的触发器支持。本次重构将四类触发条件分离,使触发器系统各司其职。 + +# 需求理解 + +## 明确需求 + +1. 四类触发条件分离。进入图块(`onEnter`)、离开图块(`onLeave`)、撞击图块(`onHit`)、无法移动至图块(`onCannotEnter`)必须各自独立,图块可为每种条件分别指定触发器类型。 +2. 撞击由 `eventPass` 决定。当 `eventPass` 为 `false` 时走撞击逻辑,应触发 `onHit`,而非复用进入图块的触发器。 +3. 无法移动由 `inPass` / `outPass` 决定。当地形通行性检查失败时走无法移动逻辑,应触发 `onCannotEnter`。 +4. 撞击与无法移动互斥。一次移动中只会发生其中一种,前者由 `eventPass` 控制,后者由 `inPass` / `outPass` 控制,二者不会同时触发。 +5. `onLeave` 的触发条件。仅在未发生撞击且未发生无法移动时(即移动成功,`HeroMoveCode.Step`),英雄将要离开当前图块时触发。 +6. `onEnter` 的触发时机。英雄到达目标图块且位置数据已切换后触发,通过 `ObjectMover` 的 `onStepEnd` 钩子实现。 +7. 触发器不区分触发条件。一个触发器同时包含四个触发方法(`onEnter`、`onLeave`、`onHit`、`onCannotEnter`),而非为四种条件分别注册四个触发器。触发器注册表仍按数字类型注册。 + +## 隐含需求 + +1. 同一个触发器类型可被不同条件复用。由于一个触发器实例同时包含四个方法,同一种触发器类型既可被指定为进入触发器也可被指定为撞击触发器——取决于调用方调用哪个方法。 +2. 离开图块的触发作用于出发图块。英雄从 A 移动到 B,`onLeave` 作用于 A 的图块,需要使用 `currLoc` 作为收集坐标。 +3. 图块可同时拥有多种条件的触发器。图块属性可能在游戏中变化,点级覆盖可与底层图块不同,因此同一坐标可为不同条件指定不同的触发器类型。 +4. 动态图块与静态图块拥有一致的触发器能力,`IDynamicTile` 也需要支持按条件读写触发器类型。 + +# 设计前提 + +1. 触发条件由游戏逻辑判定,不由触发器系统决定。`eventPass`、`inPass` / `outPass` 属于通行性判定范畴,判定结果决定走哪条触发路径。触发器系统只负责在给定条件后收集并执行对应触发器。 +2. 触发器类型仍以数字标识。`ITriggerRegistry` 基于数字管理触发器工厂创建,不感知触发条件。注册一个触发器类型后,该类型即拥有四个条件方法。 +3. 收集器不判定条件。`ITriggerCollector` 仅根据调用者给定的条件类型,从指定图块中收集该条件对应的触发器。条件选择由调用者(英雄移动系统)负责。 +4. 收集必定执行,不做预检查。触发器数量极少,收集一个空集合而后自然跳过的开销可忽略不计。不提供 `hasTrigger` 等预检查方法。 + +# 核心概念定义 + +## 触发条件 + +一种由英雄移动行为引发的交互类型,决定应当调用图块触发器上的哪个方法。触发条件共有四种,分别对应四类英雄与图块的交互方式。一次移动中只会产生一种触发条件(Step → enter/leave、Hit → hit、CannotMove → cannotEnter),因此四种条件不是并行触发而是排他的。 + +## 进入触发 + +当英雄成功移动到某个图块上且位置数据已切换后执行。判定条件为 `canPass` 为 `true` 且 `shouldHit` 为 `false`,此时英雄成功进入图块,应调用目标图块对应触发器实例上的 `onEnter` 方法。由于触发时机在位置切换之后,使用 `ObjectMover` 的 `onStepEnd` 钩子——在该钩子中 `tile` 的坐标已更新为新位置。 + +## 离开触发 + +当英雄将要离开当前图块,且该步移动属于成功移动(未发生撞击、未发生无法移动)时执行。离开触发作用于出发图块,使用英雄当前位置坐标(`currLoc`)收集触发器,应调用对应触发器实例上的 `onLeave` 方法。由于触发时机是"将要离开",在 `HeroMover.onStepEnd` 方法中、`setPos` 执行前调用 `leave`,此时英雄位置尚未变更。 + +## 撞击触发 + +当英雄尝试进入某个图块但被 `eventPass` 阻挡时执行。判定由 `eventPass` 完成:当 `eventPass` 为 `false` 时,英雄撞击目标图块并停止移动,应调用目标图块对应触发器实例上的 `onHit` 方法。 + +## 无法进入触发 + +当英雄尝试进入某个图块但被地形通行性(`inPass` / `outPass`)阻挡时执行。判定由 `canPass` 完成:当 `canPass` 为 `false` 时,英雄无法进入目标图块,应调用目标图块对应触发器实例上的 `onCannotEnter` 方法。在移动判定中 `canPass` 检查先于 `shouldHit` 检查,因此无法进入触发与撞击触发互斥。 + +# 接口设计分析 + +核心改动分为三个层面:触发器实例从单一方法变为四个方法,收集器增加条件参数以读取正确的触发器类型,集合执行时根据条件分发到对应方法。 + +## ITrigger + +### 设计思路 + +当前 `ITrigger` 仅有一个 `trigger(handler)` 方法,所有触发条件共用这一个人口。由于四种触发条件需要各自独立的行为,触发器实例需要为每种条件提供独立的方法入口。 + +因此将 `trigger` 方法替换为四个条件方法:`onEnter`、`onLeave`、`onHit`、`onCannotEnter`。这四个方法各自接收 `ITriggerHandler` 上下文对象,独立实现对应逻辑。这样同一个触发器类型被指定为进入触发时调用 `onEnter`,被指定为撞击触发时调用 `onHit`,互不干扰。 + +由于触发器注册表仍按数字类型注册,同一个触发器工厂产出的实例自动具备四种方法。不需要为四种条件注册四个不同触发器——条件的选择由调用方在运行时决定调用哪个方法。 + +### 接口分析 + +- 方法 `onEnter(handler: ITriggerHandler)`:预期频率**高频**。绝大多数的移动目的地都是可以进入的图块,每次进入都会调用。 +- 方法 `onLeave(handler: ITriggerHandler)`:预期频率**高频**。与 `onEnter` 对称,每次成功移动都会离开出发图块。 +- 方法 `onHit(handler: ITriggerHandler)`:预期频率**中频**。仅 `eventPass` 为 `false` 的图块会产生撞击,不如进入频繁。 +- 方法 `onCannotEnter(handler: ITriggerHandler)`:预期频率**低频**。仅地形阻挡时触发,场景相对最少。 + +### 预期体量 + +`ITrigger` 接口改动预期 5 行:四个方法替换原有的一个 `trigger` 方法。 + +--- + +## ITriggerCollection + +### 设计思路 + +当前 `ITriggerCollection.trigger(handler)` 遍历所有触发器调用各自的 `trigger` 方法。由于 `ITrigger` 不再有 `trigger` 方法而变为四个条件方法,集合在执行时需要知道应当调用哪个方法。因此 `trigger` 方法需要增加 `condition` 参数,在遍历时根据条件分发给每个触发器的对应方法。 + +`triggerIter` 同理,也需要增加 `condition` 参数,以便在迭代分步执行时知道分发到哪个方法。 + +由于集合中存储的仍然是 `ITrigger` 实例,同一个集合可以被重复用于不同条件(虽然实际场景中不会这样使用)。集合不记录自身是"为哪个条件创建的",条件信息仅在触发执行时传入。 + +### 接口分析 + +- 方法 `trigger(condition: TriggerCondition, handler: ITriggerHandler)`:预期频率**高频**。每次触发执行都要通过此方法,是集合的核心入口。修改仅增加 `condition` 参数,调用方式变化不大。 +- 方法 `triggerIter(condition: TriggerCondition, handler: ITriggerHandler)`:预期频率**中频**。用于需要逐步控制触发流程的高级场景,使用频率低于普通 `trigger`。 + +### 预期体量 + +预期改动 10 行。`trigger` 方法中的遍历循环,将 `trigger.trigger(handler)` 替换为对 `condition` 的 `switch` 分发(或等价的条件方法调用)。逻辑简单,仅增加分发层。 + +--- + +## ITriggerCollector + +### 设计思路 + +当前 `ITriggerCollector.collect(x, y, layer)` 不区分触发条件,统一读取图块上的单一触发器类型。由于四种条件可能有各自不同的触发器类型,收集器必须知道当前条件,才能从图块数据中读取该条件对应的触发器类型。 + +因此 `collect` 方法新增 `condition` 参数,收集器根据条件调用图块上对应的方法(如 `layer.getEnterTrigger(x, y)`)获取触发器类型,经由注册表创建 `ITrigger` 实例。收集到的触发器实例本身不绑定条件——条件信息仅用于"读取哪个触发器类型",不传递给集合或触发器。 + +### 接口分析 + +- 方法 `collect(condition: TriggerCondition, x: number, y: number, layer: IMapLayer)`:预期频率**高频**。英雄每移动一格都要至少调用一次,是触发系统的核心入口。新增参数不影响复杂度。 + +### 预期体量 + +预期代码体量为 90-120 行。分析如下: + +- 静态与动态图块的收集逻辑预期 90 行:保留当前 `collect` 方法的优先级逻辑(单动态优先值比较、多动态排序去重),差异在于读取触发器类型时需根据 `condition` 分发到不同字段,每个分支代码量较小但分支较多。 + +--- + +## IMapLayer / IDynamicTile + +### 设计思路 + +当前 `IMapLayer` 有 `getTriggerType(x, y)` 和 `setTriggerType(type, x, y)` 两个方法,`IDynamicTile` 有 `triggerType` 字段和 `setTriggerType` 方法。由于四种触发条件各自独立,这些单一入口需要扩展为按条件读写。 + +对于 `IMapLayer`,需要四个 `getXxxTrigger(x, y)` 和四个 `setXxxTrigger(type, x, y)` 方法(`xxx` 为 Enter / Leave / Hit / CannotEnter)。读取时先查点级覆盖映射,若未设置则回退到 `ITileStore` 的对应条件默认值。 + +对于 `IDynamicTile`,需要为每种条件提供字段及 setter。 + +`ITileRawData` 的设计不在此文档范围内,由项目负责人单独处理。 + +### 接口分析 + +- 方法 `IMapLayer.getEnterTrigger(x, y)` 等四个:预期频率**高频**。收集器每次收集都需调用其中之一,与进入触发等条件频率一致。 +- 方法 `IMapLayer.setEnterTrigger(type, x, y)` 等四个:预期频率**中频**。运行时修改触发器类型有一定场景(事件修改图块行为等),但远低于读取频率。 +- 字段 `IDynamicTile.enterTrigger` 等四个字段:预期频率**高频**。收集动态图块触发器时需要读取对应条件字段。 +- 方法 `IDynamicTile.setEnterTrigger(type)` 等四个 setter:预期频率**中频**。同 `IMapLayer` 的 setter 频率。 + +### 预期体量 + +不在此文档范围内展开——`ITileRawData` 的内部实现由项目负责人处理。接口层面的方法签名改动预期约 20 行(四个 getter + 四个 setter 的方法声明)。 + +--- + +## IHeroMoveTopImpl + +### 设计思路 + +当前 `IHeroMoveTopImpl` 仅有 `hit` 方法处理撞击触发,进入触发和无法进入触发均未覆盖。四种条件分离后,需要补齐缺失的触发入口:`enter`、`leave`、`cannotEnter`。 + +`onLeave` 的触发目标是出发图块,使用 `handler.currLoc` 坐标收集,触发时机是离开前(在 `onStepEnd` 方法中调用)。`onEnter` 的触发目标是目标图块,使用 `handler.nextLoc` 坐标收集,触发时机是位置切换后,通过 `ObjectMover` 的 `onStepEnd` 钩子实现——钩子中的 `tile` 坐标已更新为新位置。`onHit` 和 `onCannotEnter` 的触发目标都是目标图块,使用 `handler.nextLoc` 坐标收集。 + +在英雄移动流程中各条件的分发: +- `Step`:`onStepEnd` 方法中调用 `leave(handler)` 触发离开,`onStepEnd` 钩子中调用 `enter(handler)` 触发进入。 +- `Hit`:`onStepEnd` 方法中调用 `hit(handler)` 触发撞击(原逻辑,内部改为收集时指定 `Hit` 条件)。 +- `CannotMove`:`onStepEnd` 方法中调用 `cannotEnter(handler)` 触发无法进入。 + +### 接口分析 + +- 方法 `enter(handler: IHeroMoveTopHandler)`:预期频率**高频**。绝大多数移动成功后会调用。 +- 方法 `leave(handler: IHeroMoveTopHandler)`:预期频率**高频**。与 `enter` 对称,每次成功移动前调用。 +- 方法 `cannotEnter(handler: IHeroMoveTopHandler)`:预期频率**低频**。仅地形阻挡时触发。 +- 方法 `hit(handler: IHeroMoveTopHandler)`:现有方法,内部逻辑修改为收集时指定 `TriggerCondition.Hit`。预期频率不变。 + +### 预期体量 + +预期代码体量为 50 行。分析如下: + +- `enter` 方法预期 12 行:与当前 `hit` 类似——获取 `floorId`、获取 `ILayerState`、取 `eventLayer`、收集指定条件的触发器、执行。 +- `leave` 方法预期 12 行:与 `enter` 对称,差异在于收集时坐标使用 `currLoc`。 +- `cannotEnter` 方法预期 12 行:与 `hit` 对称,条件替换为 `CannotEnter`。 +- `hit` 方法修改预期 14 行:在现有骨架基础上透传条件参数。 + +--- + +# 实现思路 + +## ITrigger 方法拆分 + +1. 移除 `ITrigger.trigger(handler)` 方法,新增 `onEnter`、`onLeave`、`onHit`、`onCannotEnter` 四个方法,签名均为 `(handler: ITriggerHandler): Promise`。 +2. `BaseTrigger` 抽象类中四个方法均标记为 `abstract`,子类必须实现。 +3. `BaseTrigger.collection()` 方法不变。 + +## 集合执行分发 + +1. `ITriggerCollection.trigger` 和 `triggerIter` 增加 `condition: TriggerCondition` 参数。 +2. 遍历触发器的循环中,根据 `condition` 调用对应方法,替代原来的 `trigger.trigger(handler)`。 + +## 收集器改造 + +1. `collect` 方法增加 `condition` 参数。 +2. 静态触发器读取根据 `condition` 选择调用 `layer.getEnterTrigger` 等对应方法。 +3. 动态触发器读取根据 `condition` 选择读取 `tile.enterTrigger` 等对应字段。 +4. 优先级排序逻辑不变。 + +## 英雄移动系统适配 + +1. `IHeroMoveTopImpl` 新增 `enter`、`leave`、`cannotEnter` 三个方法,直接调用收集器收集对应条件的触发器并执行。 +2. `HeroMover.onStepEnd` 中 `Step` 分支:调用 `leave(handler)` 触发离开后再返回 `nextLoc`。 +3. `HeroMover` 注册 `onStepEnd` 钩子:当 `code === HeroMoveCode.Step` 时,调用 `enter(handler)`。此时钩子中的 `tile` 坐标已通过 `setPos` 更新为新位置,可直接从 `tile` 获取当前坐标构造 handler。 +4. `HeroMover.onStepEnd` 中 `CannotMove` 分支:调用 `cannotEnter(handler)`。 +5. `HeroMoveCode.Hit` 注释更新为"不能移动,并触发目标格撞击触发器"。 + +# 涉及文件 + +## `@user/data-system/trigger/types.ts` + +- [ ] 新增 `TriggerCondition` 枚举:定义 `Enter`、`Leave`、`Hit`、`CannotEnter` 四个成员。 +- [ ] 修改 `ITrigger` 接口:移除 `trigger` 方法,新增 `onEnter`、`onLeave`、`onHit`、`onCannotEnter` 四个方法。 +- [ ] 修改 `ITriggerCollection.trigger` 和 `triggerIter` 方法签名:新增 `condition: TriggerCondition` 参数。 +- [ ] 修改 `ITriggerCollector.collect` 方法签名:新增 `condition: TriggerCondition` 参数。 + +## `@user/data-system/trigger/trigger.ts` + +- [ ] 修改 `BaseTrigger` 抽象类:四个条件方法标记为 `abstract`,移除 `trigger`。 + +## `@user/data-system/trigger/collection.ts` + +- [ ] 修改 `TriggerCollection.trigger`:遍历中根据 `condition` 分发给对应条件方法。 +- [ ] 修改 `TriggerCollection.triggerIter`:同上。 + +## `@user/data-system/trigger/collector.ts` + +- [ ] 修改 `TriggerCollector.collect`:根据 `condition` 读取图块对应字段。 + +## `@user/data-base/map/types.ts` + +- [ ] 修改 `IMapLayer` 接口:新增四个 `getXxxTrigger` 和四个 `setXxxTrigger` 方法,替换原有的 `getTriggerType` / `setTriggerType`。 +- [ ] 修改 `IDynamicTile` 接口:新增四个条件字段及对应的 setter,替换原有的 `triggerType` / `setTriggerType`。 + +## `@user/data-base/map/mapLayer.ts` + +- [ ] 实现新增的八个方法,内部映射管理适配。 + +## `@user/data-base/map/dynamicTile.ts` + +- [ ] 拆分 `triggerType` 为四个条件字段,实现四个 setter。 + +## `@user/data-base/map/dynamicLayer.ts` + +- [ ] 修改 `createDynamic` 和 `transferToDynamic` 中触发器同步逻辑:同步四个条件字段。 + +## `@user/data-base/hero/types.ts` + +- [ ] 新增 `IHeroMoveTopImpl.enter` 方法。 +- [ ] 新增 `IHeroMoveTopImpl.leave` 方法。 +- [ ] 新增 `IHeroMoveTopImpl.cannotEnter` 方法。 +- [ ] 更新 `HeroMoveCode` 枚举中 `Hit` 和 `CannotMove` 的注释。 + +## `@user/data-state/hero/moverImpl.ts` + +- [ ] 实现新增的 `enter`、`leave`、`cannotEnter` 方法。 +- [ ] 修改 `hit` 方法:调用收集器时传入 `TriggerCondition.Hit`。 + +## `@user/data-base/hero/mover.ts` + +- [ ] 修改 `onStepEnd`:`Step` 分支调用 `leave`;`CannotMove` 分支调用 `cannotEnter`。 +- [ ] 注册 `onStepEnd` 钩子:`Step` 时调用 `enter`,利用钩子中 `tile` 坐标已更新的时机。 diff --git a/packages-user/data-base/src/hero/mover.ts b/packages-user/data-base/src/hero/mover.ts index 3bb6a35..96ec308 100644 --- a/packages-user/data-base/src/hero/mover.ts +++ b/packages-user/data-base/src/hero/mover.ts @@ -128,6 +128,27 @@ export class HeroMover } } + /** + * 根据当前位置及要移动至的位置创建信息对象 + * @param curr 当前位置 + * @param next 移动至的位置 + * @param floorId 楼层 id + */ + private createHandlerFromLoc( + curr: ITileLocator, + next: ITileLocator, + floorId: string | undefined + ): IHeroMoveTopHandler { + return { + currLoc: { x: curr.x, y: curr.y }, + nextLoc: { x: next.x, y: next.y }, + direction: this.moveDirection, + floorId, + face: this.faceHandler, + state: this.state + }; + } + protected async onMoveStart(): Promise { if (!this.topImpl) { logger.warn(144); @@ -195,28 +216,53 @@ export class HeroMover ): Promise { if (!this.topImpl) return { x: tile.x, y: tile.y }; + const trigger = !this.ignoreTerrain; + const handler = this.createHandler(tile, step, tile.floorId); - // 不能移动或停止 - if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Stop) { + if (code === HeroMoveCode.CannotMove) { + if (trigger) { + await this.topImpl.cannotEnter(handler); + } // 这里不能 await,因为其 Promise 会在当前步结束后兑现,如果 await 就会卡住 controller.stop(); return { x: tile.x, y: tile.y }; } - // 撞击时也使用当前位置,同时处理撞击行为 - if (code === HeroMoveCode.Hit) { - await this.topImpl.hit(handler); - // 这里同样不能 await + if (code === HeroMoveCode.Stop) { + controller.stop(); + return { x: tile.x, y: tile.y }; + } + + if (code === HeroMoveCode.Hit) { + if (trigger) { + await this.topImpl.hit(handler); + } controller.stop(); return { x: tile.x, y: tile.y }; } - // 正常移动 if (code === HeroMoveCode.Step) { return handler.nextLoc; } return { x: tile.x, y: tile.y }; } + + protected async onStepSettled( + before: ITileLocator, + curr: ITileLocator, + tile: T + ): Promise { + if (!this.topImpl) return; + + const handler = this.createHandlerFromLoc(before, curr, tile.floorId); + + const trigger = !this.ignoreTerrain; + + if (trigger) { + await this.topImpl.leave(handler); + await this.topImpl.enter(handler); + } + } } diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index fbedf1c..f5cddb1 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -280,9 +280,9 @@ export const enum HeroMoveCode { Step, /** 移动被停止 */ Stop, - /** 不能移动,并撞击目标格触发器 */ + /** 不能移动,并触发目标格撞击触发器 */ Hit, - /** 不能移动,不触发触发器 */ + /** 不能移动,并触发目标格无法进入触发器 */ CannotMove } @@ -315,16 +315,34 @@ export interface IHeroMoveTopImpl { canPass(handler: IHeroMoveTopHandler): boolean; /** - * 判断在指定楼层中,从指定坐标向指定方向移动时是否应该产生撞击,撞击将会触发目标位置的触发器 + * 判断在指定楼层中,从指定坐标向指定方向移动时是否应该产生撞击,撞击将会触发目标位置的撞击触发器 * @param handler 通行性检查对象 */ shouldHit(handler: IHeroMoveTopHandler): boolean; /** - * 勇士撞击某一个图块时执行的内容,一般用于触发目标位置的触发器 + * 勇士正常移动到某个图块上时触发进入触发器 + * @param handler 移动信息对象 + */ + enter(handler: IHeroMoveTopHandler): Promise; + + /** + * 勇士将要离开当前图块时触发离开触发器 + * @param handler 移动信息对象 + */ + leave(handler: IHeroMoveTopHandler): Promise; + + /** + * 勇士撞击某一个图块时执行的内容,一般用于触发目标位置的撞击触发器 * @param handler 撞击行为对象 */ hit(handler: IHeroMoveTopHandler): Promise; + + /** + * 勇士无法进入某一个图块时执行的内容,一般用于触发目标位置的无法进入触发器 + * @param handler 移动信息对象 + */ + cannotEnter(handler: IHeroMoveTopHandler): Promise; } export interface IHeroMoverConfig { diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index eee5ceb..6ccb711 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -173,7 +173,9 @@ export class MapLayer tile: num, raw, trigger, - dynamics + dynamics, + locator: { x, y }, + block: raw }; return data; diff --git a/packages-user/data-base/src/map/mover.ts b/packages-user/data-base/src/map/mover.ts index 8215c09..05093d4 100644 --- a/packages-user/data-base/src/map/mover.ts +++ b/packages-user/data-base/src/map/mover.ts @@ -1,6 +1,7 @@ import { ITileLocator, logger } from '@motajs/common'; import { getFaceMovement, + IMoverController, ObjectMover, ObjectMoveStep, ObjectMoveType @@ -88,6 +89,10 @@ export class DynamicTileMover extends ObjectMover { } return Promise.resolve(locator); } + + protected onStepSettled(): Promise { + return Promise.resolve(); + } } //#endregion diff --git a/packages-user/data-common/src/common/mover.ts b/packages-user/data-common/src/common/mover.ts index 9df55a5..086195a 100644 --- a/packages-user/data-common/src/common/mover.ts +++ b/packages-user/data-common/src/common/mover.ts @@ -432,6 +432,21 @@ export abstract class ObjectMover controller: Readonly ): Promise; + /** + * 当单步移动结束后触发,此时对象坐标已经被设置 + * @param code 移动代码,由 {@link onStepStart} 返回值传递而来 + * @param step 当前移动步对象 + * @param tile 移动图块 + * @param controller 移动控制器 + */ + protected abstract onStepSettled( + before: ITileLocator, + curr: ITileLocator, + tile: T, + step: Readonly, + controller: Readonly + ): Promise; + /** * 向计划队列末尾追加一个步骤 * @param step 要追加的步骤 @@ -631,9 +646,12 @@ export abstract class ObjectMover ); await Promise.all(stepStartHooks); const loc = await this.onStepEnd(code, step, this.tile, controller); + const before: ITileLocator = { x: this.tile.x, y: this.tile.y }; + const curr: ITileLocator = { x: loc.x, y: loc.y }; if (this.tile.x !== loc.x && this.tile.y !== loc.y) { this.tile.setPos(loc.x, loc.y); } + await this.onStepSettled(before, curr, this.tile, step, controller); const stepEndHooks = this.forEachHook(hook => hook.onStepEnd?.(code, step, this.tile, this) ); diff --git a/packages-user/data-state/src/content/triggers.ts b/packages-user/data-state/src/content/triggers.ts index eae22e1..edca49f 100644 --- a/packages-user/data-state/src/content/triggers.ts +++ b/packages-user/data-state/src/content/triggers.ts @@ -133,7 +133,11 @@ export class ChangeFloorTrigger extends BaseTrigger implements ITrigger { } } - async trigger(handler: ITriggerHandler): Promise { + /** + * 触发楼层切换 + * @param handler 触发信息对象 + */ + private async trigger(handler: ITriggerHandler): Promise { if (!handler.layer || !handler.locator || !handler.mapLayer) { logger.warn(164); return; @@ -159,4 +163,20 @@ export class ChangeFloorTrigger extends BaseTrigger implements ITrigger { collection(): ITriggerCollection { return new TriggerCollection([this]); } + + onEnter(handler: ITriggerHandler): Promise { + return this.trigger(handler); + } + + onHit(handler: ITriggerHandler): Promise { + return this.trigger(handler); + } + + onLeave(): Promise { + return Promise.resolve(); + } + + onCannotEnter(): Promise { + return Promise.resolve(); + } } diff --git a/packages-user/data-state/src/hero/moverImpl.ts b/packages-user/data-state/src/hero/moverImpl.ts index 9b46db3..4bcbec0 100644 --- a/packages-user/data-state/src/hero/moverImpl.ts +++ b/packages-user/data-state/src/hero/moverImpl.ts @@ -7,7 +7,8 @@ import { FaceDirection, PassBit } from '@user/data-common'; import { IStateSystem, ITriggerCollector, - ITriggerHandler + ITriggerHandler, + TriggerType } from '@user/data-system'; import { isNil } from 'lodash-es'; @@ -130,25 +131,56 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { //#region 触发器行为 - async hit(handler: IHeroMoveTopHandler): Promise { - if (isNil(handler.floorId)) return; + /** + * 统一的触发器收集与执行流程 + * @param type 触发条件 + * @param handler 移动信息对象 + * @param x 收集横坐标 + * @param y 收集纵坐标 + */ + private commonTrigger( + type: TriggerType, + handler: IHeroMoveTopHandler, + x: number, + y: number + ): Promise { + if (isNil(handler.floorId)) return Promise.resolve(); const map = this.maps.getLayerState(handler.floorId); - if (!map) return; + if (!map) return Promise.resolve(); const event = map.eventLayer; - if (!event) return; + if (!event) return Promise.resolve(); - const { x, y } = handler.nextLoc; const triggers = this.collector.collect(x, y, event); const triggerHandler: ITriggerHandler = { state: this.state, layer: map, mapLayer: event, - locator: handler.nextLoc + locator: { x, y } }; - return triggers.trigger(triggerHandler); + return triggers.trigger(type, triggerHandler); + } + + async enter(handler: IHeroMoveTopHandler): Promise { + const { x, y } = handler.nextLoc; + return this.commonTrigger(TriggerType.Enter, handler, x, y); + } + + async leave(handler: IHeroMoveTopHandler): Promise { + const { x, y } = handler.currLoc; + return this.commonTrigger(TriggerType.Leave, handler, x, y); + } + + async hit(handler: IHeroMoveTopHandler): Promise { + const { x, y } = handler.nextLoc; + return this.commonTrigger(TriggerType.Hit, handler, x, y); + } + + async cannotEnter(handler: IHeroMoveTopHandler): Promise { + const { x, y } = handler.nextLoc; + return this.commonTrigger(TriggerType.CannotEnter, handler, x, y); } //#endregion diff --git a/packages-user/data-system/src/trigger/collection.ts b/packages-user/data-system/src/trigger/collection.ts index a1fe3cd..7611738 100644 --- a/packages-user/data-system/src/trigger/collection.ts +++ b/packages-user/data-system/src/trigger/collection.ts @@ -1,4 +1,9 @@ -import { ITrigger, ITriggerCollection, ITriggerHandler } from './types'; +import { + ITrigger, + ITriggerCollection, + ITriggerHandler, + TriggerType +} from './types'; export class TriggerCollection implements ITriggerCollection { /** 当前集合内部维护的触发器列表 */ @@ -12,18 +17,22 @@ export class TriggerCollection implements ITriggerCollection { return this.triggerList.length; } - async trigger(handler: ITriggerHandler): Promise { + async trigger( + condition: TriggerType, + handler: ITriggerHandler + ): Promise { for (const trigger of this.triggerList) { - await trigger.trigger(handler); + await this.dispatch(trigger, condition, handler); } } async *triggerIter( + condition: TriggerType, handler: ITriggerHandler ): AsyncGenerator { let currentHandler = handler; for (const trigger of this.triggerList) { - await trigger.trigger(currentHandler); + await this.dispatch(trigger, condition, currentHandler); const nextHandler = yield trigger; if (nextHandler) { currentHandler = nextHandler; @@ -33,6 +42,29 @@ export class TriggerCollection implements ITriggerCollection { } } + /** + * 根据触发条件将请求分发给对应方法 + * @param trigger 触发器实例 + * @param condition 触发条件 + * @param handler 触发上下文 + */ + private dispatch( + trigger: ITrigger, + condition: TriggerType, + handler: ITriggerHandler + ): Promise { + switch (condition) { + case TriggerType.Enter: + return trigger.onEnter(handler); + case TriggerType.Leave: + return trigger.onLeave(handler); + case TriggerType.Hit: + return trigger.onHit(handler); + case TriggerType.CannotEnter: + return trigger.onCannotEnter(handler); + } + } + iterate(): Iterable { return this.triggerList.values(); } diff --git a/packages-user/data-system/src/trigger/trigger.ts b/packages-user/data-system/src/trigger/trigger.ts index fad56a3..1347170 100644 --- a/packages-user/data-system/src/trigger/trigger.ts +++ b/packages-user/data-system/src/trigger/trigger.ts @@ -8,7 +8,13 @@ export abstract class BaseTrigger implements ITrigger { constructor(readonly state: IStateBase) {} - abstract trigger(handler: ITriggerHandler): Promise; + abstract onEnter(handler: ITriggerHandler): Promise; + + abstract onLeave(handler: ITriggerHandler): Promise; + + abstract onHit(handler: ITriggerHandler): Promise; + + abstract onCannotEnter(handler: ITriggerHandler): Promise; collection(): ITriggerCollection { return new TriggerCollection([this]); diff --git a/packages-user/data-system/src/trigger/types.ts b/packages-user/data-system/src/trigger/types.ts index 8b11ec7..8f61071 100644 --- a/packages-user/data-system/src/trigger/types.ts +++ b/packages-user/data-system/src/trigger/types.ts @@ -6,6 +6,17 @@ import { IDataBaseExtended } from '@user/data-base'; +export const enum TriggerType { + /** 进入图块 */ + Enter, + /** 离开图块 */ + Leave, + /** 撞击图块 */ + Hit, + /** 无法进入图块 */ + CannotEnter +} + export interface ITriggerHandler { /** 当前全局状态对象 */ readonly state: IStateBase; @@ -26,10 +37,28 @@ export interface ITrigger extends IDataBaseExtended { readonly priority: number; /** - * 使用给定上下文触发当前触发器 + * 进入图块时触发 * @param handler 触发上下文对象 */ - trigger(handler: ITriggerHandler): Promise; + onEnter(handler: ITriggerHandler): Promise; + + /** + * 离开图块时触发 + * @param handler 触发上下文对象 + */ + onLeave(handler: ITriggerHandler): Promise; + + /** + * 撞击图块时触发 + * @param handler 触发上下文对象 + */ + onHit(handler: ITriggerHandler): Promise; + + /** + * 无法进入图块时触发 + * @param handler 触发上下文对象 + */ + onCannotEnter(handler: ITriggerHandler): Promise; /** * 将当前触发器包装为单元素触发器集合 @@ -65,16 +94,19 @@ export interface ITriggerCollection { count(): number; /** - * 顺序触发当前集合中的所有触发器 + * 按指定触发条件顺序触发当前集合中的所有触发器 + * @param type 触发类型 * @param handler 初始触发上下文对象 */ - trigger(handler: ITriggerHandler): Promise; + trigger(type: TriggerType, handler: ITriggerHandler): Promise; /** * 逐个触发当前集合中的触发器,并允许为下一次推进提供新上下文 + * @param condition 触发条件 * @param handler 初始触发上下文对象 */ triggerIter( + condition: TriggerType, handler: ITriggerHandler ): AsyncGenerator;