mirror of
https://github.com/motajs/template.git
synced 2026-07-17 01:11:09 +08:00
Compare commits
2 Commits
02bf39a62d
...
931f610d37
| Author | SHA1 | Date | |
|---|---|---|---|
| 931f610d37 | |||
| fadcc98864 |
18
dev.md
18
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` 为游戏的入口代码。
|
||||
@ -26,11 +26,14 @@
|
||||
## 构建说明
|
||||
|
||||
| 命令 | 说明 |
|
||||
| --------------------- | ------------------------------------------------------- |
|
||||
| --------------------- | ------------------------------------------------------------ |
|
||||
| `pnpm build:packages` | 以库模式构建 `packages` 下的所有内容 |
|
||||
| `pnpm build:game` | 构建为可直接部署的游戏包 |
|
||||
| `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` | 对仓库执行循环引用检查 |
|
||||
|
||||
## 术语统一
|
||||
@ -62,7 +65,7 @@
|
||||
| 代码文件名 | 小驼峰 |
|
||||
| Markdown 文档文件名 | 连字符命名法 |
|
||||
|
||||
不使用下划线命名法。
|
||||
不使用下划线命名法,私有属性或方法不要以下划线开头。
|
||||
|
||||
### 注释规范
|
||||
|
||||
@ -85,9 +88,10 @@
|
||||
- 未使用的变量或方法以下划线开头命名。
|
||||
- 合理使用 `readonly`、`protected`、`private` 关键字。
|
||||
- 可选参数过多时(大于两个),考虑改用对象参数。
|
||||
- 尽量避免 `as` 类型断言,除非必要。
|
||||
- 尽量避免 `as` 类型断言,除非必要。绝对不允许出现连续 `as`,例如 `as unknown as xxx`。
|
||||
- 函数类型单独开一个 `type` 类型别名,除非函数类型本身较短(小于 20 字符,且不会因为此函数类型导致换行)。
|
||||
- 当任何时候必须出现对象类型的时候,单独声明一个 `interface`,不得出现直接的对象类型。
|
||||
- 对于某个类中的对象成员,必须使用接口作为类型,不得使用类作为类型,除非某些极特殊情况。
|
||||
|
||||
### 其他要求
|
||||
|
||||
@ -99,7 +103,7 @@
|
||||
- 只进行必要的非空判断,非必要时直接使用非空断言 `!`。
|
||||
- 除非参数要求传入函数等情况,不建议在函数内定义局部函数。
|
||||
- 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。
|
||||
- 一般不建议写 `getter` 和 `setter`,但某些场景下还是可以使用的。
|
||||
- 一般不建议写 `getter` 和 `setter`,但在极少数场景下还是可以使用的。
|
||||
|
||||
## 双端分离
|
||||
|
||||
|
||||
140
docs/dev/hero/hero-follower-interfaces.md
Normal file
140
docs/dev/hero/hero-follower-interfaces.md
Normal 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`:预期频率**低频**。
|
||||
|
||||
### 预期体量
|
||||
|
||||
预期代码体量合计约 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<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
121
docs/dev/hero/hero-mover.md
Normal 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'`:导出新模块。
|
||||
@ -8,8 +8,10 @@
|
||||
"test": "vitest",
|
||||
"preview": "vite preview",
|
||||
"declare": "tsx script/declare.ts",
|
||||
"type": "vue-tsc --noEmit",
|
||||
"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:game": "tsx script/declare.ts && vue-tsc --noEmit && tsx script/build-game.ts",
|
||||
"build:lib": "vue-tsc --noEmit && tsx script/build-lib.ts",
|
||||
@ -17,6 +19,7 @@
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"pack:template": "tsx script/pack-template.ts",
|
||||
"check:type": "vue-tsc --noEmit",
|
||||
"check:circular": "madge --circular src/main.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
77
packages-user/data-base/src/hero/follower.ts
Normal file
77
packages-user/data-base/src/hero/follower.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
146
packages-user/data-base/src/hero/followersController.ts
Normal file
146
packages-user/data-base/src/hero/followersController.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
48
packages-user/data-base/src/hero/location.ts
Normal file
48
packages-user/data-base/src/hero/location.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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<IHeroMoveControllerHooks>
|
||||
implements IHeroMoveController
|
||||
export class HeroMover<T extends IHeroLocation>
|
||||
extends ObjectMover<T>
|
||||
implements IHeroMover<T>
|
||||
{
|
||||
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<IHeroMoveControllerHooks>
|
||||
): IHookController<IHeroMoveControllerHooks> {
|
||||
return new HookController(this, hook);
|
||||
constructor(tile: T, faceHandler: IFaceHandler<FaceDirection>) {
|
||||
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<IHeroMoverConfig>): 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<IHeroMoverConfig> {
|
||||
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<IMoverController>
|
||||
): Promise<void> {
|
||||
// TODO: 通知渲染端开始移动,同步平滑视角
|
||||
}
|
||||
|
||||
async move(dir: FaceDirection, time: number = 100): Promise<void> {
|
||||
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<IMoverController>
|
||||
): Promise<void> {
|
||||
// TODO: 通知渲染端移动结束
|
||||
// TODO: 清除自动寻路状态
|
||||
}
|
||||
|
||||
async endMove(): Promise<void> {
|
||||
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<IMoverController>
|
||||
): Promise<number> {
|
||||
const type = step.type;
|
||||
|
||||
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,
|
||||
y: number,
|
||||
time: number = 500,
|
||||
waitFollower: boolean = false
|
||||
): Promise<void> {
|
||||
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<IMoverController>
|
||||
): HeroMoveCode {
|
||||
// TODO: 需要地形检测接口(替代 core.noPass / core.canMoveHero)
|
||||
return HeroMoveCode.Step;
|
||||
}
|
||||
}
|
||||
|
||||
41
packages-user/data-base/src/hero/rendering.ts
Normal file
41
packages-user/data-base/src/hero/rendering.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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<THero> implements IHeroState<THero> {
|
||||
/** 修饰器工厂函数注册表 */
|
||||
@ -32,6 +32,10 @@ export class HeroState<THero> implements IHeroState<THero> {
|
||||
return this.mover;
|
||||
}
|
||||
|
||||
getLocation(): IFacedTileLocator {
|
||||
return this.mover;
|
||||
}
|
||||
|
||||
getModifiableAttribute(): IHeroAttribute<THero> {
|
||||
return this.attribute;
|
||||
}
|
||||
|
||||
@ -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<THero> extends IReadonlyHeroAttribute<THero> {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 勇士位置
|
||||
|
||||
export interface IHeroLocationSave extends Readonly<IFacedTileLocator> {}
|
||||
|
||||
export interface IHeroLocation
|
||||
extends
|
||||
ISaveableContent<IHeroLocationSave>,
|
||||
IObjectMovable,
|
||||
IDataCommonExtended {
|
||||
/** 勇士的移动对象 */
|
||||
readonly mover: IObjectMover<this>;
|
||||
}
|
||||
|
||||
//#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<T extends IHeroLocation> extends IObjectMover<T> {
|
||||
/**
|
||||
* 当设置勇士的坐标时触发
|
||||
* @param x 勇士横坐标
|
||||
* @param y 勇士纵坐标
|
||||
* 配置本次移动的行为模式
|
||||
* @param config 配置对象,未传入的字段保持当前值
|
||||
*/
|
||||
onSetPosition(x: number, y: number): void;
|
||||
config(config: Readonly<IHeroMoverConfig>): this;
|
||||
|
||||
/**
|
||||
* 当设置勇士朝向时触发
|
||||
* @param direction 勇士朝向
|
||||
* 获取当前移动配置的只读快照
|
||||
*/
|
||||
onTurnHero(direction: FaceDirection): void;
|
||||
getConfig(): Readonly<IHeroMoverConfig>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当勇士开始移动时触发
|
||||
*/
|
||||
onStartMove(): void;
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* 当移动勇士时触发
|
||||
* @param direction 移动方向
|
||||
* @param time 移动动画时长
|
||||
*/
|
||||
onMoveHero(direction: FaceDirection, time: number): Promise<void>;
|
||||
//#region 勇士渲染
|
||||
|
||||
/**
|
||||
* 当停止移动时触发
|
||||
*/
|
||||
onEndMove(): Promise<void>;
|
||||
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<void>;
|
||||
|
||||
/**
|
||||
* 当设置勇士图片时触发
|
||||
* @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<IHeroMoveControllerHooks> {
|
||||
/** 勇士横坐标 */
|
||||
readonly x: number;
|
||||
/** 勇士纵坐标 */
|
||||
readonly y: number;
|
||||
/** 勇士朝向 */
|
||||
readonly direction: FaceDirection;
|
||||
/** 勇士图片 */
|
||||
readonly image?: ImageIds;
|
||||
/** 跟随者列表 */
|
||||
readonly followers: readonly IHeroFollower[];
|
||||
/** 勇士当前的不透明度 */
|
||||
export interface IHeroRendering
|
||||
extends
|
||||
ISaveableContent<IHeroRenderingSave>,
|
||||
IHookable<IHeroRenderingHooks>,
|
||||
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<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 不透明度
|
||||
*/
|
||||
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 跟随者的图块数字
|
||||
* @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<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
|
||||
@ -381,9 +369,9 @@ export interface IHeroStateSave<THero> {
|
||||
/** 勇士属性状态 */
|
||||
readonly attribute: THero;
|
||||
/** 勇士当前位置 */
|
||||
readonly locator: IFacedTileLocator;
|
||||
readonly locator: IHeroLocationSave;
|
||||
/** 勇士当前的跟随者 */
|
||||
readonly followers: readonly Readonly<IHeroFollower>[];
|
||||
readonly followers: readonly IHeroFollowerSave[];
|
||||
/** 勇士属性修饰器状态 */
|
||||
readonly modifiers: readonly IModifierStateSave[];
|
||||
}
|
||||
@ -392,20 +380,18 @@ export interface IHeroState<THero> extends ISaveableContent<
|
||||
IHeroStateSave<THero>
|
||||
> {
|
||||
/** 勇士移动对象 */
|
||||
readonly mover: IHeroMoveController;
|
||||
readonly location: IHeroLocation;
|
||||
/** 勇士属性对象 */
|
||||
readonly attribute: IReadonlyHeroAttribute<THero>;
|
||||
/** 勇士跟随者对象 */
|
||||
readonly followers: IHeroFollowersController;
|
||||
/** 勇士的渲染对象,包含一些必要渲染信息,存在于数据端,并非渲染端 */
|
||||
readonly rendering: IHeroRendering;
|
||||
|
||||
/**
|
||||
* 绑定勇士移动对象
|
||||
* @param mover 勇士移动对象
|
||||
* 获取勇士当前的位置
|
||||
*/
|
||||
attachMover(mover: IHeroMoveController): void;
|
||||
|
||||
/**
|
||||
* 获取勇士移动对象
|
||||
*/
|
||||
getHeroMover(): IHeroMoveController;
|
||||
getLocation(): IFacedTileLocator;
|
||||
|
||||
/**
|
||||
* 绑定勇士属性对象
|
||||
|
||||
@ -72,6 +72,19 @@ export class DynamicTileMover extends ObjectMover<IDynamicTile> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -2,3 +2,5 @@ import { FaceGroup } from '@user/data-common';
|
||||
|
||||
/** 动态图块所使用的默认移动组,不知道干什么的就别动 */
|
||||
export const DYNAMIC_MOVER_FACE = FaceGroup.Dir8;
|
||||
/** 勇士移动所使用的默认移动组,不知道干什么的就别动 */
|
||||
export const HERO_MOVER_FACE = FaceGroup.Dir8;
|
||||
|
||||
@ -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<T extends IObjectMovable> 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<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 {
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.pushStep({
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
export function create() {}
|
||||
|
||||
export * from './enemy';
|
||||
export * from './hero';
|
||||
export * from './legacy';
|
||||
|
||||
export * from './core';
|
||||
export * from './ins';
|
||||
|
||||
@ -25,5 +25,4 @@ export function create() {
|
||||
|
||||
function createModule() {
|
||||
LegacyPluginData.createLegacy();
|
||||
DataState.create();
|
||||
}
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
24
prompt.md
24
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 行。
|
||||
|
||||
## 可能风险
|
||||
### 可能风险
|
||||
|
||||
在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。
|
||||
在任何时候,需要修改已有接口时,必须在文档中写明修改这一接口的风险。这里的已有接口指的是本次设计新增接口之外的接口,即所有已存在于代码中的接口。本章节**只写**关于修改已有接口可能导致的风险,例如导致大面积的类型污染、或部分其他接口会出现类型错误等,**不写任何关于实现的风险**,关于实现的风险应该写到最后的问题节,而不是这里。一些细小的内容不要写到这。如果没有任何风险,不要写无风险,直接跳过此章节。
|
||||
|
||||
# 实现思路
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user