From fadcc98864a2cea8f6e4f5c320e14a66ca650ab5 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sat, 27 Jun 2026 20:33:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=B7=9F=E9=9A=8F=E8=80=85?= =?UTF-8?q?=E4=B8=8E=E5=8B=87=E5=A3=AB=E7=A7=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev.md | 11 +- docs/dev/hero/hero-follower-interfaces.md | 140 ++++++++ docs/dev/hero/hero-mover.md | 121 +++++++ package.json | 1 + packages-user/data-base/src/hero/follower.ts | 77 ++++ .../data-base/src/hero/followersController.ts | 146 ++++++++ packages-user/data-base/src/hero/index.ts | 4 + packages-user/data-base/src/hero/location.ts | 48 +++ packages-user/data-base/src/hero/mover.ts | 276 ++++++++------- packages-user/data-base/src/hero/rendering.ts | 41 +++ packages-user/data-base/src/hero/state.ts | 6 +- packages-user/data-base/src/hero/types.ts | 332 +++++++++--------- packages-user/data-base/src/map/mover.ts | 13 + packages-user/data-base/src/shared.ts | 2 + packages-user/data-common/src/common/mover.ts | 68 +++- packages-user/data-state/src/index.ts | 3 - packages-user/entry-data/src/create.ts | 1 - packages/common/src/logger.json | 3 +- prompt.md | 24 +- task.md | 4 + 20 files changed, 1011 insertions(+), 310 deletions(-) create mode 100644 docs/dev/hero/hero-follower-interfaces.md create mode 100644 docs/dev/hero/hero-mover.md create mode 100644 packages-user/data-base/src/hero/follower.ts create mode 100644 packages-user/data-base/src/hero/followersController.ts create mode 100644 packages-user/data-base/src/hero/location.ts create mode 100644 packages-user/data-base/src/hero/rendering.ts diff --git a/dev.md b/dev.md index cd31042..a3da7f3 100644 --- a/dev.md +++ b/dev.md @@ -3,8 +3,8 @@ ## 项目结构 - `public`: mota-js 样板所在目录 -- `packages`: 核心引擎代码 monorepo -- `packages-user`: 用户代码 monorepo +- `packages`: 核心引擎代码 monorepo(`@motajs/`) +- `packages-user`: 用户代码 monorepo(`@user/`) - `src`: 游戏入口代码 依赖关系为单向:`src` → `packages-user` → `packages`。`packages` 与 `packages-user` 均可独立打包为库模式,`src` 为游戏的入口代码。 @@ -62,7 +62,7 @@ | 代码文件名 | 小驼峰 | | Markdown 文档文件名 | 连字符命名法 | -不使用下划线命名法。 +不使用下划线命名法,私有属性或方法不要以下划线开头。 ### 注释规范 @@ -85,9 +85,10 @@ - 未使用的变量或方法以下划线开头命名。 - 合理使用 `readonly`、`protected`、`private` 关键字。 - 可选参数过多时(大于两个),考虑改用对象参数。 -- 尽量避免 `as` 类型断言,除非必要。 +- 尽量避免 `as` 类型断言,除非必要。绝对不允许出现连续 `as`,例如 `as unknown as xxx`。 - 函数类型单独开一个 `type` 类型别名,除非函数类型本身较短(小于 20 字符,且不会因为此函数类型导致换行)。 - 当任何时候必须出现对象类型的时候,单独声明一个 `interface`,不得出现直接的对象类型。 +- 对于某个类中的对象成员,必须使用接口作为类型,不得使用类作为类型,除非某些极特殊情况。 ### 其他要求 @@ -99,7 +100,7 @@ - 只进行必要的非空判断,非必要时直接使用非空断言 `!`。 - 除非参数要求传入函数等情况,不建议在函数内定义局部函数。 - 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。 -- 一般不建议写 `getter` 和 `setter`,但某些场景下还是可以使用的。 +- 一般不建议写 `getter` 和 `setter`,但在极少数场景下还是可以使用的。 ## 双端分离 diff --git a/docs/dev/hero/hero-follower-interfaces.md b/docs/dev/hero/hero-follower-interfaces.md new file mode 100644 index 0000000..dcae1c6 --- /dev/null +++ b/docs/dev/hero/hero-follower-interfaces.md @@ -0,0 +1,140 @@ +# 需求综述 + +对勇士跟随者相关接口进行结构性重构。核心动机是让跟随者复用勇士已有的位置与渲染接口(`IHeroLocation`、`IHeroRendering`),而非为跟随者定义独立的位置/渲染类型。这样跟随者与勇士共享同一套移动与渲染模型,简化接口层级,避免重复定义。 + +重构四个接口:`IHeroLocation`、`IHeroRendering`、`IHeroFollower`、`IHeroFollowersController`。前两者是勇士与跟随者共用的基础接口,后两者专属于跟随者管理。同时需要将旧的 `HeroMoveController` 类拆分为 `HeroLocation`、`HeroRendering`、`HeroFollowersController` 三个独立类,`HeroFollower` 为新实现类,并修改 `HeroState` 适配新接口。 + +# 接口设计分析 + +## IHeroLocation + +### 接口综述 + +勇士位置接口,代表一个可移动对象在游戏世界中的坐标与朝向,并挂载移动器负责移动控制。该接口继承 `ISaveableContent` 实现存档读档,继承 `IObjectMovable` 获得基础位置读写能力。存档格式为 `IFacedTileLocator`(包含 `x`、`y`、`direction`)。移动能力由 `IObjectMover` 提供,支持行走、跳跃、瞬移、朝向控制等全部需求。 + +### 接口分析 + +- `IHeroLocation.mover`:预期频率**高频**。几乎所有移动操作都经由 `mover` 进行,在剧情演出与逻辑中频繁出现。典型使用场景:演出中让角色沿指定路线移动。 + +## IHeroRendering + +### 接口综述 + +勇士渲染信息接口,存在于数据端,存放无需进入渲染端即可管理的渲染相关数据。当前仅包含透明度(`alpha`),后续可能扩展。继承 `ISaveableContent` 实现存档读档,继承 `IHookable` 支持透明度变化时触发回调。旧实现中的 `image` 作为纯渲染端概念被移除。 + +### 接口分析 + +- `IHeroRendering.alpha`:预期频率**低频**。直接读取透明度值的需求极少。 +- `IHeroRendering.setAlpha`:预期频率**低频**。设置透明度仅在特殊效果中出现,一座塔中调用次数有限。 + +## IHeroFollower + +### 接口综述 + +跟随者接口,包含跟随者的渲染信息与位置信息。直接复用 `IHeroRendering` 和 `IHeroLocation`,使跟随者拥有与勇士完全一致的移动与渲染能力。存档格式 `IHeroFollowerSave` 嵌套包含 `IHeroRenderingSave` 和 `IHeroLocationSave`。对比旧 `IHeroFollower = { num, identifier, alpha }` 的纯数据结构,新接口让跟随者从静态数据升级为拥有完整移动能力的独立对象。 + +### 接口分析 + +- `IHeroFollower.rendering`:预期频率**低频**。 +- `IHeroFollower.location`:预期频率**低频**。 + +> 跟随者本身就是一个使用场景极低的需求,大多数游戏中都不会使用到一两次,所以几乎所有跟随者相关接口都应该是低频。 + +## IHeroFollowersController + +### 接口综述 + +跟随者控制器接口,负责跟随者的增删查改与聚集操作。继承 `IHookable`,提供添加、移除、聚集三个事件的钩子支持。对比旧实现:索引取代 `identifier` 作为查找方式,`setFollowerAlpha` 移除(改为通过 `follower.rendering.setAlpha` 操作),`addFollower` 的 `num` 参数合并了旧的图块数字与标识符。 + +### 接口分析 + +- `IHeroFollowersController.addFollower`:预期频率**低频**。 +- `IHeroFollowersController.getFollower`:预期频率**低频**。 +- `IHeroFollowersController.getFollowersById`:预期频率**低频**。 +- `IHeroFollowersController.getAllFollowers`:预期频率**低频**。 +- `IHeroFollowersController.removeFollower`:预期频率**低频**。 +- `IHeroFollowersController.removeAllFollowers`:预期频率**低频**。 +- `IHeroFollowersController.gatherFollowers`:预期频率**低频**。 +- `IHeroFollowersController.gatherFollowersSync`:预期频率**低频**。 + +### 预期体量 + +预期代码体量合计约 250–300 行。分析如下: + +- `HeroLocation`:需实现 `IObjectMovable` 和 `ISaveableContent`,并创建内部 `ObjectMover` 子类,预计 70–90 行。 +- `HeroRendering`:仅管理 `alpha` 的存取和钩子触发,预计 25–35 行。 +- `HeroFollower`:组合 `HeroRendering` 和 `HeroLocation` 并实现存档读档,预计 35–45 行。 +- `HeroFollowersController`:维护跟随者列表和增删查改聚集操作,预计 120–150 行。 + +# 涉及文件 + +## 需要修改的文件 + +### `@user/data-base/hero/types.ts` + +- [x] 重构 `IHeroLocation` 接口:改为继承 `ISaveableContent` 与 `IObjectMovable`,仅保留 `mover` 成员。 +- [x] 新增 `IHeroLocationSave` 类型别名。 +- [x] 重构 `IHeroRendering` 接口:新增继承 `ISaveableContent` 与 `IHookable`。 +- [x] 新增 `IHeroRenderingSave` 接口。 +- [x] 新增 `IHeroRenderingHooks` 接口。 +- [x] 新增 `IHeroFollowerSave` 接口。 +- [x] 重构 `IHeroFollower` 接口:改为组合 `rendering` 与 `location`,继承 `ISaveableContent`。 +- [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` 子类实例,挂载为 `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` 的导出。 diff --git a/docs/dev/hero/hero-mover.md b/docs/dev/hero/hero-mover.md new file mode 100644 index 0000000..3869506 --- /dev/null +++ b/docs/dev/hero/hero-mover.md @@ -0,0 +1,121 @@ +# 需求综述 + +将旧样板中的 `HeroMover`(`packages-user/data-state/src/legacy/move.ts`)基于新的 `IObjectMover` / `ObjectMover` 基类进行彻底性重构,编写新的 `IHeroMover` 接口和 `HeroMover` 实现类。 + +新实现的核心变化: + +- 基类从旧 `ObjectMoverBase`(EventEmitter 模式)迁移至 `ObjectMover`(Hook 模式)。 +- `inLockControl` 删除,重构后不再需要。 +- 移动速度不再由 `HeroMover` 存储,速度变更通过 `ObjectMover.speed()` 方法体现,实际动画时长在 Hook 中自行处理。 +- `noRoute`、`ignoreTerrain`、`autoSave` 不再作为公开属性暴露,改为通过 `config()` / `getConfig()` 方法管理,配置对象为 `IHeroMoverConfig`。 +- 摒弃所有 `core.xxx` 调用,这些功能将在后续通过新接口逐步补充。 +- `HeroMover` 的实例化由 `IHeroLocation` 的实现类内部完成,构造时传入 `this` 作为 `tile`,解决循环依赖。 + +# 接口设计分析 + +`IHeroMover` 直接扩展 `IObjectMover`,以下仅分析新增内容。 + +## 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`。由于 `IHeroMover extends IObjectMover`,若 `HeroMover` 的构造在 `IHeroLocation` 内部完成(传入 `this`),类型可自然满足且无循环依赖问题。但需确保 `IHeroLocation` 的实现类确实在其构造过程中创建并持有 `HeroMover` 实例。 + +# 实现思路 + +## 1. 在 `types.ts` 中新增相关接口与枚举 + +新增 `IHeroMoverConfig` 接口,包含 `noRoute`、`ignoreTerrain`、`autoSave` 三个可选布尔成员。 + +新增 `IHeroMover` 接口,继承 `IObjectMover`,新增 `config(config)` 和 `getConfig()` 两个方法。 + +新增 `HeroMoveCode` 枚举,公开导出,包含正常移动、停止、撞击、不可移动等代码,供 Hook 用户判断每步移动结果。 + +## 2. 完成 `HeroMover` 类 + +在 `packages-user/data-state/src/hero/mover.ts` 中实现 `HeroMover extends ObjectMover`。 + +构造函数接收两个参数: +- `tile: IHeroLocation` — 勇士位置对象,同时满足 `IObjectMovable`,作为基类移动操作的绑定目标。 +- `faceHandler: IFaceHandler` — 朝向处理器,由 `IHeroLocation` 实现类在创建 `HeroMover` 时传入(本样式中为 `Dir4FaceHandler`)。 + +`config(config)`:将传入配置中已显式提供的字段分别赋值到对应的私有属性 `noRoute`、`ignoreTerrain`、`autoSave`,未传入的字段保持原值,返回 `this` 以支持链式调用。 + +`getConfig()`:从三个私有属性分别读取当前值,组装为 `Readonly` 返回,默认值均为 `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`,新增 `config()` 和 `getConfig()` 方法。 +- [ ] 新增 `HeroMoveCode` 枚举:定义勇士移动步骤返回码,公开导出供 Hook 使用。 + +### `packages-user/data-state/src/hero/mover.ts`(新建) + +- [ ] 实现 `HeroMover` 类:继承 `ObjectMover`,实现 `config`、`getConfig` 及基类四个抽象方法。 + +### `packages-user/data-state/src/hero/index.ts` + +- [ ] 新增 `export * from './mover'`:导出新模块。 diff --git a/package.json b/package.json index 4d52ac9..eda7629 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "declare": "tsx script/declare.ts", "type": "vue-tsc --noEmit", "lines": "tsx script/lines.ts packages packages-user", + "lint": "eslint", "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:lib": "vue-tsc --noEmit && tsx script/build-lib.ts", diff --git a/packages-user/data-base/src/hero/follower.ts b/packages-user/data-base/src/hero/follower.ts new file mode 100644 index 0000000..cf4f002 --- /dev/null +++ b/packages-user/data-base/src/hero/follower.ts @@ -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, + 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); + } +} diff --git a/packages-user/data-base/src/hero/followersController.ts b/packages-user/data-base/src/hero/followersController.ts new file mode 100644 index 0000000..5b00340 --- /dev/null +++ b/packages-user/data-base/src/hero/followersController.ts @@ -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 + implements IHeroFollowersController +{ + readonly state: IDataCommon; + /** 跟随者列表 */ + private readonly followers: HeroFollower[] = []; + /** 勇士位置对象,用于获取聚集目标位置 */ + private readonly heroLocation: IHeroLocation; + /** 朝向处理器,传递给新创建的跟随者 */ + private readonly faceHandler: IFaceHandler; + + constructor( + state: IDataCommon, + heroLocation: IHeroLocation, + faceHandler: IFaceHandler + ) { + super(); + this.state = state; + this.heroLocation = heroLocation; + this.faceHandler = faceHandler; + } + + protected createController( + hook: Partial + ): IHookController { + 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 { + const removed = this.followers.splice(index, 1); + if (removed.length > 0) { + this.forEachHook(hook => { + hook.onRemoveFollower?.(removed[0], index); + }); + } + } + + async removeAllFollowers(): Promise { + const snapshot = this.followers.slice(); + this.followers.length = 0; + this.forEachHook(hook => { + snapshot.forEach((v, i) => { + hook.onRemoveFollower?.(v, i); + }); + }); + } + + async gatherFollowers(): Promise { + 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); + }); + } +} diff --git a/packages-user/data-base/src/hero/index.ts b/packages-user/data-base/src/hero/index.ts index b53355f..41dec93 100644 --- a/packages-user/data-base/src/hero/index.ts +++ b/packages-user/data-base/src/hero/index.ts @@ -1,4 +1,8 @@ export * from './attribute'; +export * from './follower'; +export * from './followersController'; +export * from './location'; +export * from './rendering'; export * from './mover'; export * from './state'; export * from './types'; diff --git a/packages-user/data-base/src/hero/location.ts b/packages-user/data-base/src/hero/location.ts new file mode 100644 index 0000000..206788d --- /dev/null +++ b/packages-user/data-base/src/hero/location.ts @@ -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; + + constructor( + state: IDataCommon, + loc: IFacedTileLocator, + faceHandler: IFaceHandler + ) { + 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(); + } +} diff --git a/packages-user/data-base/src/hero/mover.ts b/packages-user/data-base/src/hero/mover.ts index bab6792..3328285 100644 --- a/packages-user/data-base/src/hero/mover.ts +++ b/packages-user/data-base/src/hero/mover.ts @@ -1,142 +1,182 @@ -import { Hookable, HookController, IHookController } from '@motajs/common'; -import { isNil } from 'lodash-es'; -import { getFaceMovement, nextFaceDirection } from '@user/data-common'; +import { ITileLocator } from '@motajs/common'; import { - IHeroFollower, - IHeroMoveController, - IHeroMoveControllerHooks + FaceDirection, + getFaceMovement, + IFaceHandler, + IMoverController, + ObjectMoveStep, + ObjectMover, + ObjectMoveStepType +} from '@user/data-common'; +import { + HeroMoveCode, + IHeroLocation, + IHeroMover, + IHeroMoverConfig } from './types'; -import { FaceDirection } from '@user/data-common'; -const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png'; - -export class HeroMoveController - extends Hookable - implements IHeroMoveController +export class HeroMover + extends ObjectMover + implements IHeroMover { - x: number = 0; - y: number = 0; - direction: FaceDirection = FaceDirection.Down; - image: ImageIds = DEFAULT_HERO_IMAGE; + readonly tile: T; - /** 当前勇士是否正在移动 */ - moving: boolean = false; - alpha: number = 1; + /** 本次移动是否不记录进路线系统 */ + private noRoute: boolean = false; + /** 本次移动是否忽略地形碰撞检测 */ + private ignoreTerrain: boolean = false; + /** 本次移动是否在特定时机触发自动存档 */ + private autoSave: boolean = false; - readonly followers: IHeroFollower[] = []; - - protected createController( - hook: Partial - ): IHookController { - return new HookController(this, hook); + constructor(tile: T, faceHandler: IFaceHandler) { + super(faceHandler); + this.tile = tile; } - setPosition(x: number, y: number): void { - this.x = x; - this.y = y; - this.forEachHook(hook => { - hook.onSetPosition?.(x, y); - }); + config(config: Readonly): this { + if (config.noRoute !== undefined) { + this.noRoute = config.noRoute; + } + if (config.ignoreTerrain !== undefined) { + this.ignoreTerrain = config.ignoreTerrain; + } + if (config.autoSave !== undefined) { + this.autoSave = config.autoSave; + } + return this; } - turn(direction?: FaceDirection): void { - const next = isNil(direction) - ? nextFaceDirection(this.direction) - : direction; - this.direction = next; - this.forEachHook(hook => { - hook.onTurnHero?.(next); - }); + getConfig(): Readonly { + return { + noRoute: this.noRoute, + ignoreTerrain: this.ignoreTerrain, + autoSave: this.autoSave + }; } - startMove(): void { - this.moving = true; - this.forEachHook(hook => { - hook.onStartMove?.(); - }); + protected async onMoveStart( + _tile: IHeroLocation, + _controller: Readonly + ): Promise { + // TODO: 通知渲染端开始移动,同步平滑视角 } - async move(dir: FaceDirection, time: number = 100): Promise { - await Promise.all( - this.forEachHook(hook => { - return hook.onMoveHero?.(dir, time); - }) - ); - const { x, y } = getFaceMovement(dir); - this.x += x; - this.y += y; + protected async onMoveEnd( + _tile: IHeroLocation, + _controller: Readonly + ): Promise { + // TODO: 通知渲染端移动结束 + // TODO: 清除自动寻路状态 } - async endMove(): Promise { - if (!this.moving) return; - await Promise.all( - this.forEachHook(hook => { - return hook.onEndMove?.(); - }) - ); - this.moving = false; + protected async onStepStart( + step: ObjectMoveStep, + tile: IHeroLocation, + controller: Readonly + ): Promise { + const type = step.type; + + if ( + type !== ObjectMoveStepType.Dir && + type !== ObjectMoveStepType.DirFace && + type !== ObjectMoveStepType.Special + ) { + return HeroMoveCode.Step; + } + + if (!this.ignoreTerrain) { + const result = this.checkTerrain(tile, controller); + if (result !== HeroMoveCode.Step) { + return result; + } + } + + if (this.autoSave) { + // TODO: 检查当前步是否需要触发自动存档(如进入/离开地图伤害区域) + } + + return HeroMoveCode.Step; } - async jumpHero( + protected async onStepEnd( + code: number, + step: ObjectMoveStep, + tile: IHeroLocation, + controller: Readonly + ): Promise { + 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, y: number, - time: number = 500, - waitFollower: boolean = false - ): Promise { - await Promise.all( - this.forEachHook(hook => { - return hook.onJumpHero?.(x, y, time, waitFollower); - }) - ); - this.x = x; - this.y = y; + rel: boolean + ): ITileLocator { + if (rel) { + return { x: tile.x + x, y: tile.y + y }; + } + return { x, y }; } - setImage(image: ImageIds): void { - this.image = image; - this.forEachHook(hook => { - hook.onSetImage?.(image); - }); - } - - setAlpha(alpha: number): void { - this.alpha = alpha; - 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?.(); - }); + /** + * 检测当前移动方向前方一格的地形是否可通行 + */ + private checkTerrain( + _tile: IHeroLocation, + _controller: Readonly + ): HeroMoveCode { + // TODO: 需要地形检测接口(替代 core.noPass / core.canMoveHero) + return HeroMoveCode.Step; } } diff --git a/packages-user/data-base/src/hero/rendering.ts b/packages-user/data-base/src/hero/rendering.ts new file mode 100644 index 0000000..029788e --- /dev/null +++ b/packages-user/data-base/src/hero/rendering.ts @@ -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 + implements IHeroRendering +{ + alpha: number = 1; + readonly state: IDataCommon; + + constructor(state: IDataCommon) { + super(); + this.state = state; + } + + protected createController( + hook: Partial + ): IHookController { + 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); + } +} diff --git a/packages-user/data-base/src/hero/state.ts b/packages-user/data-base/src/hero/state.ts index ece98db..9e45d56 100644 --- a/packages-user/data-base/src/hero/state.ts +++ b/packages-user/data-base/src/hero/state.ts @@ -9,7 +9,7 @@ import { IReadonlyHeroAttribute } from './types'; import { SaveCompression } from '@user/data-common'; -import { logger } from '@motajs/common'; +import { IFacedTileLocator, logger } from '@motajs/common'; export class HeroState implements IHeroState { /** 修饰器工厂函数注册表 */ @@ -32,6 +32,10 @@ export class HeroState implements IHeroState { return this.mover; } + getLocation(): IFacedTileLocator { + return this.mover; + } + getModifiableAttribute(): IHeroAttribute { return this.attribute; } diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 42febfe..99f5a85 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -1,5 +1,10 @@ import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common'; -import { FaceDirection, ISaveableContent } from '@user/data-common'; +import { + IDataCommonExtended, + IObjectMovable, + IObjectMover, + ISaveableContent +} from '@user/data-common'; //#region 勇士属性 @@ -171,206 +176,189 @@ export interface IHeroAttribute extends IReadonlyHeroAttribute { //#endregion +//#region 勇士位置 + +export interface IHeroLocationSave extends Readonly {} + +export interface IHeroLocation + extends + ISaveableContent, + IObjectMovable, + IDataCommonExtended { + /** 勇士的移动对象 */ + readonly mover: IObjectMover; +} + +//#endregion + //#region 勇士移动 -export const enum HeroAnimateDirection { - /** 正向播放动画 */ - Forward, - /** 反向播放动画 */ - Backward +export const enum HeroMoveCode { + /** 正常移动 */ + Step, + /** 移动被停止 */ + Stop, + /** 不能移动,并撞击目标格触发器 */ + Hit, + /** 不能移动,不触发触发器 */ + CannotMove } -export interface IHeroFollower { - /** 跟随者的图块数字 */ - readonly num: number; - /** 跟随者的标识符 */ - readonly identifier: string; - /** 跟随者的不透明度 */ - alpha: number; +export interface IHeroMoverConfig { + /** 本次移动是否不记录进路线系统 */ + noRoute?: boolean; + /** 本次移动是否忽略地形碰撞检测 */ + ignoreTerrain?: boolean; + /** 本次移动是否在特定时机触发自动存档 */ + autoSave?: boolean; } -export interface IHeroMoveControllerHooks extends IHookBase { +export interface IHeroMover extends IObjectMover { /** - * 当设置勇士的坐标时触发 - * @param x 勇士横坐标 - * @param y 勇士纵坐标 + * 配置本次移动的行为模式 + * @param config 配置对象,未传入的字段保持当前值 */ - onSetPosition(x: number, y: number): void; + config(config: Readonly): this; /** - * 当设置勇士朝向时触发 - * @param direction 勇士朝向 + * 获取当前移动配置的只读快照 */ - onTurnHero(direction: FaceDirection): void; + getConfig(): Readonly; +} - /** - * 当勇士开始移动时触发 - */ - onStartMove(): void; +//#endregion - /** - * 当移动勇士时触发 - * @param direction 移动方向 - * @param time 移动动画时长 - */ - onMoveHero(direction: FaceDirection, time: number): Promise; +//#region 勇士渲染 - /** - * 当停止移动时触发 - */ - onEndMove(): Promise; +export interface IHeroRenderingSave { + /** 勇士的不透明度 */ + readonly alpha: number; +} +export interface IHeroRenderingHooks extends IHookBase { /** - * 当勇士跳跃时触发 - * @param x 目标点横坐标 - * @param y 目标点纵坐标 - * @param time 跳跃动画时长 - * @param waitFollower 是否等待跟随者跳跃完毕 - */ - onJumpHero( - x: number, - y: number, - time: number, - waitFollower: boolean - ): Promise; - - /** - * 当设置勇士图片时触发 - * @param image 勇士图片 id - */ - onSetImage(image: ImageIds): void; - - /** - * 当设置勇士不透明度时执行 + * 当勇士的不透明度被修改时触发 * @param alpha 不透明度 */ - 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; + onSetAlpha?(alpha: number): void; } -export interface IHeroMoveController extends IHookable { - /** 勇士横坐标 */ - readonly x: number; - /** 勇士纵坐标 */ - readonly y: number; - /** 勇士朝向 */ - readonly direction: FaceDirection; - /** 勇士图片 */ - readonly image?: ImageIds; - /** 跟随者列表 */ - readonly followers: readonly IHeroFollower[]; - /** 勇士当前的不透明度 */ +export interface IHeroRendering + extends + ISaveableContent, + IHookable, + IDataCommonExtended { + /** 勇士的当前不透明度 */ 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; - - /** - * 结束勇士移动 - * @returns 当移动动画结束后兑现的 `Promise` - */ - endMove(): Promise; - - /** - * 跳跃勇士至目标点 - * @param x 目标点横坐标 - * @param y 目标点纵坐标 - * @param time 跳跃动画时长,默认 500ms - * @param waitFollower 是否等待跟随者跳跃完毕,默认不等待 - * @returns 跳跃的 `Promise`,当相关的移动动画结束后兑现 - */ - jumpHero( - x: number, - y: number, - time?: number, - waitFollower?: boolean - ): Promise; - - /** - * 设置勇士图片 - * @param image 图片 id - */ - setImage(image: ImageIds): void; - /** * 设置勇士的不透明度 * @param alpha 不透明度 */ setAlpha(alpha: number): void; +} + +//#endregion + +//#region 勇士跟随者 + +export interface IHeroFollowerSave { + /** 跟随者渲染对象保存 */ + readonly rendering: IHeroRenderingSave; + /** 跟随者位置保存 */ + readonly location: IHeroLocationSave; +} + +export interface IHeroFollower + extends ISaveableContent, IDataCommonExtended { + /** 跟随者的图块数字 */ + readonly num: number; + /** 跟随者的渲染信息对象 */ + readonly rendering: IHeroRendering; + /** 跟随者的位置对象 */ + readonly location: IHeroLocation; /** - * 添加一个跟随者 - * @param follower 跟随者的图块数字 - * @param identifier 跟随者的标识符,可以用来移除 + * 获取下一个跟随者 + * @returns null 表示当前为最后一个 */ - addFollower(follower: number, identifier: string): void; + next(): IHeroFollower | null; /** - * 移除指定的跟随者 - * @param identifier 跟随者的标识符 - * @param animate 填 `true` 的话,如果删除了中间的跟随者,后续跟随者会使用移动动画移动到下一格,否则瞬移至下一格 + * 获取上一个跟随者 + * @returns null 表示当前为第一个 */ - 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 alpha 跟随者不透明度 + * 当聚集跟随者时触发 + * @param sync 是否为同步调用(即是否通过 `gatherFollowersSync` 触发) */ - setFollowerAlpha(identifier: string, alpha: number): void; + onGatherFollowers?(sync: boolean): void; +} + +export interface IHeroFollowersController + extends IHookable, 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; + + /** + * 移除所有的跟随者 + */ + removeAllFollowers(): Promise; + + /** + * 将所有跟随者聚集到勇士位置,并调整朝向为与勇士相同 + */ + gatherFollowers(): Promise; + + /** + * 立刻将所有跟随者聚集到勇士位置,并调整朝向为与勇士相同,不会播放移动动画 + */ + gatherFollowersSync(): void; } //#endregion @@ -381,9 +369,9 @@ export interface IHeroStateSave { /** 勇士属性状态 */ readonly attribute: THero; /** 勇士当前位置 */ - readonly locator: IFacedTileLocator; + readonly locator: IHeroLocationSave; /** 勇士当前的跟随者 */ - readonly followers: readonly Readonly[]; + readonly followers: readonly IHeroFollowerSave[]; /** 勇士属性修饰器状态 */ readonly modifiers: readonly IModifierStateSave[]; } @@ -392,20 +380,18 @@ export interface IHeroState extends ISaveableContent< IHeroStateSave > { /** 勇士移动对象 */ - readonly mover: IHeroMoveController; + readonly location: IHeroLocation; /** 勇士属性对象 */ readonly attribute: IReadonlyHeroAttribute; + /** 勇士跟随者对象 */ + readonly followers: IHeroFollowersController; + /** 勇士的渲染对象,包含一些必要渲染信息,存在于数据端,并非渲染端 */ + readonly rendering: IHeroRendering; /** - * 绑定勇士移动对象 - * @param mover 勇士移动对象 + * 获取勇士当前的位置 */ - attachMover(mover: IHeroMoveController): void; - - /** - * 获取勇士移动对象 - */ - getHeroMover(): IHeroMoveController; + getLocation(): IFacedTileLocator; /** * 绑定勇士属性对象 diff --git a/packages-user/data-base/src/map/mover.ts b/packages-user/data-base/src/map/mover.ts index 2340ff2..8415a1a 100644 --- a/packages-user/data-base/src/map/mover.ts +++ b/packages-user/data-base/src/map/mover.ts @@ -72,6 +72,19 @@ export class DynamicTileMover extends ObjectMover { locator.y += y; 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); } diff --git a/packages-user/data-base/src/shared.ts b/packages-user/data-base/src/shared.ts index 0464e06..9f89134 100644 --- a/packages-user/data-base/src/shared.ts +++ b/packages-user/data-base/src/shared.ts @@ -2,3 +2,5 @@ import { FaceGroup } from '@user/data-common'; /** 动态图块所使用的默认移动组,不知道干什么的就别动 */ export const DYNAMIC_MOVER_FACE = FaceGroup.Dir8; +/** 勇士移动所使用的默认移动组,不知道干什么的就别动 */ +export const HERO_MOVER_FACE = FaceGroup.Dir8; diff --git a/packages-user/data-common/src/common/mover.ts b/packages-user/data-common/src/common/mover.ts index eeba89d..c062351 100644 --- a/packages-user/data-common/src/common/mover.ts +++ b/packages-user/data-common/src/common/mover.ts @@ -23,7 +23,11 @@ export const enum ObjectMoveStepType { /** 特殊步,如前进或后退 */ Special, /** 动画方向步 */ - AnimDir + AnimDir, + /** 传送步 */ + Teleport, + /** 跳跃步 */ + Jump } export const enum ObjectSpecialStep { @@ -103,13 +107,37 @@ export interface IObjectMoveAnimDir { 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 = | IObjectMoveStepDir | IObjectMoveStepDirFace | IObjectMoveStepSpeed | IObjectMoveStepFace | IObjectMoveStepSpecial - | IObjectMoveAnimDir; + | IObjectMoveAnimDir + | IObjectMoveTP + | IObjectMoveJump; export interface IMoverController { /** 本次移动是否已经全部完成 */ @@ -195,6 +223,22 @@ export interface IObjectMover extends IHookable< /** 当前移动速度,单位毫秒 */ 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 移动方向 @@ -396,6 +440,26 @@ export abstract class ObjectMover } } + 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 { for (let i = 0; i < count; i++) { this.pushStep({ diff --git a/packages-user/data-state/src/index.ts b/packages-user/data-state/src/index.ts index d459569..89c9dbb 100644 --- a/packages-user/data-state/src/index.ts +++ b/packages-user/data-state/src/index.ts @@ -1,8 +1,5 @@ -export function create() {} - export * from './enemy'; export * from './hero'; -export * from './legacy'; export * from './core'; export * from './ins'; diff --git a/packages-user/entry-data/src/create.ts b/packages-user/entry-data/src/create.ts index 95be547..9724796 100644 --- a/packages-user/entry-data/src/create.ts +++ b/packages-user/entry-data/src/create.ts @@ -25,5 +25,4 @@ export function create() { function createModule() { LegacyPluginData.createLegacy(); - DataState.create(); } diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index fe90781..2e013e4 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -198,6 +198,7 @@ "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.", "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." } } diff --git a/prompt.md b/prompt.md index e1f2cfa..dd26423 100644 --- a/prompt.md +++ b/prompt.md @@ -12,6 +12,12 @@ - **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。 7. **不要有任何“顺手”的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug,然后**顺手**修复、发现一处类型错误,然后**顺手**修复等,这种情况下应当遵循规则 1。 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. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。 ## 示例文档 -对于新增接口/彻底性地重构接口,按照以下格式编写,其余需求按照自己的理解去写即可。如某部分需要详细描述,可单独开设标题;若某个接口内容较多,也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。文档保证简洁,不要过于啰嗦,但必须包含所有的必要信息,控制在 100-250 行以内。 +对于新增接口/彻底性地重构接口,按照以下格式编写,其余需求按照自己的理解去写即可。如某部分需要详细描述,可单独开设标题;若某个接口内容较多,也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。文档保证简洁,不要过于啰嗦,但必须包含所有的必要信息,控制在 100-250 行,格式清晰,不要擅自修改示例文档的格式。一般情况下不需要画任何流程图,如果必须要画,使用 `mermaid` 图。不要在文档中使用引用块,因为这是我在给文档添加批注时使用的东西。 ```md # 需求综述 描述清楚需求的内容、动机与目的。 -# 接口设计与预期 +# 接口设计分析 这是相当重要的一章。需要按接口逐一列出其方法与成员,分析每个成员和方法的预期使用频率(高 / 中 / 低)并说明判断依据;对于中频和高频成员,还需列出至少一个典型使用场景。必须认真分析频率,想明白**用户**调用的频率,而不是**引擎**调用的频率,比如战斗函数在引擎中可能是中频,但是对用户来说,用户一般会使用系统默认行为来战斗,而**不会**自己调用,因此属于低频。 @@ -54,11 +60,17 @@ ## IObjectMover +### 接口综述 + +这里讲解接口的设计,注意不要直接写成 ts 代码,也不要简单地一个个列出,而是需要分析需求,然后经过推理自然地得出接口设计。 + +### 接口分析 + - `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。 - `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。 - `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。 -## 预期体量 +### 预期体量 本节应当写出预期的代码体量,并分析原因。示例如下: @@ -67,9 +79,9 @@ - `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。 - `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。 -## 可能风险 +### 可能风险 -在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。 +在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,例如导致大面积的类型污染、或部分其他接口会出现类型错误等,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。一些细小的内容不要写到这。如果没有任何风险,不要写无风险,直接跳过此章节。 # 实现思路 diff --git a/task.md b/task.md index 4d829e4..8e8fe35 100644 --- a/task.md +++ b/task.md @@ -23,3 +23,7 @@ - 寻路系统 - `core.status.hero` - `core.status.hero.flags` + +# 其他备忘录 + +- 大地图远景?