From fbdd609001164783bd373d2d9aa0dc670ab15803 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 11 May 2026 23:35:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A8=E6=80=81=E5=9B=BE=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev.md | 7 +- docs/dev/{ => enemy}/enemy-manager-save.md | 0 docs/dev/{ => hero}/hero-modifier-save.md | 0 docs/dev/map/dynamic-tile-move.md | 595 ++++++++++++++++++ docs/dev/map/dynamic-tile.md | 407 ++++++++++++ docs/dev/{ => map}/map-store-improve.md | 0 docs/dev/{ => map}/map-store-save.md | 0 docs/dev/{ => system}/save-system.md | 0 packages-user/data-base/src/common/index.ts | 1 + packages-user/data-base/src/common/mover.ts | 579 +++++++++++++++++ .../data-base/src/map/dynamicLayer.ts | 142 +++++ .../data-base/src/map/dynamicTile.ts | 72 +++ packages-user/data-base/src/map/index.ts | 1 + packages-user/data-base/src/map/mapLayer.ts | 5 + packages-user/data-base/src/map/mover.ts | 78 +++ packages-user/data-base/src/map/types.ts | 168 ++++- .../data-state/src/enemy/calculator.ts | 9 - packages/common/src/logger.json | 5 + prompt.md | 9 +- 19 files changed, 2063 insertions(+), 15 deletions(-) rename docs/dev/{ => enemy}/enemy-manager-save.md (100%) rename docs/dev/{ => hero}/hero-modifier-save.md (100%) create mode 100644 docs/dev/map/dynamic-tile-move.md create mode 100644 docs/dev/map/dynamic-tile.md rename docs/dev/{ => map}/map-store-improve.md (100%) rename docs/dev/{ => map}/map-store-save.md (100%) rename docs/dev/{ => system}/save-system.md (100%) create mode 100644 packages-user/data-base/src/common/mover.ts create mode 100644 packages-user/data-base/src/map/dynamicLayer.ts create mode 100644 packages-user/data-base/src/map/dynamicTile.ts create mode 100644 packages-user/data-base/src/map/mover.ts diff --git a/dev.md b/dev.md index 19d9c08..de5e670 100644 --- a/dev.md +++ b/dev.md @@ -57,13 +57,15 @@ ### 注释规范 -- 常用属性成员、方法、接口、类型必须添加 `jsDoc` 注释。 +- 公共方法、接口必须在**源头处**(多数情况下为 `interface`)添加 `jsDoc` 注释;其他常用成员、方法、类型也必须添加注释(含义极为明确或极少使用的可例外,但建议全部添加)。 +- 继承或 `implements` 而来的 API(方法、成员等),若注释说明无需变更,则**不应重复添加** `jsDoc` 注释。 +- 不对构造器添加注释。若构造器使用了属性声明语法(`constructor(public prop: T)`)且成员需要说明,可仅对该成员添加参数注释,不写构造器描述。**在这种情况下**建议避免在构造器中使用属性声明语法,将成员单独声明并在构造器中赋值。此条建议**并非**要求不使用构造器的属性声明语法,而是仅在这一情况下不建议使用,常规情况下推荐使用此语法来缩短代码长度并提高可读性。 - 长文件可使用 `#region` / `#endregion` 分段以支持折叠。 - TODO 使用 `// TODO:` 或 `// todo:` 格式。 - 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。 - 注释合理换行:考虑中文字符较宽,建议每 40–60 个字符在标点符号后换行,不允许在句中换行;参数注释换行后保持对齐。 - 单行注释结尾不加句号;较长的多行注释结尾可加句号。 -- 一般不建议给接口、类型别名或类本身写注释(不好看),特殊情况除外。 +- 一般不建议给接口(`inteface` 本身)、类型别名或类本身写注释(不好看),特殊情况除外。 ### 类型规范 @@ -77,6 +79,7 @@ ### 其他要求 +- 不使用字符串作为键或特殊标识符(如枚举值、事件名、状态名等),应使用枚举代替。仅当明确表示字符串本身(如字符串类型的 id 别名、文件路径等)时方可使用字符串字面量。 - 严格遵循 `eslint` 配置,不允许出现 eslint 报错。 - 尽量不使用 `?.` 运算符,仅推荐在以下两种场景中使用: - 副作用函数调用,如 `this.obj?.func()` 或 `this.obj.func?.()` diff --git a/docs/dev/enemy-manager-save.md b/docs/dev/enemy/enemy-manager-save.md similarity index 100% rename from docs/dev/enemy-manager-save.md rename to docs/dev/enemy/enemy-manager-save.md diff --git a/docs/dev/hero-modifier-save.md b/docs/dev/hero/hero-modifier-save.md similarity index 100% rename from docs/dev/hero-modifier-save.md rename to docs/dev/hero/hero-modifier-save.md diff --git a/docs/dev/map/dynamic-tile-move.md b/docs/dev/map/dynamic-tile-move.md new file mode 100644 index 0000000..ea4c905 --- /dev/null +++ b/docs/dev/map/dynamic-tile-move.md @@ -0,0 +1,595 @@ +# 动态图块移动系统 + +> 本文档为 [动态图块系统](./dynamic-tile.md) 的移动子系统设计文档, +> 重点描述双方向模型、`IDynamicMover` 计划式移动接口及执行流程。 +> 本文档中新增的类型与接口同样需要添加至 `@user/data-base/src/map/types.ts`。 + +--- + +# 背景与动机 + +`IDynamicLayer.moveDynamicStep` / `moveDynamicWith` 是命令式简单接口,适用于 +一次性的简单移动,但对以下场景力不从心: + +- **后退移动**:角色面朝上、向下移动时,朝向不变但位移反向, + 单一 `FaceDirection` 参数无法同时描述朝向和移动方向; +- **连续路径中途动态追加步骤**:命令式接口执行后无法继续追加; +- **移动速度切换**:路径中途需要变速,命令式接口不支持; +- **复杂动画编排**:需要按序执行多段计划时,纯 `Promise` 链表达不够直观。 + +因此引入以图块为粒度绑定的 `IDynamicMover`,采用**先建计划、再执行**的设计理念, +参考 `@motajs/animate/types.ts` 的链式计划构建模式, +以及 `@user/data-state/legacy/move.ts` 中 `ObjectMoverBase` 的双方向与队列设计。 + +--- + +# 实现思路 + +## 1. 双方向模型 + +每个动态图块维护两个方向字段: + +| 字段 | 含义 | 影响 | +| --------------- | ---------------------------- | --------------------------------------- | +| `faceDirection` | **朝向**,视觉上「面朝哪里」 | 通过 `IRoleFaceBinder` 决定渲染图块数字 | +| `moveDirection` | **移动方向**,「向哪边走」 | 结合 `getFaceMovement` 计算位移 | + +两者分离后可以表达: + +- **后退**:`moveDirection = Up`,执行 `backward()` 步, + `moveDirection = Down`,`faceDirection = Down`,位移向下,朝向同步翻转。 +- **横向移动不转身**:使用 `stepFace(move, face)` 单独指定朝向, + `moveDirection` 与 `faceDirection` 独立设置。 + +绝对方向步骤(传入 `FaceDirection`)默认将两者同时更新; +`forward()` 沿当前 `moveDirection` 移动并对齐 `faceDirection`; +`backward()` 将 `moveDirection` 取反并同步 `faceDirection`。 + +## 2. 方向步语义 + +绝对方向步(`IDynamicMoveStepDir` / `IDynamicMoveStepDirFace`)直接传入 `FaceDirection`: + +- `step(dir, count?: number)`:追加 `count`(默认 1)个绝对方向步,`moveDirection` 和 `faceDirection` 均更新为 `dir`; +- `stepFace(move, face, count?: number)`:追加 `count`(默认 1)个绝对方向步,`moveDirection` 更新为 `move`, + `faceDirection` 更新为 `face`——用于移动方向与朝向不一致的场景(如斜切移动保持正向)。 + +相对方向步(`IDynamicMoveStepSpecial`)通过独立方法构建: + +- `forward(count?)`:沿当前 `tile.moveDirection` 移动,`moveDirection` 不变, + `faceDirection` 对齐为 `moveDirection`; +- `backward(count?)`:沿 `opposite(tile.moveDirection)` 移动,`moveDirection` 取反, + `faceDirection` 同步更新为新的 `moveDirection`,自动使用逆向动画。 + +## 3. DynamicMoveStep 类型 + +步骤类型通过 `const enum` 区分,避免字符串比较。 +前进/后退方向用独立的 `DynamicSpecialStep` 枚举表示; +动画播放方向用独立的 `DynamicAnimDirection` 枚举表示,两者互不依赖: + +```ts +const enum DynamicMoveStepType { + /** 绝对方向步,同步更新 moveDirection 和 faceDirection */ + Dir, + /** 绝对方向步,单独指定 faceDirection */ + DirFace, + /** 速度步 */ + Speed, + /** 纯转向步,不产生位移 */ + Face, + /** 特殊步(前进或后退) */ + Special, + /** 动画方向步,不影响 moveDirection/faceDirection */ + AnimDir +} + +/** 特殊步方向:前进或后退 */ +const enum DynamicSpecialStep { + Forward, + Backward +} + +/** 动画播放方向:正向或逆向 */ +const enum DynamicAnimDirection { + Forward, + Backward +} + +/** 绝对方向步:move 方向既是移动方向也是朝向 */ +interface IDynamicMoveStepDir { + type: DynamicMoveStepType.Dir; + move: FaceDirection; +} + +/** 绝对方向步:move 是移动方向,face 是显式指定的朝向 */ +interface IDynamicMoveStepDirFace { + type: DynamicMoveStepType.DirFace; + move: FaceDirection; + face: FaceDirection; +} + +/** 速度步:修改后续移动的每格耗时(单位 ms,越小越快) */ +interface IDynamicMoveStepSpeed { + type: DynamicMoveStepType.Speed; + value: number; +} + +/** 转向步:仅更新 faceDirection,不产生位移,等待渲染动画完成 */ +interface IDynamicMoveStepFace { + type: DynamicMoveStepType.Face; + value: FaceDirection; +} + +/** 特殊步:前进(Forward)或后退(Backward)。`forward(n)`/`backward(n)` 构建时 n>1 会 push n 个相同步骤 */ +interface IDynamicMoveStepSpecial { + type: DynamicMoveStepType.Special; + direction: DynamicSpecialStep; +} + +/** 动画方向步:指定后续步骤动画正向播放(Forward)还是逆向播放(Backward) */ +interface IDynamicMoveAnimDir { + type: DynamicMoveStepType.AnimDir; + dir: DynamicAnimDirection; +} + +type DynamicMoveStep = + | IDynamicMoveStepDir + | IDynamicMoveStepDirFace + | IDynamicMoveStepSpeed + | IDynamicMoveStepFace + | IDynamicMoveStepSpecial + | IDynamicMoveAnimDir; +``` + +## 4. IMoverController 接口 + +`IDynamicMover.start()` 返回 `IMoverController`,用于控制进行中的移动。 + +```ts +interface IMoverController { + /** 本次移动是否已全部完成 */ + readonly done: boolean; + /** 当本次移动全部步骤完成时兑现 */ + readonly onEnd: Promise; + /** + * 向当前队列末尾追加步骤(仅在移动进行中有效,完成后追加无效) + */ + push(...steps: DynamicMoveStep[]): void; + /** + * 停止移动,等待当前步骤完成后停止,返回的 Promise 在停止后兑现 + */ + stop(): Promise; +} +``` + +## 5. IDynamicMover 接口 + +每个 `IDynamicTile` 持有一个**绑定**的 `IDynamicMover` 实例(`tile.mover`), +无需单独创建或管理生命周期。 + +### 5.1 状态读取 + +```ts +// 以下状态属性来自 IObjectMover,IDynamicMover 全部继承 +interface IObjectMover { + /** 是否正在移动 */ + readonly moving: boolean; + /** 当前朝向(与 tile.faceDirection 同步) */ + readonly faceDirection: FaceDirection; + /** 当前移动方向(与 tile.moveDirection 同步) */ + readonly moveDirection: FaceDirection; + /** 所属的可移动对象 */ + readonly tile: T; + // ... +} +``` + +### 5.2 计划构建(链式) + +所有构建方法均返回 `this`,支持链式调用。 +计划在调用 `start()` 前不执行,仅追加到内部队列。 + +```ts +interface IDynamicMover { + // ... + /** 追加 count(默认 1)个绝对方向步,同步更新 moveDirection 和 faceDirection */ + step(dir: FaceDirection, count?: number): this; + /** 追加 count(默认 1)个绝对方向步,单独指定 faceDirection(用于移动方向与朝向不一致的场景) */ + stepFace(move: FaceDirection, face: FaceDirection, count?: number): this; + /** 追加前进步,沿当前 moveDirection 移动,faceDirection 对齐为 moveDirection */ + forward(count?: number): this; + /** 追加后退步,moveDirection 取反,faceDirection 同步更新,自动使用逆向动画 */ + backward(count?: number): this; + /** 追加一个速度步,改变后续移动的每格耗时(ms) */ + speed(value: number): this; + /** 追加一个纯转向步,仅更新 faceDirection,不产生位移 */ + face(dir: FaceDirection): this; + /** 追加一个动画方向步,控制后续步骤的动画播放方向(Forward 正向 / Backward 逆向) */ + animDir(dir: DynamicAnimDirection): this; + /** 清空尚未执行的计划队列 */ + clear(): this; + // ... +} +``` + +**用法示例**: + +```ts +// 先向下走 2 步,切换为半速,再向右走 1 步 +tile.mover + .step(FaceDirection.Down, 2) + .speed(200) + .step(FaceDirection.Right) + .start(); + +// 后退 3 步(moveDirection/faceDirection 翻转,逆向动画) +tile.mover.backward(3).start(); + +// 向右移动但保持朝下(如横向滑步) +tile.mover.stepFace(FaceDirection.Right, FaceDirection.Down, 2).start(); +``` + +### 5.3 执行控制 + +```ts +interface IDynamicMover { + // ... + /** + * 开始执行计划队列,返回控制对象。 + * 若已在移动则返回 null(不中断当前移动)。 + */ + start(): IMoverController | null; +} +``` + +## 6. 移动执行流程 + +每次 `start()` 启动后,执行器按以下总流程处理队列: + +触发 `onMoveStart` → 循环每步:[ +`abstract onStepStart(step, tile, mover)` 执行并返回 `code: number`(子类计算本步场景)→ +触发 `IObjectMoverHooks.onStepStart(code, step, tile, mover)` 钩子(渲染预处理,此时 `tile.x/y` 为**移动前坐标**)→ +等待所有 Promise → +`abstract onStepEnd(code, step, tile, mover)` 执行**逻辑效果**(子类根据 code 决定如何执行)→ +触发 `IObjectMoverHooks.onStepEnd(code, step, tile, mover)` 钩子(此时 `tile.x/y` 为**移动后坐标**) +] → 触发 `onMoveEnd`。 + +### 6.1 绝对方向步(`DynamicMoveStepType.Dir`) + +**onStepStart 阶段**(`tile.x/y` 为原始坐标): + +1. `abstract onStepStart(step, tile, mover)` 执行,子类计算本步 `code`(正常移动、碰墙等)并返回 `Promise`。 +2. 父类拿到 `code`,触发 `IObjectMoverHooks.onStepStart(code, step, tile, mover)` 钩子——渲染端在此阶段播放移动动画, + 可由 `step.move` 与当前 `tile.x/y` 推算目标位置。 +3. `await Promise.all(promises)`——等待全部动画完成。 + +**onStepEnd 阶段**(逻辑效果): + +1. `abstract onStepEnd(code, step, tile, mover)` 执行——子类依据 `code` 决定本步实际效果: + 更新 `tile.moveDirection = step.move`。 +2. 计算 `{dx, dy} = getFaceMovement(step.move)`,**立即**更新 `tile.x += dx`、`tile.y += dy`。 +3. 同步更新 `posMap`。 +4. 执行转向逻辑(见 [dynamic-tile.md §8](./dynamic-tile.md#8-转向逻辑)), + 更新 `tile.faceDirection` 与 `tile.num`。 +5. 子类返回后,父类触发 `IObjectMoverHooks.onStepEnd(code, step, tile, mover)` 钩子,此时 `tile.x/y` 已为新坐标。 + +### 6.1.1 含显式朝向的绝对方向步(`DynamicMoveStepType.DirFace`) + +与 `Dir` 步骤相同,但步骤 5 改为直接将 `tile.faceDirection` 设为 `step.face`, +并通过 `getFaceOf(tile.num, step.face)` 更新 `tile.num`,不经过 `degradeFace` 降级。 + +### 6.2 速度步(`DynamicMoveStepType.Speed`) + +更新内部速度状态,立即生效,无位移,无需等待。 + +### 6.3 纯转向步(`DynamicMoveStepType.Face`) + +经由 `onStepStart` 触发钩子(`tile.x/y` 不变,`step.type === DynamicMoveStepType.Face` 供渲染端区分); +等待渲染端动画完成后,`onStepEnd` 阶段执行转向逻辑,更新 `faceDirection` 与 `tile.num`, +触发 `onStepEnd` 钩子。 + +### 6.4 特殊步(`DynamicMoveStepType.Special`) + +**语义说明**:`forward`/`backward` 以**上一步的移动方向**(即当前 `tile.moveDirection`)为基准; +若无上一步,则回退到当前 `faceDirection`;若图块无朝向绑定,则抛出错误。 + +**Forward**(`DynamicSpecialStep.Forward`): + +1. `actualDir = tile.moveDirection`(不改变 moveDirection); +2. 执行与 `Dir` 步骤相同的位移流程; +3. 步骤 2:跳过(`moveDirection` 保持不变); +4. 步骤 5:执行转向逻辑,以 `actualDir` 更新 `tile.faceDirection` 与 `tile.num`。 + +**Backward**(`DynamicSpecialStep.Backward`): + +1. `actualDir = opposite(tile.moveDirection)`; +2. 在执行前临时将逆向动画标志设为开启(步骤完成后恢复); +3. 执行与 `Dir` 步骤相同的位移流程; +4. 步骤 2:`tile.moveDirection = actualDir`; +5. 步骤 5:执行转向逻辑,以 `actualDir` 更新 `tile.faceDirection` 与 `tile.num`。 + +### 6.5 动画方向步(`DynamicMoveStepType.AnimDir`) + +指定后续步骤动画是**正向播放**(`DynamicAnimDirection.Forward`)还是**逆向播放**(`DynamicAnimDirection.Backward`), +与图块朝向无关(朝向始终由 `faceDirection` 决定)。 +修改 mover 内部的 `currentAnimDir` 状态。 +渲染端在 `onStepStart` 收到 `AnimDir` 步骤时读取并更新本地动画方向状态,后续步骤的 `onStepStart` 触发时依据此状态决定播放方向。 +`onStepEnd` 阶段无逻辑操作,立即完成,不影响坐标与方向。 + +## 7. onStepStart / onStepEnd 钩子返回值 + +`IObjectMoverHooks.onStepStart` 与 `onStepEnd` 均为可选钩子(`?`),返回类型为 `Promise`, +执行器始终通过 `Promise.all` 等待所有订阅方完成后再继续。 + +关键时序:`onStepStart` 所有 Promise 兑现后,才执行逻辑效果(abstract onStepEnd); +`onStepEnd` 全部兑现后,才进入下一步。 +多个订阅方通过 `Promise.all` 并行等待,与 `forEachHook` 返回所有值的设计天然契合—— +`const promises = forEachHook(hook => hook.onStepStart?.(code, step, tile, mover));` +`await Promise.all(promises);` + +## 8. 与 IDynamicLayer / IDynamicTile 接口的关系 + +`IDynamicLayer` 和 `IDynamicTile` **仅保留 `step(dir)` 单步便捷接口** +(等价于 `tile.mover.step(dir).start()`),不再提供 `moveDynamicWith` / `moveWith` 等批量接口。 +复杂移动能力(`forward`/`backward`/`animDir` 等)通过 `tile.mover` 访问。 + +**渲染端订阅移动事件的流程**: + +1. 订阅 `IDynamicLayer.onCreateDynamicTile`; +2. 创建图块时获取 `tile.mover` 引用,直接向其添加钩子; +3. 移动事件从 `tile.mover` 触发,无需经过 `IDynamicLayer` 转发。 + +为此,`IDynamicMover` 扩展为可订阅对象, +实现 `IHookable>`(见接口定义汇总)。 + +--- + +# 涉及文件 + +## 需要修改的文件 + +### `@user/data-base/src/map/types.ts` + +修改现有接口(移动相关新增类型已移至 `move/types.ts`): + +- [ ] `IDynamicTile` 新增 `readonly mover: IDynamicMover` +- [ ] `IDynamicTile` 已有 `readonly faceDirection: FaceDirection` 与 + `readonly moveDirection: FaceDirection`(已在主文档确认) +- [ ] `IObjectMovable` 接口(`x`、`y`、`moveDirection`、`faceDirection` 只读属性 + + `setPos`/`setMoveDirection`/`setFaceDirection`,定义在 `dynamic-tile.md`) +- [ ] `IDynamicLayer` / `IDynamicTile` 仅保留 `step(dir)` 便捷接口, + 移除 `moveDynamicWith` / `moveDynamicStep` / `moveWith` / `moveStep` + (主文档接口汇总需同步更新) + +#### `@user/data-base/src/map/types.ts` + +- [ ] `DynamicMoveStepType` `const enum`: + 含 `Dir`/`DirFace`/`Speed`/`Face`/`Special`/`AnimDir` 六个成员 +- [ ] `DynamicSpecialStep` `const enum`:含 `Forward`/`Backward` 两个成员 +- [ ] `DynamicAnimDirection` `const enum`:含 `Forward`/`Backward` 两个成员 +- [ ] `IDynamicMoveStepDir` 接口(`type: DynamicMoveStepType.Dir`,`move: FaceDirection`) +- [ ] `IDynamicMoveStepDirFace` 接口(`type: DynamicMoveStepType.DirFace`,`move`/`face: FaceDirection`) +- [ ] `IDynamicMoveStepSpeed` 接口(`type: DynamicMoveStepType.Speed`) +- [ ] `IDynamicMoveStepFace` 接口(`type: DynamicMoveStepType.Face`) +- [ ] `IDynamicMoveStepSpecial` 接口(`type: Special`,`direction: DynamicSpecialStep`;无 `count` 字段,`forward(n)`/`backward(n)` 改为 push n 个步骤对象) +- [ ] `IDynamicMoveAnimDir` 接口(`type: DynamicMoveStepType.AnimDir`,`dir: DynamicAnimDirection`) +- [ ] `DynamicMoveStep` 联合类型(含以上六种步骤) +- [ ] `IMoverController` 接口 +- [ ] `IObjectMoverHooks` 接口(含 `onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`; + `onStepStart`/`onStepEnd` 参数顺序为 `(code, step, tile, mover)`,按使用频率排列) +- [ ] `IObjectMover` 接口(含 `faceDirection`/`moveDirection` 只读属性及全部计划构建方法;`step`/`stepFace` 含 `count?` 参数) +- [ ] `IDynamicMover extends IObjectMover`(无额外成员,继承基类全部能力;`IDynamicMoverHooks` 已移除,直接使用 `IObjectMoverHooks`) + +#### `@user/data-base/src/map/mover.ts` + +- [ ] `abstract class ObjectMover` 抽象基类: + - 实现 `IObjectMover`,在此提供全部**计划构建方法**(`step`/`stepFace`/`forward`/ + `backward`/`speed`/`face`/`animDir`/`clear`/`start`)及队列执行调度逻辑; + - 含四个**抽象方法**,子类必须实现,组成移动核心控制流: + - `abstract onMoveStart(tile, mover): Promise`: 整次移动开始时调用 + - `abstract onMoveEnd(tile, mover): Promise`: 整次移动结束时调用 + - `abstract onStepStart(step, tile, mover): Promise`: 单步逻辑执行前调用, + 子类计算并**返回** `code`;父类将 `code` 传给 `IObjectMoverHooks.onStepStart` 钩子后等待 + - `abstract onStepEnd(code, step, tile, mover): Promise`: 单步钩子等待完成后调用,子类依据 `code` 执行本步实际逻辑效果 +- [ ] `class DynamicMover extends ObjectMover implements IDynamicMover`: + 持有 `tile: IDynamicTile` 引用,维护 `moveQueue`、`moving`、 + `faceDirection`、`moveDirection` 状态;实现四个抽象方法(含 posMap 更新、转向逻辑等) +- [ ] `DynamicTile` 类新增 `readonly mover: DynamicMover`, + 在构造时以 `this` 为参数创建 + +--- + +# 接口定义汇总 + +```ts +const enum DynamicMoveStepType { + Dir, + DirFace, + Speed, + Face, + Special, + AnimDir +} + +const enum DynamicSpecialStep { + Forward, + Backward +} + +const enum DynamicAnimDirection { + Forward, + Backward +} + +interface IDynamicMoveStepDir { + type: DynamicMoveStepType.Dir; + move: FaceDirection; +} + +interface IDynamicMoveStepDirFace { + type: DynamicMoveStepType.DirFace; + move: FaceDirection; + face: FaceDirection; +} + +interface IDynamicMoveStepSpeed { + type: DynamicMoveStepType.Speed; + value: number; +} + +interface IDynamicMoveStepFace { + type: DynamicMoveStepType.Face; + value: FaceDirection; +} + +/** forward(n)/backward(n) 构建时 n>1 会 push n 个相同步骤,步骤对象本身不携带 count */ +interface IDynamicMoveStepSpecial { + type: DynamicMoveStepType.Special; + direction: DynamicSpecialStep; +} + +interface IDynamicMoveAnimDir { + type: DynamicMoveStepType.AnimDir; + dir: DynamicAnimDirection; +} + +type DynamicMoveStep = + | IDynamicMoveStepDir + | IDynamicMoveStepDirFace + | IDynamicMoveStepSpeed + | IDynamicMoveStepFace + | IDynamicMoveStepSpecial + | IDynamicMoveAnimDir; + +interface IMoverController { + readonly done: boolean; + readonly onEnd: Promise; + push(...steps: DynamicMoveStep[]): void; + stop(): Promise; +} + +interface IObjectMoverHooks extends IHookBase { + onMoveStart(mover: IObjectMover, tile: T): Promise; + onMoveEnd(mover: IObjectMover, tile: T): Promise; + /** 触发时 tile.x/y 为移动前坐标,适合渲染端在此播放动画 */ + onStepStart?( + code: number, + step: DynamicMoveStep, + tile: T, + mover: IObjectMover + ): Promise; + /** 触发时 tile.x/y 已更新为移动后坐标 */ + onStepEnd?( + code: number, + step: DynamicMoveStep, + tile: T, + mover: IObjectMover + ): Promise; +} + +interface IObjectMover extends IHookable< + IObjectMoverHooks +> { + readonly moving: boolean; + readonly tile: T; + readonly faceDirection: FaceDirection; + readonly moveDirection: FaceDirection; + + step(dir: FaceDirection, count?: number): this; + stepFace(move: FaceDirection, face: FaceDirection, count?: number): this; + forward(count?: number): this; + backward(count?: number): this; + speed(value: number): this; + face(dir: FaceDirection): this; + animDir(dir: DynamicAnimDirection): this; + clear(): this; + start(): IMoverController | null; +} + +// 这里的 onStepStart 不再接受 code 作为参数,因为这应当是其返回值 `Promise`, +// 具体来说,这四个方法是整个移动的核心方法而非钩子,它应当提供接口让子类实现, +// 子类应当真正执行移动效果,并进行一定的控制。显然子类是没办法知道 onStepStart 中的信息的, +// 所以才提供了一个 code,用于子类向 ObjectMover 提供信息,然后父类再传递给子类, +// 从而子类了解到 onStepStart 中的信息,再进一步决定这一步真正应该如何执行。 +// 而对于钩子,实际上是在子类的 onStepStart 执行完毕后进行的,所以是知道 code 的,所以才是参数。 +// 你应该好好想一想这之间的关系。 +// 以及子类是要求必须实现的,所以不应该会有返回 `void` 的场景,四个方法都应该返回 `Promise`。 +// 总的来说,父类是一个“系统级”的流程控制器,它通过这四个接口来实现真正的移动控制, +// 就像 legacy ObjectMoverBase 中的 startMove 一样。 +abstract class ObjectMover< + T extends IObjectMovable +> implements IObjectMover { + abstract onMoveStart(tile: T, mover: IObjectMover): Promise; + abstract onMoveEnd(tile: T, mover: IObjectMover): Promise; + /** 子类计算并返回本步 code;父类取得 code 后再触发 IObjectMoverHooks.onStepStart 钩子 */ + abstract onStepStart( + step: DynamicMoveStep, + tile: T, + mover: IObjectMover + ): Promise; + /** 子类依据 code 执行本步实际逻辑效果;父类在此之后触发 IObjectMoverHooks.onStepEnd 钩子 */ + abstract onStepEnd( + code: number, + step: DynamicMoveStep, + tile: T, + mover: IObjectMover + ): Promise; + // 计划构建方法(step/stepFace/forward/backward/speed/face/animDir/clear/start)在此实现 +} + +// faceDirection/moveDirection 已提升至 IObjectMover,IDynamicMover 无需额外声明 +// IDynamicMoverHooks 已移除,直接使用 IObjectMoverHooks +interface IDynamicMover extends IObjectMover { + // 继承自 IObjectMover:moving, faceDirection, moveDirection, tile, 全部计划构建方法及钩子 +} +``` + +--- + +# 问题 + +1. **`backward()` 的移动方向基准** ✅ 已确定 + + `backward()` 以 `opposite(tile.moveDirection)` 作为移动方向(基于 `moveDirection` 取反,非 `faceDirection`)。 + `moveDirection` 更新为反向,`faceDirection` 通过转向逻辑跟随同步更新。 + +2. **`forward()` 是否更新 faceDirection** ✅ 已确定 + + `forward()` 沿当前 `moveDirection` 移动,`moveDirection` 不变, + `faceDirection` 通过转向逻辑对齐为 `moveDirection`。 + +3. **转向步触发钩子方式** ✅ 已确定 + + 统一经由 `onStepStart(code, step, tile, mover)` 钩子处理,在步骤逻辑执行前触发。 + 渲染端通过 `step.type === DynamicMoveStepType.Face` 区分纯转向步与位移步; + `onStepStart` 触发时 `tile.x/y` 为原始坐标,渲染端可直接读取,无需反推。 + 转向逻辑(`faceDirection`/`num` 更新)在 `onStepEnd` 阶段执行。 + +4. **`animDir` 与渲染钩子集成** ✅ 已确定 + + `onStepStart` 统一接收所有步骤,`IDynamicMoveAnimDir` 步骤本身即为信息载体。 + 渲染端在 `onStepStart` 收到 `AnimDir` 步骤时更新本地动画方向状态; + 后续方向步的 `onStepStart` 触发时,渲染端依据本地状态决定动画播放方向。 + +5. **玩家移动与图块移动的复用** ✅ 已确定 + + 玩家(勇士)移动逻辑与 `IDynamicMover` 高度一致,共同抽象为 + `IObjectMover`(参考 legacy `ObjectMoverBase`,适配新 API); + 核心抽象类为 `abstract class ObjectMover`,含四个抽象方法组成控制流: + - **计划构建方法**(`step`/`stepFace`/`forward`/`backward`/ + `speed`/`face`/`animDir`/`clear`/`start`)全部在 `ObjectMover` 基类实现,子类无需重复声明。 + - **移动流程钩子**(`IObjectMoverHooks`,实现 `IHookable>`): + - `onMoveStart(mover, tile)`:整次移动队列开始时触发; + - `onMoveEnd(mover, tile)`:整次移动队列结束时触发; + - `onStepStart(code, step, tile, mover)`:单步开始前触发(`tile.x/y` 为移动前坐标); + - `onStepEnd(code, step, tile, mover)`:单步完成后触发(`tile.x/y` 已更新); + 其中 `code: number` 标识移动场景(如碰墙、遇敌、循环地图等),由具体实现层设置。 + - **四个抽象方法**(`onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`):子类(`DynamicMover`/`HeroMover`) + 通过实现这四个方法完全控制移动行为,执行流程由 `ObjectMover` 基类统一调度。 + 关键:`onStepStart` 返回 `Promise`(code),父类将此 code 传给钩子后再调用 `onStepEnd(code, ...)`, + 子类在 `onStepEnd` 中依据 code 执行实际状态变更。四个方法均返回 `Promise`,无 `void` 选项。 + - **泛型参数** `T extends IObjectMovable`:`IObjectMovable` 是可移动对象的最小接口 + (`x`、`y`、`moveDirection`、`faceDirection` 只读属性 + + `setPos`/`setMoveDirection`/`setFaceDirection` 方法),定义在 `dynamic-tile.md`; + `IDynamicTile extends IObjectMovable`,玩家侧使用 `IHeroMovable extends IObjectMovable`。 + - **`IDynamicMover extends IObjectMover`**: + 无额外成员,直接继承基类全部能力(含 `faceDirection`/`moveDirection` 及 `IObjectMoverHooks`)。 diff --git a/docs/dev/map/dynamic-tile.md b/docs/dev/map/dynamic-tile.md new file mode 100644 index 0000000..3d59a4e --- /dev/null +++ b/docs/dev/map/dynamic-tile.md @@ -0,0 +1,407 @@ +# 需求综述 + +当前地图系统仅支持静态图块(通过 `IMapLayer` / `Uint32Array` 存储), +无法支持可移动的动态图块(例如 NPC、推箱子、可交互物件等)。 +动态图块的核心特点: + +1. **允许重叠**——同一格点可同时存在多个动态图块; +2. **整数坐标**——始终处于格点整数坐标,不出现小数,便于渲染优化与交互; +3. **异步移动**——移动接口返回 `Promise`,配合渲染动画; +4. **朝向转向**——部分图块移动时需同步更新朝向(如四方向 NPC 行走图)。 + +--- + +# 实现思路 + +## 1. 动态图块标识方案 + +**采用引用标识**:`createDynamicTile` 与 `transferToDynamic` 均返回 +`IDynamicTile` 对象引用,调用方持有该引用作为后续操作(移动、删除、 +设置朝向等)的唯一凭证。无需维护任何 ID,无唯一性管理负担。 + +调用方可将返回的 `IDynamicTile` 引用存储在自己的数据结构中, +`tile.x`、`tile.y`、`tile.num`、`tile.direction` 等字段始终反映图块的 +最新状态——内部实现类持有可变字段,接口以 `readonly` 暴露给外部, +保证外部不能直接修改,内部可通过类实例更新。 + +## 2. 坐标表示 + +使用独立的 `x: number, y: number` 参数,与现有 `IMapLayer` 接口风格 +(如 `setBlock(block, x, y)`、`getBlock(x, y)`)保持一致,不引入 `ITileLocator`。 + +## 3. 方向表示 + +移动方向与转向方向统一使用 `FaceDirection` 枚举: + +- 语义明确,与已有 `IRoleFaceBinder`(朝向绑定)直接对接; +- 已有工具函数 `getFaceMovement(dir)` 可将其转换为坐标偏移,无需重复实现; +- 多步路径(`moveDynamicWith` 的 `steps` 参数)使用 `FaceDirection[]`。 + +`IDirectionDescriptor` 更偏向纯数学计算(用于范围迭代等), +语义上不适合表达「图块朝向某方向移动」的含义。 + +**已确定**:移动方向(`moveDirection`)与朝向(`faceDirection`)分离为两个独立字段。 +后退等相对移动必须依赖两者分离才能正确表达(例如面朝上、向下后退时,`faceDirection` +保持 `Up`,`moveDirection` 更新为 `Down`)。 + +移动系统另立文档独立设计,包含双方向模型、`IDynamicMover` 抽象、 +计划式移动接口及执行流程等完整方案,见 +[动态图块移动系统](./dynamic-tile-move.md)。 + +`moveDynamicStep` / `moveDynamicWith` 保留为命令式便捷接口,适用于单次简单移动; +复杂路径和计划式移动统一使用 `IDynamicMover`。 + +## 4. 动态图块数据模型与图层归属 + +动态图块按**所属静态图层**组织层级关系:每个 `IMapLayer` 持有一个 +`IDynamicLayer`,动态图块的渲染深度即为其所属 `IMapLayer` 的 `zIndex`, +与静态图块一致。因此 `IDynamicLayer` 接口挂载在 `IMapLayer` 上, +而不是 `ILayerState` 上(见第 5 节)。 + +每个动态图块存储以下信息: + +```ts +/** 可移动对象的最小公共接口,供 `IObjectMover` 泛型约束使用 */ +interface IObjectMovable { + readonly x: number; + readonly y: number; + readonly moveDirection: FaceDirection; + readonly faceDirection: FaceDirection; + setPos(x: number, y: number): void; + setMoveDirection(dir: FaceDirection): void; + setFaceDirection(dir: FaceDirection): void; +} + +interface IDynamicTile extends IObjectMovable { + readonly num: number; // 图块数字(决定渲染图像) + readonly layer: IDynamicLayer; // 所属动态图层 + readonly mover: IDynamicMover; // 绑定的移动器(计划式移动) + /** 删除此图块,转发至 layer.deleteDynamic */ + delete(): void; + /** 还原为静态图块,转发至 layer.transferToStatic */ + toStatic(): void; + /** 还原为静态图块,如果当前位置有东西则不转换,转发只 layer.transferToStaticIfSafe */ + toStaticIfSafe(): boolean; + /** + * 单步便捷移动接口,等价于 `mover.step(dir, count); return mover.start();`。 + * 适用于简单移动场景,复杂路径通过 `tile.mover` 访问。 + */ + step(dir: FaceDirection, count?: number): IMoverController | null; +} +``` + +`x`、`y`、`moveDirection`、`faceDirection` 及 `setPos`/`setMoveDirection`/`setFaceDirection` +均由 `IObjectMovable` 提供,`IDynamicTile` 不再重复声明。 +`IObjectMover` 以此接口为泛型约束, +使图块和玩家 mover 共享核心执行逻辑;具体渲染效果通过 `IObjectMoverHooks` 的 `onStepStart`/`onStepEnd` 钩子实现。 + +`DynamicLayer` 内部维护两个结构: + +- `tileSet: Set` — 所有图块的集合,用于迭代与归属判断; +- `posMap: Map>>` — 按坐标索引 + (外层 key = y,内层 key = x,value = 该格点所有图块对象集合), + 图块移动时同步更新,支持越界坐标(见第 4.1 节)。 + +调用方持有 `IDynamicTile` 对象引用即可完成所有操作, +`posMap` 仅供按坐标查询(`getDynamicTilesAt`)使用, +因使用频率低,嵌套 `Map` 的开销完全可以接受。 + +`IDynamicTile` 上的 `moveStep`/`moveWith`/`delete`/`setDirection`/`setPos`/`toStatic` +均为便捷转发方法,内部直接调用 `tile.layer` 对应接口,不修改任何内部状态。 +调用方可直接通过 `tile.xxx()` 操作,无需额外持有 `IDynamicLayer` 引用。 + +### 4.1 越界移动 + +动态图块的坐标不受楼层 `width`/`height` 约束,允许出现负值或超出地图范围的坐标。 +这满足了如「图块飞出屏幕」「过场动画」等临时越界需求。 +`DynamicLayer` 无需感知楼层尺寸,`resizeLayer` 也无需通知 `DynamicLayer`。 + +## 5. IDynamicLayer 挂载在 IMapLayer 上 + +动态图块的存储结构与 `IMapLayer`(Uint32Array)本质不同,独立封装为 `IDynamicLayer`。 +`IMapLayer` 新增 `readonly dynamicLayer: IDynamicLayer` 属性, +调用方通过具体的图层对象操作该层的动态图块,z 层级天然与静态图块一致。 + +`IDynamicLayer` 实现 `IHookable` 以支持渲染端订阅变更事件。 +`IRoleFaceBinder` 通过 `setFaceBinder(binder)` 方法注入(见第 8 节), +不在构造时传入,使多楼层共用同一个 `binder` 实例更灵活。 + +`transferToDynamic(x, y)` 不需要 `layer` 参数, +因为它隶属于某个具体的 `IDynamicLayer`,直接从同级 `IMapLayer` 读写图块即可, +并返回创建的 `IDynamicTile` 引用。 + +## 6. 移动函数的坐标来源 + +`moveDynamicStep(direction, tile)` 与 `moveDynamicWith(steps, tile)` 均不接受 +`ox, oy` 参数,改为直接接受 `IDynamicTile` 引用。当前坐标直接读取 `tile.x`、 +`tile.y`,无需调用方额外传入。 + +内部行为: + +- 直接读取 `tile.x`、`tile.y` 作为出发点; +- **立即**将图块逻辑坐标更新为目标位置(单步 `(tile.x+dx, tile.y+dy)`); +- 同步更新 `posMap`; +- 触发 `onMoveTile` 钩子,收集所有钩子返回的 `Promise | void`; +- `await Promise.all(promises)`—— + 等待所有渲染动画完成后,本步才算兑现,再进入下一步。 + +详细多步路径与计划式移动的执行流程见 +[动态图块移动系统](./dynamic-tile-move.md)。 + +## 7. transferToDynamic 与 transferToStatic + +`transferToDynamic(x, y)` 不需要额外的 `layer` 参数。 +`DynamicLayer` 在构造时持有对所属 `MapLayer` 的引用, +直接调用 `mapLayer.getBlock(x, y)` 读取图块数字,再调用 `mapLayer.setBlock(0, x, y)` 清除, +最后创建对应的动态图块并返回其引用。 +若该位置图块为 0(空白),则发出 logger 警告并仍然创建 `num = 0` 的动态图块。 + +新增 `transferToStaticIfSafe(tile)` 作为安全版本:仅当目标位置静态图块为 0(空白)时才执行还原, +否则不转换并返回 `false`;转换成功返回 `true`。适用于不确定目标格是否已有图块的场景。 + +新增 `transferToStatic(tile)` 作为逆操作:将动态图块还原为静态图块。 +对于「只移动一次就固定」的图块(如推箱子放到指定位置后不再移动), +及时转回静态存储可以降低存档体积、简化渲染订阅。执行流程: + +1. 读取 `tile.x`、`tile.y`、`tile.num`; +2. 若坐标不在所属 `MapLayer` 的合法范围内(即 `x < 0`、`y < 0`、 + `x >= width` 或 `y >= height`),发出 logger 警告并放弃操作; +3. 若 `mapLayer.getBlock(tile.x, tile.y) !== 0`,在开发环境下发出 logger 警告 + (目标格点已有静态图块内容,将被覆盖); + 调用 `mapLayer.setBlock(tile.num, tile.x, tile.y)` 写回静态图块; +4. 从 `tileSet` 和 `posMap` 中移除该图块,触发 `onDeleteTile` 钩子。 + +## 8. 转向逻辑 + +`moveDynamicStep` / `moveDynamicWith` 在每一步移动时自动更新朝向(使用双方向模型, +详见 [动态图块移动系统](./dynamic-tile-move.md)): + +1. 更新 `tile.moveDirection` 为本步实际移动方向; +2. 通过外部注入的 `IRoleFaceBinder` 调用 `getFaceOf(tile.num, direction)` 查询 + 该方向对应的图块数字; +3. 若图块无朝向绑定(返回 `null`),则不修改 `num`,仅更新 `faceDirection`; +4. 若有绑定,将 `tile.num` 与 `tile.faceDirection` 更新为查询结果。 + +`IRoleFaceBinder` 通过 `setFaceBinder(binder: IRoleFaceBinder): void` 方法注入, +不在构造时传入;初始状态视为无朝向绑定(`getFaceOf` 始终返回 `null`)。 +若多个楼层共用同一个 `RoleFaceBinder` 实例,直接对各楼层各图层分别调用 `setFaceBinder` 即可。 + +对于八方向移动,转向查询逻辑扩展如下: + +1. 更新 `tile.moveDirection` 为本步实际移动方向; +2. 调用 `getFaceOf(tile.num, direction)` 查询; +3. 若返回 `null` 且当前方向为斜向(八方向之一), + 调用 `degradeFace(direction)` 降级为四方向后再查询一次; +4. 若仍返回 `null`,不修改 `num`,仅更新 `faceDirection`; +5. 若查询到结果,将 `tile.num` 更新为结果图块数字,并更新 `faceDirection`。 + +手动设置朝向通过独立接口 `setDynamicDirection(tile, direction)` 完成, +逻辑与上述步骤 1–5 相同,但不触发移动。 + +## 9. IDynamicLayerHooks 与 ILayerStateHooks + +`IDynamicLayerHooks` 定义三个钩子: + +- `onCreateTile(tile: IDynamicTile)`:图块被创建(含转换)时触发; +- `onDeleteTile(tile: IDynamicTile)`:图块被删除时触发(传入删除前的快照); +- `onMoveTile(tile: IDynamicTile, fromX: number, fromY: number): Promise | void`: + 图块移动一步时触发,`tile` 为更新后的状态,`fromX`/`fromY` 为移动前坐标。 + 返回 `Promise` 时,移动器将等待其兑现后再进行下一步(配合 `Promise.all` 并行等待所有订阅方)。 + +`ILayerStateHooks` 新增对应的三个转发钩子,额外携带 `layer: IMapLayer` 参数, +与现有的 `onUpdateLayerArea`、`onResizeLayer` 等钩子风格一致: + +- `onCreateDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile)` +- `onDeleteDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile)` +- `onMoveDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile, fromX: number, fromY: number)` + +`LayerState` 在为每个 `MapLayer` 注册 `StateMapLayerHook` 时, +同步订阅该层 `dynamicLayer` 的三个钩子,将事件加上 `layer`、`dynamicLayer` 参数后转发至楼层钩子。 + +--- + +# 涉及文件 + +> 移动相关的接口(`DynamicMoveDir`、`IDynamicMoveStep`、`IMoverController`、`IDynamicMover`) +> 设计详情见 [动态图块移动系统](./dynamic-tile-move.md), +> 同样需要添加至 `types.ts`。 + +## 需要引用的文件 + +- `@motajs/common`:`IHookable`, `IHookBase`, `Hookable`, `logger` +- `@user/data-base/src/common`:`FaceDirection`, `IRoleFaceBinder`, + `getFaceMovement`, `degradeFace` +- `@user/data-base/src/map/types.ts`:`IMapLayer`, `IMapLayerHooks`, + `ILayerState`, `ILayerStateHooks` + +## 需要修改的文件 + +### `@user/data-base/src/map/types.ts` + +- [ ] 新增 `IObjectMovable` 接口: + 含 `x`、`y`、`moveDirection`、`faceDirection` 四个只读属性及 + `setPos`/`setMoveDirection`/`setFaceDirection` 三个方法; + 供 `IObjectMover` 泛型约束使用 +- [ ] 新增 `IDynamicTile extends IObjectMovable` 接口: + 含 `num`、`layer`、`mover` 三个只读属性及 `delete`/`toStatic` 两个便捷方法; + 无 `id` 字段,坐标/方向成员由 `IObjectMovable` 提供 +- [ ] 新增移动相关类型(详见移动文档接口定义汇总,已移至 `move/types.ts`): + `DynamicMoveStepType`、`DynamicMoveStep`、`IMoverController`、 + `IObjectMoverHooks`、`IObjectMover`、`IDynamicMover` +- [ ] 新增 `IDynamicLayerHooks extends IHookBase` 接口: + 含 `onCreateTile`、`onDeleteTile`、`onMoveTile` 三个钩子方法 +- [ ] 新增 `IDynamicLayer extends IHookable` 接口: + - [ ] `createDynamicTile(num, x, y): IDynamicTile`: 在指定位置创建动态图块, + 返回图块引用 + - [ ] `transferToDynamic(x, y): IDynamicTile`: 从所属静态图层读取并清除 + 指定位置图块,创建对应动态图块并返回引用 + - [ ] `transferToStatic(tile: IDynamicTile)`: 将动态图块还原为静态图块; + 坐标越界则警告并放弃,否则写回静态图层并触发 `onDeleteTile` + - [ ] `transferToStaticIfSafe(tile: IDynamicTile): boolean`: 仅当目标位置静态图块为 0 + 时才还原,否则不转换;返回是否转换成功 + - [ ] `deleteDynamicTile(tile: IDynamicTile)`: 删除指定图块,不在此层则警告 + - [ ] `getDynamicTilesAt(x, y)`: 获取指定格点所有动态图块的可迭代对象 + - [ ] `moveDynamicWith` / `moveDynamicStep` 已由 `IDynamicMover` 权其责。 + `IDynamicLayer` 不再提供此两个方法,删除对应条目。 + - [ ] `setDynamicDirection(tile: IDynamicTile, direction)`: 手动设置朝向, + 同步更新 `direction` 与 `num`(若有朝向绑定) + - [ ] `setDynamicPos(tile: IDynamicTile, x: number, y: number)`: 直接设置图块位置, + 同步更新 `posMap`,触发 `onMoveTile` 钩子,不更新朝向 + - [ ] `setFaceBinder(binder: IRoleFaceBinder)`: 注入朝向绑定器 +- [ ] 修改 `IMapLayer`:新增 `readonly dynamicLayer: IDynamicLayer` +- [ ] 修改 `ILayerStateHooks`:新增 `onCreateDynamicTile`、`onDeleteDynamicTile`、 + `onMoveDynamicTile` 三个转发钩子,额外携带 `layer: IMapLayer` 与 + `dynamicLayer: IDynamicLayer` 两个参数 + +### `@user/data-base/src/map/dynamicLayer.ts`(新文件) + +- [ ] 实现 `DynamicLayer extends Hookable` 类, + 实现 `IDynamicLayer` +- [ ] 实现内部类 `DynamicTile implements IDynamicTile`: + 持有 `layer: IDynamicLayer` 引用,`delete`/`toStatic` 转发至 `layer`; + `step(dir, count?)` 封装为便捷方法 +- [ ] `DynamicTile` 构造时同步创建 `readonly mover: DynamicMover`, + 移动调度由 `move/dynamicMover.ts` 中的 `DynamicMover` 类实现 +- [ ] `private mapLayer: IMapLayer`:构造参数,所属静态图层引用, + 供 `transferToDynamic` / `transferToStatic` 读写使用 +- [ ] `private faceBinder: IRoleFaceBinder | null = null`:朝向绑定器, + 通过 `setFaceBinder` 注入 +- [ ] `private tileSet: Set`:所有图块的集合,用于迭代与归属判断 +- [ ] `private posMap: Map>>`: + 按坐标索引(外层 key = y,内层 key = x),支持越界坐标 +- [ ] 实现全部接口方法,移动时同步更新 `tileSet` 与 `posMap` + +### `@user/data-base/src/map/mapLayer.ts` + +- [ ] 新增 `readonly dynamicLayer: DynamicLayer`:构造时以 `this` 为参数创建 + +### `@user/data-base/src/map/layerState.ts` + +- [ ] 修改 `StateMapLayerHook`(或 `addLayer` 中的订阅逻辑): + 在为每个 `MapLayer` 注册钩子时,同时订阅其 `dynamicLayer` 的三个钩子, + 将事件加上 `layer`、`dynamicLayer` 参数后转发至楼层的 `ILayerStateHooks` + +--- + +# 接口定义汇总 + +以下为本次新增与修改的完整接口签名,供实现时参考。 + +移动相关接口(`IObjectMoverHooks`、`IObjectMover`、`IDynamicMover`、`IMoverController`、`DynamicMoveStep` 等) +详见 [dynamic-tile-move.md](./dynamic-tile-move.md) 接口定义汇总,此处不再重复。 + +## 新增接口 + +### IObjectMovable + +```ts +interface IObjectMovable { + readonly x: number; + readonly y: number; + readonly moveDirection: FaceDirection; + readonly faceDirection: FaceDirection; + setPos(x: number, y: number): void; + setMoveDirection(dir: FaceDirection): void; + setFaceDirection(dir: FaceDirection): void; +} +``` + +### IDynamicTile + +```ts +interface IDynamicTile extends IObjectMovable { + readonly num: number; + readonly layer: IDynamicLayer; + readonly mover: IDynamicMover; + delete(): void; + toStatic(): void; + toStaticIfSafe(): boolean; + /** 等价于 mover.step(dir, count); return mover.start(); */ + step(dir: FaceDirection, count?: number): IMoverController | null; +} +``` + +### IDynamicLayerHooks + +```ts +interface IDynamicLayerHooks extends IHookBase { + onCreateTile(tile: IDynamicTile, layer: IDynamicLayer): void; + onDeleteTile(tile: IDynamicTile, layer: IDynamicLayer): void; +} +``` + +### IDynamicLayer + +```ts +interface IDynamicLayer extends IHookable { + createDynamic(num: number, x: number, y: number): IDynamicTile; + transferToDynamic(x: number, y: number): IDynamicTile; + transferToStatic(tile: IDynamicTile): void; + transferToStaticIfSafe(tile: IDynamicTile): boolean; + deleteDynamic(tile: IDynamicTile): void; + getDynamicTilesAt(x: number, y: number): Iterable; + setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void; + setDynamicPos(tile: IDynamicTile, x: number, y: number): void; + setFaceBinder(binder: IRoleFaceBinder): void; +} +``` + +## 修改接口 + +### IMapLayer(新增属性) + +```ts +interface IMapLayer { + // ...现有成员... + readonly dynamicLayer: IDynamicLayer; +} +``` + +--- + +# 问题 + +1. **`ox, oy` 是否为必要参数?** ✅ 已确定 + + 已确定移除。调用方持有 `IDynamicTile` 引用,坐标直接从 `tile.x`、`tile.y` 读取, + `ox, oy` 为冗余参数,接口简化为 `moveDynamicStep(direction, tile)` 和 + `moveDynamicWith(steps, tile)`。 + +2. **`IRoleFaceBinder` 的注入方式** ✅ 已确定 + + 已确定使用 `setFaceBinder(binder)` 方法注入,不在构造时传入。 + 实际修改 `faceBinder` 的场景极少,接口保持简单。 + +3. **动态图块的存档支持** ⏸ 暂缓 + + 动态图块状态(NPC 当前位置、推箱子位置等)属于游戏核心存档内容, + 长期来看必须纳入存档体系。但当前设计与旧引擎完全不同, + 存档格式需从头设计,建议在动态图块功能稳定后单独立项设计存档方案。 + **本次实现不涉及存档,`IDynamicLayer` 暂不实现 `ISaveableContent`。** + +4. **`IDynamicLayer` 是否需要感知楼层尺寸** ✅ 已确定 + + 已确定不感知。标识方案改为引用,`posMap` 同步改为 + `Map>>` 嵌套结构(外层 key = y,内层 key = x), + 彻底去掉字符串键,天然支持越界坐标,`resizeLayer` 无需通知 `DynamicLayer`。 + `getDynamicTilesAt` 接口保留,使用频率低,嵌套 Map 开销完全可接受。 diff --git a/docs/dev/map-store-improve.md b/docs/dev/map/map-store-improve.md similarity index 100% rename from docs/dev/map-store-improve.md rename to docs/dev/map/map-store-improve.md diff --git a/docs/dev/map-store-save.md b/docs/dev/map/map-store-save.md similarity index 100% rename from docs/dev/map-store-save.md rename to docs/dev/map/map-store-save.md diff --git a/docs/dev/save-system.md b/docs/dev/system/save-system.md similarity index 100% rename from docs/dev/save-system.md rename to docs/dev/system/save-system.md diff --git a/packages-user/data-base/src/common/index.ts b/packages-user/data-base/src/common/index.ts index 5fcbbcb..64a2b6f 100644 --- a/packages-user/data-base/src/common/index.ts +++ b/packages-user/data-base/src/common/index.ts @@ -1,3 +1,4 @@ export * from './face'; +export * from './mover'; export * from './types'; export * from './utils'; diff --git a/packages-user/data-base/src/common/mover.ts b/packages-user/data-base/src/common/mover.ts new file mode 100644 index 0000000..f6eb757 --- /dev/null +++ b/packages-user/data-base/src/common/mover.ts @@ -0,0 +1,579 @@ +import { + Hookable, + HookController, + IHookable, + IHookBase, + IHookController, + ITileLocator +} from '@motajs/common'; +import { FaceDirection } from './types'; + +//#region 对象移动 + +export const enum ObjectMoveStepType { + /** 绝对方向步,同步更新移动方向与朝向 */ + Dir, + /** 绝对方向步,显式指定朝向 */ + DirFace, + /** 速度步 */ + Speed, + /** 纯转向步 */ + Face, + /** 特殊步,如前进或后退 */ + Special, + /** 动画方向步 */ + AnimDir +} + +export const enum ObjectSpecialStep { + /** 前进 */ + Forward, + /** 后退 */ + Backward +} + +export const enum ObjectAnimDirection { + /** 正向播放动画 */ + Forward, + /** 反向播放动画 */ + Backward +} + +export interface IObjectMovable { + /** 当前横坐标 */ + readonly x: number; + /** 当前纵坐标 */ + readonly y: number; + + /** + * 设置对象位置 + * @param x 横坐标 + * @param y 纵坐标 + */ + setPos(x: number, y: number): void; + + /** + * 获取当前朝向 + */ + getCurrentFaceDirection(): FaceDirection; +} + +export interface IObjectMoveStepDir { + /** 步骤类型 */ + type: ObjectMoveStepType.Dir; + /** 本步移动方向 */ + move: FaceDirection; +} + +export interface IObjectMoveStepDirFace { + /** 步骤类型 */ + type: ObjectMoveStepType.DirFace; + /** 本步移动方向 */ + move: FaceDirection; + /** 本步显式朝向 */ + face: FaceDirection; +} + +export interface IObjectMoveStepSpeed { + /** 步骤类型 */ + type: ObjectMoveStepType.Speed; + /** 后续移动的每格耗时,单位为 ms */ + value: number; +} + +export interface IObjectMoveStepFace { + /** 步骤类型 */ + type: ObjectMoveStepType.Face; + /** 要设置的朝向 */ + value: FaceDirection; +} + +export interface IObjectMoveStepSpecial { + /** 步骤类型 */ + type: ObjectMoveStepType.Special; + /** 特殊步方向 */ + direction: ObjectSpecialStep; +} + +export interface IObjectMoveAnimDir { + /** 步骤类型 */ + type: ObjectMoveStepType.AnimDir; + /** 动画播放方向 */ + dir: ObjectAnimDirection; +} + +export type ObjectMoveStep = + | IObjectMoveStepDir + | IObjectMoveStepDirFace + | IObjectMoveStepSpeed + | IObjectMoveStepFace + | IObjectMoveStepSpecial + | IObjectMoveAnimDir; + +export interface IMoverController { + /** 本次移动是否已经全部完成 */ + done: boolean; + /** 当本次移动结束时兑现 */ + onEnd: Promise; + + /** + * 向当前移动队列末尾追加步骤 + * @param steps 要追加的步骤列表 + */ + push(...steps: ObjectMoveStep[]): void; + + /** + * 在当前步移动之后立刻插入指定步,顺序为参数传入的顺序 + * @param steps 要插入的步骤列表 + */ + insert(...steps: ObjectMoveStep[]): void; + + /** + * 停止当前移动,在当前步骤完成后兑现 + */ + stop(): Promise; +} + +export interface IObjectMoverHooks extends IHookBase { + /** + * 当移动开始时触发 + * @param tile 移动器绑定的图块 + * @param mover 当前移动器 + */ + onMoveStart?(tile: T, mover: IObjectMover): Promise; + + /** + * 当移动结束时触发 + * @param tile 移动器绑定的图块 + * @param mover 当前移动器 + */ + onMoveEnd?(tile: T, mover: IObjectMover): Promise; + + /** + * 当单步移动开始前触发,此时对象坐标仍为移动前坐标 + * @param code 当前步移动代码 + * @param step 当前步骤 + * @param tile 移动器绑定的图块 + * @param mover 当前移动器 + */ + onStepStart?( + code: number, + step: ObjectMoveStep, + tile: T, + mover: IObjectMover + ): Promise; + + /** + * 当单步移动结束后触发,此时对象坐标已更新为移动后坐标 + * @param code 当前步移动代码 + * @param step 当前步骤 + * @param tile 移动器绑定的图块 + * @param mover 当前移动器 + */ + onStepEnd?( + code: number, + step: ObjectMoveStep, + tile: T, + mover: IObjectMover + ): Promise; +} + +export interface IObjectMover extends IHookable< + IObjectMoverHooks +> { + /** 当前是否正在移动 */ + readonly moving: boolean; + /** 当前绑定的对象 */ + readonly tile: T; + /** 当前朝向 */ + readonly faceDirection: FaceDirection; + /** 当前移动方向 */ + readonly moveDirection: FaceDirection; + /** 当前动画播放方向 */ + readonly currAnimDir: ObjectAnimDirection; + /** 当前移动速度,单位毫秒 */ + readonly currentSpeed: number; + + /** + * 追加若干个绝对方向步,并同步更新移动方向与朝向 + * @param dir 移动方向 + * @param count 追加次数,默认 1 + */ + step(dir: FaceDirection, count?: number): this; + + /** + * 追加若干个绝对方向步,移动方向与朝向分别指定 + * @param move 移动方向 + * @param face 朝向方向 + * @param count 追加次数,默认 1 + */ + stepFace(move: FaceDirection, face: FaceDirection, count?: number): this; + + /** + * 追加若干个前进步,沿当前移动方向移动 + * @param count 追加次数,默认 1 + */ + forward(count?: number): this; + + /** + * 追加若干个后退步,沿当前移动方向的反方向移动 + * @param count 追加次数,默认 1 + */ + backward(count?: number): this; + + /** + * 设置后续移动速度 + * @param value 每格耗时,单位为 ms + */ + speed(value: number): this; + + /** + * 追加一个纯转向步骤 + * @param dir 目标朝向 + */ + face(dir: FaceDirection): this; + + /** + * 设置后续步骤的动画播放方向 + * @param dir 动画播放方向 + */ + animDir(dir: ObjectAnimDirection): this; + + /** + * 清空尚未执行的步骤队列 + */ + clear(): this; + + /** + * 开始执行当前计划队列 + * @returns 若当前已在移动,则返回 `null` + */ + start(): Readonly | null; +} + +//#endregion + +//#region 移动基类 + +export abstract class ObjectMover + extends Hookable> + implements IObjectMover +{ + abstract readonly tile: T; + + /** 尚未开始执行的移动步骤队列 */ + protected readonly moveQueue: Readonly[] = []; + + /** 当前是否正在移动 */ + moving: boolean = false; + /** 当前朝向 */ + faceDirection: FaceDirection = FaceDirection.Unknown; + /** 当前移动方向 */ + moveDirection: FaceDirection = FaceDirection.Unknown; + /** 当前动画播放方向 */ + currAnimDir: ObjectAnimDirection = ObjectAnimDirection.Forward; + /** 当前移动速度,单位毫秒 */ + currentSpeed: number = 100; + + /** 是否调用了 `IMoverController.stop` 接口 */ + private shouldStop: boolean = false; + + protected createController( + hook: Partial> + ): IHookController> { + return new HookController(this, hook); + } + + /** + * 当移动开始时执行 + * @param tile 移动图块 + * @param controller 移动控制器 + */ + protected abstract onMoveStart( + tile: T, + controller: Readonly + ): Promise; + + /** + * 当移动结束时触发 + * @param tile 移动图块 + * @param controller 移动控制器 + */ + protected abstract onMoveEnd( + tile: T, + controller: Readonly + ): Promise; + + /** + * 当单步移动开始时触发,返回移动代码,此移动代码将会传递至 {@link onStepEnd} + * @param step 当前移动步对象 + * @param tile 移动图块 + * @param controller 移动控制器 + */ + protected abstract onStepStart( + step: ObjectMoveStep, + tile: T, + controller: Readonly + ): Promise; + + /** + * 当单步移动结束时触发,返回坐标对象代表这一步移动结果 + * @param code 移动代码,由 {@link onStepStart} 返回值传递而来 + * @param step 当前移动步对象 + * @param tile 移动图块 + * @param controller 移动控制器 + */ + protected abstract onStepEnd( + code: number, + step: ObjectMoveStep, + tile: T, + controller: Readonly + ): Promise; + + /** + * 向计划队列末尾追加一个步骤 + * @param step 要追加的步骤 + */ + protected pushStep(step: Readonly): void { + this.moveQueue.push(step); + } + + /** + * 获取当前应当作为相对移动基准的方向 + */ + private getCurrentDirection(): FaceDirection { + if (this.moveDirection !== FaceDirection.Unknown) { + return this.moveDirection; + } else { + return this.faceDirection; + } + } + + // TODO: 需要做一个朝向系统以解决朝向难以处理的问题 + /** + * 获取指定方向的反方向 + * @param dir 当前方向 + */ + private getOppositeDirection(dir: FaceDirection): FaceDirection { + switch (dir) { + case FaceDirection.Left: + return FaceDirection.Right; + case FaceDirection.Up: + return FaceDirection.Down; + case FaceDirection.Right: + return FaceDirection.Left; + case FaceDirection.Down: + return FaceDirection.Up; + case FaceDirection.LeftUp: + return FaceDirection.RightDown; + case FaceDirection.RightUp: + return FaceDirection.LeftDown; + case FaceDirection.LeftDown: + return FaceDirection.RightUp; + case FaceDirection.RightDown: + return FaceDirection.LeftUp; + case FaceDirection.Unknown: + return FaceDirection.Unknown; + } + } + + /** + * 根据步骤内容预先同步移动器内部状态 + * @param step 当前步骤 + */ + private prepareStep(step: ObjectMoveStep): void { + switch (step.type) { + case ObjectMoveStepType.Dir: + this.moveDirection = step.move; + this.faceDirection = step.move; + break; + case ObjectMoveStepType.DirFace: + this.moveDirection = step.move; + this.faceDirection = step.face; + break; + case ObjectMoveStepType.Face: + this.faceDirection = step.value; + break; + case ObjectMoveStepType.Special: { + const dir = this.getCurrentDirection(); + if (step.direction === ObjectSpecialStep.Backward) { + const opposite = this.getOppositeDirection(dir); + this.moveDirection = opposite; + this.faceDirection = opposite; + } else { + this.moveDirection = dir; + this.faceDirection = dir; + } + break; + } + case ObjectMoveStepType.AnimDir: + this.currAnimDir = step.dir; + break; + case ObjectMoveStepType.Speed: + this.currentSpeed = step.value; + break; + } + } + + step(dir: FaceDirection, count: number = 1): this { + for (let i = 0; i < count; i++) { + this.pushStep({ + type: ObjectMoveStepType.Dir, + move: dir + }); + } + return this; + } + + stepFace( + move: FaceDirection, + face: FaceDirection, + count: number = 1 + ): this { + for (let i = 0; i < count; i++) { + this.pushStep({ + type: ObjectMoveStepType.DirFace, + move, + face + }); + } + return this; + } + + forward(count: number = 1): this { + for (let i = 0; i < count; i++) { + this.pushStep({ + type: ObjectMoveStepType.Special, + direction: ObjectSpecialStep.Forward + }); + } + return this; + } + + backward(count: number = 1): this { + for (let i = 0; i < count; i++) { + this.pushStep({ + type: ObjectMoveStepType.Special, + direction: ObjectSpecialStep.Backward + }); + } + return this; + } + + speed(value: number): this { + this.pushStep({ + type: ObjectMoveStepType.Speed, + value + }); + return this; + } + + face(dir: FaceDirection): this { + this.pushStep({ + type: ObjectMoveStepType.Face, + value: dir + }); + return this; + } + + animDir(dir: ObjectAnimDirection): this { + this.pushStep({ + type: ObjectMoveStepType.AnimDir, + dir + }); + return this; + } + + clear(): this { + this.moveQueue.length = 0; + return this; + } + + /** + * 开始移动流程 + * @param queue 移动队列 + */ + private async moveProgress( + queue: ObjectMoveStep[], + controller: Readonly + ) { + // 移动开始 + await this.onMoveStart(this.tile, controller); + await Promise.all( + this.forEachHook(hook => { + return hook.onMoveStart?.(this.tile, this); + }) + ); + + // 移动流程 + while (queue.length > 0) { + const step = queue.shift(); + if (!step || !this.moving || this.shouldStop) break; + this.prepareStep(step); + const code = await this.onStepStart(step, this.tile, controller); + const stepStartHooks = this.forEachHook(hook => + hook.onStepStart?.(code, step, this.tile, this) + ); + await Promise.all(stepStartHooks); + const loc = await this.onStepEnd(code, step, this.tile, controller); + this.tile.setPos(loc.x, loc.y); + const stepEndHooks = this.forEachHook(hook => + hook.onStepEnd?.(code, step, this.tile, this) + ); + await Promise.all(stepEndHooks); + } + + // 移动结束 + await this.onMoveEnd(this.tile, controller); + const moveEndHooks = this.forEachHook(hook => + hook.onMoveEnd?.(this.tile, this) + ); + await Promise.all(moveEndHooks); + } + + start(): IMoverController | null { + if (this.moving) return null; + + const queue = this.moveQueue.slice(); + this.clear(); + this.shouldStop = false; + + this.faceDirection = this.tile.getCurrentFaceDirection(); + this.moveDirection = FaceDirection.Unknown; + + const { promise, resolve } = Promise.withResolvers(); + + const controller: IMoverController = { + done: false, + onEnd: promise, + push: (...steps: ObjectMoveStep[]) => { + if (!this.moving || this.shouldStop || controller.done) { + return; + } + queue.push(...steps); + }, + insert: (...steps: ObjectMoveStep[]) => { + if (!this.moving || this.shouldStop || controller.done) { + return; + } + queue.unshift(...steps); + }, + stop: () => { + this.shouldStop = true; + return controller.onEnd; + } + }; + + this.moving = true; + const moving = this.moveProgress(queue, controller); + moving.then(() => { + this.moving = false; + controller.done = true; + resolve(); + }); + + return controller; + } +} + +//#endregion diff --git a/packages-user/data-base/src/map/dynamicLayer.ts b/packages-user/data-base/src/map/dynamicLayer.ts new file mode 100644 index 0000000..31a4026 --- /dev/null +++ b/packages-user/data-base/src/map/dynamicLayer.ts @@ -0,0 +1,142 @@ +import { + HookController, + Hookable, + IHookController, + ITileLocator, + logger +} from '@motajs/common'; +import { + IDynamicLayer, + IDynamicLayerHooks, + IDynamicTile, + IMapLayer +} from './types'; +import { FaceDirection, degradeFace } from '../common'; +import { DynamicTile } from './dynamicTile'; + +export class DynamicLayer + extends Hookable + implements IDynamicLayer +{ + /** 坐标到动态图块集合的映射,外层 key = y,内层 key = x,不使用 index 是为了支持地图外图块 */ + private readonly tilePosMap: Map>> = + new Map(); + /** 动态图块到其当前坐标的映射 */ + private readonly posTileMap: Map = new Map(); + + constructor(public readonly layer: IMapLayer) { + super(); + } + + protected createController( + hook: Partial + ): IHookController { + return new HookController(this, hook); + } + + createDynamic(num: number, x: number, y: number): IDynamicTile { + const tile = new DynamicTile(num, x, y, this); + this.addTileToPosMap(tile, x, y); + this.posTileMap.set(tile, { x, y }); + this.forEachHook(hook => hook.onCreateTile?.(tile, this)); + return tile; + } + + transferToDynamic(x: number, y: number): IDynamicTile { + const num = this.layer.getBlock(x, y); + if (num === 0) { + logger.warn(127, x.toString(), y.toString()); + } + this.layer.setBlock(0, x, y); + return this.createDynamic(num, x, y); + } + + transferToStatic(tile: IDynamicTile): void { + const { x, y } = tile; + const { width, height } = this.layer; + if (x < 0 || y < 0 || x >= width || y >= height) { + logger.warn(128, x.toString(), y.toString()); + return; + } + if (this.layer.getBlock(x, y) !== 0) { + logger.warn(129, x.toString(), y.toString()); + } + this.layer.setBlock(tile.num, x, y); + this.removeTile(tile); + this.forEachHook(hook => { + void hook.onDeleteTile?.(tile, this); + }); + } + + transferToStaticIfSafe(tile: IDynamicTile): boolean { + if (this.layer.getBlock(tile.x, tile.y) !== 0) return false; + this.layer.setBlock(tile.num, tile.x, tile.y); + this.deleteDynamic(tile); + return true; + } + + async deleteDynamic(tile: IDynamicTile): Promise { + if (!this.posTileMap.has(tile)) { + logger.warn(130); + return; + } + this.removeTile(tile); + const hooks = this.forEachHook(hook => hook.onDeleteTile?.(tile, this)); + await Promise.all(hooks); + } + + getDynamicTilesAt(x: number, y: number): Iterable { + return this.tilePosMap.get(y)?.get(x) ?? new Set(); + } + + iterateDynamicTiles(): Iterable { + return this.posTileMap.keys(); + } + + setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void { + const numBefore = tile.num; + tile.setFaceDirection(direction); + if (tile.num !== numBefore) return; + const degraded = degradeFace(direction); + if (degraded !== direction) { + tile.setFaceDirection(degraded); + } + } + + updateDynamicTile(tile: IDynamicTile): void { + const oldPos = this.posTileMap.get(tile); + if (oldPos) { + this.removeTileFromPosMap(tile, oldPos.x, oldPos.y); + oldPos.x = tile.x; + oldPos.y = tile.y; + this.addTileToPosMap(tile, tile.x, tile.y); + } else { + this.addTileToPosMap(tile, tile.x, tile.y); + this.posTileMap.set(tile, { x: tile.x, y: tile.y }); + } + this.forEachHook(hook => hook.onUpdateTilePosition?.(tile, this)); + } + + private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void { + const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map()); + const set = xMap.getOrInsertComputed(x, () => new Set()); + set.add(tile); + } + + private removeTileFromPosMap( + tile: IDynamicTile, + x: number, + y: number + ): void { + this.tilePosMap.get(y)?.get(x)?.delete(tile); + } + + /** 从两个内部映射中移除图块记录 */ + private removeTile(tile: IDynamicTile): void { + const pos = this.posTileMap.get(tile); + if (pos) { + this.removeTileFromPosMap(tile, pos.x, pos.y); + } + this.posTileMap.delete(tile); + } +} diff --git a/packages-user/data-base/src/map/dynamicTile.ts b/packages-user/data-base/src/map/dynamicTile.ts new file mode 100644 index 0000000..f4702ed --- /dev/null +++ b/packages-user/data-base/src/map/dynamicTile.ts @@ -0,0 +1,72 @@ +import { isNil } from 'lodash-es'; +import { + FaceDirection, + IMoverController, + IObjectMover, + IRoleFaceBinder +} from '../common'; +import { IDynamicLayer, IDynamicTile } from './types'; +import { DynamicTileMover } from './mover'; + +export class DynamicTile implements IDynamicTile { + readonly mover: IObjectMover; + + /** 当前的朝向绑定对象 */ + private face: IRoleFaceBinder | null = null; + + constructor( + public num: number, + public x: number, + public y: number, + public readonly layer: IDynamicLayer + ) { + this.mover = new DynamicTileMover(this); + } + + setFaceBinder(binder: IRoleFaceBinder | null): void { + this.face = binder; + } + + setFaceDirection(direction: FaceDirection): number { + if (!this.face) return this.num; + const next = this.face.getFaceOf(this.num, direction); + if (next) { + this.num = next.identifier; + } + return this.num; + } + + delete(): Promise { + return this.layer.deleteDynamic(this); + } + + toStatic(): void { + this.layer.transferToStatic(this); + } + + toStaticIfSafe(): boolean { + return this.layer.transferToStaticIfSafe(this); + } + + step(dir: FaceDirection, count?: number): IMoverController | null { + if (this.mover.moving) return null; + this.mover.step(dir, count); + return this.mover.start(); + } + + setPos(x: number, y: number): void { + this.x = x; + this.y = y; + this.layer.updateDynamicTile(this); + } + + getCurrentFaceDirection(): FaceDirection { + if (this.face) { + const face = this.face.getFaceDirection(this.num); + if (isNil(face)) return FaceDirection.Unknown; + else return face; + } else { + return FaceDirection.Down; + } + } +} diff --git a/packages-user/data-base/src/map/index.ts b/packages-user/data-base/src/map/index.ts index 8a6081a..55e9fdb 100644 --- a/packages-user/data-base/src/map/index.ts +++ b/packages-user/data-base/src/map/index.ts @@ -1,4 +1,5 @@ export * from './layerState'; export * from './mapLayer'; export * from './mapStore'; +export * from './mover'; export * from './types'; diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index 326c4cf..372d468 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -1,11 +1,13 @@ import { isNil } from 'lodash-es'; import { + IDynamicLayer, IMapLayer, IMapLayerData, IMapLayerHookController, IMapLayerHooks } from './types'; import { Hookable, HookController, logger } from '@motajs/common'; +import { DynamicLayer } from './dynamicLayer'; // todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口 @@ -23,6 +25,8 @@ export class MapLayer /** 地图数据引用 */ private mapData: IMapLayerData; + readonly dynamicLayer: IDynamicLayer; + constructor(array: Uint32Array, width: number, height: number) { super(); this.width = width; @@ -35,6 +39,7 @@ export class MapLayer expired: false, array: this.mapArray }; + this.dynamicLayer = new DynamicLayer(this); } resize(width: number, height: number): void { diff --git a/packages-user/data-base/src/map/mover.ts b/packages-user/data-base/src/map/mover.ts new file mode 100644 index 0000000..c6cd03a --- /dev/null +++ b/packages-user/data-base/src/map/mover.ts @@ -0,0 +1,78 @@ +import { ITileLocator, logger } from '@motajs/common'; +import { + getFaceMovement, + ObjectMover, + ObjectMoveStep, + ObjectMoveStepType +} from '../common'; +import { IDynamicTile } from './types'; + +//#region 动态图块 + +const enum DynamicMoveCode { + /** 正常执行 */ + Success +} + +export class DynamicTileMover extends ObjectMover { + constructor(public readonly tile: IDynamicTile) { + super(); + } + + protected onMoveStart(): Promise { + return Promise.resolve(); + } + + protected onMoveEnd(): Promise { + return Promise.resolve(); + } + + protected onStepStart(): Promise { + return Promise.resolve(DynamicMoveCode.Success); + } + + protected onStepEnd( + code: number, + step: ObjectMoveStep, + tile: IDynamicTile + ): Promise { + if (code !== DynamicMoveCode.Success) { + logger.warn(126, 'DynamicMoveCode.Success (0)', code.toString()); + return Promise.resolve({ x: tile.x, y: tile.y }); + } + const locator: ITileLocator = { + x: tile.x, + y: tile.y + }; + switch (step.type) { + case ObjectMoveStepType.Dir: { + const { x, y } = getFaceMovement(step.move); + tile.setFaceDirection(step.move); + locator.x += x; + locator.y += y; + break; + } + case ObjectMoveStepType.DirFace: { + const { x, y } = getFaceMovement(step.move); + tile.setFaceDirection(step.face); + locator.x += x; + locator.y += y; + break; + } + case ObjectMoveStepType.Face: { + tile.setFaceDirection(step.value); + break; + } + case ObjectMoveStepType.Special: { + const { x, y } = getFaceMovement(this.moveDirection); + tile.setFaceDirection(this.faceDirection); + locator.x += x; + locator.y += y; + break; + } + } + return Promise.resolve(locator); + } +} + +//#endregion diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 47f0bfc..49e6da5 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -1,5 +1,14 @@ import { IHookable, IHookBase, IHookController } from '@motajs/common'; -import { ISaveableContent } from '../common'; +import { + FaceDirection, + IMoverController, + IObjectMovable, + IObjectMover, + IRoleFaceBinder, + ISaveableContent +} from '../common'; + +//#region 静态图层 export interface IMapLayerData { /** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */ @@ -75,6 +84,9 @@ export interface IMapLayer extends IHookable< /** 图层纵深 */ readonly zIndex: number; + /** 此图层对应的动态图块图层,z 层级与静态图块一致 */ + readonly dynamicLayer: IDynamicLayer; + /** * 设置某一点的图块 * @param block 图块数字 @@ -154,6 +166,10 @@ export interface IMapLayer extends IHookable< setMapRef(array: Uint32Array): void; } +//#endregion + +//#region 图层管理 + export interface ILayerStateHooks extends IHookBase { /** * 当设置背景图块时执行,如果设置的背景图块与原先一样,则不会执行 @@ -288,6 +304,10 @@ export interface ILayerState extends IHookable { setDirty(dirty: boolean): void; } +//#endregion + +//#region 楼层管理 + /** 单个 MapLayer 的存档数据 */ export interface IMapLayerSave { readonly width: number; @@ -428,3 +448,149 @@ export interface IMapStore extends ISaveableContent { */ notifyEnterFloor(id: string): void; } + +//#endregion + +//#region 动态图块 + +export interface IDynamicLayerHooks extends IHookBase { + /** + * 当图块被创建(含从静态图块转换)时触发 + * @param tile 被创建的动态图块 + * @param layer 所属的动态图层 + */ + onCreateTile(tile: IDynamicTile, layer: IDynamicLayer): void; + + /** + * 当图块被删除时触发 + * @param tile 被删除的动态图块 + * @param layer 所属的动态图层 + */ + onDeleteTile(tile: IDynamicTile, layer: IDynamicLayer): Promise; + + /** + * 当更新动态图块的位置时触发(包括使用 `mover` 触发的移动) + * @param tile 更新位置的图块 + * @param layer 所属的动态图层 + */ + onUpdateTilePosition(tile: IDynamicTile, layer: IDynamicLayer): void; +} + +export interface IDynamicLayer extends IHookable { + /** 当前动态图层所属的静态图层 */ + readonly layer: IMapLayer; + + /** + * 在指定位置创建一个动态图块 + * @param num 图块数字 + * @param x 横坐标 + * @param y 纵坐标 + * @returns 创建的动态图块引用 + */ + createDynamic(num: number, x: number, y: number): IDynamicTile; + + /** + * 从所属静态图层读取并清除指定位置的图块,创建对应动态图块并返回引用。 + * 若该位置图块为 0,则发出警告并仍然创建 `num = 0` 的动态图块 + * @param x 横坐标 + * @param y 纵坐标 + * @returns 创建的动态图块引用 + */ + transferToDynamic(x: number, y: number): IDynamicTile; + + /** + * 将动态图块还原为静态图块。坐标越界则警告并放弃, + * 否则写回静态图层并触发 {@link IDynamicLayerHooks.onDeleteTile} + * @param tile 要还原的动态图块 + */ + transferToStatic(tile: IDynamicTile): void; + + /** + * 仅当目标位置静态图块为 0(空白)时才还原为静态图块,否则不转换 + * @param tile 要还原的动态图块 + * @returns 是否转换成功 + */ + transferToStaticIfSafe(tile: IDynamicTile): boolean; + + /** + * 删除指定动态图块,触发 {@link IDynamicLayerHooks.onDeleteTile} 钩子。 + * 若图块不属于此层则发出警告 + * @param tile 要删除的动态图块 + */ + deleteDynamic(tile: IDynamicTile): Promise; + + /** + * 获取指定格点上所有动态图块的可迭代对象 + * @param x 横坐标 + * @param y 纵坐标 + */ + getDynamicTilesAt(x: number, y: number): Iterable; + + /** + * 迭代所有的动态图块 + */ + iterateDynamicTiles(): Iterable; + + /** + * 手动设置动态图块的朝向,更新 `tile.num`(若有朝向绑定)。 + * 转向逻辑与移动时的转向逻辑相同,但不触发移动 + * @param tile 要设置朝向的动态图块 + * @param direction 目标朝向 + */ + setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void; + + /** + * 更新图块内部存储位置 + * @param tile 动态图块 + */ + updateDynamicTile(tile: IDynamicTile): void; +} + +export interface IDynamicTile extends IObjectMovable { + /** 当前图块数字 */ + readonly num: number; + /** 当前图块所属的动态图层 */ + readonly layer: IDynamicLayer; + /** 当前动态图块的移动器 */ + readonly mover: IObjectMover; + + /** + * 设置图块朝向,会一并修改 {@link num},返回设置后的当前图块数字 + * @param direction 图块朝向 + */ + setFaceDirection(direction: FaceDirection): number; + + /** + * 直接删除此图块 + */ + delete(): Promise; + + /** + * 将当前图块还原为静态图块 + */ + toStatic(): void; + + /** + * 还原为静态图块,如果当前位置有东西则不转换 + */ + toStaticIfSafe(): boolean; + + /** + * 单步便捷移动接口,适用于简单移动场景,复杂路径通过 `tile.mover` 访问。 + * 等价于: + * + * ```ts + * mover.step(dir, count); + * return mover.start(); + * ``` + */ + step(dir: FaceDirection, count?: number): IMoverController | null; + + /** + * 注入朝向绑定器,初始状态视为无朝向绑定 + * @param binder 朝向绑定器 + */ + setFaceBinder(binder: IRoleFaceBinder | null): void; +} + +//#endregion diff --git a/packages-user/data-state/src/enemy/calculator.ts b/packages-user/data-state/src/enemy/calculator.ts index 364ba32..4831ba6 100644 --- a/packages-user/data-state/src/enemy/calculator.ts +++ b/packages-user/data-state/src/enemy/calculator.ts @@ -16,10 +16,6 @@ export class MainDamageCalculator implements IDamageCalculator< /** 当前是否正在计算支援怪的伤害 */ private inGuard: boolean = false; - /** - * 计算战斗伤害信息 - * @param handler 信息对象 - */ calculate( handler: IReadonlyEnemyHandler ): IEnemyDamageInfo { @@ -153,11 +149,6 @@ export class MainDamageCalculator implements IDamageCalculator< }; } - /** - * 获取临界计算的上界 - * @param handler 信息对象 - * @param attribute 目标属性名 - */ getCriticalLimit( handler: IReadonlyEnemyHandler, attribute: CriticalableHeroStatus diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 20fe5f9..571c3b9 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -183,6 +183,11 @@ "123": "MapLayer.setMapRef: array length $1 does not match expected size $2, setMapRef will be ignored.", "124": "MapStore.loadState: floor '$1' or its layer(s) not found in current reference data, skipping.", "125": "Expected sorted floor id array has a same floor id set, but an array with a different floor id set is returned.", + "126": "Only move code '$1' is expected, but got '$2'.", + "127": "Block at $1,$2 is 0, the dynamic tile will have num 0.", + "128": "Position $1,$2 is out of map bounds, operation cancelled.", + "129": "Position $1,$2 is not empty, existing block will be overwritten.", + "130": "The given tile is not managed by this dynamic layer.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } } diff --git a/prompt.md b/prompt.md index 1d7a51f..f0bd64b 100644 --- a/prompt.md +++ b/prompt.md @@ -2,11 +2,12 @@ 以下规则必须时刻遵守,任何情况下都不允许违反。 -1. **不擅自修改已有代码**:将我已经写好的代码视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在逻辑错误,应在对话中提出,而不是直接修改。 +1. **不擅自修改已有代码**:将我已经写好的代码视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在逻辑错误,应在对话中提出,而不是直接修改,不要做任何“顺手”的事。 2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。 3. **以目的驱动,而非以接口驱动**:实现前先想清楚我为什么要这样设计接口、这个接口设计的目的是什么,而不是单纯地以将接口填满为目标。 4. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。 -5. **按我说的方式重构**:若目标是重构某个接口,按照我指定的方式执行: +5. **接口缺失时停止并提问**:若完成需求所需的接口尚不存在,应立即停止实现,向我提出疑问,而不是擅自新增接口来推进。 +6. **按我说的方式重构**:若目标是重构某个接口,按照我指定的方式执行: - **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑与思路上的参考。 - **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。 @@ -17,6 +18,8 @@ 1. **合理参考建议**:我有时会在对话中给出实现建议,应合理参考,切忌滥用。 2. **以类型标注为参考依据**:实现与类型标注有冲突时,以类型标注(一般是 `types.ts`)中的内容为准。 3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。 +4. **接口设计兼顾合理性与便捷性**:设计接口时不仅要考虑合理性,还要考虑使用便捷性。罕见场景应当被支持,但不应与常见场景共用同一接口——这只会增加常见场景的使用难度。 +5. **避免多余的非空判断与类型守卫**:若某个类型已满足目标的类型约束,不应再对其添加任何判断或过滤操作。典型例子:`Promise.all` 对数组元素类型没有任何限制,传入 `(Promise | void)[]` 完全合法,无需多此一举地写成 `Promise.all(arr.filter(v => !!v))`。 **时刻谨记上述要求,避免一个需求反复修改仍无法满足预期。** @@ -29,7 +32,7 @@ ## 示例文档 -大致按照以下格式编写,如某部分需要详细描述,可单独开设标题。我会使用引用块的形式在文档中提出建议或回答。 +大致按照以下格式编写,如某部分需要详细描述,可单独开设标题;若某个接口内容较多,也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。 ```md # 需求综述