Compare commits

..

2 Commits

Author SHA1 Message Date
931f610d37 chore: 更新 packages 和 dev.md 2026-06-27 21:36:34 +08:00
fadcc98864 refactor: 跟随者与勇士移动 2026-06-27 20:33:49 +08:00
20 changed files with 1024 additions and 318 deletions

18
dev.md
View File

@ -3,8 +3,8 @@
## 项目结构 ## 项目结构
- `public`: mota-js 样板所在目录 - `public`: mota-js 样板所在目录
- `packages`: 核心引擎代码 monorepo - `packages`: 核心引擎代码 monorepo`@motajs/`
- `packages-user`: 用户代码 monorepo - `packages-user`: 用户代码 monorepo`@user/`
- `src`: 游戏入口代码 - `src`: 游戏入口代码
依赖关系为单向:`src` → `packages-user``packages`。`packages` 与 `packages-user` 均可独立打包为库模式,`src` 为游戏的入口代码。 依赖关系为单向:`src` → `packages-user``packages`。`packages` 与 `packages-user` 均可独立打包为库模式,`src` 为游戏的入口代码。
@ -26,11 +26,14 @@
## 构建说明 ## 构建说明
| 命令 | 说明 | | 命令 | 说明 |
| --------------------- | ------------------------------------------------------- | | --------------------- | ------------------------------------------------------------ |
| `pnpm build:packages` | 以库模式构建 `packages` 下的所有内容 | | `pnpm build:packages` | 以库模式构建 `packages` 下的所有内容 |
| `pnpm build:game` | 构建为可直接部署的游戏包 | | `pnpm build:game` | 构建为可直接部署的游戏包 |
| `pnpm build:lib` | 以库模式构建 `packages``packages-user` 下的所有内容 | | `pnpm build:lib` | 以库模式构建 `packages``packages-user` 下的所有内容 |
| `pnpm type` | 对仓库执行类型检查 | | `pnpm lint:packages` | 运行 `packages` 文件夹下的 `eslint` 格式检查 |
| `pnpm lint:user` | 运行 `packages-user` 文件夹下的 `eslint` 格式检查 |
| `pnpm lint:custom` | 运行自定义文件的 `eslint` 格式检查,相当于 `pnpm eslint ...` |
| `pnpm check:type` | 对仓库执行类型检查 |
| `pnpm check:circular` | 对仓库执行循环引用检查 | | `pnpm check:circular` | 对仓库执行循环引用检查 |
## 术语统一 ## 术语统一
@ -62,7 +65,7 @@
| 代码文件名 | 小驼峰 | | 代码文件名 | 小驼峰 |
| Markdown 文档文件名 | 连字符命名法 | | Markdown 文档文件名 | 连字符命名法 |
不使用下划线命名法。 不使用下划线命名法,私有属性或方法不要以下划线开头
### 注释规范 ### 注释规范
@ -85,9 +88,10 @@
- 未使用的变量或方法以下划线开头命名。 - 未使用的变量或方法以下划线开头命名。
- 合理使用 `readonly`、`protected`、`private` 关键字。 - 合理使用 `readonly`、`protected`、`private` 关键字。
- 可选参数过多时(大于两个),考虑改用对象参数。 - 可选参数过多时(大于两个),考虑改用对象参数。
- 尽量避免 `as` 类型断言,除非必要。 - 尽量避免 `as` 类型断言,除非必要。绝对不允许出现连续 `as`,例如 `as unknown as xxx`
- 函数类型单独开一个 `type` 类型别名,除非函数类型本身较短(小于 20 字符,且不会因为此函数类型导致换行)。 - 函数类型单独开一个 `type` 类型别名,除非函数类型本身较短(小于 20 字符,且不会因为此函数类型导致换行)。
- 当任何时候必须出现对象类型的时候,单独声明一个 `interface`,不得出现直接的对象类型。 - 当任何时候必须出现对象类型的时候,单独声明一个 `interface`,不得出现直接的对象类型。
- 对于某个类中的对象成员,必须使用接口作为类型,不得使用类作为类型,除非某些极特殊情况。
### 其他要求 ### 其他要求
@ -99,7 +103,7 @@
- 只进行必要的非空判断,非必要时直接使用非空断言 `!` - 只进行必要的非空判断,非必要时直接使用非空断言 `!`
- 除非参数要求传入函数等情况,不建议在函数内定义局部函数。 - 除非参数要求传入函数等情况,不建议在函数内定义局部函数。
- 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。 - 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。
- 一般不建议写 `getter``setter`,但某些场景下还是可以使用的。 - 一般不建议写 `getter``setter`,但在极少数场景下还是可以使用的。
## 双端分离 ## 双端分离

View File

@ -0,0 +1,140 @@
# 需求综述
对勇士跟随者相关接口进行结构性重构。核心动机是让跟随者复用勇士已有的位置与渲染接口(`IHeroLocation`、`IHeroRendering`),而非为跟随者定义独立的位置/渲染类型。这样跟随者与勇士共享同一套移动与渲染模型,简化接口层级,避免重复定义。
重构四个接口:`IHeroLocation`、`IHeroRendering`、`IHeroFollower`、`IHeroFollowersController`。前两者是勇士与跟随者共用的基础接口,后两者专属于跟随者管理。同时需要将旧的 `HeroMoveController` 类拆分为 `HeroLocation`、`HeroRendering`、`HeroFollowersController` 三个独立类,`HeroFollower` 为新实现类,并修改 `HeroState` 适配新接口。
# 接口设计分析
## IHeroLocation
### 接口综述
勇士位置接口,代表一个可移动对象在游戏世界中的坐标与朝向,并挂载移动器负责移动控制。该接口继承 `ISaveableContent<IHeroLocationSave>` 实现存档读档,继承 `IObjectMovable` 获得基础位置读写能力。存档格式为 `IFacedTileLocator`(包含 `x`、`y`、`direction`)。移动能力由 `IObjectMover<this>` 提供,支持行走、跳跃、瞬移、朝向控制等全部需求。
### 接口分析
- `IHeroLocation.mover`:预期频率**高频**。几乎所有移动操作都经由 `mover` 进行,在剧情演出与逻辑中频繁出现。典型使用场景:演出中让角色沿指定路线移动。
## IHeroRendering
### 接口综述
勇士渲染信息接口,存在于数据端,存放无需进入渲染端即可管理的渲染相关数据。当前仅包含透明度(`alpha`),后续可能扩展。继承 `ISaveableContent<IHeroRenderingSave>` 实现存档读档,继承 `IHookable<IHeroRenderingHooks>` 支持透明度变化时触发回调。旧实现中的 `image` 作为纯渲染端概念被移除。
### 接口分析
- `IHeroRendering.alpha`:预期频率**低频**。直接读取透明度值的需求极少。
- `IHeroRendering.setAlpha`:预期频率**低频**。设置透明度仅在特殊效果中出现,一座塔中调用次数有限。
## IHeroFollower
### 接口综述
跟随者接口,包含跟随者的渲染信息与位置信息。直接复用 `IHeroRendering``IHeroLocation`,使跟随者拥有与勇士完全一致的移动与渲染能力。存档格式 `IHeroFollowerSave` 嵌套包含 `IHeroRenderingSave``IHeroLocationSave`。对比旧 `IHeroFollower = { num, identifier, alpha }` 的纯数据结构,新接口让跟随者从静态数据升级为拥有完整移动能力的独立对象。
### 接口分析
- `IHeroFollower.rendering`:预期频率**低频**。
- `IHeroFollower.location`:预期频率**低频**。
> 跟随者本身就是一个使用场景极低的需求,大多数游戏中都不会使用到一两次,所以几乎所有跟随者相关接口都应该是低频。
## IHeroFollowersController
### 接口综述
跟随者控制器接口,负责跟随者的增删查改与聚集操作。继承 `IHookable<IHeroFollowersControllerHooks>`,提供添加、移除、聚集三个事件的钩子支持。对比旧实现:索引取代 `identifier` 作为查找方式,`setFollowerAlpha` 移除(改为通过 `follower.rendering.setAlpha` 操作),`addFollower` 的 `num` 参数合并了旧的图块数字与标识符。
### 接口分析
- `IHeroFollowersController.addFollower`:预期频率**低频**。
- `IHeroFollowersController.getFollower`:预期频率**低频**。
- `IHeroFollowersController.getFollowersById`:预期频率**低频**。
- `IHeroFollowersController.getAllFollowers`:预期频率**低频**。
- `IHeroFollowersController.removeFollower`:预期频率**低频**。
- `IHeroFollowersController.removeAllFollowers`:预期频率**低频**。
- `IHeroFollowersController.gatherFollowers`:预期频率**低频**。
- `IHeroFollowersController.gatherFollowersSync`:预期频率**低频**。
### 预期体量
预期代码体量合计约 250300 行。分析如下:
- `HeroLocation`:需实现 `IObjectMovable``ISaveableContent`,并创建内部 `ObjectMover` 子类,预计 7090 行。
- `HeroRendering`:仅管理 `alpha` 的存取和钩子触发,预计 2535 行。
- `HeroFollower`:组合 `HeroRendering``HeroLocation` 并实现存档读档,预计 3545 行。
- `HeroFollowersController`:维护跟随者列表和增删查改聚集操作,预计 120150 行。
# 涉及文件
## 需要修改的文件
### `@user/data-base/hero/types.ts`
- [x] 重构 `IHeroLocation` 接口:改为继承 `ISaveableContent<IHeroLocationSave>``IObjectMovable`,仅保留 `mover` 成员。
- [x] 新增 `IHeroLocationSave` 类型别名。
- [x] 重构 `IHeroRendering` 接口:新增继承 `ISaveableContent<IHeroRenderingSave>``IHookable<IHeroRenderingHooks>`
- [x] 新增 `IHeroRenderingSave` 接口。
- [x] 新增 `IHeroRenderingHooks` 接口。
- [x] 新增 `IHeroFollowerSave` 接口。
- [x] 重构 `IHeroFollower` 接口:改为组合 `rendering``location`,继承 `ISaveableContent<IHeroFollowerSave>`
- [x] 重构 `IHeroFollowersController` 接口:修改为上文中的新设计。
- [x] 新增 `IHeroFollowersControllerHooks` 接口。
- [x] 修改 `IHeroState` 接口:新增 `location`、`rendering`、`followers` 成员,移除旧的 `mover`、`attachMover`、`getHeroMover`。
- [x] 修改 `IHeroStateSave``locator` 类型改为 `IHeroLocationSave``followers` 类型改为 `readonly IHeroFollowerSave[]`
- [ ] 删除旧的 `IHeroMoveController`、`IHeroMoveControllerHooks` 接口及旧 `IHeroFollower`、`HeroAnimateDirection` 枚举。
### `@user/data-base/hero/mover.ts`
删除此文件,内容拆分至 `location.ts`、`rendering.ts`、`followersController.ts`。
### `@user/data-base/hero/location.ts`
新建文件。
- [ ] 实现 `HeroLocation` 类:基于新 `IHeroLocation` 接口。
- [ ] 内部创建 `ObjectMover<this>` 子类实例,挂载为 `mover`
### `@user/data-base/hero/rendering.ts`
新建文件。
- [ ] 实现 `HeroRendering` 类:基于新 `IHeroRendering` 接口。
- [ ] 实现 `setAlpha`:更新值并触发 `onSetAlpha` 钩子。
### `@user/data-base/hero/follower.ts`
新建文件。
- [ ] 实现 `HeroFollower` 类:组合 `HeroRendering``HeroLocation` 实例,实现新 `IHeroFollower` 接口。
- [ ] 内部保存 `num`,供 `getFollowersById` 查询匹配。
### `@user/data-base/hero/followersController.ts`
新建文件。
- [ ] 实现 `HeroFollowersController` 类:基于新 `IHeroFollowersController` 接口。
- [ ] 内部持有勇士 `IHeroLocation` 引用,供聚集操作使用。
- [ ] 实现 `addFollower(num)`:创建 `HeroFollower`,加入列表,触发 `onAddFollower` 钩子。
- [ ] 实现 `getFollower(index)`:按索引返回跟随者,越界返回 `null`
- [ ] 实现 `getFollowersById(num)`:返回匹配 `num``[index, follower]` 迭代器。
- [ ] 实现 `getAllFollowers()`:返回跟随者列表。
- [ ] 实现 `removeFollower(index)`:按索引移除跟随者,触发 `onRemoveFollower` 钩子,动画结束后兑现。
- [ ] 实现 `removeAllFollowers()`:清空跟随者列表,触发钩子,动画结束后兑现。
- [ ] 实现 `gatherFollowers()`:将所有跟随者移动至勇士位置并同步朝向,动画结束后兑现。
- [ ] 实现 `gatherFollowersSync()`:立即将所有跟随者瞬移至勇士位置并同步朝向。
### `@user/data-base/hero/state.ts`
修改 `HeroState` 类。
- [ ] 修改构造函数参数并新增 `location`、`rendering`、`followers` 成员,替代旧的 `mover`
- [ ] 删除 `attachMover`、`getHeroMover` 方法。
- [ ] 修改 `saveState`:通过 `location.saveState`、`rendering.saveState` 及各跟随者的 `saveState` 获取存档数据。
- [ ] 修改 `loadState`:通过 `location.loadState`、`rendering.loadState` 恢复位置与渲染,重建跟随者列表。
### `@user/data-base/hero/index.ts`
- [ ] 新增导出 `location.ts`、`rendering.ts`、`follower.ts`、`followersController.ts`。
- [ ] 移除 `mover.ts` 的导出。

121
docs/dev/hero/hero-mover.md Normal file
View File

@ -0,0 +1,121 @@
# 需求综述
将旧样板中的 `HeroMover``packages-user/data-state/src/legacy/move.ts`)基于新的 `IObjectMover` / `ObjectMover` 基类进行彻底性重构,编写新的 `IHeroMover` 接口和 `HeroMover` 实现类。
新实现的核心变化:
- 基类从旧 `ObjectMoverBase`EventEmitter 模式)迁移至 `ObjectMover<IHeroLocation>`Hook 模式)。
- `inLockControl` 删除,重构后不再需要。
- 移动速度不再由 `HeroMover` 存储,速度变更通过 `ObjectMover.speed()` 方法体现,实际动画时长在 Hook 中自行处理。
- `noRoute`、`ignoreTerrain`、`autoSave` 不再作为公开属性暴露,改为通过 `config()` / `getConfig()` 方法管理,配置对象为 `IHeroMoverConfig`
- 摒弃所有 `core.xxx` 调用,这些功能将在后续通过新接口逐步补充。
- `HeroMover` 的实例化由 `IHeroLocation` 的实现类内部完成,构造时传入 `this` 作为 `tile`,解决循环依赖。
# 接口设计分析
`IHeroMover` 直接扩展 `IObjectMover<IHeroLocation>`,以下仅分析新增内容。
## IHeroMover
### 接口综述
`IHeroMover` 在通用对象移动器基础上,通过 `config()` 方法接收一个 `IHeroMoverConfig` 配置对象来控制本次移动的行为模式(是否忽略地形、是否记录路径、是否触发自动存档)。这种方法将三个相关标志聚合到统一的配置接口中,调用方便且后续扩展新配置项时无需新增独立属性。`getConfig()` 返回当前生效的只读配置,供内部逻辑和 Hook 查询移动上下文。
### 接口分析
- `IHeroMover.config(config)`:预期频率**中频**。在移动开始前统一配置本次移动的行为标志,一次性设置多个相关选项,比分别设置三个独立属性更加便利。典型使用场景:自动寻路系统在每次移动前调用 `mover.config({ noRoute: true }).forward(3).start()`,通过链式调用一气呵成。
- `IHeroMover.getConfig()`:预期频率**低频**。查询当前移动器的配置快照,主要用于 Hook 内部判断移动上下文或调试时检查配置状态。典型使用场景Hook 中的 `onStepStart` 获取 `mover.getConfig()` 以判断当前步是否需要路线记录。
### IHeroMoverConfig 成员分析
- `noRoute`:预期频率**中频**。控制本次移动是否记录进路线系统。路线回放、自动寻路等系统均需跳过路线记录,涉及面较广,故为中频。
- `ignoreTerrain`:预期频率**低频**。控制本次移动是否跳过地形碰撞检测,仅用于瞬移、强制移动等特殊场景。
- `autoSave`:预期频率**低频**。控制本次移动是否在特定时机触发自动存档,仅在地图伤害相关全局系统内部设置。
> 三个配置项在 `config()` 参数中均为可选,未显式设置的项保持当前值不变,`getConfig()` 返回的是已补全默认值的完整配置只读快照。
### 预期体量
接口定义部分(`IHeroMover`、`IHeroMoverConfig`)约 15-20 行。
`HeroMover` 类的预期代码体量为 200-250 行。分析如下:
- 类定义与私有属性(`noRoute`、`ignoreTerrain`、`autoSave` 各自独立声明),以及 `config` / `getConfig` 实现,约 30 行。
- `onMoveStart` 实现(同步渲染端状态、预检查首步地形等),约 30 行。
- `onMoveEnd` 实现(恢复状态、通知渲染端结束、清除自动寻路状态),约 20 行。
- `onStepStart` 实现(地形检测、自动存档判断、渲染端动画调用等),约 55 行。
- `onStepEnd` 实现(根据移动代码分发处理、返回新坐标、路线记录、触发器等),约 65 行。
- 辅助方法(坐标计算、地形检测等),约 30 行。
- `HeroMoveCode` 枚举定义,约 10 行。
### 可能风险
- `IHeroLocation` 接口已定义 `readonly mover: IObjectMover<this>`。由于 `IHeroMover extends IObjectMover<IHeroLocation>`,若 `HeroMover` 的构造在 `IHeroLocation` 内部完成(传入 `this`),类型可自然满足且无循环依赖问题。但需确保 `IHeroLocation` 的实现类确实在其构造过程中创建并持有 `HeroMover` 实例。
# 实现思路
## 1. 在 `types.ts` 中新增相关接口与枚举
新增 `IHeroMoverConfig` 接口,包含 `noRoute`、`ignoreTerrain`、`autoSave` 三个可选布尔成员。
新增 `IHeroMover` 接口,继承 `IObjectMover<IHeroLocation>`,新增 `config(config)``getConfig()` 两个方法。
新增 `HeroMoveCode` 枚举,公开导出,包含正常移动、停止、撞击、不可移动等代码,供 Hook 用户判断每步移动结果。
## 2. 完成 `HeroMover`
`packages-user/data-state/src/hero/mover.ts` 中实现 `HeroMover extends ObjectMover<IHeroLocation>`
构造函数接收两个参数:
- `tile: IHeroLocation` — 勇士位置对象,同时满足 `IObjectMovable`,作为基类移动操作的绑定目标。
- `faceHandler: IFaceHandler<FaceDirection>` — 朝向处理器,由 `IHeroLocation` 实现类在创建 `HeroMover` 时传入(本样式中为 `Dir4FaceHandler`)。
`config(config)`:将传入配置中已显式提供的字段分别赋值到对应的私有属性 `noRoute`、`ignoreTerrain`、`autoSave`,未传入的字段保持原值,返回 `this` 以支持链式调用。
`getConfig()`:从三个私有属性分别读取当前值,组装为 `Readonly<IHeroMoverConfig>` 返回,默认值均为 `false`
`onMoveStart`:移动开始时读取当前配置,通过 `IHeroLocation.mover`(渲染端 `HeroMoveController` 实例)启动渲染端移动动画。若 `ignoreTerrain``false`,预检查第一步是否能通过。
`onMoveEnd`:移动结束时通知渲染端移动结束,清除自动寻路相关状态。
`onStepStart`:根据 `ignoreTerrain` 判断是否进行地形检测,若不可通过则返回对应的 `HeroMoveCode`。处理 `autoSave` 的存档判断逻辑。启动渲染端单步移动动画。根据 `noRoute` 判断是否记录路线。
> **注意**:地形检测、路线记录、触发器、中毒伤害等具体逻辑依赖的新接口尚不存在,`core.xxx` 调用在此重构中摒弃。当前在这些位置以 `// TODO:` 标记占位,等待后续接口补充。
`onStepEnd`:根据 `onStepStart` 返回的 `HeroMoveCode` 执行对应逻辑:
- `Step`:返回目标坐标 `ITileLocator` 让基类更新位置,执行路线记录与后置逻辑。
- `Hit`:停止移动,触发目标格触发器。
- `CannotMove`:停止移动,不触发触发器。
- `Stop`:停止移动,清除自动寻路。
## 3. 在 `hero/index.ts` 中导出
`packages-user/data-state/src/hero/index.ts` 中新增 `export * from './mover'`
# 涉及文件
## 需要引用的文件
- `@motajs/common`:引用 `ITileLocator` 等基础接口。
- `@user/data-common`:引用 `IObjectMover`、`IObjectMovable`、`ObjectMover`、`ObjectMoveStep`、`ObjectMoveStepType`、`FaceDirection`、`IFaceHandler`、`IFaceDescriptor` 等类型。
- `@user/data-base`:引用 `IHeroLocation`、`IHeroMoveController` 等勇士相关接口。
- `types.ts`(同包顶层):引用 `IHeroMover`、`IHeroMoverConfig`、`HeroMoveCode`(类实现接口时自引用)。
## 需要修改的文件
### `packages-user/data-state/src/types.ts`
- [ ] 新增 `IHeroMoverConfig` 接口:定义移动配置对象的三个可选布尔成员 `noRoute`、`ignoreTerrain`、`autoSave`。
- [ ] 新增 `IHeroMover` 接口:继承 `IObjectMover<IHeroLocation>`,新增 `config()``getConfig()` 方法。
- [ ] 新增 `HeroMoveCode` 枚举:定义勇士移动步骤返回码,公开导出供 Hook 使用。
### `packages-user/data-state/src/hero/mover.ts`(新建)
- [ ] 实现 `HeroMover` 类:继承 `ObjectMover<IHeroLocation>`,实现 `config`、`getConfig` 及基类四个抽象方法。
### `packages-user/data-state/src/hero/index.ts`
- [ ] 新增 `export * from './mover'`:导出新模块。

View File

@ -8,8 +8,10 @@
"test": "vitest", "test": "vitest",
"preview": "vite preview", "preview": "vite preview",
"declare": "tsx script/declare.ts", "declare": "tsx script/declare.ts",
"type": "vue-tsc --noEmit",
"lines": "tsx script/lines.ts packages packages-user", "lines": "tsx script/lines.ts packages packages-user",
"lint:packages": "eslint packages/",
"lint:user": "eslint packages-user/",
"lint:custom": "eslint",
"build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts", "build:packages": "vue-tsc --noEmit && tsx script/build-packages.ts",
"build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts", "build:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts", "build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
@ -17,6 +19,7 @@
"docs:build": "vitepress build docs", "docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs", "docs:preview": "vitepress preview docs",
"pack:template": "tsx script/pack-template.ts", "pack:template": "tsx script/pack-template.ts",
"check:type": "vue-tsc --noEmit",
"check:circular": "madge --circular src/main.ts" "check:circular": "madge --circular src/main.ts"
}, },
"dependencies": { "dependencies": {

View File

@ -0,0 +1,77 @@
import { IFacedTileLocator, logger } from '@motajs/common';
import {
FaceDirection,
IDataCommon,
IFaceHandler,
SaveCompression
} from '@user/data-common';
import { HeroLocation } from './location';
import { HeroRendering } from './rendering';
import {
IHeroFollower,
IHeroFollowerSave,
IHeroFollowersController,
IHeroLocation,
IHeroRendering
} from './types';
import { isNil } from 'lodash-es';
export class HeroFollower implements IHeroFollower {
readonly num: number;
readonly state: IDataCommon;
readonly rendering: IHeroRendering;
readonly location: IHeroLocation;
/** 所属的跟随者控制器,用于获取相邻跟随者 */
private readonly controller: IHeroFollowersController;
constructor(
num: number | string,
loc: IFacedTileLocator,
faceHandler: IFaceHandler<FaceDirection>,
controller: IHeroFollowersController
) {
const state = controller.state;
if (typeof num === 'string') {
const n = state.tileStore.idToNumber(num);
if (isNil(n)) {
logger.warn(142);
this.num = 0;
} else {
this.num = n;
}
} else {
this.num = num;
}
this.state = state;
this.rendering = new HeroRendering(state);
this.location = new HeroLocation(state, loc, faceHandler);
this.controller = controller;
}
next(): IHeroFollower | null {
const all = this.controller.getAllFollowers();
const myIndex = all.indexOf(this);
if (myIndex === -1) return null;
return this.controller.getFollower(myIndex + 1);
}
last(): IHeroFollower | null {
const all = this.controller.getAllFollowers();
const myIndex = all.indexOf(this);
if (myIndex === -1) return null;
return this.controller.getFollower(myIndex - 1);
}
saveState(compression: SaveCompression): IHeroFollowerSave {
return {
rendering: this.rendering.saveState(compression),
location: this.location.saveState(compression)
};
}
loadState(state: IHeroFollowerSave, compression: SaveCompression): void {
this.rendering.loadState(state.rendering, compression);
this.location.loadState(state.location, compression);
}
}

View File

@ -0,0 +1,146 @@
import {
Hookable,
HookController,
IFacedTileLocator,
IHookController
} from '@motajs/common';
import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import { HeroFollower } from './follower';
import {
IHeroFollowersController,
IHeroFollowersControllerHooks,
IHeroFollower,
IHeroLocation
} from './types';
export class HeroFollowersController
extends Hookable<IHeroFollowersControllerHooks>
implements IHeroFollowersController
{
readonly state: IDataCommon;
/** 跟随者列表 */
private readonly followers: HeroFollower[] = [];
/** 勇士位置对象,用于获取聚集目标位置 */
private readonly heroLocation: IHeroLocation;
/** 朝向处理器,传递给新创建的跟随者 */
private readonly faceHandler: IFaceHandler<FaceDirection>;
constructor(
state: IDataCommon,
heroLocation: IHeroLocation,
faceHandler: IFaceHandler<FaceDirection>
) {
super();
this.state = state;
this.heroLocation = heroLocation;
this.faceHandler = faceHandler;
}
protected createController(
hook: Partial<IHeroFollowersControllerHooks>
): IHookController<IHeroFollowersControllerHooks> {
return new HookController(this, hook);
}
addFollower(num: number | string): IHeroFollower {
const loc: IFacedTileLocator = {
x: this.heroLocation.x,
y: this.heroLocation.y,
direction: this.heroLocation.mover.faceDirection
};
const follower = new HeroFollower(num, loc, this.faceHandler, this);
this.followers.push(follower);
const index = this.followers.length - 1;
this.forEachHook(hook => {
hook.onAddFollower?.(follower, index);
});
return follower;
}
getFollower(index: number): IHeroFollower | null {
return this.followers[index] ?? null;
}
*getFollowersById(
num: number | string
): IterableIterator<[number, IHeroFollower]> {
const store = this.state.tileStore;
const target = typeof num === 'string' ? store.idToNumber(num)! : num;
for (let i = 0; i < this.followers.length; i++) {
if (this.followers[i].num === target) {
yield [i, this.followers[i]];
}
}
}
getAllFollowers(): IHeroFollower[] {
return this.followers.slice();
}
async removeFollower(index: number): Promise<void> {
const removed = this.followers.splice(index, 1);
if (removed.length > 0) {
this.forEachHook(hook => {
hook.onRemoveFollower?.(removed[0], index);
});
}
}
async removeAllFollowers(): Promise<void> {
const snapshot = this.followers.slice();
this.followers.length = 0;
this.forEachHook(hook => {
snapshot.forEach((v, i) => {
hook.onRemoveFollower?.(v, i);
});
});
}
async gatherFollowers(): Promise<void> {
const { x, y, mover } = this.heroLocation;
for (let i = 0; i < this.followers.length; i++) {
const follower = this.followers[i];
follower.location.mover.clear();
let skip = false;
for (let j = i - 1; j >= 0; j--) {
const dir = this.followers[j].location.mover.moveDirection;
if (dir === FaceDirection.Unknown) {
follower.location.mover.clear();
follower.location.mover.tp(x, y);
follower.location.mover.face(mover.faceDirection);
skip = true;
break;
} else {
follower.location.mover.step(dir);
}
}
if (!skip) {
follower.location.mover.step(mover.moveDirection);
}
}
const controllers = this.followers.map(
v => v.location.mover.start()?.onEnd
);
await Promise.all(controllers);
this.forEachHook(hook => {
hook.onGatherFollowers?.(false);
});
}
gatherFollowersSync(): void {
const targetX = this.heroLocation.x;
const targetY = this.heroLocation.y;
const targetDir = this.heroLocation.mover.faceDirection;
for (const follower of this.followers) {
follower.location.setPos(targetX, targetY);
follower.location.mover.face(targetDir).start();
}
this.forEachHook(hook => {
hook.onGatherFollowers?.(true);
});
}
}

View File

@ -1,4 +1,8 @@
export * from './attribute'; export * from './attribute';
export * from './follower';
export * from './followersController';
export * from './location';
export * from './rendering';
export * from './mover'; export * from './mover';
export * from './state'; export * from './state';
export * from './types'; export * from './types';

View File

@ -0,0 +1,48 @@
import { IFacedTileLocator } from '@motajs/common';
import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import { HeroMover } from './mover';
import { IHeroLocation, IHeroLocationSave, IHeroMover } from './types';
export class HeroLocation implements IHeroLocation {
x: number;
y: number;
readonly state: IDataCommon;
readonly mover: IHeroMover<this>;
constructor(
state: IDataCommon,
loc: IFacedTileLocator,
faceHandler: IFaceHandler<FaceDirection>
) {
this.state = state;
this.x = loc.x;
this.y = loc.y;
const mover = new HeroMover(this, faceHandler);
mover.faceDirection = loc.direction;
this.mover = mover;
}
setPos(x: number, y: number): void {
this.x = x;
this.y = y;
}
getCurrentFaceDirection(): FaceDirection {
return this.mover.faceDirection;
}
saveState(): IHeroLocationSave {
return {
x: this.x,
y: this.y,
direction: this.mover.faceDirection
};
}
loadState(state: IHeroLocationSave): void {
this.x = state.x;
this.y = state.y;
this.mover.face(state.direction).start();
}
}

View File

@ -1,142 +1,182 @@
import { Hookable, HookController, IHookController } from '@motajs/common'; import { ITileLocator } from '@motajs/common';
import { isNil } from 'lodash-es';
import { getFaceMovement, nextFaceDirection } from '@user/data-common';
import { import {
IHeroFollower, FaceDirection,
IHeroMoveController, getFaceMovement,
IHeroMoveControllerHooks IFaceHandler,
IMoverController,
ObjectMoveStep,
ObjectMover,
ObjectMoveStepType
} from '@user/data-common';
import {
HeroMoveCode,
IHeroLocation,
IHeroMover,
IHeroMoverConfig
} from './types'; } from './types';
import { FaceDirection } from '@user/data-common';
const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png'; export class HeroMover<T extends IHeroLocation>
extends ObjectMover<T>
export class HeroMoveController implements IHeroMover<T>
extends Hookable<IHeroMoveControllerHooks>
implements IHeroMoveController
{ {
x: number = 0; readonly tile: T;
y: number = 0;
direction: FaceDirection = FaceDirection.Down;
image: ImageIds = DEFAULT_HERO_IMAGE;
/** 当前勇士是否正在移动 */ /** 本次移动是否不记录进路线系统 */
moving: boolean = false; private noRoute: boolean = false;
alpha: number = 1; /** 本次移动是否忽略地形碰撞检测 */
private ignoreTerrain: boolean = false;
/** 本次移动是否在特定时机触发自动存档 */
private autoSave: boolean = false;
readonly followers: IHeroFollower[] = []; constructor(tile: T, faceHandler: IFaceHandler<FaceDirection>) {
super(faceHandler);
protected createController( this.tile = tile;
hook: Partial<IHeroMoveControllerHooks>
): IHookController<IHeroMoveControllerHooks> {
return new HookController(this, hook);
} }
setPosition(x: number, y: number): void { config(config: Readonly<IHeroMoverConfig>): this {
this.x = x; if (config.noRoute !== undefined) {
this.y = y; this.noRoute = config.noRoute;
this.forEachHook(hook => { }
hook.onSetPosition?.(x, y); if (config.ignoreTerrain !== undefined) {
}); this.ignoreTerrain = config.ignoreTerrain;
}
if (config.autoSave !== undefined) {
this.autoSave = config.autoSave;
}
return this;
} }
turn(direction?: FaceDirection): void { getConfig(): Readonly<IHeroMoverConfig> {
const next = isNil(direction) return {
? nextFaceDirection(this.direction) noRoute: this.noRoute,
: direction; ignoreTerrain: this.ignoreTerrain,
this.direction = next; autoSave: this.autoSave
this.forEachHook(hook => { };
hook.onTurnHero?.(next);
});
} }
startMove(): void { protected async onMoveStart(
this.moving = true; _tile: IHeroLocation,
this.forEachHook(hook => { _controller: Readonly<IMoverController>
hook.onStartMove?.(); ): Promise<void> {
}); // TODO: 通知渲染端开始移动,同步平滑视角
} }
async move(dir: FaceDirection, time: number = 100): Promise<void> { protected async onMoveEnd(
await Promise.all( _tile: IHeroLocation,
this.forEachHook(hook => { _controller: Readonly<IMoverController>
return hook.onMoveHero?.(dir, time); ): Promise<void> {
}) // TODO: 通知渲染端移动结束
); // TODO: 清除自动寻路状态
const { x, y } = getFaceMovement(dir);
this.x += x;
this.y += y;
} }
async endMove(): Promise<void> { protected async onStepStart(
if (!this.moving) return; step: ObjectMoveStep,
await Promise.all( tile: IHeroLocation,
this.forEachHook(hook => { controller: Readonly<IMoverController>
return hook.onEndMove?.(); ): Promise<number> {
}) const type = step.type;
);
this.moving = false; if (
type !== ObjectMoveStepType.Dir &&
type !== ObjectMoveStepType.DirFace &&
type !== ObjectMoveStepType.Special
) {
return HeroMoveCode.Step;
} }
async jumpHero( if (!this.ignoreTerrain) {
const result = this.checkTerrain(tile, controller);
if (result !== HeroMoveCode.Step) {
return result;
}
}
if (this.autoSave) {
// TODO: 检查当前步是否需要触发自动存档(如进入/离开地图伤害区域)
}
return HeroMoveCode.Step;
}
protected async onStepEnd(
code: number,
step: ObjectMoveStep,
tile: IHeroLocation,
controller: Readonly<IMoverController>
): Promise<ITileLocator> {
const type = step.type;
const isMovementStep =
type === ObjectMoveStepType.Dir ||
type === ObjectMoveStepType.DirFace ||
type === ObjectMoveStepType.Special;
if (
!isMovementStep &&
type !== ObjectMoveStepType.Teleport &&
type !== ObjectMoveStepType.Jump
) {
return { x: tile.x, y: tile.y };
}
if (code === HeroMoveCode.CannotMove || code === HeroMoveCode.Hit) {
void controller.stop();
// TODO: 处理撞击图块逻辑
return { x: tile.x, y: tile.y };
}
if (code === HeroMoveCode.Stop) {
void controller.stop();
// TODO: 清除自动寻路
return { x: tile.x, y: tile.y };
}
if (type === ObjectMoveStepType.Teleport) {
return this.calcPos(tile, step.x, step.y, step.rel);
}
if (type === ObjectMoveStepType.Jump) {
return this.calcPos(tile, step.x, step.y, step.rel);
}
const offset = getFaceMovement(this.moveDirection);
const nx = tile.x + offset.x;
const ny = tile.y + offset.y;
// TODO: 路线记录
// TODO: 中毒伤害处理
// TODO: 步数累计
return { x: nx, y: ny };
}
/**
* /
* @param tile
* @param x
* @param y
* @param rel 使
*/
private calcPos(
tile: IHeroLocation,
x: number, x: number,
y: number, y: number,
time: number = 500, rel: boolean
waitFollower: boolean = false ): ITileLocator {
): Promise<void> { if (rel) {
await Promise.all( return { x: tile.x + x, y: tile.y + y };
this.forEachHook(hook => { }
return hook.onJumpHero?.(x, y, time, waitFollower); return { x, y };
})
);
this.x = x;
this.y = y;
} }
setImage(image: ImageIds): void { /**
this.image = image; *
this.forEachHook(hook => { */
hook.onSetImage?.(image); private checkTerrain(
}); _tile: IHeroLocation,
} _controller: Readonly<IMoverController>
): HeroMoveCode {
setAlpha(alpha: number): void { // TODO: 需要地形检测接口(替代 core.noPass / core.canMoveHero
this.alpha = alpha; return HeroMoveCode.Step;
this.forEachHook(hook => {
hook.onSetAlpha?.(alpha);
});
}
setFollowerAlpha(identifier: string, alpha: number): void {
const follower = this.followers.find(v => v.identifier === identifier);
if (!follower) return;
follower.alpha = alpha;
this.forEachHook(hook => {
hook.onSetFollowerAlpha?.(identifier, alpha);
});
}
addFollower(follower: number, identifier: string): void {
this.followers.push({ num: follower, identifier, alpha: 1 });
this.forEachHook(hook => {
hook.onAddFollower?.(follower, identifier);
});
}
removeFollower(identifier: string, animate: boolean = false): void {
const index = this.followers.findIndex(
v => v.identifier === identifier
);
if (index === -1) return;
this.followers.splice(index, 1);
this.forEachHook(hook => {
hook.onRemoveFollower?.(identifier, animate);
});
}
removeAllFollowers(): void {
this.followers.length = 0;
this.forEachHook(hook => {
hook.onRemoveAllFollowers?.();
});
} }
} }

View File

@ -0,0 +1,41 @@
import { Hookable, HookController, IHookController } from '@motajs/common';
import { IDataCommon } from '@user/data-common';
import {
IHeroRendering,
IHeroRenderingHooks,
IHeroRenderingSave
} from './types';
export class HeroRendering
extends Hookable<IHeroRenderingHooks>
implements IHeroRendering
{
alpha: number = 1;
readonly state: IDataCommon;
constructor(state: IDataCommon) {
super();
this.state = state;
}
protected createController(
hook: Partial<IHeroRenderingHooks>
): IHookController<IHeroRenderingHooks> {
return new HookController(this, hook);
}
setAlpha(alpha: number): void {
this.alpha = alpha;
this.forEachHook(hook => {
hook.onSetAlpha?.(alpha);
});
}
saveState(): IHeroRenderingSave {
return { alpha: this.alpha };
}
loadState(state: IHeroRenderingSave): void {
this.setAlpha(state.alpha);
}
}

View File

@ -9,7 +9,7 @@ import {
IReadonlyHeroAttribute IReadonlyHeroAttribute
} from './types'; } from './types';
import { SaveCompression } from '@user/data-common'; import { SaveCompression } from '@user/data-common';
import { logger } from '@motajs/common'; import { IFacedTileLocator, logger } from '@motajs/common';
export class HeroState<THero> implements IHeroState<THero> { export class HeroState<THero> implements IHeroState<THero> {
/** 修饰器工厂函数注册表 */ /** 修饰器工厂函数注册表 */
@ -32,6 +32,10 @@ export class HeroState<THero> implements IHeroState<THero> {
return this.mover; return this.mover;
} }
getLocation(): IFacedTileLocator {
return this.mover;
}
getModifiableAttribute(): IHeroAttribute<THero> { getModifiableAttribute(): IHeroAttribute<THero> {
return this.attribute; return this.attribute;
} }

View File

@ -1,5 +1,10 @@
import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common'; import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common';
import { FaceDirection, ISaveableContent } from '@user/data-common'; import {
IDataCommonExtended,
IObjectMovable,
IObjectMover,
ISaveableContent
} from '@user/data-common';
//#region 勇士属性 //#region 勇士属性
@ -171,206 +176,189 @@ export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
//#endregion //#endregion
//#region 勇士位置
export interface IHeroLocationSave extends Readonly<IFacedTileLocator> {}
export interface IHeroLocation
extends
ISaveableContent<IHeroLocationSave>,
IObjectMovable,
IDataCommonExtended {
/** 勇士的移动对象 */
readonly mover: IObjectMover<this>;
}
//#endregion
//#region 勇士移动 //#region 勇士移动
export const enum HeroAnimateDirection { export const enum HeroMoveCode {
/** 正向播放动画 */ /** 正常移动 */
Forward, Step,
/** 反向播放动画 */ /** 移动被停止 */
Backward Stop,
/** 不能移动,并撞击目标格触发器 */
Hit,
/** 不能移动,不触发触发器 */
CannotMove
} }
export interface IHeroFollower { export interface IHeroMoverConfig {
/** 跟随者的图块数字 */ /** 本次移动是否不记录进路线系统 */
readonly num: number; noRoute?: boolean;
/** 跟随者的标识符 */ /** 本次移动是否忽略地形碰撞检测 */
readonly identifier: string; ignoreTerrain?: boolean;
/** 跟随者的不透明度 */ /** 本次移动是否在特定时机触发自动存档 */
alpha: number; autoSave?: boolean;
} }
export interface IHeroMoveControllerHooks extends IHookBase { export interface IHeroMover<T extends IHeroLocation> extends IObjectMover<T> {
/** /**
* *
* @param x * @param config
* @param y
*/ */
onSetPosition(x: number, y: number): void; config(config: Readonly<IHeroMoverConfig>): this;
/** /**
* *
* @param direction
*/ */
onTurnHero(direction: FaceDirection): void; getConfig(): Readonly<IHeroMoverConfig>;
}
/** //#endregion
*
*/
onStartMove(): void;
/** //#region 勇士渲染
*
* @param direction
* @param time
*/
onMoveHero(direction: FaceDirection, time: number): Promise<void>;
/** export interface IHeroRenderingSave {
* /** 勇士的不透明度 */
*/ readonly alpha: number;
onEndMove(): Promise<void>; }
export interface IHeroRenderingHooks extends IHookBase {
/** /**
* *
* @param x
* @param y
* @param time
* @param waitFollower
*/
onJumpHero(
x: number,
y: number,
time: number,
waitFollower: boolean
): Promise<void>;
/**
*
* @param image id
*/
onSetImage(image: ImageIds): void;
/**
*
* @param alpha * @param alpha
*/ */
onSetAlpha(alpha: number): void; onSetAlpha?(alpha: number): void;
/**
*
* @param follower
* @param identifier
*/
onAddFollower(follower: number, identifier: string): void;
/**
*
* @param identifier
* @param animate `true` 使
*/
onRemoveFollower(identifier: string, animate: boolean): void;
/**
*
*/
onRemoveAllFollowers(): void;
/**
*
* @param identifier
* @param alpha
*/
onSetFollowerAlpha(identifier: string, alpha: number): void;
} }
export interface IHeroMoveController extends IHookable<IHeroMoveControllerHooks> { export interface IHeroRendering
/** 勇士横坐标 */ extends
readonly x: number; ISaveableContent<IHeroRenderingSave>,
/** 勇士纵坐标 */ IHookable<IHeroRenderingHooks>,
readonly y: number; IDataCommonExtended {
/** 勇士朝向 */ /** 勇士的当前不透明度 */
readonly direction: FaceDirection;
/** 勇士图片 */
readonly image?: ImageIds;
/** 跟随者列表 */
readonly followers: readonly IHeroFollower[];
/** 勇士当前的不透明度 */
readonly alpha: number; readonly alpha: number;
/**
*
* @param x
* @param y
*/
setPosition(x: number, y: number): void;
/**
*
* @param direction
*/
turn(direction?: FaceDirection): void;
/**
*
*/
startMove(): void;
/**
*
* @param dir
* @param time 100ms
* @returns `Promise`
*/
move(dir: FaceDirection, time?: number): Promise<void>;
/**
*
* @returns `Promise`
*/
endMove(): Promise<void>;
/**
*
* @param x
* @param y
* @param time 500ms
* @param waitFollower
* @returns `Promise`
*/
jumpHero(
x: number,
y: number,
time?: number,
waitFollower?: boolean
): Promise<void>;
/**
*
* @param image id
*/
setImage(image: ImageIds): void;
/** /**
* *
* @param alpha * @param alpha
*/ */
setAlpha(alpha: number): void; setAlpha(alpha: number): void;
}
//#endregion
//#region 勇士跟随者
export interface IHeroFollowerSave {
/** 跟随者渲染对象保存 */
readonly rendering: IHeroRenderingSave;
/** 跟随者位置保存 */
readonly location: IHeroLocationSave;
}
export interface IHeroFollower
extends ISaveableContent<IHeroFollowerSave>, IDataCommonExtended {
/** 跟随者的图块数字 */
readonly num: number;
/** 跟随者的渲染信息对象 */
readonly rendering: IHeroRendering;
/** 跟随者的位置对象 */
readonly location: IHeroLocation;
/** /**
* *
* @param follower * @returns null
* @param identifier
*/ */
addFollower(follower: number, identifier: string): void; next(): IHeroFollower | null;
/** /**
* *
* @param identifier * @returns null
* @param animate `true` 使
*/ */
removeFollower(identifier: string, animate?: boolean): void; last(): IHeroFollower | null;
}
export interface IHeroFollowersControllerHooks extends IHookBase {
/**
*
* @param follower
* @param index
*/
onAddFollower?(follower: IHeroFollower, index: number): void;
/** /**
* *
* @param follower
* @param index
*/ */
removeAllFollowers(): void; onRemoveFollower?(follower: IHeroFollower, index: number): void;
/** /**
* *
* @param identifier * @param sync `gatherFollowersSync`
* @param alpha
*/ */
setFollowerAlpha(identifier: string, alpha: number): void; onGatherFollowers?(sync: boolean): void;
}
export interface IHeroFollowersController
extends IHookable<IHeroFollowersControllerHooks>, IDataCommonExtended {
/**
*
* @param num id
*/
addFollower(num: number | string): IHeroFollower;
/**
*
* @param index
*/
getFollower(index: number): IHeroFollower | null;
/**
* id
* @param num id
* @returns [, ]
*/
getFollowersById(num: number | string): Iterable<[number, IHeroFollower]>;
/**
*
*/
getAllFollowers(): IHeroFollower[];
/**
*
* @param index
*/
removeFollower(index: number): Promise<void>;
/**
*
*/
removeAllFollowers(): Promise<void>;
/**
*
*/
gatherFollowers(): Promise<void>;
/**
*
*/
gatherFollowersSync(): void;
} }
//#endregion //#endregion
@ -381,9 +369,9 @@ export interface IHeroStateSave<THero> {
/** 勇士属性状态 */ /** 勇士属性状态 */
readonly attribute: THero; readonly attribute: THero;
/** 勇士当前位置 */ /** 勇士当前位置 */
readonly locator: IFacedTileLocator; readonly locator: IHeroLocationSave;
/** 勇士当前的跟随者 */ /** 勇士当前的跟随者 */
readonly followers: readonly Readonly<IHeroFollower>[]; readonly followers: readonly IHeroFollowerSave[];
/** 勇士属性修饰器状态 */ /** 勇士属性修饰器状态 */
readonly modifiers: readonly IModifierStateSave[]; readonly modifiers: readonly IModifierStateSave[];
} }
@ -392,20 +380,18 @@ export interface IHeroState<THero> extends ISaveableContent<
IHeroStateSave<THero> IHeroStateSave<THero>
> { > {
/** 勇士移动对象 */ /** 勇士移动对象 */
readonly mover: IHeroMoveController; readonly location: IHeroLocation;
/** 勇士属性对象 */ /** 勇士属性对象 */
readonly attribute: IReadonlyHeroAttribute<THero>; readonly attribute: IReadonlyHeroAttribute<THero>;
/** 勇士跟随者对象 */
readonly followers: IHeroFollowersController;
/** 勇士的渲染对象,包含一些必要渲染信息,存在于数据端,并非渲染端 */
readonly rendering: IHeroRendering;
/** /**
* *
* @param mover
*/ */
attachMover(mover: IHeroMoveController): void; getLocation(): IFacedTileLocator;
/**
*
*/
getHeroMover(): IHeroMoveController;
/** /**
* *

View File

@ -72,6 +72,19 @@ export class DynamicTileMover extends ObjectMover<IDynamicTile> {
locator.y += y; locator.y += y;
break; break;
} }
case ObjectMoveStepType.Teleport:
case ObjectMoveStepType.Jump: {
const { x, y, rel } = step;
tile.setFaceDirection(this.faceDirection);
if (rel) {
locator.x += x;
locator.y += y;
} else {
locator.x = x;
locator.y = y;
}
break;
}
} }
return Promise.resolve(locator); return Promise.resolve(locator);
} }

View File

@ -2,3 +2,5 @@ import { FaceGroup } from '@user/data-common';
/** 动态图块所使用的默认移动组,不知道干什么的就别动 */ /** 动态图块所使用的默认移动组,不知道干什么的就别动 */
export const DYNAMIC_MOVER_FACE = FaceGroup.Dir8; export const DYNAMIC_MOVER_FACE = FaceGroup.Dir8;
/** 勇士移动所使用的默认移动组,不知道干什么的就别动 */
export const HERO_MOVER_FACE = FaceGroup.Dir8;

View File

@ -23,7 +23,11 @@ export const enum ObjectMoveStepType {
/** 特殊步,如前进或后退 */ /** 特殊步,如前进或后退 */
Special, Special,
/** 动画方向步 */ /** 动画方向步 */
AnimDir AnimDir,
/** 传送步 */
Teleport,
/** 跳跃步 */
Jump
} }
export const enum ObjectSpecialStep { export const enum ObjectSpecialStep {
@ -103,13 +107,37 @@ export interface IObjectMoveAnimDir {
dir: ObjectAnimDirection; dir: ObjectAnimDirection;
} }
export interface IObjectMoveTP {
/** 步骤类型 */
type: ObjectMoveStepType.Teleport;
/** 目标横坐标 */
x: number;
/** 目标纵坐标 */
y: number;
/** 是否相对模式 */
rel: boolean;
}
export interface IObjectMoveJump {
/** 步骤类型 */
type: ObjectMoveStepType.Jump;
/** 目标横坐标 */
x: number;
/** 目标纵坐标 */
y: number;
/** 是否相对模式 */
rel: boolean;
}
export type ObjectMoveStep = export type ObjectMoveStep =
| IObjectMoveStepDir | IObjectMoveStepDir
| IObjectMoveStepDirFace | IObjectMoveStepDirFace
| IObjectMoveStepSpeed | IObjectMoveStepSpeed
| IObjectMoveStepFace | IObjectMoveStepFace
| IObjectMoveStepSpecial | IObjectMoveStepSpecial
| IObjectMoveAnimDir; | IObjectMoveAnimDir
| IObjectMoveTP
| IObjectMoveJump;
export interface IMoverController { export interface IMoverController {
/** 本次移动是否已经全部完成 */ /** 本次移动是否已经全部完成 */
@ -195,6 +223,22 @@ export interface IObjectMover<T extends IObjectMovable> extends IHookable<
/** 当前移动速度,单位毫秒 */ /** 当前移动速度,单位毫秒 */
readonly currentSpeed: number; readonly currentSpeed: number;
/**
*
* @param x
* @param y
* @param rel 使
*/
tp(x: number, y: number, rel?: boolean): this;
/**
*
* @param x
* @param y
* @param rel 使
*/
jump(x: number, y: number, rel?: boolean): this;
/** /**
* *
* @param dir * @param dir
@ -396,6 +440,26 @@ export abstract class ObjectMover<T extends IObjectMovable>
} }
} }
tp(x: number, y: number, rel: boolean = false): this {
this.pushStep({
type: ObjectMoveStepType.Teleport,
x,
y,
rel
});
return this;
}
jump(x: number, y: number, rel: boolean = false): this {
this.pushStep({
type: ObjectMoveStepType.Jump,
x,
y,
rel
});
return this;
}
step(dir: FaceDirection, count: number = 1): this { step(dir: FaceDirection, count: number = 1): this {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
this.pushStep({ this.pushStep({

View File

@ -1,8 +1,5 @@
export function create() {}
export * from './enemy'; export * from './enemy';
export * from './hero'; export * from './hero';
export * from './legacy';
export * from './core'; export * from './core';
export * from './ins'; export * from './ins';

View File

@ -25,5 +25,4 @@ export function create() {
function createModule() { function createModule() {
LegacyPluginData.createLegacy(); LegacyPluginData.createLegacy();
DataState.create();
} }

View File

@ -198,6 +198,7 @@
"138": "Expected $1 with a same state with the CombatFlow object, but got a difference one.", "138": "Expected $1 with a same state with the CombatFlow object, but got a difference one.",
"139": "Expected $1 binding before start battle flow, but got a null object.", "139": "Expected $1 binding before start battle flow, but got a null object.",
"140": "Different priority of combat script is expected, but got a same one.", "140": "Different priority of combat script is expected, but got a same one.",
"141": "Some issue may occur in binded damage system on combat flow, please check the console." "141": "Some issue may occur in binded damage system on combat flow, please check the console.",
"142": "Expected a specific tile number after convert id '$1' to number, but got null."
} }
} }

View File

@ -12,6 +12,12 @@
- **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。 - **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。
7. **不要有任何“顺手”的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug然后**顺手**修复、发现一处类型错误,然后**顺手**修复等,这种情况下应当遵循规则 1。 7. **不要有任何“顺手”的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug然后**顺手**修复、发现一处类型错误,然后**顺手**修复等,这种情况下应当遵循规则 1。
8. **写完代码后说明修改内容**:写完代码后,必须在对话中说明自己修改了哪些内容,包括所有的文件及修改了每个文件的哪些东西。 8. **写完代码后说明修改内容**:写完代码后,必须在对话中说明自己修改了哪些内容,包括所有的文件及修改了每个文件的哪些东西。
9. **关于 `dev.md` 中的一些补充**:为了提高代码质量和可读性,所有包含“一般不建议”或“尽量避免”等字样的,在此处要求为绝对不能使用,如果必须使用,需在使用前向我确认,`as` 关键字除外。所有包含“建议怎么做”或“最好怎么做”的内容,在此处要求为必须使用。
10. **关于文件修改**:任何时候要进行文件修改时,务必重新阅读文件以确保上下文中的文件是最新版,因为我可能会在两次对话间修改文件。
11. **参数最小化 / 级联获取**:不从外部传入可通过已有引用获取的对象。若对象已持有对父对象或相关对象的引用(如 `controller`),应直接通过该引用获取所需数据(如 `controller.state`),不应再单独传递参数。即“非必要不传参”。
12. **转换下沉**:数据的格式或类型转换(如 `string` 的 id 转 `number`)应放在最终消费该值的对象中完成,而非在调用方预先转换后再传入。这样可以减少调用方的职责并避免重复转换。
13. **通过公有接口操作,不直接赋值内部属性**:对某个对象内部状态的修改,必须通过该对象对外暴露的公有方法进行(如用 `mover.face(dir).start()` 设置朝向并发起移动),而不是通过类型断言访问其内部属性直接赋值(如 `mover.faceDirection = dir`)。
14. **map/filter 优于 for 循环**:对数组进行数据收集或转换时(如收集 Promise使用 `map`、`filter` 等函数式写法,比 `for` 循环 + `push` 的可读性更高。
# 建议规则 # 建议规则
@ -30,19 +36,19 @@
当我提出需求时,若没有明确说明直接实现或有其他明确要求,则遵循如下开发流程: 当我提出需求时,若没有明确说明直接实现或有其他明确要求,则遵循如下开发流程:
1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,放在 `docs/dev` 目录下。文档中需明确标注需求细节,以及代码实现的大体思路。此阶段需考虑全面,遇到任何问题应向我提问并确认,不得自行假设。 1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,放在 `docs/dev` 目录下,注意有可能在其子文件夹下,例如 `docs/dev/hero`,自行判断应该放到哪。文档中需明确标注需求细节,以及代码实现的大体思路。此阶段需考虑全面,遇到任何问题应向我提问并确认,不得自行假设。
2. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。 2. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。
## 示例文档 ## 示例文档
对于新增接口/彻底性地重构接口按照以下格式编写其余需求按照自己的理解去写即可。如某部分需要详细描述可单独开设标题若某个接口内容较多也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。文档保证简洁,不要过于啰嗦,但必须包含所有的必要信息,控制在 100-250 行以内 对于新增接口/彻底性地重构接口按照以下格式编写其余需求按照自己的理解去写即可。如某部分需要详细描述可单独开设标题若某个接口内容较多也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。文档保证简洁,不要过于啰嗦,但必须包含所有的必要信息,控制在 100-250 行,格式清晰,不要擅自修改示例文档的格式。一般情况下不需要画任何流程图,如果必须要画,使用 `mermaid` 图。不要在文档中使用引用块,因为这是我在给文档添加批注时使用的东西
```md ```md
# 需求综述 # 需求综述
描述清楚需求的内容、动机与目的。 描述清楚需求的内容、动机与目的。
# 接口设计与预期 # 接口设计分析
这是相当重要的一章。需要按接口逐一列出其方法与成员,分析每个成员和方法的预期使用频率(高 / 中 / 低)并说明判断依据;对于中频和高频成员,还需列出至少一个典型使用场景。必须认真分析频率,想明白**用户**调用的频率,而不是**引擎**调用的频率,比如战斗函数在引擎中可能是中频,但是对用户来说,用户一般会使用系统默认行为来战斗,而**不会**自己调用,因此属于低频。 这是相当重要的一章。需要按接口逐一列出其方法与成员,分析每个成员和方法的预期使用频率(高 / 中 / 低)并说明判断依据;对于中频和高频成员,还需列出至少一个典型使用场景。必须认真分析频率,想明白**用户**调用的频率,而不是**引擎**调用的频率,比如战斗函数在引擎中可能是中频,但是对用户来说,用户一般会使用系统默认行为来战斗,而**不会**自己调用,因此属于低频。
@ -54,11 +60,17 @@
## IObjectMover ## IObjectMover
### 接口综述
这里讲解接口的设计,注意不要直接写成 ts 代码,也不要简单地一个个列出,而是需要分析需求,然后经过推理自然地得出接口设计。
### 接口分析
- `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。 - `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率通常只在特殊演出或逻辑中出现故为中频。典型使用场景NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。 - `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率通常只在特殊演出或逻辑中出现故为中频。典型使用场景NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。 - `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
## 预期体量 ### 预期体量
本节应当写出预期的代码体量,并分析原因。示例如下: 本节应当写出预期的代码体量,并分析原因。示例如下:
@ -67,9 +79,9 @@
- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。 - `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。
- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。 - `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。
## 可能风险 ### 可能风险
在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。 在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,例如导致大面积的类型污染、或部分其他接口会出现类型错误等,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。一些细小的内容不要写到这。如果没有任何风险,不要写无风险,直接跳过此章节。
# 实现思路 # 实现思路

View File

@ -23,3 +23,7 @@
- 寻路系统 - 寻路系统
- `core.status.hero` - `core.status.hero`
- `core.status.hero.flags` - `core.status.hero.flags`
# 其他备忘录
- 大地图远景?