feat: 动态图层

This commit is contained in:
unanmed 2026-05-11 23:35:46 +08:00
parent 4c48b63e7e
commit fbdd609001
19 changed files with 2063 additions and 15 deletions

7
dev.md
View File

@ -57,13 +57,15 @@
### 注释规范 ### 注释规范
- 常用属性成员、方法、接口、类型必须添加 `jsDoc` 注释。 - 公共方法、接口必须在**源头处**(多数情况下为 `interface`)添加 `jsDoc` 注释;其他常用成员、方法、类型也必须添加注释(含义极为明确或极少使用的可例外,但建议全部添加)。
- 继承或 `implements` 而来的 API方法、成员等若注释说明无需变更则**不应重复添加** `jsDoc` 注释。
- 不对构造器添加注释。若构造器使用了属性声明语法(`constructor(public prop: T)`)且成员需要说明,可仅对该成员添加参数注释,不写构造器描述。**在这种情况下**建议避免在构造器中使用属性声明语法,将成员单独声明并在构造器中赋值。此条建议**并非**要求不使用构造器的属性声明语法,而是仅在这一情况下不建议使用,常规情况下推荐使用此语法来缩短代码长度并提高可读性。
- 长文件可使用 `#region` / `#endregion` 分段以支持折叠。 - 长文件可使用 `#region` / `#endregion` 分段以支持折叠。
- TODO 使用 `// TODO:``// todo:` 格式。 - TODO 使用 `// TODO:``// todo:` 格式。
- 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。 - 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。
- 注释合理换行:考虑中文字符较宽,建议每 4060 个字符在标点符号后换行,不允许在句中换行;参数注释换行后保持对齐。 - 注释合理换行:考虑中文字符较宽,建议每 4060 个字符在标点符号后换行,不允许在句中换行;参数注释换行后保持对齐。
- 单行注释结尾不加句号;较长的多行注释结尾可加句号。 - 单行注释结尾不加句号;较长的多行注释结尾可加句号。
- 一般不建议给接口、类型别名或类本身写注释(不好看),特殊情况除外。 - 一般不建议给接口`inteface` 本身)、类型别名或类本身写注释(不好看),特殊情况除外。
### 类型规范 ### 类型规范
@ -77,6 +79,7 @@
### 其他要求 ### 其他要求
- 不使用字符串作为键或特殊标识符(如枚举值、事件名、状态名等),应使用枚举代替。仅当明确表示字符串本身(如字符串类型的 id 别名、文件路径等)时方可使用字符串字面量。
- 严格遵循 `eslint` 配置,不允许出现 eslint 报错。 - 严格遵循 `eslint` 配置,不允许出现 eslint 报错。
- 尽量不使用 `?.` 运算符,仅推荐在以下两种场景中使用: - 尽量不使用 `?.` 运算符,仅推荐在以下两种场景中使用:
- 副作用函数调用,如 `this.obj?.func()``this.obj.func?.()` - 副作用函数调用,如 `this.obj?.func()``this.obj.func?.()`

View File

@ -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<void>;
/**
* 向当前队列末尾追加步骤(仅在移动进行中有效,完成后追加无效)
*/
push(...steps: DynamicMoveStep[]): void;
/**
* 停止移动,等待当前步骤完成后停止,返回的 Promise 在停止后兑现
*/
stop(): Promise<void>;
}
```
## 5. IDynamicMover 接口
每个 `IDynamicTile` 持有一个**绑定**的 `IDynamicMover` 实例(`tile.mover`
无需单独创建或管理生命周期。
### 5.1 状态读取
```ts
// 以下状态属性来自 IObjectMover<IDynamicTile>IDynamicMover 全部继承
interface IObjectMover<T extends IObjectMovable> {
/** 是否正在移动 */
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<number>`
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<void>`
执行器始终通过 `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<IObjectMoverHooks<IDynamicTile>>`(见接口定义汇总)。
---
# 涉及文件
## 需要修改的文件
### `@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<T extends IObjectMovable>` 接口(含 `onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`
`onStepStart`/`onStepEnd` 参数顺序为 `(code, step, tile, mover)`,按使用频率排列)
- [ ] `IObjectMover<T extends IObjectMovable>` 接口(含 `faceDirection`/`moveDirection` 只读属性及全部计划构建方法;`step`/`stepFace` 含 `count?` 参数)
- [ ] `IDynamicMover extends IObjectMover<IDynamicTile>`(无额外成员,继承基类全部能力;`IDynamicMoverHooks` 已移除,直接使用 `IObjectMoverHooks<IDynamicTile>`
#### `@user/data-base/src/map/mover.ts`
- [ ] `abstract class ObjectMover<T extends IObjectMovable>` 抽象基类:
- 实现 `IObjectMover<T>`,在此提供全部**计划构建方法**`step`/`stepFace`/`forward`/
`backward`/`speed`/`face`/`animDir`/`clear`/`start`)及队列执行调度逻辑;
- 含四个**抽象方法**,子类必须实现,组成移动核心控制流:
- `abstract onMoveStart(tile, mover): Promise<void>`: 整次移动开始时调用
- `abstract onMoveEnd(tile, mover): Promise<void>`: 整次移动结束时调用
- `abstract onStepStart(step, tile, mover): Promise<number>`: 单步逻辑执行前调用,
子类计算并**返回** `code`;父类将 `code` 传给 `IObjectMoverHooks.onStepStart` 钩子后等待
- `abstract onStepEnd(code, step, tile, mover): Promise<void>`: 单步钩子等待完成后调用,子类依据 `code` 执行本步实际逻辑效果
- [ ] `class DynamicMover extends ObjectMover<IDynamicTile> 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<void>;
push(...steps: DynamicMoveStep[]): void;
stop(): Promise<void>;
}
interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
onMoveStart(mover: IObjectMover<T>, tile: T): Promise<void>;
onMoveEnd(mover: IObjectMover<T>, tile: T): Promise<void>;
/** 触发时 tile.x/y 为移动前坐标,适合渲染端在此播放动画 */
onStepStart?(
code: number,
step: DynamicMoveStep,
tile: T,
mover: IObjectMover<T>
): Promise<void>;
/** 触发时 tile.x/y 已更新为移动后坐标 */
onStepEnd?(
code: number,
step: DynamicMoveStep,
tile: T,
mover: IObjectMover<T>
): Promise<void>;
}
interface IObjectMover<T extends IObjectMovable> extends IHookable<
IObjectMoverHooks<T>
> {
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<number>`
// 具体来说,这四个方法是整个移动的核心方法而非钩子,它应当提供接口让子类实现,
// 子类应当真正执行移动效果,并进行一定的控制。显然子类是没办法知道 onStepStart 中的信息的,
// 所以才提供了一个 code用于子类向 ObjectMover 提供信息,然后父类再传递给子类,
// 从而子类了解到 onStepStart 中的信息,再进一步决定这一步真正应该如何执行。
// 而对于钩子,实际上是在子类的 onStepStart 执行完毕后进行的,所以是知道 code 的,所以才是参数。
// 你应该好好想一想这之间的关系。
// 以及子类是要求必须实现的,所以不应该会有返回 `void` 的场景,四个方法都应该返回 `Promise<void>`
// 总的来说,父类是一个“系统级”的流程控制器,它通过这四个接口来实现真正的移动控制,
// 就像 legacy ObjectMoverBase 中的 startMove 一样。
abstract class ObjectMover<
T extends IObjectMovable
> implements IObjectMover<T> {
abstract onMoveStart(tile: T, mover: IObjectMover<T>): Promise<void>;
abstract onMoveEnd(tile: T, mover: IObjectMover<T>): Promise<void>;
/** 子类计算并返回本步 code父类取得 code 后再触发 IObjectMoverHooks.onStepStart 钩子 */
abstract onStepStart(
step: DynamicMoveStep,
tile: T,
mover: IObjectMover<T>
): Promise<number>;
/** 子类依据 code 执行本步实际逻辑效果;父类在此之后触发 IObjectMoverHooks.onStepEnd 钩子 */
abstract onStepEnd(
code: number,
step: DynamicMoveStep,
tile: T,
mover: IObjectMover<T>
): Promise<void>;
// 计划构建方法step/stepFace/forward/backward/speed/face/animDir/clear/start在此实现
}
// faceDirection/moveDirection 已提升至 IObjectMoverIDynamicMover 无需额外声明
// IDynamicMoverHooks 已移除,直接使用 IObjectMoverHooks<IDynamicTile>
interface IDynamicMover extends IObjectMover<IDynamicTile> {
// 继承自 IObjectMovermoving, 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<T extends IObjectMovable>`(参考 legacy `ObjectMoverBase`,适配新 API
核心抽象类为 `abstract class ObjectMover<T>`,含四个抽象方法组成控制流:
- **计划构建方法**`step`/`stepFace`/`forward`/`backward`/
`speed`/`face`/`animDir`/`clear`/`start`)全部在 `ObjectMover` 基类实现,子类无需重复声明。
- **移动流程钩子**`IObjectMoverHooks<T>`,实现 `IHookable<IObjectMoverHooks<T>>`
- `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<number>`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<IDynamicTile>`**
无额外成员,直接继承基类全部能力(含 `faceDirection`/`moveDirection` 及 `IObjectMoverHooks`)。

View File

@ -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<T extends IObjectMovable>` 以此接口为泛型约束,
使图块和玩家 mover 共享核心执行逻辑;具体渲染效果通过 `IObjectMoverHooks``onStepStart`/`onStepEnd` 钩子实现。
`DynamicLayer` 内部维护两个结构:
- `tileSet: Set<IDynamicTile>` — 所有图块的集合,用于迭代与归属判断;
- `posMap: Map<number, Map<number, Set<IDynamicTile>>>` — 按坐标索引
(外层 key = y内层 key = xvalue = 该格点所有图块对象集合),
图块移动时同步更新,支持越界坐标(见第 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<IDynamicLayerHooks>` 以支持渲染端订阅变更事件。
`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> | 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)` 完成,
逻辑与上述步骤 15 相同,但不触发移动。
## 9. IDynamicLayerHooks 与 ILayerStateHooks
`IDynamicLayerHooks` 定义三个钩子:
- `onCreateTile(tile: IDynamicTile)`:图块被创建(含转换)时触发;
- `onDeleteTile(tile: IDynamicTile)`:图块被删除时触发(传入删除前的快照);
- `onMoveTile(tile: IDynamicTile, fromX: number, fromY: number): Promise<void> | void`
图块移动一步时触发,`tile` 为更新后的状态,`fromX`/`fromY` 为移动前坐标。
返回 `Promise<void>` 时,移动器将等待其兑现后再进行下一步(配合 `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<T extends IObjectMovable>` 泛型约束使用
- [ ] 新增 `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<IDynamicLayerHooks>` 接口:
- [ ] `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<IDynamicLayerHooks>` 类,
实现 `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<IDynamicTile>`:所有图块的集合,用于迭代与归属判断
- [ ] `private posMap: Map<number, Map<number, Set<IDynamicTile>>>`
按坐标索引(外层 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<IDynamicLayerHooks> {
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<IDynamicTile>;
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<number, Map<number, Set<IDynamicTile>>>` 嵌套结构(外层 key = y内层 key = x
彻底去掉字符串键,天然支持越界坐标,`resizeLayer` 无需通知 `DynamicLayer`
`getDynamicTilesAt` 接口保留,使用频率低,嵌套 Map 开销完全可接受。

View File

@ -1,3 +1,4 @@
export * from './face'; export * from './face';
export * from './mover';
export * from './types'; export * from './types';
export * from './utils'; export * from './utils';

View File

@ -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<void>;
/**
*
* @param steps
*/
push(...steps: ObjectMoveStep[]): void;
/**
*
* @param steps
*/
insert(...steps: ObjectMoveStep[]): void;
/**
*
*/
stop(): Promise<void>;
}
export interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
/**
*
* @param tile
* @param mover
*/
onMoveStart?(tile: T, mover: IObjectMover<T>): Promise<void>;
/**
*
* @param tile
* @param mover
*/
onMoveEnd?(tile: T, mover: IObjectMover<T>): Promise<void>;
/**
*
* @param code
* @param step
* @param tile
* @param mover
*/
onStepStart?(
code: number,
step: ObjectMoveStep,
tile: T,
mover: IObjectMover<T>
): Promise<void>;
/**
*
* @param code
* @param step
* @param tile
* @param mover
*/
onStepEnd?(
code: number,
step: ObjectMoveStep,
tile: T,
mover: IObjectMover<T>
): Promise<void>;
}
export interface IObjectMover<T extends IObjectMovable> extends IHookable<
IObjectMoverHooks<T>
> {
/** 当前是否正在移动 */
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<IMoverController> | null;
}
//#endregion
//#region 移动基类
export abstract class ObjectMover<T extends IObjectMovable>
extends Hookable<IObjectMoverHooks<T>>
implements IObjectMover<T>
{
abstract readonly tile: T;
/** 尚未开始执行的移动步骤队列 */
protected readonly moveQueue: Readonly<ObjectMoveStep>[] = [];
/** 当前是否正在移动 */
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<IObjectMoverHooks<T>>
): IHookController<IObjectMoverHooks<T>> {
return new HookController(this, hook);
}
/**
*
* @param tile
* @param controller
*/
protected abstract onMoveStart(
tile: T,
controller: Readonly<IMoverController>
): Promise<void>;
/**
*
* @param tile
* @param controller
*/
protected abstract onMoveEnd(
tile: T,
controller: Readonly<IMoverController>
): Promise<void>;
/**
* {@link onStepEnd}
* @param step
* @param tile
* @param controller
*/
protected abstract onStepStart(
step: ObjectMoveStep,
tile: T,
controller: Readonly<IMoverController>
): Promise<number>;
/**
*
* @param code {@link onStepStart}
* @param step
* @param tile
* @param controller
*/
protected abstract onStepEnd(
code: number,
step: ObjectMoveStep,
tile: T,
controller: Readonly<IMoverController>
): Promise<ITileLocator>;
/**
*
* @param step
*/
protected pushStep(step: Readonly<ObjectMoveStep>): 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<IMoverController>
) {
// 移动开始
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<void>();
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

View File

@ -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<IDynamicLayerHooks>
implements IDynamicLayer
{
/** 坐标到动态图块集合的映射,外层 key = y内层 key = x不使用 index 是为了支持地图外图块 */
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
new Map();
/** 动态图块到其当前坐标的映射 */
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
constructor(public readonly layer: IMapLayer) {
super();
}
protected createController(
hook: Partial<IDynamicLayerHooks>
): IHookController<IDynamicLayerHooks> {
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<void> {
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<IDynamicTile> {
return this.tilePosMap.get(y)?.get(x) ?? new Set();
}
iterateDynamicTiles(): Iterable<IDynamicTile> {
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);
}
}

View File

@ -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<IDynamicTile>;
/** 当前的朝向绑定对象 */
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<void> {
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;
}
}
}

View File

@ -1,4 +1,5 @@
export * from './layerState'; export * from './layerState';
export * from './mapLayer'; export * from './mapLayer';
export * from './mapStore'; export * from './mapStore';
export * from './mover';
export * from './types'; export * from './types';

View File

@ -1,11 +1,13 @@
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { import {
IDynamicLayer,
IMapLayer, IMapLayer,
IMapLayerData, IMapLayerData,
IMapLayerHookController, IMapLayerHookController,
IMapLayerHooks IMapLayerHooks
} from './types'; } from './types';
import { Hookable, HookController, logger } from '@motajs/common'; import { Hookable, HookController, logger } from '@motajs/common';
import { DynamicLayer } from './dynamicLayer';
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList以及前景背景的接口 // todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList以及前景背景的接口
@ -23,6 +25,8 @@ export class MapLayer
/** 地图数据引用 */ /** 地图数据引用 */
private mapData: IMapLayerData; private mapData: IMapLayerData;
readonly dynamicLayer: IDynamicLayer;
constructor(array: Uint32Array, width: number, height: number) { constructor(array: Uint32Array, width: number, height: number) {
super(); super();
this.width = width; this.width = width;
@ -35,6 +39,7 @@ export class MapLayer
expired: false, expired: false,
array: this.mapArray array: this.mapArray
}; };
this.dynamicLayer = new DynamicLayer(this);
} }
resize(width: number, height: number): void { resize(width: number, height: number): void {

View File

@ -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<IDynamicTile> {
constructor(public readonly tile: IDynamicTile) {
super();
}
protected onMoveStart(): Promise<void> {
return Promise.resolve();
}
protected onMoveEnd(): Promise<void> {
return Promise.resolve();
}
protected onStepStart(): Promise<number> {
return Promise.resolve(DynamicMoveCode.Success);
}
protected onStepEnd(
code: number,
step: ObjectMoveStep,
tile: IDynamicTile
): Promise<ITileLocator> {
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

View File

@ -1,5 +1,14 @@
import { IHookable, IHookBase, IHookController } from '@motajs/common'; import { IHookable, IHookBase, IHookController } from '@motajs/common';
import { ISaveableContent } from '../common'; import {
FaceDirection,
IMoverController,
IObjectMovable,
IObjectMover,
IRoleFaceBinder,
ISaveableContent
} from '../common';
//#region 静态图层
export interface IMapLayerData { export interface IMapLayerData {
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */ /** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
@ -75,6 +84,9 @@ export interface IMapLayer extends IHookable<
/** 图层纵深 */ /** 图层纵深 */
readonly zIndex: number; readonly zIndex: number;
/** 此图层对应的动态图块图层z 层级与静态图块一致 */
readonly dynamicLayer: IDynamicLayer;
/** /**
* *
* @param block * @param block
@ -154,6 +166,10 @@ export interface IMapLayer extends IHookable<
setMapRef(array: Uint32Array): void; setMapRef(array: Uint32Array): void;
} }
//#endregion
//#region 图层管理
export interface ILayerStateHooks extends IHookBase { export interface ILayerStateHooks extends IHookBase {
/** /**
* *
@ -288,6 +304,10 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
setDirty(dirty: boolean): void; setDirty(dirty: boolean): void;
} }
//#endregion
//#region 楼层管理
/** 单个 MapLayer 的存档数据 */ /** 单个 MapLayer 的存档数据 */
export interface IMapLayerSave { export interface IMapLayerSave {
readonly width: number; readonly width: number;
@ -428,3 +448,149 @@ export interface IMapStore extends ISaveableContent<IMapStoreSave> {
*/ */
notifyEnterFloor(id: string): void; 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<void>;
/**
* 使 `mover`
* @param tile
* @param layer
*/
onUpdateTilePosition(tile: IDynamicTile, layer: IDynamicLayer): void;
}
export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
/** 当前动态图层所属的静态图层 */
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<void>;
/**
*
* @param x
* @param y
*/
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
/**
*
*/
iterateDynamicTiles(): Iterable<IDynamicTile>;
/**
* `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<IDynamicTile>;
/**
* {@link num}
* @param direction
*/
setFaceDirection(direction: FaceDirection): number;
/**
*
*/
delete(): Promise<void>;
/**
*
*/
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

View File

@ -16,10 +16,6 @@ export class MainDamageCalculator implements IDamageCalculator<
/** 当前是否正在计算支援怪的伤害 */ /** 当前是否正在计算支援怪的伤害 */
private inGuard: boolean = false; private inGuard: boolean = false;
/**
*
* @param handler
*/
calculate( calculate(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr> handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): IEnemyDamageInfo { ): IEnemyDamageInfo {
@ -153,11 +149,6 @@ export class MainDamageCalculator implements IDamageCalculator<
}; };
} }
/**
*
* @param handler
* @param attribute
*/
getCriticalLimit( getCriticalLimit(
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>, handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr> attribute: CriticalableHeroStatus<IHeroAttr>

View File

@ -183,6 +183,11 @@
"123": "MapLayer.setMapRef: array length $1 does not match expected size $2, setMapRef will be ignored.", "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.", "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.", "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." "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
} }
} }

View File

@ -2,11 +2,12 @@
以下规则必须时刻遵守,任何情况下都不允许违反。 以下规则必须时刻遵守,任何情况下都不允许违反。
1. **不擅自修改已有代码**:将我已经写好的代码视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在逻辑错误,应在对话中提出,而不是直接修改。 1. **不擅自修改已有代码**:将我已经写好的代码视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在逻辑错误,应在对话中提出,而不是直接修改,不要做任何“顺手”的事
2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。 2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
3. **以目的驱动,而非以接口驱动**:实现前先想清楚我为什么要这样设计接口、这个接口设计的目的是什么,而不是单纯地以将接口填满为目标。 3. **以目的驱动,而非以接口驱动**:实现前先想清楚我为什么要这样设计接口、这个接口设计的目的是什么,而不是单纯地以将接口填满为目标。
4. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。 4. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。
5. **按我说的方式重构**:若目标是重构某个接口,按照我指定的方式执行: 5. **接口缺失时停止并提问**:若完成需求所需的接口尚不存在,应立即停止实现,向我提出疑问,而不是擅自新增接口来推进。
6. **按我说的方式重构**:若目标是重构某个接口,按照我指定的方式执行:
- **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑与思路上的参考。 - **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑与思路上的参考。
- **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。 - **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。
@ -17,6 +18,8 @@
1. **合理参考建议**:我有时会在对话中给出实现建议,应合理参考,切忌滥用。 1. **合理参考建议**:我有时会在对话中给出实现建议,应合理参考,切忌滥用。
2. **以类型标注为参考依据**:实现与类型标注有冲突时,以类型标注(一般是 `types.ts`)中的内容为准。 2. **以类型标注为参考依据**:实现与类型标注有冲突时,以类型标注(一般是 `types.ts`)中的内容为准。
3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。 3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。
4. **接口设计兼顾合理性与便捷性**:设计接口时不仅要考虑合理性,还要考虑使用便捷性。罕见场景应当被支持,但不应与常见场景共用同一接口——这只会增加常见场景的使用难度。
5. **避免多余的非空判断与类型守卫**:若某个类型已满足目标的类型约束,不应再对其添加任何判断或过滤操作。典型例子:`Promise.all` 对数组元素类型没有任何限制,传入 `(Promise<unknown> | void)[]` 完全合法,无需多此一举地写成 `Promise.all(arr.filter(v => !!v))`
**时刻谨记上述要求,避免一个需求反复修改仍无法满足预期。** **时刻谨记上述要求,避免一个需求反复修改仍无法满足预期。**
@ -29,7 +32,7 @@
## 示例文档 ## 示例文档
大致按照以下格式编写,如某部分需要详细描述,可单独开设标题。我会使用引用块的形式在文档中提出建议或回答。 大致按照以下格式编写,如某部分需要详细描述,可单独开设标题;若某个接口内容较多,也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。
```md ```md
# 需求综述 # 需求综述