mirror of
https://github.com/motajs/template.git
synced 2026-06-14 18:51:12 +08:00
Merge 501a598de0 into 1a569804d8
This commit is contained in:
commit
0cc525c7fb
153
dev.md
153
dev.md
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
`public`: mota-js 样板所在目录。
|
- `public`: mota-js 样板所在目录
|
||||||
`packages`: 核心引擎代码 monorepo。
|
- `packages`: 核心引擎代码 monorepo
|
||||||
`packages-user`: 用户代码 monorepo。
|
- `packages-user`: 用户代码 monorepo
|
||||||
`src`: 游戏入口代码。
|
- `src`: 游戏入口代码
|
||||||
|
|
||||||
`packages` `packages-user` 可以单独打包为库模式,`src` 单向引用 `packages-user`,`packages-user` 单向引用 `packages`,`src` 为游戏的入口代码。
|
依赖关系为单向:`src` → `packages-user` → `packages`。`packages` 与 `packages-user` 均可独立打包为库模式,`src` 为游戏的入口代码。
|
||||||
|
|
||||||
## 开发环境
|
## 开发环境
|
||||||
|
|
||||||
@ -15,50 +15,121 @@
|
|||||||
- `pnpm >= 10.0.0`
|
- `pnpm >= 10.0.0`
|
||||||
- 任意支持 `ESNext` 特性的浏览器
|
- 任意支持 `ESNext` 特性的浏览器
|
||||||
|
|
||||||
**建议使用 `vscode`,搭配 `prettier` `eslint` 插件**
|
**建议使用 `vscode`,搭配 `prettier` `eslint` 插件。**
|
||||||
|
|
||||||
## 开发说明
|
## 开发说明
|
||||||
|
|
||||||
1. 将项目拉取到本地。
|
1. 将项目拉取到本地。
|
||||||
2. 运行 `pnpm i` 安装所有依赖,如有要求运行 `pnpm approve-builds`,请允许全部。
|
2. 运行 `pnpm i` 安装所有依赖(如提示运行 `pnpm approve-builds`,请允许全部)。
|
||||||
3. 运行 `pnpm dev` 进入开发环境。
|
3. 运行 `pnpm dev` 进入开发环境。
|
||||||
|
|
||||||
## 构建说明
|
## 构建说明
|
||||||
|
|
||||||
- `pnpm build:packages`: 构建所有 `packages` 文件夹下的内容,使用库模式。
|
| 命令 | 说明 |
|
||||||
- `pnpm build:game`: 构建为可以直接部署的构建包。
|
| --------------------- | ------------------------------------------------------- |
|
||||||
- `pnpm build:lib`: 构建所有 `packages` `packages-user` 文件夹下的内容,使用库模式。
|
| `pnpm build:packages` | 以库模式构建 `packages` 下的所有内容 |
|
||||||
|
| `pnpm build:game` | 构建为可直接部署的游戏包 |
|
||||||
|
| `pnpm build:lib` | 以库模式构建 `packages` 与 `packages-user` 下的所有内容 |
|
||||||
|
| `pnpm type` | 对仓库执行类型检查 |
|
||||||
|
| `pnpm check:circular` | 对仓库执行循环引用检查 |
|
||||||
|
|
||||||
|
## 术语统一
|
||||||
|
|
||||||
|
- 方法:一般指挂载到接口 `interface` 或类 `class` 上的函数。
|
||||||
|
- 函数:一般指在文件顶层定义的函数,有时也会指方法,需要根据语境判断。
|
||||||
|
- 接口:有时指 `interface`,有时也会指方法、成员等内容,需要根据语境判断。
|
||||||
|
- 成员/属性:一般指接口 `interface`、类 `class`、对象 `object` 上的字段。
|
||||||
|
|
||||||
## 开发原则
|
## 开发原则
|
||||||
|
|
||||||
- 模块无副作用原则:
|
### 模块原则
|
||||||
- 所有模块不包含副作用内容,全部由函数、类、常量的声明组成,不出现导出的变量声明、代码执行内容,允许但不建议编写类的静态块。
|
|
||||||
- 如果需要模块初始化,编写一个 `createXxx` 函数,然后在 `index.ts` 中整合,再逐级向上传递,直至遇到包含 `create` 函数的 `index.ts`,所有初始化将会统一在顶层模块中执行。
|
- **无副作用**:所有模块只包含函数、类、常量的声明,不允许出现导出的变量声明或顶层代码执行,允许但不建议编写类的静态块。
|
||||||
- 命名规则:
|
- **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。
|
||||||
- 变量、成员、一般常量、方法、函数使用小驼峰。
|
- **不转发导出**:不允许一个文件导出不属于当前 monorepo 或当前文件夹的内容。
|
||||||
- 类、接口、类型别名、命名空间、泛型、枚举、组件使用大驼峰。
|
- **无循环引用**:不允许出现循环引用。若遇到不得不循环引用的情况,应首先反思接口设计是否存在问题。
|
||||||
- 不变常量使用全大写命名法,单词之间使用下划线连接。
|
|
||||||
- 专有名词缩写如 `HTTP`, `URI` 全部大写。
|
### 命名规则
|
||||||
- 会被 `implements` 的接口使用大写 `I` 开头。
|
|
||||||
- `id`, `class` 等 `HTML/CSS` 内容使用连字符命名法。
|
| 命名对象 | 规范 |
|
||||||
- 不使用下划线命名法。
|
| ---------------------------------------------- | ------------------------------------------ |
|
||||||
- 注释:
|
| 变量、成员、一般常量、方法、函数 | 小驼峰 |
|
||||||
- 常用属性成员、方法、接口、类型必须添加 `jsDoc` 注释。
|
| 类、接口、类型别名、命名空间、泛型、枚举、组件 | 大驼峰 |
|
||||||
- 长文件可使用 `#region` 分段,可以写上 `#endretion` 允许折叠。
|
| 不变常量 | 全大写,单词间下划线分隔(如 `MAX_COUNT`) |
|
||||||
- TODO 使用 `// TODO:` 或 `// todo:` 格式。
|
| 专有名词缩写(如 `HTTP`、`URI`) | 全大写 |
|
||||||
- 单行注释的双斜杠与注释内容之间添加一个空格,多行注释只允许出现 `jsDoc` 注释,如果需要多行非 `jsDoc` 注释,使用多个单行注释。
|
| 需被 `implements` 的接口 | 大写 `I` 开头 |
|
||||||
- 注释进行合理换行,考虑到中文字符较宽,建议 40-60 个字符进行换行。不允许在句中换行,必须在标点符号后换行。
|
| HTML/CSS 中的 `id`、`class` 等 | 连字符命名法 |
|
||||||
- 单行注释结尾不添加句号,对于多行长注释,可以在结尾添加句号。
|
| 代码文件名 | 小驼峰 |
|
||||||
- 类型:
|
| Markdown 文档文件名 | 连字符命名法 |
|
||||||
- 不允许出现非必要的 `any` 类型。
|
|
||||||
- 所有类的成员必须显式声明类型。
|
不使用下划线命名法。
|
||||||
- 如果有无法避免出现类型错误的地方,使用 `// @ts-expect-error` 标记,并填写原因。
|
|
||||||
- 没用到的变量、方法使用下划线开头。
|
### 注释规范
|
||||||
- 合理运用 `readonly` `protected` `private` 关键字。
|
|
||||||
- 函数不建议使用过多可选参数,如果可选参数过多,可以考虑换用对象。
|
- 公共方法、接口必须在**源头处**(多数情况下为 `interface`)添加 `jsDoc` 注释;其他常用成员、方法、类型也必须添加注释(含义极为明确或极少使用的可例外,但建议全部添加)。
|
||||||
- 尽量少地使用 `as` 关键字进行类型断言,一般情况下不建议进行任何 `as` 类型断言,除非必要。
|
- 继承或 `implements` 而来的 API(方法、成员等),若注释说明无需变更,则**不应重复添加** `jsDoc` 注释。
|
||||||
- 其他要求:
|
- 不对构造器添加注释。若构造器使用了属性声明语法(`constructor(public prop: T)`)且成员需要说明,可仅对该成员添加参数注释,不写构造器描述。**在这种情况下**建议避免在构造器中使用属性声明语法,将成员单独声明并在构造器中赋值。此条建议**并非**要求不使用构造器的属性声明语法,而是仅在这一情况下不建议使用,常规情况下推荐使用此语法来缩短代码长度并提高可读性。
|
||||||
- 严格遵循 `eslint` 配置,不允许出现 `eslint` 报错。
|
- 长文件可使用 `#region` / `#endregion` 分段以支持折叠。
|
||||||
- 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()`,`this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符。
|
- TODO 使用 `// TODO:` 或 `// todo:` 格式。
|
||||||
- 只进行必要的非空判断,不必要的非空判断直接使用非空断言 `!` 实现。
|
- 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。
|
||||||
- 除非参数要求传入函数等情况,不建议在函数内写任何局部函数。
|
- 注释合理换行:考虑中文字符较宽,建议每 40–60 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不允许在句中换行;参数注释换行后保持对齐。
|
||||||
|
- 单行注释结尾不加句号;较长的多行注释结尾可加句号。
|
||||||
|
- 一般不建议给接口(`inteface` 本身)、类型别名或类本身写注释(不好看),特殊情况除外。
|
||||||
|
- 对于 jsDoc 注释,方法注释必须使用换行风格;对于成员 jsDoc 注释,除非注释较长需要换行,否则使用不换行的风格。
|
||||||
|
|
||||||
|
### 类型规范
|
||||||
|
|
||||||
|
- 不允许出现非必要的 `any` 类型。
|
||||||
|
- 所有类的成员必须显式声明类型。
|
||||||
|
- 无法避免类型错误时,使用 `// @ts-expect-error` 标记并说明原因。
|
||||||
|
- 未使用的变量或方法以下划线开头命名。
|
||||||
|
- 合理使用 `readonly`、`protected`、`private` 关键字。
|
||||||
|
- 可选参数过多时(大于两个),考虑改用对象参数。
|
||||||
|
- 尽量避免 `as` 类型断言,除非必要。
|
||||||
|
- 函数类型单独开一个 `type` 类型别名,除非函数类型本身较短(小于 20 字符,且不会因为此函数类型导致换行)
|
||||||
|
|
||||||
|
### 其他要求
|
||||||
|
|
||||||
|
- 不使用字符串作为键或特殊标识符(如枚举值、事件名、状态名等),应使用枚举代替。仅当明确表示字符串本身(如字符串类型的 id 别名、文件路径等)时方可使用字符串字面量。
|
||||||
|
- 严格遵循 `eslint` 配置,不允许出现 eslint 报错。
|
||||||
|
- 尽量不使用 `?.` 运算符,仅推荐在以下两种场景中使用:
|
||||||
|
- 副作用函数调用,如 `this.obj?.func()` 或 `this.obj.func?.()`
|
||||||
|
- 对象 Required 化,如 `{ value: obj?.value ?? 0 }`
|
||||||
|
- 只进行必要的非空判断,非必要时直接使用非空断言 `!`。
|
||||||
|
- 除非参数要求传入函数等情况,不建议在函数内定义局部函数。
|
||||||
|
- 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。
|
||||||
|
- 任何时候都不应该写 `getter` 和 `setter`。
|
||||||
|
|
||||||
|
## 双端分离
|
||||||
|
|
||||||
|
样板将渲染端与数据端彻底分离:
|
||||||
|
|
||||||
|
- **数据端**:可在 `node` 环境中单独运行,可直接用于录像验证,不负责任何渲染逻辑。
|
||||||
|
- **渲染端**:仅负责向数据端发送消息,不负责任何逻辑运算。
|
||||||
|
|
||||||
|
数据端允许调用渲染端代码,但必须使用全局接口 `Mota.r(() => {})` 包裹。除非必要,否则不建议在数据端调用渲染端代码。
|
||||||
|
|
||||||
|
### 渲染端
|
||||||
|
|
||||||
|
渲染端目前已基本制作完成,分为两层:
|
||||||
|
|
||||||
|
| 包 | 层级 | 说明 |
|
||||||
|
| ---------------------- | ------ | ------------------------------------ |
|
||||||
|
| `@user/client-base` | 系统层 | 负责渲染端核心系统 |
|
||||||
|
| `@user/client-modules` | 实现层 | 依赖系统层实现客户端的渲染与用户交互 |
|
||||||
|
|
||||||
|
### 数据端
|
||||||
|
|
||||||
|
数据端目前正在从旧引擎进行彻底性重构,分为三层:
|
||||||
|
|
||||||
|
**Layer 0 — 公共层**:包含公共接口、工具函数等内容,不依赖任何外部游戏逻辑,可被任意高层直接引用。内容较少,与 Layer 1 共同放在 `@user/data-base` 中,不单独开包。
|
||||||
|
|
||||||
|
**Layer 1 — 数据层**:包含所有会影响游戏存档与流程的数据内容,如地图、怪物、玩家属性等。本层通过统一接口 `IStateBase` 对外暴露数据访问能力,各类数据模块均以此接口为核心组织。
|
||||||
|
|
||||||
|
**Layer 2 — 执行层**:直接引用 Layer 1,负责产生影响游戏进程的动作,如玩家控制、战斗计算等。本层内容不会进入存档,仅通过修改 Layer 1 的数据来影响游戏状态,并通过统一接口 `ICoreState` 对外暴露执行能力。
|
||||||
|
|
||||||
|
| 包 | 层级 | 说明 |
|
||||||
|
| ------------------- | ----------------- | ------------------------------------------------------------------------------- |
|
||||||
|
| `@user/data-base` | Layer 0 / Layer 1 | 公共层与数据层,定义 `IStateBase` 及各类游戏数据(地图、怪物、玩家属性等) |
|
||||||
|
| `@user/data-state` | — | 数据端的顶层模块,指导 Layer 2 的执行行为,不直接参与执行 |
|
||||||
|
| `@user/data-system` | Layer 2 | 执行层,定义 `ICoreState`,依赖数据层实现玩家控制、战斗计算等影响游戏进程的动作 |
|
||||||
|
|||||||
85
docs/dev/common/face-manager.md
Normal file
85
docs/dev/common/face-manager.md
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
动态图块移动功能完成后,发现 `FaceDirection` 朝向相关操作(位移、反方向、旋转方向、降级)分散在独立工具函数中(`getFaceMovement`、`nextFaceDirection`、`degradeFace` 等),缺乏统一抽象,导致调用点分散、扩展性不足(例如:四方向与八方向切换时需要手动处理,不同移动场景难以共享同一套逻辑)。
|
||||||
|
|
||||||
|
目标是设计一套统一的朝向管理接口:`IFaceHandler<T>` 代表一组朝向并提供相应操作,`IFaceManager` 作为注册中心管理多个 handler,同时内置两个 handler 分别对应四方向和八方向。新接口位于 `@user/data-base/src/common/`,与 `FaceDirection` 同包,同时取代现有的 `DirectionMapper`。
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 设计 IFaceHandler 接口
|
||||||
|
|
||||||
|
`IFaceHandler<T extends number>` 代表**一组朝向**,泛型 `T` 表示本组的朝向类型(通常为 `FaceDirection`,也可以是自定义枚举以支持拓展)。内部隐含一个该组支持的方向集合,通过 `directions` 成员暴露。对于不在集合内的输入方向,通过 `degrade` 方法将其映射到集合内最合适的方向,其余操作方法在调用时**均先执行 degrade**。
|
||||||
|
|
||||||
|
`mapDirection` 同时取代 `DirectionMapper.map()` 的功能,不再需要单独维护 `DirectionMapper`。
|
||||||
|
|
||||||
|
主要成员与方法:
|
||||||
|
|
||||||
|
- `degrade(dir: number): T`:将任意朝向值(含其他枚举类型或 `FaceDirection`)降级为本组支持的方向。对于无法合理降级的方向(包括 `Unknown`),返回 `FaceDirection.Unknown`(数值 `0`,兼容所有 `T`)。
|
||||||
|
|
||||||
|
- `movement(dir: number): IFaceDescriptor`:获取指定方向的坐标偏移量,输入先经过 `degrade`,`Unknown` 返回 `{ x: 0, y: 0 }`。
|
||||||
|
|
||||||
|
- `move(dir: number, count: number): IFaceDescriptor`:获取指定方向走 `count` 步的坐标偏移量,等价于 `movement * count`。`count` 允许为负数,表示反向位移。输入先经过 `degrade`。
|
||||||
|
|
||||||
|
- `opposite(dir: number): T`:获取本组内的反方向,输入先经过 `degrade`,`Unknown` 返回 `Unknown`。
|
||||||
|
|
||||||
|
- `next(dir: number, anticlockwise?: boolean): T`:在本组方向集合内,顺时针(默认)或逆时针旋转一步,输入先经过 `degrade`,`Unknown` 返回 `Unknown`。
|
||||||
|
|
||||||
|
- `mapDirection(): Iterable<T>`:迭代本组支持的所有朝向,包含 `Unknown`(其与其他方向一视同仁,不作特例处理)。
|
||||||
|
|
||||||
|
- `mapMovement(): Iterable<[T, IFaceDescriptor]>`:迭代本组所有朝向及其对应的坐标描述器,包含 `Unknown`(对应 `{ x: 0, y: 0 }`)。
|
||||||
|
|
||||||
|
## 2. 设计 IFaceManager 接口
|
||||||
|
|
||||||
|
`IFaceManager` 是 handler 的注册中心,同时支持数字 key 与字符串 id 两种注册与查找方式。数字 key 适合内置组的高频调用,字符串 id 适合使用频率较低的自定义场景:
|
||||||
|
|
||||||
|
- `register(group: number, handler: IFaceHandler<number>): void`:以数字 key 注册一个 handler。
|
||||||
|
- `registerById(id: string, handler: IFaceHandler<number>): void`:以字符串 id 注册一个 handler。
|
||||||
|
- `get<T extends number>(group: number): IFaceHandler<T> | null`:按数字 key 查找 handler,未找到返回 `null`。
|
||||||
|
- `getById<T extends number>(id: string): IFaceHandler<T> | null`:按字符串 id 查找 handler,未找到返回 `null`。
|
||||||
|
|
||||||
|
内置的数字 key 组用新增的 `InternalFaceGroup` 枚举标识(与现有的 `InternalDirectionGroup` 风格一致)。
|
||||||
|
|
||||||
|
## 3. 内置 Handler 实现
|
||||||
|
|
||||||
|
### Dir8FaceHandler(八方向)
|
||||||
|
|
||||||
|
- `directions`:包含全部八个有效方向与 `Unknown`,共九个成员。
|
||||||
|
- `degrade`:直接返回输入(转为 `T`),无需降级。
|
||||||
|
- `next`:`Unknown` 返回 `Unknown`;按 45° 步进,顺时针顺序:Up → RightUp → Right → RightDown → Down → LeftDown → Left → LeftUp → Up。
|
||||||
|
- `opposite`:`Unknown` 返回 `Unknown`;Up↔Down,Left↔Right,LeftUp↔RightDown,RightUp↔LeftDown。
|
||||||
|
- `movement`:与现有 `getFaceMovement` 一致。
|
||||||
|
|
||||||
|
### Dir4FaceHandler(四方向)
|
||||||
|
|
||||||
|
- `directions`:包含 Up、Down、Left、Right 四个方向与 `Unknown`,共五个成员。
|
||||||
|
- `degrade`:四方向不变;斜向降级为水平分量(LeftUp/LeftDown → Left,RightUp/RightDown → Right);`Unknown` → `Unknown`。与现有 `degradeFace` 行为一致。
|
||||||
|
- `next`:先 degrade,`Unknown` 返回 `Unknown`;再按 90° 步进,顺时针:Up → Right → Down → Left → Up。
|
||||||
|
- `opposite`:先 degrade,`Unknown` 返回 `Unknown`;Up↔Down,Left↔Right。
|
||||||
|
- `movement`:先 degrade,再返回偏移量。
|
||||||
|
|
||||||
|
## 4. 实现 FaceManager 类
|
||||||
|
|
||||||
|
实现 `IFaceManager`,不导出全局单例,应将实例挂载到游戏实例下。两个内置 handler(`Dir8FaceHandler` 与 `Dir4FaceHandler`)不在构造时注册,而在游戏实例初始化阶段注册,key 分别为 `InternalFaceGroup.Dir8` 和 `InternalFaceGroup.Dir4`。
|
||||||
|
|
||||||
|
## 5. 现有代码处理
|
||||||
|
|
||||||
|
- 现有 `getFaceMovement`、`nextFaceDirection`、`degradeFace`、`fromDirectionString` 等工具函数**暂时保留**,不做删改;新代码直接使用新接口,旧代码的迁移视后续情况另行处理。
|
||||||
|
- `@motajs/common` 中的 `DirectionMapper` 及 `IDirectionMapper` 接口将被废弃,其调用方(如 `range.ts`)的迁移视后续情况另行处理。
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/src/common/faceManager.ts`(新增文件)
|
||||||
|
|
||||||
|
- [ ] 新增 `InternalFaceGroup` 枚举:包含 `Dir4` 与 `Dir8` 两个成员,作为 `IFaceManager` 的内置数字 key
|
||||||
|
- [ ] 新增 `IFaceDescriptor` 接口:描述一个方向的坐标增量,包含 `x` 与 `y` 两个只读成员
|
||||||
|
- [ ] 新增 `IFaceHandler<T extends number>` 接口:包含 `degrade`、`movement`、`move`、`opposite`、`next`、`mapDirection`、`mapMovement` 七个成员
|
||||||
|
- [ ] 新增 `IFaceManager` 接口:包含 `register`、`registerById`、`get`、`getById` 四个方法
|
||||||
|
- [ ] 实现 `Dir8FaceHandler` 类(`implements IFaceHandler<FaceDirection>`)
|
||||||
|
- [ ] 实现 `Dir4FaceHandler` 类(`implements IFaceHandler<FaceDirection>`)
|
||||||
|
- [ ] 实现 `FaceManager` 类(`implements IFaceManager`)
|
||||||
|
|
||||||
|
### `@user/data-base/src/common/index.ts`
|
||||||
|
|
||||||
|
- [ ] 导出新增的枚举、接口与类
|
||||||
194
docs/dev/enemy/enemy-manager-save.md
Normal file
194
docs/dev/enemy/enemy-manager-save.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
对怪物管理器 `IEnemyManager` 进行存档适配,使其能够参与游戏存档系统。
|
||||||
|
由于大多数情况下怪物模板不会被修改,不需要全量存储,
|
||||||
|
只需对比"参考状态"(游戏加载完成时的初始模板),仅保存发生了变化的模板。
|
||||||
|
|
||||||
|
为此需要:
|
||||||
|
|
||||||
|
- `IEnemy` 和 `ISpecial` 继承 `ISaveableContent` 以支持自身序列化;
|
||||||
|
- 给 `ISpecial` 添加 `deepEqualsTo` 接口用于特殊属性间的深度比较;
|
||||||
|
- `IEnemyManager` 继承 `ISaveableContent`,新增 `compareWith`、`modifyPrefabAttribute`、
|
||||||
|
`attachEnemyComparer`、`getEnemyComparer` 接口;
|
||||||
|
- `IEnemyManager` 内部维护 dirty 集合,以首次 `compareWith` 传入的参考为唯一基准;
|
||||||
|
- `getPrefab` / `getPrefabById` 返回值收窄为 `IReadonlyEnemy<TAttr>`,
|
||||||
|
统一由 `modifyPrefabAttribute` 承担模板修改职责。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 新增存档状态类型
|
||||||
|
|
||||||
|
在 `types.ts` 中新增如下类型,用于序列化怪物与管理器的状态:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 单个 IEnemy 的存档状态 */
|
||||||
|
interface IEnemySaveState<TAttr> {
|
||||||
|
readonly attrs: TAttr;
|
||||||
|
// 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果
|
||||||
|
readonly specials: ReadonlyMap<number, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */
|
||||||
|
interface IEnemyManagerSaveState<TAttr> {
|
||||||
|
// code -> 变更后的 IEnemySaveState
|
||||||
|
readonly modified: ReadonlyMap<number, IEnemySaveState<TAttr>>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 新增 `IEnemyComparer<TAttr>` 接口
|
||||||
|
|
||||||
|
由于管理器外部没有比较怪物属性的需求,将比较逻辑封装为独立的比较器,
|
||||||
|
附着在 `EnemyManager` 上。比较器接口如下:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IEnemyComparer<TAttr> {
|
||||||
|
compare(
|
||||||
|
enemyA: IReadonlyEnemy<TAttr>,
|
||||||
|
enemyB: IReadonlyEnemy<TAttr>
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
由用户在初始化时通过 `attachEnemyComparer` 提供。若未提供比较器,
|
||||||
|
在调用 `modifyPrefabAttribute` 或 `changePrefab` 时需发出警告,且视所有怪物均为脏。
|
||||||
|
|
||||||
|
## 3. `ISpecial<T>` 继承 `ISaveableContent<T>`
|
||||||
|
|
||||||
|
- `saveState` 返回 `structuredClone(this.value)`(即 `getValue()` 的深拷贝);
|
||||||
|
- `loadState` 调用 `setValue(state)`;
|
||||||
|
- 新增 `deepEqualsTo(other: ISpecial<T>): boolean`:先对比 `code`,
|
||||||
|
再对 `value` 进行深度比较。
|
||||||
|
|
||||||
|
各内置实现类的比较策略:
|
||||||
|
|
||||||
|
- `NonePropertySpecial`:只需比较 `code`,`value` 为 `void` 无需对比;
|
||||||
|
- `CommonSerializableSpecial`:`value` 为普通可序列化对象,
|
||||||
|
使用 `lodash-es` 的 `isEqual` 进行递归深度比较。
|
||||||
|
|
||||||
|
## 4. `IEnemy<TAttr>` 继承 `ISaveableContent<IEnemySaveState<TAttr>>`
|
||||||
|
|
||||||
|
- `saveState(compression)`:深拷贝 `attrs`,对每个 special 调用
|
||||||
|
`saveState(compression)` 收集到 `specials` Map,返回 `IEnemySaveState<TAttr>`;
|
||||||
|
- `loadState(state, compression)`:以 `state.attrs` 还原属性,
|
||||||
|
然后对已有的每个 special 按 code 查找存档中的对应条目并调用 `loadState`;
|
||||||
|
若存档中出现当前怪物未注册的 special code,发出 logger 警告并跳过。
|
||||||
|
|
||||||
|
## 5. `IEnemyManager<TAttr>` 接口修改
|
||||||
|
|
||||||
|
### 5a. 继承 `ISaveableContent<IEnemyManagerSaveState<TAttr>>`
|
||||||
|
|
||||||
|
- `saveState(compression)`:遍历 dirty 集合,对每个脏模板调用
|
||||||
|
`prefab.saveState(compression)`,汇总为 `IEnemyManagerSaveState<TAttr>` 并返回;
|
||||||
|
- `loadState(state, compression)`:遍历 `state.modified`,
|
||||||
|
找到 code 对应的现有模板,调用 `prefab.loadState(enemyState, compression)` 还原;
|
||||||
|
若某 code 不在当前 prefab 表中,发出 logger 警告并跳过;
|
||||||
|
**不清空 dirty 集合**,始终以首次 `compareWith` 提供的参考为唯一基准;
|
||||||
|
`loadState` 结束后重新用比较器对每个已有脏模板进行比对,
|
||||||
|
刷新 dirty 集合(避免加载后实际已恢复初始值的模板仍停留在 dirty 中)。
|
||||||
|
|
||||||
|
### 5b. 新增 `compareWith`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
- 由调用方在游戏初始化完成后提供参考快照,外部传入,管理器保存引用;
|
||||||
|
- **首次调用**:直接存储参考,清空 dirty 集合;
|
||||||
|
- **非首次调用**:通过 logger 发出警告,提示此操作风险高,
|
||||||
|
请作者确认操作意图,但仍然执行覆盖(直接替换参考,重置 dirty 集合)。
|
||||||
|
|
||||||
|
### 5c. `getPrefab` / `getPrefabById` 返回值改为 `IReadonlyEnemy<TAttr>`
|
||||||
|
|
||||||
|
原来返回 `IEnemy<TAttr>`,外部可以直接修改模板。
|
||||||
|
改为只读引用,外部不能直接修改,必须通过 `modifyPrefabAttribute` 完成。
|
||||||
|
|
||||||
|
### 5d. 新增 `modifyPrefabAttribute`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
modifyPrefabAttribute(
|
||||||
|
code: number | string,
|
||||||
|
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||||
|
): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
执行流程:
|
||||||
|
|
||||||
|
1. 根据 `code`(数字或 id 字符串)找到对应的模板;
|
||||||
|
2. 将模板以可写引用传入 `modify`,获得修改结果;
|
||||||
|
3. 若 `modify` 返回的是**新引用**(与传入的不同),则将该新对象替换模板表条目
|
||||||
|
(同时更新 `prefabByCode` 与 `prefabById`);
|
||||||
|
4. 将最终生效的模板与 `compareWith` 中提供的参考模板进行 `IEnemyComparer.compare` 比较:
|
||||||
|
- 若不相等,则将此 code 加入 dirty 集合;
|
||||||
|
- 若相等(改回了初始值),则从 dirty 集合中移除;
|
||||||
|
5. 若未附加比较器,则始终视为脏,并发出 logger 警告。
|
||||||
|
|
||||||
|
### 5e. 新增 `attachEnemyComparer` / `getEnemyComparer`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void;
|
||||||
|
getEnemyComparer(): IEnemyComparer<TAttr> | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
- `attachEnemyComparer`:设置当前管理器使用的比较器;
|
||||||
|
- `getEnemyComparer`:返回当前比较器,如未设置则返回 `null`,
|
||||||
|
允许外部在特殊场景下借用比较器。
|
||||||
|
|
||||||
|
### 5f. `changePrefab` 也参与 dirty 追踪
|
||||||
|
|
||||||
|
`changePrefab` 直接替换模板表,修改完成后同样与参考模板进行比较,
|
||||||
|
更新 dirty 集合(逻辑与 `modifyPrefabAttribute` 步骤 4 相同)。
|
||||||
|
|
||||||
|
`deletePrefab` 不参与 dirty 追踪,存档时直接跳过被删除的模板。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `lodash-es`:`CommonSerializableSpecial.deepEqualsTo` 中使用 `isEqual` 进行深度比较
|
||||||
|
- `@motajs/common`:引用 `logger` 接口
|
||||||
|
- `@user/data-base/common/types.ts`:引用 `ISaveableContent`、`SaveCompression`
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `packages-user/data-base/src/enemy/types.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `IEnemySaveState<TAttr>` 类型:单个怪物的存档状态
|
||||||
|
- [ ] 新增 `IEnemyManagerSaveState<TAttr>` 类型:管理器的存档状态
|
||||||
|
- [ ] 新增 `IEnemyComparer<TAttr>` 接口:包含 `compare` 方法,由用户实现
|
||||||
|
- [ ] 修改 `ISpecial<T>`:继承 `ISaveableContent<T>`,
|
||||||
|
新增 `deepEqualsTo(other: ISpecial<T>): boolean`
|
||||||
|
- [ ] 修改 `IEnemy<TAttr>`:继承 `ISaveableContent<IEnemySaveState<TAttr>>`
|
||||||
|
- [ ] 修改 `IEnemyManager<TAttr>`:继承 `ISaveableContent<IEnemyManagerSaveState<TAttr>>`,
|
||||||
|
新增 `compareWith`、`modifyPrefabAttribute`、`attachEnemyComparer`、`getEnemyComparer`;
|
||||||
|
修改 `getPrefab` 与 `getPrefabById` 返回类型为 `IReadonlyEnemy<TAttr>`
|
||||||
|
|
||||||
|
### `packages-user/data-base/src/enemy/enemy.ts`(`Enemy` 类)
|
||||||
|
|
||||||
|
- [ ] 实现 `saveState(compression): IEnemySaveState<TAttr>`
|
||||||
|
- [ ] 实现 `loadState(state, compression): void`
|
||||||
|
|
||||||
|
### `packages-user/data-base/src/enemy/manager.ts`(`EnemyManager` 类)
|
||||||
|
|
||||||
|
- [ ] 新增 `private readonly dirtySet: Set<number>` 成员:记录脏模板的 code
|
||||||
|
- [ ] 新增 `private referenceByCode: Map<number, IReadonlyEnemy<TAttr>>` 成员:
|
||||||
|
保存参考快照
|
||||||
|
- [ ] 新增 `private comparer: IEnemyComparer<TAttr> | null` 成员:比较器
|
||||||
|
- [ ] 新增 `private hasReference: boolean` 成员:标记是否已首次调用 `compareWith`
|
||||||
|
- [ ] 实现 `compareWith`:存储参考快照,非首次调用发出警告,重置 dirty 集合
|
||||||
|
- [ ] 实现 `modifyPrefabAttribute`:调用 modify、处理引用变化、比较、更新 dirty 集合
|
||||||
|
- [ ] 修改 `changePrefab`:替换模板后同步更新 dirty 集合
|
||||||
|
- [ ] 修改 `getPrefab` / `getPrefabById` 返回类型(仅类型,实现无需改动)
|
||||||
|
- [ ] 实现 `attachEnemyComparer` / `getEnemyComparer`
|
||||||
|
- [ ] 实现 `saveState`:遍历 dirty 集合,序列化并返回
|
||||||
|
- [ ] 实现 `loadState`:根据存档恢复脏模板,恢复后重新刷新 dirty 集合
|
||||||
|
|
||||||
|
### 引擎内置特殊属性(当前包内)
|
||||||
|
|
||||||
|
- [ ] `NonePropertySpecial`:实现 `saveState`、`loadState`、`deepEqualsTo`
|
||||||
|
(`value` 为 `void`,`deepEqualsTo` 只比较 `code`)
|
||||||
|
- [ ] `CommonSerializableSpecial`:实现 `saveState`、`loadState`、`deepEqualsTo`
|
||||||
|
(`deepEqualsTo` 的 value 比较使用 `lodash-es` 的 `isEqual`)
|
||||||
54
docs/dev/enemy/enemy-types-restructure.md
Normal file
54
docs/dev/enemy/enemy-types-restructure.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# 怪物系统文件结构重构
|
||||||
|
|
||||||
|
**目的:** 使 `IEnemyHandler` 能持有 `IStateBase` 引用而不产生循环引用。
|
||||||
|
|
||||||
|
**循环根源:** `enemy/types.ts` 混写了两层接口,`data-base/src/types.ts` 经由 `./enemy` index 引入 `IEnemyManager`,导致无法反向引用。
|
||||||
|
|
||||||
|
## 接口归属
|
||||||
|
|
||||||
|
**`enemy/types.ts`(Layer 1 — 存档/管理层,保留)**
|
||||||
|
|
||||||
|
只保留以下接口,其余全部迁出:
|
||||||
|
|
||||||
|
- `IEnemySaveState`、`IEnemyManagerSaveState`、`IEnemyComparer`
|
||||||
|
- `ISpecial`、`IReadonlyEnemy`、`IEnemy`
|
||||||
|
- `SpecialCreation`、`IEnemyLegacyBridge`、`IEnemyManager`
|
||||||
|
|
||||||
|
只允许引用 `@motajs/common`、`../common`,不得引用 `../types`。
|
||||||
|
|
||||||
|
**`enemy/utils.ts`(Layer 0 — 工具层,与实现类并列)**
|
||||||
|
|
||||||
|
从 `enemy/types.ts` 迁出以下接口(实现类 `MapLocIndexer` 已在此文件):
|
||||||
|
|
||||||
|
- `IMapLocHelper`、`IMapLocIndexer`
|
||||||
|
|
||||||
|
**`combat/types.ts`(Layer 2 — 战斗/上下文层,现为空文件)**
|
||||||
|
|
||||||
|
从 `enemy/types.ts` 迁入以下接口:
|
||||||
|
|
||||||
|
- `IEnemyHandler`、`IReadonlyEnemyHandler`、`IEnemyView`
|
||||||
|
- `IEnemySpecialModifier`、`IAuraView`、`IEnemyAuraView`、`IAuraConverter`
|
||||||
|
- `IEnemySpecialQueryModifier`、`IEnemySpecialQueryEffect`、`IEnemyCommonQueryEffect`、`IEnemyFinalEffect`
|
||||||
|
- `IMapDamageInfoExtra`、`IMapDamageInfo`、`IMapDamageView`、`IMapDamageConverter`、`IMapDamageReducer`、`IMapDamage`
|
||||||
|
- `IEnemyDamageInfo`、`IEnemyCritical`、`CriticalableHeroStatus`、`IDamageCalculator`、`IDamageContext`、`IDamageSystem`
|
||||||
|
- `IEnemyContext`
|
||||||
|
|
||||||
|
需引用 `../enemy/types`(Layer 1)、`../enemy/utils`(Layer 0)、`../types`(`IStateBase`,现在无循环)。在 `IEnemyHandler` 和 `IReadonlyEnemyHandler` 中新增 `readonly state: IStateBase<TAttr, THero>`。
|
||||||
|
|
||||||
|
## 文件修改清单
|
||||||
|
|
||||||
|
| 文件 | 操作 |
|
||||||
|
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `combat/types.ts` | 迁入所有 Layer 2 接口;新增三条 import(`../enemy/types`、`../enemy/utils`、`../types`);`IEnemyHandler` 系列加 `state` 字段 |
|
||||||
|
| `combat/context.ts` | 从 `enemy/context.ts` **移动**;import 路径改为从 `./types`、`../enemy/types`、`../enemy/utils` 引入 |
|
||||||
|
| `combat/damage.ts` | 从 `enemy/damage.ts` **移动**;import 路径同上 |
|
||||||
|
| `combat/mapDamage.ts` | 从 `enemy/mapDamage.ts` **移动**;import 路径同上 |
|
||||||
|
| `combat/index.ts` | 新建,`export * from './types/context/damage/mapDamage'` |
|
||||||
|
| `enemy/types.ts` | 删除所有 Layer 2 接口(迁出后仅剩 Layer 1 内容) |
|
||||||
|
| `enemy/utils.ts` | 新增 `IMapLocHelper`、`IMapLocIndexer` 接口定义 |
|
||||||
|
| `enemy/index.ts` | 删除对 `context`、`damage`、`mapDamage` 的导出 |
|
||||||
|
| `data-base/src/index.ts` | 新增 `export * from './combat'` |
|
||||||
|
| `data-base/src/types.ts` | `import { IEnemyManager }` 改从 `./enemy/types` 直接引入(`enemy/types.ts` 不再引用 `../types`,无循环) |
|
||||||
|
| `enemy/enemy.ts` | import 路径无需改动(`IEnemy` 等仍在 `./types`) |
|
||||||
|
| `enemy/manager.ts` | import 路径无需改动(全部 Layer 1 接口仍在 `./types`) |
|
||||||
|
| `enemy/special.ts` | import 路径无需改动 |
|
||||||
110
docs/dev/hero/hero-modifier-save.md
Normal file
110
docs/dev/hero/hero-modifier-save.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
当前勇士属性修饰器 `IHeroModifier` 是非注册式的,无法直接存档。
|
||||||
|
目标是将其改为注册式,每种修饰器在使用前需先在属性对象上注册,
|
||||||
|
并实现 `ISaveableContent` 接口以支持存档读档。
|
||||||
|
|
||||||
|
注册接口签名:`registerModifier(identifier: string, cons: () => IHeroModifier): void`
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 修改 `IHeroModifier` 接口
|
||||||
|
|
||||||
|
- 将 `identifier` 改为 `type`:一类修饰器可能被添加多次,
|
||||||
|
`type` 比 `identifier` 更准确地表达"修饰器类型"的含义。
|
||||||
|
- 新增泛型参数 `S = unknown` 作为存档类型,让接口继承 `ISaveableContent<S>`:
|
||||||
|
`IHeroModifier<T, V, S = unknown>`
|
||||||
|
- 大多数修饰器的可变状态只有 `value`,因此 `BaseHeroModifier` 将 `S` 默认为 `V`。
|
||||||
|
若修饰器需要特殊存储结构,可以不继承 `BaseHeroModifier`,自行编写实现类。
|
||||||
|
|
||||||
|
## 2. 新增 `IModifierStateSave` 类型
|
||||||
|
|
||||||
|
`IModifierStateSave` 记录单条修饰器的存档信息:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IModifierStateSave {
|
||||||
|
readonly name: PropertyKey; // 属性名,如 'atk'
|
||||||
|
readonly type: string; // 修饰器类型(与注册时的 key 对应)
|
||||||
|
readonly state: unknown; // 修饰器 saveState 结果
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 修改 `IHeroStateSave`,新增 `modifiers` 字段
|
||||||
|
|
||||||
|
`attribute` 字段维持原来的 `THero` 只保存基础属性值,
|
||||||
|
修饰器列表单独作为顶层字段:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
readonly modifiers: readonly IModifierStateSave[];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 修改 `IHeroAttribute`,新增 `iterateModifiers` 方法
|
||||||
|
|
||||||
|
`HeroAttribute` 不再继承 `ISaveableContent`,也不负责存读档,
|
||||||
|
保留现有的 `toStructured(): THero`。
|
||||||
|
新增 `iterateModifiers` 方法,供 `HeroState` 在存档时遍历所有已挂载的修饰器:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
iterateModifiers(): Iterable<[keyof THero, IHeroModifier]>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 修改 `BaseHeroModifier`,新增 `S` 泛型并实现 `ISaveableContent<V>`
|
||||||
|
|
||||||
|
- 改为 `BaseHeroModifier<T, V>` 隐式以 `S = V` 实现 `ISaveableContent<V>`
|
||||||
|
- 将 `abstract readonly identifier: string` 改为 `abstract readonly type: string`
|
||||||
|
- `saveState()` 直接返回 `this.currentValue`
|
||||||
|
- `loadState(state)` 调用 `this.setValue(state)` 恢复值
|
||||||
|
|
||||||
|
## 6. 修改 `HeroState`
|
||||||
|
|
||||||
|
修饰器注册表移至 `HeroState`:
|
||||||
|
|
||||||
|
- 新增 `private readonly registry: Map<string, () => IHeroModifier>` 存储工厂函数
|
||||||
|
- 实现 `registerModifier(type, cons)`:向 `registry` 写入工厂,
|
||||||
|
并同步注册到接口签名中
|
||||||
|
- 实现 `createModifier<V>(type)`:从 `registry` 取出对应工厂,
|
||||||
|
调用工厂函数创建并返回修饰器实例,类型为 `IHeroModifier<unknown, V>`
|
||||||
|
- 实现 `createAndInsertModifier<K, V>(type, name)`:
|
||||||
|
调用 `createModifier` 创建实例后,自动调用 `this.attribute.addModifier(name, modifier)` 插入属性对象,
|
||||||
|
返回该修饰器实例,类型与 `createModifier` 一致
|
||||||
|
- 修改 `saveState()`:
|
||||||
|
1. `attribute` 字段调用 `this.attribute.toStructured()` 获取基础属性值(与现在一致)
|
||||||
|
2. 遍历 `this.attribute.iterateModifiers()`,对每个修饰器调用
|
||||||
|
`modifier.saveState(compression)` 并拼装 `IModifierStateSave[]`,
|
||||||
|
写入 `modifiers` 字段
|
||||||
|
- 修改 `loadState()`:
|
||||||
|
1. 创建新的 `HeroAttribute` 实例(使用 `state.attribute` 还原基础属性值,与现在一致)
|
||||||
|
2. 遍历 `state.modifiers`,通过 `registry.get(type)` 创建修饰器实例,
|
||||||
|
调用 `modifier.loadState(state)` 恢复值,再 `addModifier(name, modifier)` 挂载
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/hero/types.ts`
|
||||||
|
|
||||||
|
- [x] 修改 `IHeroModifier<T, V>` 接口:
|
||||||
|
改为 `IHeroModifier<T, V, S = unknown>`,`identifier` 改名为 `type`,
|
||||||
|
继承 `ISaveableContent<S>`
|
||||||
|
- [x] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式
|
||||||
|
- [x] 修改 `IHeroStateSave<THero>`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段
|
||||||
|
- [x] 修改 `IReadonlyHeroAttribute<THero>`:新增 `iterateModifiers()` 方法签名
|
||||||
|
- [x] 修改 `IHeroState<THero>`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier<V>(type: string): IHeroModifier<unknown, V>` - `createAndInsertModifier<K extends keyof THero, V>(type: string, name: K): IHeroModifier<unknown, V>`
|
||||||
|
|
||||||
|
### `@user/data-base/hero/attribute.ts`
|
||||||
|
|
||||||
|
- [x] 修改 `BaseHeroModifier<T, V>`:
|
||||||
|
将 `abstract readonly identifier` 改为 `abstract readonly type`;
|
||||||
|
实现 `saveState` / `loadState`
|
||||||
|
- [x] 修改 `HeroAttribute<THero>`:实现 `iterateModifiers()`
|
||||||
|
|
||||||
|
### `@user/data-base/hero/state.ts`
|
||||||
|
|
||||||
|
- [x] 修改 `HeroState<THero>`:新增 `private readonly registry: Map<string, () => IHeroModifier>` 成员
|
||||||
|
- [x] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry`
|
||||||
|
- [x] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例;
|
||||||
|
若 `type` 未注册则抛出错误
|
||||||
|
- [x] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后,
|
||||||
|
再调用 `this.attribute.addModifier(name, modifier)`,返回同一实例
|
||||||
|
- [x] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段
|
||||||
|
- [x] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载
|
||||||
595
docs/dev/map/dynamic-tile-move.md
Normal file
595
docs/dev/map/dynamic-tile-move.md
Normal file
@ -0,0 +1,595 @@
|
|||||||
|
# 动态图块移动系统
|
||||||
|
|
||||||
|
> 本文档为 [动态图块系统](./dynamic-tile.md) 的移动子系统设计文档,
|
||||||
|
> 重点描述双方向模型、`IDynamicMover` 计划式移动接口及执行流程。
|
||||||
|
> 本文档中新增的类型与接口同样需要添加至 `@user/data-base/src/map/types.ts`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 背景与动机
|
||||||
|
|
||||||
|
`IDynamicLayer.moveDynamicStep` / `moveDynamicWith` 是命令式简单接口,适用于
|
||||||
|
一次性的简单移动,但对以下场景力不从心:
|
||||||
|
|
||||||
|
- **后退移动**:角色面朝上、向下移动时,朝向不变但位移反向,
|
||||||
|
单一 `FaceDirection` 参数无法同时描述朝向和移动方向;
|
||||||
|
- **连续路径中途动态追加步骤**:命令式接口执行后无法继续追加;
|
||||||
|
- **移动速度切换**:路径中途需要变速,命令式接口不支持;
|
||||||
|
- **复杂动画编排**:需要按序执行多段计划时,纯 `Promise` 链表达不够直观。
|
||||||
|
|
||||||
|
因此引入以图块为粒度绑定的 `IDynamicMover`,采用**先建计划、再执行**的设计理念,
|
||||||
|
参考 `@motajs/animate/types.ts` 的链式计划构建模式,
|
||||||
|
以及 `@user/data-state/legacy/move.ts` 中 `ObjectMoverBase` 的双方向与队列设计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 双方向模型
|
||||||
|
|
||||||
|
每个动态图块维护两个方向字段:
|
||||||
|
|
||||||
|
| 字段 | 含义 | 影响 |
|
||||||
|
| --------------- | ---------------------------- | --------------------------------------- |
|
||||||
|
| `faceDirection` | **朝向**,视觉上「面朝哪里」 | 通过 `IRoleFaceBinder` 决定渲染图块数字 |
|
||||||
|
| `moveDirection` | **移动方向**,「向哪边走」 | 结合 `getFaceMovement` 计算位移 |
|
||||||
|
|
||||||
|
两者分离后可以表达:
|
||||||
|
|
||||||
|
- **后退**:`moveDirection = Up`,执行 `backward()` 步,
|
||||||
|
`moveDirection = Down`,`faceDirection = Down`,位移向下,朝向同步翻转。
|
||||||
|
- **横向移动不转身**:使用 `stepFace(move, face)` 单独指定朝向,
|
||||||
|
`moveDirection` 与 `faceDirection` 独立设置。
|
||||||
|
|
||||||
|
绝对方向步骤(传入 `FaceDirection`)默认将两者同时更新;
|
||||||
|
`forward()` 沿当前 `moveDirection` 移动并对齐 `faceDirection`;
|
||||||
|
`backward()` 将 `moveDirection` 取反并同步 `faceDirection`。
|
||||||
|
|
||||||
|
## 2. 方向步语义
|
||||||
|
|
||||||
|
绝对方向步(`IDynamicMoveStepDir` / `IDynamicMoveStepDirFace`)直接传入 `FaceDirection`:
|
||||||
|
|
||||||
|
- `step(dir, count?: number)`:追加 `count`(默认 1)个绝对方向步,`moveDirection` 和 `faceDirection` 均更新为 `dir`;
|
||||||
|
- `stepFace(move, face, count?: number)`:追加 `count`(默认 1)个绝对方向步,`moveDirection` 更新为 `move`,
|
||||||
|
`faceDirection` 更新为 `face`——用于移动方向与朝向不一致的场景(如斜切移动保持正向)。
|
||||||
|
|
||||||
|
相对方向步(`IDynamicMoveStepSpecial`)通过独立方法构建:
|
||||||
|
|
||||||
|
- `forward(count?)`:沿当前 `tile.moveDirection` 移动,`moveDirection` 不变,
|
||||||
|
`faceDirection` 对齐为 `moveDirection`;
|
||||||
|
- `backward(count?)`:沿 `opposite(tile.moveDirection)` 移动,`moveDirection` 取反,
|
||||||
|
`faceDirection` 同步更新为新的 `moveDirection`,自动使用逆向动画。
|
||||||
|
|
||||||
|
## 3. DynamicMoveStep 类型
|
||||||
|
|
||||||
|
步骤类型通过 `const enum` 区分,避免字符串比较。
|
||||||
|
前进/后退方向用独立的 `DynamicSpecialStep` 枚举表示;
|
||||||
|
动画播放方向用独立的 `DynamicAnimDirection` 枚举表示,两者互不依赖:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const enum DynamicMoveStepType {
|
||||||
|
/** 绝对方向步,同步更新 moveDirection 和 faceDirection */
|
||||||
|
Dir,
|
||||||
|
/** 绝对方向步,单独指定 faceDirection */
|
||||||
|
DirFace,
|
||||||
|
/** 速度步 */
|
||||||
|
Speed,
|
||||||
|
/** 纯转向步,不产生位移 */
|
||||||
|
Face,
|
||||||
|
/** 特殊步(前进或后退) */
|
||||||
|
Special,
|
||||||
|
/** 动画方向步,不影响 moveDirection/faceDirection */
|
||||||
|
AnimDir
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 特殊步方向:前进或后退 */
|
||||||
|
const enum DynamicSpecialStep {
|
||||||
|
Forward,
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 动画播放方向:正向或逆向 */
|
||||||
|
const enum DynamicAnimDirection {
|
||||||
|
Forward,
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绝对方向步:move 方向既是移动方向也是朝向 */
|
||||||
|
interface IDynamicMoveStepDir {
|
||||||
|
type: DynamicMoveStepType.Dir;
|
||||||
|
move: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绝对方向步:move 是移动方向,face 是显式指定的朝向 */
|
||||||
|
interface IDynamicMoveStepDirFace {
|
||||||
|
type: DynamicMoveStepType.DirFace;
|
||||||
|
move: FaceDirection;
|
||||||
|
face: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 速度步:修改后续移动的每格耗时(单位 ms,越小越快) */
|
||||||
|
interface IDynamicMoveStepSpeed {
|
||||||
|
type: DynamicMoveStepType.Speed;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 转向步:仅更新 faceDirection,不产生位移,等待渲染动画完成 */
|
||||||
|
interface IDynamicMoveStepFace {
|
||||||
|
type: DynamicMoveStepType.Face;
|
||||||
|
value: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 特殊步:前进(Forward)或后退(Backward)。`forward(n)`/`backward(n)` 构建时 n>1 会 push n 个相同步骤 */
|
||||||
|
interface IDynamicMoveStepSpecial {
|
||||||
|
type: DynamicMoveStepType.Special;
|
||||||
|
direction: DynamicSpecialStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 动画方向步:指定后续步骤动画正向播放(Forward)还是逆向播放(Backward) */
|
||||||
|
interface IDynamicMoveAnimDir {
|
||||||
|
type: DynamicMoveStepType.AnimDir;
|
||||||
|
dir: DynamicAnimDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicMoveStep =
|
||||||
|
| IDynamicMoveStepDir
|
||||||
|
| IDynamicMoveStepDirFace
|
||||||
|
| IDynamicMoveStepSpeed
|
||||||
|
| IDynamicMoveStepFace
|
||||||
|
| IDynamicMoveStepSpecial
|
||||||
|
| IDynamicMoveAnimDir;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. IMoverController 接口
|
||||||
|
|
||||||
|
`IDynamicMover.start()` 返回 `IMoverController`,用于控制进行中的移动。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IMoverController {
|
||||||
|
/** 本次移动是否已全部完成 */
|
||||||
|
readonly done: boolean;
|
||||||
|
/** 当本次移动全部步骤完成时兑现 */
|
||||||
|
readonly onEnd: Promise<void>;
|
||||||
|
/**
|
||||||
|
* 向当前队列末尾追加步骤(仅在移动进行中有效,完成后追加无效)
|
||||||
|
*/
|
||||||
|
push(...steps: DynamicMoveStep[]): void;
|
||||||
|
/**
|
||||||
|
* 停止移动,等待当前步骤完成后停止,返回的 Promise 在停止后兑现
|
||||||
|
*/
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. IDynamicMover 接口
|
||||||
|
|
||||||
|
每个 `IDynamicTile` 持有一个**绑定**的 `IDynamicMover` 实例(`tile.mover`),
|
||||||
|
无需单独创建或管理生命周期。
|
||||||
|
|
||||||
|
### 5.1 状态读取
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 以下状态属性来自 IObjectMover<IDynamicTile>,IDynamicMover 全部继承
|
||||||
|
interface IObjectMover<T extends IObjectMovable> {
|
||||||
|
/** 是否正在移动 */
|
||||||
|
readonly moving: boolean;
|
||||||
|
/** 当前朝向(与 tile.faceDirection 同步) */
|
||||||
|
readonly faceDirection: FaceDirection;
|
||||||
|
/** 当前移动方向(与 tile.moveDirection 同步) */
|
||||||
|
readonly moveDirection: FaceDirection;
|
||||||
|
/** 所属的可移动对象 */
|
||||||
|
readonly tile: T;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 计划构建(链式)
|
||||||
|
|
||||||
|
所有构建方法均返回 `this`,支持链式调用。
|
||||||
|
计划在调用 `start()` 前不执行,仅追加到内部队列。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IDynamicMover {
|
||||||
|
// ...
|
||||||
|
/** 追加 count(默认 1)个绝对方向步,同步更新 moveDirection 和 faceDirection */
|
||||||
|
step(dir: FaceDirection, count?: number): this;
|
||||||
|
/** 追加 count(默认 1)个绝对方向步,单独指定 faceDirection(用于移动方向与朝向不一致的场景) */
|
||||||
|
stepFace(move: FaceDirection, face: FaceDirection, count?: number): this;
|
||||||
|
/** 追加前进步,沿当前 moveDirection 移动,faceDirection 对齐为 moveDirection */
|
||||||
|
forward(count?: number): this;
|
||||||
|
/** 追加后退步,moveDirection 取反,faceDirection 同步更新,自动使用逆向动画 */
|
||||||
|
backward(count?: number): this;
|
||||||
|
/** 追加一个速度步,改变后续移动的每格耗时(ms) */
|
||||||
|
speed(value: number): this;
|
||||||
|
/** 追加一个纯转向步,仅更新 faceDirection,不产生位移 */
|
||||||
|
face(dir: FaceDirection): this;
|
||||||
|
/** 追加一个动画方向步,控制后续步骤的动画播放方向(Forward 正向 / Backward 逆向) */
|
||||||
|
animDir(dir: DynamicAnimDirection): this;
|
||||||
|
/** 清空尚未执行的计划队列 */
|
||||||
|
clear(): this;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**用法示例**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 先向下走 2 步,切换为半速,再向右走 1 步
|
||||||
|
tile.mover
|
||||||
|
.step(FaceDirection.Down, 2)
|
||||||
|
.speed(200)
|
||||||
|
.step(FaceDirection.Right)
|
||||||
|
.start();
|
||||||
|
|
||||||
|
// 后退 3 步(moveDirection/faceDirection 翻转,逆向动画)
|
||||||
|
tile.mover.backward(3).start();
|
||||||
|
|
||||||
|
// 向右移动但保持朝下(如横向滑步)
|
||||||
|
tile.mover.stepFace(FaceDirection.Right, FaceDirection.Down, 2).start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 执行控制
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IDynamicMover {
|
||||||
|
// ...
|
||||||
|
/**
|
||||||
|
* 开始执行计划队列,返回控制对象。
|
||||||
|
* 若已在移动则返回 null(不中断当前移动)。
|
||||||
|
*/
|
||||||
|
start(): IMoverController | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 移动执行流程
|
||||||
|
|
||||||
|
每次 `start()` 启动后,执行器按以下总流程处理队列:
|
||||||
|
|
||||||
|
触发 `onMoveStart` → 循环每步:[
|
||||||
|
`abstract onStepStart(step, tile, mover)` 执行并返回 `code: number`(子类计算本步场景)→
|
||||||
|
触发 `IObjectMoverHooks.onStepStart(code, step, tile, mover)` 钩子(渲染预处理,此时 `tile.x/y` 为**移动前坐标**)→
|
||||||
|
等待所有 Promise →
|
||||||
|
`abstract onStepEnd(code, step, tile, mover)` 执行**逻辑效果**(子类根据 code 决定如何执行)→
|
||||||
|
触发 `IObjectMoverHooks.onStepEnd(code, step, tile, mover)` 钩子(此时 `tile.x/y` 为**移动后坐标**)
|
||||||
|
] → 触发 `onMoveEnd`。
|
||||||
|
|
||||||
|
### 6.1 绝对方向步(`DynamicMoveStepType.Dir`)
|
||||||
|
|
||||||
|
**onStepStart 阶段**(`tile.x/y` 为原始坐标):
|
||||||
|
|
||||||
|
1. `abstract onStepStart(step, tile, mover)` 执行,子类计算本步 `code`(正常移动、碰墙等)并返回 `Promise<number>`。
|
||||||
|
2. 父类拿到 `code`,触发 `IObjectMoverHooks.onStepStart(code, step, tile, mover)` 钩子——渲染端在此阶段播放移动动画,
|
||||||
|
可由 `step.move` 与当前 `tile.x/y` 推算目标位置。
|
||||||
|
3. `await Promise.all(promises)`——等待全部动画完成。
|
||||||
|
|
||||||
|
**onStepEnd 阶段**(逻辑效果):
|
||||||
|
|
||||||
|
1. `abstract onStepEnd(code, step, tile, mover)` 执行——子类依据 `code` 决定本步实际效果:
|
||||||
|
更新 `tile.moveDirection = step.move`。
|
||||||
|
2. 计算 `{dx, dy} = getFaceMovement(step.move)`,**立即**更新 `tile.x += dx`、`tile.y += dy`。
|
||||||
|
3. 同步更新 `posMap`。
|
||||||
|
4. 执行转向逻辑(见 [dynamic-tile.md §8](./dynamic-tile.md#8-转向逻辑)),
|
||||||
|
更新 `tile.faceDirection` 与 `tile.num`。
|
||||||
|
5. 子类返回后,父类触发 `IObjectMoverHooks.onStepEnd(code, step, tile, mover)` 钩子,此时 `tile.x/y` 已为新坐标。
|
||||||
|
|
||||||
|
### 6.1.1 含显式朝向的绝对方向步(`DynamicMoveStepType.DirFace`)
|
||||||
|
|
||||||
|
与 `Dir` 步骤相同,但步骤 5 改为直接将 `tile.faceDirection` 设为 `step.face`,
|
||||||
|
并通过 `getFaceOf(tile.num, step.face)` 更新 `tile.num`,不经过 `degradeFace` 降级。
|
||||||
|
|
||||||
|
### 6.2 速度步(`DynamicMoveStepType.Speed`)
|
||||||
|
|
||||||
|
更新内部速度状态,立即生效,无位移,无需等待。
|
||||||
|
|
||||||
|
### 6.3 纯转向步(`DynamicMoveStepType.Face`)
|
||||||
|
|
||||||
|
经由 `onStepStart` 触发钩子(`tile.x/y` 不变,`step.type === DynamicMoveStepType.Face` 供渲染端区分);
|
||||||
|
等待渲染端动画完成后,`onStepEnd` 阶段执行转向逻辑,更新 `faceDirection` 与 `tile.num`,
|
||||||
|
触发 `onStepEnd` 钩子。
|
||||||
|
|
||||||
|
### 6.4 特殊步(`DynamicMoveStepType.Special`)
|
||||||
|
|
||||||
|
**语义说明**:`forward`/`backward` 以**上一步的移动方向**(即当前 `tile.moveDirection`)为基准;
|
||||||
|
若无上一步,则回退到当前 `faceDirection`;若图块无朝向绑定,则抛出错误。
|
||||||
|
|
||||||
|
**Forward**(`DynamicSpecialStep.Forward`):
|
||||||
|
|
||||||
|
1. `actualDir = tile.moveDirection`(不改变 moveDirection);
|
||||||
|
2. 执行与 `Dir` 步骤相同的位移流程;
|
||||||
|
3. 步骤 2:跳过(`moveDirection` 保持不变);
|
||||||
|
4. 步骤 5:执行转向逻辑,以 `actualDir` 更新 `tile.faceDirection` 与 `tile.num`。
|
||||||
|
|
||||||
|
**Backward**(`DynamicSpecialStep.Backward`):
|
||||||
|
|
||||||
|
1. `actualDir = opposite(tile.moveDirection)`;
|
||||||
|
2. 在执行前临时将逆向动画标志设为开启(步骤完成后恢复);
|
||||||
|
3. 执行与 `Dir` 步骤相同的位移流程;
|
||||||
|
4. 步骤 2:`tile.moveDirection = actualDir`;
|
||||||
|
5. 步骤 5:执行转向逻辑,以 `actualDir` 更新 `tile.faceDirection` 与 `tile.num`。
|
||||||
|
|
||||||
|
### 6.5 动画方向步(`DynamicMoveStepType.AnimDir`)
|
||||||
|
|
||||||
|
指定后续步骤动画是**正向播放**(`DynamicAnimDirection.Forward`)还是**逆向播放**(`DynamicAnimDirection.Backward`),
|
||||||
|
与图块朝向无关(朝向始终由 `faceDirection` 决定)。
|
||||||
|
修改 mover 内部的 `currentAnimDir` 状态。
|
||||||
|
渲染端在 `onStepStart` 收到 `AnimDir` 步骤时读取并更新本地动画方向状态,后续步骤的 `onStepStart` 触发时依据此状态决定播放方向。
|
||||||
|
`onStepEnd` 阶段无逻辑操作,立即完成,不影响坐标与方向。
|
||||||
|
|
||||||
|
## 7. onStepStart / onStepEnd 钩子返回值
|
||||||
|
|
||||||
|
`IObjectMoverHooks.onStepStart` 与 `onStepEnd` 均为可选钩子(`?`),返回类型为 `Promise<void>`,
|
||||||
|
执行器始终通过 `Promise.all` 等待所有订阅方完成后再继续。
|
||||||
|
|
||||||
|
关键时序:`onStepStart` 所有 Promise 兑现后,才执行逻辑效果(abstract onStepEnd);
|
||||||
|
`onStepEnd` 全部兑现后,才进入下一步。
|
||||||
|
多个订阅方通过 `Promise.all` 并行等待,与 `forEachHook` 返回所有值的设计天然契合——
|
||||||
|
`const promises = forEachHook(hook => hook.onStepStart?.(code, step, tile, mover));`
|
||||||
|
`await Promise.all(promises);`
|
||||||
|
|
||||||
|
## 8. 与 IDynamicLayer / IDynamicTile 接口的关系
|
||||||
|
|
||||||
|
`IDynamicLayer` 和 `IDynamicTile` **仅保留 `step(dir)` 单步便捷接口**
|
||||||
|
(等价于 `tile.mover.step(dir).start()`),不再提供 `moveDynamicWith` / `moveWith` 等批量接口。
|
||||||
|
复杂移动能力(`forward`/`backward`/`animDir` 等)通过 `tile.mover` 访问。
|
||||||
|
|
||||||
|
**渲染端订阅移动事件的流程**:
|
||||||
|
|
||||||
|
1. 订阅 `IDynamicLayer.onCreateDynamicTile`;
|
||||||
|
2. 创建图块时获取 `tile.mover` 引用,直接向其添加钩子;
|
||||||
|
3. 移动事件从 `tile.mover` 触发,无需经过 `IDynamicLayer` 转发。
|
||||||
|
|
||||||
|
为此,`IDynamicMover` 扩展为可订阅对象,
|
||||||
|
实现 `IHookable<IObjectMoverHooks<IDynamicTile>>`(见接口定义汇总)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/types.ts`
|
||||||
|
|
||||||
|
修改现有接口(移动相关新增类型已移至 `move/types.ts`):
|
||||||
|
|
||||||
|
- [ ] `IDynamicTile` 新增 `readonly mover: IDynamicMover`
|
||||||
|
- [ ] `IDynamicTile` 已有 `readonly faceDirection: FaceDirection` 与
|
||||||
|
`readonly moveDirection: FaceDirection`(已在主文档确认)
|
||||||
|
- [ ] `IObjectMovable` 接口(`x`、`y`、`moveDirection`、`faceDirection` 只读属性 +
|
||||||
|
`setPos`/`setMoveDirection`/`setFaceDirection`,定义在 `dynamic-tile.md`)
|
||||||
|
- [ ] `IDynamicLayer` / `IDynamicTile` 仅保留 `step(dir)` 便捷接口,
|
||||||
|
移除 `moveDynamicWith` / `moveDynamicStep` / `moveWith` / `moveStep`
|
||||||
|
(主文档接口汇总需同步更新)
|
||||||
|
|
||||||
|
#### `@user/data-base/src/map/types.ts`
|
||||||
|
|
||||||
|
- [ ] `DynamicMoveStepType` `const enum`:
|
||||||
|
含 `Dir`/`DirFace`/`Speed`/`Face`/`Special`/`AnimDir` 六个成员
|
||||||
|
- [ ] `DynamicSpecialStep` `const enum`:含 `Forward`/`Backward` 两个成员
|
||||||
|
- [ ] `DynamicAnimDirection` `const enum`:含 `Forward`/`Backward` 两个成员
|
||||||
|
- [ ] `IDynamicMoveStepDir` 接口(`type: DynamicMoveStepType.Dir`,`move: FaceDirection`)
|
||||||
|
- [ ] `IDynamicMoveStepDirFace` 接口(`type: DynamicMoveStepType.DirFace`,`move`/`face: FaceDirection`)
|
||||||
|
- [ ] `IDynamicMoveStepSpeed` 接口(`type: DynamicMoveStepType.Speed`)
|
||||||
|
- [ ] `IDynamicMoveStepFace` 接口(`type: DynamicMoveStepType.Face`)
|
||||||
|
- [ ] `IDynamicMoveStepSpecial` 接口(`type: Special`,`direction: DynamicSpecialStep`;无 `count` 字段,`forward(n)`/`backward(n)` 改为 push n 个步骤对象)
|
||||||
|
- [ ] `IDynamicMoveAnimDir` 接口(`type: DynamicMoveStepType.AnimDir`,`dir: DynamicAnimDirection`)
|
||||||
|
- [ ] `DynamicMoveStep` 联合类型(含以上六种步骤)
|
||||||
|
- [ ] `IMoverController` 接口
|
||||||
|
- [ ] `IObjectMoverHooks<T extends IObjectMovable>` 接口(含 `onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`;
|
||||||
|
`onStepStart`/`onStepEnd` 参数顺序为 `(code, step, tile, mover)`,按使用频率排列)
|
||||||
|
- [ ] `IObjectMover<T extends IObjectMovable>` 接口(含 `faceDirection`/`moveDirection` 只读属性及全部计划构建方法;`step`/`stepFace` 含 `count?` 参数)
|
||||||
|
- [ ] `IDynamicMover extends IObjectMover<IDynamicTile>`(无额外成员,继承基类全部能力;`IDynamicMoverHooks` 已移除,直接使用 `IObjectMoverHooks<IDynamicTile>`)
|
||||||
|
|
||||||
|
#### `@user/data-base/src/map/mover.ts`
|
||||||
|
|
||||||
|
- [ ] `abstract class ObjectMover<T extends IObjectMovable>` 抽象基类:
|
||||||
|
- 实现 `IObjectMover<T>`,在此提供全部**计划构建方法**(`step`/`stepFace`/`forward`/
|
||||||
|
`backward`/`speed`/`face`/`animDir`/`clear`/`start`)及队列执行调度逻辑;
|
||||||
|
- 含四个**抽象方法**,子类必须实现,组成移动核心控制流:
|
||||||
|
- `abstract onMoveStart(tile, mover): Promise<void>`: 整次移动开始时调用
|
||||||
|
- `abstract onMoveEnd(tile, mover): Promise<void>`: 整次移动结束时调用
|
||||||
|
- `abstract onStepStart(step, tile, mover): Promise<number>`: 单步逻辑执行前调用,
|
||||||
|
子类计算并**返回** `code`;父类将 `code` 传给 `IObjectMoverHooks.onStepStart` 钩子后等待
|
||||||
|
- `abstract onStepEnd(code, step, tile, mover): Promise<void>`: 单步钩子等待完成后调用,子类依据 `code` 执行本步实际逻辑效果
|
||||||
|
- [ ] `class DynamicMover extends ObjectMover<IDynamicTile> implements IDynamicMover`:
|
||||||
|
持有 `tile: IDynamicTile` 引用,维护 `moveQueue`、`moving`、
|
||||||
|
`faceDirection`、`moveDirection` 状态;实现四个抽象方法(含 posMap 更新、转向逻辑等)
|
||||||
|
- [ ] `DynamicTile` 类新增 `readonly mover: DynamicMover`,
|
||||||
|
在构造时以 `this` 为参数创建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 接口定义汇总
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const enum DynamicMoveStepType {
|
||||||
|
Dir,
|
||||||
|
DirFace,
|
||||||
|
Speed,
|
||||||
|
Face,
|
||||||
|
Special,
|
||||||
|
AnimDir
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum DynamicSpecialStep {
|
||||||
|
Forward,
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum DynamicAnimDirection {
|
||||||
|
Forward,
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDynamicMoveStepDir {
|
||||||
|
type: DynamicMoveStepType.Dir;
|
||||||
|
move: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDynamicMoveStepDirFace {
|
||||||
|
type: DynamicMoveStepType.DirFace;
|
||||||
|
move: FaceDirection;
|
||||||
|
face: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDynamicMoveStepSpeed {
|
||||||
|
type: DynamicMoveStepType.Speed;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDynamicMoveStepFace {
|
||||||
|
type: DynamicMoveStepType.Face;
|
||||||
|
value: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** forward(n)/backward(n) 构建时 n>1 会 push n 个相同步骤,步骤对象本身不携带 count */
|
||||||
|
interface IDynamicMoveStepSpecial {
|
||||||
|
type: DynamicMoveStepType.Special;
|
||||||
|
direction: DynamicSpecialStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDynamicMoveAnimDir {
|
||||||
|
type: DynamicMoveStepType.AnimDir;
|
||||||
|
dir: DynamicAnimDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicMoveStep =
|
||||||
|
| IDynamicMoveStepDir
|
||||||
|
| IDynamicMoveStepDirFace
|
||||||
|
| IDynamicMoveStepSpeed
|
||||||
|
| IDynamicMoveStepFace
|
||||||
|
| IDynamicMoveStepSpecial
|
||||||
|
| IDynamicMoveAnimDir;
|
||||||
|
|
||||||
|
interface IMoverController {
|
||||||
|
readonly done: boolean;
|
||||||
|
readonly onEnd: Promise<void>;
|
||||||
|
push(...steps: DynamicMoveStep[]): void;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
|
||||||
|
onMoveStart(mover: IObjectMover<T>, tile: T): Promise<void>;
|
||||||
|
onMoveEnd(mover: IObjectMover<T>, tile: T): Promise<void>;
|
||||||
|
/** 触发时 tile.x/y 为移动前坐标,适合渲染端在此播放动画 */
|
||||||
|
onStepStart?(
|
||||||
|
code: number,
|
||||||
|
step: DynamicMoveStep,
|
||||||
|
tile: T,
|
||||||
|
mover: IObjectMover<T>
|
||||||
|
): Promise<void>;
|
||||||
|
/** 触发时 tile.x/y 已更新为移动后坐标 */
|
||||||
|
onStepEnd?(
|
||||||
|
code: number,
|
||||||
|
step: DynamicMoveStep,
|
||||||
|
tile: T,
|
||||||
|
mover: IObjectMover<T>
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IObjectMover<T extends IObjectMovable> extends IHookable<
|
||||||
|
IObjectMoverHooks<T>
|
||||||
|
> {
|
||||||
|
readonly moving: boolean;
|
||||||
|
readonly tile: T;
|
||||||
|
readonly faceDirection: FaceDirection;
|
||||||
|
readonly moveDirection: FaceDirection;
|
||||||
|
|
||||||
|
step(dir: FaceDirection, count?: number): this;
|
||||||
|
stepFace(move: FaceDirection, face: FaceDirection, count?: number): this;
|
||||||
|
forward(count?: number): this;
|
||||||
|
backward(count?: number): this;
|
||||||
|
speed(value: number): this;
|
||||||
|
face(dir: FaceDirection): this;
|
||||||
|
animDir(dir: DynamicAnimDirection): this;
|
||||||
|
clear(): this;
|
||||||
|
start(): IMoverController | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里的 onStepStart 不再接受 code 作为参数,因为这应当是其返回值 `Promise<number>`,
|
||||||
|
// 具体来说,这四个方法是整个移动的核心方法而非钩子,它应当提供接口让子类实现,
|
||||||
|
// 子类应当真正执行移动效果,并进行一定的控制。显然子类是没办法知道 onStepStart 中的信息的,
|
||||||
|
// 所以才提供了一个 code,用于子类向 ObjectMover 提供信息,然后父类再传递给子类,
|
||||||
|
// 从而子类了解到 onStepStart 中的信息,再进一步决定这一步真正应该如何执行。
|
||||||
|
// 而对于钩子,实际上是在子类的 onStepStart 执行完毕后进行的,所以是知道 code 的,所以才是参数。
|
||||||
|
// 你应该好好想一想这之间的关系。
|
||||||
|
// 以及子类是要求必须实现的,所以不应该会有返回 `void` 的场景,四个方法都应该返回 `Promise<void>`。
|
||||||
|
// 总的来说,父类是一个“系统级”的流程控制器,它通过这四个接口来实现真正的移动控制,
|
||||||
|
// 就像 legacy ObjectMoverBase 中的 startMove 一样。
|
||||||
|
abstract class ObjectMover<
|
||||||
|
T extends IObjectMovable
|
||||||
|
> implements IObjectMover<T> {
|
||||||
|
abstract onMoveStart(tile: T, mover: IObjectMover<T>): Promise<void>;
|
||||||
|
abstract onMoveEnd(tile: T, mover: IObjectMover<T>): Promise<void>;
|
||||||
|
/** 子类计算并返回本步 code;父类取得 code 后再触发 IObjectMoverHooks.onStepStart 钩子 */
|
||||||
|
abstract onStepStart(
|
||||||
|
step: DynamicMoveStep,
|
||||||
|
tile: T,
|
||||||
|
mover: IObjectMover<T>
|
||||||
|
): Promise<number>;
|
||||||
|
/** 子类依据 code 执行本步实际逻辑效果;父类在此之后触发 IObjectMoverHooks.onStepEnd 钩子 */
|
||||||
|
abstract onStepEnd(
|
||||||
|
code: number,
|
||||||
|
step: DynamicMoveStep,
|
||||||
|
tile: T,
|
||||||
|
mover: IObjectMover<T>
|
||||||
|
): Promise<void>;
|
||||||
|
// 计划构建方法(step/stepFace/forward/backward/speed/face/animDir/clear/start)在此实现
|
||||||
|
}
|
||||||
|
|
||||||
|
// faceDirection/moveDirection 已提升至 IObjectMover,IDynamicMover 无需额外声明
|
||||||
|
// IDynamicMoverHooks 已移除,直接使用 IObjectMoverHooks<IDynamicTile>
|
||||||
|
interface IDynamicMover extends IObjectMover<IDynamicTile> {
|
||||||
|
// 继承自 IObjectMover:moving, faceDirection, moveDirection, tile, 全部计划构建方法及钩子
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 问题
|
||||||
|
|
||||||
|
1. **`backward()` 的移动方向基准** ✅ 已确定
|
||||||
|
|
||||||
|
`backward()` 以 `opposite(tile.moveDirection)` 作为移动方向(基于 `moveDirection` 取反,非 `faceDirection`)。
|
||||||
|
`moveDirection` 更新为反向,`faceDirection` 通过转向逻辑跟随同步更新。
|
||||||
|
|
||||||
|
2. **`forward()` 是否更新 faceDirection** ✅ 已确定
|
||||||
|
|
||||||
|
`forward()` 沿当前 `moveDirection` 移动,`moveDirection` 不变,
|
||||||
|
`faceDirection` 通过转向逻辑对齐为 `moveDirection`。
|
||||||
|
|
||||||
|
3. **转向步触发钩子方式** ✅ 已确定
|
||||||
|
|
||||||
|
统一经由 `onStepStart(code, step, tile, mover)` 钩子处理,在步骤逻辑执行前触发。
|
||||||
|
渲染端通过 `step.type === DynamicMoveStepType.Face` 区分纯转向步与位移步;
|
||||||
|
`onStepStart` 触发时 `tile.x/y` 为原始坐标,渲染端可直接读取,无需反推。
|
||||||
|
转向逻辑(`faceDirection`/`num` 更新)在 `onStepEnd` 阶段执行。
|
||||||
|
|
||||||
|
4. **`animDir` 与渲染钩子集成** ✅ 已确定
|
||||||
|
|
||||||
|
`onStepStart` 统一接收所有步骤,`IDynamicMoveAnimDir` 步骤本身即为信息载体。
|
||||||
|
渲染端在 `onStepStart` 收到 `AnimDir` 步骤时更新本地动画方向状态;
|
||||||
|
后续方向步的 `onStepStart` 触发时,渲染端依据本地状态决定动画播放方向。
|
||||||
|
|
||||||
|
5. **玩家移动与图块移动的复用** ✅ 已确定
|
||||||
|
|
||||||
|
玩家(勇士)移动逻辑与 `IDynamicMover` 高度一致,共同抽象为
|
||||||
|
`IObjectMover<T extends IObjectMovable>`(参考 legacy `ObjectMoverBase`,适配新 API);
|
||||||
|
核心抽象类为 `abstract class ObjectMover<T>`,含四个抽象方法组成控制流:
|
||||||
|
- **计划构建方法**(`step`/`stepFace`/`forward`/`backward`/
|
||||||
|
`speed`/`face`/`animDir`/`clear`/`start`)全部在 `ObjectMover` 基类实现,子类无需重复声明。
|
||||||
|
- **移动流程钩子**(`IObjectMoverHooks<T>`,实现 `IHookable<IObjectMoverHooks<T>>`):
|
||||||
|
- `onMoveStart(mover, tile)`:整次移动队列开始时触发;
|
||||||
|
- `onMoveEnd(mover, tile)`:整次移动队列结束时触发;
|
||||||
|
- `onStepStart(code, step, tile, mover)`:单步开始前触发(`tile.x/y` 为移动前坐标);
|
||||||
|
- `onStepEnd(code, step, tile, mover)`:单步完成后触发(`tile.x/y` 已更新);
|
||||||
|
其中 `code: number` 标识移动场景(如碰墙、遇敌、循环地图等),由具体实现层设置。
|
||||||
|
- **四个抽象方法**(`onMoveStart`/`onMoveEnd`/`onStepStart`/`onStepEnd`):子类(`DynamicMover`/`HeroMover`)
|
||||||
|
通过实现这四个方法完全控制移动行为,执行流程由 `ObjectMover` 基类统一调度。
|
||||||
|
关键:`onStepStart` 返回 `Promise<number>`(code),父类将此 code 传给钩子后再调用 `onStepEnd(code, ...)`,
|
||||||
|
子类在 `onStepEnd` 中依据 code 执行实际状态变更。四个方法均返回 `Promise`,无 `void` 选项。
|
||||||
|
- **泛型参数** `T extends IObjectMovable`:`IObjectMovable` 是可移动对象的最小接口
|
||||||
|
(`x`、`y`、`moveDirection`、`faceDirection` 只读属性 +
|
||||||
|
`setPos`/`setMoveDirection`/`setFaceDirection` 方法),定义在 `dynamic-tile.md`;
|
||||||
|
`IDynamicTile extends IObjectMovable`,玩家侧使用 `IHeroMovable extends IObjectMovable`。
|
||||||
|
- **`IDynamicMover extends IObjectMover<IDynamicTile>`**:
|
||||||
|
无额外成员,直接继承基类全部能力(含 `faceDirection`/`moveDirection` 及 `IObjectMoverHooks`)。
|
||||||
407
docs/dev/map/dynamic-tile.md
Normal file
407
docs/dev/map/dynamic-tile.md
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
当前地图系统仅支持静态图块(通过 `IMapLayer` / `Uint32Array` 存储),
|
||||||
|
无法支持可移动的动态图块(例如 NPC、推箱子、可交互物件等)。
|
||||||
|
动态图块的核心特点:
|
||||||
|
|
||||||
|
1. **允许重叠**——同一格点可同时存在多个动态图块;
|
||||||
|
2. **整数坐标**——始终处于格点整数坐标,不出现小数,便于渲染优化与交互;
|
||||||
|
3. **异步移动**——移动接口返回 `Promise`,配合渲染动画;
|
||||||
|
4. **朝向转向**——部分图块移动时需同步更新朝向(如四方向 NPC 行走图)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 动态图块标识方案
|
||||||
|
|
||||||
|
**采用引用标识**:`createDynamicTile` 与 `transferToDynamic` 均返回
|
||||||
|
`IDynamicTile` 对象引用,调用方持有该引用作为后续操作(移动、删除、
|
||||||
|
设置朝向等)的唯一凭证。无需维护任何 ID,无唯一性管理负担。
|
||||||
|
|
||||||
|
调用方可将返回的 `IDynamicTile` 引用存储在自己的数据结构中,
|
||||||
|
`tile.x`、`tile.y`、`tile.num`、`tile.direction` 等字段始终反映图块的
|
||||||
|
最新状态——内部实现类持有可变字段,接口以 `readonly` 暴露给外部,
|
||||||
|
保证外部不能直接修改,内部可通过类实例更新。
|
||||||
|
|
||||||
|
## 2. 坐标表示
|
||||||
|
|
||||||
|
使用独立的 `x: number, y: number` 参数,与现有 `IMapLayer` 接口风格
|
||||||
|
(如 `setBlock(block, x, y)`、`getBlock(x, y)`)保持一致,不引入 `ITileLocator`。
|
||||||
|
|
||||||
|
## 3. 方向表示
|
||||||
|
|
||||||
|
移动方向与转向方向统一使用 `FaceDirection` 枚举:
|
||||||
|
|
||||||
|
- 语义明确,与已有 `IRoleFaceBinder`(朝向绑定)直接对接;
|
||||||
|
- 已有工具函数 `getFaceMovement(dir)` 可将其转换为坐标偏移,无需重复实现;
|
||||||
|
- 多步路径(`moveDynamicWith` 的 `steps` 参数)使用 `FaceDirection[]`。
|
||||||
|
|
||||||
|
`IDirectionDescriptor` 更偏向纯数学计算(用于范围迭代等),
|
||||||
|
语义上不适合表达「图块朝向某方向移动」的含义。
|
||||||
|
|
||||||
|
**已确定**:移动方向(`moveDirection`)与朝向(`faceDirection`)分离为两个独立字段。
|
||||||
|
后退等相对移动必须依赖两者分离才能正确表达(例如面朝上、向下后退时,`faceDirection`
|
||||||
|
保持 `Up`,`moveDirection` 更新为 `Down`)。
|
||||||
|
|
||||||
|
移动系统另立文档独立设计,包含双方向模型、`IDynamicMover` 抽象、
|
||||||
|
计划式移动接口及执行流程等完整方案,见
|
||||||
|
[动态图块移动系统](./dynamic-tile-move.md)。
|
||||||
|
|
||||||
|
`moveDynamicStep` / `moveDynamicWith` 保留为命令式便捷接口,适用于单次简单移动;
|
||||||
|
复杂路径和计划式移动统一使用 `IDynamicMover`。
|
||||||
|
|
||||||
|
## 4. 动态图块数据模型与图层归属
|
||||||
|
|
||||||
|
动态图块按**所属静态图层**组织层级关系:每个 `IMapLayer` 持有一个
|
||||||
|
`IDynamicLayer`,动态图块的渲染深度即为其所属 `IMapLayer` 的 `zIndex`,
|
||||||
|
与静态图块一致。因此 `IDynamicLayer` 接口挂载在 `IMapLayer` 上,
|
||||||
|
而不是 `ILayerState` 上(见第 5 节)。
|
||||||
|
|
||||||
|
每个动态图块存储以下信息:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 可移动对象的最小公共接口,供 `IObjectMover` 泛型约束使用 */
|
||||||
|
interface IObjectMovable {
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
readonly moveDirection: FaceDirection;
|
||||||
|
readonly faceDirection: FaceDirection;
|
||||||
|
setPos(x: number, y: number): void;
|
||||||
|
setMoveDirection(dir: FaceDirection): void;
|
||||||
|
setFaceDirection(dir: FaceDirection): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IDynamicTile extends IObjectMovable {
|
||||||
|
readonly num: number; // 图块数字(决定渲染图像)
|
||||||
|
readonly layer: IDynamicLayer; // 所属动态图层
|
||||||
|
readonly mover: IDynamicMover; // 绑定的移动器(计划式移动)
|
||||||
|
/** 删除此图块,转发至 layer.deleteDynamic */
|
||||||
|
delete(): void;
|
||||||
|
/** 还原为静态图块,转发至 layer.transferToStatic */
|
||||||
|
toStatic(): void;
|
||||||
|
/** 还原为静态图块,如果当前位置有东西则不转换,转发只 layer.transferToStaticIfSafe */
|
||||||
|
toStaticIfSafe(): boolean;
|
||||||
|
/**
|
||||||
|
* 单步便捷移动接口,等价于 `mover.step(dir, count); return mover.start();`。
|
||||||
|
* 适用于简单移动场景,复杂路径通过 `tile.mover` 访问。
|
||||||
|
*/
|
||||||
|
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`x`、`y`、`moveDirection`、`faceDirection` 及 `setPos`/`setMoveDirection`/`setFaceDirection`
|
||||||
|
均由 `IObjectMovable` 提供,`IDynamicTile` 不再重复声明。
|
||||||
|
`IObjectMover<T extends IObjectMovable>` 以此接口为泛型约束,
|
||||||
|
使图块和玩家 mover 共享核心执行逻辑;具体渲染效果通过 `IObjectMoverHooks` 的 `onStepStart`/`onStepEnd` 钩子实现。
|
||||||
|
|
||||||
|
`DynamicLayer` 内部维护两个结构:
|
||||||
|
|
||||||
|
- `tileSet: Set<IDynamicTile>` — 所有图块的集合,用于迭代与归属判断;
|
||||||
|
- `posMap: Map<number, Map<number, Set<IDynamicTile>>>` — 按坐标索引
|
||||||
|
(外层 key = y,内层 key = x,value = 该格点所有图块对象集合),
|
||||||
|
图块移动时同步更新,支持越界坐标(见第 4.1 节)。
|
||||||
|
|
||||||
|
调用方持有 `IDynamicTile` 对象引用即可完成所有操作,
|
||||||
|
`posMap` 仅供按坐标查询(`getDynamicTilesAt`)使用,
|
||||||
|
因使用频率低,嵌套 `Map` 的开销完全可以接受。
|
||||||
|
|
||||||
|
`IDynamicTile` 上的 `moveStep`/`moveWith`/`delete`/`setDirection`/`setPos`/`toStatic`
|
||||||
|
均为便捷转发方法,内部直接调用 `tile.layer` 对应接口,不修改任何内部状态。
|
||||||
|
调用方可直接通过 `tile.xxx()` 操作,无需额外持有 `IDynamicLayer` 引用。
|
||||||
|
|
||||||
|
### 4.1 越界移动
|
||||||
|
|
||||||
|
动态图块的坐标不受楼层 `width`/`height` 约束,允许出现负值或超出地图范围的坐标。
|
||||||
|
这满足了如「图块飞出屏幕」「过场动画」等临时越界需求。
|
||||||
|
`DynamicLayer` 无需感知楼层尺寸,`resizeLayer` 也无需通知 `DynamicLayer`。
|
||||||
|
|
||||||
|
## 5. IDynamicLayer 挂载在 IMapLayer 上
|
||||||
|
|
||||||
|
动态图块的存储结构与 `IMapLayer`(Uint32Array)本质不同,独立封装为 `IDynamicLayer`。
|
||||||
|
`IMapLayer` 新增 `readonly dynamicLayer: IDynamicLayer` 属性,
|
||||||
|
调用方通过具体的图层对象操作该层的动态图块,z 层级天然与静态图块一致。
|
||||||
|
|
||||||
|
`IDynamicLayer` 实现 `IHookable<IDynamicLayerHooks>` 以支持渲染端订阅变更事件。
|
||||||
|
`IRoleFaceBinder` 通过 `setFaceBinder(binder)` 方法注入(见第 8 节),
|
||||||
|
不在构造时传入,使多楼层共用同一个 `binder` 实例更灵活。
|
||||||
|
|
||||||
|
`transferToDynamic(x, y)` 不需要 `layer` 参数,
|
||||||
|
因为它隶属于某个具体的 `IDynamicLayer`,直接从同级 `IMapLayer` 读写图块即可,
|
||||||
|
并返回创建的 `IDynamicTile` 引用。
|
||||||
|
|
||||||
|
## 6. 移动函数的坐标来源
|
||||||
|
|
||||||
|
`moveDynamicStep(direction, tile)` 与 `moveDynamicWith(steps, tile)` 均不接受
|
||||||
|
`ox, oy` 参数,改为直接接受 `IDynamicTile` 引用。当前坐标直接读取 `tile.x`、
|
||||||
|
`tile.y`,无需调用方额外传入。
|
||||||
|
|
||||||
|
内部行为:
|
||||||
|
|
||||||
|
- 直接读取 `tile.x`、`tile.y` 作为出发点;
|
||||||
|
- **立即**将图块逻辑坐标更新为目标位置(单步 `(tile.x+dx, tile.y+dy)`);
|
||||||
|
- 同步更新 `posMap`;
|
||||||
|
- 触发 `onMoveTile` 钩子,收集所有钩子返回的 `Promise<void> | void`;
|
||||||
|
- `await Promise.all(promises)`——
|
||||||
|
等待所有渲染动画完成后,本步才算兑现,再进入下一步。
|
||||||
|
|
||||||
|
详细多步路径与计划式移动的执行流程见
|
||||||
|
[动态图块移动系统](./dynamic-tile-move.md)。
|
||||||
|
|
||||||
|
## 7. transferToDynamic 与 transferToStatic
|
||||||
|
|
||||||
|
`transferToDynamic(x, y)` 不需要额外的 `layer` 参数。
|
||||||
|
`DynamicLayer` 在构造时持有对所属 `MapLayer` 的引用,
|
||||||
|
直接调用 `mapLayer.getBlock(x, y)` 读取图块数字,再调用 `mapLayer.setBlock(0, x, y)` 清除,
|
||||||
|
最后创建对应的动态图块并返回其引用。
|
||||||
|
若该位置图块为 0(空白),则发出 logger 警告并仍然创建 `num = 0` 的动态图块。
|
||||||
|
|
||||||
|
新增 `transferToStaticIfSafe(tile)` 作为安全版本:仅当目标位置静态图块为 0(空白)时才执行还原,
|
||||||
|
否则不转换并返回 `false`;转换成功返回 `true`。适用于不确定目标格是否已有图块的场景。
|
||||||
|
|
||||||
|
新增 `transferToStatic(tile)` 作为逆操作:将动态图块还原为静态图块。
|
||||||
|
对于「只移动一次就固定」的图块(如推箱子放到指定位置后不再移动),
|
||||||
|
及时转回静态存储可以降低存档体积、简化渲染订阅。执行流程:
|
||||||
|
|
||||||
|
1. 读取 `tile.x`、`tile.y`、`tile.num`;
|
||||||
|
2. 若坐标不在所属 `MapLayer` 的合法范围内(即 `x < 0`、`y < 0`、
|
||||||
|
`x >= width` 或 `y >= height`),发出 logger 警告并放弃操作;
|
||||||
|
3. 若 `mapLayer.getBlock(tile.x, tile.y) !== 0`,在开发环境下发出 logger 警告
|
||||||
|
(目标格点已有静态图块内容,将被覆盖);
|
||||||
|
调用 `mapLayer.setBlock(tile.num, tile.x, tile.y)` 写回静态图块;
|
||||||
|
4. 从 `tileSet` 和 `posMap` 中移除该图块,触发 `onDeleteTile` 钩子。
|
||||||
|
|
||||||
|
## 8. 转向逻辑
|
||||||
|
|
||||||
|
`moveDynamicStep` / `moveDynamicWith` 在每一步移动时自动更新朝向(使用双方向模型,
|
||||||
|
详见 [动态图块移动系统](./dynamic-tile-move.md)):
|
||||||
|
|
||||||
|
1. 更新 `tile.moveDirection` 为本步实际移动方向;
|
||||||
|
2. 通过外部注入的 `IRoleFaceBinder` 调用 `getFaceOf(tile.num, direction)` 查询
|
||||||
|
该方向对应的图块数字;
|
||||||
|
3. 若图块无朝向绑定(返回 `null`),则不修改 `num`,仅更新 `faceDirection`;
|
||||||
|
4. 若有绑定,将 `tile.num` 与 `tile.faceDirection` 更新为查询结果。
|
||||||
|
|
||||||
|
`IRoleFaceBinder` 通过 `setFaceBinder(binder: IRoleFaceBinder): void` 方法注入,
|
||||||
|
不在构造时传入;初始状态视为无朝向绑定(`getFaceOf` 始终返回 `null`)。
|
||||||
|
若多个楼层共用同一个 `RoleFaceBinder` 实例,直接对各楼层各图层分别调用 `setFaceBinder` 即可。
|
||||||
|
|
||||||
|
对于八方向移动,转向查询逻辑扩展如下:
|
||||||
|
|
||||||
|
1. 更新 `tile.moveDirection` 为本步实际移动方向;
|
||||||
|
2. 调用 `getFaceOf(tile.num, direction)` 查询;
|
||||||
|
3. 若返回 `null` 且当前方向为斜向(八方向之一),
|
||||||
|
调用 `degradeFace(direction)` 降级为四方向后再查询一次;
|
||||||
|
4. 若仍返回 `null`,不修改 `num`,仅更新 `faceDirection`;
|
||||||
|
5. 若查询到结果,将 `tile.num` 更新为结果图块数字,并更新 `faceDirection`。
|
||||||
|
|
||||||
|
手动设置朝向通过独立接口 `setDynamicDirection(tile, direction)` 完成,
|
||||||
|
逻辑与上述步骤 1–5 相同,但不触发移动。
|
||||||
|
|
||||||
|
## 9. IDynamicLayerHooks 与 ILayerStateHooks
|
||||||
|
|
||||||
|
`IDynamicLayerHooks` 定义三个钩子:
|
||||||
|
|
||||||
|
- `onCreateTile(tile: IDynamicTile)`:图块被创建(含转换)时触发;
|
||||||
|
- `onDeleteTile(tile: IDynamicTile)`:图块被删除时触发(传入删除前的快照);
|
||||||
|
- `onMoveTile(tile: IDynamicTile, fromX: number, fromY: number): Promise<void> | void`:
|
||||||
|
图块移动一步时触发,`tile` 为更新后的状态,`fromX`/`fromY` 为移动前坐标。
|
||||||
|
返回 `Promise<void>` 时,移动器将等待其兑现后再进行下一步(配合 `Promise.all` 并行等待所有订阅方)。
|
||||||
|
|
||||||
|
`ILayerStateHooks` 新增对应的三个转发钩子,额外携带 `layer: IMapLayer` 参数,
|
||||||
|
与现有的 `onUpdateLayerArea`、`onResizeLayer` 等钩子风格一致:
|
||||||
|
|
||||||
|
- `onCreateDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile)`
|
||||||
|
- `onDeleteDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile)`
|
||||||
|
- `onMoveDynamicTile(layer: IMapLayer, dynamicLayer: IDynamicLayer, tile: IDynamicTile, fromX: number, fromY: number)`
|
||||||
|
|
||||||
|
`LayerState` 在为每个 `MapLayer` 注册 `StateMapLayerHook` 时,
|
||||||
|
同步订阅该层 `dynamicLayer` 的三个钩子,将事件加上 `layer`、`dynamicLayer` 参数后转发至楼层钩子。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
> 移动相关的接口(`DynamicMoveDir`、`IDynamicMoveStep`、`IMoverController`、`IDynamicMover`)
|
||||||
|
> 设计详情见 [动态图块移动系统](./dynamic-tile-move.md),
|
||||||
|
> 同样需要添加至 `types.ts`。
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `@motajs/common`:`IHookable`, `IHookBase`, `Hookable`, `logger`
|
||||||
|
- `@user/data-base/src/common`:`FaceDirection`, `IRoleFaceBinder`,
|
||||||
|
`getFaceMovement`, `degradeFace`
|
||||||
|
- `@user/data-base/src/map/types.ts`:`IMapLayer`, `IMapLayerHooks`,
|
||||||
|
`ILayerState`, `ILayerStateHooks`
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/types.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `IObjectMovable` 接口:
|
||||||
|
含 `x`、`y`、`moveDirection`、`faceDirection` 四个只读属性及
|
||||||
|
`setPos`/`setMoveDirection`/`setFaceDirection` 三个方法;
|
||||||
|
供 `IObjectMover<T extends IObjectMovable>` 泛型约束使用
|
||||||
|
- [ ] 新增 `IDynamicTile extends IObjectMovable` 接口:
|
||||||
|
含 `num`、`layer`、`mover` 三个只读属性及 `delete`/`toStatic` 两个便捷方法;
|
||||||
|
无 `id` 字段,坐标/方向成员由 `IObjectMovable` 提供
|
||||||
|
- [ ] 新增移动相关类型(详见移动文档接口定义汇总,已移至 `move/types.ts`):
|
||||||
|
`DynamicMoveStepType`、`DynamicMoveStep`、`IMoverController`、
|
||||||
|
`IObjectMoverHooks`、`IObjectMover`、`IDynamicMover`
|
||||||
|
- [ ] 新增 `IDynamicLayerHooks extends IHookBase` 接口:
|
||||||
|
含 `onCreateTile`、`onDeleteTile`、`onMoveTile` 三个钩子方法
|
||||||
|
- [ ] 新增 `IDynamicLayer extends IHookable<IDynamicLayerHooks>` 接口:
|
||||||
|
- [ ] `createDynamicTile(num, x, y): IDynamicTile`: 在指定位置创建动态图块,
|
||||||
|
返回图块引用
|
||||||
|
- [ ] `transferToDynamic(x, y): IDynamicTile`: 从所属静态图层读取并清除
|
||||||
|
指定位置图块,创建对应动态图块并返回引用
|
||||||
|
- [ ] `transferToStatic(tile: IDynamicTile)`: 将动态图块还原为静态图块;
|
||||||
|
坐标越界则警告并放弃,否则写回静态图层并触发 `onDeleteTile`
|
||||||
|
- [ ] `transferToStaticIfSafe(tile: IDynamicTile): boolean`: 仅当目标位置静态图块为 0
|
||||||
|
时才还原,否则不转换;返回是否转换成功
|
||||||
|
- [ ] `deleteDynamicTile(tile: IDynamicTile)`: 删除指定图块,不在此层则警告
|
||||||
|
- [ ] `getDynamicTilesAt(x, y)`: 获取指定格点所有动态图块的可迭代对象
|
||||||
|
- [ ] `moveDynamicWith` / `moveDynamicStep` 已由 `IDynamicMover` 权其责。
|
||||||
|
`IDynamicLayer` 不再提供此两个方法,删除对应条目。
|
||||||
|
- [ ] `setDynamicDirection(tile: IDynamicTile, direction)`: 手动设置朝向,
|
||||||
|
同步更新 `direction` 与 `num`(若有朝向绑定)
|
||||||
|
- [ ] `setDynamicPos(tile: IDynamicTile, x: number, y: number)`: 直接设置图块位置,
|
||||||
|
同步更新 `posMap`,触发 `onMoveTile` 钩子,不更新朝向
|
||||||
|
- [ ] `setFaceBinder(binder: IRoleFaceBinder)`: 注入朝向绑定器
|
||||||
|
- [ ] 修改 `IMapLayer`:新增 `readonly dynamicLayer: IDynamicLayer`
|
||||||
|
- [ ] 修改 `ILayerStateHooks`:新增 `onCreateDynamicTile`、`onDeleteDynamicTile`、
|
||||||
|
`onMoveDynamicTile` 三个转发钩子,额外携带 `layer: IMapLayer` 与
|
||||||
|
`dynamicLayer: IDynamicLayer` 两个参数
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/dynamicLayer.ts`(新文件)
|
||||||
|
|
||||||
|
- [ ] 实现 `DynamicLayer extends Hookable<IDynamicLayerHooks>` 类,
|
||||||
|
实现 `IDynamicLayer`
|
||||||
|
- [ ] 实现内部类 `DynamicTile implements IDynamicTile`:
|
||||||
|
持有 `layer: IDynamicLayer` 引用,`delete`/`toStatic` 转发至 `layer`;
|
||||||
|
`step(dir, count?)` 封装为便捷方法
|
||||||
|
- [ ] `DynamicTile` 构造时同步创建 `readonly mover: DynamicMover`,
|
||||||
|
移动调度由 `move/dynamicMover.ts` 中的 `DynamicMover` 类实现
|
||||||
|
- [ ] `private mapLayer: IMapLayer`:构造参数,所属静态图层引用,
|
||||||
|
供 `transferToDynamic` / `transferToStatic` 读写使用
|
||||||
|
- [ ] `private faceBinder: IRoleFaceBinder | null = null`:朝向绑定器,
|
||||||
|
通过 `setFaceBinder` 注入
|
||||||
|
- [ ] `private tileSet: Set<IDynamicTile>`:所有图块的集合,用于迭代与归属判断
|
||||||
|
- [ ] `private posMap: Map<number, Map<number, Set<IDynamicTile>>>`:
|
||||||
|
按坐标索引(外层 key = y,内层 key = x),支持越界坐标
|
||||||
|
- [ ] 实现全部接口方法,移动时同步更新 `tileSet` 与 `posMap`
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `readonly dynamicLayer: DynamicLayer`:构造时以 `this` 为参数创建
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/layerState.ts`
|
||||||
|
|
||||||
|
- [ ] 修改 `StateMapLayerHook`(或 `addLayer` 中的订阅逻辑):
|
||||||
|
在为每个 `MapLayer` 注册钩子时,同时订阅其 `dynamicLayer` 的三个钩子,
|
||||||
|
将事件加上 `layer`、`dynamicLayer` 参数后转发至楼层的 `ILayerStateHooks`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 接口定义汇总
|
||||||
|
|
||||||
|
以下为本次新增与修改的完整接口签名,供实现时参考。
|
||||||
|
|
||||||
|
移动相关接口(`IObjectMoverHooks`、`IObjectMover`、`IDynamicMover`、`IMoverController`、`DynamicMoveStep` 等)
|
||||||
|
详见 [dynamic-tile-move.md](./dynamic-tile-move.md) 接口定义汇总,此处不再重复。
|
||||||
|
|
||||||
|
## 新增接口
|
||||||
|
|
||||||
|
### IObjectMovable
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IObjectMovable {
|
||||||
|
readonly x: number;
|
||||||
|
readonly y: number;
|
||||||
|
readonly moveDirection: FaceDirection;
|
||||||
|
readonly faceDirection: FaceDirection;
|
||||||
|
setPos(x: number, y: number): void;
|
||||||
|
setMoveDirection(dir: FaceDirection): void;
|
||||||
|
setFaceDirection(dir: FaceDirection): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDynamicTile
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IDynamicTile extends IObjectMovable {
|
||||||
|
readonly num: number;
|
||||||
|
readonly layer: IDynamicLayer;
|
||||||
|
readonly mover: IDynamicMover;
|
||||||
|
delete(): void;
|
||||||
|
toStatic(): void;
|
||||||
|
toStaticIfSafe(): boolean;
|
||||||
|
/** 等价于 mover.step(dir, count); return mover.start(); */
|
||||||
|
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDynamicLayerHooks
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IDynamicLayerHooks extends IHookBase {
|
||||||
|
onCreateTile(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||||
|
onDeleteTile(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDynamicLayer
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||||
|
createDynamic(num: number, x: number, y: number): IDynamicTile;
|
||||||
|
transferToDynamic(x: number, y: number): IDynamicTile;
|
||||||
|
transferToStatic(tile: IDynamicTile): void;
|
||||||
|
transferToStaticIfSafe(tile: IDynamicTile): boolean;
|
||||||
|
deleteDynamic(tile: IDynamicTile): void;
|
||||||
|
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
|
||||||
|
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void;
|
||||||
|
setDynamicPos(tile: IDynamicTile, x: number, y: number): void;
|
||||||
|
setFaceBinder(binder: IRoleFaceBinder): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 修改接口
|
||||||
|
|
||||||
|
### IMapLayer(新增属性)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IMapLayer {
|
||||||
|
// ...现有成员...
|
||||||
|
readonly dynamicLayer: IDynamicLayer;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 问题
|
||||||
|
|
||||||
|
1. **`ox, oy` 是否为必要参数?** ✅ 已确定
|
||||||
|
|
||||||
|
已确定移除。调用方持有 `IDynamicTile` 引用,坐标直接从 `tile.x`、`tile.y` 读取,
|
||||||
|
`ox, oy` 为冗余参数,接口简化为 `moveDynamicStep(direction, tile)` 和
|
||||||
|
`moveDynamicWith(steps, tile)`。
|
||||||
|
|
||||||
|
2. **`IRoleFaceBinder` 的注入方式** ✅ 已确定
|
||||||
|
|
||||||
|
已确定使用 `setFaceBinder(binder)` 方法注入,不在构造时传入。
|
||||||
|
实际修改 `faceBinder` 的场景极少,接口保持简单。
|
||||||
|
|
||||||
|
3. **动态图块的存档支持** ⏸ 暂缓
|
||||||
|
|
||||||
|
动态图块状态(NPC 当前位置、推箱子位置等)属于游戏核心存档内容,
|
||||||
|
长期来看必须纳入存档体系。但当前设计与旧引擎完全不同,
|
||||||
|
存档格式需从头设计,建议在动态图块功能稳定后单独立项设计存档方案。
|
||||||
|
**本次实现不涉及存档,`IDynamicLayer` 暂不实现 `ISaveableContent`。**
|
||||||
|
|
||||||
|
4. **`IDynamicLayer` 是否需要感知楼层尺寸** ✅ 已确定
|
||||||
|
|
||||||
|
已确定不感知。标识方案改为引用,`posMap` 同步改为
|
||||||
|
`Map<number, Map<number, Set<IDynamicTile>>>` 嵌套结构(外层 key = y,内层 key = x),
|
||||||
|
彻底去掉字符串键,天然支持越界坐标,`resizeLayer` 无需通知 `DynamicLayer`。
|
||||||
|
`getDynamicTilesAt` 接口保留,使用频率低,嵌套 Map 开销完全可接受。
|
||||||
212
docs/dev/map/map-store-improve.md
Normal file
212
docs/dev/map/map-store-improve.md
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
本次改动目标:
|
||||||
|
|
||||||
|
1. **自动化分区激活器**:将楼层按游戏进程划分为若干"区域",
|
||||||
|
到达新区域时自动激活对应楼层、失活旧区域楼层,
|
||||||
|
从而替代目前繁琐的手动 `setActiveStatus` 调用。
|
||||||
|
2. **楼层尺寸上移至 `LayerState`**:同一楼层的所有图层尺寸应当
|
||||||
|
保持一致,因此将尺寸的权威来源从 `MapLayer` 移至 `LayerState`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 有序地图 id 列表
|
||||||
|
|
||||||
|
当前 `IMapStore.maps` 是 `ReadonlySet<string>`,无序且随楼层创建
|
||||||
|
自动填充。区域功能需要以**下标**标识范围,因此需改为有序数组。
|
||||||
|
|
||||||
|
修改方案:
|
||||||
|
|
||||||
|
- `IMapStore.maps` 类型改为 `ReadonlyArray<string>`;
|
||||||
|
- 新增 `setMapList(maps: string[]): void`,由外部显式指定有序列表
|
||||||
|
(一般在游戏初始化时调用一次);
|
||||||
|
- 新增 `useManualOrder(sort: (arr: string[]) => string[]): void`,
|
||||||
|
允许自定义地图列表排序函数。调用时将当前 `maps` 的 `slice` 拷贝
|
||||||
|
传入 `sort`,再对输出做合法性校验:将新旧数组各转为 `Set`,
|
||||||
|
校验 `size` 相等且新集合是旧集合的子集(利用 `Set.prototype.isSubsetOf`);
|
||||||
|
校验通过后用返回值替换内部的 `maps`。这样当地图是动态生成时,
|
||||||
|
作者依然可以自定义顺序,而不必手动维护全量列表;
|
||||||
|
- `createLayerState` 不再维护 `maps`,`maps` 完全由 `setMapList` 管理;
|
||||||
|
- 若 `createLayerState` 传入的 id 不在 `maps` 中,仍可正常创建,
|
||||||
|
不影响存档逻辑,但该楼层不参与任何区域判断。
|
||||||
|
|
||||||
|
## 2. 区域定义与管理
|
||||||
|
|
||||||
|
### 类型定义
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 单段闭区间 [start, end],start 和 end 均为 maps 下标 */
|
||||||
|
export interface IMapAreaInterval {
|
||||||
|
readonly start: number;
|
||||||
|
readonly end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一个区域由一个或多个独立区间组成 */
|
||||||
|
export type MapArea = IMapAreaInterval[];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接口
|
||||||
|
|
||||||
|
- `setArea(areas: Set<MapArea>): void`:一次性设置所有区域信息,
|
||||||
|
覆盖原有区域定义;每个元素代表一个区域,区域可包含多个区间,
|
||||||
|
使用 `Set<MapArea>` 表示无序区域集合;
|
||||||
|
- `activeArea(id: string): void`:手动激活指定楼层所在区域的所有楼层。
|
||||||
|
系统遍历 `areaList`,找到包含该楼层 id 的区域后,对该区域内的所有
|
||||||
|
楼层调用 `setMapActiveStatus(floor, true)`;
|
||||||
|
- `deactiveArea(id: string): void`:手动取消激活指定楼层所在区域的
|
||||||
|
所有楼层,逻辑与 `activeArea` 对称;判断时遍历 `areaList`,
|
||||||
|
此操作为低频调用,无需缓存;
|
||||||
|
|
||||||
|
## 3. 自动分区激活器
|
||||||
|
|
||||||
|
### 接口
|
||||||
|
|
||||||
|
- `useAutoActivitor(enable: boolean): void`:是否启用自动激活器。
|
||||||
|
|
||||||
|
### 触发接口
|
||||||
|
|
||||||
|
需要一个通知接口供玩家相关模块调用:
|
||||||
|
|
||||||
|
- `notifyEnterFloor(id: string): void`:玩家进入指定楼层时调用此接口,
|
||||||
|
通知地图管理器进行自动激活判断。
|
||||||
|
|
||||||
|
### 逻辑
|
||||||
|
|
||||||
|
`notifyEnterFloor(id)` 的执行流程(每次进入楼层均调用,内部短路):
|
||||||
|
|
||||||
|
1. 若自动激活器未启用,直接返回;
|
||||||
|
2. 若 `isMapActive(id)` 为 `true`,直接返回(楼层已激活,无需操作);
|
||||||
|
3. 遍历 `areaList`,找出包含 `id` 的区域;
|
||||||
|
4. 若未找到,直接返回(该楼层不在任何区域内);
|
||||||
|
5. 若 `lastFloorId !== null`,调用 `deactiveArea(lastFloorId)` 失活上一个区域;
|
||||||
|
6. 调用 `activeArea(id)` 激活新区域,更新 `lastFloorId = id`。
|
||||||
|
|
||||||
|
### 内部状态
|
||||||
|
|
||||||
|
`MapStore` 新增:
|
||||||
|
|
||||||
|
- `private areaList: Set<MapArea>`:所有区域定义;
|
||||||
|
- `private lastFloorId: string | null = null`:上一次触发 `notifyEnterFloor`
|
||||||
|
的楼层 id,用于定位并失活上一个激活区域;
|
||||||
|
- `private autoActivitorEnabled: boolean = false`:自动激活器开关。
|
||||||
|
|
||||||
|
## 4. 楼层尺寸上移至 LayerState
|
||||||
|
|
||||||
|
### 动机
|
||||||
|
|
||||||
|
当前 `MapLayer.width` / `MapLayer.height` 存储在图层中,
|
||||||
|
但同一楼层的所有图层尺寸必须一致,权威来源应当是 `LayerState`。
|
||||||
|
|
||||||
|
### 接口变动
|
||||||
|
|
||||||
|
**`ILayerState` 新增**:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
```
|
||||||
|
|
||||||
|
**`addLayer` 签名调整**:
|
||||||
|
|
||||||
|
目前 `addLayer(width: number, height: number): IMapLayer`,
|
||||||
|
移除 width/height 参数,改为 `addLayer(): IMapLayer`,
|
||||||
|
使用 `LayerState` 内部存储的尺寸创建图层。
|
||||||
|
|
||||||
|
楼层尺寸在 `createLayerState` 创建时指定,`createLayerState` 签名改为:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
createLayerState(id: string, width: number, height: number): ILayerState;
|
||||||
|
```
|
||||||
|
|
||||||
|
运行时仍可通过 `resizeLayer` 修改楼层尺寸,该方法会同步对楼层内所有
|
||||||
|
图层执行 resize,保持尺寸一致。
|
||||||
|
|
||||||
|
**`resizeLayer` 签名调整**:
|
||||||
|
|
||||||
|
当前 `resizeLayer(layer, width, height, keepBlock?)` 只 resize 单个图层,
|
||||||
|
但既然尺寸是楼层级的,建议改为对该楼层的所有图层同步 resize:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
resizeLayer(width: number, height: number, keepBlock?: boolean): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
**`IMapLayer.resize` / `IMapLayer.resize2`**:
|
||||||
|
|
||||||
|
从 `IMapLayer` 接口中移除,保留为 `MapLayer` 的内部实现,
|
||||||
|
仅由 `LayerState.resizeLayer` 调用。
|
||||||
|
|
||||||
|
**`IMapLayer.width` / `IMapLayer.height`**:
|
||||||
|
|
||||||
|
保留在 `IMapLayer` 接口中,供外部通过图层对象直接获取尺寸。
|
||||||
|
其值始终与所属 `LayerState` 的 `width`/`height` 保持一致。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 附加建议结论
|
||||||
|
|
||||||
|
1. **`IMapLayer.setMapRef` 可见性**:保留现有设计,偶尔有外部需求。
|
||||||
|
2. **`active` 状态管理**:不需要单独维护区域激活状态;
|
||||||
|
`activeArea(id)` / `deactiveArea(id)` 是 `setMapActiveStatus` 的
|
||||||
|
快捷方式,遍历区域楼层批量调用即可,无需额外的区域状态字段。
|
||||||
|
3. **`notifyEnterFloor` 返回值**:暂不添加,后续有需求再改进。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `@user/data-base/src/map/types.ts`: 全部现有地图接口
|
||||||
|
- `@user/data-base/src/map/mapStore.ts`: `MapStore` 实现类
|
||||||
|
- `@user/data-base/src/map/layerState.ts`: `LayerState` 实现类
|
||||||
|
- `@user/data-base/src/map/mapLayer.ts`: `MapLayer` 实现类
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/types.ts`
|
||||||
|
|
||||||
|
- [x] 新增 `IMapAreaInterval` 接口:区间定义,含 `start`、`end`
|
||||||
|
- [x] 新增 `MapArea` 类型别名:`IMapAreaInterval[]`,表示一个区域
|
||||||
|
- [x] 修改 `ILayerState`:
|
||||||
|
- [x] 新增 `readonly width: number` 和 `readonly height: number`
|
||||||
|
- [x] 修改 `addLayer` 签名,移除 `width`/`height` 参数(使用 `LayerState` 自身尺寸)
|
||||||
|
- [x] 修改 `resizeLayer` 签名:移除 `layer` 参数,改为对整个楼层所有图层同步 resize
|
||||||
|
- [x] 修改 `IMapLayer`:
|
||||||
|
- [x] 移除 `resize` / `resize2`(改为 `MapLayer` 内部方法)
|
||||||
|
- [x] 修改 `IMapStore`:
|
||||||
|
- [x] 将 `readonly maps` 类型改为 `ReadonlyArray<string>`
|
||||||
|
- [x] 修改 `createLayerState` 签名:新增 `width: number`、`height: number` 参数
|
||||||
|
- [x] 新增 `setMapList(maps: string[]): void`
|
||||||
|
- [x] 新增 `useManualOrder(sort: (arr: string[]) => string[]): void`
|
||||||
|
- [x] 新增 `setArea(areas: Set<MapArea>): void`
|
||||||
|
- [x] 新增 `activeArea(id: string): void`
|
||||||
|
- [x] 新增 `deactiveArea(id: string): void`
|
||||||
|
- [x] 新增 `useAutoActivitor(enable: boolean): void`
|
||||||
|
- [x] 新增 `notifyEnterFloor(id: string): void`
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapStore.ts`
|
||||||
|
|
||||||
|
- [x] 将 `maps: Set<string>` 改为 `maps: string[]`
|
||||||
|
- [x] 修改 `createLayerState`:添加 `width`/`height` 参数,不再维护 `maps`
|
||||||
|
- [x] 实现 `setMapList`
|
||||||
|
- [x] 实现 `useManualOrder`
|
||||||
|
- [x] 新增 `private areaList: Set<MapArea>`
|
||||||
|
- [x] 新增 `private lastFloorId: string | null`
|
||||||
|
- [x] 新增 `private autoActivitorEnabled: boolean`
|
||||||
|
- [x] 实现 `setArea`、`activeArea`、`deactiveArea`
|
||||||
|
- [x] 实现 `useAutoActivitor`
|
||||||
|
- [x] 实现 `notifyEnterFloor`
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/layerState.ts`
|
||||||
|
|
||||||
|
- [x] 新增 `width: number` 和 `height: number` 成员(由构造参数初始化)
|
||||||
|
- [x] 修改 `addLayer`,移除 `width`/`height` 参数,使用 `this.width`/`this.height`
|
||||||
|
- [x] 修改 `resizeLayer`,移除 `layer` 参数,改为对所有图层同步 resize
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
- [x] 将 `resize`/`resize2` 改为内部方法(从公共接口移除)
|
||||||
|
|
||||||
|
---
|
||||||
294
docs/dev/map/map-store-save.md
Normal file
294
docs/dev/map/map-store-save.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
当前 `LayerState` 只存储当前激活地图的数据,切换地图时原地图内容彻底丢失,
|
||||||
|
无法参与存档系统。为此引入 `IMapStore`,集中管理所有楼层的 `LayerState`,
|
||||||
|
并实现 `ISaveableContent` 接口以支持存档读档。
|
||||||
|
|
||||||
|
核心目标:
|
||||||
|
|
||||||
|
- 多楼层数据同时存在于内存中,通过 id 访问;
|
||||||
|
- 通过 `active` 标记区分"玩家可能到达"与"无需关注"的楼层,节省存档开销;
|
||||||
|
- 通过 `compareWith` 提供参考基准,配合分级压缩大幅减少存档体积;
|
||||||
|
- `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`,
|
||||||
|
操作楼层必须先通过 `getLayerState(id)` 取得具体楼层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 给 LayerState 添加 active 成员
|
||||||
|
|
||||||
|
`ILayerState` 新增 `readonly active: boolean` 和 `setActiveStatus(active: boolean): void`
|
||||||
|
两个接口,`LayerState` 实现类中 `active` 默认为 `false`。
|
||||||
|
|
||||||
|
两者的关系:
|
||||||
|
|
||||||
|
- `ILayerState.setActiveStatus`:直接操作楼层对象;
|
||||||
|
- `IMapStore.setMapActiveStatus(id, active)`:通过 id 操作,
|
||||||
|
内部查找对应楼层后调用其 `setActiveStatus`。
|
||||||
|
|
||||||
|
## 2. 脏数据追踪(dirty tracking)
|
||||||
|
|
||||||
|
为支持 `LowCompression` 和 `HighCompression` 的差分存档,
|
||||||
|
需要知道哪些楼层相对于参考基准是否发生了修改。
|
||||||
|
|
||||||
|
**推荐方案:楼层级简单脏标记 + 存档时实际比较**
|
||||||
|
|
||||||
|
在 `LayerState` 内部维护 `private dirty: boolean = false`:
|
||||||
|
|
||||||
|
- 当楼层内任意 `MapLayer` 触发 `onUpdateBlock`、`onUpdateArea`、`onResize`
|
||||||
|
钩子时,将 `dirty` 置为 `true`;
|
||||||
|
- `dirty` 只在 `compareWith` 首次调用时根据实际数据对比结果初始化,
|
||||||
|
初始化后的 gameplay 过程中不再重置(仅置 true)。
|
||||||
|
|
||||||
|
在 `saveState` 时:
|
||||||
|
|
||||||
|
- 若 `dirty = false`,跳过该楼层(初始化后从未被触碰过);
|
||||||
|
- 若 `dirty = true`:
|
||||||
|
- **LowCompression**:与参考基准进行全量比较,若完全一致则跳过
|
||||||
|
(消除"改了又改回"场景下的误判),否则存储所有行;
|
||||||
|
- **HighCompression**:逐行与参考基准比较,只存储不一致的行。
|
||||||
|
|
||||||
|
在 `loadState` 时:若存档中此楼层没有任何数据(即未出现在 `floors` 中),
|
||||||
|
读档后将 `dirty` 置为 `false`(视为与参考基准一致)。
|
||||||
|
|
||||||
|
**不在 `MapLayer` 内维护 `dirtyRows`**,行级比较在 `saveState` 时直接对照参考基准进行。
|
||||||
|
这避免了每次 `setBlock`/`putMapData` 都更新行级标记的热路径开销,
|
||||||
|
且存档时实际比较已能消除误判,无需 `probablyDirty` + `setInterval` 或哈希方案。
|
||||||
|
|
||||||
|
存档时比较的开销:Uint32Array 内存连续,实际耗时极低,且保存操作本身是低频的,
|
||||||
|
若将来发现存档耗时问题,可考虑将比较逻辑移至 Web Worker。
|
||||||
|
|
||||||
|
## 3. compareWith 接口与参数类型
|
||||||
|
|
||||||
|
```ts
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
外层 `Map` 以楼层 id 为键,内层 `Map` 以图层 `zIndex` 为键,
|
||||||
|
值为对应图层的完整图块数组(Uint32Array,含所有行的扁平数据)。
|
||||||
|
|
||||||
|
使用此类型而非 `IMapStore` 的理由:接口更轻量,调用方可直接从游戏原始数据构建,
|
||||||
|
无需额外持有一个完整的 `IMapStore` 实例。
|
||||||
|
|
||||||
|
关于图层标识符:继续使用 `zIndex`,在单个楼层内 `zIndex` 是语义唯一的,
|
||||||
|
与已有 `MapLayer.zIndex` 接口保持一致。
|
||||||
|
|
||||||
|
**`compareWith` 以首次调用为唯一基准**,再次调用不更新参考(以游戏原始数据为基准,
|
||||||
|
避免存档之间产生依赖关系)。
|
||||||
|
|
||||||
|
实现步骤:
|
||||||
|
|
||||||
|
1. 若 `refData` 已存在,直接返回;
|
||||||
|
2. 保存 `ref` 引用到 `private refData`;
|
||||||
|
3. 遍历当前所有楼层,对每个楼层在 `ref` 中查找对应 id:
|
||||||
|
- 不存在:`dirty = true`(新楼层,视为全脏);
|
||||||
|
- 存在:对每个 `MapLayer`(按 `zIndex` 匹配)做全量比较,
|
||||||
|
若所有行与参考数据完全一致则 `dirty = false`,否则 `dirty = true`。
|
||||||
|
|
||||||
|
## 4. 楼层的创建与管理
|
||||||
|
|
||||||
|
`MapStore` 内部以 `Map<string, LayerState>` 存储所有楼层。
|
||||||
|
`getLayerState(id)` 对不存在的 id 直接返回 `null`,不自动创建。
|
||||||
|
|
||||||
|
只提供一个创建接口:
|
||||||
|
|
||||||
|
- `createLayerState(id: string): ILayerState`:创建并注册一个空白楼层
|
||||||
|
(无任何 `MapLayer`,用户拿到后再调用 `addLayer` 配置图层结构),返回楼层对象。
|
||||||
|
|
||||||
|
注册时若 id 已存在,发出 logger 警告并覆盖。
|
||||||
|
|
||||||
|
若 `compareWith` 已调用后再通过上述接口新增楼层,新楼层直接视为全脏(`dirty = true`),
|
||||||
|
因为 `refData` 中不存在对应数据。
|
||||||
|
|
||||||
|
## 5. 存档数据格式
|
||||||
|
|
||||||
|
### 类型定义
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 单个 MapLayer 的存档数据 */
|
||||||
|
interface IMapLayerSave {
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
/**
|
||||||
|
* key = 行索引,value = 该行完整的 Uint32Array 数据;
|
||||||
|
* NoCompression/LowCompression 时包含所有行(0 到 height - 1);
|
||||||
|
* HighCompression 时只包含与参考基准不同的行;
|
||||||
|
* 读档时,不在此 Map 中的行从参考基准还原。
|
||||||
|
*/
|
||||||
|
readonly rows: ReadonlyMap<number, Uint32Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个楼层的存档数据 */
|
||||||
|
interface ILayerStateSave {
|
||||||
|
readonly background: number;
|
||||||
|
/**
|
||||||
|
* key = zIndex,value = 对应图层存档数据;
|
||||||
|
* 使用 Map 格式以支持图层的动态增删。
|
||||||
|
*/
|
||||||
|
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整个 MapStore 的存档数据 */
|
||||||
|
interface IMapStoreSave {
|
||||||
|
/**
|
||||||
|
* key = 楼层 id,只包含 active 的楼层;
|
||||||
|
* inactive 的楼层不写入,读档时无需处理。
|
||||||
|
*/
|
||||||
|
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 各压缩等级存储策略
|
||||||
|
|
||||||
|
| 压缩级别 | 楼层粒度 | 行粒度 |
|
||||||
|
| ----------------- | --------------------------------------------------------------- | ------------------------ |
|
||||||
|
| `NoCompression` | 存储所有 active 楼层 | 存储该楼层所有行 |
|
||||||
|
| `LowCompression` | 跳过 `dirty = false` 的楼层;dirty 楼层全量比较后仍一致的也跳过 | 存储该楼层所有行 |
|
||||||
|
| `HighCompression` | 同 LowCompression | 只存储与参考基准不同的行 |
|
||||||
|
|
||||||
|
### 读档策略
|
||||||
|
|
||||||
|
读档时直接操作数组引用(通过 `setMapRef`),避免逐行拷贝的额外开销:
|
||||||
|
|
||||||
|
1. 若参考基准(`refData`)未设置,抛出 logger 错误,**不进行任何读档操作**;
|
||||||
|
2. 遍历 `state.floors`,对每个楼层 id:
|
||||||
|
- 若当前 `MapStore` 中不存在该 id,发出 logger 警告并跳过;
|
||||||
|
- 对该楼层每个图层,先从参考基准取出对应 `zIndex` 的数组,
|
||||||
|
将其深拷贝为新数组作为底层(确保未存档行使用参考基准值);
|
||||||
|
- 再将 `ILayerStateSave.layers` 中对应图层的 `rows` 数据写入该数组的对应行;
|
||||||
|
- 调用 `MapLayer.setMapRef(array)` 直接替换内部引用,无需额外拷贝;
|
||||||
|
3. 对未出现在 `state.floors` 中的 active 楼层,
|
||||||
|
从参考基准深拷贝完整数组后调用 `setMapRef` 还原,并将 `dirty` 置为 `false`。
|
||||||
|
|
||||||
|
## 6. saveState / loadState 实现
|
||||||
|
|
||||||
|
根据压缩等级分别编写三个存档函数和三个读档函数,
|
||||||
|
`saveState(compression)` 和 `loadState(state, compression)` 根据 `compression` 分发,
|
||||||
|
无需在每个楼层的遍历循环内部判断等级:
|
||||||
|
|
||||||
|
- `private saveNoCompression(): IMapStoreSave`
|
||||||
|
- `private saveLowCompression(): IMapStoreSave`
|
||||||
|
- `private saveHighCompression(): IMapStoreSave`
|
||||||
|
- `private loadNoCompression(state: IMapStoreSave): void`
|
||||||
|
- `private loadLowCompression(state: IMapStoreSave): void`
|
||||||
|
- `private loadHighCompression(state: IMapStoreSave): void`
|
||||||
|
|
||||||
|
`saveState` 结果需通过 `structuredClone` 深拷贝后返回。
|
||||||
|
|
||||||
|
## 7. IMapStore 接口设计(新增到 `map/types.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||||
|
/** 所有楼层的 id 集合 */
|
||||||
|
readonly maps: ReadonlySet<string>;
|
||||||
|
|
||||||
|
// --- 楼层访问 ---
|
||||||
|
/** 获取指定 id 的楼层状态,不存在则返回 null */
|
||||||
|
getLayerState(id: string): ILayerState | null;
|
||||||
|
/** 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null */
|
||||||
|
getActiveMap(id: string): ILayerState | null;
|
||||||
|
|
||||||
|
// --- 楼层管理 ---
|
||||||
|
/** 创建并注册一个空白楼层,返回楼层状态对象 */
|
||||||
|
createLayerState(id: string): ILayerState;
|
||||||
|
|
||||||
|
// --- active 管理 ---
|
||||||
|
/** 获取指定 id 的楼层是否激活,不存在的 id 返回 false */
|
||||||
|
isMapActive(id: string): boolean;
|
||||||
|
/** 设置指定 id 楼层的激活状态 */
|
||||||
|
setMapActiveStatus(id: string, active: boolean): void;
|
||||||
|
/** 迭代所有 active 的楼层,yield [id, ILayerState] */
|
||||||
|
iterateActiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
/** 迭代所有 inactive 的楼层,yield [id, ILayerState] */
|
||||||
|
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
/** 迭代所有楼层,yield [id, ILayerState] */
|
||||||
|
iterateAllMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
// --- 差分压缩基准 ---
|
||||||
|
/**
|
||||||
|
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
|
||||||
|
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
|
||||||
|
*/
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. ILayerState 接口修改
|
||||||
|
|
||||||
|
在现有 `ILayerState` 上新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 此楼层是否处于激活状态 */
|
||||||
|
readonly active: boolean;
|
||||||
|
/** 设置楼层激活状态 */
|
||||||
|
setActiveStatus(active: boolean): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. IStateBase 修改
|
||||||
|
|
||||||
|
将 `IStateBase.layer: ILayerState` 改为 `IStateBase.layer: IMapStore`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `@user/common/types.ts`: `ISaveableContent`, `SaveCompression`
|
||||||
|
- `@user/data-base/map/types.ts`: 全部现有地图接口(`IMapLayer`, `ILayerState`, 等)
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/types.ts`
|
||||||
|
|
||||||
|
- [x] 新增 `IMapLayerSave` 接口:单个 MapLayer 存档数据格式
|
||||||
|
- [x] 新增 `ILayerStateSave` 接口:单个楼层存档数据格式
|
||||||
|
- [x] 新增 `IMapStoreSave` 接口:MapStore 整体存档数据格式
|
||||||
|
- [x] 修改 `ILayerState`:新增 `readonly active: boolean` 和
|
||||||
|
`setActiveStatus(active: boolean): void`
|
||||||
|
- [x] 修改 `IMapLayer`:新增 `setMapRef(array: Uint32Array): void`
|
||||||
|
- [x] 新增 `IMapStore` 接口:继承 `ISaveableContent<IMapStoreSave>`,
|
||||||
|
含全部接口(见第 7 节)
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/layerState.ts`
|
||||||
|
|
||||||
|
- [x] 新增 `active: boolean = false` 成员:楼层激活状态
|
||||||
|
- [x] 实现 `setActiveStatus(active: boolean): void`
|
||||||
|
- [x] 新增 `private dirty: boolean = false` 成员:楼层级脏标记
|
||||||
|
- [x] 修改 `StateMapLayerHook.onUpdateArea`、`onUpdateBlock`、`onResize`:
|
||||||
|
在转发钩子的同时,将 `state.dirty` 置 `true`
|
||||||
|
- [x] 新增 `isDirty(): boolean` 方法:返回 `this.dirty`,供 `MapStore` 读取
|
||||||
|
- [x] 新增 `setDirty(dirty: boolean): void` 方法:
|
||||||
|
供 `MapStore.compareWith` 时根据实际比较结果设置
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
- [x] 新增 `setMapRef(array: Uint32Array): void` 方法:
|
||||||
|
直接替换内部图块数组引用,跳过拷贝,供 `MapStore` 读档时使用。
|
||||||
|
需确保传入数组长度与 `width × height` 匹配,
|
||||||
|
并触发必要的钩子通知(不触发 `onResize`,应触发 `onUpdateArea` 通知全区域更新)。
|
||||||
|
在方法注释中明确标注:调用后不得再持有或修改传入的数组。
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapStore.ts`(新文件)
|
||||||
|
|
||||||
|
- [x] 实现 `MapStore` 类,实现 `IMapStore`
|
||||||
|
- [x] `private mapData: Map<string, LayerState>`:楼层 id 到状态对象的映射
|
||||||
|
- [x] `readonly maps: ReadonlySet<string>`:所有楼层 id 的只读集合视图
|
||||||
|
- [x] `private refData: Map<string, Map<number, Uint32Array>> | null`:参考基准
|
||||||
|
- [x] 实现 `getLayerState`、`getActiveMap`、`createLayerState`
|
||||||
|
- [x] 实现 `isMapActive`、`setMapActiveStatus`、`iterateActiveMaps`、`iterateInactiveMaps`、`iterateAllMaps`
|
||||||
|
- [x] 实现 `compareWith`
|
||||||
|
- [x] 实现 `saveNoCompression`、`saveLowCompression`、`saveHighCompression`
|
||||||
|
- [x] 实现 `loadNoCompression`、`loadLowCompression`、`loadHighCompression`
|
||||||
|
- [x] 实现 `saveState(compression)` 和 `loadState(state, compression)` 分发
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/index.ts`
|
||||||
|
|
||||||
|
- [x] 补充导出 `mapStore.ts`
|
||||||
|
|
||||||
|
### `@user/data-base/src/types.ts`
|
||||||
|
|
||||||
|
- [x] 将 `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`
|
||||||
103
docs/dev/map/trigger.md
Normal file
103
docs/dev/map/trigger.md
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
当前地图系统已完成图层管理与动态图块的构建,但地图上每个格点可能存在的**事件逻辑**(如战斗、对话、商店等)尚无统一抽象。本需求旨在建立一套触发器系统,将地图事件的注册、收集与触发解耦为三个独立接口层,最终可通过统一的方式触发地图上某一点所有匹配的事件。
|
||||||
|
|
||||||
|
触发器本身不与地图绑定,可独立于地图存在。由于触发的执行依赖 Layer 2 的上下文(`IStateBase` 等),整个触发器系统均归属 Layer 2(`@user/data-system`),地图在 Layer 1 中只存储可序列化的图块数据,不直接持有 `ITrigger`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 接口设计与预期
|
||||||
|
|
||||||
|
## ITrigger
|
||||||
|
|
||||||
|
- `ITrigger.type`:预期频率**低频**,触发器的类型标识符,以数字表示,用于区分不同种类的触发器(如战斗、对话等);一般仅在实例化时通过构造参数传入一次,运行期基本不读写,故为低频。
|
||||||
|
- `ITrigger.priority`:预期频率**低频**,仅在创建触发器时设置一次,用于控制同格点多触发器的执行顺序;绝大多数情况下一个格点只会有一个触发器,多触发器仅为少量拓展场景预留,在 `collect` 场景下,同一次收集中的 `priority` 重复会被视为配置冲突,故为低频。
|
||||||
|
- `ITrigger.trigger(handler)`:预期频率**中频**,用户在脚本中有时会直接持有某个触发器引用并手动触发,但大多数事件触发都通过 `ITriggerCollection.trigger` 完成,直接调用此方法的场合相对有限,故为中频。接受 `ITriggerHandler` 上下文对象(必选 `state: IStateBase`,可选 `layer?: ILayerState`、`mapLayer?: IMapLayer`、`locator?: ITileLocator`),返回 `Promise<void>`。典型使用场景:脚本中已持有某个战斗触发器引用,希望直接触发而不经过格点收集流程。
|
||||||
|
- `ITrigger.collection()`:预期频率**中频**,当用户希望将单个触发器包装为集合并走统一触发流程时使用,场景较为固定但有一定出现概率,故为中频。典型使用场景:脚本中手动构造只含单个触发器的集合,再调用 `collection.trigger(...)` 统一触发。
|
||||||
|
|
||||||
|
## ITriggerRegistry
|
||||||
|
|
||||||
|
以触发器**类型**(`type: number`)为核心提供注册与查询;`registerString` / `getString` 是触发器自身 id 的字符串别名,与地图图块绑定无关,不参与 `collect` 的自动收集流程。共四个方法,不拆分为子接口。
|
||||||
|
|
||||||
|
- `ITriggerRegistry.register(type, factory)`:预期频率**低频**,按触发器类型注册触发器工厂函数(`(type: number) => ITrigger`),由注册表在实例化时将当前 `type` 透传给工厂,使 `ITrigger.type` 跟随注册表,`collect` 时每次调用工厂创建独立实例,一种触发器类型只注册一次,故为低频。
|
||||||
|
- `ITriggerRegistry.get(type)`:预期频率**低频**,主要供 `ITriggerCollector` 内部调用,用户代码几乎不会直接使用,故为低频。
|
||||||
|
- `ITriggerRegistry.registerString(id, factory)`:预期频率**低频**,为触发器注册字符串别名(`() => ITrigger`),仅用于手动按字符串 id 查询,与地图图块收集无关,故为低频。
|
||||||
|
- `ITriggerRegistry.getString(id)`:预期频率**低频**,按字符串 id 查询触发器工厂,调用方手动获取后实例化,主要供内部调用,故为低频。
|
||||||
|
|
||||||
|
## ITriggerCollection
|
||||||
|
|
||||||
|
- `ITriggerCollection.count`:预期频率**低频**,在"当格点无任何触发器时跳过处理"的场景下可快速判断,避免额外迭代,故为低频。
|
||||||
|
- `ITriggerCollection.trigger(handler)`:预期频率**高频**,这是触发器系统对用户暴露的最主要入口,凡是需要触发某格事件的地方都会出现此调用,故为高频。接受 `ITriggerHandler` 上下文对象,顺序异步执行所有触发器,返回 `Promise<void>`。典型使用场景:玩家移动到某格后,移动系统调用 `collection.trigger(handler)` 依次执行该格所有已排序的触发器。
|
||||||
|
- `ITriggerCollection.triggerIter(handler)`:预期频率**低频**,返回 `AsyncGenerator<ITrigger, void, ITriggerHandler | null>`,允许调用方逐个手动推进触发器执行,每次 `next(handler)` 可传入新的上下文(传 `null` 则沿用初始 `handler`),故为低频。典型使用场景:需要在两个触发器之间插入额外效果(如战斗结束后立刻播放特效)的进阶场景。
|
||||||
|
- `ITriggerCollection.iterate()`:预期频率**低频**,仅在需要检查当前集合包含哪些触发器时使用(如 UI 显示交互提示),多数情况下直接触发而无需遍历,故为低频。
|
||||||
|
- `ITriggerCollection.push(trigger)`:预期频率**低频**,在脚本中偶尔需要向已有集合末尾追加一个触发器(如特殊演出追加额外效果),直接插入末尾不重新排序,故为低频。
|
||||||
|
- `ITriggerCollection.unshift(trigger)`:预期频率**低频**,与 `push` 对称,向集合头部强制插入触发器,直接插入头部不重新排序,场景更为罕见,故为低频。
|
||||||
|
- `ITriggerCollection.concat(...others)`:预期频率**低频**,将当前集合与一个或多个其他集合按自身在前、传入参数依序在后的顺序合并为新集合,不重新排序。此接口需求存疑,先行提供,故为低频。
|
||||||
|
|
||||||
|
## ITriggerCollector
|
||||||
|
|
||||||
|
`ITriggerCollector` 独立存在于 Layer 2(`@user/data-system`),不附属于 `ILayerState`。调用方持有 collector 引用,调用 `collect(x, y, layer)` 时显式传入目标图层(`IMapLayer`)。收集时会依赖后续由 `IMapLayer.getTileInfo` 暴露的图块信息,其中包含触发器类型。引擎层会利用 `ILayerState.eventLayer` 自动调用 `collect` 实现默认收集行为,该行为不在本接口设计范围内;若需对多个图层收集,调用方自行多次调用并合并结果,合并顺序、跨层排序结果与跨层冲突处理均由调用方自行决定。
|
||||||
|
|
||||||
|
- `ITriggerCollector.collect(x, y, layer)`:预期频率**中频**,在移动系统或交互系统中需要确认某格事件时调用,使用场景固定且有一定频率,故为中频。`layer` 为必选,调用方显式指定要收集的目标图层(`IMapLayer`)。典型使用场景:玩家向某格移动时,移动系统持有 collector 引用并调用 `collector.collect(x, y, layer)` 获取目标格的 `ITriggerCollection`,再决定是否阻断移动或直接触发。
|
||||||
|
- `ITriggerCollector.attachRegistry(registry)`:预期频率**低频**,仅在初始化或切换注册表时调用,故为低频。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 触发器对象 ITrigger
|
||||||
|
|
||||||
|
`ITrigger` 代表一类事件逻辑(如"战斗触发器"、"对话触发器"),是整个系统的原子单元。`type` 成员以数字标识触发器种类,在常规注册流程下由注册表在实例化时通过构造参数注入;`priority` 控制同格点的执行顺序,并在 `collect` 场景下充当唯一执行位。触发器本身是极其宽泛的东西,一般只有几种类型,比如战斗、触发系统事件、触发自定义事件等,不会每种战斗和每种对话都开一种触发器。触发时接受外部传入的 `ITriggerHandler` 上下文参数,执行对应逻辑。
|
||||||
|
|
||||||
|
`ITrigger` 提供 `collection()` 方法,将单个触发器包装为 `ITriggerCollection`,供不需要收集步骤的场景直接使用。
|
||||||
|
|
||||||
|
## 2. 触发器注册接口 ITriggerRegistry
|
||||||
|
|
||||||
|
触发器以类型为单位注册。为保证每个收集场景下的 `ITrigger` 实例相互独立(触发器可在内部保存状态而不互相影响),注册时传入工厂函数 `(type: number) => ITrigger`,由注册表在创建实例时将当前类型透传给工厂,使实例上的 `ITrigger.type` 跟随注册表;`collect` 时按需调用工厂创建新实例。字符串别名注册仍用于手动查询场景,不参与 `collect` 自动收集。`ITriggerRegistry` 直接包含以下四个方法:
|
||||||
|
|
||||||
|
- `register(type: number, factory: (type: number) => ITrigger)`:按触发器类型注册触发器工厂
|
||||||
|
- `get(type: number)`:按触发器类型查询触发器工厂
|
||||||
|
- `registerString(id: string, factory: () => ITrigger)`:为触发器注册字符串别名(仅用于手动查询,不参与 `collect` 自动收集)
|
||||||
|
- `getString(id: string)`:按字符串 id 查询触发器工厂
|
||||||
|
|
||||||
|
每个 key 只对应一个工厂。重复注册同一 key 时,发出警告并以新工厂覆盖旧的。
|
||||||
|
|
||||||
|
## 3. 触发收集 ITriggerCollector
|
||||||
|
|
||||||
|
`ITriggerCollector` 属于 Layer 2(`@user/data-system`),独立于 `ILayerState` 存在,不挂载到任何 Layer 1 接口上。地图在 Layer 1 中只存储可序列化的图块数据(如图块编号),不持有 `ITrigger` 实例。收集时,调用方显式传入目标图层 `layer`(`IMapLayer`),`ITriggerCollector` 读取该图层中该格点的图块数据——包括静态图块(通过 `IMapLayer.getBlock`)以及动态图块(通过 `IMapLayer.dynamicLayer`)——并结合后续由 `IMapLayer.getTileInfo` 暴露的图块信息读取其中记录的触发器类型,再通过 `ITriggerRegistry` 获取工厂并构造对应的 `ITrigger`,收集后排序,返回 `ITriggerCollection`。注册表通过 `attachRegistry(registry)` 设置。
|
||||||
|
|
||||||
|
排序规则:按触发器自身 `priority` 降序排列。这里的 `priority` 不仅表示执行顺序,也表示单次收集内的唯一执行位;正常情况下同一坐标只会有一个触发器,多触发器支持仅用于极少数拓展场景。收集阶段若发现两个或以上触发器的 `priority` 相同,则视为配置冲突,发出警告,并将本次结果中所有该优先级的触发器全部剔除,不进入 `ITriggerCollection`。
|
||||||
|
|
||||||
|
## 4. 触发器集合 ITriggerCollection
|
||||||
|
|
||||||
|
`ITriggerCollection` 是一组已排序的 `ITrigger` 的载体,提供统一的触发入口 `trigger(...)` 与迭代方法 `iterate()`、数量属性 `count`。`push` / `unshift` / `concat` 均不对集合重新排序,语义为"强制指定执行位置",与 `priority` 排序体系完全分离(`ITriggerCollection` 本身与地图无关,不应自行排序)。可由:
|
||||||
|
|
||||||
|
- `ITriggerCollector.collect` 返回;
|
||||||
|
- `ITrigger.collection()` 方法直接构造(单触发器集合)。
|
||||||
|
|
||||||
|
触发执行策略:`trigger(handler)` 顺序异步执行(返回 `Promise<void>`),触发器之间串行等待;`triggerIter(handler)` 返回异步生成器,调用方可逐个推进执行并为每一步单独传入上下文,适用于需要在触发器间插入额外逻辑的进阶场景。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `@user/data-base`(跨包引用):`@user/data-system/trigger/types.ts` 中的 `ITriggerHandler` 需要引用 `IStateBase`(全局状态)、`ILayerState`(楼层状态)、`IMapLayer`(图层)与 `ITileLocator`;`ITriggerCollector.collect` 同样需要引用 `IMapLayer`,并依赖其后续提供的 `getTileInfo` 读取图块信息中的触发器类型
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base` 中的 `ILayerState`
|
||||||
|
|
||||||
|
- [x] 新增 `readonly eventLayer: IMapLayer | null` 属性:表示该楼层的默认事件图层,供引擎默认收集行为使用(引擎默认收集逻辑不在本设计范围内)
|
||||||
|
- [x] 新增 `setEventLayer(layer: IMapLayer | null): void` 方法:设置默认事件图层
|
||||||
|
|
||||||
|
## 需要新建的文件
|
||||||
|
|
||||||
|
### `@user/data-system/trigger/types.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `ITriggerHandler` 接口:触发时传入的上下文对象,包含必选的 `state: IStateBase` 以及可选的 `layer?: ILayerState`、`mapLayer?: IMapLayer`、`locator?: ITileLocator`
|
||||||
|
- [ ] 新增 `ITrigger` 接口:触发器原子对象,代表一类事件逻辑;包含 `type: number` 类型标识、`priority` 优先级成员、`trigger(handler: ITriggerHandler): Promise<void>` 触发方法与 `collection()` 方法以便包装为集合
|
||||||
|
- [ ] 新增 `ITriggerRegistry` 接口:包含 `register(type, factory: (type: number) => ITrigger)` `get(type)` `registerString(id, factory: () => ITrigger)` `getString(id)` 四个方法;以触发器类型 `type` 为核心管理注册与查询,字符串 id 为触发器自身别名,不参与 `collect` 自动收集;数字注册路径下由注册表透传 `type` 给工厂,保证实例的 `ITrigger.type` 与注册项一致
|
||||||
|
- [ ] 新增 `ITriggerCollection` 接口:包含已排序的触发器集合,提供 `trigger(handler)` 顺序异步触发入口(`Promise<void>`)与 `triggerIter(handler)` 异步迭代触发入口(`AsyncGenerator<ITrigger, void, ITriggerHandler | null>`)、`iterate()` 迭代方法、`count` 数量属性、`push` / `unshift` 向集合末尾或头部插入(不重排序)的方法,以及 `concat(...others)` 按自身在前的顺序合并多个集合(不重排序)的方法
|
||||||
|
- [ ] 新增 `ITriggerCollector` 接口:包含 `collect(x: number, y: number, layer: IMapLayer)` 收集方法与 `attachRegistry(registry: ITriggerRegistry | null)` 注册表设置方法
|
||||||
215
docs/dev/system/save-system.md
Normal file
215
docs/dev/system/save-system.md
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
实现游戏引擎存档系统(`SaveSystem`)及全局事务(`GlobalTransaction`)两个类的完整逻辑。
|
||||||
|
存档系统分为两部分:
|
||||||
|
|
||||||
|
- **存档内容**:按 slot id 保存/读取游戏当前状态(`Map<string, ISaveableContent<unknown>>`),
|
||||||
|
使用 Dexie 将数据存入 IndexedDB。
|
||||||
|
- **全局存储**:跨存档的 key-value 存储,用于存放存档 meta data、全局设置等。
|
||||||
|
支持事务处理,事务中的写入操作在发生错误时全部回滚。
|
||||||
|
|
||||||
|
此外,系统提供基于内存的自动存档,并支持 undo/redo 操作。
|
||||||
|
自动存档不主动写入 IndexedDB,只有显式调用 `saveAutosaveToDB` 时才将 undo
|
||||||
|
栈顶存档写入数据库。
|
||||||
|
存档操作使用 `performance` 接口监控耗时,超过配置阈值时通过 logger 发出警告。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. Dexie 数据库 Schema 设计
|
||||||
|
|
||||||
|
`SaveSystem` 构造函数接收数据库名称 `name`,在其中创建如下两张表:
|
||||||
|
|
||||||
|
| 表名 | 主键 | 说明 |
|
||||||
|
| -------- | --------------- | --------------------------------------------------------- |
|
||||||
|
| `saves` | `id`(number) | 按 slot id 存储存档数据;`id = -1` 固定用于持久化自动存档 |
|
||||||
|
| `global` | `key`(string) | 全局 key-value 存储 |
|
||||||
|
|
||||||
|
- `saves` 表中每条记录结构为 `{ id: number, compression: SaveCompression, data: Map<string, unknown> }`,
|
||||||
|
与 `ISaveRead` 对应,直接利用 IndexedDB 的结构化克隆存储,不进行 JSON 序列化。
|
||||||
|
- `global` 表中每条记录结构为 `{ key: string, value: unknown }`,
|
||||||
|
同样直接存储,不进行 JSON 序列化。
|
||||||
|
|
||||||
|
## 2. 内部状态
|
||||||
|
|
||||||
|
`SaveSystem` 需要维护如下私有成员:
|
||||||
|
|
||||||
|
- `private undoStack: ISaveRead[]`:undo 栈,存储 `ISaveRead` 快照
|
||||||
|
- `private redoStack: ISaveRead[]`:redo 栈,存储 `ISaveRead` 快照
|
||||||
|
- `private stackSize: number`:undo/redo 栈最大容量(默认 `20`)
|
||||||
|
- `private autosaveLevel: SaveCompression`:默认 `SaveCompression.LowCompression`
|
||||||
|
- `private commonSaveLevel: SaveCompression`:默认 `SaveCompression.HighCompressoin`
|
||||||
|
- `private saveTimeTolerance: number`:默认 `100`(ms)
|
||||||
|
- `private autosaveTimeTolerance: number`:默认 `50`(ms)
|
||||||
|
|
||||||
|
## 3. ISaveRead 数据结构
|
||||||
|
|
||||||
|
栈与数据库读写均使用新接口 `ISaveRead`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ISaveRead {
|
||||||
|
readonly compression: SaveCompression;
|
||||||
|
readonly data: Map<string, unknown>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `compression`:存档时使用的压缩等级,读档时传回给 `loadState`,使接收方能够正确解压。
|
||||||
|
- `data`:key 到每个可存档对象序列化数据的 Map,key 与 `ISaveableContent` 注册时的 id 对应。
|
||||||
|
|
||||||
|
内存栈和数据库均直接存储 `ISaveRead`,不需要引入辅助包装层。
|
||||||
|
存档系统本身不负责将数据写回游戏对象,调用方拿到 `ISaveRead` 后自行遍历并调用
|
||||||
|
各可存档对象的 `loadState(data, compression)` 完成状态恢复。
|
||||||
|
|
||||||
|
## 4. 各方法实现
|
||||||
|
|
||||||
|
### `config(config)`
|
||||||
|
|
||||||
|
将 config 各字段写入对应私有成员,使用传入的值覆盖默认值。
|
||||||
|
|
||||||
|
### `setAutosaveStackSize(size)`
|
||||||
|
|
||||||
|
将 `stackSize` 更新为 `size`。如果当前 undo 栈超过新的 `size`,
|
||||||
|
从栈底移除多余条目(保留最新的);redo 栈同理。
|
||||||
|
|
||||||
|
### `autosave(state)`
|
||||||
|
|
||||||
|
1. 遍历 `state`,对每个 `(key, content)` 调用
|
||||||
|
`content.saveState(this.autosaveLevel)` 获取序列化数据,
|
||||||
|
汇总为 `Map<string, unknown>`,构建 `ISaveRead { compression: autosaveLevel, data }` 并压入 `undoStack`;
|
||||||
|
2. **清空 `redoStack`**(执行新的自动存档后无法再 redo);
|
||||||
|
3. 若 `undoStack.length > stackSize`,从栈底(`[0]`)移除多余条目。
|
||||||
|
|
||||||
|
> IndexedDB 支持结构化克隆,Map、Set、TypedArray 等均可直接存储,无需 JSON 序列化。
|
||||||
|
|
||||||
|
### `undoAutosave(current)`
|
||||||
|
|
||||||
|
1. 若 `undoStack` 为空,返回 `null`;
|
||||||
|
2. 将 `current` 序列化为 `ISaveRead { compression: autosaveLevel, data: Map }`,
|
||||||
|
压入 `redoStack`;检查 `redoStack.length > stackSize`,超长时从栈底移除多余条目;
|
||||||
|
3. 弹出 `undoStack` 栈顶(`pop()`),返回弹出的 `ISaveRead`;
|
||||||
|
4. 调用方拿到返回的 `ISaveRead` 后,自行遍历并对各游戏对象调用 `loadState` 完成恢复。
|
||||||
|
|
||||||
|
### `redoAutosave(current)`
|
||||||
|
|
||||||
|
与 `undoAutosave` 逻辑对称:将 `current` 序列化压入 `undoStack`,
|
||||||
|
弹出 `redoStack` 栈顶并返回,调用方自行恢复状态。
|
||||||
|
|
||||||
|
### `getUndoStack()` / `getRedoStack()`
|
||||||
|
|
||||||
|
使用 `slice()` 返回栈数组的浅拷贝快照,防止外部意外修改栈结构。
|
||||||
|
|
||||||
|
### `saveAutosaveToDB()`
|
||||||
|
|
||||||
|
1. 若 `undoStack` 为空,直接返回(无需写入);
|
||||||
|
2. 记录 `t0 = performance.now()`;
|
||||||
|
3. 取 `undoStack` 栈顶(`ISaveRead`),将其连同 `id = -1` 一起写入 `saves` 表;
|
||||||
|
4. 记录 `t1 = performance.now()`;若 `t1 - t0 > autosaveTimeTolerance`,
|
||||||
|
调用 `logger.warn(115, (t1 - t0).toFixed(0), this.autosaveTimeTolerance.toString())`。
|
||||||
|
|
||||||
|
### `save(id, state)`
|
||||||
|
|
||||||
|
1. 记录 `t0 = performance.now()`;
|
||||||
|
2. 遍历 `state`,对每个 `(key, content)` 调用
|
||||||
|
`content.saveState(this.commonSaveLevel)` 汇总为 `Map<string, unknown>`,
|
||||||
|
构建 `{ id, compression: commonSaveLevel, data }` 写入 `saves` 表;
|
||||||
|
3. 将 `id` 写入全局存储 `'lastSlot'` 键(用于 `getLastSlot()`);
|
||||||
|
4. 记录 `t1 = performance.now()`;若 `t1 - t0 > saveTimeTolerance`,
|
||||||
|
调用 `logger.warn(114, (t1 - t0).toFixed(0), this.saveTimeTolerance.toString())`。
|
||||||
|
|
||||||
|
### `load(id)`
|
||||||
|
|
||||||
|
1. 从 Dexie `saves` 表查询 `id`;
|
||||||
|
2. 若不存在返回 `null`;
|
||||||
|
3. 将读取到的记录中的 `compression` 和 `data` 字段组装成 `ISaveRead` 返回。
|
||||||
|
调用方自行遍历 `data` 并对各游戏对象调用 `loadState` 完成恢复。
|
||||||
|
|
||||||
|
> `load(-1)` 可用于读取持久化的自动存档。
|
||||||
|
|
||||||
|
### `deleteSave(id)`
|
||||||
|
|
||||||
|
直接从 Dexie `saves` 表删除对应记录。
|
||||||
|
|
||||||
|
### `getLastSlot()`
|
||||||
|
|
||||||
|
从全局存储读取 `'lastSlot'` 键对应的值并返回;若不存在则返回 `0`。
|
||||||
|
|
||||||
|
### `getGlobal<T>(key)` / `setGlobal(key, value)`
|
||||||
|
|
||||||
|
- `getGlobal`:从 Dexie `global` 表读取 `key` 对应的 `value` 字段并返回,类型断言为 `T`;
|
||||||
|
- `setGlobal`:将 `{ key, value }` 直接写入 Dexie `global` 表,无需 JSON 序列化。
|
||||||
|
|
||||||
|
### `startGlobalTransaction<R>(handle)`
|
||||||
|
|
||||||
|
使用 `Dexie.transaction('rw', this.db.table('global'), ...)` 包裹 `handle` 调用,
|
||||||
|
传入 `GlobalTransaction` 实例,出错时自动回滚。
|
||||||
|
|
||||||
|
### `GlobalTransaction.get<T>(key)` / `GlobalTransaction.set(key, value)`
|
||||||
|
|
||||||
|
在事务上下文中直接读写 `table`(即全局 `global` 表的引用),无需 JSON 序列化。
|
||||||
|
|
||||||
|
## 5. logger.json 新增 warn 代码
|
||||||
|
|
||||||
|
当前最大 warn 代码为 `113`,新增如下两条(写入 `packages/common/src/logger.json`
|
||||||
|
的 `warn` 对象,置于 `113` 之后):
|
||||||
|
|
||||||
|
| 代码 | 消息 |
|
||||||
|
| ----- | ----------------------------------------------------------------------------------------------------- |
|
||||||
|
| `114` | `Save operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.` |
|
||||||
|
| `115` | `Autosave operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `dexie`:Dexie / Table 类型,用于创建和操作 IndexedDB 数据库
|
||||||
|
- `@motajs/common`:`logger`,用于输出存档耗时超限警告
|
||||||
|
- `@user/data-base`:`ISaveableContent`、`SaveCompression`,存档接口与压缩枚举
|
||||||
|
- `./types`:`ISaveRead`、`IGlobalTrasaction`、`ISaveSystem`、`ISaveSystemConfig`
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `packages/common/src/logger.json`
|
||||||
|
|
||||||
|
- [x] 在 `warn` 对象中新增代码 `114`:普通存档耗时超限警告
|
||||||
|
- [x] 在 `warn` 对象中新增代码 `115`:自动存档耗时超限警告
|
||||||
|
|
||||||
|
### `packages-user/data-state/src/save/system.ts`
|
||||||
|
|
||||||
|
- [x] 新增 `private undoStack: ISaveRead[]` 成员:存储 undo 历史快照
|
||||||
|
- [x] 新增 `private redoStack: ISaveRead[]` 成员:存储 redo 历史快照
|
||||||
|
- [x] 新增 `private stackSize: number` 成员:undo/redo 栈容量上限,默认 `20`
|
||||||
|
- [x] 新增 `private autosaveLevel: SaveCompression` 成员:
|
||||||
|
默认 `SaveCompression.LowCompression`
|
||||||
|
- [x] 新增 `private commonSaveLevel: SaveCompression` 成员:
|
||||||
|
默认 `SaveCompression.HighCompressoin`
|
||||||
|
- [x] 新增 `private saveTimeTolerance: number` 成员:普通存档耗时阈值,默认 `100`
|
||||||
|
- [x] 新增 `private autosaveTimeTolerance: number` 成员:自动存档耗时阈值,默认 `50`
|
||||||
|
- [x] 编写构造函数:初始化 Dexie 实例,定义 `saves`(主键 `id`)和
|
||||||
|
`global`(主键 `key`)两张表的 schema
|
||||||
|
- [x] 编写 `config` 方法:将配置项写入私有成员
|
||||||
|
- [x] 编写 `setAutosaveStackSize` 方法:更新 stackSize,修剪超长的 undo/redo 栈
|
||||||
|
- [x] 编写 `autosave` 方法:遍历 state 序列化为 `ISaveRead` 压入 undoStack,
|
||||||
|
清空 redoStack,超长时修剪栈底
|
||||||
|
- [x] 编写 `undoAutosave` 方法:将 current 序列化为 `ISaveRead` 压入 redoStack,
|
||||||
|
弹出 undoStack 栈顶返回 `ISaveRead`(或 null)
|
||||||
|
- [x] 编写 `redoAutosave` 方法:与 undoAutosave 对称
|
||||||
|
- [x] 编写 `getUndoStack` / `getRedoStack` 方法:使用 `slice()` 返回栈的浅拷贝快照
|
||||||
|
- [x] 编写 `saveAutosaveToDB` 方法:取 undoStack 栈顶以 `id = -1` 写入 `saves` 表,
|
||||||
|
performance 监控,超限时调用 `logger.warn(115, ...)`
|
||||||
|
- [x] 编写 `save` 方法:遍历 state 序列化为 `ISaveRead` 写入 `saves` 表,
|
||||||
|
更新 `lastSlot`,performance 监控,超限时调用 `logger.warn(114, ...)`
|
||||||
|
- [x] 编写 `load` 方法:从 Dexie `saves` 表读取记录组装为 `ISaveRead` 返回
|
||||||
|
(不存在返回 null);`load(-1)` 可读取持久化的自动存档
|
||||||
|
- [x] 编写 `deleteSave` 方法:从 Dexie `saves` 表删除指定记录
|
||||||
|
- [x] 编写 `getLastSlot` 方法:从全局存储读取 `'lastSlot'`,不存在时返回 `0`
|
||||||
|
- [x] 编写 `getGlobal` / `setGlobal` 方法:直接读写 Dexie `global` 表,不进行 JSON 序列化
|
||||||
|
- [x] 编写 `startGlobalTransaction` 方法:
|
||||||
|
使用 Dexie 事务包裹 handle,传入 GlobalTransaction 实例
|
||||||
|
|
||||||
|
### `packages-user/data-state/src/save/system.ts`(GlobalTransaction 部分)
|
||||||
|
|
||||||
|
- [x] 编写 `GlobalTransaction.get` 方法:在事务上下文中直接读取 table 中 key 对应的 value
|
||||||
|
- [x] 编写 `GlobalTransaction.set` 方法:在事务上下文中直接写入 key-value,不进行 JSON 序列化
|
||||||
@ -114,7 +114,7 @@ export class ClientCore implements IClientCore {
|
|||||||
// 使用分频器,用户可以在设置中调整,如果设备性能较差调高分频有助于提高性能表现
|
// 使用分频器,用户可以在设置中调整,如果设备性能较差调高分频有助于提高性能表现
|
||||||
excitaion: excitationDivider
|
excitaion: excitationDivider
|
||||||
});
|
});
|
||||||
this.mainMapRenderer = new MapRenderer(this.materials, data.layer);
|
this.mainMapRenderer = new MapRenderer(this.materials, data.maps);
|
||||||
this.mainMapExtension = new MapExtensionManager(this.mainMapRenderer);
|
this.mainMapExtension = new MapExtensionManager(this.mainMapRenderer);
|
||||||
|
|
||||||
// 兼容层
|
// 兼容层
|
||||||
@ -133,7 +133,7 @@ export class ClientCore implements IClientCore {
|
|||||||
await this.materials.trackedAsset.then();
|
await this.materials.trackedAsset.then();
|
||||||
|
|
||||||
this.mainMapRenderer.useAsset(this.materials.trackedAsset);
|
this.mainMapRenderer.useAsset(this.materials.trackedAsset);
|
||||||
const layer = this.data.layer.getLayerByAlias('event');
|
const layer = this.data.maps.getLayerByAlias('event');
|
||||||
if (layer) {
|
if (layer) {
|
||||||
this.mainMapExtension.addHero(this.data.hero.mover, layer);
|
this.mainMapExtension.addHero(this.data.hero.mover, layer);
|
||||||
this.mainMapExtension.addDoor(layer);
|
this.mainMapExtension.addDoor(layer);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
||||||
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
|
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
|
||||||
import { ILayerState } from '@user/data-state';
|
import { ILayerState } from '@user/data-base';
|
||||||
import { IMapExtensionManager, IMapRenderer } from '../map';
|
import { IMapExtensionManager, IMapRenderer } from '../map';
|
||||||
|
|
||||||
export interface IconProps extends BaseProps {
|
export interface IconProps extends BaseProps {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
|
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
|
||||||
import { ILayerState } from '@user/data-state';
|
import { ILayerState } from '@user/data-base';
|
||||||
import { IMapRenderer } from './types';
|
import { IMapRenderer } from './types';
|
||||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||||
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared';
|
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {
|
|||||||
IMapLayer,
|
IMapLayer,
|
||||||
IMapLayerHookController,
|
IMapLayerHookController,
|
||||||
IMapLayerHooks
|
IMapLayerHooks
|
||||||
} from '@user/data-state';
|
} from '@user/data-base';
|
||||||
import { IMapDoorRenderer } from './types';
|
import { IMapDoorRenderer } from './types';
|
||||||
import { IMapRenderer } from '../types';
|
import { IMapRenderer } from '../types';
|
||||||
import { sleep } from 'mutate-animate';
|
import { sleep } from 'mutate-animate';
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
IHeroMovingHooks,
|
IHeroMovingHooks,
|
||||||
nextFaceDirection
|
nextFaceDirection
|
||||||
} from '@user/data-base';
|
} from '@user/data-base';
|
||||||
import { IMapLayer, state } from '@user/data-state';
|
import { IMapLayer } from '@user/data-base';
|
||||||
import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
|
import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { IHookController, logger } from '@motajs/common';
|
import { IHookController, logger } from '@motajs/common';
|
||||||
@ -15,6 +15,7 @@ import { BlockCls, IMaterialFramedData } from '@user/client-base';
|
|||||||
import { ITexture, ITextureSplitter, TextureRowSplitter } from '@motajs/render';
|
import { ITexture, ITextureSplitter, TextureRowSplitter } from '@motajs/render';
|
||||||
import { IMapHeroRenderer } from './types';
|
import { IMapHeroRenderer } from './types';
|
||||||
import { TimingFn } from 'mutate-animate';
|
import { TimingFn } from 'mutate-animate';
|
||||||
|
import { state } from '@user/data-state';
|
||||||
|
|
||||||
/** 默认的移动时长 */
|
/** 默认的移动时长 */
|
||||||
const DEFAULT_TIME = 100;
|
const DEFAULT_TIME = 100;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { IHeroMover } from '@user/data-base';
|
import { IHeroMover } from '@user/data-base';
|
||||||
import { IMapLayer } from '@user/data-state';
|
import { IMapLayer } from '@user/data-base';
|
||||||
import {
|
import {
|
||||||
IMapDoorRenderer,
|
IMapDoorRenderer,
|
||||||
IMapExtensionManager,
|
IMapExtensionManager,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
HeroAnimateDirection,
|
HeroAnimateDirection,
|
||||||
IHeroMover
|
IHeroMover
|
||||||
} from '@user/data-base';
|
} from '@user/data-base';
|
||||||
import { IMapLayer } from '@user/data-state';
|
import { IMapLayer } from '@user/data-base';
|
||||||
|
|
||||||
import { IMapRenderResult } from '../types';
|
import { IMapRenderResult } from '../types';
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { linear, TimingFn } from 'mutate-animate';
|
|||||||
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
|
import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types';
|
||||||
import { IMaterialFramedData, IMaterialManager } from '@user/client-base';
|
import { IMaterialFramedData, IMaterialManager } from '@user/client-base';
|
||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import { IMapLayer } from '@user/data-state';
|
import { IMapLayer } from '@user/data-base';
|
||||||
import { DynamicBlockStatus } from './status';
|
import { DynamicBlockStatus } from './status';
|
||||||
|
|
||||||
export interface IMovingRenderer {
|
export interface IMovingRenderer {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
MapTileBehavior,
|
MapTileBehavior,
|
||||||
MapTileSizeTestMode
|
MapTileSizeTestMode
|
||||||
} from './types';
|
} from './types';
|
||||||
import { ILayerState, ILayerStateHooks, IMapLayer } from '@user/data-state';
|
import { ILayerState, ILayerStateHooks, IMapLayer } from '@user/data-base';
|
||||||
import { IHookController, logger } from '@motajs/common';
|
import { IHookController, logger } from '@motajs/common';
|
||||||
import { compileProgramWith } from '@motajs/client-base';
|
import { compileProgramWith } from '@motajs/client-base';
|
||||||
import { isNil, maxBy } from 'lodash-es';
|
import { isNil, maxBy } from 'lodash-es';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IMapLayer } from '@user/data-state';
|
import { IMapLayer } from '@user/data-base';
|
||||||
import { IBlockStatus, IMapVertexStatus } from './types';
|
import { IBlockStatus, IMapVertexStatus } from './types';
|
||||||
|
|
||||||
export class StaticBlockStatus implements IBlockStatus {
|
export class StaticBlockStatus implements IBlockStatus {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
IMaterialManager,
|
IMaterialManager,
|
||||||
ITrackedAssetData
|
ITrackedAssetData
|
||||||
} from '@user/client-base';
|
} from '@user/client-base';
|
||||||
import { ILayerState, IMapLayer } from '@user/data-state';
|
import { ILayerState, IMapLayer } from '@user/data-base';
|
||||||
import { TimingFn } from 'mutate-animate';
|
import { TimingFn } from 'mutate-animate';
|
||||||
|
|
||||||
export const enum MapBackgroundRepeat {
|
export const enum MapBackgroundRepeat {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IMapLayer } from '@user/data-state';
|
import { IMapLayer } from '@user/data-base';
|
||||||
import {
|
import {
|
||||||
IBlockData,
|
IBlockData,
|
||||||
IBlockSplitter,
|
IBlockSplitter,
|
||||||
|
|||||||
@ -243,7 +243,7 @@ const MainScene = defineComponent(() => {
|
|||||||
>
|
>
|
||||||
<map-render
|
<map-render
|
||||||
renderer={mainMapRenderer}
|
renderer={mainMapRenderer}
|
||||||
layerState={state.layer}
|
layerState={state.maps}
|
||||||
extension={mainMapExtension}
|
extension={mainMapExtension}
|
||||||
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
loc={[0, 0, MAP_WIDTH, MAP_HEIGHT]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
282
packages-user/data-base/src/common/faceManager.ts
Normal file
282
packages-user/data-base/src/common/faceManager.ts
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import { FaceDirection } from './types';
|
||||||
|
|
||||||
|
//#region 接口与枚举
|
||||||
|
|
||||||
|
export const enum InternalFaceGroup {
|
||||||
|
/** 四方向(上下左右) */
|
||||||
|
Dir4,
|
||||||
|
/** 八方向(上下左右+斜向) */
|
||||||
|
Dir8
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFaceDescriptor {
|
||||||
|
/** 横坐标增量 */
|
||||||
|
readonly x: number;
|
||||||
|
/** 纵坐标增量 */
|
||||||
|
readonly y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFaceHandler<T extends number> {
|
||||||
|
/**
|
||||||
|
* 将任意朝向值降级为本组支持的方向。
|
||||||
|
* 对于无法合理降级的方向(包括 `Unknown`),返回 `FaceDirection.Unknown`
|
||||||
|
* @param dir 任意朝向值
|
||||||
|
*/
|
||||||
|
degrade(dir: number): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定方向的单步坐标偏移量,输入先经过 `degrade`
|
||||||
|
* @param dir 朝向
|
||||||
|
*/
|
||||||
|
movement(dir: number): IFaceDescriptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定方向走 `count` 步的坐标偏移量,等价于 `movement * count`。
|
||||||
|
* `count` 允许为负数,表示反向位移,输入先经过 `degrade`
|
||||||
|
* @param dir 朝向
|
||||||
|
* @param count 步数,允许为负
|
||||||
|
*/
|
||||||
|
move(dir: number, count: number): IFaceDescriptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本组内的反方向,输入先经过 `degrade`,`Unknown` 返回 `Unknown`
|
||||||
|
* @param dir 朝向
|
||||||
|
*/
|
||||||
|
opposite(dir: number): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在本组方向集合内顺时针(默认)或逆时针旋转一步,输入先经过 `degrade`,
|
||||||
|
* `Unknown` 返回 `Unknown`
|
||||||
|
* @param dir 朝向
|
||||||
|
* @param anticlockwise 是否逆时针,默认顺时针
|
||||||
|
*/
|
||||||
|
next(dir: number, anticlockwise?: boolean): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代本组支持的所有朝向,包含 `Unknown`
|
||||||
|
*/
|
||||||
|
mapDirection(): Iterable<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代本组所有朝向及其对应的坐标描述器,包含 `Unknown`(对应 `{ x: 0, y: 0 }`)
|
||||||
|
*/
|
||||||
|
mapMovement(): Iterable<[T, IFaceDescriptor]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFaceManager {
|
||||||
|
/**
|
||||||
|
* 以数字 key 注册一个 handler
|
||||||
|
* @param group 数字 key
|
||||||
|
* @param handler 朝向处理器
|
||||||
|
*/
|
||||||
|
register(group: number, handler: IFaceHandler<number>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 以字符串 id 注册一个 handler
|
||||||
|
* @param id 字符串 id
|
||||||
|
* @param handler 朝向处理器
|
||||||
|
*/
|
||||||
|
registerById(id: string, handler: IFaceHandler<number>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按数字 key 查找 handler,未找到返回 `null`
|
||||||
|
* @param group 数字 key
|
||||||
|
*/
|
||||||
|
get<T extends number>(group: number): IFaceHandler<T> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字符串 id 查找 handler,未找到返回 `null`
|
||||||
|
* @param id 字符串 id
|
||||||
|
*/
|
||||||
|
getById<T extends number>(id: string): IFaceHandler<T> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 内置 Handler
|
||||||
|
|
||||||
|
const ZERO_DESCRIPTOR: IFaceDescriptor = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
const DIR8_MOVEMENTS: ReadonlyMap<FaceDirection, IFaceDescriptor> = new Map([
|
||||||
|
[FaceDirection.Unknown, ZERO_DESCRIPTOR],
|
||||||
|
[FaceDirection.Left, { x: -1, y: 0 }],
|
||||||
|
[FaceDirection.Up, { x: 0, y: -1 }],
|
||||||
|
[FaceDirection.Right, { x: 1, y: 0 }],
|
||||||
|
[FaceDirection.Down, { x: 0, y: 1 }],
|
||||||
|
[FaceDirection.LeftUp, { x: -1, y: -1 }],
|
||||||
|
[FaceDirection.RightUp, { x: 1, y: -1 }],
|
||||||
|
[FaceDirection.LeftDown, { x: -1, y: 1 }],
|
||||||
|
[FaceDirection.RightDown, { x: 1, y: 1 }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 顺时针旋转顺序(不含 Unknown) */
|
||||||
|
const DIR8_CW: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Up, FaceDirection.RightUp],
|
||||||
|
[FaceDirection.RightUp, FaceDirection.Right],
|
||||||
|
[FaceDirection.Right, FaceDirection.RightDown],
|
||||||
|
[FaceDirection.RightDown, FaceDirection.Down],
|
||||||
|
[FaceDirection.Down, FaceDirection.LeftDown],
|
||||||
|
[FaceDirection.LeftDown, FaceDirection.Left],
|
||||||
|
[FaceDirection.Left, FaceDirection.LeftUp],
|
||||||
|
[FaceDirection.LeftUp, FaceDirection.Up]
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** 逆时针旋转顺序(不含 Unknown) */
|
||||||
|
const DIR8_CCW: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Up, FaceDirection.LeftUp],
|
||||||
|
[FaceDirection.LeftUp, FaceDirection.Left],
|
||||||
|
[FaceDirection.Left, FaceDirection.LeftDown],
|
||||||
|
[FaceDirection.LeftDown, FaceDirection.Down],
|
||||||
|
[FaceDirection.Down, FaceDirection.RightDown],
|
||||||
|
[FaceDirection.RightDown, FaceDirection.Right],
|
||||||
|
[FaceDirection.Right, FaceDirection.RightUp],
|
||||||
|
[FaceDirection.RightUp, FaceDirection.Up]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DIR8_OPPOSITE: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Up, FaceDirection.Down],
|
||||||
|
[FaceDirection.Down, FaceDirection.Up],
|
||||||
|
[FaceDirection.Left, FaceDirection.Right],
|
||||||
|
[FaceDirection.Right, FaceDirection.Left],
|
||||||
|
[FaceDirection.LeftUp, FaceDirection.RightDown],
|
||||||
|
[FaceDirection.RightDown, FaceDirection.LeftUp],
|
||||||
|
[FaceDirection.RightUp, FaceDirection.LeftDown],
|
||||||
|
[FaceDirection.LeftDown, FaceDirection.RightUp]
|
||||||
|
]);
|
||||||
|
|
||||||
|
export class Dir8FaceHandler implements IFaceHandler<FaceDirection> {
|
||||||
|
degrade(dir: number): FaceDirection {
|
||||||
|
return dir as FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement(dir: number): IFaceDescriptor {
|
||||||
|
return DIR8_MOVEMENTS.get(this.degrade(dir)) ?? ZERO_DESCRIPTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
move(dir: number, count: number): IFaceDescriptor {
|
||||||
|
const { x, y } = this.movement(dir);
|
||||||
|
return { x: x * count, y: y * count };
|
||||||
|
}
|
||||||
|
|
||||||
|
opposite(dir: number): FaceDirection {
|
||||||
|
const degraded = this.degrade(dir);
|
||||||
|
return DIR8_OPPOSITE.get(degraded) ?? FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(dir: number, anticlockwise: boolean = false): FaceDirection {
|
||||||
|
const degraded = this.degrade(dir);
|
||||||
|
if (degraded === FaceDirection.Unknown) return FaceDirection.Unknown;
|
||||||
|
const map = anticlockwise ? DIR8_CCW : DIR8_CW;
|
||||||
|
return map.get(degraded) ?? FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapDirection(): Iterable<FaceDirection> {
|
||||||
|
return DIR8_MOVEMENTS.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
mapMovement(): Iterable<[FaceDirection, IFaceDescriptor]> {
|
||||||
|
return DIR8_MOVEMENTS.entries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DIR4_DEGRADE: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Left, FaceDirection.Left],
|
||||||
|
[FaceDirection.Up, FaceDirection.Up],
|
||||||
|
[FaceDirection.Right, FaceDirection.Right],
|
||||||
|
[FaceDirection.Down, FaceDirection.Down],
|
||||||
|
[FaceDirection.LeftUp, FaceDirection.Left],
|
||||||
|
[FaceDirection.LeftDown, FaceDirection.Left],
|
||||||
|
[FaceDirection.RightUp, FaceDirection.Right],
|
||||||
|
[FaceDirection.RightDown, FaceDirection.Right]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DIR4_MOVEMENTS: ReadonlyMap<FaceDirection, IFaceDescriptor> = new Map([
|
||||||
|
[FaceDirection.Unknown, ZERO_DESCRIPTOR],
|
||||||
|
[FaceDirection.Left, { x: -1, y: 0 }],
|
||||||
|
[FaceDirection.Up, { x: 0, y: -1 }],
|
||||||
|
[FaceDirection.Right, { x: 1, y: 0 }],
|
||||||
|
[FaceDirection.Down, { x: 0, y: 1 }]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DIR4_CW: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Up, FaceDirection.Right],
|
||||||
|
[FaceDirection.Right, FaceDirection.Down],
|
||||||
|
[FaceDirection.Down, FaceDirection.Left],
|
||||||
|
[FaceDirection.Left, FaceDirection.Up]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DIR4_CCW: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Up, FaceDirection.Left],
|
||||||
|
[FaceDirection.Left, FaceDirection.Down],
|
||||||
|
[FaceDirection.Down, FaceDirection.Right],
|
||||||
|
[FaceDirection.Right, FaceDirection.Up]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const DIR4_OPPOSITE: ReadonlyMap<FaceDirection, FaceDirection> = new Map([
|
||||||
|
[FaceDirection.Up, FaceDirection.Down],
|
||||||
|
[FaceDirection.Down, FaceDirection.Up],
|
||||||
|
[FaceDirection.Left, FaceDirection.Right],
|
||||||
|
[FaceDirection.Right, FaceDirection.Left]
|
||||||
|
]);
|
||||||
|
|
||||||
|
export class Dir4FaceHandler implements IFaceHandler<FaceDirection> {
|
||||||
|
degrade(dir: number): FaceDirection {
|
||||||
|
return DIR4_DEGRADE.get(dir as FaceDirection) ?? FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement(dir: number): IFaceDescriptor {
|
||||||
|
return DIR4_MOVEMENTS.get(this.degrade(dir)) ?? ZERO_DESCRIPTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
move(dir: number, count: number): IFaceDescriptor {
|
||||||
|
const { x, y } = this.movement(dir);
|
||||||
|
return { x: x * count, y: y * count };
|
||||||
|
}
|
||||||
|
|
||||||
|
opposite(dir: number): FaceDirection {
|
||||||
|
const degraded = this.degrade(dir);
|
||||||
|
return DIR4_OPPOSITE.get(degraded) ?? FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
next(dir: number, anticlockwise: boolean = false): FaceDirection {
|
||||||
|
const degraded = this.degrade(dir);
|
||||||
|
if (degraded === FaceDirection.Unknown) return FaceDirection.Unknown;
|
||||||
|
const map = anticlockwise ? DIR4_CCW : DIR4_CW;
|
||||||
|
return map.get(degraded) ?? FaceDirection.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapDirection(): Iterable<FaceDirection> {
|
||||||
|
return DIR4_MOVEMENTS.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
mapMovement(): Iterable<[FaceDirection, IFaceDescriptor]> {
|
||||||
|
return DIR4_MOVEMENTS.entries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region FaceManager
|
||||||
|
|
||||||
|
export class FaceManager implements IFaceManager {
|
||||||
|
private readonly byGroup: Map<number, IFaceHandler<number>> = new Map();
|
||||||
|
private readonly byId: Map<string, IFaceHandler<number>> = new Map();
|
||||||
|
|
||||||
|
register(group: number, handler: IFaceHandler<number>): void {
|
||||||
|
this.byGroup.set(group, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerById(id: string, handler: IFaceHandler<number>): void {
|
||||||
|
this.byId.set(id, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T extends number>(group: number): IFaceHandler<T> | null {
|
||||||
|
return (this.byGroup.get(group) as IFaceHandler<T>) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getById<T extends number>(id: string): IFaceHandler<T> | null {
|
||||||
|
return (this.byId.get(id) as IFaceHandler<T>) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
@ -1,2 +1,6 @@
|
|||||||
|
export * from './face';
|
||||||
|
export * from './faceManager';
|
||||||
|
export * from './indexer';
|
||||||
|
export * from './mover';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|||||||
61
packages-user/data-base/src/common/indexer.ts
Normal file
61
packages-user/data-base/src/common/indexer.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ITileLocator } from '@motajs/common';
|
||||||
|
|
||||||
|
//#region 接口定义
|
||||||
|
|
||||||
|
export interface ILocationHelper {
|
||||||
|
/**
|
||||||
|
* 坐标 -> 索引
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
*/
|
||||||
|
locToIndex(x: number, y: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位符 -> 索引
|
||||||
|
* @param locator 定位符
|
||||||
|
*/
|
||||||
|
locaterToIndex(locator: ITileLocator): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引 -> 定位符
|
||||||
|
* @param index 索引
|
||||||
|
*/
|
||||||
|
indexToLocator(index: number): ITileLocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILocationIndexer extends ILocationHelper {
|
||||||
|
/**
|
||||||
|
* 设置地图宽度
|
||||||
|
* @param width 地图宽度
|
||||||
|
*/
|
||||||
|
setWidth(width: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 默认实现
|
||||||
|
|
||||||
|
export class MapLocIndexer implements ILocationIndexer {
|
||||||
|
private width: number = 0;
|
||||||
|
|
||||||
|
setWidth(width: number): void {
|
||||||
|
this.width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
locToIndex(x: number, y: number): number {
|
||||||
|
return y * this.width + x;
|
||||||
|
}
|
||||||
|
|
||||||
|
locaterToIndex(locator: ITileLocator): number {
|
||||||
|
return locator.y * this.width + locator.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexToLocator(index: number): ITileLocator {
|
||||||
|
return {
|
||||||
|
x: index % this.width,
|
||||||
|
y: Math.floor(index / this.width)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
560
packages-user/data-base/src/common/mover.ts
Normal file
560
packages-user/data-base/src/common/mover.ts
Normal file
@ -0,0 +1,560 @@
|
|||||||
|
import {
|
||||||
|
Hookable,
|
||||||
|
HookController,
|
||||||
|
IHookable,
|
||||||
|
IHookBase,
|
||||||
|
IHookController,
|
||||||
|
ITileLocator
|
||||||
|
} from '@motajs/common';
|
||||||
|
import { FaceDirection } from './types';
|
||||||
|
import { IFaceHandler } from './faceManager';
|
||||||
|
|
||||||
|
//#region 对象移动
|
||||||
|
|
||||||
|
export const enum ObjectMoveStepType {
|
||||||
|
/** 绝对方向步,同步更新移动方向与朝向 */
|
||||||
|
Dir,
|
||||||
|
/** 绝对方向步,显式指定朝向 */
|
||||||
|
DirFace,
|
||||||
|
/** 速度步 */
|
||||||
|
Speed,
|
||||||
|
/** 纯转向步 */
|
||||||
|
Face,
|
||||||
|
/** 特殊步,如前进或后退 */
|
||||||
|
Special,
|
||||||
|
/** 动画方向步 */
|
||||||
|
AnimDir
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum ObjectSpecialStep {
|
||||||
|
/** 前进 */
|
||||||
|
Forward,
|
||||||
|
/** 后退 */
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum ObjectAnimDirection {
|
||||||
|
/** 正向播放动画 */
|
||||||
|
Forward,
|
||||||
|
/** 反向播放动画 */
|
||||||
|
Backward
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMovable {
|
||||||
|
/** 当前横坐标 */
|
||||||
|
readonly x: number;
|
||||||
|
/** 当前纵坐标 */
|
||||||
|
readonly y: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置对象位置
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
*/
|
||||||
|
setPos(x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前朝向
|
||||||
|
*/
|
||||||
|
getCurrentFaceDirection(): FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoveStepDir {
|
||||||
|
/** 步骤类型 */
|
||||||
|
type: ObjectMoveStepType.Dir;
|
||||||
|
/** 本步移动方向 */
|
||||||
|
move: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoveStepDirFace {
|
||||||
|
/** 步骤类型 */
|
||||||
|
type: ObjectMoveStepType.DirFace;
|
||||||
|
/** 本步移动方向 */
|
||||||
|
move: FaceDirection;
|
||||||
|
/** 本步显式朝向 */
|
||||||
|
face: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoveStepSpeed {
|
||||||
|
/** 步骤类型 */
|
||||||
|
type: ObjectMoveStepType.Speed;
|
||||||
|
/** 后续移动的每格耗时,单位为 ms */
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoveStepFace {
|
||||||
|
/** 步骤类型 */
|
||||||
|
type: ObjectMoveStepType.Face;
|
||||||
|
/** 要设置的朝向 */
|
||||||
|
value: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoveStepSpecial {
|
||||||
|
/** 步骤类型 */
|
||||||
|
type: ObjectMoveStepType.Special;
|
||||||
|
/** 特殊步方向 */
|
||||||
|
direction: ObjectSpecialStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoveAnimDir {
|
||||||
|
/** 步骤类型 */
|
||||||
|
type: ObjectMoveStepType.AnimDir;
|
||||||
|
/** 动画播放方向 */
|
||||||
|
dir: ObjectAnimDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ObjectMoveStep =
|
||||||
|
| IObjectMoveStepDir
|
||||||
|
| IObjectMoveStepDirFace
|
||||||
|
| IObjectMoveStepSpeed
|
||||||
|
| IObjectMoveStepFace
|
||||||
|
| IObjectMoveStepSpecial
|
||||||
|
| IObjectMoveAnimDir;
|
||||||
|
|
||||||
|
export interface IMoverController {
|
||||||
|
/** 本次移动是否已经全部完成 */
|
||||||
|
done: boolean;
|
||||||
|
/** 当本次移动结束时兑现 */
|
||||||
|
onEnd: Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向当前移动队列末尾追加步骤
|
||||||
|
* @param steps 要追加的步骤列表
|
||||||
|
*/
|
||||||
|
push(...steps: ObjectMoveStep[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在当前步移动之后立刻插入指定步,顺序为参数传入的顺序
|
||||||
|
* @param steps 要插入的步骤列表
|
||||||
|
*/
|
||||||
|
insert(...steps: ObjectMoveStep[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止当前移动,在当前步骤完成后兑现
|
||||||
|
*/
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMoverHooks<T extends IObjectMovable> extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当移动开始时触发
|
||||||
|
* @param tile 移动器绑定的图块
|
||||||
|
* @param mover 当前移动器
|
||||||
|
*/
|
||||||
|
onMoveStart?(tile: T, mover: IObjectMover<T>): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当移动结束时触发
|
||||||
|
* @param tile 移动器绑定的图块
|
||||||
|
* @param mover 当前移动器
|
||||||
|
*/
|
||||||
|
onMoveEnd?(tile: T, mover: IObjectMover<T>): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当单步移动开始前触发,此时对象坐标仍为移动前坐标
|
||||||
|
* @param code 当前步移动代码
|
||||||
|
* @param step 当前步骤
|
||||||
|
* @param tile 移动器绑定的图块
|
||||||
|
* @param mover 当前移动器
|
||||||
|
*/
|
||||||
|
onStepStart?(
|
||||||
|
code: number,
|
||||||
|
step: ObjectMoveStep,
|
||||||
|
tile: T,
|
||||||
|
mover: IObjectMover<T>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当单步移动结束后触发,此时对象坐标已更新为移动后坐标
|
||||||
|
* @param code 当前步移动代码
|
||||||
|
* @param step 当前步骤
|
||||||
|
* @param tile 移动器绑定的图块
|
||||||
|
* @param mover 当前移动器
|
||||||
|
*/
|
||||||
|
onStepEnd?(
|
||||||
|
code: number,
|
||||||
|
step: ObjectMoveStep,
|
||||||
|
tile: T,
|
||||||
|
mover: IObjectMover<T>
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IObjectMover<T extends IObjectMovable> extends IHookable<
|
||||||
|
IObjectMoverHooks<T>
|
||||||
|
> {
|
||||||
|
/** 当前是否正在移动 */
|
||||||
|
readonly moving: boolean;
|
||||||
|
/** 当前绑定的对象 */
|
||||||
|
readonly tile: T;
|
||||||
|
/** 当前朝向 */
|
||||||
|
readonly faceDirection: FaceDirection;
|
||||||
|
/** 当前移动方向 */
|
||||||
|
readonly moveDirection: FaceDirection;
|
||||||
|
/** 当前动画播放方向 */
|
||||||
|
readonly currAnimDir: ObjectAnimDirection;
|
||||||
|
/** 当前移动速度,单位毫秒 */
|
||||||
|
readonly currentSpeed: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加若干个绝对方向步,并同步更新移动方向与朝向
|
||||||
|
* @param dir 移动方向
|
||||||
|
* @param count 追加次数,默认 1
|
||||||
|
*/
|
||||||
|
step(dir: FaceDirection, count?: number): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加若干个绝对方向步,移动方向与朝向分别指定
|
||||||
|
* @param move 移动方向
|
||||||
|
* @param face 朝向方向
|
||||||
|
* @param count 追加次数,默认 1
|
||||||
|
*/
|
||||||
|
stepFace(move: FaceDirection, face: FaceDirection, count?: number): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加若干个前进步,沿当前移动方向移动
|
||||||
|
* @param count 追加次数,默认 1
|
||||||
|
*/
|
||||||
|
forward(count?: number): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加若干个后退步,沿当前移动方向的反方向移动
|
||||||
|
* @param count 追加次数,默认 1
|
||||||
|
*/
|
||||||
|
backward(count?: number): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置后续移动速度
|
||||||
|
* @param value 每格耗时,单位为 ms
|
||||||
|
*/
|
||||||
|
speed(value: number): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加一个纯转向步骤
|
||||||
|
* @param dir 目标朝向
|
||||||
|
*/
|
||||||
|
face(dir: FaceDirection): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置后续步骤的动画播放方向
|
||||||
|
* @param dir 动画播放方向
|
||||||
|
*/
|
||||||
|
animDir(dir: ObjectAnimDirection): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空尚未执行的步骤队列
|
||||||
|
*/
|
||||||
|
clear(): this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始执行当前计划队列
|
||||||
|
* @returns 若当前已在移动,则返回 `null`
|
||||||
|
*/
|
||||||
|
start(): Readonly<IMoverController> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 移动基类
|
||||||
|
|
||||||
|
export abstract class ObjectMover<T extends IObjectMovable>
|
||||||
|
extends Hookable<IObjectMoverHooks<T>>
|
||||||
|
implements IObjectMover<T>
|
||||||
|
{
|
||||||
|
abstract readonly tile: T;
|
||||||
|
|
||||||
|
/** 尚未开始执行的移动步骤队列 */
|
||||||
|
protected readonly moveQueue: Readonly<ObjectMoveStep>[] = [];
|
||||||
|
|
||||||
|
/** 当前是否正在移动 */
|
||||||
|
moving: boolean = false;
|
||||||
|
/** 当前朝向 */
|
||||||
|
faceDirection: FaceDirection = FaceDirection.Unknown;
|
||||||
|
/** 当前移动方向 */
|
||||||
|
moveDirection: FaceDirection = FaceDirection.Unknown;
|
||||||
|
/** 当前动画播放方向 */
|
||||||
|
currAnimDir: ObjectAnimDirection = ObjectAnimDirection.Forward;
|
||||||
|
/** 当前移动速度,单位毫秒 */
|
||||||
|
currentSpeed: number = 100;
|
||||||
|
|
||||||
|
/** 是否调用了 `IMoverController.stop` 接口 */
|
||||||
|
private shouldStop: boolean = false;
|
||||||
|
|
||||||
|
/** 朝向处理 */
|
||||||
|
private readonly faceHandler: IFaceHandler<FaceDirection>;
|
||||||
|
|
||||||
|
constructor(faceHandler: IFaceHandler<FaceDirection>) {
|
||||||
|
super();
|
||||||
|
this.faceHandler = faceHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createController(
|
||||||
|
hook: Partial<IObjectMoverHooks<T>>
|
||||||
|
): IHookController<IObjectMoverHooks<T>> {
|
||||||
|
return new HookController(this, hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当移动开始时执行
|
||||||
|
* @param tile 移动图块
|
||||||
|
* @param controller 移动控制器
|
||||||
|
*/
|
||||||
|
protected abstract onMoveStart(
|
||||||
|
tile: T,
|
||||||
|
controller: Readonly<IMoverController>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当移动结束时触发
|
||||||
|
* @param tile 移动图块
|
||||||
|
* @param controller 移动控制器
|
||||||
|
*/
|
||||||
|
protected abstract onMoveEnd(
|
||||||
|
tile: T,
|
||||||
|
controller: Readonly<IMoverController>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当单步移动开始时触发,返回移动代码,此移动代码将会传递至 {@link onStepEnd}
|
||||||
|
* @param step 当前移动步对象
|
||||||
|
* @param tile 移动图块
|
||||||
|
* @param controller 移动控制器
|
||||||
|
*/
|
||||||
|
protected abstract onStepStart(
|
||||||
|
step: ObjectMoveStep,
|
||||||
|
tile: T,
|
||||||
|
controller: Readonly<IMoverController>
|
||||||
|
): Promise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当单步移动结束时触发,返回坐标对象代表这一步移动结果
|
||||||
|
* @param code 移动代码,由 {@link onStepStart} 返回值传递而来
|
||||||
|
* @param step 当前移动步对象
|
||||||
|
* @param tile 移动图块
|
||||||
|
* @param controller 移动控制器
|
||||||
|
*/
|
||||||
|
protected abstract onStepEnd(
|
||||||
|
code: number,
|
||||||
|
step: ObjectMoveStep,
|
||||||
|
tile: T,
|
||||||
|
controller: Readonly<IMoverController>
|
||||||
|
): Promise<ITileLocator>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向计划队列末尾追加一个步骤
|
||||||
|
* @param step 要追加的步骤
|
||||||
|
*/
|
||||||
|
protected pushStep(step: Readonly<ObjectMoveStep>): void {
|
||||||
|
this.moveQueue.push(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前应当作为相对移动基准的方向
|
||||||
|
*/
|
||||||
|
private getCurrentDirection(): FaceDirection {
|
||||||
|
if (this.moveDirection !== FaceDirection.Unknown) {
|
||||||
|
return this.moveDirection;
|
||||||
|
} else {
|
||||||
|
return this.faceDirection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据步骤内容预先同步移动器内部状态
|
||||||
|
* @param step 当前步骤
|
||||||
|
*/
|
||||||
|
private prepareStep(step: ObjectMoveStep): void {
|
||||||
|
switch (step.type) {
|
||||||
|
case ObjectMoveStepType.Dir:
|
||||||
|
this.moveDirection = step.move;
|
||||||
|
this.faceDirection = step.move;
|
||||||
|
break;
|
||||||
|
case ObjectMoveStepType.DirFace:
|
||||||
|
this.moveDirection = step.move;
|
||||||
|
this.faceDirection = step.face;
|
||||||
|
break;
|
||||||
|
case ObjectMoveStepType.Face:
|
||||||
|
this.faceDirection = step.value;
|
||||||
|
break;
|
||||||
|
case ObjectMoveStepType.Special: {
|
||||||
|
const dir = this.getCurrentDirection();
|
||||||
|
if (step.direction === ObjectSpecialStep.Backward) {
|
||||||
|
const opposite = this.faceHandler.opposite(dir);
|
||||||
|
this.moveDirection = opposite;
|
||||||
|
this.faceDirection = opposite;
|
||||||
|
} else {
|
||||||
|
this.moveDirection = dir;
|
||||||
|
this.faceDirection = dir;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ObjectMoveStepType.AnimDir:
|
||||||
|
this.currAnimDir = step.dir;
|
||||||
|
break;
|
||||||
|
case ObjectMoveStepType.Speed:
|
||||||
|
this.currentSpeed = step.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step(dir: FaceDirection, count: number = 1): this {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.Dir,
|
||||||
|
move: dir
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
stepFace(
|
||||||
|
move: FaceDirection,
|
||||||
|
face: FaceDirection,
|
||||||
|
count: number = 1
|
||||||
|
): this {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.DirFace,
|
||||||
|
move,
|
||||||
|
face
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
forward(count: number = 1): this {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.Special,
|
||||||
|
direction: ObjectSpecialStep.Forward
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
backward(count: number = 1): this {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.Special,
|
||||||
|
direction: ObjectSpecialStep.Backward
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
speed(value: number): this {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.Speed,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
face(dir: FaceDirection): this {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.Face,
|
||||||
|
value: dir
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
animDir(dir: ObjectAnimDirection): this {
|
||||||
|
this.pushStep({
|
||||||
|
type: ObjectMoveStepType.AnimDir,
|
||||||
|
dir
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): this {
|
||||||
|
this.moveQueue.length = 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始移动流程
|
||||||
|
* @param queue 移动队列
|
||||||
|
*/
|
||||||
|
private async moveProgress(
|
||||||
|
queue: ObjectMoveStep[],
|
||||||
|
controller: Readonly<IMoverController>
|
||||||
|
) {
|
||||||
|
// 移动开始
|
||||||
|
await this.onMoveStart(this.tile, controller);
|
||||||
|
await Promise.all(
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
return hook.onMoveStart?.(this.tile, this);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 移动流程
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const step = queue.shift();
|
||||||
|
if (!step || !this.moving || this.shouldStop) break;
|
||||||
|
this.prepareStep(step);
|
||||||
|
const code = await this.onStepStart(step, this.tile, controller);
|
||||||
|
const stepStartHooks = this.forEachHook(hook =>
|
||||||
|
hook.onStepStart?.(code, step, this.tile, this)
|
||||||
|
);
|
||||||
|
await Promise.all(stepStartHooks);
|
||||||
|
const loc = await this.onStepEnd(code, step, this.tile, controller);
|
||||||
|
this.tile.setPos(loc.x, loc.y);
|
||||||
|
const stepEndHooks = this.forEachHook(hook =>
|
||||||
|
hook.onStepEnd?.(code, step, this.tile, this)
|
||||||
|
);
|
||||||
|
await Promise.all(stepEndHooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动结束
|
||||||
|
await this.onMoveEnd(this.tile, controller);
|
||||||
|
const moveEndHooks = this.forEachHook(hook =>
|
||||||
|
hook.onMoveEnd?.(this.tile, this)
|
||||||
|
);
|
||||||
|
await Promise.all(moveEndHooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): IMoverController | null {
|
||||||
|
if (this.moving) return null;
|
||||||
|
|
||||||
|
const queue = this.moveQueue.slice();
|
||||||
|
this.clear();
|
||||||
|
this.shouldStop = false;
|
||||||
|
|
||||||
|
this.faceDirection = this.tile.getCurrentFaceDirection();
|
||||||
|
this.moveDirection = FaceDirection.Unknown;
|
||||||
|
|
||||||
|
const { promise, resolve } = Promise.withResolvers<void>();
|
||||||
|
|
||||||
|
const controller: IMoverController = {
|
||||||
|
done: false,
|
||||||
|
onEnd: promise,
|
||||||
|
push: (...steps: ObjectMoveStep[]) => {
|
||||||
|
if (!this.moving || this.shouldStop || controller.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.push(...steps);
|
||||||
|
},
|
||||||
|
insert: (...steps: ObjectMoveStep[]) => {
|
||||||
|
if (!this.moving || this.shouldStop || controller.done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
queue.unshift(...steps);
|
||||||
|
},
|
||||||
|
stop: () => {
|
||||||
|
this.shouldStop = true;
|
||||||
|
return controller.onEnd;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.moving = true;
|
||||||
|
const moving = this.moveProgress(queue, controller);
|
||||||
|
moving.then(() => {
|
||||||
|
this.moving = false;
|
||||||
|
controller.done = true;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
@ -16,3 +16,67 @@ export interface IFaceData {
|
|||||||
/** 图块朝向 */
|
/** 图块朝向 */
|
||||||
readonly face: FaceDirection;
|
readonly face: FaceDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IRoleFaceBinder {
|
||||||
|
/**
|
||||||
|
* 给指定的图块分配朝向绑定
|
||||||
|
* @param identifier 图块数字
|
||||||
|
* @param main 主图块朝向,一般是朝下
|
||||||
|
*/
|
||||||
|
malloc(identifier: number, main: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将一个图块与另一个图块绑定朝向,需要注意要先调用 {@link malloc} 分配朝向信息
|
||||||
|
* @param identifier 当前图块数字
|
||||||
|
* @param main 主图块数字,即当前图块与目标图块属于主图块的另一个朝向
|
||||||
|
* @param face 当前图块的朝向方向
|
||||||
|
*/
|
||||||
|
bind(identifier: number, main: number, face: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取一个图块指定朝向的图块数字
|
||||||
|
* @param identifier 图块数字,可以是任意朝向的图块数字
|
||||||
|
* @param face 要获取的朝向
|
||||||
|
*/
|
||||||
|
getFaceOf(identifier: number, face: FaceDirection): IFaceData | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定图块数字是哪个朝向
|
||||||
|
* @param identifier 图块数字
|
||||||
|
*/
|
||||||
|
getFaceDirection(identifier: number): FaceDirection | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定图块数字绑定至的主朝向
|
||||||
|
* @param identifier 图块数字,可以是任意朝向的图块数字
|
||||||
|
*/
|
||||||
|
getMainFace(identifier: number): IFaceData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 功能接口
|
||||||
|
|
||||||
|
export const enum SaveCompression {
|
||||||
|
/** 不进行压缩,仅提取必要数据 */
|
||||||
|
NoCompression,
|
||||||
|
/** 进行小幅度压缩,以性能为主要考虑目标 */
|
||||||
|
LowCompression,
|
||||||
|
/** 进行大幅度压缩,以体积为主要考虑目标 */
|
||||||
|
HighCompression
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaveableContent<T> {
|
||||||
|
/**
|
||||||
|
* 保存对象状态,返回的对象应该经过深拷贝(即 `structuedClone`)
|
||||||
|
* @param compression 压缩级别
|
||||||
|
*/
|
||||||
|
saveState(compression: SaveCompression): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取对象状态
|
||||||
|
* @param state 状态对象
|
||||||
|
* @param compression 压缩级别
|
||||||
|
*/
|
||||||
|
loadState(state: T, compression: SaveCompression): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
import {
|
import { SaveCompression } from '../common';
|
||||||
IEnemy,
|
import { IEnemy, IEnemySaveState, IReadonlyEnemy, ISpecial } from './types';
|
||||||
IEnemyContext,
|
|
||||||
IReadonlyEnemy,
|
|
||||||
ISpecial,
|
|
||||||
IEnemyView
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
export class Enemy<TAttr> implements IEnemy<TAttr> {
|
export class Enemy<TAttr> implements IEnemy<TAttr> {
|
||||||
/** 怪物身上的特殊属性列表 */
|
/** 怪物身上的特殊属性列表 */
|
||||||
@ -87,44 +82,27 @@ export class Enemy<TAttr> implements IEnemy<TAttr> {
|
|||||||
this.addSpecial(special.clone());
|
this.addSpecial(special.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
saveState(_compression: SaveCompression): IEnemySaveState<TAttr> {
|
||||||
/** 计算后怪物 */
|
const specials: Map<number, unknown> = new Map();
|
||||||
private readonly computedEnemy: IEnemy<TAttr>;
|
for (const special of this.specials) {
|
||||||
|
specials.set(special.code, special.saveState(_compression));
|
||||||
constructor(
|
}
|
||||||
readonly baseEnemy: IEnemy<TAttr>,
|
return { attrs: structuredClone(this.attributes), specials };
|
||||||
readonly context: IEnemyContext<TAttr, unknown>
|
|
||||||
) {
|
|
||||||
this.computedEnemy = baseEnemy.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
loadState(
|
||||||
this.computedEnemy.copyFrom(this.baseEnemy);
|
state: IEnemySaveState<TAttr>,
|
||||||
|
compression: SaveCompression
|
||||||
|
): void {
|
||||||
|
this.attributes = structuredClone(state.attrs);
|
||||||
|
for (const special of this.specials) {
|
||||||
|
const saved = state.specials.get(special.code);
|
||||||
|
if (saved === undefined) {
|
||||||
|
logger.warn(120, special.code.toString(), this.id);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
special.loadState(saved, compression);
|
||||||
getBaseEnemy(): IReadonlyEnemy<TAttr> {
|
|
||||||
return this.baseEnemy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getComputedEnemy(): IReadonlyEnemy<TAttr> {
|
|
||||||
this.context.requestRefresh(this);
|
|
||||||
return this.computedEnemy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取计算中怪物对象,这个接口不对外暴露,仅在系统内部的 EnemyContext 中使用。
|
|
||||||
*/
|
|
||||||
getComputingEnemy(): IEnemy<TAttr> {
|
|
||||||
return this.computedEnemy;
|
|
||||||
}
|
|
||||||
|
|
||||||
getModifiableEnemy(): IEnemy<TAttr> {
|
|
||||||
return this.baseEnemy;
|
|
||||||
}
|
|
||||||
|
|
||||||
markDirty(): void {
|
|
||||||
this.context.markDirty(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
export * from './enemy';
|
export * from './enemy';
|
||||||
export * from './context';
|
|
||||||
export * from './damage';
|
|
||||||
export * from './mapDamage';
|
|
||||||
export * from './manager';
|
export * from './manager';
|
||||||
export * from './special';
|
export * from './special';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
|
||||||
|
|||||||
@ -2,10 +2,15 @@ import { logger } from '@motajs/common';
|
|||||||
import { Enemy as EnemyImpl } from './enemy';
|
import { Enemy as EnemyImpl } from './enemy';
|
||||||
import {
|
import {
|
||||||
IEnemy,
|
IEnemy,
|
||||||
|
IEnemyComparer,
|
||||||
IEnemyManager,
|
IEnemyManager,
|
||||||
|
IEnemyManagerSaveState,
|
||||||
IEnemyLegacyBridge,
|
IEnemyLegacyBridge,
|
||||||
SpecialCreation
|
IReadonlyEnemy,
|
||||||
|
SpecialCreation,
|
||||||
|
IEnemySaveState
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
|
|
||||||
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||||
/** 特殊属性注册表,code -> 创建函数 */
|
/** 特殊属性注册表,code -> 创建函数 */
|
||||||
@ -19,6 +24,18 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
|||||||
private readonly prefabById: Map<string, IEnemy<TAttr>> = new Map();
|
private readonly prefabById: Map<string, IEnemy<TAttr>> = new Map();
|
||||||
/** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */
|
/** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */
|
||||||
private readonly legacyIdToCode: Map<string, number> = new Map();
|
private readonly legacyIdToCode: Map<string, number> = new Map();
|
||||||
|
/** 复用映射,reusedCode -> sourceCode */
|
||||||
|
private readonly reuseByCode: Map<number, number> = new Map();
|
||||||
|
/** 复用映射,reusedId -> sourceId */
|
||||||
|
private readonly reuseById: Map<string, string> = new Map();
|
||||||
|
/** 脏模板集合,存储发生了变化的模板 code */
|
||||||
|
private readonly dirtySet: Set<number> = new Set();
|
||||||
|
/** 参考快照,code -> IReadonlyEnemy,由 compareWith 提供 */
|
||||||
|
private referenceByCode: Map<number, IReadonlyEnemy<TAttr>> = new Map();
|
||||||
|
/** 当前附加的怪物比较器 */
|
||||||
|
private comparer: IEnemyComparer<TAttr> | null = null;
|
||||||
|
/** 是否已首次调用 compareWith */
|
||||||
|
private hasReference: boolean = false;
|
||||||
|
|
||||||
constructor(readonly bridge: IEnemyLegacyBridge<TAttr>) {}
|
constructor(readonly bridge: IEnemyLegacyBridge<TAttr>) {}
|
||||||
|
|
||||||
@ -111,9 +128,11 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
|||||||
|
|
||||||
private internalGetPrefab(code: number | string) {
|
private internalGetPrefab(code: number | string) {
|
||||||
if (typeof code === 'number') {
|
if (typeof code === 'number') {
|
||||||
return this.prefabByCode.get(code) ?? null;
|
const sourceCode = this.reuseByCode.get(code) ?? code;
|
||||||
|
return this.prefabByCode.get(sourceCode) ?? null;
|
||||||
} else {
|
} else {
|
||||||
return this.prefabById.get(code) ?? null;
|
const sourceId = this.reuseById.get(code) ?? code;
|
||||||
|
return this.prefabById.get(sourceId) ?? null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,6 +146,7 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
|||||||
const cloned = enemy.clone();
|
const cloned = enemy.clone();
|
||||||
this.prefabByCode.set(enemy.code, cloned);
|
this.prefabByCode.set(enemy.code, cloned);
|
||||||
this.prefabById.set(enemy.id, cloned);
|
this.prefabById.set(enemy.id, cloned);
|
||||||
|
this.updateDirty(cloned.code, cloned);
|
||||||
}
|
}
|
||||||
|
|
||||||
addPrefabFromLegacy(code: number, enemy: Enemy): void {
|
addPrefabFromLegacy(code: number, enemy: Enemy): void {
|
||||||
@ -137,14 +157,17 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
|||||||
this.prefabByCode.set(code, prefab);
|
this.prefabByCode.set(code, prefab);
|
||||||
this.prefabById.set(prefab.id, prefab);
|
this.prefabById.set(prefab.id, prefab);
|
||||||
this.legacyIdToCode.set(enemy.id, code);
|
this.legacyIdToCode.set(enemy.id, code);
|
||||||
|
this.updateDirty(code, prefab);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrefab(code: number): IEnemy<TAttr> | null {
|
getPrefab(code: number): IReadonlyEnemy<TAttr> | null {
|
||||||
return this.prefabByCode.get(code) ?? null;
|
const sourceCode = this.reuseByCode.get(code) ?? code;
|
||||||
|
return this.prefabByCode.get(sourceCode) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrefabById(id: string): IEnemy<TAttr> | null {
|
getPrefabById(id: string): IReadonlyEnemy<TAttr> | null {
|
||||||
return this.prefabById.get(id) ?? null;
|
const sourceId = this.reuseById.get(id) ?? id;
|
||||||
|
return this.prefabById.get(sourceId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePrefab(code: number | string): void {
|
deletePrefab(code: number | string): void {
|
||||||
@ -160,12 +183,126 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
|||||||
// 再添加新的模板
|
// 再添加新的模板
|
||||||
this.prefabByCode.set(enemy.code, enemy);
|
this.prefabByCode.set(enemy.code, enemy);
|
||||||
this.prefabById.set(enemy.id, enemy);
|
this.prefabById.set(enemy.id, enemy);
|
||||||
|
this.updateDirty(enemy.code, enemy);
|
||||||
}
|
}
|
||||||
|
|
||||||
reusePrefab(source: number | string, code: number, id: string): void {
|
reusePrefab(source: number | string, code: number, id: string): void {
|
||||||
const prefab = this.internalGetPrefab(source);
|
const prefab = this.internalGetPrefab(source);
|
||||||
if (!prefab) return;
|
if (!prefab) return;
|
||||||
this.prefabByCode.set(code, prefab);
|
this.reuseByCode.set(code, prefab.code);
|
||||||
this.prefabById.set(id, prefab);
|
this.reuseById.set(id, prefab.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void {
|
||||||
|
const isSubsequentCall = this.hasReference;
|
||||||
|
if (isSubsequentCall) {
|
||||||
|
logger.warn(117);
|
||||||
|
}
|
||||||
|
this.referenceByCode = new Map();
|
||||||
|
reference.forEach((enemy, key) => {
|
||||||
|
this.referenceByCode.set(key, enemy.clone());
|
||||||
|
});
|
||||||
|
this.hasReference = true;
|
||||||
|
this.dirtySet.clear();
|
||||||
|
if (isSubsequentCall) {
|
||||||
|
this.refreshDirty(reference.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyPrefabAttribute(
|
||||||
|
code: number | string,
|
||||||
|
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||||
|
): void {
|
||||||
|
const prefab = this.internalGetPrefab(code);
|
||||||
|
if (!prefab) return;
|
||||||
|
const result = modify(prefab);
|
||||||
|
const prefabCode = prefab.code;
|
||||||
|
if (result !== prefab) {
|
||||||
|
this.prefabByCode.set(result.code, result);
|
||||||
|
this.prefabById.set(result.id, result);
|
||||||
|
if (result.code !== prefabCode) {
|
||||||
|
this.prefabByCode.delete(prefabCode);
|
||||||
|
}
|
||||||
|
if (result.id !== prefab.id) {
|
||||||
|
this.prefabById.delete(prefab.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateDirty(result.code, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void {
|
||||||
|
this.comparer = comparer;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnemyComparer(): IEnemyComparer<TAttr> | null {
|
||||||
|
return this.comparer;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(compression: SaveCompression): IEnemyManagerSaveState<TAttr> {
|
||||||
|
const modified: Map<number, IEnemySaveState<TAttr>> = new Map();
|
||||||
|
for (const code of this.dirtySet) {
|
||||||
|
const prefab = this.prefabByCode.get(code);
|
||||||
|
if (!prefab) continue;
|
||||||
|
modified.set(code, prefab.saveState(compression));
|
||||||
|
}
|
||||||
|
return { modified };
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(
|
||||||
|
state: IEnemyManagerSaveState<TAttr>,
|
||||||
|
compression: SaveCompression
|
||||||
|
): void {
|
||||||
|
for (const [code, enemyState] of state.modified) {
|
||||||
|
const prefab = this.prefabByCode.get(code);
|
||||||
|
if (!prefab) {
|
||||||
|
logger.warn(119, code.toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
prefab.loadState(enemyState, compression);
|
||||||
|
}
|
||||||
|
// loadState 结束后重新刷新 dirty 集合
|
||||||
|
this.refreshDirty(state.modified.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据参考快照更新指定 code 的脏状态
|
||||||
|
* @param code 怪物图块数字
|
||||||
|
* @param current 当前模板对象
|
||||||
|
*/
|
||||||
|
private updateDirty(code: number, current: IEnemy<TAttr>): void {
|
||||||
|
if (!this.hasReference) return;
|
||||||
|
if (!this.comparer) {
|
||||||
|
logger.warn(118);
|
||||||
|
this.dirtySet.add(code);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ref = this.referenceByCode.get(code);
|
||||||
|
if (!ref || !this.comparer.compare(current, ref)) {
|
||||||
|
this.dirtySet.add(code);
|
||||||
|
} else {
|
||||||
|
this.dirtySet.delete(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将所有模板加入脏集合,再与参考比较,去除未变化的模板
|
||||||
|
*/
|
||||||
|
private refreshDirty(dirties: Iterable<number>): void {
|
||||||
|
if (!this.hasReference) return;
|
||||||
|
for (const code of dirties) {
|
||||||
|
this.dirtySet.add(code);
|
||||||
|
}
|
||||||
|
if (!this.comparer) return;
|
||||||
|
for (const code of [...this.dirtySet]) {
|
||||||
|
const prefab = this.prefabByCode.get(code);
|
||||||
|
if (!prefab) {
|
||||||
|
this.dirtySet.delete(code);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ref = this.referenceByCode.get(code);
|
||||||
|
if (ref && this.comparer.compare(prefab, ref)) {
|
||||||
|
this.dirtySet.delete(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
import { ISpecial, SpecialCreation } from './types';
|
import { ISpecial, SpecialCreation } from './types';
|
||||||
|
|
||||||
// TODO: 颜色参数
|
// TODO: 颜色参数
|
||||||
@ -45,6 +47,19 @@ export class CommonSerializableSpecial<T> implements ISpecial<T> {
|
|||||||
this.config
|
this.config
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveState(_compression: SaveCompression): T {
|
||||||
|
return structuredClone(this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(state: T, _compression: SaveCompression): void {
|
||||||
|
this.setValue(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
deepEqualsTo(other: ISpecial<T>): boolean {
|
||||||
|
if (this.code !== other.code) return false;
|
||||||
|
return isEqual(this.value, other.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NonePropertySpecial implements ISpecial<void> {
|
export class NonePropertySpecial implements ISpecial<void> {
|
||||||
@ -78,6 +93,18 @@ export class NonePropertySpecial implements ISpecial<void> {
|
|||||||
clone(): ISpecial<void> {
|
clone(): ISpecial<void> {
|
||||||
return new NonePropertySpecial(this.code, this.config);
|
return new NonePropertySpecial(this.code, this.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveState(_compression: SaveCompression): void {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(_state: void, _compression: SaveCompression): void {
|
||||||
|
// 无属性,无需操作
|
||||||
|
}
|
||||||
|
|
||||||
|
deepEqualsTo(other: ISpecial<void>): boolean {
|
||||||
|
return this.code === other.code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineCommonSerializableSpecial<T, TAttr = any>(
|
export function defineCommonSerializableSpecial<T, TAttr = any>(
|
||||||
|
|||||||
@ -1,9 +1,34 @@
|
|||||||
import { IRange, ITileLocator } from '@motajs/common';
|
import { ISaveableContent } from '../common';
|
||||||
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
|
|
||||||
|
|
||||||
//#region 怪物基础
|
//#region 怪物基础
|
||||||
|
|
||||||
export interface ISpecial<T = void> {
|
/** 单个 IEnemy 的存档状态 */
|
||||||
|
export interface IEnemySaveState<TAttr> {
|
||||||
|
/** 怪物属性的深拷贝 */
|
||||||
|
readonly attrs: TAttr;
|
||||||
|
/** 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 */
|
||||||
|
readonly specials: ReadonlyMap<number, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */
|
||||||
|
export interface IEnemyManagerSaveState<TAttr> {
|
||||||
|
/** code -> 变更后的 IEnemySaveState,仅包含脏模板 */
|
||||||
|
readonly modified: ReadonlyMap<number, IEnemySaveState<TAttr>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemyComparer<TAttr> {
|
||||||
|
/**
|
||||||
|
* 比较两个怪物是否完全相同
|
||||||
|
* @param enemyA 怪物 A
|
||||||
|
* @param enemyB 怪物 B
|
||||||
|
*/
|
||||||
|
compare(
|
||||||
|
enemyA: IReadonlyEnemy<TAttr>,
|
||||||
|
enemyB: IReadonlyEnemy<TAttr>
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISpecial<T = void> extends ISaveableContent<T> {
|
||||||
/** 特殊属性代码 */
|
/** 特殊属性代码 */
|
||||||
readonly code: number;
|
readonly code: number;
|
||||||
/** 特殊属性需要的数值 */
|
/** 特殊属性需要的数值 */
|
||||||
@ -40,6 +65,12 @@ export interface ISpecial<T = void> {
|
|||||||
* 深拷贝此特殊属性
|
* 深拷贝此特殊属性
|
||||||
*/
|
*/
|
||||||
clone(): ISpecial<T>;
|
clone(): ISpecial<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度比较此特殊属性与另一特殊属性是否相同
|
||||||
|
* @param other 另一特殊属性
|
||||||
|
*/
|
||||||
|
deepEqualsTo(other: ISpecial<T>): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReadonlyEnemy<TAttr> {
|
export interface IReadonlyEnemy<TAttr> {
|
||||||
@ -82,7 +113,8 @@ export interface IReadonlyEnemy<TAttr> {
|
|||||||
clone(): IReadonlyEnemy<TAttr>;
|
clone(): IReadonlyEnemy<TAttr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEnemy<TAttr> extends IReadonlyEnemy<TAttr> {
|
export interface IEnemy<TAttr>
|
||||||
|
extends IReadonlyEnemy<TAttr>, ISaveableContent<IEnemySaveState<TAttr>> {
|
||||||
/**
|
/**
|
||||||
* 添加特殊属性
|
* 添加特殊属性
|
||||||
* @param special 特殊属性对象
|
* @param special 特殊属性对象
|
||||||
@ -138,7 +170,9 @@ export interface IEnemyLegacyBridge<TAttr> {
|
|||||||
fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr;
|
fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEnemyManager<TAttr> {
|
export interface IEnemyManager<TAttr> extends ISaveableContent<
|
||||||
|
IEnemyManagerSaveState<TAttr>
|
||||||
|
> {
|
||||||
/**
|
/**
|
||||||
* 注册一个特殊属性
|
* 注册一个特殊属性
|
||||||
* @param code 特殊属性代码
|
* @param code 特殊属性代码
|
||||||
@ -193,13 +227,13 @@ export interface IEnemyManager<TAttr> {
|
|||||||
* 获取指定怪物的模板
|
* 获取指定怪物的模板
|
||||||
* @param code 怪物图块数字
|
* @param code 怪物图块数字
|
||||||
*/
|
*/
|
||||||
getPrefab(code: number): IEnemy<TAttr> | null;
|
getPrefab(code: number): IReadonlyEnemy<TAttr> | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据怪物的 `id` 获取对应的怪物模板
|
* 根据怪物的 `id` 获取对应的怪物模板
|
||||||
* @param id 怪物 `id`
|
* @param id 怪物 `id`
|
||||||
*/
|
*/
|
||||||
getPrefabById(id: string): IEnemy<TAttr> | null;
|
getPrefabById(id: string): IReadonlyEnemy<TAttr> | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除指定的怪物模板
|
* 删除指定的怪物模板
|
||||||
@ -221,719 +255,34 @@ export interface IEnemyManager<TAttr> {
|
|||||||
* @param id 复用怪物 id
|
* @param id 复用怪物 id
|
||||||
*/
|
*/
|
||||||
reusePrefab(source: number | string, code: number, id: string): void;
|
reusePrefab(source: number | string, code: number, id: string): void;
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 辅助接口
|
|
||||||
|
|
||||||
export interface IMapLocHelper {
|
|
||||||
/**
|
|
||||||
* 坐标 -> 索引
|
|
||||||
* @param x 横坐标
|
|
||||||
* @param y 纵坐标
|
|
||||||
*/
|
|
||||||
locToIndex(x: number, y: number): number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定位符 -> 索引
|
* 设置参考快照,后续对模板的修改将与此比较以确定是否脏。
|
||||||
* @param locator 定位符
|
* 非首次调用时会发出警告,但仍执行覆盖
|
||||||
|
* @param reference code -> 参考怪物的 Map
|
||||||
*/
|
*/
|
||||||
locaterToIndex(locator: ITileLocator): number;
|
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 索引 -> 定位符
|
* 修改指定怪物模板的属性,修改完成后自动与参考模板比较并更新 dirty 集合
|
||||||
* @param index 索引
|
* @param code 怪物的图块数字或 `id`
|
||||||
|
* @param modify 修改函数,传入可写怪物对象,返回修改后的对象
|
||||||
*/
|
*/
|
||||||
indexToLocator(index: number): ITileLocator;
|
modifyPrefabAttribute(
|
||||||
}
|
code: number | string,
|
||||||
|
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||||
export interface IMapLocIndexer extends IMapLocHelper {
|
|
||||||
/**
|
|
||||||
* 设置地图宽度
|
|
||||||
* @param width 地图宽度
|
|
||||||
*/
|
|
||||||
setWidth(width: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemyHandler<TAttr, THero> {
|
|
||||||
/** 怪物属性信息 */
|
|
||||||
readonly enemy: IEnemy<TAttr>;
|
|
||||||
/** 怪物定位符 */
|
|
||||||
readonly locator: ITileLocator;
|
|
||||||
/** 勇士属性信息 */
|
|
||||||
readonly hero: IReadonlyHeroAttribute<THero>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IReadonlyEnemyHandler<TAttr, THero> {
|
|
||||||
/** 怪物属性信息 */
|
|
||||||
readonly enemy: IReadonlyEnemy<TAttr>;
|
|
||||||
/** 怪物定位符 */
|
|
||||||
readonly locator: ITileLocator;
|
|
||||||
/** 勇士属性信息 */
|
|
||||||
readonly hero: IReadonlyHeroAttribute<THero>;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 怪物对象
|
|
||||||
|
|
||||||
export interface IEnemyView<TAttr> {
|
|
||||||
/** 怪物视图所属的上下文 */
|
|
||||||
readonly context: IEnemyContext<TAttr, unknown>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置此怪物视图的状态,将计算后怪物对象恢复至初始状态
|
|
||||||
*/
|
|
||||||
reset(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取基本怪物对象
|
|
||||||
*/
|
|
||||||
getBaseEnemy(): IReadonlyEnemy<TAttr>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取计算后的怪物对象,返回的怪物对象同引用
|
|
||||||
*/
|
|
||||||
getComputedEnemy(): IReadonlyEnemy<TAttr>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取可修改的怪物对象。如果修改此方法获取的怪物对象,那么怪物的真实信息是不会刷新的,
|
|
||||||
* 需要手动调用 markDirty 方法来刷新。
|
|
||||||
*/
|
|
||||||
getModifiableEnemy(): IEnemy<TAttr>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将此怪物标记为脏,需要更新
|
|
||||||
*/
|
|
||||||
markDirty(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 光环与查询
|
|
||||||
|
|
||||||
export interface IEnemySpecialModifier<TAttr> {
|
|
||||||
/**
|
|
||||||
* 获取要添加到指定怪物身上的特殊属性
|
|
||||||
* @param handler 信息对象
|
|
||||||
*/
|
|
||||||
add(handler: IReadonlyEnemyHandler<TAttr, unknown>): ISpecial<any>[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取制定怪物身上要删除的特殊属性
|
|
||||||
* @param handler 信息对象
|
|
||||||
*/
|
|
||||||
delete(handler: IReadonlyEnemyHandler<TAttr, unknown>): ISpecial<any>[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改一个怪物的特殊属性,如果真正进行了修改则返回 true,否则返回 false
|
|
||||||
* @param handler 信息对象
|
|
||||||
* @param special 要修改的怪物特殊属性
|
|
||||||
*/
|
|
||||||
modify(
|
|
||||||
handler: IEnemyHandler<TAttr, unknown>,
|
|
||||||
special: ISpecial<any>
|
|
||||||
): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAuraView<TAttr, T = any> {
|
|
||||||
/** 此光环视图的优先级 */
|
|
||||||
readonly priority: number;
|
|
||||||
/** 此光环视图的影响范围 */
|
|
||||||
readonly range: IRange<T>;
|
|
||||||
|
|
||||||
/** 这个光环视图是否有可能修改怪物的基本属性 */
|
|
||||||
readonly couldApplyBase: boolean;
|
|
||||||
/** 这个光环视图是否有可能修改怪物的特殊属性 */
|
|
||||||
readonly couldApplySpecial: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取范围扫描参数
|
|
||||||
*/
|
|
||||||
getRangeParam(): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对指定怪物对象施加修饰器
|
|
||||||
* @param handler 信息对象
|
|
||||||
* @param baseEnemy 原始怪物对象,即未进行任何修改的怪物对象
|
|
||||||
*/
|
|
||||||
apply(
|
|
||||||
handler: IEnemyHandler<TAttr, unknown>,
|
|
||||||
baseEnemy: IReadonlyEnemy<TAttr>
|
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对指定怪物对象添加特殊属性修饰器
|
* 附加怪物比较器,用于 dirty 集合的判断
|
||||||
* @param handler 信息对象
|
* @param comparer 比较器对象
|
||||||
* @param baseEnemy 原始怪物对象,即未进行任何修改的怪物对象
|
|
||||||
*/
|
*/
|
||||||
applySpecial(
|
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void;
|
||||||
handler: IEnemyHandler<TAttr, unknown>,
|
|
||||||
baseEnemy: IReadonlyEnemy<TAttr>
|
|
||||||
): IEnemySpecialModifier<TAttr> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemyAuraView<TAttr, R, S> extends IAuraView<TAttr, R> {
|
|
||||||
/** 此光环视图所属的怪物 */
|
|
||||||
readonly enemy: IReadonlyEnemy<TAttr>;
|
|
||||||
/** 此光环视图所属的特殊属性 */
|
|
||||||
readonly special: ISpecial<S>;
|
|
||||||
/** 此光环视图所属怪物的定位符 */
|
|
||||||
readonly locator: ITileLocator;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAuraConverter<TAttr, THero> {
|
|
||||||
/**
|
|
||||||
* 判断一个特殊属性是否应该被当前光环转换器执行转换
|
|
||||||
* @param special 要转换的特殊属性
|
|
||||||
* @param handler 信息对象
|
|
||||||
*/
|
|
||||||
shouldConvert(
|
|
||||||
special: ISpecial<any>,
|
|
||||||
handler: IReadonlyEnemyHandler<TAttr, THero>
|
|
||||||
): boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将一个特殊属性转换为光环视图
|
* 获取当前附加的怪物比较器,如未设置则返回 `null`
|
||||||
*/
|
*/
|
||||||
convert(
|
getEnemyComparer(): IEnemyComparer<TAttr> | null;
|
||||||
special: ISpecial<any>,
|
|
||||||
handler: IReadonlyEnemyHandler<TAttr, THero>,
|
|
||||||
context: IEnemyContext<TAttr, THero>
|
|
||||||
): IEnemyAuraView<TAttr, any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemySpecialQueryModifier<
|
|
||||||
TAttr,
|
|
||||||
THero
|
|
||||||
> extends IEnemySpecialModifier<TAttr> {
|
|
||||||
/**
|
|
||||||
* 判断一个怪物是否应该查询外部状态
|
|
||||||
*/
|
|
||||||
shouldQuery(handler: IReadonlyEnemyHandler<TAttr, THero>): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemySpecialQueryEffect<TAttr, THero> {
|
|
||||||
/** 效果优先级,与光环属性共用 */
|
|
||||||
readonly priority: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据传入的怪物上下文,获取对应的怪物特殊属性修饰器
|
|
||||||
*/
|
|
||||||
for(
|
|
||||||
ctx: IEnemyContext<TAttr, THero>
|
|
||||||
): IEnemySpecialQueryModifier<TAttr, THero>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemyCommonQueryEffect<TAttr, THero> {
|
|
||||||
/** 优先级,越高的越先执行 */
|
|
||||||
readonly priority: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 对怪物的某个特殊属性施加常规查询效果
|
|
||||||
*/
|
|
||||||
apply(
|
|
||||||
handler: IEnemyHandler<TAttr, THero>,
|
|
||||||
special: ISpecial<any>,
|
|
||||||
query: () => IEnemyContext<TAttr, THero>
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemyFinalEffect<TAttr, THero> {
|
|
||||||
/** 效果优先级,越高会越先被执行 */
|
|
||||||
readonly priority: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 向怪物施加最终修饰效果
|
|
||||||
*/
|
|
||||||
apply(handler: IEnemyHandler<TAttr, THero>): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 地图伤害
|
|
||||||
|
|
||||||
export interface IMapDamageInfoExtra {
|
|
||||||
/** 捕捉怪物信息 */
|
|
||||||
catch: Set<ITileLocator>;
|
|
||||||
/** 阻击怪物信息 */
|
|
||||||
repulse: Set<ITileLocator>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapDamageInfo {
|
|
||||||
/** 伤害值 */
|
|
||||||
damage: number;
|
|
||||||
/** 伤害类型 */
|
|
||||||
type: number;
|
|
||||||
/** 地图伤害额外信息 */
|
|
||||||
extra: IMapDamageInfoExtra;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapDamageView<T = any> {
|
|
||||||
/**
|
|
||||||
* 获取地图伤害影响范围
|
|
||||||
*/
|
|
||||||
getRange(): IRange<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取范围参数
|
|
||||||
*/
|
|
||||||
getRangeParam(): T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定位置的地图伤害,会对坐标进行判断
|
|
||||||
* @param locator 伤害位置
|
|
||||||
*/
|
|
||||||
getDamageAt(locator: ITileLocator): Readonly<IMapDamageInfo> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定位置的地图伤害,会对坐标进行判断
|
|
||||||
* @param locator 伤害位置
|
|
||||||
*/
|
|
||||||
getDamageWithoutCheck(
|
|
||||||
locator: ITileLocator
|
|
||||||
): Readonly<IMapDamageInfo> | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapDamageConverter<TAttr, THero> {
|
|
||||||
/**
|
|
||||||
* 转换地图伤害视图
|
|
||||||
*/
|
|
||||||
convert(
|
|
||||||
handler: IReadonlyEnemyHandler<TAttr, THero>,
|
|
||||||
context: IEnemyContext<TAttr, THero>
|
|
||||||
): IMapDamageView<any>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapDamageReducer {
|
|
||||||
/**
|
|
||||||
* 对伤害信息进行合并
|
|
||||||
*/
|
|
||||||
reduce(
|
|
||||||
info: Iterable<Readonly<IMapDamageInfo>>,
|
|
||||||
locator: ITileLocator
|
|
||||||
): Readonly<IMapDamageInfo>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapDamage<TAttr, THero> {
|
|
||||||
/** 当前绑定的怪物上下文 */
|
|
||||||
readonly context: IEnemyContext<TAttr, THero>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置地图伤害转换器,并基于当前上下文重建所有地图伤害视图
|
|
||||||
* @param converter 地图伤害转换器
|
|
||||||
*/
|
|
||||||
useConverter(converter: IMapDamageConverter<TAttr, THero>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置地图伤害合并器
|
|
||||||
* @param reducer 地图伤害合并器
|
|
||||||
*/
|
|
||||||
useReducer(reducer: IMapDamageReducer): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在指定位置添加一条无来源地图伤害
|
|
||||||
* @param locator 地图定位符
|
|
||||||
* @param info 地图伤害信息
|
|
||||||
*/
|
|
||||||
addMapDamage(locator: ITileLocator, info: IMapDamageInfo): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在指定位置删除一条无来源地图伤害
|
|
||||||
* @param locator 地图定位符
|
|
||||||
* @param info 地图伤害信息
|
|
||||||
*/
|
|
||||||
deleteMapDamage(locator: ITileLocator, info: IMapDamageInfo): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将指定位置标记为脏,后续访问时会重新计算该点的有来源伤害
|
|
||||||
* @param locator 地图定位符
|
|
||||||
*/
|
|
||||||
markDirty(locator: ITileLocator): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将指定怪物对应的地图伤害标记为脏并刷新
|
|
||||||
* @param view 怪物视图
|
|
||||||
*/
|
|
||||||
markEnemyDirty(view: IEnemyView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于当前上下文重新刷新全部有来源地图伤害
|
|
||||||
*/
|
|
||||||
refreshAll(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除指定怪物带来的全部地图伤害来源
|
|
||||||
* @param view 怪物视图
|
|
||||||
*/
|
|
||||||
deleteEnemy(view: IEnemyView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定位置合并后的地图伤害
|
|
||||||
* @param locator 地图定位符
|
|
||||||
*/
|
|
||||||
getReducedDamage(locator: ITileLocator): Readonly<IMapDamageInfo> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定位置未合并的地图伤害列表
|
|
||||||
* @param locator 地图定位符
|
|
||||||
*/
|
|
||||||
getSeparatedDamage(
|
|
||||||
locator: ITileLocator
|
|
||||||
): Iterable<Readonly<IMapDamageInfo>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 伤害系统
|
|
||||||
|
|
||||||
export interface IEnemyDamageInfo {
|
|
||||||
/** 战斗伤害值 */
|
|
||||||
readonly damage: number;
|
|
||||||
/** 战斗回合数 */
|
|
||||||
readonly turn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IEnemyCritical {
|
|
||||||
/** 此临界点中指定勇士属性的值 */
|
|
||||||
readonly nextValue: number;
|
|
||||||
/** 当前勇士指定属性的值 */
|
|
||||||
readonly baseValue: number;
|
|
||||||
/** 此临界点中指定勇士数值的值与当前值的差,即 `nextValue - baseValue` */
|
|
||||||
readonly nextDiff: number;
|
|
||||||
/** 当前状态下怪物的伤害信息 */
|
|
||||||
readonly baseInfo: IEnemyDamageInfo;
|
|
||||||
/** 此临界点下怪物的伤害信息 */
|
|
||||||
readonly info: IEnemyDamageInfo;
|
|
||||||
/** 此临界点的伤害值与当前伤害值的差 */
|
|
||||||
readonly damageDiff: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CriticalableHeroStatus<THero> = keyof {
|
|
||||||
[P in keyof THero as THero[P] extends number ? P : never]: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IDamageCalculator<TAttr, THero> {
|
|
||||||
/**
|
|
||||||
* 计算战斗伤害信息
|
|
||||||
* @param handler 信息对象
|
|
||||||
*/
|
|
||||||
calculate(handler: IReadonlyEnemyHandler<TAttr, THero>): IEnemyDamageInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取临界计算的上界
|
|
||||||
* @param handler 信息对象
|
|
||||||
* @param attribute 勇士的临界属性
|
|
||||||
*/
|
|
||||||
getCriticalLimit(
|
|
||||||
handler: IReadonlyEnemyHandler<TAttr, THero>,
|
|
||||||
attribute: CriticalableHeroStatus<THero>
|
|
||||||
): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDamageContext<TAttr, THero> {
|
|
||||||
/**
|
|
||||||
* 获取战斗伤害信息
|
|
||||||
* @param enemy 怪物视图
|
|
||||||
*/
|
|
||||||
getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据怪物对象获取战斗伤害信息
|
|
||||||
* @param enemy 怪物对象
|
|
||||||
*/
|
|
||||||
getDamageInfoByComputed(
|
|
||||||
enemy: IReadonlyEnemy<TAttr>
|
|
||||||
): IEnemyDamageInfo | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算怪物在指定勇士属性下的临界
|
|
||||||
* @param enemy 怪物视图
|
|
||||||
* @param attribute 计算临界的目标勇士属性,比如计算攻击临界、自定义属性的临界等等
|
|
||||||
* @param precision 临界计算精度,表示会进行多少次二分计算,一般填写 `12-16` 之间的数即可,默认是 12
|
|
||||||
*/
|
|
||||||
calculateCritical(
|
|
||||||
enemy: IEnemyView<TAttr>,
|
|
||||||
attribute: CriticalableHeroStatus<THero>,
|
|
||||||
precision?: number
|
|
||||||
): Generator<IEnemyCritical, void, void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IDamageSystem<TAttr, THero> extends IDamageContext<
|
|
||||||
TAttr,
|
|
||||||
THero
|
|
||||||
> {
|
|
||||||
/** 伤害系统所属的上下文 */
|
|
||||||
readonly context: IEnemyContext<TAttr, THero>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置当前伤害计算系统使用的伤害计算器
|
|
||||||
* @param calculator 伤害计算器
|
|
||||||
*/
|
|
||||||
useCalculator(calculator: IDamageCalculator<TAttr, THero>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前使用的伤害计算器
|
|
||||||
*/
|
|
||||||
getCalculator(): IDamageCalculator<TAttr, THero> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定勇士信息
|
|
||||||
* @param hero 勇士信息
|
|
||||||
*/
|
|
||||||
bindHeroStatus(hero: IReadonlyHeroAttribute<THero> | null): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将指定的怪物标记为脏
|
|
||||||
* @param enemy 怪物视图
|
|
||||||
*/
|
|
||||||
markDirty(enemy: IEnemyView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除指定的怪物
|
|
||||||
* @param enemy 怪物视图
|
|
||||||
*/
|
|
||||||
deleteEnemy(enemy: IEnemyView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将所有怪物标记为脏
|
|
||||||
*/
|
|
||||||
markAllDirty(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改勇士属性,然后返回修改后勇士属性所组成的计算对象,不影响当前伤害系统的状态
|
|
||||||
* @param modify 勇士修改函数
|
|
||||||
*/
|
|
||||||
with(hero: IHeroAttribute<THero>): IDamageContext<TAttr, THero>;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 上下文
|
|
||||||
|
|
||||||
export interface IEnemyContext<TAttr, THero> {
|
|
||||||
/** 怪物上下文宽度 */
|
|
||||||
readonly width: number;
|
|
||||||
/** 怪物上下文高度 */
|
|
||||||
readonly height: number;
|
|
||||||
/** 此上下文使用的索引对象 */
|
|
||||||
readonly indexer: IMapLocIndexer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调整上下文尺寸,并清空当前上下文中的所有怪物与状态
|
|
||||||
* @param width 地图宽度
|
|
||||||
* @param height 地图高度
|
|
||||||
*/
|
|
||||||
resize(width: number, height: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册一个光环转换器
|
|
||||||
* @param converter 光环转换器
|
|
||||||
*/
|
|
||||||
registerAuraConverter(converter: IAuraConverter<TAttr, THero>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注销一个光环转换器
|
|
||||||
* @param converter 光环转换器
|
|
||||||
*/
|
|
||||||
unregisterAuraConverter(converter: IAuraConverter<TAttr, THero>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置光环转换器的启用状态
|
|
||||||
* @param converter 光环转换器
|
|
||||||
* @param enabled 是否启用
|
|
||||||
*/
|
|
||||||
setAuraConverterEnabled(
|
|
||||||
converter: IAuraConverter<TAttr, THero>,
|
|
||||||
enabled: boolean
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册一个特殊属性查询效果
|
|
||||||
* @param effect 特殊属性查询效果
|
|
||||||
*/
|
|
||||||
registerSpecialQueryEffect(
|
|
||||||
effect: IEnemySpecialQueryEffect<TAttr, THero>
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注销一个特殊属性查询效果
|
|
||||||
* @param effect 特殊属性查询效果
|
|
||||||
*/
|
|
||||||
unregisterSpecialQueryEffect(
|
|
||||||
effect: IEnemySpecialQueryEffect<TAttr, THero>
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 为指定特殊属性代码注册常规查询效果
|
|
||||||
* @param code 特殊属性代码
|
|
||||||
* @param effect 常规查询效果
|
|
||||||
*/
|
|
||||||
registerCommonQueryEffect(
|
|
||||||
code: number,
|
|
||||||
effect: IEnemyCommonQueryEffect<TAttr, THero>
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注销指定特殊属性代码上的常规查询效果
|
|
||||||
* @param code 特殊属性代码
|
|
||||||
* @param effect 常规查询效果
|
|
||||||
*/
|
|
||||||
unregisterCommonQueryEffect(
|
|
||||||
code: number,
|
|
||||||
effect: IEnemyCommonQueryEffect<TAttr, THero>
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册一个最终效果
|
|
||||||
* @param effect 最终效果
|
|
||||||
*/
|
|
||||||
registerFinalEffect(effect: IEnemyFinalEffect<TAttr, THero>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注销一个最终效果
|
|
||||||
* @param effect 最终效果
|
|
||||||
*/
|
|
||||||
unregisterFinalEffect(effect: IEnemyFinalEffect<TAttr, THero>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定勇士对象
|
|
||||||
* @param hero 勇士属性对象
|
|
||||||
*/
|
|
||||||
bindHero(hero: IReadonlyHeroAttribute<THero> | null): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前绑定的勇士属性对象
|
|
||||||
*/
|
|
||||||
getBindedHero(): IReadonlyHeroAttribute<THero> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定怪物对象当前所在位置
|
|
||||||
* @param enemy 怪物对象
|
|
||||||
*/
|
|
||||||
getEnemyLocator(enemy: IEnemy<TAttr>): Readonly<ITileLocator> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定怪物视图当前所在位置
|
|
||||||
* @param view 怪物视图
|
|
||||||
*/
|
|
||||||
getEnemyLocatorByView(
|
|
||||||
view: IEnemyView<TAttr>
|
|
||||||
): Readonly<ITileLocator> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据定位符获取怪物视图
|
|
||||||
* @param locator 地图定位符
|
|
||||||
*/
|
|
||||||
getEnemyByLocator(locator: ITileLocator): IEnemyView<TAttr> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据坐标获取怪物视图
|
|
||||||
* @param x 横坐标
|
|
||||||
* @param y 纵坐标
|
|
||||||
*/
|
|
||||||
getEnemyByLoc(x: number, y: number): IEnemyView<TAttr> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据计算后怪物对象反查怪物视图
|
|
||||||
* @param enemy 计算后怪物对象
|
|
||||||
*/
|
|
||||||
getViewByComputed(enemy: IReadonlyEnemy<TAttr>): IEnemyView<TAttr> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在指定位置放置一个怪物对象
|
|
||||||
* @param locator 地图定位符
|
|
||||||
* @param enemy 怪物对象
|
|
||||||
*/
|
|
||||||
setEnemyAt(locator: ITileLocator, enemy: IEnemy<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除指定位置的怪物
|
|
||||||
* @param locator 地图定位符
|
|
||||||
*/
|
|
||||||
deleteEnemy(locator: ITileLocator): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫描指定范围内的怪物视图
|
|
||||||
* @param range 范围对象
|
|
||||||
* @param param 范围参数
|
|
||||||
*/
|
|
||||||
scanRange<T>(
|
|
||||||
range: IRange<T>,
|
|
||||||
param: T
|
|
||||||
): Iterable<[ITileLocator, IEnemyView<TAttr>]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 迭代上下文中的全部怪物
|
|
||||||
*/
|
|
||||||
iterateEnemy(): Iterable<[ITileLocator, IEnemyView<TAttr>]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加一个全局光环视图
|
|
||||||
* @param aura 光环视图
|
|
||||||
*/
|
|
||||||
addAura(aura: IAuraView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除一个全局光环视图
|
|
||||||
* @param aura 光环视图
|
|
||||||
*/
|
|
||||||
deleteAura(aura: IAuraView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定地图伤害管理器
|
|
||||||
* @param damage 地图伤害管理器
|
|
||||||
*/
|
|
||||||
attachMapDamage(damage: IMapDamage<TAttr, THero> | null): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前绑定的地图伤害管理器
|
|
||||||
*/
|
|
||||||
getMapDamage(): IMapDamage<TAttr, THero> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绑定伤害计算系统
|
|
||||||
* @param system 伤害系统
|
|
||||||
*/
|
|
||||||
attachDamageSystem(system: IDamageSystem<TAttr, unknown> | null): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前绑定的伤害计算系统
|
|
||||||
*/
|
|
||||||
getDamageSystem(): IDamageSystem<TAttr, THero> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重建当前上下文中的全部怪物计算结果
|
|
||||||
*
|
|
||||||
* 1. 对所有光环及特殊查询进行构建操作,这一步中会决定每个怪物所拥有的特殊属性,后续不会变动
|
|
||||||
* 2. 执行所有的普通光环效果,修改怪物的基础属性
|
|
||||||
* 3. 执行常规查询效果,允许查询上下文状态并修改怪物自身的基础属性
|
|
||||||
* 4. 执行最终效果,不允许查询上下文状态,仅允许修改怪物自身的基础属性
|
|
||||||
*/
|
|
||||||
buildup(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将指定怪物视图标记为脏
|
|
||||||
* @param view 怪物视图
|
|
||||||
*/
|
|
||||||
markDirty(view: IEnemyView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 申请刷新指定怪物视图
|
|
||||||
* @param view 怪物视图
|
|
||||||
*/
|
|
||||||
requestRefresh(view: IEnemyView<TAttr>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空当前上下文中的所有对象与运行状态
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁当前上下文
|
|
||||||
*/
|
|
||||||
destroy(): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
import { ITileLocator } from '@motajs/common';
|
|
||||||
import { IMapLocIndexer } from './types';
|
|
||||||
|
|
||||||
export class MapLocIndexer implements IMapLocIndexer {
|
|
||||||
private width: number = 0;
|
|
||||||
|
|
||||||
setWidth(width: number): void {
|
|
||||||
this.width = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
locToIndex(x: number, y: number): number {
|
|
||||||
return y * this.width + x;
|
|
||||||
}
|
|
||||||
|
|
||||||
locaterToIndex(locator: ITileLocator): number {
|
|
||||||
return locator.y * this.width + locator.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
indexToLocator(index: number): ITileLocator {
|
|
||||||
return {
|
|
||||||
x: index % this.width,
|
|
||||||
y: Math.floor(index / this.width)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,7 +2,7 @@ import { IFlagCommonField, IFlagSystem, IFlagSystemSave } from './types';
|
|||||||
import { FlagCommonField } from './field';
|
import { FlagCommonField } from './field';
|
||||||
|
|
||||||
export class FlagSystem implements IFlagSystem {
|
export class FlagSystem implements IFlagSystem {
|
||||||
private readonly fieldMap: Map<PropertyKey, FlagCommonField<any>> =
|
private readonly fieldMap: Map<PropertyKey, IFlagCommonField<any>> =
|
||||||
new Map();
|
new Map();
|
||||||
|
|
||||||
occupied(field: PropertyKey): boolean {
|
occupied(field: PropertyKey): boolean {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
//#region 字段
|
//#region 字段
|
||||||
|
|
||||||
|
import { ISaveableContent } from '../common';
|
||||||
|
|
||||||
export interface IFlagCommonField<T> {
|
export interface IFlagCommonField<T> {
|
||||||
/** 此字段所处的 Flag 系统 */
|
/** 此字段所处的 Flag 系统 */
|
||||||
readonly system: IFlagSystem;
|
readonly system: IFlagSystem;
|
||||||
@ -42,7 +44,7 @@ export interface IFlagSystemSave {
|
|||||||
readonly fields: Map<PropertyKey, any>;
|
readonly fields: Map<PropertyKey, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFlagSystem {
|
export interface IFlagSystem extends ISaveableContent<IFlagSystemSave> {
|
||||||
/**
|
/**
|
||||||
* 判断一个字段是否被占用,类似于旧样板的 `core.hasFlag`
|
* 判断一个字段是否被占用,类似于旧样板的 `core.hasFlag`
|
||||||
* @param field 字段名称
|
* @param field 字段名称
|
||||||
@ -131,17 +133,6 @@ export interface IFlagSystem {
|
|||||||
* @param defaultValue 字段默认值
|
* @param defaultValue 字段默认值
|
||||||
*/
|
*/
|
||||||
getFieldValueDefaults<T>(field: PropertyKey, defaultValue: T): T;
|
getFieldValueDefaults<T>(field: PropertyKey, defaultValue: T): T;
|
||||||
|
|
||||||
/**
|
|
||||||
* 对 Flag 系统进行结构化复制,形成存档对象
|
|
||||||
*/
|
|
||||||
saveState(): IFlagSystemSave;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从指定存档对象读取信息
|
|
||||||
* @param state 存档对象
|
|
||||||
*/
|
|
||||||
loadState(state: IFlagSystemSave): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { logger } from '@motajs/common';
|
import { logger } from '@motajs/common';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
import { IHeroAttribute, IHeroModifier } from './types';
|
import { IHeroAttribute, IHeroModifier } from './types';
|
||||||
|
|
||||||
export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V> {
|
export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V, V> {
|
||||||
|
abstract readonly type: string;
|
||||||
abstract readonly priority: number;
|
abstract readonly priority: number;
|
||||||
|
|
||||||
owner: IHeroAttribute<unknown> | null = null;
|
owner: IHeroAttribute<unknown> | null = null;
|
||||||
@ -25,6 +27,14 @@ export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V> {
|
|||||||
this.owner = attribute;
|
this.owner = attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveState(_compression: SaveCompression): V {
|
||||||
|
return this.currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(state: V, _compression: SaveCompression): void {
|
||||||
|
this.setValue(state);
|
||||||
|
}
|
||||||
|
|
||||||
abstract modify(value: T, baseValue: T, name: string): T;
|
abstract modify(value: T, baseValue: T, name: string): T;
|
||||||
|
|
||||||
abstract clone(): IHeroModifier<T, V>;
|
abstract clone(): IHeroModifier<T, V>;
|
||||||
@ -161,4 +171,14 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
|||||||
getModifiableClone(): IHeroAttribute<THero> {
|
getModifiableClone(): IHeroAttribute<THero> {
|
||||||
return this.clone();
|
return this.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toStructured(): THero {
|
||||||
|
return structuredClone(this.attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateModifiers(): IterableIterator<[keyof THero, IHeroModifier]> {
|
||||||
|
for (const [modifier, name] of this.modifierName) {
|
||||||
|
yield [name, modifier];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,20 @@
|
|||||||
|
import { HeroAttribute } from './attribute';
|
||||||
import {
|
import {
|
||||||
IHeroAttribute,
|
IHeroAttribute,
|
||||||
|
IHeroModifier,
|
||||||
IHeroMover,
|
IHeroMover,
|
||||||
IHeroState,
|
IHeroState,
|
||||||
|
IHeroStateSave,
|
||||||
|
IModifierStateSave,
|
||||||
IReadonlyHeroAttribute
|
IReadonlyHeroAttribute
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
|
||||||
export class HeroState<THero> implements IHeroState<THero> {
|
export class HeroState<THero> implements IHeroState<THero> {
|
||||||
|
/** 修饰器工厂函数注册表 */
|
||||||
|
private readonly registry: Map<string, () => IHeroModifier> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public mover: IHeroMover,
|
public mover: IHeroMover,
|
||||||
public attribute: IHeroAttribute<THero>
|
public attribute: IHeroAttribute<THero>
|
||||||
@ -34,4 +43,74 @@ export class HeroState<THero> implements IHeroState<THero> {
|
|||||||
getIsolatedAttribute(): IHeroAttribute<THero> {
|
getIsolatedAttribute(): IHeroAttribute<THero> {
|
||||||
return this.attribute.getModifiableClone();
|
return this.attribute.getModifiableClone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerModifier(type: string, cons: () => IHeroModifier): void {
|
||||||
|
this.registry.set(type, cons);
|
||||||
|
}
|
||||||
|
|
||||||
|
createModifier<T, V>(type: string): IHeroModifier<T, V> | null {
|
||||||
|
const cons = this.registry.get(type);
|
||||||
|
if (!cons) {
|
||||||
|
logger.warn(116, type);
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return cons() as IHeroModifier<T, V>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createAndInsertModifier<K extends keyof THero, V>(
|
||||||
|
type: string,
|
||||||
|
name: K
|
||||||
|
): IHeroModifier<THero[K], V> | null {
|
||||||
|
const modifier = this.createModifier<THero[K], V>(type);
|
||||||
|
if (!modifier) return null;
|
||||||
|
this.attribute.addModifier(name, modifier);
|
||||||
|
return modifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(compression: SaveCompression): IHeroStateSave<THero> {
|
||||||
|
const modifiers: IModifierStateSave[] = [];
|
||||||
|
for (const [name, modifier] of this.attribute.iterateModifiers()) {
|
||||||
|
modifiers.push({
|
||||||
|
name,
|
||||||
|
type: modifier.type,
|
||||||
|
state: modifier.saveState(compression)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
attribute: this.attribute.toStructured(),
|
||||||
|
locator: {
|
||||||
|
x: this.mover.x,
|
||||||
|
y: this.mover.y,
|
||||||
|
direction: this.mover.direction
|
||||||
|
},
|
||||||
|
followers: structuredClone(this.mover.followers),
|
||||||
|
modifiers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(
|
||||||
|
state: IHeroStateSave<THero>,
|
||||||
|
compression: SaveCompression
|
||||||
|
): void {
|
||||||
|
const newAttribute = new HeroAttribute<THero>(state.attribute);
|
||||||
|
for (const save of state.modifiers) {
|
||||||
|
const cons = this.registry.get(save.type);
|
||||||
|
if (!cons) continue;
|
||||||
|
const modifier = cons();
|
||||||
|
modifier.loadState(save.state as never, compression);
|
||||||
|
newAttribute.addModifier(
|
||||||
|
save.name as keyof THero,
|
||||||
|
modifier as unknown as IHeroModifier<THero[keyof THero]>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.attribute = newAttribute;
|
||||||
|
this.mover.setPosition(state.locator.x, state.locator.y);
|
||||||
|
this.mover.turn(state.locator.direction);
|
||||||
|
this.mover.removeAllFollowers();
|
||||||
|
state.followers.forEach(follower => {
|
||||||
|
this.mover.addFollower(follower.num, follower.identifier);
|
||||||
|
this.mover.setFollowerAlpha(follower.identifier, follower.alpha);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import { IHookBase, IHookable } from '@motajs/common';
|
import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common';
|
||||||
import { FaceDirection } from '@user/data-state';
|
import { FaceDirection, ISaveableContent } from '../common';
|
||||||
|
|
||||||
//#region 勇士属性
|
//#region 勇士属性
|
||||||
|
|
||||||
export interface IHeroModifier<T = unknown, V = unknown> {
|
export interface IHeroModifier<
|
||||||
|
T = unknown,
|
||||||
|
V = unknown,
|
||||||
|
S = unknown
|
||||||
|
> extends ISaveableContent<S> {
|
||||||
|
/** 修饰器类型 */
|
||||||
|
readonly type: string;
|
||||||
/** 修饰器优先级 */
|
/** 修饰器优先级 */
|
||||||
readonly priority: number;
|
readonly priority: number;
|
||||||
/** 修饰器参数值 */
|
/** 修饰器参数值 */
|
||||||
@ -42,6 +48,15 @@ export interface IHeroModifier<T = unknown, V = unknown> {
|
|||||||
clone(): IHeroModifier<T, V>;
|
clone(): IHeroModifier<T, V>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IModifierStateSave {
|
||||||
|
/** 属性名称 */
|
||||||
|
readonly name: PropertyKey;
|
||||||
|
/** 修饰器类型 */
|
||||||
|
readonly type: string;
|
||||||
|
/** 修饰器存档数据 */
|
||||||
|
readonly state: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IReadonlyHeroAttribute<THero> {
|
export interface IReadonlyHeroAttribute<THero> {
|
||||||
/**
|
/**
|
||||||
* 获取勇士的基础属性,即未经过任何 Buff 或装备等加成的属性
|
* 获取勇士的基础属性,即未经过任何 Buff 或装备等加成的属性
|
||||||
@ -77,6 +92,16 @@ export interface IReadonlyHeroAttribute<THero> {
|
|||||||
* 获取此勇士属性对象的可修改副本
|
* 获取此勇士属性对象的可修改副本
|
||||||
*/
|
*/
|
||||||
getModifiableClone(): IHeroAttribute<THero>;
|
getModifiableClone(): IHeroAttribute<THero>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为结构化对象
|
||||||
|
*/
|
||||||
|
toStructured(): THero;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遍历所有已挂载的属性修饰器
|
||||||
|
*/
|
||||||
|
iterateModifiers(): Iterable<[PropertyKey, IHeroModifier]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
|
export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
|
||||||
@ -332,7 +357,20 @@ export interface IHeroMover extends IHookable<IHeroMovingHooks> {
|
|||||||
|
|
||||||
//#region 勇士状态
|
//#region 勇士状态
|
||||||
|
|
||||||
export interface IHeroState<THero> {
|
export interface IHeroStateSave<THero> {
|
||||||
|
/** 勇士属性状态 */
|
||||||
|
readonly attribute: THero;
|
||||||
|
/** 勇士当前位置 */
|
||||||
|
readonly locator: IFacedTileLocator;
|
||||||
|
/** 勇士当前的跟随者 */
|
||||||
|
readonly followers: readonly Readonly<IHeroFollower>[];
|
||||||
|
/** 勇士属性修饰器状态 */
|
||||||
|
readonly modifiers: readonly IModifierStateSave[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHeroState<THero> extends ISaveableContent<
|
||||||
|
IHeroStateSave<THero>
|
||||||
|
> {
|
||||||
/** 勇士移动对象 */
|
/** 勇士移动对象 */
|
||||||
readonly mover: IHeroMover;
|
readonly mover: IHeroMover;
|
||||||
/** 勇士属性对象 */
|
/** 勇士属性对象 */
|
||||||
@ -369,6 +407,29 @@ export interface IHeroState<THero> {
|
|||||||
* 获取独立勇士属性对象,修改此对象不会影响勇士本身的属性
|
* 获取独立勇士属性对象,修改此对象不会影响勇士本身的属性
|
||||||
*/
|
*/
|
||||||
getIsolatedAttribute(): IHeroAttribute<THero>;
|
getIsolatedAttribute(): IHeroAttribute<THero>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个修饰器工厂函数
|
||||||
|
* @param type 修饰器类型
|
||||||
|
* @param cons 工厂函数
|
||||||
|
*/
|
||||||
|
registerModifier(type: string, cons: () => IHeroModifier): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建指定类型的修饰器实例
|
||||||
|
* @param type 修饰器类型
|
||||||
|
*/
|
||||||
|
createModifier<T, V>(type: string): IHeroModifier<T, V> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建指定类型的修饰器实例并插入至勇士属性对象
|
||||||
|
* @param type 修饰器类型
|
||||||
|
* @param name 属性名称
|
||||||
|
*/
|
||||||
|
createAndInsertModifier<K extends keyof THero, V>(
|
||||||
|
type: string,
|
||||||
|
name: K
|
||||||
|
): IHeroModifier<THero[K], V> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@ -3,5 +3,7 @@ export * from './enemy';
|
|||||||
export * from './flag';
|
export * from './flag';
|
||||||
export * from './hero';
|
export * from './hero';
|
||||||
export * from './load';
|
export * from './load';
|
||||||
|
export * from './map';
|
||||||
|
|
||||||
export * from './game';
|
export * from './game';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
142
packages-user/data-base/src/map/dynamicLayer.ts
Normal file
142
packages-user/data-base/src/map/dynamicLayer.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
HookController,
|
||||||
|
Hookable,
|
||||||
|
IHookController,
|
||||||
|
ITileLocator,
|
||||||
|
logger
|
||||||
|
} from '@motajs/common';
|
||||||
|
import {
|
||||||
|
IDynamicLayer,
|
||||||
|
IDynamicLayerHooks,
|
||||||
|
IDynamicTile,
|
||||||
|
IMapLayer
|
||||||
|
} from './types';
|
||||||
|
import { FaceDirection, degradeFace } from '../common';
|
||||||
|
import { DynamicTile } from './dynamicTile';
|
||||||
|
|
||||||
|
export class DynamicLayer
|
||||||
|
extends Hookable<IDynamicLayerHooks>
|
||||||
|
implements IDynamicLayer
|
||||||
|
{
|
||||||
|
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x,不使用 index 是为了支持地图外图块 */
|
||||||
|
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
|
||||||
|
new Map();
|
||||||
|
/** 动态图块到其当前坐标的映射 */
|
||||||
|
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
|
||||||
|
|
||||||
|
constructor(public readonly layer: IMapLayer) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createController(
|
||||||
|
hook: Partial<IDynamicLayerHooks>
|
||||||
|
): IHookController<IDynamicLayerHooks> {
|
||||||
|
return new HookController(this, hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDynamic(num: number, x: number, y: number): IDynamicTile {
|
||||||
|
const tile = new DynamicTile(num, x, y, this);
|
||||||
|
this.addTileToPosMap(tile, x, y);
|
||||||
|
this.posTileMap.set(tile, { x, y });
|
||||||
|
this.forEachHook(hook => hook.onCreateTile?.(tile, this));
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
transferToDynamic(x: number, y: number): IDynamicTile {
|
||||||
|
const num = this.layer.getBlock(x, y);
|
||||||
|
if (num === 0) {
|
||||||
|
logger.warn(127, x.toString(), y.toString());
|
||||||
|
}
|
||||||
|
this.layer.setBlock(0, x, y);
|
||||||
|
return this.createDynamic(num, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
transferToStatic(tile: IDynamicTile): void {
|
||||||
|
const { x, y } = tile;
|
||||||
|
const { width, height } = this.layer;
|
||||||
|
if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||||
|
logger.warn(128, x.toString(), y.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.layer.getBlock(x, y) !== 0) {
|
||||||
|
logger.warn(129, x.toString(), y.toString());
|
||||||
|
}
|
||||||
|
this.layer.setBlock(tile.num, x, y);
|
||||||
|
this.removeTile(tile);
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
void hook.onDeleteTile?.(tile, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transferToStaticIfSafe(tile: IDynamicTile): boolean {
|
||||||
|
if (this.layer.getBlock(tile.x, tile.y) !== 0) return false;
|
||||||
|
this.layer.setBlock(tile.num, tile.x, tile.y);
|
||||||
|
this.deleteDynamic(tile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDynamic(tile: IDynamicTile): Promise<void> {
|
||||||
|
if (!this.posTileMap.has(tile)) {
|
||||||
|
logger.warn(130);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.removeTile(tile);
|
||||||
|
const hooks = this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||||
|
await Promise.all(hooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile> {
|
||||||
|
return this.tilePosMap.get(y)?.get(x) ?? new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
iterateDynamicTiles(): Iterable<IDynamicTile> {
|
||||||
|
return this.posTileMap.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void {
|
||||||
|
const numBefore = tile.num;
|
||||||
|
tile.setFaceDirection(direction);
|
||||||
|
if (tile.num !== numBefore) return;
|
||||||
|
const degraded = degradeFace(direction);
|
||||||
|
if (degraded !== direction) {
|
||||||
|
tile.setFaceDirection(degraded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDynamicTile(tile: IDynamicTile): void {
|
||||||
|
const oldPos = this.posTileMap.get(tile);
|
||||||
|
if (oldPos) {
|
||||||
|
this.removeTileFromPosMap(tile, oldPos.x, oldPos.y);
|
||||||
|
oldPos.x = tile.x;
|
||||||
|
oldPos.y = tile.y;
|
||||||
|
this.addTileToPosMap(tile, tile.x, tile.y);
|
||||||
|
} else {
|
||||||
|
this.addTileToPosMap(tile, tile.x, tile.y);
|
||||||
|
this.posTileMap.set(tile, { x: tile.x, y: tile.y });
|
||||||
|
}
|
||||||
|
this.forEachHook(hook => hook.onUpdateTilePosition?.(tile, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void {
|
||||||
|
const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map());
|
||||||
|
const set = xMap.getOrInsertComputed(x, () => new Set());
|
||||||
|
set.add(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeTileFromPosMap(
|
||||||
|
tile: IDynamicTile,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
): void {
|
||||||
|
this.tilePosMap.get(y)?.get(x)?.delete(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从两个内部映射中移除图块记录 */
|
||||||
|
private removeTile(tile: IDynamicTile): void {
|
||||||
|
const pos = this.posTileMap.get(tile);
|
||||||
|
if (pos) {
|
||||||
|
this.removeTileFromPosMap(tile, pos.x, pos.y);
|
||||||
|
}
|
||||||
|
this.posTileMap.delete(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
packages-user/data-base/src/map/dynamicTile.ts
Normal file
72
packages-user/data-base/src/map/dynamicTile.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
import {
|
||||||
|
FaceDirection,
|
||||||
|
IMoverController,
|
||||||
|
IObjectMover,
|
||||||
|
IRoleFaceBinder
|
||||||
|
} from '../common';
|
||||||
|
import { IDynamicLayer, IDynamicTile } from './types';
|
||||||
|
import { DynamicTileMover } from './mover';
|
||||||
|
|
||||||
|
export class DynamicTile implements IDynamicTile {
|
||||||
|
readonly mover: IObjectMover<IDynamicTile>;
|
||||||
|
|
||||||
|
/** 当前的朝向绑定对象 */
|
||||||
|
private face: IRoleFaceBinder | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public num: number,
|
||||||
|
public x: number,
|
||||||
|
public y: number,
|
||||||
|
public readonly layer: IDynamicLayer
|
||||||
|
) {
|
||||||
|
this.mover = new DynamicTileMover(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFaceBinder(binder: IRoleFaceBinder | null): void {
|
||||||
|
this.face = binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFaceDirection(direction: FaceDirection): number {
|
||||||
|
if (!this.face) return this.num;
|
||||||
|
const next = this.face.getFaceOf(this.num, direction);
|
||||||
|
if (next) {
|
||||||
|
this.num = next.identifier;
|
||||||
|
}
|
||||||
|
return this.num;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(): Promise<void> {
|
||||||
|
return this.layer.deleteDynamic(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toStatic(): void {
|
||||||
|
this.layer.transferToStatic(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
toStaticIfSafe(): boolean {
|
||||||
|
return this.layer.transferToStaticIfSafe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
step(dir: FaceDirection, count?: number): IMoverController | null {
|
||||||
|
if (this.mover.moving) return null;
|
||||||
|
this.mover.step(dir, count);
|
||||||
|
return this.mover.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPos(x: number, y: number): void {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.layer.updateDynamicTile(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentFaceDirection(): FaceDirection {
|
||||||
|
if (this.face) {
|
||||||
|
const face = this.face.getFaceDirection(this.num);
|
||||||
|
if (isNil(face)) return FaceDirection.Unknown;
|
||||||
|
else return face;
|
||||||
|
} else {
|
||||||
|
return FaceDirection.Down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
export * from './layerState';
|
export * from './layerState';
|
||||||
export * from './mapLayer';
|
export * from './mapLayer';
|
||||||
|
export * from './mapStore';
|
||||||
|
export * from './mover';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
@ -18,6 +18,8 @@ export class LayerState
|
|||||||
implements ILayerState
|
implements ILayerState
|
||||||
{
|
{
|
||||||
readonly layerList: Set<IMapLayer> = new Set();
|
readonly layerList: Set<IMapLayer> = new Set();
|
||||||
|
/** 具体 MapLayer 实例列表,供内部 resize 使用 */
|
||||||
|
private readonly mapLayerList: Set<MapLayer> = new Set();
|
||||||
/** 图层到图层别名映射 */
|
/** 图层到图层别名映射 */
|
||||||
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
|
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
|
||||||
/** 图层别名到图层的映射 */
|
/** 图层别名到图层的映射 */
|
||||||
@ -29,10 +31,24 @@ export class LayerState
|
|||||||
/** 图层钩子映射 */
|
/** 图层钩子映射 */
|
||||||
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
||||||
|
|
||||||
addLayer(width: number, height: number): IMapLayer {
|
active: boolean = false;
|
||||||
const array = new Uint32Array(width * height);
|
eventLayer: IMapLayer | null = null;
|
||||||
const layer = new MapLayer(array, width, height);
|
|
||||||
|
/** 楼层级脏标记 */
|
||||||
|
private dirty: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public width: number,
|
||||||
|
public height: number
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
addLayer(): IMapLayer {
|
||||||
|
const array = new Uint32Array(this.width * this.height);
|
||||||
|
const layer = new MapLayer(array, this.width, this.height);
|
||||||
this.layerList.add(layer);
|
this.layerList.add(layer);
|
||||||
|
this.mapLayerList.add(layer);
|
||||||
this.forEachHook(hook => {
|
this.forEachHook(hook => {
|
||||||
hook.onUpdateLayer?.(this.layerList);
|
hook.onUpdateLayer?.(this.layerList);
|
||||||
});
|
});
|
||||||
@ -44,6 +60,7 @@ export class LayerState
|
|||||||
|
|
||||||
removeLayer(layer: IMapLayer): void {
|
removeLayer(layer: IMapLayer): void {
|
||||||
this.layerList.delete(layer);
|
this.layerList.delete(layer);
|
||||||
|
this.mapLayerList.delete(layer as MapLayer);
|
||||||
const alias = this.layerAliasMap.get(layer);
|
const alias = this.layerAliasMap.get(layer);
|
||||||
if (alias) {
|
if (alias) {
|
||||||
const symbol = Symbol.for(alias);
|
const symbol = Symbol.for(alias);
|
||||||
@ -83,17 +100,20 @@ export class LayerState
|
|||||||
}
|
}
|
||||||
|
|
||||||
resizeLayer(
|
resizeLayer(
|
||||||
layer: IMapLayer,
|
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
keepBlock: boolean = false
|
keepBlock: boolean = false
|
||||||
): void {
|
): void {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
for (const layer of this.mapLayerList) {
|
||||||
if (keepBlock) {
|
if (keepBlock) {
|
||||||
layer.resize(width, height);
|
layer.resize(width, height);
|
||||||
} else {
|
} else {
|
||||||
layer.resize2(width, height);
|
layer.resize2(width, height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setBackground(tile: number): void {
|
setBackground(tile: number): void {
|
||||||
this.backgroundTile = tile;
|
this.backgroundTile = tile;
|
||||||
@ -106,6 +126,29 @@ export class LayerState
|
|||||||
return this.backgroundTile;
|
return this.backgroundTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveStatus(active: boolean): void {
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEventLayer(layer: IMapLayer | null): void {
|
||||||
|
if (!layer) {
|
||||||
|
this.eventLayer = null;
|
||||||
|
} else {
|
||||||
|
if (!this.layerList.has(layer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.eventLayer = layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty(): boolean {
|
||||||
|
return this.dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirty(dirty: boolean): void {
|
||||||
|
this.dirty = dirty;
|
||||||
|
}
|
||||||
|
|
||||||
protected createController(
|
protected createController(
|
||||||
hook: Partial<ILayerStateHooks>
|
hook: Partial<ILayerStateHooks>
|
||||||
): IHookController<ILayerStateHooks> {
|
): IHookController<ILayerStateHooks> {
|
||||||
@ -120,18 +163,21 @@ class StateMapLayerHook implements Partial<IMapLayerHooks> {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
onUpdateArea(x: number, y: number, width: number, height: number): void {
|
onUpdateArea(x: number, y: number, width: number, height: number): void {
|
||||||
|
this.state.setDirty(true);
|
||||||
this.state.forEachHook(hook => {
|
this.state.forEachHook(hook => {
|
||||||
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
|
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateBlock(block: number, x: number, y: number): void {
|
onUpdateBlock(block: number, x: number, y: number): void {
|
||||||
|
this.state.setDirty(true);
|
||||||
this.state.forEachHook(hook => {
|
this.state.forEachHook(hook => {
|
||||||
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
|
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize(width: number, height: number): void {
|
onResize(width: number, height: number): void {
|
||||||
|
this.state.setDirty(true);
|
||||||
this.state.forEachHook(hook => {
|
this.state.forEachHook(hook => {
|
||||||
hook.onResizeLayer?.(this.layer, width, height);
|
hook.onResizeLayer?.(this.layer, width, height);
|
||||||
});
|
});
|
||||||
@ -1,11 +1,13 @@
|
|||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
|
IDynamicLayer,
|
||||||
IMapLayer,
|
IMapLayer,
|
||||||
IMapLayerData,
|
IMapLayerData,
|
||||||
IMapLayerHookController,
|
IMapLayerHookController,
|
||||||
IMapLayerHooks
|
IMapLayerHooks
|
||||||
} from './types';
|
} from './types';
|
||||||
import { Hookable, HookController, logger } from '@motajs/common';
|
import { Hookable, HookController, logger } from '@motajs/common';
|
||||||
|
import { DynamicLayer } from './dynamicLayer';
|
||||||
|
|
||||||
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口
|
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口
|
||||||
|
|
||||||
@ -23,6 +25,8 @@ export class MapLayer
|
|||||||
/** 地图数据引用 */
|
/** 地图数据引用 */
|
||||||
private mapData: IMapLayerData;
|
private mapData: IMapLayerData;
|
||||||
|
|
||||||
|
readonly dynamicLayer: IDynamicLayer;
|
||||||
|
|
||||||
constructor(array: Uint32Array, width: number, height: number) {
|
constructor(array: Uint32Array, width: number, height: number) {
|
||||||
super();
|
super();
|
||||||
this.width = width;
|
this.width = width;
|
||||||
@ -35,6 +39,7 @@ export class MapLayer
|
|||||||
expired: false,
|
expired: false,
|
||||||
array: this.mapArray
|
array: this.mapArray
|
||||||
};
|
};
|
||||||
|
this.dynamicLayer = new DynamicLayer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(width: number, height: number): void {
|
resize(width: number, height: number): void {
|
||||||
@ -186,14 +191,12 @@ export class MapLayer
|
|||||||
}
|
}
|
||||||
const res = new Uint32Array(width * height);
|
const res = new Uint32Array(width * height);
|
||||||
const arr = this.mapArray;
|
const arr = this.mapArray;
|
||||||
const nr = Math.min(r, w);
|
|
||||||
const nb = Math.min(b, h);
|
const nb = Math.min(b, h);
|
||||||
for (let nx = x; nx < nr; nx++) {
|
|
||||||
for (let ny = y; ny < nb; ny++) {
|
for (let ny = y; ny < nb; ny++) {
|
||||||
const origin = ny * w + nx;
|
const lineStart = ny * w + x;
|
||||||
const target = (ny - y) * width + (nx - x);
|
const lineEnd = lineStart + width;
|
||||||
res[target] = arr[origin];
|
const dy = ny - y;
|
||||||
}
|
res.set(arr.subarray(lineStart, lineEnd), dy * width);
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -205,6 +208,27 @@ export class MapLayer
|
|||||||
return this.mapData;
|
return this.mapData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMapRef(array: Uint32Array): void {
|
||||||
|
if (array.length !== this.width * this.height) {
|
||||||
|
logger.warn(
|
||||||
|
123,
|
||||||
|
array.length.toString(),
|
||||||
|
(this.width * this.height).toString()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mapData.expired = true;
|
||||||
|
this.mapArray = array;
|
||||||
|
this.mapData = {
|
||||||
|
expired: false,
|
||||||
|
array: this.mapArray
|
||||||
|
};
|
||||||
|
this.empty = !array.some(v => v !== 0);
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateArea?.(0, 0, this.width, this.height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected createController(
|
protected createController(
|
||||||
hook: Partial<IMapLayerHooks>
|
hook: Partial<IMapLayerHooks>
|
||||||
): IMapLayerHookController {
|
): IMapLayerHookController {
|
||||||
472
packages-user/data-base/src/map/mapStore.ts
Normal file
472
packages-user/data-base/src/map/mapStore.ts
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
|
import {
|
||||||
|
ILayerState,
|
||||||
|
ILayerStateSave,
|
||||||
|
IMapLayer,
|
||||||
|
IMapLayerSave,
|
||||||
|
IMapStore,
|
||||||
|
IMapStoreSave,
|
||||||
|
MapArea
|
||||||
|
} from './types';
|
||||||
|
import { LayerState } from './layerState';
|
||||||
|
import { uniq } from 'lodash-es';
|
||||||
|
|
||||||
|
export class MapStore implements IMapStore {
|
||||||
|
/** 楼层 id 到状态对象的映射 */
|
||||||
|
private readonly mapData: Map<string, LayerState> = new Map();
|
||||||
|
|
||||||
|
/** 所有楼层 id 的有序数组 */
|
||||||
|
readonly maps: string[] = [];
|
||||||
|
|
||||||
|
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */
|
||||||
|
private refData: Map<string, Map<number, Uint32Array>> | null = null;
|
||||||
|
|
||||||
|
/** 分区列表 */
|
||||||
|
private areaList: Set<MapArea> = new Set();
|
||||||
|
|
||||||
|
/** 上一次调用 notifyEnterFloor 传入的楼层 id */
|
||||||
|
private lastFloorId: string | null = null;
|
||||||
|
|
||||||
|
/** 自动分区激活器开关 */
|
||||||
|
private autoActivitorEnabled: boolean = false;
|
||||||
|
|
||||||
|
//#region 楼层管理
|
||||||
|
|
||||||
|
createLayerState(id: string, width: number, height: number): ILayerState {
|
||||||
|
if (this.mapData.has(id)) {
|
||||||
|
logger.warn(121, id);
|
||||||
|
} else {
|
||||||
|
this.maps.push(id);
|
||||||
|
}
|
||||||
|
const state = new LayerState(width, height);
|
||||||
|
// 若 refData 已存在,新楼层直接视为全脏
|
||||||
|
if (this.refData !== null) {
|
||||||
|
state.setDirty(true);
|
||||||
|
}
|
||||||
|
this.mapData.set(id, state);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapList(maps: string[]): void {
|
||||||
|
this.maps.length = 0;
|
||||||
|
this.maps.push(...uniq(maps));
|
||||||
|
}
|
||||||
|
|
||||||
|
useManualOrder(sort: (arr: string[]) => string[]): void {
|
||||||
|
const copy = this.maps.slice();
|
||||||
|
const sorted = sort(copy);
|
||||||
|
const oldSet = new Set(this.maps);
|
||||||
|
const newSet = new Set(sorted);
|
||||||
|
if (oldSet.size !== newSet.size || !newSet.isSubsetOf(oldSet)) {
|
||||||
|
logger.warn(125);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.maps.length = 0;
|
||||||
|
this.maps.push(...uniq(sorted));
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerState(id: string): ILayerState | null {
|
||||||
|
return this.mapData.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveMap(id: string): ILayerState | null {
|
||||||
|
const state = this.mapData.get(id);
|
||||||
|
if (!state || !state.active) return null;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 分区管理
|
||||||
|
|
||||||
|
setArea(areas: Set<MapArea>): void {
|
||||||
|
this.areaList = areas;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeArea(id: string): void {
|
||||||
|
const idx = this.maps.indexOf(id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const area = this.findAreaByIndex(idx);
|
||||||
|
if (!area) return;
|
||||||
|
this.setAreaActive(area, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
deactiveArea(id: string): void {
|
||||||
|
const idx = this.maps.indexOf(id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const area = this.findAreaByIndex(idx);
|
||||||
|
if (!area) return;
|
||||||
|
this.setAreaActive(area, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useAutoActivitor(enable: boolean): void {
|
||||||
|
this.autoActivitorEnabled = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyEnterFloor(id: string): void {
|
||||||
|
if (!this.autoActivitorEnabled) return;
|
||||||
|
const idx = this.maps.indexOf(id);
|
||||||
|
if (idx === -1) return;
|
||||||
|
const area = this.findAreaByIndex(idx);
|
||||||
|
if (!area) return;
|
||||||
|
if (this.lastFloorId !== null) {
|
||||||
|
this.deactiveArea(this.lastFloorId);
|
||||||
|
}
|
||||||
|
this.activeArea(id);
|
||||||
|
this.lastFloorId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findAreaByIndex(idx: number): MapArea | null {
|
||||||
|
for (const area of this.areaList) {
|
||||||
|
for (const interval of area) {
|
||||||
|
if (idx >= interval.start && idx <= interval.end) {
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setAreaActive(area: MapArea, active: boolean): void {
|
||||||
|
for (const interval of area) {
|
||||||
|
for (let i = interval.start; i <= interval.end; i++) {
|
||||||
|
const floorId = this.maps[i];
|
||||||
|
if (floorId !== undefined) {
|
||||||
|
this.setMapActiveStatus(floorId, active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region active 管理
|
||||||
|
|
||||||
|
isMapActive(id: string): boolean {
|
||||||
|
return this.mapData.get(id)?.active ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapActiveStatus(id: string, active: boolean): void {
|
||||||
|
this.mapData.get(id)?.setActiveStatus(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateActiveMaps(): Iterable<[string, ILayerState]> {
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (state.active) yield [id, state];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateInactiveMaps(): Iterable<[string, ILayerState]> {
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) yield [id, state];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iterateAllMaps(): Iterable<[string, ILayerState]> {
|
||||||
|
return this.mapData;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 存档及压缩
|
||||||
|
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
|
||||||
|
if (this.refData !== null) return;
|
||||||
|
this.refData = ref;
|
||||||
|
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
const refFloor = ref.get(id);
|
||||||
|
if (!refFloor) {
|
||||||
|
state.setDirty(true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dirty = false;
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const refArray = refFloor.get(layer.zIndex);
|
||||||
|
if (!refArray) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const cur = layer.getMapRef().array;
|
||||||
|
if (cur.length !== refArray.length) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (cur.some((v, i) => refArray[i] !== v)) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.setDirty(dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveNoCompression(): IMapStoreSave {
|
||||||
|
const floors = new Map<string, ILayerStateSave>();
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) continue;
|
||||||
|
floors.set(id, this.saveLayerStateFull(state));
|
||||||
|
}
|
||||||
|
return { floors };
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveLowCompression(): IMapStoreSave {
|
||||||
|
const floors = new Map<string, ILayerStateSave>();
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) continue;
|
||||||
|
// 非 dirty 或 dirty 但与参考基准完全一致 → 空 layers(读档时从参考基准恢复)
|
||||||
|
if (
|
||||||
|
!state.isDirty() ||
|
||||||
|
(this.refData && this.isStateEqualToRef(id, state))
|
||||||
|
) {
|
||||||
|
floors.set(id, {
|
||||||
|
background: state.getBackground(),
|
||||||
|
layers: new Map()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
floors.set(id, this.saveLayerStateFull(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { floors };
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveHighCompression(): IMapStoreSave {
|
||||||
|
const floors = new Map<string, ILayerStateSave>();
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) continue;
|
||||||
|
if (!state.isDirty()) {
|
||||||
|
floors.set(id, {
|
||||||
|
background: state.getBackground(),
|
||||||
|
layers: new Map()
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const refFloor = this.refData?.get(id);
|
||||||
|
const layersMap = new Map<number, IMapLayerSave>();
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const refArray = refFloor?.get(layer.zIndex);
|
||||||
|
const rows = this.diffRows(layer, refArray);
|
||||||
|
if (rows.size === 0 && refArray) continue; // 与参考完全一致
|
||||||
|
layersMap.set(layer.zIndex, {
|
||||||
|
width: layer.width,
|
||||||
|
height: layer.height,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
}
|
||||||
|
floors.set(id, {
|
||||||
|
background: state.getBackground(),
|
||||||
|
layers: layersMap
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { floors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NoCompression 读档:每个图层均有 fullMap,直接转移所有权,无需参考基准。
|
||||||
|
*/
|
||||||
|
private loadNoCompression(state: IMapStoreSave): void {
|
||||||
|
for (const [id, cur] of this.mapData) {
|
||||||
|
cur.setActiveStatus(state.floors.has(id));
|
||||||
|
}
|
||||||
|
for (const [id, layerStateSave] of state.floors) {
|
||||||
|
const cur = this.mapData.get(id);
|
||||||
|
if (!cur) {
|
||||||
|
logger.warn(122, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur.setBackground(layerStateSave.background);
|
||||||
|
for (const layer of cur.layerList) {
|
||||||
|
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||||
|
if (!layerSave?.fullMap) continue;
|
||||||
|
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||||
|
}
|
||||||
|
cur.setDirty(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LowCompression 读档:
|
||||||
|
* - layers 有数据(dirty 楼层)→ fullMap 直接转移所有权
|
||||||
|
* - layers 为空(非 dirty 楼层)→ 从参考基准恢复
|
||||||
|
*/
|
||||||
|
private loadLowCompression(state: IMapStoreSave): void {
|
||||||
|
if (!this.refData) {
|
||||||
|
logger.error(55);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [id, cur] of this.mapData) {
|
||||||
|
cur.setActiveStatus(state.floors.has(id));
|
||||||
|
}
|
||||||
|
for (const [id, layerStateSave] of state.floors) {
|
||||||
|
const cur = this.mapData.get(id);
|
||||||
|
const refFloor = this.refData.get(id);
|
||||||
|
if (!cur) {
|
||||||
|
logger.warn(122, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!refFloor) {
|
||||||
|
logger.warn(124, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur.setBackground(layerStateSave.background);
|
||||||
|
for (const layer of cur.layerList) {
|
||||||
|
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||||
|
if (layerSave?.fullMap) {
|
||||||
|
layer.setMapRef(layerSave.fullMap);
|
||||||
|
} else {
|
||||||
|
const refArray = refFloor?.get(layer.zIndex);
|
||||||
|
if (!refArray) {
|
||||||
|
logger.warn(124, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.setMapRef(new Uint32Array(refArray));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur.setDirty(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HighCompression 读档:
|
||||||
|
* - layers 有数据(dirty 楼层)→ 以参考基准为底,叠加差分行
|
||||||
|
* - layers 为空(非 dirty 楼层)或图层无变化(rows 缺失)→ 从参考基准恢复
|
||||||
|
*/
|
||||||
|
private loadHighCompression(state: IMapStoreSave): void {
|
||||||
|
if (!this.refData) {
|
||||||
|
logger.error(55);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [id, cur] of this.mapData) {
|
||||||
|
cur.setActiveStatus(state.floors.has(id));
|
||||||
|
}
|
||||||
|
for (const [id, layerStateSave] of state.floors) {
|
||||||
|
const cur = this.mapData.get(id);
|
||||||
|
const refFloor = this.refData.get(id);
|
||||||
|
if (!cur) {
|
||||||
|
logger.warn(122, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!refFloor) {
|
||||||
|
logger.warn(124, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur.setBackground(layerStateSave.background);
|
||||||
|
let isMapDirty = true;
|
||||||
|
for (const layer of cur.layerList) {
|
||||||
|
const refArray = refFloor.get(layer.zIndex);
|
||||||
|
if (!refArray) {
|
||||||
|
logger.warn(124, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||||
|
if (!layerSave?.rows || layerSave.rows.size === 0) {
|
||||||
|
// 图层无变化或非 dirty 楼层,从参考基准恢复
|
||||||
|
layer.setMapRef(new Uint32Array(refArray));
|
||||||
|
} else {
|
||||||
|
// 以参考基准为底,叠加差分行
|
||||||
|
isMapDirty = false;
|
||||||
|
const size = layer.width * layer.height;
|
||||||
|
const buf = new Uint32Array(size);
|
||||||
|
if (refArray) buf.set(refArray.subarray(0, size));
|
||||||
|
for (const [rowIdx, rowData] of layerSave.rows) {
|
||||||
|
buf.set(
|
||||||
|
rowData.subarray(0, layer.width),
|
||||||
|
rowIdx * layer.width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
layer.setMapRef(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur.setDirty(isMapDirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(compression: SaveCompression): IMapStoreSave {
|
||||||
|
if (compression === SaveCompression.HighCompression) {
|
||||||
|
return this.saveHighCompression();
|
||||||
|
} else if (compression === SaveCompression.LowCompression) {
|
||||||
|
return this.saveLowCompression();
|
||||||
|
} else {
|
||||||
|
return this.saveNoCompression();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(state: IMapStoreSave, compression: SaveCompression): void {
|
||||||
|
if (compression === SaveCompression.HighCompression) {
|
||||||
|
this.loadHighCompression(state);
|
||||||
|
} else if (compression === SaveCompression.LowCompression) {
|
||||||
|
this.loadLowCompression(state);
|
||||||
|
} else {
|
||||||
|
this.loadNoCompression(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 内部方法
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将楼层所有图层全量序列化(NoCompression / LowCompression 用)
|
||||||
|
*/
|
||||||
|
private saveLayerStateFull(state: LayerState): ILayerStateSave {
|
||||||
|
const layersMap = new Map<number, IMapLayerSave>();
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const arr = layer.getMapRef().array;
|
||||||
|
layersMap.set(layer.zIndex, {
|
||||||
|
width: layer.width,
|
||||||
|
height: layer.height,
|
||||||
|
fullMap: new Uint32Array(arr)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { background: state.getBackground(), layers: layersMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅返回与参考基准不同的行(HighCompression 用)
|
||||||
|
*/
|
||||||
|
private diffRows(
|
||||||
|
layer: IMapLayer,
|
||||||
|
refArray?: Uint32Array
|
||||||
|
): Map<number, Uint32Array> {
|
||||||
|
const rows = new Map<number, Uint32Array>();
|
||||||
|
const arr = layer.getMapRef().array;
|
||||||
|
if (refArray) {
|
||||||
|
for (let row = 0; row < layer.height; row++) {
|
||||||
|
const start = row * layer.width;
|
||||||
|
const end = start + layer.width;
|
||||||
|
const slice = arr.subarray(start, end);
|
||||||
|
const refSlice = refArray.subarray(start, end);
|
||||||
|
const same = refSlice.every((v, i) => slice[i] === v);
|
||||||
|
if (!same) {
|
||||||
|
rows.set(row, new Uint32Array(slice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let row = 0; row < layer.height; row++) {
|
||||||
|
const start = row * layer.width;
|
||||||
|
const end = start + layer.width;
|
||||||
|
rows.set(row, new Uint32Array(arr.subarray(start, end)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断楼层所有图层是否与参考基准完全一致(LowCompression 去误判用)
|
||||||
|
*/
|
||||||
|
private isStateEqualToRef(id: string, state: LayerState): boolean {
|
||||||
|
const refFloor = this.refData?.get(id);
|
||||||
|
if (!refFloor) return false;
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const refArray = refFloor.get(layer.zIndex);
|
||||||
|
if (!refArray) return false;
|
||||||
|
const cur = layer.getMapRef().array;
|
||||||
|
if (cur.length !== refArray.length) return false;
|
||||||
|
for (let i = 0; i < cur.length; i++) {
|
||||||
|
if (cur[i] !== refArray[i]) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
78
packages-user/data-base/src/map/mover.ts
Normal file
78
packages-user/data-base/src/map/mover.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { ITileLocator, logger } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
getFaceMovement,
|
||||||
|
ObjectMover,
|
||||||
|
ObjectMoveStep,
|
||||||
|
ObjectMoveStepType
|
||||||
|
} from '../common';
|
||||||
|
import { IDynamicTile } from './types';
|
||||||
|
|
||||||
|
//#region 动态图块
|
||||||
|
|
||||||
|
const enum DynamicMoveCode {
|
||||||
|
/** 正常执行 */
|
||||||
|
Success
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicTileMover extends ObjectMover<IDynamicTile> {
|
||||||
|
constructor(public readonly tile: IDynamicTile) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onMoveStart(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onMoveEnd(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onStepStart(): Promise<number> {
|
||||||
|
return Promise.resolve(DynamicMoveCode.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onStepEnd(
|
||||||
|
code: number,
|
||||||
|
step: ObjectMoveStep,
|
||||||
|
tile: IDynamicTile
|
||||||
|
): Promise<ITileLocator> {
|
||||||
|
if (code !== DynamicMoveCode.Success) {
|
||||||
|
logger.warn(126, 'DynamicMoveCode.Success (0)', code.toString());
|
||||||
|
return Promise.resolve({ x: tile.x, y: tile.y });
|
||||||
|
}
|
||||||
|
const locator: ITileLocator = {
|
||||||
|
x: tile.x,
|
||||||
|
y: tile.y
|
||||||
|
};
|
||||||
|
switch (step.type) {
|
||||||
|
case ObjectMoveStepType.Dir: {
|
||||||
|
const { x, y } = getFaceMovement(step.move);
|
||||||
|
tile.setFaceDirection(step.move);
|
||||||
|
locator.x += x;
|
||||||
|
locator.y += y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ObjectMoveStepType.DirFace: {
|
||||||
|
const { x, y } = getFaceMovement(step.move);
|
||||||
|
tile.setFaceDirection(step.face);
|
||||||
|
locator.x += x;
|
||||||
|
locator.y += y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ObjectMoveStepType.Face: {
|
||||||
|
tile.setFaceDirection(step.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ObjectMoveStepType.Special: {
|
||||||
|
const { x, y } = getFaceMovement(this.moveDirection);
|
||||||
|
tile.setFaceDirection(this.faceDirection);
|
||||||
|
locator.x += x;
|
||||||
|
locator.y += y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(locator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
604
packages-user/data-base/src/map/types.ts
Normal file
604
packages-user/data-base/src/map/types.ts
Normal file
@ -0,0 +1,604 @@
|
|||||||
|
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
FaceDirection,
|
||||||
|
IMoverController,
|
||||||
|
IObjectMovable,
|
||||||
|
IObjectMover,
|
||||||
|
IRoleFaceBinder,
|
||||||
|
ISaveableContent
|
||||||
|
} from '../common';
|
||||||
|
|
||||||
|
//#region 静态图层
|
||||||
|
|
||||||
|
export interface IMapLayerData {
|
||||||
|
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
||||||
|
expired: boolean;
|
||||||
|
/** 地图图块数组,是对内部存储的直接引用 */
|
||||||
|
array: Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLayerHooks extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当地图大小发生变化时执行,如果调用了地图的 `resize` 方法,但是地图大小没变,则不会触发
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
onResize(width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当更新某个区域的图块时执行
|
||||||
|
* @param x 更新区域左上角横坐标
|
||||||
|
* @param y 更新区域左上角纵坐标
|
||||||
|
* @param width 更新区域宽度
|
||||||
|
* @param height 更新区域高度
|
||||||
|
*/
|
||||||
|
onUpdateArea(x: number, y: number, width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法
|
||||||
|
* @param block 更新为的图块数字
|
||||||
|
* @param x 更新点横坐标
|
||||||
|
* @param y 更新点纵坐标
|
||||||
|
*/
|
||||||
|
onUpdateBlock(block: number, x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当开门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
onOpenDoor(x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当关门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
|
||||||
|
* @param num 门的图块数字
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLayerHookController extends IHookController<IMapLayerHooks> {
|
||||||
|
/** 拓展所属的图层对象 */
|
||||||
|
readonly layer: IMapLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取地图数据,是对内部存储的直接引用
|
||||||
|
*/
|
||||||
|
getMapData(): Readonly<IMapLayerData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapLayer extends IHookable<
|
||||||
|
IMapLayerHooks,
|
||||||
|
IMapLayerHookController
|
||||||
|
> {
|
||||||
|
/** 地图宽度 */
|
||||||
|
readonly width: number;
|
||||||
|
/** 地图高度 */
|
||||||
|
readonly height: number;
|
||||||
|
/**
|
||||||
|
* 地图是否全部空白,此值具有充分性,但不具有必要性,
|
||||||
|
* 即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白
|
||||||
|
*/
|
||||||
|
readonly empty: boolean;
|
||||||
|
/** 图层纵深 */
|
||||||
|
readonly zIndex: number;
|
||||||
|
|
||||||
|
/** 此图层对应的动态图块图层,z 层级与静态图块一致 */
|
||||||
|
readonly dynamicLayer: IDynamicLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置某一点的图块
|
||||||
|
* @param block 图块数字
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
*/
|
||||||
|
setBlock(block: number, x: number, y: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定点的图块
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
* @returns 指定点的图块,如果没有图块,返回 0,如果不在地图上,返回 -1
|
||||||
|
*/
|
||||||
|
getBlock(x: number, y: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图图块
|
||||||
|
* @param array 地图图块数组
|
||||||
|
* @param x 数组第一项代表的横坐标
|
||||||
|
* @param y 数组第一项代表的纵坐标
|
||||||
|
* @param width 传入数组所表示的矩形范围的宽度
|
||||||
|
*/
|
||||||
|
putMapData(array: Uint32Array, x: number, y: number, width: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取整个地图的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容
|
||||||
|
*/
|
||||||
|
getMapData(): Uint32Array;
|
||||||
|
/**
|
||||||
|
* 获取地图指定区域的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容
|
||||||
|
* @param x 左上角横坐标
|
||||||
|
* @param y 左上角纵坐标
|
||||||
|
* @param width 获取区域的宽度
|
||||||
|
* @param height 获取区域的高度
|
||||||
|
*/
|
||||||
|
getMapData(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Uint32Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取整个地图的地图数组,是对内部数组的直接引用
|
||||||
|
*/
|
||||||
|
getMapRef(): IMapLayerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图纵深,会影响渲染的遮挡顺序
|
||||||
|
* @param zIndex 纵深
|
||||||
|
*/
|
||||||
|
setZIndex(zIndex: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启指定位置的门
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
openDoor(x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置关门,门的图块数字由参数指定
|
||||||
|
* @param num 门图块数字
|
||||||
|
* @param x 门横坐标
|
||||||
|
* @param y 门纵坐标
|
||||||
|
*/
|
||||||
|
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||||
|
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||||
|
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||||
|
* 且调用后不得再持有或修改传入的数组。
|
||||||
|
* @param array 地图数组,会直接替换内部引用
|
||||||
|
*/
|
||||||
|
setMapRef(array: Uint32Array): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 图层管理
|
||||||
|
|
||||||
|
export interface ILayerStateHooks extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当设置背景图块时执行,如果设置的背景图块与原先一样,则不会执行
|
||||||
|
* @param tile 背景图块
|
||||||
|
*/
|
||||||
|
onChangeBackground(tile: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图列表发生变化时执行
|
||||||
|
* @param layerList 地图图层列表
|
||||||
|
*/
|
||||||
|
onUpdateLayer(layerList: Set<IMapLayer>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图状态对象的某个图层发生区域更新时执行
|
||||||
|
* @param layer 触发更新的地图图层对象
|
||||||
|
* @param x 更新区域左上角横坐标
|
||||||
|
* @param y 更新区域左上角纵坐标
|
||||||
|
* @param width 更新区域宽度
|
||||||
|
* @param height 更新区域高度
|
||||||
|
*/
|
||||||
|
onUpdateLayerArea(
|
||||||
|
layer: IMapLayer,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图状态对象的某个图层设置图块时执行,如果设置的图块与原先一样则不会触发
|
||||||
|
* @param layer 触发更新的地图图层对象
|
||||||
|
* @param block 设置为的图块
|
||||||
|
* @param x 图块横坐标
|
||||||
|
* @param y 图块纵坐标
|
||||||
|
*/
|
||||||
|
onUpdateLayerBlock(
|
||||||
|
layer: IMapLayer,
|
||||||
|
block: number,
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当地图状态对象的某个图层大小发生变化时执行
|
||||||
|
* @param layer 触发更新的地图图层对象
|
||||||
|
* @param width 地图的新宽度
|
||||||
|
* @param height 地图的新高度
|
||||||
|
*/
|
||||||
|
onResizeLayer(layer: IMapLayer, width: number, height: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||||
|
/** 地图列表 */
|
||||||
|
readonly layerList: Set<IMapLayer>;
|
||||||
|
/** 此楼层是否处于激活状态 */
|
||||||
|
readonly active: boolean;
|
||||||
|
/** 此楼层的地图宽度 */
|
||||||
|
readonly width: number;
|
||||||
|
/** 此楼层的地图高度 */
|
||||||
|
readonly height: number;
|
||||||
|
/** 当前楼层的默认事件层 */
|
||||||
|
readonly eventLayer: IMapLayer | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加图层,使用楼层预设的宽高
|
||||||
|
*/
|
||||||
|
addLayer(): IMapLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定图层
|
||||||
|
* @param layer 图层对象
|
||||||
|
*/
|
||||||
|
removeLayer(layer: IMapLayer): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前地图状态对象是否包含指定图层对象
|
||||||
|
* @param layer 图层对象
|
||||||
|
*/
|
||||||
|
hasLayer(layer: IMapLayer): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置图层别名
|
||||||
|
* @param layer 图层对象
|
||||||
|
* @param alias 图层别名
|
||||||
|
*/
|
||||||
|
setLayerAlias(layer: IMapLayer, alias: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据图层别名获取图层对象
|
||||||
|
* @param alias 图层别名
|
||||||
|
*/
|
||||||
|
getLayerByAlias(alias: string): IMapLayer | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取图层对象的别名
|
||||||
|
* @param layer 图层对象
|
||||||
|
*/
|
||||||
|
getLayerAlias(layer: IMapLayer): string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新设置所有图层的大小,同时更新楼层预设宽高
|
||||||
|
* @param width 新的地图宽度
|
||||||
|
* @param height 新的地图高度
|
||||||
|
* @param keepBlock 是否保留原有图块,默认不保留
|
||||||
|
*/
|
||||||
|
resizeLayer(width: number, height: number, keepBlock?: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置背景图块
|
||||||
|
* @param tile 背景图块数字
|
||||||
|
*/
|
||||||
|
setBackground(tile: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取背景图块数字,如果没有设置过,则返回 0
|
||||||
|
*/
|
||||||
|
getBackground(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置楼层激活状态
|
||||||
|
* @param active 激活状态
|
||||||
|
*/
|
||||||
|
setActiveStatus(active: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置本楼层的事件层图层
|
||||||
|
* @param layer 图层
|
||||||
|
*/
|
||||||
|
setEventLayer(layer: IMapLayer | null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼层是否被修改过(相对于参考基准)
|
||||||
|
*/
|
||||||
|
isDirty(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置楼层脏标记
|
||||||
|
*/
|
||||||
|
setDirty(dirty: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 楼层管理
|
||||||
|
|
||||||
|
/** 单个 MapLayer 的存档数据 */
|
||||||
|
export interface IMapLayerSave {
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key = 行索引,value = 该行完整的 Uint32Array 数据;
|
||||||
|
* HighCompression 时使用此接口,仅包含与参考基准不同的行;
|
||||||
|
* 读档时,不在此 Map 中的行从参考基准还原。
|
||||||
|
*/
|
||||||
|
readonly rows?: ReadonlyMap<number, Uint32Array>;
|
||||||
|
|
||||||
|
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
|
||||||
|
readonly fullMap?: Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个楼层的存档数据 */
|
||||||
|
export interface ILayerStateSave {
|
||||||
|
readonly background: number;
|
||||||
|
|
||||||
|
/** key = zIndex,value = 对应图层存档数据 */
|
||||||
|
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整个 MapStore 的存档数据 */
|
||||||
|
export interface IMapStoreSave {
|
||||||
|
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
|
||||||
|
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单段闭区间 [start, end],start 和 end 均为 maps 下标 */
|
||||||
|
export interface IMapAreaInterval {
|
||||||
|
readonly start: number;
|
||||||
|
readonly end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 一个区域由一个或多个独立区间组成 */
|
||||||
|
export type MapArea = IMapAreaInterval[];
|
||||||
|
|
||||||
|
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||||
|
/** 所有楼层的 id 有序数组 */
|
||||||
|
readonly maps: ReadonlyArray<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 id 的楼层状态,不存在则返回 null
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
getLayerState(id: string): ILayerState | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
getActiveMap(id: string): ILayerState | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并注册一个空白楼层,若 id 已存在则警告并覆盖,返回楼层状态对象
|
||||||
|
* @param id 楼层 id
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
createLayerState(id: string, width: number, height: number): ILayerState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 id 的楼层是否激活,不存在的 id 返回 false
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
isMapActive(id: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定 id 楼层的激活状态
|
||||||
|
* @param id 楼层 id
|
||||||
|
* @param active 激活状态
|
||||||
|
*/
|
||||||
|
setMapActiveStatus(id: string, active: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有 active 的楼层,yield [id, ILayerState]
|
||||||
|
*/
|
||||||
|
iterateActiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有 inactive 的楼层,yield [id, ILayerState]
|
||||||
|
*/
|
||||||
|
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有楼层,yield [id, ILayerState]
|
||||||
|
*/
|
||||||
|
iterateAllMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
|
||||||
|
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
|
||||||
|
*/
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设定楼层有序列表。设定后有序列表将用于分区索引计算
|
||||||
|
* @param maps 楼层 id 数组
|
||||||
|
*/
|
||||||
|
setMapList(maps: string[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用自定义排序函数重排 maps。排序函数接收当前列表的副本,返回新顺序。
|
||||||
|
* 若返回的数组元素集合与原列表不一致,则警告并放弃本次排序
|
||||||
|
* @param sort 排序函数
|
||||||
|
*/
|
||||||
|
useManualOrder(sort: (arr: string[]) => string[]): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设定分区列表。每个分区由一个或多个区间组成
|
||||||
|
* @param areas 分区集合
|
||||||
|
*/
|
||||||
|
setArea(areas: Set<MapArea>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活指定楼层所属分区的所有楼层
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
activeArea(id: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去激活指定楼层所属分区的所有楼层
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
deactiveArea(id: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启或关闭自动分区激活器
|
||||||
|
* @param enable 是否开启
|
||||||
|
*/
|
||||||
|
useAutoActivitor(enable: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知当前进入的楼层。开启自动激活器时,将自动去激活上一个分区并激活新分区
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
notifyEnterFloor(id: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 动态图块
|
||||||
|
|
||||||
|
export interface IDynamicLayerHooks extends IHookBase {
|
||||||
|
/**
|
||||||
|
* 当图块被创建(含从静态图块转换)时触发
|
||||||
|
* @param tile 被创建的动态图块
|
||||||
|
* @param layer 所属的动态图层
|
||||||
|
*/
|
||||||
|
onCreateTile(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当图块被删除时触发
|
||||||
|
* @param tile 被删除的动态图块
|
||||||
|
* @param layer 所属的动态图层
|
||||||
|
*/
|
||||||
|
onDeleteTile(tile: IDynamicTile, layer: IDynamicLayer): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当更新动态图块的位置时触发(包括使用 `mover` 触发的移动)
|
||||||
|
* @param tile 更新位置的图块
|
||||||
|
* @param layer 所属的动态图层
|
||||||
|
*/
|
||||||
|
onUpdateTilePosition(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||||
|
/** 当前动态图层所属的静态图层 */
|
||||||
|
readonly layer: IMapLayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置创建一个动态图块
|
||||||
|
* @param num 图块数字
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
* @returns 创建的动态图块引用
|
||||||
|
*/
|
||||||
|
createDynamic(num: number, x: number, y: number): IDynamicTile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从所属静态图层读取并清除指定位置的图块,创建对应动态图块并返回引用。
|
||||||
|
* 若该位置图块为 0,则发出警告并仍然创建 `num = 0` 的动态图块
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
* @returns 创建的动态图块引用
|
||||||
|
*/
|
||||||
|
transferToDynamic(x: number, y: number): IDynamicTile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将动态图块还原为静态图块。坐标越界则警告并放弃,
|
||||||
|
* 否则写回静态图层并触发 {@link IDynamicLayerHooks.onDeleteTile}
|
||||||
|
* @param tile 要还原的动态图块
|
||||||
|
*/
|
||||||
|
transferToStatic(tile: IDynamicTile): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅当目标位置静态图块为 0(空白)时才还原为静态图块,否则不转换
|
||||||
|
* @param tile 要还原的动态图块
|
||||||
|
* @returns 是否转换成功
|
||||||
|
*/
|
||||||
|
transferToStaticIfSafe(tile: IDynamicTile): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定动态图块,触发 {@link IDynamicLayerHooks.onDeleteTile} 钩子。
|
||||||
|
* 若图块不属于此层则发出警告
|
||||||
|
* @param tile 要删除的动态图块
|
||||||
|
*/
|
||||||
|
deleteDynamic(tile: IDynamicTile): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定格点上所有动态图块的可迭代对象
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
*/
|
||||||
|
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有的动态图块
|
||||||
|
*/
|
||||||
|
iterateDynamicTiles(): Iterable<IDynamicTile>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动设置动态图块的朝向,更新 `tile.num`(若有朝向绑定)。
|
||||||
|
* 转向逻辑与移动时的转向逻辑相同,但不触发移动
|
||||||
|
* @param tile 要设置朝向的动态图块
|
||||||
|
* @param direction 目标朝向
|
||||||
|
*/
|
||||||
|
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新图块内部存储位置
|
||||||
|
* @param tile 动态图块
|
||||||
|
*/
|
||||||
|
updateDynamicTile(tile: IDynamicTile): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDynamicTile extends IObjectMovable {
|
||||||
|
/** 当前图块数字 */
|
||||||
|
readonly num: number;
|
||||||
|
/** 当前图块所属的动态图层 */
|
||||||
|
readonly layer: IDynamicLayer;
|
||||||
|
/** 当前动态图块的移动器 */
|
||||||
|
readonly mover: IObjectMover<IDynamicTile>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置图块朝向,会一并修改 {@link num},返回设置后的当前图块数字
|
||||||
|
* @param direction 图块朝向
|
||||||
|
*/
|
||||||
|
setFaceDirection(direction: FaceDirection): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接删除此图块
|
||||||
|
*/
|
||||||
|
delete(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前图块还原为静态图块
|
||||||
|
*/
|
||||||
|
toStatic(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 还原为静态图块,如果当前位置有东西则不转换
|
||||||
|
*/
|
||||||
|
toStaticIfSafe(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单步便捷移动接口,适用于简单移动场景,复杂路径通过 `tile.mover` 访问。
|
||||||
|
* 等价于:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* mover.step(dir, count);
|
||||||
|
* return mover.start();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注入朝向绑定器,初始状态视为无朝向绑定
|
||||||
|
* @param binder 朝向绑定器
|
||||||
|
*/
|
||||||
|
setFaceBinder(binder: IRoleFaceBinder | null): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
45
packages-user/data-base/src/types.ts
Normal file
45
packages-user/data-base/src/types.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { IHeroFollower, IHeroState } from './hero';
|
||||||
|
import { IEnemyManager } from './enemy';
|
||||||
|
import { IFlagSystem } from './flag';
|
||||||
|
import { IFaceManager, IRoleFaceBinder, ISaveableContent } from './common';
|
||||||
|
import { IMapStore } from './map';
|
||||||
|
|
||||||
|
export interface IStateSaveData {
|
||||||
|
/** 跟随者列表 */
|
||||||
|
readonly followers: readonly IHeroFollower[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStateBase<TEnemy, THero> {
|
||||||
|
/** 朝向绑定 */
|
||||||
|
readonly roleFace: IRoleFaceBinder;
|
||||||
|
/** 朝向管理 */
|
||||||
|
readonly faceManager: IFaceManager;
|
||||||
|
/** id 到图块数字的映射 */
|
||||||
|
readonly idNumberMap: Map<string, number>;
|
||||||
|
/** 图块数字到 id 的映射 */
|
||||||
|
readonly numberIdMap: Map<number, string>;
|
||||||
|
|
||||||
|
/** 地图状态 */
|
||||||
|
readonly maps: IMapStore;
|
||||||
|
/** 勇士状态 */
|
||||||
|
readonly hero: IHeroState<THero>;
|
||||||
|
|
||||||
|
/** 怪物管理器 */
|
||||||
|
readonly enemyManager: IEnemyManager<TEnemy>;
|
||||||
|
|
||||||
|
/** Flag 系统 */
|
||||||
|
readonly flags: IFlagSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加可存档对象,添加后系统将会自动在存档时将对象存储
|
||||||
|
* @param id 可存档对象的 id
|
||||||
|
* @param content 可存档对象
|
||||||
|
*/
|
||||||
|
addSaveableContent(id: string, content: ISaveableContent<unknown>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 id 获取对应的可存档对象
|
||||||
|
* @param id 可存档对象的 id
|
||||||
|
*/
|
||||||
|
getSaveableContent<T>(id: string): ISaveableContent<T> | null;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@
|
|||||||
"@motajs/types": "workspace:*",
|
"@motajs/types": "workspace:*",
|
||||||
"@motajs/common": "workspace:*",
|
"@motajs/common": "workspace:*",
|
||||||
"@user/data-base": "workspace:*",
|
"@user/data-base": "workspace:*",
|
||||||
|
"@user/data-system": "workspace:*",
|
||||||
"@user/data-utils": "workspace:*"
|
"@user/data-utils": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export * from './face';
|
|
||||||
export * from './types';
|
|
||||||
export * from './utils';
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { FaceDirection, type IFaceData } from '@user/data-base';
|
|
||||||
|
|
||||||
export { FaceDirection };
|
|
||||||
export type { IFaceData } from '@user/data-base';
|
|
||||||
|
|
||||||
export interface IRoleFaceBinder {
|
|
||||||
/**
|
|
||||||
* 给指定的图块分配朝向绑定
|
|
||||||
* @param identifier 图块数字
|
|
||||||
* @param main 主图块朝向,一般是朝下
|
|
||||||
*/
|
|
||||||
malloc(identifier: number, main: FaceDirection): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将一个图块与另一个图块绑定朝向,需要注意要先调用 {@link malloc} 分配朝向信息
|
|
||||||
* @param identifier 当前图块数字
|
|
||||||
* @param main 主图块数字,即当前图块与目标图块属于主图块的另一个朝向
|
|
||||||
* @param face 当前图块的朝向方向
|
|
||||||
*/
|
|
||||||
bind(identifier: number, main: number, face: FaceDirection): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取一个图块指定朝向的图块数字
|
|
||||||
* @param identifier 图块数字,可以是任意朝向的图块数字
|
|
||||||
* @param face 要获取的朝向
|
|
||||||
*/
|
|
||||||
getFaceOf(identifier: number, face: FaceDirection): IFaceData | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定图块数字是哪个朝向
|
|
||||||
* @param identifier 图块数字
|
|
||||||
*/
|
|
||||||
getFaceDirection(identifier: number): FaceDirection | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定图块数字绑定至的主朝向
|
|
||||||
* @param identifier 图块数字,可以是任意朝向的图块数字
|
|
||||||
*/
|
|
||||||
getMainFace(identifier: number): IFaceData | null;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export {
|
|
||||||
degradeFace,
|
|
||||||
fromDirectionString,
|
|
||||||
getFaceMovement,
|
|
||||||
nextFaceDirection
|
|
||||||
} from '@user/data-base';
|
|
||||||
@ -1,6 +1,4 @@
|
|||||||
import { ICoreState, IStateSaveData } from './types';
|
import { ICoreState, ISaveableExecutor } from './types';
|
||||||
import { ILayerState, LayerState } from './map';
|
|
||||||
import { FaceDirection, IRoleFaceBinder, RoleFaceBinder } from './common';
|
|
||||||
import {
|
import {
|
||||||
DamageSystem,
|
DamageSystem,
|
||||||
EnemyContext,
|
EnemyContext,
|
||||||
@ -16,9 +14,21 @@ import {
|
|||||||
FlagSystem,
|
FlagSystem,
|
||||||
IMotaDataLoader,
|
IMotaDataLoader,
|
||||||
MotaDataLoader,
|
MotaDataLoader,
|
||||||
loading
|
loading,
|
||||||
|
IRoleFaceBinder,
|
||||||
|
RoleFaceBinder,
|
||||||
|
FaceDirection,
|
||||||
|
ISaveableContent,
|
||||||
|
SaveCompression,
|
||||||
|
IReadonlyEnemy,
|
||||||
|
IMapStore,
|
||||||
|
MapStore,
|
||||||
|
IFaceManager,
|
||||||
|
FaceManager,
|
||||||
|
InternalFaceGroup,
|
||||||
|
Dir4FaceHandler,
|
||||||
|
Dir8FaceHandler
|
||||||
} from '@user/data-base';
|
} from '@user/data-base';
|
||||||
import { IEnemyAttr } from './enemy/types';
|
|
||||||
import {
|
import {
|
||||||
CommonAuraConverter,
|
CommonAuraConverter,
|
||||||
EnemyLegacyBridge,
|
EnemyLegacyBridge,
|
||||||
@ -27,32 +37,57 @@ import {
|
|||||||
MainEnemyFinalEffect,
|
MainEnemyFinalEffect,
|
||||||
MainMapDamageConverter,
|
MainMapDamageConverter,
|
||||||
MainMapDamageReducer,
|
MainMapDamageReducer,
|
||||||
registerSpecials
|
registerSpecials,
|
||||||
|
MainEnemyComparer,
|
||||||
|
IEnemyAttr
|
||||||
} from './enemy';
|
} from './enemy';
|
||||||
import { HERO_DEFAULT_ATTRIBUTE, TILE_HEIGHT, TILE_WIDTH } from './shared';
|
import {
|
||||||
|
BG2_ZINDEX,
|
||||||
|
BG_ZINDEX,
|
||||||
|
EVENT_ZINDEX,
|
||||||
|
FG2_ZINDEX,
|
||||||
|
FG_ZINDEX,
|
||||||
|
HERO_DEFAULT_ATTRIBUTE,
|
||||||
|
TILE_HEIGHT,
|
||||||
|
TILE_WIDTH
|
||||||
|
} from './shared';
|
||||||
import { IHeroAttr } from './hero';
|
import { IHeroAttr } from './hero';
|
||||||
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { ISaveSystem, SaveSystem } from './save';
|
||||||
|
|
||||||
export class CoreState implements ICoreState {
|
export class CoreState implements ICoreState {
|
||||||
|
// 全局内容
|
||||||
readonly roleFace: IRoleFaceBinder;
|
readonly roleFace: IRoleFaceBinder;
|
||||||
|
readonly faceManager: IFaceManager;
|
||||||
readonly idNumberMap: Map<string, number>;
|
readonly idNumberMap: Map<string, number>;
|
||||||
readonly numberIdMap: Map<number, string>;
|
readonly numberIdMap: Map<number, string>;
|
||||||
|
|
||||||
readonly loadProgress: ILoadProgressTotal;
|
// 可存档内容
|
||||||
readonly dataLoader: IMotaDataLoader;
|
readonly maps: IMapStore;
|
||||||
|
|
||||||
readonly layer: ILayerState;
|
|
||||||
readonly hero: IHeroState<IHeroAttr>;
|
readonly hero: IHeroState<IHeroAttr>;
|
||||||
|
|
||||||
readonly enemyManager: IEnemyManager<IEnemyAttr>;
|
readonly enemyManager: IEnemyManager<IEnemyAttr>;
|
||||||
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
|
||||||
|
|
||||||
readonly flags: IFlagSystem;
|
readonly flags: IFlagSystem;
|
||||||
|
|
||||||
|
// 状态内容
|
||||||
|
readonly loadProgress: ILoadProgressTotal;
|
||||||
|
readonly dataLoader: IMotaDataLoader;
|
||||||
|
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
||||||
|
readonly saveSystem: ISaveSystem;
|
||||||
|
|
||||||
|
/** 可存档对象映射 */
|
||||||
|
private readonly saveables: Map<string, ISaveableContent<any>> = new Map();
|
||||||
|
/** 所有已添加的可存档对象 */
|
||||||
|
private readonly addedSaveables: Set<ISaveableContent<any>> = new Set();
|
||||||
|
/** 已绑定的存档执行器 */
|
||||||
|
private readonly executors: Map<
|
||||||
|
ISaveableContent<any>,
|
||||||
|
ISaveableExecutor<any>
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.layer = new LayerState();
|
this.maps = new MapStore();
|
||||||
this.roleFace = new RoleFaceBinder();
|
|
||||||
this.idNumberMap = new Map();
|
this.idNumberMap = new Map();
|
||||||
this.numberIdMap = new Map();
|
this.numberIdMap = new Map();
|
||||||
|
|
||||||
@ -71,7 +106,9 @@ export class CoreState implements ICoreState {
|
|||||||
//#region 怪物初始化
|
//#region 怪物初始化
|
||||||
|
|
||||||
// 怪物管理器初始化
|
// 怪物管理器初始化
|
||||||
|
const comparer = new MainEnemyComparer();
|
||||||
const enemyManager = new EnemyManager(new EnemyLegacyBridge());
|
const enemyManager = new EnemyManager(new EnemyLegacyBridge());
|
||||||
|
enemyManager.attachEnemyComparer(comparer);
|
||||||
enemyManager.setAttributeDefaults('hp', 0);
|
enemyManager.setAttributeDefaults('hp', 0);
|
||||||
enemyManager.setAttributeDefaults('atk', 0);
|
enemyManager.setAttributeDefaults('atk', 0);
|
||||||
enemyManager.setAttributeDefaults('def', 0);
|
enemyManager.setAttributeDefaults('def', 0);
|
||||||
@ -98,15 +135,53 @@ export class CoreState implements ICoreState {
|
|||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region 存档系统
|
||||||
|
|
||||||
|
this.saveSystem = new SaveSystem();
|
||||||
|
// 配置存档系统,一般情况下不建议动,除非你知道你在干什么
|
||||||
|
this.saveSystem.config({
|
||||||
|
autosaveLevel: SaveCompression.LowCompression,
|
||||||
|
commonSaveLevel: SaveCompression.HighCompression,
|
||||||
|
autosaveTimeTolerance: 50,
|
||||||
|
saveTimeTolerance: 100,
|
||||||
|
autosaveStackSize: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化存档数据库,不要动
|
||||||
|
loading.once('coreInit', () => {
|
||||||
|
this.saveSystem.init(`@game/${core.firstData.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 其他初始化
|
//#region 其他初始化
|
||||||
|
|
||||||
|
// 朝向
|
||||||
|
this.roleFace = new RoleFaceBinder();
|
||||||
|
this.faceManager = new FaceManager();
|
||||||
|
const dir4 = new Dir4FaceHandler();
|
||||||
|
const dir8 = new Dir8FaceHandler();
|
||||||
|
this.faceManager.register(InternalFaceGroup.Dir4, dir4);
|
||||||
|
this.faceManager.registerById('dir4', dir4);
|
||||||
|
this.faceManager.register(InternalFaceGroup.Dir8, dir8);
|
||||||
|
this.faceManager.registerById('dir8', dir8);
|
||||||
|
|
||||||
this.flags = new FlagSystem();
|
this.flags = new FlagSystem();
|
||||||
|
|
||||||
// 加载先使用兼容层实现
|
// 加载先使用兼容层实现
|
||||||
loading.once('loaded', () => {
|
loading.once('loaded', () => {
|
||||||
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
|
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
|
||||||
|
this.initMapStore(
|
||||||
|
core.floorIds,
|
||||||
|
core.floors as Record<FloorIds, ResolvedFloor>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addSaveableContent('@system/hero', this.hero);
|
||||||
|
this.addSaveableContent('@system/flags', this.flags);
|
||||||
|
this.addSaveableContent('@system/maps', this.maps);
|
||||||
|
this.addSaveableContent('@system/enemy', this.enemyManager);
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +192,7 @@ export class CoreState implements ICoreState {
|
|||||||
private initEnemyManager(data: Record<EnemyIds, Enemy>) {
|
private initEnemyManager(data: Record<EnemyIds, Enemy>) {
|
||||||
// TODO: 修改怪物模板并存入存档,即 core.setEnemy
|
// TODO: 修改怪物模板并存入存档,即 core.setEnemy
|
||||||
const manager = this.enemyManager;
|
const manager = this.enemyManager;
|
||||||
|
const reference = new Map<number, IReadonlyEnemy<IEnemyAttr>>();
|
||||||
for (const [id, enemy] of Object.entries(structuredClone(data))) {
|
for (const [id, enemy] of Object.entries(structuredClone(data))) {
|
||||||
const num = this.idNumberMap.get(id);
|
const num = this.idNumberMap.get(id);
|
||||||
if (isNil(num)) continue;
|
if (isNil(num)) continue;
|
||||||
@ -127,7 +203,9 @@ export class CoreState implements ICoreState {
|
|||||||
const upCode = this.idNumberMap.get(up)!;
|
const upCode = this.idNumberMap.get(up)!;
|
||||||
const rightCode = this.idNumberMap.get(right)!;
|
const rightCode = this.idNumberMap.get(right)!;
|
||||||
const downCode = this.idNumberMap.get(down)!;
|
const downCode = this.idNumberMap.get(down)!;
|
||||||
manager.addPrefabFromLegacy(downCode, enemy);
|
const prefab = manager.fromLegacyEnemy(downCode, enemy);
|
||||||
|
reference.set(downCode, prefab);
|
||||||
|
manager.addPrefab(prefab);
|
||||||
this.roleFace.malloc(downCode, FaceDirection.Down);
|
this.roleFace.malloc(downCode, FaceDirection.Down);
|
||||||
this.roleFace.bind(leftCode, downCode, FaceDirection.Left);
|
this.roleFace.bind(leftCode, downCode, FaceDirection.Left);
|
||||||
this.roleFace.bind(upCode, downCode, FaceDirection.Up);
|
this.roleFace.bind(upCode, downCode, FaceDirection.Up);
|
||||||
@ -136,21 +214,119 @@ export class CoreState implements ICoreState {
|
|||||||
manager.reusePrefab(num, upCode, up);
|
manager.reusePrefab(num, upCode, up);
|
||||||
manager.reusePrefab(num, rightCode, right);
|
manager.reusePrefab(num, rightCode, right);
|
||||||
} else {
|
} else {
|
||||||
manager.addPrefabFromLegacy(num, enemy);
|
const prefab = manager.fromLegacyEnemy(num, enemy);
|
||||||
|
reference.set(num, prefab);
|
||||||
|
manager.addPrefab(prefab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
manager.compareWith(reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveState(): IStateSaveData {
|
private initMapStore(
|
||||||
return structuredClone({
|
floors: FloorIds[],
|
||||||
followers: this.hero.mover.followers
|
data: Record<FloorIds, ResolvedFloor>
|
||||||
});
|
) {
|
||||||
|
const reference = new Map<string, Map<number, Uint32Array>>();
|
||||||
|
for (const id of floors) {
|
||||||
|
const floor = data[id];
|
||||||
|
const state = this.maps.createLayerState(
|
||||||
|
id,
|
||||||
|
floor.width,
|
||||||
|
floor.height
|
||||||
|
);
|
||||||
|
const bg = state.addLayer();
|
||||||
|
const bg2 = state.addLayer();
|
||||||
|
const event = state.addLayer();
|
||||||
|
const fg = state.addLayer();
|
||||||
|
const fg2 = state.addLayer();
|
||||||
|
bg.setZIndex(BG_ZINDEX);
|
||||||
|
bg2.setZIndex(BG2_ZINDEX);
|
||||||
|
event.setZIndex(EVENT_ZINDEX);
|
||||||
|
fg.setZIndex(FG_ZINDEX);
|
||||||
|
fg2.setZIndex(FG2_ZINDEX);
|
||||||
|
state.setLayerAlias(bg, 'bg');
|
||||||
|
state.setLayerAlias(bg2, 'bg2');
|
||||||
|
state.setLayerAlias(event, 'event');
|
||||||
|
state.setLayerAlias(fg, 'fg');
|
||||||
|
state.setLayerAlias(fg2, 'fg2');
|
||||||
|
state.setActiveStatus(false);
|
||||||
|
|
||||||
|
const size = floor.width * floor.height;
|
||||||
|
const ref = new Map<number, Uint32Array>();
|
||||||
|
|
||||||
|
if (floor.bgmap && floor.bgmap.length > 0) {
|
||||||
|
const arr = new Uint32Array(floor.bgmap.flat());
|
||||||
|
bg.setMapRef(arr);
|
||||||
|
ref.set(BG_ZINDEX, new Uint32Array(arr));
|
||||||
|
} else {
|
||||||
|
ref.set(BG_ZINDEX, new Uint32Array(size));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadState(data: IStateSaveData): void {
|
if (floor.bg2map && floor.bg2map.length > 0) {
|
||||||
this.hero.mover.removeAllFollowers();
|
const arr = new Uint32Array(floor.bg2map.flat());
|
||||||
data.followers.forEach(v => {
|
bg2.setMapRef(arr);
|
||||||
this.hero.mover.addFollower(v.num, v.identifier);
|
ref.set(BG2_ZINDEX, new Uint32Array(arr));
|
||||||
});
|
} else {
|
||||||
|
ref.set(BG2_ZINDEX, new Uint32Array(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floor.map && floor.map.length > 0) {
|
||||||
|
const arr = new Uint32Array(floor.map.flat());
|
||||||
|
event.setMapRef(arr);
|
||||||
|
ref.set(EVENT_ZINDEX, new Uint32Array(arr));
|
||||||
|
} else {
|
||||||
|
ref.set(EVENT_ZINDEX, new Uint32Array(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floor.fgmap && floor.fgmap.length > 0) {
|
||||||
|
const arr = new Uint32Array(floor.fgmap.flat());
|
||||||
|
fg.setMapRef(arr);
|
||||||
|
ref.set(FG_ZINDEX, new Uint32Array(arr));
|
||||||
|
} else {
|
||||||
|
ref.set(FG_ZINDEX, new Uint32Array(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floor.fg2map && floor.fg2map.length > 0) {
|
||||||
|
const arr = new Uint32Array(floor.fg2map.flat());
|
||||||
|
fg2.setMapRef(arr);
|
||||||
|
ref.set(FG2_ZINDEX, new Uint32Array(arr));
|
||||||
|
} else {
|
||||||
|
ref.set(FG2_ZINDEX, new Uint32Array(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
reference.set(id, ref);
|
||||||
|
}
|
||||||
|
this.maps.compareWith(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSaveableContent(id: string, content: ISaveableContent<unknown>): void {
|
||||||
|
if (this.saveables.has(id)) {
|
||||||
|
logger.warn(112, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.saveables.set(id, content);
|
||||||
|
this.addedSaveables.add(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveableContent<T>(id: string): ISaveableContent<T> | null {
|
||||||
|
const content = this.saveables.get(id);
|
||||||
|
return (content as ISaveableContent<T>) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindSaveableExecuter<T>(
|
||||||
|
content: ISaveableContent<T> | string,
|
||||||
|
executor: ISaveableExecutor<T>
|
||||||
|
): void {
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
const saveable = this.saveables.get(content);
|
||||||
|
if (!saveable) return;
|
||||||
|
this.executors.set(saveable, executor);
|
||||||
|
} else {
|
||||||
|
if (!this.addedSaveables.has(content)) {
|
||||||
|
logger.warn(113);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.executors.set(content, executor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,10 +14,9 @@ import {
|
|||||||
IEnemyContext,
|
IEnemyContext,
|
||||||
IEnemySpecialModifier,
|
IEnemySpecialModifier,
|
||||||
IEnemyView,
|
IEnemyView,
|
||||||
IReadonlyEnemyHandler,
|
IReadonlyEnemyHandler
|
||||||
IReadonlyEnemy,
|
} from '@user/data-system';
|
||||||
ISpecial
|
import { IReadonlyEnemy, ISpecial } from '@user/data-base';
|
||||||
} from '@user/data-base';
|
|
||||||
import { IHaloValue } from './special';
|
import { IHaloValue } from './special';
|
||||||
import { IEnemyAttr } from './types';
|
import { IEnemyAttr } from './types';
|
||||||
import { IHeroAttr } from '../hero';
|
import { IHeroAttr } from '../hero';
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {
|
|||||||
IDamageCalculator,
|
IDamageCalculator,
|
||||||
IEnemyDamageInfo,
|
IEnemyDamageInfo,
|
||||||
IReadonlyEnemyHandler
|
IReadonlyEnemyHandler
|
||||||
} from '@user/data-base';
|
} from '@user/data-system';
|
||||||
import { IEnemyAttr } from './types';
|
import { IEnemyAttr } from './types';
|
||||||
import { IVampireValue } from './special';
|
import { IVampireValue } from './special';
|
||||||
import { IHeroAttr } from '../hero';
|
import { IHeroAttr } from '../hero';
|
||||||
@ -16,10 +16,6 @@ export class MainDamageCalculator implements IDamageCalculator<
|
|||||||
/** 当前是否正在计算支援怪的伤害 */
|
/** 当前是否正在计算支援怪的伤害 */
|
||||||
private inGuard: boolean = false;
|
private inGuard: boolean = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算战斗伤害信息
|
|
||||||
* @param handler 信息对象
|
|
||||||
*/
|
|
||||||
calculate(
|
calculate(
|
||||||
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
|
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
|
||||||
): IEnemyDamageInfo {
|
): IEnemyDamageInfo {
|
||||||
@ -94,7 +90,8 @@ export class MainDamageCalculator implements IDamageCalculator<
|
|||||||
const extraInfo = this.calculate({
|
const extraInfo = this.calculate({
|
||||||
enemy: guard.getComputedEnemy(),
|
enemy: guard.getComputedEnemy(),
|
||||||
locator,
|
locator,
|
||||||
hero
|
hero,
|
||||||
|
data: handler.data
|
||||||
});
|
});
|
||||||
turn += extraInfo.turn;
|
turn += extraInfo.turn;
|
||||||
damage += extraInfo.damage;
|
damage += extraInfo.damage;
|
||||||
@ -153,11 +150,6 @@ export class MainDamageCalculator implements IDamageCalculator<
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取临界计算的上界
|
|
||||||
* @param handler 信息对象
|
|
||||||
* @param attribute 目标属性名
|
|
||||||
*/
|
|
||||||
getCriticalLimit(
|
getCriticalLimit(
|
||||||
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
|
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>,
|
||||||
attribute: CriticalableHeroStatus<IHeroAttr>
|
attribute: CriticalableHeroStatus<IHeroAttr>
|
||||||
|
|||||||
31
packages-user/data-state/src/enemy/comparer.ts
Normal file
31
packages-user/data-state/src/enemy/comparer.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { IEnemyComparer, IReadonlyEnemy } from '@user/data-base';
|
||||||
|
import { IEnemyAttr } from './types';
|
||||||
|
|
||||||
|
export class MainEnemyComparer implements IEnemyComparer<IEnemyAttr> {
|
||||||
|
compare(
|
||||||
|
enemyA: IReadonlyEnemy<IEnemyAttr>,
|
||||||
|
enemyB: IReadonlyEnemy<IEnemyAttr>
|
||||||
|
): boolean {
|
||||||
|
// 比较基本属性
|
||||||
|
if (
|
||||||
|
enemyA.getAttribute('hp') !== enemyB.getAttribute('hp') ||
|
||||||
|
enemyA.getAttribute('atk') !== enemyB.getAttribute('atk') ||
|
||||||
|
enemyA.getAttribute('def') !== enemyB.getAttribute('def') ||
|
||||||
|
enemyA.getAttribute('money') !== enemyB.getAttribute('money') ||
|
||||||
|
enemyA.getAttribute('exp') !== enemyB.getAttribute('exp') ||
|
||||||
|
enemyA.getAttribute('point') !== enemyB.getAttribute('point')
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较特殊属性
|
||||||
|
const specialsA = [...enemyA.iterateSpecials()];
|
||||||
|
const specialsB = [...enemyB.iterateSpecials()];
|
||||||
|
if (specialsA.length !== specialsB.length) return false;
|
||||||
|
for (const special of specialsA) {
|
||||||
|
const other = enemyB.getSpecial(special.code);
|
||||||
|
if (!other || !special.deepEqualsTo(other)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { IEnemyFinalEffect, IEnemyHandler } from '@user/data-base';
|
import { IEnemyFinalEffect, IEnemyHandler } from '@user/data-system';
|
||||||
import { IEnemyAttr } from './types';
|
import { IEnemyAttr } from './types';
|
||||||
import { IHeroAttr } from '../hero';
|
import { IHeroAttr } from '../hero';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
export * from './aura';
|
export * from './aura';
|
||||||
export * from './calculator';
|
export * from './calculator';
|
||||||
|
export * from './comparer';
|
||||||
export * from './damage';
|
export * from './damage';
|
||||||
export * from './final';
|
export * from './final';
|
||||||
export * from './legacy';
|
export * from './legacy';
|
||||||
export * from './mapDamage';
|
export * from './mapDamage';
|
||||||
export * from './special';
|
export * from './special';
|
||||||
|
export * from './types';
|
||||||
|
|||||||
@ -1,7 +1,4 @@
|
|||||||
import {
|
import {
|
||||||
DirectionMapper,
|
|
||||||
IDirectionDescriptor,
|
|
||||||
InternalDirectionGroup,
|
|
||||||
IManhattanRangeParam,
|
IManhattanRangeParam,
|
||||||
IRange,
|
IRange,
|
||||||
IRayRangeParam,
|
IRayRangeParam,
|
||||||
@ -9,7 +6,8 @@ import {
|
|||||||
ManhattanRange,
|
ManhattanRange,
|
||||||
RayRange,
|
RayRange,
|
||||||
RectRange,
|
RectRange,
|
||||||
ITileLocator
|
ITileLocator,
|
||||||
|
IDirectionDescriptor
|
||||||
} from '@motajs/common';
|
} from '@motajs/common';
|
||||||
import {
|
import {
|
||||||
IEnemyContext,
|
IEnemyContext,
|
||||||
@ -18,10 +16,14 @@ import {
|
|||||||
IMapDamageInfoExtra,
|
IMapDamageInfoExtra,
|
||||||
IMapDamageReducer,
|
IMapDamageReducer,
|
||||||
IReadonlyEnemyHandler,
|
IReadonlyEnemyHandler,
|
||||||
|
IMapDamageView
|
||||||
|
} from '@user/data-system';
|
||||||
|
import {
|
||||||
|
IFaceHandler,
|
||||||
ISpecial,
|
ISpecial,
|
||||||
IMapDamageView,
|
|
||||||
IReadonlyHeroAttribute,
|
IReadonlyHeroAttribute,
|
||||||
IReadonlyEnemy
|
IReadonlyEnemy,
|
||||||
|
InternalFaceGroup
|
||||||
} from '@user/data-base';
|
} from '@user/data-base';
|
||||||
import { IZoneValue } from './special';
|
import { IZoneValue } from './special';
|
||||||
import { IEnemyAttr, MapDamageType } from './types';
|
import { IEnemyAttr, MapDamageType } from './types';
|
||||||
@ -31,9 +33,6 @@ const RECT_RANGE = new RectRange();
|
|||||||
const MANHATTAN_RANGE = new ManhattanRange();
|
const MANHATTAN_RANGE = new ManhattanRange();
|
||||||
const RAY_RANGE = new RayRange();
|
const RAY_RANGE = new RayRange();
|
||||||
|
|
||||||
const DIRECTION_MAPPER = new DirectionMapper();
|
|
||||||
const DIR4 = [...DIRECTION_MAPPER.map(InternalDirectionGroup.Dir4)];
|
|
||||||
|
|
||||||
//#region 地图伤害
|
//#region 地图伤害
|
||||||
|
|
||||||
abstract class BaseMapDamageView<T> implements IMapDamageView<T> {
|
abstract class BaseMapDamageView<T> implements IMapDamageView<T> {
|
||||||
@ -154,13 +153,17 @@ export class RepulseDamageView extends BaseMapDamageView<IManhattanRangeParam> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LaserDamageView extends BaseMapDamageView<IRayRangeParam> {
|
export class LaserDamageView extends BaseMapDamageView<IRayRangeParam> {
|
||||||
|
/** 激光方向列表 */
|
||||||
|
private readonly dirs: IDirectionDescriptor[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
|
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
|
||||||
private readonly locator: Readonly<ITileLocator>,
|
private readonly locator: Readonly<ITileLocator>,
|
||||||
private readonly special: Readonly<ISpecial<number>>,
|
private readonly special: Readonly<ISpecial<number>>,
|
||||||
private readonly dir: IDirectionDescriptor[] = DIR4
|
dir: IFaceHandler<number>
|
||||||
) {
|
) {
|
||||||
super(context);
|
super(context);
|
||||||
|
this.dirs = [...dir.mapMovement()].map(v => v[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRange(): IRange<IRayRangeParam> {
|
getRange(): IRange<IRayRangeParam> {
|
||||||
@ -171,7 +174,7 @@ export class LaserDamageView extends BaseMapDamageView<IRayRangeParam> {
|
|||||||
return {
|
return {
|
||||||
cx: this.locator.x,
|
cx: this.locator.x,
|
||||||
cy: this.locator.y,
|
cy: this.locator.y,
|
||||||
dir: this.dir
|
dir: this.dirs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +312,8 @@ export class MainMapDamageConverter implements IMapDamageConverter<
|
|||||||
|
|
||||||
const laser = enemy.getSpecial<number>(24);
|
const laser = enemy.getSpecial<number>(24);
|
||||||
if (laser) {
|
if (laser) {
|
||||||
views.push(new LaserDamageView(context, locator, laser));
|
const face = handler.data.faceManager.get(InternalFaceGroup.Dir4)!;
|
||||||
|
views.push(new LaserDamageView(context, locator, laser, face));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enemy.hasSpecial(27)) {
|
if (enemy.hasSpecial(27)) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IEnemyView } from '@user/data-base';
|
import { IEnemyView } from '@user/data-system';
|
||||||
|
|
||||||
export interface IEnemyAttr {
|
export interface IEnemyAttr {
|
||||||
/** 怪物生命值 */
|
/** 怪物生命值 */
|
||||||
|
|||||||
@ -1,28 +1,9 @@
|
|||||||
import { loading } from '@user/data-base';
|
import { FaceDirection, loading } from '@user/data-base';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { FaceDirection } from './common';
|
|
||||||
import { ICoreState } from './types';
|
import { ICoreState } from './types';
|
||||||
import { TILE_HEIGHT, TILE_WIDTH } from './shared';
|
|
||||||
import { state } from './ins';
|
import { state } from './ins';
|
||||||
|
|
||||||
function createCoreState(state: ICoreState) {
|
function createCoreState(state: ICoreState) {
|
||||||
//#region 地图部分
|
|
||||||
|
|
||||||
const width = TILE_WIDTH;
|
|
||||||
const height = TILE_HEIGHT;
|
|
||||||
const bg = state.layer.addLayer(width, height);
|
|
||||||
const bg2 = state.layer.addLayer(width, height);
|
|
||||||
const event = state.layer.addLayer(width, height);
|
|
||||||
const fg = state.layer.addLayer(width, height);
|
|
||||||
const fg2 = state.layer.addLayer(width, height);
|
|
||||||
state.layer.setLayerAlias(bg, 'bg');
|
|
||||||
state.layer.setLayerAlias(bg2, 'bg2');
|
|
||||||
state.layer.setLayerAlias(event, 'event');
|
|
||||||
state.layer.setLayerAlias(fg, 'fg');
|
|
||||||
state.layer.setLayerAlias(fg2, 'fg2');
|
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 图块部分
|
//#region 图块部分
|
||||||
|
|
||||||
const data = Object.entries(core.maps.blocksInfo);
|
const data = Object.entries(core.maps.blocksInfo);
|
||||||
@ -62,11 +43,9 @@ export function create() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './common';
|
|
||||||
export * from './enemy';
|
export * from './enemy';
|
||||||
export * from './hero';
|
export * from './hero';
|
||||||
export * from './legacy';
|
export * from './legacy';
|
||||||
export * from './map';
|
|
||||||
|
|
||||||
export * from './core';
|
export * from './core';
|
||||||
export * from './ins';
|
export * from './ins';
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import { backDir, toDir } from './utils';
|
import { backDir, toDir } from './utils';
|
||||||
import { loading } from '@user/data-base';
|
import { fromDirectionString, loading } from '@user/data-base';
|
||||||
import type { RenderAdapter } from '@motajs/render';
|
|
||||||
import type { HeroKeyMover } from '@user/client-modules';
|
import type { HeroKeyMover } from '@user/client-modules';
|
||||||
import { sleep } from '@motajs/common';
|
import { sleep } from '@motajs/common';
|
||||||
import { fromDirectionString, state } from '..';
|
import { state } from '..';
|
||||||
|
|
||||||
// todo: 转身功能
|
// todo: 转身功能
|
||||||
|
|
||||||
|
|||||||
@ -1,278 +0,0 @@
|
|||||||
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
|
||||||
|
|
||||||
export interface IMapLayerData {
|
|
||||||
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
|
||||||
expired: boolean;
|
|
||||||
/** 地图图块数组,是对内部存储的直接引用 */
|
|
||||||
array: Uint32Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapLayerHooks extends IHookBase {
|
|
||||||
/**
|
|
||||||
* 当地图大小发生变化时执行,如果调用了地图的 `resize` 方法,但是地图大小没变,则不会触发
|
|
||||||
* @param width 地图宽度
|
|
||||||
* @param height 地图高度
|
|
||||||
*/
|
|
||||||
onResize(width: number, height: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当更新某个区域的图块时执行
|
|
||||||
* @param x 更新区域左上角横坐标
|
|
||||||
* @param y 更新区域左上角纵坐标
|
|
||||||
* @param width 更新区域宽度
|
|
||||||
* @param height 更新区域高度
|
|
||||||
*/
|
|
||||||
onUpdateArea(x: number, y: number, width: number, height: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法
|
|
||||||
* @param block 更新为的图块数字
|
|
||||||
* @param x 更新点横坐标
|
|
||||||
* @param y 更新点纵坐标
|
|
||||||
*/
|
|
||||||
onUpdateBlock(block: number, x: number, y: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当开门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
|
|
||||||
* @param x 门横坐标
|
|
||||||
* @param y 门纵坐标
|
|
||||||
*/
|
|
||||||
onOpenDoor(x: number, y: number): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当关门时触发,返回一个 `Promise`,当相关动画执行完毕后兑现
|
|
||||||
* @param num 门的图块数字
|
|
||||||
* @param x 门横坐标
|
|
||||||
* @param y 门纵坐标
|
|
||||||
*/
|
|
||||||
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapLayerHookController
|
|
||||||
extends IHookController<IMapLayerHooks> {
|
|
||||||
/** 拓展所属的图层对象 */
|
|
||||||
readonly layer: IMapLayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取地图数据,是对内部存储的直接引用
|
|
||||||
*/
|
|
||||||
getMapData(): Readonly<IMapLayerData>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IMapLayer
|
|
||||||
extends IHookable<IMapLayerHooks, IMapLayerHookController> {
|
|
||||||
/** 地图宽度 */
|
|
||||||
readonly width: number;
|
|
||||||
/** 地图高度 */
|
|
||||||
readonly height: number;
|
|
||||||
/**
|
|
||||||
* 地图是否全部空白,此值具有充分性,但不具有必要性,
|
|
||||||
* 即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白
|
|
||||||
*/
|
|
||||||
readonly empty: boolean;
|
|
||||||
/** 图层纵深 */
|
|
||||||
readonly zIndex: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调整地图尺寸,维持原有图块。如果尺寸变大,那么会补零,如果尺寸变小,那么会将当前数组裁剪
|
|
||||||
* @param width 地图宽度
|
|
||||||
* @param height 地图高度
|
|
||||||
*/
|
|
||||||
resize(width: number, height: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调整地图尺寸,但是将地图全部重置为零,不保留原地图数据
|
|
||||||
* @param width 地图宽度
|
|
||||||
* @param height 地图高度
|
|
||||||
*/
|
|
||||||
resize2(width: number, height: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置某一点的图块
|
|
||||||
* @param block 图块数字
|
|
||||||
* @param x 图块横坐标
|
|
||||||
* @param y 图块纵坐标
|
|
||||||
*/
|
|
||||||
setBlock(block: number, x: number, y: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定点的图块
|
|
||||||
* @param x 图块横坐标
|
|
||||||
* @param y 图块纵坐标
|
|
||||||
* @returns 指定点的图块,如果没有图块,返回 0,如果不在地图上,返回 -1
|
|
||||||
*/
|
|
||||||
getBlock(x: number, y: number): number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置地图图块
|
|
||||||
* @param array 地图图块数组
|
|
||||||
* @param x 数组第一项代表的横坐标
|
|
||||||
* @param y 数组第一项代表的纵坐标
|
|
||||||
* @param width 传入数组所表示的矩形范围的宽度
|
|
||||||
*/
|
|
||||||
putMapData(array: Uint32Array, x: number, y: number, width: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取整个地图的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容
|
|
||||||
*/
|
|
||||||
getMapData(): Uint32Array;
|
|
||||||
/**
|
|
||||||
* 获取地图指定区域的地图数组,是对内部地图数组的拷贝,并不能通过修改它来直接修改地图内容
|
|
||||||
* @param x 左上角横坐标
|
|
||||||
* @param y 左上角纵坐标
|
|
||||||
* @param width 获取区域的宽度
|
|
||||||
* @param height 获取区域的高度
|
|
||||||
*/
|
|
||||||
getMapData(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
): Uint32Array;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取整个地图的地图数组,是对内部数组的直接引用
|
|
||||||
*/
|
|
||||||
getMapRef(): IMapLayerData;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置地图纵深,会影响渲染的遮挡顺序
|
|
||||||
* @param zIndex 纵深
|
|
||||||
*/
|
|
||||||
setZIndex(zIndex: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开启指定位置的门
|
|
||||||
* @param x 门横坐标
|
|
||||||
* @param y 门纵坐标
|
|
||||||
*/
|
|
||||||
openDoor(x: number, y: number): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在指定位置关门,门的图块数字由参数指定
|
|
||||||
* @param num 门图块数字
|
|
||||||
* @param x 门横坐标
|
|
||||||
* @param y 门纵坐标
|
|
||||||
*/
|
|
||||||
closeDoor(num: number, x: number, y: number): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILayerStateHooks extends IHookBase {
|
|
||||||
/**
|
|
||||||
* 当设置背景图块时执行,如果设置的背景图块与原先一样,则不会执行
|
|
||||||
* @param tile 背景图块
|
|
||||||
*/
|
|
||||||
onChangeBackground(tile: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当地图列表发生变化时执行
|
|
||||||
* @param layerList 地图图层列表
|
|
||||||
*/
|
|
||||||
onUpdateLayer(layerList: Set<IMapLayer>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当地图状态对象的某个图层发生区域更新时执行
|
|
||||||
* @param layer 触发更新的地图图层对象
|
|
||||||
* @param x 更新区域左上角横坐标
|
|
||||||
* @param y 更新区域左上角纵坐标
|
|
||||||
* @param width 更新区域宽度
|
|
||||||
* @param height 更新区域高度
|
|
||||||
*/
|
|
||||||
onUpdateLayerArea(
|
|
||||||
layer: IMapLayer,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
width: number,
|
|
||||||
height: number
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当地图状态对象的某个图层设置图块时执行,如果设置的图块与原先一样则不会触发
|
|
||||||
* @param layer 触发更新的地图图层对象
|
|
||||||
* @param block 设置为的图块
|
|
||||||
* @param x 图块横坐标
|
|
||||||
* @param y 图块纵坐标
|
|
||||||
*/
|
|
||||||
onUpdateLayerBlock(
|
|
||||||
layer: IMapLayer,
|
|
||||||
block: number,
|
|
||||||
x: number,
|
|
||||||
y: number
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当地图状态对象的某个图层大小发生变化时执行
|
|
||||||
* @param layer 触发更新的地图图层对象
|
|
||||||
* @param width 地图的新宽度
|
|
||||||
* @param height 地图的新高度
|
|
||||||
*/
|
|
||||||
onResizeLayer(layer: IMapLayer, width: number, height: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
|
||||||
/** 地图列表 */
|
|
||||||
readonly layerList: Set<IMapLayer>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加图层
|
|
||||||
* @param width 地图宽度
|
|
||||||
* @param height 地图高度
|
|
||||||
*/
|
|
||||||
addLayer(width: number, height: number): IMapLayer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除指定图层
|
|
||||||
* @param layer 图层对象
|
|
||||||
*/
|
|
||||||
removeLayer(layer: IMapLayer): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前地图状态对象是否包含指定图层对象
|
|
||||||
* @param layer 图层对象
|
|
||||||
*/
|
|
||||||
hasLayer(layer: IMapLayer): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置图层别名
|
|
||||||
* @param layer 图层对象
|
|
||||||
* @param alias 图层别名
|
|
||||||
*/
|
|
||||||
setLayerAlias(layer: IMapLayer, alias: string): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据图层别名获取图层对象
|
|
||||||
* @param alias 图层别名
|
|
||||||
*/
|
|
||||||
getLayerByAlias(alias: string): IMapLayer | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取图层对象的别名
|
|
||||||
* @param layer 图层对象
|
|
||||||
*/
|
|
||||||
getLayerAlias(layer: IMapLayer): string | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新设置图层的大小
|
|
||||||
* @param layer 图层对象
|
|
||||||
* @param width 新的图层宽度
|
|
||||||
* @param height 新的图层高度
|
|
||||||
* @param keepBlock 是否保留原有图块,默认不保留
|
|
||||||
*/
|
|
||||||
resizeLayer(
|
|
||||||
layer: IMapLayer,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
keepBlock?: boolean
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置背景图块
|
|
||||||
* @param tile 背景图块数字
|
|
||||||
*/
|
|
||||||
setBackground(tile: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取背景图块数字,如果没有设置过,则返回 0
|
|
||||||
*/
|
|
||||||
getBackground(): number;
|
|
||||||
}
|
|
||||||
2
packages-user/data-state/src/save/index.ts
Normal file
2
packages-user/data-state/src/save/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './system';
|
||||||
|
export * from './types';
|
||||||
222
packages-user/data-state/src/save/system.ts
Normal file
222
packages-user/data-state/src/save/system.ts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import Dexie, { Table } from 'dexie';
|
||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
IGlobalTrasaction,
|
||||||
|
ISaveRead,
|
||||||
|
ISaveSystem,
|
||||||
|
ISaveSystemConfig
|
||||||
|
} from './types';
|
||||||
|
import { ISaveableContent, SaveCompression } from '@user/data-base';
|
||||||
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
|
interface ISaveRecord {
|
||||||
|
/** 存档 id */
|
||||||
|
readonly id: number;
|
||||||
|
/** 存档压缩级别 */
|
||||||
|
readonly compression: SaveCompression;
|
||||||
|
/** 存档内容 */
|
||||||
|
readonly data: Map<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IGlobalRecord {
|
||||||
|
/** 全局存储的键名 */
|
||||||
|
readonly key: string;
|
||||||
|
/** 全局存储的内容 */
|
||||||
|
readonly value: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GlobalTransaction implements IGlobalTrasaction {
|
||||||
|
constructor(readonly table: Table<IGlobalRecord, string>) {}
|
||||||
|
|
||||||
|
async get<T>(key: string): Promise<T> {
|
||||||
|
const record = await this.table.get(key);
|
||||||
|
return record!.value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(key: string, value: unknown): Promise<void> {
|
||||||
|
await this.table.put({ key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SaveSystem implements ISaveSystem {
|
||||||
|
db!: Dexie;
|
||||||
|
|
||||||
|
/** 当前的撤回栈 */
|
||||||
|
private readonly undoStack: ISaveRead[] = [];
|
||||||
|
/** 当前的重做栈 */
|
||||||
|
private readonly redoStack: ISaveRead[] = [];
|
||||||
|
|
||||||
|
/** 撤回栈与重做栈的最大长度 */
|
||||||
|
private stackSize: number = 20;
|
||||||
|
/** 自动存档压缩级别 */
|
||||||
|
private autosaveLevel: SaveCompression = SaveCompression.LowCompression;
|
||||||
|
/** 普通存档压缩级别 */
|
||||||
|
private commonSaveLevel: SaveCompression = SaveCompression.HighCompression;
|
||||||
|
/** 普通存档容忍时长 */
|
||||||
|
private saveTimeTolerance: number = 100;
|
||||||
|
/** 自动存档容忍时长 */
|
||||||
|
private autosaveTimeTolerance: number = 50;
|
||||||
|
|
||||||
|
init(name: string) {
|
||||||
|
this.db = new Dexie(name);
|
||||||
|
this.db.version(1).stores({
|
||||||
|
saves: 'id',
|
||||||
|
global: 'key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config(config: Readonly<Partial<ISaveSystemConfig>>): void {
|
||||||
|
if (!isNil(config.autosaveLevel)) {
|
||||||
|
this.autosaveLevel = config.autosaveLevel;
|
||||||
|
}
|
||||||
|
if (!isNil(config.commonSaveLevel)) {
|
||||||
|
this.commonSaveLevel = config.commonSaveLevel;
|
||||||
|
}
|
||||||
|
if (!isNil(config.saveTimeTolerance)) {
|
||||||
|
this.saveTimeTolerance = config.saveTimeTolerance;
|
||||||
|
}
|
||||||
|
if (!isNil(config.autosaveTimeTolerance)) {
|
||||||
|
this.autosaveTimeTolerance = config.autosaveTimeTolerance;
|
||||||
|
}
|
||||||
|
if (!isNil(config.autosaveStackSize)) {
|
||||||
|
const size = config.autosaveStackSize;
|
||||||
|
this.stackSize = size;
|
||||||
|
if (this.undoStack.length > size) {
|
||||||
|
this.undoStack.splice(0, this.undoStack.length - size);
|
||||||
|
}
|
||||||
|
if (this.redoStack.length > size) {
|
||||||
|
this.redoStack.splice(0, this.redoStack.length - size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
undoAutosave(
|
||||||
|
current: Map<string, ISaveableContent<unknown>>
|
||||||
|
): ISaveRead | null {
|
||||||
|
if (this.undoStack.length === 0) return null;
|
||||||
|
const data = new Map<string, unknown>();
|
||||||
|
for (const [key, content] of current) {
|
||||||
|
data.set(key, content.saveState(this.autosaveLevel));
|
||||||
|
}
|
||||||
|
this.redoStack.push({ compression: this.autosaveLevel, data });
|
||||||
|
if (this.redoStack.length > this.stackSize) {
|
||||||
|
this.redoStack.splice(0, this.redoStack.length - this.stackSize);
|
||||||
|
}
|
||||||
|
return this.undoStack.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
redoAutosave(
|
||||||
|
current: Map<string, ISaveableContent<unknown>>
|
||||||
|
): ISaveRead | null {
|
||||||
|
if (this.redoStack.length === 0) return null;
|
||||||
|
const data = new Map<string, unknown>();
|
||||||
|
for (const [key, content] of current) {
|
||||||
|
data.set(key, content.saveState(this.autosaveLevel));
|
||||||
|
}
|
||||||
|
this.undoStack.push({ compression: this.autosaveLevel, data });
|
||||||
|
if (this.undoStack.length > this.stackSize) {
|
||||||
|
this.undoStack.splice(0, this.undoStack.length - this.stackSize);
|
||||||
|
}
|
||||||
|
return this.redoStack.pop()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUndoStack(): ISaveRead[] {
|
||||||
|
return this.undoStack.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRedoStack(): ISaveRead[] {
|
||||||
|
return this.redoStack.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
autosave(state: Map<string, ISaveableContent<unknown>>): void {
|
||||||
|
const data = new Map<string, unknown>();
|
||||||
|
for (const [key, content] of state) {
|
||||||
|
data.set(key, content.saveState(this.autosaveLevel));
|
||||||
|
}
|
||||||
|
this.undoStack.push({ compression: this.autosaveLevel, data });
|
||||||
|
this.redoStack.length = 0;
|
||||||
|
if (this.undoStack.length > this.stackSize) {
|
||||||
|
this.undoStack.splice(0, this.undoStack.length - this.stackSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveAutosaveToDB(): Promise<void> {
|
||||||
|
if (this.undoStack.length === 0) return;
|
||||||
|
const t0 = performance.now();
|
||||||
|
const top = this.undoStack[this.undoStack.length - 1];
|
||||||
|
const table = this.db.table<ISaveRecord, number>('saves');
|
||||||
|
await table.put({
|
||||||
|
id: -1,
|
||||||
|
compression: top.compression,
|
||||||
|
data: top.data
|
||||||
|
});
|
||||||
|
const t1 = performance.now();
|
||||||
|
if (t1 - t0 > this.autosaveTimeTolerance) {
|
||||||
|
logger.warn(
|
||||||
|
115,
|
||||||
|
(t1 - t0).toFixed(0),
|
||||||
|
this.autosaveTimeTolerance.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(
|
||||||
|
id: number,
|
||||||
|
state: Map<string, ISaveableContent<unknown>>
|
||||||
|
): Promise<void> {
|
||||||
|
const t0 = performance.now();
|
||||||
|
const data = new Map<string, unknown>();
|
||||||
|
for (const [key, content] of state) {
|
||||||
|
data.set(key, content.saveState(this.commonSaveLevel));
|
||||||
|
}
|
||||||
|
const table = this.db.table<ISaveRecord, number>('saves');
|
||||||
|
await table.put({ id, compression: this.commonSaveLevel, data });
|
||||||
|
await this.setGlobal('lastSlot', id);
|
||||||
|
const t1 = performance.now();
|
||||||
|
if (t1 - t0 > this.saveTimeTolerance) {
|
||||||
|
logger.warn(
|
||||||
|
114,
|
||||||
|
(t1 - t0).toFixed(0),
|
||||||
|
this.saveTimeTolerance.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(id: number): Promise<ISaveRead | null> {
|
||||||
|
const table = this.db.table<ISaveRecord, number>('saves');
|
||||||
|
const record = await table.get(id);
|
||||||
|
if (record === undefined) return null;
|
||||||
|
return { compression: record.compression, data: record.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSave(id: number): Promise<void> {
|
||||||
|
const table = this.db.table<ISaveRecord, number>('saves');
|
||||||
|
await table.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastSlot(): Promise<number> {
|
||||||
|
const value = await this.getGlobal<number | undefined>('lastSlot');
|
||||||
|
return value ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGlobal<T>(key: string): Promise<T | null> {
|
||||||
|
const table = this.db.table<IGlobalRecord, string>('global');
|
||||||
|
const record = await table.get(key);
|
||||||
|
if (!record) return null;
|
||||||
|
else return record.value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setGlobal(key: string, value: unknown): Promise<void> {
|
||||||
|
const table = this.db.table<IGlobalRecord, string>('global');
|
||||||
|
await table.put({ key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async startGlobalTransaction<R>(
|
||||||
|
handle: (transaction: IGlobalTrasaction) => PromiseLike<R>
|
||||||
|
): Promise<R> {
|
||||||
|
const globalTable = this.db.table<IGlobalRecord, string>('global');
|
||||||
|
return this.db.transaction('rw', globalTable, () => {
|
||||||
|
return handle(new GlobalTransaction(globalTable));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
142
packages-user/data-state/src/save/types.ts
Normal file
142
packages-user/data-state/src/save/types.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { ISaveableContent, SaveCompression } from '@user/data-base';
|
||||||
|
import { Dexie, Table } from 'dexie';
|
||||||
|
|
||||||
|
export interface IGlobalTrasaction {
|
||||||
|
/** 全局存储对应的表 */
|
||||||
|
readonly table: Table<unknown, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定键值对应的数据
|
||||||
|
* @param key 全局键值
|
||||||
|
*/
|
||||||
|
get<T>(key: string): Promise<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定键值存储的数据
|
||||||
|
* @param key 全局键值
|
||||||
|
* @param value 存储数据
|
||||||
|
*/
|
||||||
|
set(key: string, value: unknown): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaveSystemConfig {
|
||||||
|
/** 自动存档使用的压缩等级 */
|
||||||
|
autosaveLevel: SaveCompression;
|
||||||
|
/** 普通存档使用的压缩等级 */
|
||||||
|
commonSaveLevel: SaveCompression;
|
||||||
|
/** 可容忍的最大存档耗时,超过此值会抛出警告 */
|
||||||
|
saveTimeTolerance: number;
|
||||||
|
/** 可容忍的最大自动存档耗时,超过此值会抛出警告 */
|
||||||
|
autosaveTimeTolerance: number;
|
||||||
|
/** 自动存档栈最大大小 */
|
||||||
|
autosaveStackSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaveRead {
|
||||||
|
/** 该存档的压缩等级 */
|
||||||
|
readonly compression: SaveCompression;
|
||||||
|
/** 该存档的数据 */
|
||||||
|
readonly data: Map<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaveSystem {
|
||||||
|
/** Dexie 数据库 */
|
||||||
|
readonly db: Dexie;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化存档数据库
|
||||||
|
* @param name 数据库名称
|
||||||
|
*/
|
||||||
|
init(name: string): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置此存档系统
|
||||||
|
* @param config 配置对象
|
||||||
|
*/
|
||||||
|
config(config: Readonly<Partial<ISaveSystemConfig>>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 `undo` 栈读取上一个自动存档,然后将当前状态加入 `redo` 栈
|
||||||
|
* @param current 当前游戏状态,需要加入 `redo` 栈
|
||||||
|
*/
|
||||||
|
undoAutosave(
|
||||||
|
current: Map<string, ISaveableContent<unknown>>
|
||||||
|
): ISaveRead | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 `redo` 栈读取自动存档,并将当前状态加入 `undo` 栈
|
||||||
|
* @param current 当前游戏状态,需要加入 `undo` 栈
|
||||||
|
*/
|
||||||
|
redoAutosave(
|
||||||
|
current: Map<string, ISaveableContent<unknown>>
|
||||||
|
): ISaveRead | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前的撤回栈
|
||||||
|
*/
|
||||||
|
getUndoStack(): ISaveRead[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前的重做栈
|
||||||
|
*/
|
||||||
|
getRedoStack(): ISaveRead[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行自动存档,加入撤回栈
|
||||||
|
* @param state 状态对象
|
||||||
|
*/
|
||||||
|
autosave(state: Map<string, ISaveableContent<unknown>>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 `undo` 栈顶的自动存档真正存入 `IndexedDB`
|
||||||
|
*/
|
||||||
|
saveAutosaveToDB(): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将状态对象存入存档
|
||||||
|
* @param id 存档 id,用于建立存档索引及查询
|
||||||
|
* @param state 状态对象
|
||||||
|
*/
|
||||||
|
save(
|
||||||
|
id: number,
|
||||||
|
state: Map<string, ISaveableContent<unknown>>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 id 读取指定存档
|
||||||
|
* @param id 存档 id
|
||||||
|
*/
|
||||||
|
load(id: number): Promise<ISaveRead | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定存档
|
||||||
|
* @param id 存档 id
|
||||||
|
*/
|
||||||
|
deleteSave(id: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后一次存档的存档栏位
|
||||||
|
*/
|
||||||
|
getLastSlot(): Promise<number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定键值对应的全局存储
|
||||||
|
* @param key 全局键值
|
||||||
|
*/
|
||||||
|
getGlobal<T>(key: string): Promise<T | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定键值对应的全局存储
|
||||||
|
* @param key 全局键值
|
||||||
|
* @param value 存储数据
|
||||||
|
*/
|
||||||
|
setGlobal(key: string, value: unknown): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行全局存储的事务处理,适用于多内容查询与设置,当出现错误时其中的任何写入操作都不会真正存储
|
||||||
|
* @param handle 事务处理函数
|
||||||
|
*/
|
||||||
|
startGlobalTransaction<R>(
|
||||||
|
handle: (transaction: IGlobalTrasaction) => PromiseLike<R>
|
||||||
|
): Promise<R>;
|
||||||
|
}
|
||||||
@ -1,10 +1,27 @@
|
|||||||
import { IHeroAttr } from './hero';
|
import { IHeroAttr } from './hero';
|
||||||
|
|
||||||
|
//#region 地图相关
|
||||||
|
|
||||||
/** 每个地图的默认宽度 */
|
/** 每个地图的默认宽度 */
|
||||||
export const TILE_WIDTH = 13;
|
export const TILE_WIDTH = 13;
|
||||||
/** 每个地图的默认高度 */
|
/** 每个地图的默认高度 */
|
||||||
export const TILE_HEIGHT = 13;
|
export const TILE_HEIGHT = 13;
|
||||||
|
|
||||||
|
// 图层纵深,这些纵深与渲染系统的纵深没有关系,仅在地图图层之间生效
|
||||||
|
|
||||||
|
/** 背景层纵深 */
|
||||||
|
export const BG_ZINDEX = 0;
|
||||||
|
/** 背景层2纵深 */
|
||||||
|
export const BG2_ZINDEX = 10;
|
||||||
|
/** 事件层纵深 */
|
||||||
|
export const EVENT_ZINDEX = 20;
|
||||||
|
/** 前景层纵深 */
|
||||||
|
export const FG_ZINDEX = 30;
|
||||||
|
/** 前景层2纵深 */
|
||||||
|
export const FG2_ZINDEX = 40;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region 勇士相关
|
//#region 勇士相关
|
||||||
|
|
||||||
/** 默认的勇士图片 */
|
/** 默认的勇士图片 */
|
||||||
|
|||||||
@ -1,61 +1,42 @@
|
|||||||
import { ILayerState } from './map';
|
|
||||||
import { IRoleFaceBinder } from './common';
|
|
||||||
import {
|
import {
|
||||||
IEnemyContext,
|
IEnemyContext,
|
||||||
IEnemyManager,
|
IMotaDataLoader,
|
||||||
IHeroFollower,
|
ISaveableContent,
|
||||||
IHeroState,
|
IStateBase
|
||||||
IMotaDataLoader
|
|
||||||
} from '@user/data-base';
|
} from '@user/data-base';
|
||||||
import { IEnemyAttr } from './enemy/types';
|
import { IEnemyAttr } from './enemy';
|
||||||
import { IHeroAttr } from './hero';
|
import { IHeroAttr } from './hero';
|
||||||
import { IFlagSystem } from '../../data-base/src/flag/types';
|
|
||||||
import { ILoadProgressTotal } from '@motajs/loader';
|
import { ILoadProgressTotal } from '@motajs/loader';
|
||||||
|
import { ISaveSystem } from './save';
|
||||||
|
|
||||||
export interface IGameDataState {
|
export interface ISaveableExecutor<T, TEnemy = IEnemyAttr, THero = IHeroAttr> {
|
||||||
/** 怪物管理器 */
|
/**
|
||||||
readonly enemyManager: IEnemyManager<IEnemyAttr>;
|
* 当数据读取后执行的函数,允许对其他存档对象进行读取
|
||||||
|
* @param data 对应可存档对象的存档数据
|
||||||
|
* @param state 当前的基础状态
|
||||||
|
*/
|
||||||
|
afterLoad(data: T, state: IStateBase<TEnemy, THero>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStateSaveData {
|
export interface ICoreState extends IStateBase<IEnemyAttr, IHeroAttr> {
|
||||||
/** 跟随者列表 */
|
|
||||||
readonly followers: readonly IHeroFollower[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICoreState {
|
|
||||||
/** 朝向绑定 */
|
|
||||||
readonly roleFace: IRoleFaceBinder;
|
|
||||||
/** id 到图块数字的映射 */
|
|
||||||
readonly idNumberMap: Map<string, number>;
|
|
||||||
/** 图块数字到 id 的映射 */
|
|
||||||
readonly numberIdMap: Map<number, string>;
|
|
||||||
|
|
||||||
/** 加载进度对象 */
|
/** 加载进度对象 */
|
||||||
readonly loadProgress: ILoadProgressTotal;
|
readonly loadProgress: ILoadProgressTotal;
|
||||||
/** 数据端加载对象 */
|
/** 数据端加载对象 */
|
||||||
readonly dataLoader: IMotaDataLoader;
|
readonly dataLoader: IMotaDataLoader;
|
||||||
|
|
||||||
/** 地图状态 */
|
|
||||||
readonly layer: ILayerState;
|
|
||||||
/** 勇士状态 */
|
|
||||||
readonly hero: IHeroState<IHeroAttr>;
|
|
||||||
|
|
||||||
/** 怪物管理器 */
|
|
||||||
readonly enemyManager: IEnemyManager<IEnemyAttr>;
|
|
||||||
/** 怪物上下文 */
|
/** 怪物上下文 */
|
||||||
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
||||||
|
|
||||||
/** Flag 系统 */
|
/** 存档系统 */
|
||||||
readonly flags: IFlagSystem;
|
readonly saveSystem: ISaveSystem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存状态
|
* 将某个存档执行器绑定至指定的可存档对象,一个可存档对象只能绑定一个执行器,
|
||||||
|
* 但一个执行器可以绑定多个可存档对象,主要用来在读档后进行一些全局性的操作
|
||||||
|
* @param content 可存档对象或其注册 id
|
||||||
|
* @param executor 可存档对象对应的执行器
|
||||||
*/
|
*/
|
||||||
saveState(): IStateSaveData;
|
bindSaveableExecuter<T>(
|
||||||
|
content: ISaveableContent<T> | string,
|
||||||
/**
|
executor: ISaveableExecutor<T>
|
||||||
* 加载状态
|
): void;
|
||||||
* @param data 状态对象
|
|
||||||
*/
|
|
||||||
loadState(data: IStateSaveData): void;
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages-user/data-system/package.json
Normal file
7
packages-user/data-system/package.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "@user/data-system",
|
||||||
|
"dependencies": {
|
||||||
|
"@motajs/common": "workspace:*",
|
||||||
|
"@user/data-base": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ import {
|
|||||||
IAuraConverter,
|
IAuraConverter,
|
||||||
IAuraView,
|
IAuraView,
|
||||||
IDamageSystem,
|
IDamageSystem,
|
||||||
IEnemy,
|
|
||||||
IEnemyAuraView,
|
IEnemyAuraView,
|
||||||
IEnemyCommonQueryEffect,
|
IEnemyCommonQueryEffect,
|
||||||
IEnemyContext,
|
IEnemyContext,
|
||||||
@ -13,76 +12,87 @@ import {
|
|||||||
IEnemySpecialQueryEffect,
|
IEnemySpecialQueryEffect,
|
||||||
IEnemyView,
|
IEnemyView,
|
||||||
IMapDamage,
|
IMapDamage,
|
||||||
IReadonlyEnemy,
|
IReadonlyEnemyHandler
|
||||||
IReadonlyEnemyHandler,
|
|
||||||
ISpecial
|
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import {
|
||||||
|
IReadonlyHeroAttribute,
|
||||||
|
IEnemy,
|
||||||
|
IReadonlyEnemy,
|
||||||
|
ISpecial,
|
||||||
|
IStateBase,
|
||||||
|
ILocationIndexer,
|
||||||
|
MapLocIndexer
|
||||||
|
} from '@user/data-base';
|
||||||
import { EnemyView } from './enemy';
|
import { EnemyView } from './enemy';
|
||||||
import { MapLocIndexer } from './utils';
|
|
||||||
import { IReadonlyHeroAttribute } from '../hero';
|
|
||||||
|
|
||||||
export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
export class EnemyContext<TEnemy, THero> implements IEnemyContext<
|
||||||
|
TEnemy,
|
||||||
|
THero
|
||||||
|
> {
|
||||||
/** 坐标索引 -> 怪物视图 */
|
/** 坐标索引 -> 怪物视图 */
|
||||||
private readonly enemyViewMap: Map<number, EnemyView<TAttr>> = new Map();
|
private readonly enemyViewMap: Map<number, EnemyView<TEnemy>> = new Map();
|
||||||
/** 坐标索引 -> 计算前怪物对象 */
|
/** 坐标索引 -> 计算前怪物对象 */
|
||||||
private readonly enemyMap: Map<number, IEnemy<TAttr>> = new Map();
|
private readonly enemyMap: Map<number, IEnemy<TEnemy>> = new Map();
|
||||||
/** 怪物视图 -> 坐标索引 */
|
/** 怪物视图 -> 坐标索引 */
|
||||||
private readonly locatorViewMap: Map<IEnemyView<TAttr>, number> = new Map();
|
private readonly locatorViewMap: Map<IEnemyView<TEnemy>, number> =
|
||||||
|
new Map();
|
||||||
/** 计算前怪物对象 -> 坐标索引 */
|
/** 计算前怪物对象 -> 坐标索引 */
|
||||||
private readonly locatorEnemyMap: Map<IEnemy<TAttr>, number> = new Map();
|
private readonly locatorEnemyMap: Map<IEnemy<TEnemy>, number> = new Map();
|
||||||
/** 计算后怪物对象 -> 怪物视图 */
|
/** 计算后怪物对象 -> 怪物视图 */
|
||||||
private readonly computedToView: Map<
|
private readonly computedToView: Map<
|
||||||
IReadonlyEnemy<TAttr>,
|
IReadonlyEnemy<TEnemy>,
|
||||||
EnemyView<TAttr>
|
EnemyView<TEnemy>
|
||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
/** 当前已注册的光环转换器 */
|
/** 当前已注册的光环转换器 */
|
||||||
private readonly auraConverter: Set<IAuraConverter<TAttr, THero>> =
|
private readonly auraConverter: Set<IAuraConverter<TEnemy, THero>> =
|
||||||
new Set();
|
new Set();
|
||||||
/** 光环转换器是否启用 */
|
/** 光环转换器是否启用 */
|
||||||
private readonly converterStatus: Map<
|
private readonly converterStatus: Map<
|
||||||
IAuraConverter<TAttr, THero>,
|
IAuraConverter<TEnemy, THero>,
|
||||||
boolean
|
boolean
|
||||||
> = new Map();
|
> = new Map();
|
||||||
/** 所有已被转换的光环 */
|
/** 所有已被转换的光环 */
|
||||||
private readonly convertedAura: Map<ISpecial<any>, IAuraView<TAttr>> =
|
private readonly convertedAura: Map<ISpecial<any>, IAuraView<TEnemy>> =
|
||||||
new Map();
|
new Map();
|
||||||
|
|
||||||
/** 普通查询效果注册,特殊属性 -> 此特殊属性的查询效果列表,按照优先级从高到低排序 */
|
/** 普通查询效果注册,特殊属性 -> 此特殊属性的查询效果列表,按照优先级从高到低排序 */
|
||||||
private readonly commonQueryMap: Map<
|
private readonly commonQueryMap: Map<
|
||||||
number,
|
number,
|
||||||
IEnemyCommonQueryEffect<TAttr, THero>[]
|
IEnemyCommonQueryEffect<TEnemy, THero>[]
|
||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
/** 特殊查询效果注册,特殊属性 -> 此特殊属性的特殊查询效果列表,按照优先级从高到低排序 */
|
/** 特殊查询效果注册,特殊属性 -> 此特殊属性的特殊查询效果列表,按照优先级从高到低排序 */
|
||||||
private readonly specialQueryEffects: Map<
|
private readonly specialQueryEffects: Map<
|
||||||
number,
|
number,
|
||||||
IEnemySpecialQueryEffect<TAttr, THero>[]
|
IEnemySpecialQueryEffect<TEnemy, THero>[]
|
||||||
> = new Map();
|
> = new Map();
|
||||||
|
|
||||||
/** 最终效果列表,按照优先级从高到低排列 */
|
/** 最终效果列表,按照优先级从高到低排列 */
|
||||||
private readonly finalEffects: IEnemyFinalEffect<TAttr, THero>[] = [];
|
private readonly finalEffects: IEnemyFinalEffect<TEnemy, THero>[] = [];
|
||||||
/** 添加的无来源全局光环列表 */
|
/** 添加的无来源全局光环列表 */
|
||||||
private readonly globalAuraList: Set<IAuraView<TAttr>> = new Set();
|
private readonly globalAuraList: Set<IAuraView<TEnemy>> = new Set();
|
||||||
/** 排序后的光环视图,视图优先级 -> 光环视图列表 */
|
/** 排序后的光环视图,视图优先级 -> 光环视图列表 */
|
||||||
private readonly sortedAura: Map<number, Set<IAuraView<TAttr>>> = new Map();
|
private readonly sortedAura: Map<number, Set<IAuraView<TEnemy>>> =
|
||||||
|
new Map();
|
||||||
|
|
||||||
/** 当怪物更新后,需要对上下文进行全量刷新的怪物列表 */
|
/** 当怪物更新后,需要对上下文进行全量刷新的怪物列表 */
|
||||||
private readonly needTotallyRefresh: Set<IEnemyView<TAttr>> = new Set();
|
private readonly needTotallyRefresh: Set<IEnemyView<TEnemy>> = new Set();
|
||||||
/** 所有实际查询了上下文的常规查询效果,这些怪物需要在上下文或其他怪物刷新时一并刷新 */
|
/** 所有实际查询了上下文的常规查询效果,这些怪物需要在上下文或其他怪物刷新时一并刷新 */
|
||||||
private readonly requestedCommonContext: Set<IEnemyView<TAttr>> = new Set();
|
private readonly requestedCommonContext: Set<IEnemyView<TEnemy>> =
|
||||||
|
new Set();
|
||||||
/** 所有需要被标记为脏的怪物 */
|
/** 所有需要被标记为脏的怪物 */
|
||||||
private readonly dirtyEnemy: Set<IEnemyView<TAttr>> = new Set();
|
private readonly dirtyEnemy: Set<IEnemyView<TEnemy>> = new Set();
|
||||||
|
|
||||||
/** 当前绑定的勇士属性对象 */
|
/** 当前绑定的勇士属性对象 */
|
||||||
private bindedHero: IReadonlyHeroAttribute<THero> | null = null;
|
private bindedHero: IReadonlyHeroAttribute<THero> | null = null;
|
||||||
/** 地图伤害对象 */
|
/** 地图伤害对象 */
|
||||||
private mapDamage: IMapDamage<TAttr, THero> | null = null;
|
private mapDamage: IMapDamage<TEnemy, THero> | null = null;
|
||||||
/** 伤害系统对象 */
|
/** 伤害系统对象 */
|
||||||
private damageSystem: IDamageSystem<TAttr, THero> | null = null;
|
private damageSystem: IDamageSystem<TEnemy, THero> | null = null;
|
||||||
|
|
||||||
/** 索引工具 */
|
/** 索引工具 */
|
||||||
readonly indexer: MapLocIndexer = new MapLocIndexer();
|
readonly indexer: ILocationIndexer = new MapLocIndexer();
|
||||||
|
|
||||||
/** 当前是否需要全量刷新 */
|
/** 当前是否需要全量刷新 */
|
||||||
private needUpdate: boolean = true;
|
private needUpdate: boolean = true;
|
||||||
@ -91,6 +101,8 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
width: number = 0;
|
width: number = 0;
|
||||||
height: number = 0;
|
height: number = 0;
|
||||||
|
|
||||||
|
constructor(readonly dataState: IStateBase<TEnemy, THero>) {}
|
||||||
|
|
||||||
resize(width: number, height: number): void {
|
resize(width: number, height: number): void {
|
||||||
this.clear();
|
this.clear();
|
||||||
this.width = width;
|
this.width = width;
|
||||||
@ -99,20 +111,20 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerAuraConverter(converter: IAuraConverter<TAttr, THero>): void {
|
registerAuraConverter(converter: IAuraConverter<TEnemy, THero>): void {
|
||||||
this.auraConverter.add(converter);
|
this.auraConverter.add(converter);
|
||||||
this.converterStatus.set(converter, true);
|
this.converterStatus.set(converter, true);
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterAuraConverter(converter: IAuraConverter<TAttr, THero>): void {
|
unregisterAuraConverter(converter: IAuraConverter<TEnemy, THero>): void {
|
||||||
this.auraConverter.delete(converter);
|
this.auraConverter.delete(converter);
|
||||||
this.converterStatus.delete(converter);
|
this.converterStatus.delete(converter);
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuraConverterEnabled(
|
setAuraConverterEnabled(
|
||||||
converter: IAuraConverter<TAttr, THero>,
|
converter: IAuraConverter<TEnemy, THero>,
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
): void {
|
): void {
|
||||||
if (!this.auraConverter.has(converter)) return;
|
if (!this.auraConverter.has(converter)) return;
|
||||||
@ -122,7 +134,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
|
|
||||||
registerCommonQueryEffect(
|
registerCommonQueryEffect(
|
||||||
code: number,
|
code: number,
|
||||||
effect: IEnemyCommonQueryEffect<TAttr, THero>
|
effect: IEnemyCommonQueryEffect<TEnemy, THero>
|
||||||
): void {
|
): void {
|
||||||
const array = this.commonQueryMap.getOrInsert(code, []);
|
const array = this.commonQueryMap.getOrInsert(code, []);
|
||||||
array.push(effect);
|
array.push(effect);
|
||||||
@ -132,7 +144,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
|
|
||||||
unregisterCommonQueryEffect(
|
unregisterCommonQueryEffect(
|
||||||
code: number,
|
code: number,
|
||||||
effect: IEnemyCommonQueryEffect<TAttr, THero>
|
effect: IEnemyCommonQueryEffect<TEnemy, THero>
|
||||||
): void {
|
): void {
|
||||||
const array = this.commonQueryMap.get(code);
|
const array = this.commonQueryMap.get(code);
|
||||||
if (!array) return;
|
if (!array) return;
|
||||||
@ -143,7 +155,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerSpecialQueryEffect(
|
registerSpecialQueryEffect(
|
||||||
effect: IEnemySpecialQueryEffect<TAttr, THero>
|
effect: IEnemySpecialQueryEffect<TEnemy, THero>
|
||||||
): void {
|
): void {
|
||||||
const list = this.specialQueryEffects.getOrInsert(effect.priority, []);
|
const list = this.specialQueryEffects.getOrInsert(effect.priority, []);
|
||||||
list.push(effect);
|
list.push(effect);
|
||||||
@ -151,7 +163,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unregisterSpecialQueryEffect(
|
unregisterSpecialQueryEffect(
|
||||||
effect: IEnemySpecialQueryEffect<TAttr, THero>
|
effect: IEnemySpecialQueryEffect<TEnemy, THero>
|
||||||
): void {
|
): void {
|
||||||
const list = this.specialQueryEffects.get(effect.priority);
|
const list = this.specialQueryEffects.get(effect.priority);
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@ -165,13 +177,13 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFinalEffect(effect: IEnemyFinalEffect<TAttr, THero>): void {
|
registerFinalEffect(effect: IEnemyFinalEffect<TEnemy, THero>): void {
|
||||||
this.finalEffects.push(effect);
|
this.finalEffects.push(effect);
|
||||||
this.finalEffects.sort((a, b) => b.priority - a.priority);
|
this.finalEffects.sort((a, b) => b.priority - a.priority);
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterFinalEffect(effect: IEnemyFinalEffect<TAttr, THero>): void {
|
unregisterFinalEffect(effect: IEnemyFinalEffect<TEnemy, THero>): void {
|
||||||
const index = this.finalEffects.indexOf(effect);
|
const index = this.finalEffects.indexOf(effect);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.finalEffects.splice(index, 1);
|
this.finalEffects.splice(index, 1);
|
||||||
@ -196,37 +208,39 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* @param locator 怪物位置
|
* @param locator 怪物位置
|
||||||
*/
|
*/
|
||||||
private createHandler(
|
private createHandler(
|
||||||
enemy: IEnemy<TAttr>,
|
enemy: IEnemy<TEnemy>,
|
||||||
locator: ITileLocator
|
locator: ITileLocator
|
||||||
): IEnemyHandler<TAttr, THero> {
|
): IEnemyHandler<TEnemy, THero> {
|
||||||
return { enemy, locator, hero: this.bindedHero! };
|
return { enemy, locator, hero: this.bindedHero!, data: this.dataState };
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnemyLocator(enemy: IEnemy<TAttr>): Readonly<ITileLocator> | null {
|
getEnemyLocator(enemy: IEnemy<TEnemy>): Readonly<ITileLocator> | null {
|
||||||
const index = this.locatorEnemyMap.get(enemy);
|
const index = this.locatorEnemyMap.get(enemy);
|
||||||
if (index === undefined) return null;
|
if (index === undefined) return null;
|
||||||
return this.indexer.indexToLocator(index);
|
return this.indexer.indexToLocator(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnemyLocatorByView(
|
getEnemyLocatorByView(
|
||||||
view: IEnemyView<TAttr>
|
view: IEnemyView<TEnemy>
|
||||||
): Readonly<ITileLocator> | null {
|
): Readonly<ITileLocator> | null {
|
||||||
const index = this.locatorViewMap.get(view);
|
const index = this.locatorViewMap.get(view);
|
||||||
if (index === undefined) return null;
|
if (index === undefined) return null;
|
||||||
return this.indexer.indexToLocator(index);
|
return this.indexer.indexToLocator(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnemyByLocator(locator: ITileLocator): IEnemyView<TAttr> | null {
|
getEnemyByLocator(locator: ITileLocator): IEnemyView<TEnemy> | null {
|
||||||
const index = this.indexer.locToIndex(locator.x, locator.y);
|
const index = this.indexer.locToIndex(locator.x, locator.y);
|
||||||
return this.enemyViewMap.get(index) ?? null;
|
return this.enemyViewMap.get(index) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEnemyByLoc(x: number, y: number): IEnemyView<TAttr> | null {
|
getEnemyByLoc(x: number, y: number): IEnemyView<TEnemy> | null {
|
||||||
const index = this.indexer.locToIndex(x, y);
|
const index = this.indexer.locToIndex(x, y);
|
||||||
return this.enemyViewMap.get(index) ?? null;
|
return this.enemyViewMap.get(index) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewByComputed(enemy: IReadonlyEnemy<TAttr>): IEnemyView<TAttr> | null {
|
getViewByComputed(
|
||||||
|
enemy: IReadonlyEnemy<TEnemy>
|
||||||
|
): IEnemyView<TEnemy> | null {
|
||||||
return this.computedToView.get(enemy) ?? null;
|
return this.computedToView.get(enemy) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,11 +272,11 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
this.locatorEnemyMap.delete(enemy);
|
this.locatorEnemyMap.delete(enemy);
|
||||||
}
|
}
|
||||||
|
|
||||||
setEnemyAt(locator: ITileLocator, enemy: IEnemy<TAttr>): void {
|
setEnemyAt(locator: ITileLocator, enemy: IEnemy<TEnemy>): void {
|
||||||
const index = this.indexer.locToIndex(locator.x, locator.y);
|
const index = this.indexer.locToIndex(locator.x, locator.y);
|
||||||
this.deleteEnemyAt(index);
|
this.deleteEnemyAt(index);
|
||||||
|
|
||||||
const view = new EnemyView<TAttr>(enemy, this);
|
const view = new EnemyView<TEnemy>(enemy, this);
|
||||||
this.enemyMap.set(index, enemy);
|
this.enemyMap.set(index, enemy);
|
||||||
this.enemyViewMap.set(index, view);
|
this.enemyViewMap.set(index, view);
|
||||||
this.locatorEnemyMap.set(enemy, index);
|
this.locatorEnemyMap.set(enemy, index);
|
||||||
@ -292,7 +306,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
private *internalScanRange<T>(
|
private *internalScanRange<T>(
|
||||||
range: IRange<T>,
|
range: IRange<T>,
|
||||||
param: T
|
param: T
|
||||||
): Iterable<[ITileLocator, EnemyView<TAttr>]> {
|
): Iterable<[ITileLocator, EnemyView<TEnemy>]> {
|
||||||
range.bindHost(this);
|
range.bindHost(this);
|
||||||
const keys = new Set(this.enemyViewMap.keys());
|
const keys = new Set(this.enemyViewMap.keys());
|
||||||
const matched = range.autoDetect(keys, param);
|
const matched = range.autoDetect(keys, param);
|
||||||
@ -309,46 +323,46 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
scanRange<T>(
|
scanRange<T>(
|
||||||
range: IRange<T>,
|
range: IRange<T>,
|
||||||
param: T
|
param: T
|
||||||
): Iterable<[ITileLocator, IEnemyView<TAttr>]> {
|
): Iterable<[ITileLocator, IEnemyView<TEnemy>]> {
|
||||||
return this.internalScanRange(range, param);
|
return this.internalScanRange(range, param);
|
||||||
}
|
}
|
||||||
|
|
||||||
*iterateEnemy(): Iterable<[ITileLocator, IEnemyView<TAttr>]> {
|
*iterateEnemy(): Iterable<[ITileLocator, IEnemyView<TEnemy>]> {
|
||||||
for (const [index, view] of this.enemyViewMap) {
|
for (const [index, view] of this.enemyViewMap) {
|
||||||
const locator = this.indexer.indexToLocator(index);
|
const locator = this.indexer.indexToLocator(index);
|
||||||
yield [locator, view];
|
yield [locator, view];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addAura(aura: IAuraView<TAttr>): void {
|
addAura(aura: IAuraView<TEnemy>): void {
|
||||||
this.globalAuraList.add(aura);
|
this.globalAuraList.add(aura);
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAura(aura: IAuraView<TAttr>): void {
|
deleteAura(aura: IAuraView<TEnemy>): void {
|
||||||
this.globalAuraList.delete(aura);
|
this.globalAuraList.delete(aura);
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachMapDamage(damage: IMapDamage<TAttr, THero> | null): void {
|
attachMapDamage(damage: IMapDamage<TEnemy, THero> | null): void {
|
||||||
this.mapDamage = damage;
|
this.mapDamage = damage;
|
||||||
if (damage) {
|
if (damage) {
|
||||||
damage.refreshAll();
|
damage.refreshAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMapDamage(): IMapDamage<TAttr, THero> | null {
|
getMapDamage(): IMapDamage<TEnemy, THero> | null {
|
||||||
return this.mapDamage;
|
return this.mapDamage;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachDamageSystem(system: IDamageSystem<TAttr, unknown> | null): void {
|
attachDamageSystem(system: IDamageSystem<TEnemy, THero> | null): void {
|
||||||
this.damageSystem = system;
|
this.damageSystem = system;
|
||||||
if (system) {
|
if (system) {
|
||||||
system.bindHeroStatus(this.bindedHero);
|
system.bindHeroStatus(this.bindedHero);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDamageSystem(): IDamageSystem<TAttr, THero> | null {
|
getDamageSystem(): IDamageSystem<TEnemy, THero> | null {
|
||||||
return this.damageSystem;
|
return this.damageSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,9 +374,9 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
*/
|
*/
|
||||||
private convertSpecial(
|
private convertSpecial(
|
||||||
special: ISpecial<any>,
|
special: ISpecial<any>,
|
||||||
handler: IReadonlyEnemyHandler<TAttr, THero>
|
handler: IReadonlyEnemyHandler<TEnemy, THero>
|
||||||
): IEnemyAuraView<TAttr, any, any> | null {
|
): IEnemyAuraView<TEnemy, any, any> | null {
|
||||||
let matched: IAuraConverter<TAttr, THero> | null = null;
|
let matched: IAuraConverter<TEnemy, THero> | null = null;
|
||||||
for (const converter of this.auraConverter) {
|
for (const converter of this.auraConverter) {
|
||||||
if (!this.converterStatus.get(converter)) continue;
|
if (!this.converterStatus.get(converter)) continue;
|
||||||
if (converter.shouldConvert(special, handler)) {
|
if (converter.shouldConvert(special, handler)) {
|
||||||
@ -382,7 +396,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* 将光环按优先级插入到有序表中
|
* 将光环按优先级插入到有序表中
|
||||||
* @param aura 光环视图
|
* @param aura 光环视图
|
||||||
*/
|
*/
|
||||||
private insertIntoSortedAura(aura: IAuraView<TAttr>): void {
|
private insertIntoSortedAura(aura: IAuraView<TEnemy>): void {
|
||||||
const set = this.sortedAura.getOrInsertComputed(
|
const set = this.sortedAura.getOrInsertComputed(
|
||||||
aura.priority,
|
aura.priority,
|
||||||
() => new Set()
|
() => new Set()
|
||||||
@ -394,7 +408,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* 从优先级表中移除一个光环
|
* 从优先级表中移除一个光环
|
||||||
* @param aura 光环视图
|
* @param aura 光环视图
|
||||||
*/
|
*/
|
||||||
private removeFromSortedAura(aura: IAuraView<TAttr>): void {
|
private removeFromSortedAura(aura: IAuraView<TEnemy>): void {
|
||||||
const set = this.sortedAura.get(aura.priority);
|
const set = this.sortedAura.get(aura.priority);
|
||||||
if (set) {
|
if (set) {
|
||||||
set.delete(aura);
|
set.delete(aura);
|
||||||
@ -412,12 +426,12 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* @param currentPriority 当前处理的优先级
|
* @param currentPriority 当前处理的优先级
|
||||||
*/
|
*/
|
||||||
private processSpecialModifier(
|
private processSpecialModifier(
|
||||||
modifier: IEnemySpecialModifier<TAttr>,
|
modifier: IEnemySpecialModifier<TEnemy>,
|
||||||
handler: IEnemyHandler<TAttr, THero>,
|
handler: IEnemyHandler<TEnemy, THero>,
|
||||||
currentPriority: number
|
currentPriority: number
|
||||||
): Set<IAuraView<TAttr>> {
|
): Set<IAuraView<TEnemy>> {
|
||||||
const enemy = handler.enemy;
|
const enemy = handler.enemy;
|
||||||
const affectedAuras = new Set<IAuraView<TAttr>>();
|
const affectedAuras = new Set<IAuraView<TEnemy>>();
|
||||||
const toAdd = modifier.add(handler);
|
const toAdd = modifier.add(handler);
|
||||||
const toDelete = modifier.delete(handler);
|
const toDelete = modifier.delete(handler);
|
||||||
|
|
||||||
@ -489,7 +503,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* @param currentPriority 当前处理的优先级
|
* @param currentPriority 当前处理的优先级
|
||||||
*/
|
*/
|
||||||
private processSpecialQuery(
|
private processSpecialQuery(
|
||||||
effect: IEnemySpecialQueryEffect<TAttr, THero>,
|
effect: IEnemySpecialQueryEffect<TEnemy, THero>,
|
||||||
currentPriority: number
|
currentPriority: number
|
||||||
): void {
|
): void {
|
||||||
const modifier = effect.for(this);
|
const modifier = effect.for(this);
|
||||||
@ -521,7 +535,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* @param currentPriority 当前处理的优先级
|
* @param currentPriority 当前处理的优先级
|
||||||
*/
|
*/
|
||||||
private processAuraSpecial(
|
private processAuraSpecial(
|
||||||
aura: IAuraView<TAttr>,
|
aura: IAuraView<TEnemy>,
|
||||||
currentPriority: number
|
currentPriority: number
|
||||||
): void {
|
): void {
|
||||||
const param = aura.getRangeParam();
|
const param = aura.getRangeParam();
|
||||||
@ -696,7 +710,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
markDirty(view: IEnemyView<TAttr>): void {
|
markDirty(view: IEnemyView<TEnemy>): void {
|
||||||
if (!this.locatorViewMap.has(view)) return;
|
if (!this.locatorViewMap.has(view)) return;
|
||||||
this.dirtyEnemy.add(view);
|
this.dirtyEnemy.add(view);
|
||||||
if (this.damageSystem) {
|
if (this.damageSystem) {
|
||||||
@ -711,8 +725,8 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* @param locator 怪物位置
|
* @param locator 怪物位置
|
||||||
*/
|
*/
|
||||||
private refreshSpecialModifier(
|
private refreshSpecialModifier(
|
||||||
modifier: IEnemySpecialModifier<TAttr>,
|
modifier: IEnemySpecialModifier<TEnemy>,
|
||||||
handler: IEnemyHandler<TAttr, THero>
|
handler: IEnemyHandler<TEnemy, THero>
|
||||||
): void {
|
): void {
|
||||||
const enemy = handler.enemy;
|
const enemy = handler.enemy;
|
||||||
const toAdd = modifier.add(handler);
|
const toAdd = modifier.add(handler);
|
||||||
@ -758,7 +772,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
* 刷新单个怪物视图的计算结果
|
* 刷新单个怪物视图的计算结果
|
||||||
* @param view 怪物视图
|
* @param view 怪物视图
|
||||||
*/
|
*/
|
||||||
private refreshEnemy(view: EnemyView<TAttr>): void {
|
private refreshEnemy(view: EnemyView<TEnemy>): void {
|
||||||
const locator = this.getEnemyLocatorByView(view);
|
const locator = this.getEnemyLocatorByView(view);
|
||||||
if (!locator) return;
|
if (!locator) return;
|
||||||
|
|
||||||
@ -853,7 +867,7 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestRefresh(view: IEnemyView<TAttr>): void {
|
requestRefresh(view: IEnemyView<TEnemy>): void {
|
||||||
if (!this.dirtyEnemy.has(view)) return;
|
if (!this.dirtyEnemy.has(view)) return;
|
||||||
if (this.needTotallyRefresh.has(view)) {
|
if (this.needTotallyRefresh.has(view)) {
|
||||||
this.needUpdate = true;
|
this.needUpdate = true;
|
||||||
@ -863,11 +877,11 @@ export class EnemyContext<TAttr, THero> implements IEnemyContext<TAttr, THero> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refreshEnemy(view as EnemyView<TAttr>);
|
this.refreshEnemy(view as EnemyView<TEnemy>);
|
||||||
|
|
||||||
for (const requestedView of this.requestedCommonContext) {
|
for (const requestedView of this.requestedCommonContext) {
|
||||||
if (requestedView === view) continue;
|
if (requestedView === view) continue;
|
||||||
this.refreshEnemy(requestedView as EnemyView<TAttr>);
|
this.refreshEnemy(requestedView as EnemyView<TEnemy>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { clamp } from 'lodash-es';
|
||||||
import { ITileLocator, logger } from '@motajs/common';
|
import { ITileLocator, logger } from '@motajs/common';
|
||||||
import {
|
import {
|
||||||
CriticalableHeroStatus,
|
CriticalableHeroStatus,
|
||||||
@ -8,11 +9,14 @@ import {
|
|||||||
IEnemyCritical,
|
IEnemyCritical,
|
||||||
IEnemyDamageInfo,
|
IEnemyDamageInfo,
|
||||||
IReadonlyEnemyHandler,
|
IReadonlyEnemyHandler,
|
||||||
IEnemyView,
|
IEnemyView
|
||||||
IReadonlyEnemy
|
|
||||||
} from './types';
|
} from './types';
|
||||||
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
|
import {
|
||||||
import { clamp } from 'lodash-es';
|
IHeroAttribute,
|
||||||
|
IReadonlyHeroAttribute,
|
||||||
|
IReadonlyEnemy,
|
||||||
|
IStateBase
|
||||||
|
} from '@user/data-base';
|
||||||
|
|
||||||
interface ICriticalSearchResult {
|
interface ICriticalSearchResult {
|
||||||
/** 此临界点的属性值 */
|
/** 此临界点的属性值 */
|
||||||
@ -21,20 +25,23 @@ interface ICriticalSearchResult {
|
|||||||
readonly info: IEnemyDamageInfo;
|
readonly info: IEnemyDamageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DamageContext<TAttr, THero> implements IDamageContext<
|
export class DamageContext<TEnemy, THero> implements IDamageContext<
|
||||||
TAttr,
|
TEnemy,
|
||||||
THero
|
THero
|
||||||
> {
|
> {
|
||||||
/** 当前正在使用的计算器 */
|
/** 当前正在使用的计算器 */
|
||||||
protected calculator: IDamageCalculator<TAttr, THero> | null;
|
protected calculator: IDamageCalculator<TEnemy, THero> | null;
|
||||||
/** 当前勇士属性 */
|
/** 当前勇士属性 */
|
||||||
protected heroStatus: IReadonlyHeroAttribute<THero> | null;
|
protected heroStatus: IReadonlyHeroAttribute<THero> | null;
|
||||||
|
|
||||||
|
readonly dataState: IStateBase<TEnemy, THero>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly context: IEnemyContext<TAttr, THero>,
|
readonly context: IEnemyContext<TEnemy, THero>,
|
||||||
calculator: IDamageCalculator<TAttr, THero> | null = null,
|
calculator: IDamageCalculator<TEnemy, THero> | null = null,
|
||||||
heroStatus: IReadonlyHeroAttribute<THero> | null = null
|
heroStatus: IReadonlyHeroAttribute<THero> | null = null
|
||||||
) {
|
) {
|
||||||
|
this.dataState = context.dataState;
|
||||||
this.calculator = calculator;
|
this.calculator = calculator;
|
||||||
this.heroStatus = heroStatus;
|
this.heroStatus = heroStatus;
|
||||||
}
|
}
|
||||||
@ -46,14 +53,14 @@ export class DamageContext<TAttr, THero> implements IDamageContext<
|
|||||||
* @param hero 勇士属性对象
|
* @param hero 勇士属性对象
|
||||||
*/
|
*/
|
||||||
private createReadonlyHandler(
|
private createReadonlyHandler(
|
||||||
enemy: IReadonlyEnemy<TAttr>,
|
enemy: IReadonlyEnemy<TEnemy>,
|
||||||
locator: ITileLocator,
|
locator: ITileLocator,
|
||||||
hero: IReadonlyHeroAttribute<THero>
|
hero: IReadonlyHeroAttribute<THero>
|
||||||
): IReadonlyEnemyHandler<TAttr, THero> {
|
): IReadonlyEnemyHandler<TEnemy, THero> {
|
||||||
return { enemy, locator, hero };
|
return { enemy, locator, hero, data: this.dataState };
|
||||||
}
|
}
|
||||||
|
|
||||||
getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null {
|
getDamageInfo(enemy: IEnemyView<TEnemy>): IEnemyDamageInfo | null {
|
||||||
if (!this.heroStatus) {
|
if (!this.heroStatus) {
|
||||||
logger.warn(107);
|
logger.warn(107);
|
||||||
return null;
|
return null;
|
||||||
@ -73,7 +80,7 @@ export class DamageContext<TAttr, THero> implements IDamageContext<
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDamageInfoByComputed(
|
getDamageInfoByComputed(
|
||||||
enemy: IReadonlyEnemy<TAttr>
|
enemy: IReadonlyEnemy<TEnemy>
|
||||||
): IEnemyDamageInfo | null {
|
): IEnemyDamageInfo | null {
|
||||||
if (!this.heroStatus) {
|
if (!this.heroStatus) {
|
||||||
logger.warn(107);
|
logger.warn(107);
|
||||||
@ -96,7 +103,7 @@ export class DamageContext<TAttr, THero> implements IDamageContext<
|
|||||||
}
|
}
|
||||||
|
|
||||||
*calculateCritical(
|
*calculateCritical(
|
||||||
view: IEnemyView<TAttr>,
|
view: IEnemyView<TEnemy>,
|
||||||
attribute: CriticalableHeroStatus<THero>,
|
attribute: CriticalableHeroStatus<THero>,
|
||||||
precision: number = 12
|
precision: number = 12
|
||||||
): Generator<IEnemyCritical, void, void> {
|
): Generator<IEnemyCritical, void, void> {
|
||||||
@ -168,7 +175,7 @@ export class DamageContext<TAttr, THero> implements IDamageContext<
|
|||||||
* @param maxIterations 最大迭代数量
|
* @param maxIterations 最大迭代数量
|
||||||
*/
|
*/
|
||||||
private findNextCritical(
|
private findNextCritical(
|
||||||
handler: IReadonlyEnemyHandler<TAttr, THero>,
|
handler: IReadonlyEnemyHandler<TEnemy, THero>,
|
||||||
hero: IHeroAttribute<THero>,
|
hero: IHeroAttribute<THero>,
|
||||||
attribute: CriticalableHeroStatus<THero>,
|
attribute: CriticalableHeroStatus<THero>,
|
||||||
currentValue: number,
|
currentValue: number,
|
||||||
@ -206,24 +213,24 @@ export class DamageContext<TAttr, THero> implements IDamageContext<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DamageSystem<TAttr, THero>
|
export class DamageSystem<TEnemy, THero>
|
||||||
extends DamageContext<TAttr, THero>
|
extends DamageContext<TEnemy, THero>
|
||||||
implements IDamageSystem<TAttr, THero>
|
implements IDamageSystem<TEnemy, THero>
|
||||||
{
|
{
|
||||||
/** 怪物伤害缓存 */
|
/** 怪物伤害缓存 */
|
||||||
private readonly cache: Map<IEnemyView<TAttr>, IEnemyDamageInfo> =
|
private readonly cache: Map<IEnemyView<TEnemy>, IEnemyDamageInfo> =
|
||||||
new Map();
|
new Map();
|
||||||
|
|
||||||
constructor(context: IEnemyContext<TAttr, THero>) {
|
constructor(context: IEnemyContext<TEnemy, THero>) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
useCalculator(calculator: IDamageCalculator<TAttr, THero>): void {
|
useCalculator(calculator: IDamageCalculator<TEnemy, THero>): void {
|
||||||
this.calculator = calculator;
|
this.calculator = calculator;
|
||||||
this.markAllDirty();
|
this.markAllDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCalculator(): IDamageCalculator<TAttr, THero> | null {
|
getCalculator(): IDamageCalculator<TEnemy, THero> | null {
|
||||||
return this.calculator;
|
return this.calculator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +239,7 @@ export class DamageSystem<TAttr, THero>
|
|||||||
this.markAllDirty();
|
this.markAllDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null {
|
getDamageInfo(enemy: IEnemyView<TEnemy>): IEnemyDamageInfo | null {
|
||||||
const cached = this.cache.get(enemy);
|
const cached = this.cache.get(enemy);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
@ -246,7 +253,7 @@ export class DamageSystem<TAttr, THero>
|
|||||||
}
|
}
|
||||||
|
|
||||||
getDamageInfoByComputed(
|
getDamageInfoByComputed(
|
||||||
enemy: IReadonlyEnemy<TAttr>
|
enemy: IReadonlyEnemy<TEnemy>
|
||||||
): IEnemyDamageInfo | null {
|
): IEnemyDamageInfo | null {
|
||||||
const view = this.context.getViewByComputed(enemy);
|
const view = this.context.getViewByComputed(enemy);
|
||||||
if (view) {
|
if (view) {
|
||||||
@ -264,11 +271,11 @@ export class DamageSystem<TAttr, THero>
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
markDirty(enemy: IEnemyView<TAttr>): void {
|
markDirty(enemy: IEnemyView<TEnemy>): void {
|
||||||
this.cache.delete(enemy);
|
this.cache.delete(enemy);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteEnemy(enemy: IEnemyView<TAttr>): void {
|
deleteEnemy(enemy: IEnemyView<TEnemy>): void {
|
||||||
this.cache.delete(enemy);
|
this.cache.delete(enemy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +283,7 @@ export class DamageSystem<TAttr, THero>
|
|||||||
this.cache.clear();
|
this.cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
with(hero: IHeroAttribute<THero>): IDamageContext<TAttr, THero> {
|
with(hero: IHeroAttribute<THero>): IDamageContext<TEnemy, THero> {
|
||||||
return new DamageContext(this.context, this.calculator, hero);
|
return new DamageContext(this.context, this.calculator, hero);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
42
packages-user/data-system/src/combat/enemy.ts
Normal file
42
packages-user/data-system/src/combat/enemy.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { IEnemy, IReadonlyEnemy } from '@user/data-base';
|
||||||
|
import { IEnemyView, IEnemyContext } from './types';
|
||||||
|
|
||||||
|
export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
||||||
|
/** 计算后怪物 */
|
||||||
|
private readonly computedEnemy: IEnemy<TAttr>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly baseEnemy: IEnemy<TAttr>,
|
||||||
|
readonly context: IEnemyContext<TAttr, unknown>
|
||||||
|
) {
|
||||||
|
this.computedEnemy = baseEnemy.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.computedEnemy.copyFrom(this.baseEnemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseEnemy(): IReadonlyEnemy<TAttr> {
|
||||||
|
return this.baseEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
getComputedEnemy(): IReadonlyEnemy<TAttr> {
|
||||||
|
this.context.requestRefresh(this);
|
||||||
|
return this.computedEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计算中怪物对象,这个接口不对外暴露,仅在系统内部的 EnemyContext 中使用。
|
||||||
|
*/
|
||||||
|
getComputingEnemy(): IEnemy<TAttr> {
|
||||||
|
return this.computedEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
getModifiableEnemy(): IEnemy<TAttr> {
|
||||||
|
return this.baseEnemy;
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(): void {
|
||||||
|
this.context.markDirty(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages-user/data-system/src/combat/index.ts
Normal file
5
packages-user/data-system/src/combat/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './context';
|
||||||
|
export * from './damage';
|
||||||
|
export * from './enemy';
|
||||||
|
export * from './mapDamage';
|
||||||
|
export * from './types';
|
||||||
@ -7,9 +7,9 @@ import {
|
|||||||
IMapDamageConverter,
|
IMapDamageConverter,
|
||||||
IMapDamageInfo,
|
IMapDamageInfo,
|
||||||
IMapDamageReducer,
|
IMapDamageReducer,
|
||||||
IMapDamageView,
|
IMapDamageView
|
||||||
IMapLocIndexer
|
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { ILocationHelper, IStateBase } from '@user/data-base';
|
||||||
|
|
||||||
interface IPointInfo {
|
interface IPointInfo {
|
||||||
/** 该点所有的地图伤害 */
|
/** 该点所有的地图伤害 */
|
||||||
@ -34,9 +34,9 @@ interface IDamageStore<TAttr> {
|
|||||||
readonly index: number;
|
readonly index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
export class MapDamage<TEnemy, THero> implements IMapDamage<TEnemy, THero> {
|
||||||
/** 当前使用的地图伤害转换器 */
|
/** 当前使用的地图伤害转换器 */
|
||||||
private converter: IMapDamageConverter<TAttr, THero> | null = null;
|
private converter: IMapDamageConverter<TEnemy, THero> | null = null;
|
||||||
/** 当前使用的地图伤害合并器 */
|
/** 当前使用的地图伤害合并器 */
|
||||||
private reducer: IMapDamageReducer | null = null;
|
private reducer: IMapDamageReducer | null = null;
|
||||||
|
|
||||||
@ -45,14 +45,14 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
/** 有来源地图伤害,坐标 -> 点伤害信息 */
|
/** 有来源地图伤害,坐标 -> 点伤害信息 */
|
||||||
private readonly sourcedDamage: Map<number, IPointInfo> = new Map();
|
private readonly sourcedDamage: Map<number, IPointInfo> = new Map();
|
||||||
/** 地图伤害视图 -> 其信息对象 */
|
/** 地图伤害视图 -> 其信息对象 */
|
||||||
private readonly viewStore: Map<IMapDamageView<any>, IViewStore<TAttr>> =
|
private readonly viewStore: Map<IMapDamageView<any>, IViewStore<TEnemy>> =
|
||||||
new Map();
|
new Map();
|
||||||
/** 地图伤害信息 -> 其信息对象 */
|
/** 地图伤害信息 -> 其信息对象 */
|
||||||
private readonly damageStore: Map<IMapDamageInfo, IDamageStore<TAttr>> =
|
private readonly damageStore: Map<IMapDamageInfo, IDamageStore<TEnemy>> =
|
||||||
new Map();
|
new Map();
|
||||||
/** 怪物视图 -> 其影响对象 */
|
/** 怪物视图 -> 其影响对象 */
|
||||||
private readonly enemyStore: Map<
|
private readonly enemyStore: Map<
|
||||||
IEnemyView<TAttr>,
|
IEnemyView<TEnemy>,
|
||||||
Set<IMapDamageView<any>>
|
Set<IMapDamageView<any>>
|
||||||
> = new Map();
|
> = new Map();
|
||||||
/** 需要延迟刷新的坐标索引 */
|
/** 需要延迟刷新的坐标索引 */
|
||||||
@ -61,13 +61,16 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
private readonly reducedCache: Map<number, IMapDamageInfo> = new Map();
|
private readonly reducedCache: Map<number, IMapDamageInfo> = new Map();
|
||||||
|
|
||||||
/** 坐标索引对象 */
|
/** 坐标索引对象 */
|
||||||
private readonly indexer: IMapLocIndexer;
|
private readonly indexer: ILocationHelper;
|
||||||
|
|
||||||
constructor(readonly context: IEnemyContext<TAttr, THero>) {
|
readonly dataState: IStateBase<TEnemy, THero>;
|
||||||
|
|
||||||
|
constructor(readonly context: IEnemyContext<TEnemy, THero>) {
|
||||||
this.indexer = context.indexer;
|
this.indexer = context.indexer;
|
||||||
|
this.dataState = context.dataState;
|
||||||
}
|
}
|
||||||
|
|
||||||
useConverter(converter: IMapDamageConverter<TAttr, THero>): void {
|
useConverter(converter: IMapDamageConverter<TEnemy, THero>): void {
|
||||||
this.converter = converter;
|
this.converter = converter;
|
||||||
this.refreshAll();
|
this.refreshAll();
|
||||||
}
|
}
|
||||||
@ -78,15 +81,16 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
* @param locator 怪物位置
|
* @param locator 怪物位置
|
||||||
*/
|
*/
|
||||||
private createReadonlyHandler(
|
private createReadonlyHandler(
|
||||||
view: IEnemyView<TAttr>,
|
view: IEnemyView<TEnemy>,
|
||||||
locator: ITileLocator
|
locator: ITileLocator
|
||||||
): IReadonlyEnemyHandler<TAttr, THero> | null {
|
): IReadonlyEnemyHandler<TEnemy, THero> | null {
|
||||||
const hero = this.context.getBindedHero();
|
const hero = this.context.getBindedHero();
|
||||||
if (!hero) return null;
|
if (!hero) return null;
|
||||||
return {
|
return {
|
||||||
enemy: view.getComputedEnemy(),
|
enemy: view.getComputedEnemy(),
|
||||||
locator,
|
locator,
|
||||||
hero
|
hero,
|
||||||
|
data: this.context.dataState
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +130,7 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
this.markDirtyIndex(this.indexer.locaterToIndex(locator));
|
this.markDirtyIndex(this.indexer.locaterToIndex(locator));
|
||||||
}
|
}
|
||||||
|
|
||||||
markEnemyDirty(view: IEnemyView<TAttr>): void {
|
markEnemyDirty(view: IEnemyView<TEnemy>): void {
|
||||||
const store = this.enemyStore.get(view);
|
const store = this.enemyStore.get(view);
|
||||||
const locator = this.context.getEnemyLocatorByView(view);
|
const locator = this.context.getEnemyLocatorByView(view);
|
||||||
if (!store) {
|
if (!store) {
|
||||||
@ -141,7 +145,7 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
this.refreshEnemyAndClearCache(view, locator);
|
this.refreshEnemyAndClearCache(view, locator);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteEnemy(view: IEnemyView<TAttr>): void {
|
deleteEnemy(view: IEnemyView<TEnemy>): void {
|
||||||
const store = this.enemyStore.get(view);
|
const store = this.enemyStore.get(view);
|
||||||
if (!store) return;
|
if (!store) return;
|
||||||
const collection = new Set<number>();
|
const collection = new Set<number>();
|
||||||
@ -231,7 +235,7 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
* 移除指定怪物所产生的地图伤害
|
* 移除指定怪物所产生的地图伤害
|
||||||
* @param view 怪物视图
|
* @param view 怪物视图
|
||||||
*/
|
*/
|
||||||
private removeEnemyAffecting(view: IEnemyView<TAttr>) {
|
private removeEnemyAffecting(view: IEnemyView<TEnemy>) {
|
||||||
const views = this.enemyStore.get(view);
|
const views = this.enemyStore.get(view);
|
||||||
if (!views) return;
|
if (!views) return;
|
||||||
views.forEach(viewItem => {
|
views.forEach(viewItem => {
|
||||||
@ -253,7 +257,7 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
* 刷新指定位置的怪物地图伤害,并执行刷新缓存的操作
|
* 刷新指定位置的怪物地图伤害,并执行刷新缓存的操作
|
||||||
*/
|
*/
|
||||||
private refreshEnemyAndClearCache(
|
private refreshEnemyAndClearCache(
|
||||||
view: IEnemyView<TAttr>,
|
view: IEnemyView<TEnemy>,
|
||||||
locator: ITileLocator
|
locator: ITileLocator
|
||||||
) {
|
) {
|
||||||
this.removeEnemyAffecting(view);
|
this.removeEnemyAffecting(view);
|
||||||
@ -295,7 +299,7 @@ export class MapDamage<TAttr, THero> implements IMapDamage<TAttr, THero> {
|
|||||||
/**
|
/**
|
||||||
* 刷新指定位置的怪物地图伤害
|
* 刷新指定位置的怪物地图伤害
|
||||||
*/
|
*/
|
||||||
private refreshEnemy(view: IEnemyView<TAttr>, locator: ITileLocator) {
|
private refreshEnemy(view: IEnemyView<TEnemy>, locator: ITileLocator) {
|
||||||
this.removeEnemyAffecting(view);
|
this.removeEnemyAffecting(view);
|
||||||
if (!this.converter) return;
|
if (!this.converter) return;
|
||||||
const handler = this.createReadonlyHandler(view, locator);
|
const handler = this.createReadonlyHandler(view, locator);
|
||||||
708
packages-user/data-system/src/combat/types.ts
Normal file
708
packages-user/data-system/src/combat/types.ts
Normal file
@ -0,0 +1,708 @@
|
|||||||
|
import { ITileLocator, IRange } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
IEnemy,
|
||||||
|
IReadonlyEnemy,
|
||||||
|
ISpecial,
|
||||||
|
IReadonlyHeroAttribute,
|
||||||
|
IHeroAttribute,
|
||||||
|
IStateBase,
|
||||||
|
ILocationHelper
|
||||||
|
} from '@user/data-base';
|
||||||
|
|
||||||
|
//#region 辅助接口
|
||||||
|
|
||||||
|
export interface IEnemyHandler<TEnemy, THero> {
|
||||||
|
/** 怪物属性信息 */
|
||||||
|
readonly enemy: IEnemy<TEnemy>;
|
||||||
|
/** 怪物定位符 */
|
||||||
|
readonly locator: ITileLocator;
|
||||||
|
/** 勇士属性信息 */
|
||||||
|
readonly hero: IReadonlyHeroAttribute<THero>;
|
||||||
|
/** 当前全局状态对象 */
|
||||||
|
readonly data: IStateBase<TEnemy, THero>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReadonlyEnemyHandler<TEnemy, THero> {
|
||||||
|
/** 怪物属性信息 */
|
||||||
|
readonly enemy: IReadonlyEnemy<TEnemy>;
|
||||||
|
/** 怪物定位符 */
|
||||||
|
readonly locator: ITileLocator;
|
||||||
|
/** 勇士属性信息 */
|
||||||
|
readonly hero: IReadonlyHeroAttribute<THero>;
|
||||||
|
/** 当前全局状态对象 */
|
||||||
|
readonly data: IStateBase<TEnemy, THero>;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 怪物对象
|
||||||
|
|
||||||
|
export interface IEnemyView<TEnemy> {
|
||||||
|
/** 怪物视图所属的上下文 */
|
||||||
|
readonly context: IEnemyContext<TEnemy, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置此怪物视图的状态,将计算后怪物对象恢复至初始状态
|
||||||
|
*/
|
||||||
|
reset(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基本怪物对象
|
||||||
|
*/
|
||||||
|
getBaseEnemy(): IReadonlyEnemy<TEnemy>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计算后的怪物对象,返回的怪物对象同引用
|
||||||
|
*/
|
||||||
|
getComputedEnemy(): IReadonlyEnemy<TEnemy>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可修改的怪物对象。如果修改此方法获取的怪物对象,那么怪物的真实信息是不会刷新的,
|
||||||
|
* 需要手动调用 markDirty 方法来刷新。
|
||||||
|
*/
|
||||||
|
getModifiableEnemy(): IEnemy<TEnemy>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将此怪物标记为脏,需要更新
|
||||||
|
*/
|
||||||
|
markDirty(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 光环与查询
|
||||||
|
|
||||||
|
export interface IEnemySpecialModifier<TEnemy> {
|
||||||
|
/**
|
||||||
|
* 获取要添加到指定怪物身上的特殊属性
|
||||||
|
* @param handler 信息对象
|
||||||
|
*/
|
||||||
|
add(handler: IReadonlyEnemyHandler<TEnemy, unknown>): ISpecial<any>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取制定怪物身上要删除的特殊属性
|
||||||
|
* @param handler 信息对象
|
||||||
|
*/
|
||||||
|
delete(handler: IReadonlyEnemyHandler<TEnemy, unknown>): ISpecial<any>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改一个怪物的特殊属性,如果真正进行了修改则返回 true,否则返回 false
|
||||||
|
* @param handler 信息对象
|
||||||
|
* @param special 要修改的怪物特殊属性
|
||||||
|
*/
|
||||||
|
modify(
|
||||||
|
handler: IEnemyHandler<TEnemy, unknown>,
|
||||||
|
special: ISpecial<any>
|
||||||
|
): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuraView<TEnemy, TRange = any> {
|
||||||
|
/** 此光环视图的优先级 */
|
||||||
|
readonly priority: number;
|
||||||
|
/** 此光环视图的影响范围 */
|
||||||
|
readonly range: IRange<TRange>;
|
||||||
|
|
||||||
|
/** 这个光环视图是否有可能修改怪物的基本属性 */
|
||||||
|
readonly couldApplyBase: boolean;
|
||||||
|
/** 这个光环视图是否有可能修改怪物的特殊属性 */
|
||||||
|
readonly couldApplySpecial: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取范围扫描参数
|
||||||
|
*/
|
||||||
|
getRangeParam(): TRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对指定怪物对象施加修饰器
|
||||||
|
* @param handler 信息对象
|
||||||
|
* @param baseEnemy 原始怪物对象,即未进行任何修改的怪物对象
|
||||||
|
*/
|
||||||
|
apply(
|
||||||
|
handler: IEnemyHandler<TEnemy, unknown>,
|
||||||
|
baseEnemy: IReadonlyEnemy<TEnemy>
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对指定怪物对象添加特殊属性修饰器
|
||||||
|
* @param handler 信息对象
|
||||||
|
* @param baseEnemy 原始怪物对象,即未进行任何修改的怪物对象
|
||||||
|
*/
|
||||||
|
applySpecial(
|
||||||
|
handler: IEnemyHandler<TEnemy, unknown>,
|
||||||
|
baseEnemy: IReadonlyEnemy<TEnemy>
|
||||||
|
): IEnemySpecialModifier<TEnemy> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemyAuraView<TEnemy, TRange, TSpecial> extends IAuraView<
|
||||||
|
TEnemy,
|
||||||
|
TRange
|
||||||
|
> {
|
||||||
|
/** 此光环视图所属的怪物 */
|
||||||
|
readonly enemy: IReadonlyEnemy<TEnemy>;
|
||||||
|
/** 此光环视图所属的特殊属性 */
|
||||||
|
readonly special: ISpecial<TSpecial>;
|
||||||
|
/** 此光环视图所属怪物的定位符 */
|
||||||
|
readonly locator: ITileLocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuraConverter<TEnemy, THero> {
|
||||||
|
/**
|
||||||
|
* 判断一个特殊属性是否应该被当前光环转换器执行转换
|
||||||
|
* @param special 要转换的特殊属性
|
||||||
|
* @param handler 信息对象
|
||||||
|
*/
|
||||||
|
shouldConvert(
|
||||||
|
special: ISpecial<any>,
|
||||||
|
handler: IReadonlyEnemyHandler<TEnemy, THero>
|
||||||
|
): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将一个特殊属性转换为光环视图
|
||||||
|
*/
|
||||||
|
convert(
|
||||||
|
special: ISpecial<any>,
|
||||||
|
handler: IReadonlyEnemyHandler<TEnemy, THero>,
|
||||||
|
context: IEnemyContext<TEnemy, THero>
|
||||||
|
): IEnemyAuraView<TEnemy, any, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemySpecialQueryModifier<
|
||||||
|
TEnemy,
|
||||||
|
THero
|
||||||
|
> extends IEnemySpecialModifier<TEnemy> {
|
||||||
|
/**
|
||||||
|
* 判断一个怪物是否应该查询外部状态
|
||||||
|
*/
|
||||||
|
shouldQuery(handler: IReadonlyEnemyHandler<TEnemy, THero>): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemySpecialQueryEffect<TEnemy, THero> {
|
||||||
|
/** 效果优先级,与光环属性共用 */
|
||||||
|
readonly priority: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据传入的怪物上下文,获取对应的怪物特殊属性修饰器
|
||||||
|
*/
|
||||||
|
for(
|
||||||
|
ctx: IEnemyContext<TEnemy, THero>
|
||||||
|
): IEnemySpecialQueryModifier<TEnemy, THero>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemyCommonQueryEffect<TEnemy, THero> {
|
||||||
|
/** 优先级,越高的越先执行 */
|
||||||
|
readonly priority: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对怪物的某个特殊属性施加常规查询效果
|
||||||
|
*/
|
||||||
|
apply(
|
||||||
|
handler: IEnemyHandler<TEnemy, THero>,
|
||||||
|
special: ISpecial<any>,
|
||||||
|
query: () => IEnemyContext<TEnemy, THero>
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemyFinalEffect<TEnemy, THero> {
|
||||||
|
/** 效果优先级,越高会越先被执行 */
|
||||||
|
readonly priority: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向怪物施加最终修饰效果
|
||||||
|
*/
|
||||||
|
apply(handler: IEnemyHandler<TEnemy, THero>): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 地图伤害
|
||||||
|
|
||||||
|
export interface IMapDamageInfoExtra {
|
||||||
|
/** 捕捉怪物信息 */
|
||||||
|
catch: Set<ITileLocator>;
|
||||||
|
/** 阻击怪物信息 */
|
||||||
|
repulse: Set<ITileLocator>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapDamageInfo {
|
||||||
|
/** 伤害值 */
|
||||||
|
damage: number;
|
||||||
|
/** 伤害类型 */
|
||||||
|
type: number;
|
||||||
|
/** 地图伤害额外信息 */
|
||||||
|
extra: IMapDamageInfoExtra;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapDamageView<T = any> {
|
||||||
|
/**
|
||||||
|
* 获取地图伤害影响范围
|
||||||
|
*/
|
||||||
|
getRange(): IRange<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取范围参数
|
||||||
|
*/
|
||||||
|
getRangeParam(): T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定位置的地图伤害,会对坐标进行判断
|
||||||
|
* @param locator 伤害位置
|
||||||
|
*/
|
||||||
|
getDamageAt(locator: ITileLocator): Readonly<IMapDamageInfo> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定位置的地图伤害,不会对坐标进行判断
|
||||||
|
* @param locator 伤害位置
|
||||||
|
*/
|
||||||
|
getDamageWithoutCheck(
|
||||||
|
locator: ITileLocator
|
||||||
|
): Readonly<IMapDamageInfo> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapDamageConverter<TEnemy, THero> {
|
||||||
|
/**
|
||||||
|
* 转换地图伤害视图
|
||||||
|
*/
|
||||||
|
convert(
|
||||||
|
handler: IReadonlyEnemyHandler<TEnemy, THero>,
|
||||||
|
context: IEnemyContext<TEnemy, THero>
|
||||||
|
): IMapDamageView<any>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapDamageReducer {
|
||||||
|
/**
|
||||||
|
* 对伤害信息进行合并
|
||||||
|
*/
|
||||||
|
reduce(
|
||||||
|
info: Iterable<Readonly<IMapDamageInfo>>,
|
||||||
|
locator: ITileLocator
|
||||||
|
): Readonly<IMapDamageInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapDamage<TEnemy, THero> {
|
||||||
|
/** 当前绑定的怪物上下文 */
|
||||||
|
readonly context: IEnemyContext<TEnemy, THero>;
|
||||||
|
/** 地图伤害系统绑定的全局状态对象 */
|
||||||
|
readonly dataState: IStateBase<TEnemy, THero>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图伤害转换器,并基于当前上下文重建所有地图伤害视图
|
||||||
|
* @param converter 地图伤害转换器
|
||||||
|
*/
|
||||||
|
useConverter(converter: IMapDamageConverter<TEnemy, THero>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置地图伤害合并器
|
||||||
|
* @param reducer 地图伤害合并器
|
||||||
|
*/
|
||||||
|
useReducer(reducer: IMapDamageReducer): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置添加一条无来源地图伤害
|
||||||
|
* @param locator 地图定位符
|
||||||
|
* @param info 地图伤害信息
|
||||||
|
*/
|
||||||
|
addMapDamage(locator: ITileLocator, info: IMapDamageInfo): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置删除一条无来源地图伤害
|
||||||
|
* @param locator 地图定位符
|
||||||
|
* @param info 地图伤害信息
|
||||||
|
*/
|
||||||
|
deleteMapDamage(locator: ITileLocator, info: IMapDamageInfo): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定位置标记为脏,后续访问时会重新计算该点的有来源伤害
|
||||||
|
* @param locator 地图定位符
|
||||||
|
*/
|
||||||
|
markDirty(locator: ITileLocator): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定怪物对应的地图伤害标记为脏并刷新
|
||||||
|
* @param view 怪物视图
|
||||||
|
*/
|
||||||
|
markEnemyDirty(view: IEnemyView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于当前上下文重新刷新全部有来源地图伤害
|
||||||
|
*/
|
||||||
|
refreshAll(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定怪物带来的全部地图伤害来源
|
||||||
|
* @param view 怪物视图
|
||||||
|
*/
|
||||||
|
deleteEnemy(view: IEnemyView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定位置合并后的地图伤害
|
||||||
|
* @param locator 地图定位符
|
||||||
|
*/
|
||||||
|
getReducedDamage(locator: ITileLocator): Readonly<IMapDamageInfo> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定位置未合并的地图伤害列表
|
||||||
|
* @param locator 地图定位符
|
||||||
|
*/
|
||||||
|
getSeparatedDamage(
|
||||||
|
locator: ITileLocator
|
||||||
|
): Iterable<Readonly<IMapDamageInfo>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 伤害系统
|
||||||
|
|
||||||
|
export interface IEnemyDamageInfo {
|
||||||
|
/** 战斗伤害值 */
|
||||||
|
readonly damage: number;
|
||||||
|
/** 战斗回合数 */
|
||||||
|
readonly turn: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IEnemyCritical {
|
||||||
|
/** 此临界点中指定勇士属性的值 */
|
||||||
|
readonly nextValue: number;
|
||||||
|
/** 当前勇士指定属性的值 */
|
||||||
|
readonly baseValue: number;
|
||||||
|
/** 此临界点中指定勇士数值的值与当前值的差,即 `nextValue - baseValue` */
|
||||||
|
readonly nextDiff: number;
|
||||||
|
/** 当前状态下怪物的伤害信息 */
|
||||||
|
readonly baseInfo: IEnemyDamageInfo;
|
||||||
|
/** 此临界点下怪物的伤害信息 */
|
||||||
|
readonly info: IEnemyDamageInfo;
|
||||||
|
/** 此临界点的伤害值与当前伤害值的差 */
|
||||||
|
readonly damageDiff: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CriticalableHeroStatus<THero> = keyof {
|
||||||
|
[P in keyof THero as THero[P] extends number ? P : never]: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IDamageCalculator<TEnemy, THero> {
|
||||||
|
/**
|
||||||
|
* 计算战斗伤害信息
|
||||||
|
* @param handler 信息对象
|
||||||
|
*/
|
||||||
|
calculate(handler: IReadonlyEnemyHandler<TEnemy, THero>): IEnemyDamageInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取临界计算的上界
|
||||||
|
* @param handler 信息对象
|
||||||
|
* @param attribute 勇士的临界属性
|
||||||
|
*/
|
||||||
|
getCriticalLimit(
|
||||||
|
handler: IReadonlyEnemyHandler<TEnemy, THero>,
|
||||||
|
attribute: CriticalableHeroStatus<THero>
|
||||||
|
): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDamageContext<TEnemy, THero> {
|
||||||
|
/** 伤害上下文所属的全局状态对象 */
|
||||||
|
readonly dataState: IStateBase<TEnemy, THero>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取战斗伤害信息
|
||||||
|
* @param enemy 怪物视图
|
||||||
|
*/
|
||||||
|
getDamageInfo(enemy: IEnemyView<TEnemy>): IEnemyDamageInfo | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据怪物对象获取战斗伤害信息
|
||||||
|
* @param enemy 怪物对象
|
||||||
|
*/
|
||||||
|
getDamageInfoByComputed(
|
||||||
|
enemy: IReadonlyEnemy<TEnemy>
|
||||||
|
): IEnemyDamageInfo | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算怪物在指定勇士属性下的临界
|
||||||
|
* @param enemy 怪物视图
|
||||||
|
* @param attribute 计算临界的目标勇士属性,比如计算攻击临界、自定义属性的临界等等
|
||||||
|
* @param precision 临界计算精度,表示会进行多少次二分计算,一般填写 `12-16` 之间的数即可,默认是 12
|
||||||
|
*/
|
||||||
|
calculateCritical(
|
||||||
|
enemy: IEnemyView<TEnemy>,
|
||||||
|
attribute: CriticalableHeroStatus<THero>,
|
||||||
|
precision?: number
|
||||||
|
): Generator<IEnemyCritical, void, void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDamageSystem<TEnemy, THero> extends IDamageContext<
|
||||||
|
TEnemy,
|
||||||
|
THero
|
||||||
|
> {
|
||||||
|
/** 伤害系统所属的上下文 */
|
||||||
|
readonly context: IEnemyContext<TEnemy, THero>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前伤害计算系统使用的伤害计算器
|
||||||
|
* @param calculator 伤害计算器
|
||||||
|
*/
|
||||||
|
useCalculator(calculator: IDamageCalculator<TEnemy, THero>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前使用的伤害计算器
|
||||||
|
*/
|
||||||
|
getCalculator(): IDamageCalculator<TEnemy, THero> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定勇士信息
|
||||||
|
* @param hero 勇士信息
|
||||||
|
*/
|
||||||
|
bindHeroStatus(hero: IReadonlyHeroAttribute<THero> | null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定的怪物标记为脏
|
||||||
|
* @param enemy 怪物视图
|
||||||
|
*/
|
||||||
|
markDirty(enemy: IEnemyView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定的怪物
|
||||||
|
* @param enemy 怪物视图
|
||||||
|
*/
|
||||||
|
deleteEnemy(enemy: IEnemyView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将所有怪物标记为脏
|
||||||
|
*/
|
||||||
|
markAllDirty(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改勇士属性,然后返回修改后勇士属性所组成的计算对象,不影响当前伤害系统的状态
|
||||||
|
* @param modify 勇士修改函数
|
||||||
|
*/
|
||||||
|
with(hero: IHeroAttribute<THero>): IDamageContext<TEnemy, THero>;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 上下文
|
||||||
|
|
||||||
|
export interface IEnemyContext<TEnemy, THero> {
|
||||||
|
/** 怪物上下文宽度 */
|
||||||
|
readonly width: number;
|
||||||
|
/** 怪物上下文高度 */
|
||||||
|
readonly height: number;
|
||||||
|
/** 此上下文使用的索引对象 */
|
||||||
|
readonly indexer: ILocationHelper;
|
||||||
|
/** 当前怪物上下文绑定的全局状态对象 */
|
||||||
|
readonly dataState: IStateBase<TEnemy, THero>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整上下文尺寸,并清空当前上下文中的所有怪物与状态
|
||||||
|
* @param width 地图宽度
|
||||||
|
* @param height 地图高度
|
||||||
|
*/
|
||||||
|
resize(width: number, height: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个光环转换器
|
||||||
|
* @param converter 光环转换器
|
||||||
|
*/
|
||||||
|
registerAuraConverter(converter: IAuraConverter<TEnemy, THero>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销一个光环转换器
|
||||||
|
* @param converter 光环转换器
|
||||||
|
*/
|
||||||
|
unregisterAuraConverter(converter: IAuraConverter<TEnemy, THero>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置光环转换器的启用状态
|
||||||
|
* @param converter 光环转换器
|
||||||
|
* @param enabled 是否启用
|
||||||
|
*/
|
||||||
|
setAuraConverterEnabled(
|
||||||
|
converter: IAuraConverter<TEnemy, THero>,
|
||||||
|
enabled: boolean
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个特殊属性查询效果
|
||||||
|
* @param effect 特殊属性查询效果
|
||||||
|
*/
|
||||||
|
registerSpecialQueryEffect(
|
||||||
|
effect: IEnemySpecialQueryEffect<TEnemy, THero>
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销一个特殊属性查询效果
|
||||||
|
* @param effect 特殊属性查询效果
|
||||||
|
*/
|
||||||
|
unregisterSpecialQueryEffect(
|
||||||
|
effect: IEnemySpecialQueryEffect<TEnemy, THero>
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为指定特殊属性代码注册常规查询效果
|
||||||
|
* @param code 特殊属性代码
|
||||||
|
* @param effect 常规查询效果
|
||||||
|
*/
|
||||||
|
registerCommonQueryEffect(
|
||||||
|
code: number,
|
||||||
|
effect: IEnemyCommonQueryEffect<TEnemy, THero>
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销指定特殊属性代码上的常规查询效果
|
||||||
|
* @param code 特殊属性代码
|
||||||
|
* @param effect 常规查询效果
|
||||||
|
*/
|
||||||
|
unregisterCommonQueryEffect(
|
||||||
|
code: number,
|
||||||
|
effect: IEnemyCommonQueryEffect<TEnemy, THero>
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个最终效果
|
||||||
|
* @param effect 最终效果
|
||||||
|
*/
|
||||||
|
registerFinalEffect(effect: IEnemyFinalEffect<TEnemy, THero>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销一个最终效果
|
||||||
|
* @param effect 最终效果
|
||||||
|
*/
|
||||||
|
unregisterFinalEffect(effect: IEnemyFinalEffect<TEnemy, THero>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定勇士对象
|
||||||
|
* @param hero 勇士属性对象
|
||||||
|
*/
|
||||||
|
bindHero(hero: IReadonlyHeroAttribute<THero> | null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前绑定的勇士属性对象
|
||||||
|
*/
|
||||||
|
getBindedHero(): IReadonlyHeroAttribute<THero> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定怪物对象当前所在位置
|
||||||
|
* @param enemy 怪物对象
|
||||||
|
*/
|
||||||
|
getEnemyLocator(enemy: IEnemy<TEnemy>): Readonly<ITileLocator> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定怪物视图当前所在位置
|
||||||
|
* @param view 怪物视图
|
||||||
|
*/
|
||||||
|
getEnemyLocatorByView(
|
||||||
|
view: IEnemyView<TEnemy>
|
||||||
|
): Readonly<ITileLocator> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据定位符获取怪物视图
|
||||||
|
* @param locator 地图定位符
|
||||||
|
*/
|
||||||
|
getEnemyByLocator(locator: ITileLocator): IEnemyView<TEnemy> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据坐标获取怪物视图
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
*/
|
||||||
|
getEnemyByLoc(x: number, y: number): IEnemyView<TEnemy> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据计算后怪物对象反查怪物视图
|
||||||
|
* @param enemy 计算后怪物对象
|
||||||
|
*/
|
||||||
|
getViewByComputed(enemy: IReadonlyEnemy<TEnemy>): IEnemyView<TEnemy> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在指定位置放置一个怪物对象
|
||||||
|
* @param locator 地图定位符
|
||||||
|
* @param enemy 怪物对象
|
||||||
|
*/
|
||||||
|
setEnemyAt(locator: ITileLocator, enemy: IEnemy<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定位置的怪物
|
||||||
|
* @param locator 地图定位符
|
||||||
|
*/
|
||||||
|
deleteEnemy(locator: ITileLocator): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描指定范围内的怪物视图
|
||||||
|
* @param range 范围对象
|
||||||
|
* @param param 范围参数
|
||||||
|
*/
|
||||||
|
scanRange<T>(
|
||||||
|
range: IRange<T>,
|
||||||
|
param: T
|
||||||
|
): Iterable<[ITileLocator, IEnemyView<TEnemy>]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代上下文中的全部怪物
|
||||||
|
*/
|
||||||
|
iterateEnemy(): Iterable<[ITileLocator, IEnemyView<TEnemy>]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加一个全局光环视图
|
||||||
|
* @param aura 光环视图
|
||||||
|
*/
|
||||||
|
addAura(aura: IAuraView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除一个全局光环视图
|
||||||
|
* @param aura 光环视图
|
||||||
|
*/
|
||||||
|
deleteAura(aura: IAuraView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定地图伤害管理器
|
||||||
|
* @param damage 地图伤害管理器
|
||||||
|
*/
|
||||||
|
attachMapDamage(damage: IMapDamage<TEnemy, THero> | null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前绑定的地图伤害管理器
|
||||||
|
*/
|
||||||
|
getMapDamage(): IMapDamage<TEnemy, THero> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定伤害计算系统
|
||||||
|
* @param system 伤害系统
|
||||||
|
*/
|
||||||
|
attachDamageSystem(system: IDamageSystem<TEnemy, unknown> | null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前绑定的伤害计算系统
|
||||||
|
*/
|
||||||
|
getDamageSystem(): IDamageSystem<TEnemy, THero> | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重建当前上下文中的全部怪物计算结果
|
||||||
|
*
|
||||||
|
* 1. 对所有光环及特殊查询进行构建操作,这一步中会决定每个怪物所拥有的特殊属性,后续不会变动
|
||||||
|
* 2. 执行所有的普通光环效果,修改怪物的基础属性
|
||||||
|
* 3. 执行常规查询效果,允许查询上下文状态并修改怪物自身的基础属性
|
||||||
|
* 4. 执行最终效果,不允许查询上下文状态,仅允许修改怪物自身的基础属性
|
||||||
|
*/
|
||||||
|
buildup(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将指定怪物视图标记为脏
|
||||||
|
* @param view 怪物视图
|
||||||
|
*/
|
||||||
|
markDirty(view: IEnemyView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 申请刷新指定怪物视图
|
||||||
|
* @param view 怪物视图
|
||||||
|
*/
|
||||||
|
requestRefresh(view: IEnemyView<TEnemy>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前上下文中的所有对象与运行状态
|
||||||
|
*/
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销毁当前上下文
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
2
packages-user/data-system/src/index.ts
Normal file
2
packages-user/data-system/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './combat';
|
||||||
|
export * from './trigger';
|
||||||
57
packages-user/data-system/src/trigger/collection.ts
Normal file
57
packages-user/data-system/src/trigger/collection.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { ITrigger, ITriggerCollection, ITriggerHandler } from './types';
|
||||||
|
|
||||||
|
export class TriggerCollection implements ITriggerCollection {
|
||||||
|
/** 当前集合内部维护的触发器列表 */
|
||||||
|
private readonly triggerList: ITrigger[];
|
||||||
|
|
||||||
|
constructor(triggers: Iterable<ITrigger>) {
|
||||||
|
this.triggerList = Array.from(triggers);
|
||||||
|
}
|
||||||
|
|
||||||
|
count(): number {
|
||||||
|
return this.triggerList.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
async trigger<TEnemy = unknown, THero = unknown>(
|
||||||
|
handler: ITriggerHandler<TEnemy, THero>
|
||||||
|
): Promise<void> {
|
||||||
|
for (const trigger of this.triggerList) {
|
||||||
|
await trigger.trigger(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async *triggerIter<TEnemy = unknown, THero = unknown>(
|
||||||
|
handler: ITriggerHandler<TEnemy, THero>
|
||||||
|
): AsyncGenerator<ITrigger, void, ITriggerHandler<TEnemy, THero> | null> {
|
||||||
|
let currentHandler = handler;
|
||||||
|
for (const trigger of this.triggerList) {
|
||||||
|
await trigger.trigger(currentHandler);
|
||||||
|
const nextHandler = yield trigger;
|
||||||
|
if (nextHandler) {
|
||||||
|
currentHandler = nextHandler;
|
||||||
|
} else {
|
||||||
|
currentHandler = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iterate(): Iterable<ITrigger> {
|
||||||
|
return this.triggerList.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
push(trigger: ITrigger): void {
|
||||||
|
this.triggerList.push(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
unshift(trigger: ITrigger): void {
|
||||||
|
this.triggerList.unshift(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
concat(...others: ITriggerCollection[]): ITriggerCollection {
|
||||||
|
const merged = [...this.triggerList];
|
||||||
|
for (const other of others) {
|
||||||
|
merged.push(...other.iterate());
|
||||||
|
}
|
||||||
|
return new TriggerCollection(merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages-user/data-system/src/trigger/index.ts
Normal file
3
packages-user/data-system/src/trigger/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './collection';
|
||||||
|
export * from './registry';
|
||||||
49
packages-user/data-system/src/trigger/registry.ts
Normal file
49
packages-user/data-system/src/trigger/registry.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import {
|
||||||
|
ITrigger,
|
||||||
|
ITriggerRegistry,
|
||||||
|
TriggerFactory,
|
||||||
|
TriggerStringFactory
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export class TriggerRegistry implements ITriggerRegistry {
|
||||||
|
/** 数字类型到触发器工厂的映射 */
|
||||||
|
private readonly typeMap: Map<number, TriggerFactory> = new Map();
|
||||||
|
|
||||||
|
/** 字符串 id 到触发器工厂的映射 */
|
||||||
|
private readonly stringMap: Map<string, TriggerStringFactory> = new Map();
|
||||||
|
|
||||||
|
register(type: number, factory: TriggerFactory): void {
|
||||||
|
if (this.typeMap.has(type)) {
|
||||||
|
logger.warn(132, 'type', type.toString());
|
||||||
|
}
|
||||||
|
this.typeMap.set(type, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(type: number): TriggerFactory | null {
|
||||||
|
return this.typeMap.get(type) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(num: number): ITrigger | null {
|
||||||
|
const factory = this.get(num);
|
||||||
|
if (!factory) return null;
|
||||||
|
return factory(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerString(id: string, factory: TriggerStringFactory): void {
|
||||||
|
if (this.stringMap.has(id)) {
|
||||||
|
logger.warn(132, 'id', id);
|
||||||
|
}
|
||||||
|
this.stringMap.set(id, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
getString(id: string): TriggerStringFactory | null {
|
||||||
|
return this.stringMap.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createByString(id: string): ITrigger | null {
|
||||||
|
const factory = this.getString(id);
|
||||||
|
if (!factory) return null;
|
||||||
|
return factory();
|
||||||
|
}
|
||||||
|
}
|
||||||
139
packages-user/data-system/src/trigger/types.ts
Normal file
139
packages-user/data-system/src/trigger/types.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { ITileLocator } from '@motajs/common';
|
||||||
|
import { ILayerState, IMapLayer, IStateBase } from '@user/data-base';
|
||||||
|
|
||||||
|
export interface ITriggerHandler<TEnemy = unknown, THero = unknown> {
|
||||||
|
/** 当前全局状态对象 */
|
||||||
|
readonly state: IStateBase<TEnemy, THero>;
|
||||||
|
/** 当前楼层状态对象 */
|
||||||
|
readonly layer?: ILayerState;
|
||||||
|
/** 当前参与触发的图层对象 */
|
||||||
|
readonly mapLayer?: IMapLayer;
|
||||||
|
/** 当前触发点定位符 */
|
||||||
|
readonly locator?: ITileLocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TriggerFactory = (type: number) => ITrigger;
|
||||||
|
|
||||||
|
export type TriggerStringFactory = () => ITrigger;
|
||||||
|
|
||||||
|
export interface ITrigger {
|
||||||
|
/** 触发器类型标识 */
|
||||||
|
readonly type: number;
|
||||||
|
/** 触发器优先级 */
|
||||||
|
readonly priority: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用给定上下文触发当前触发器
|
||||||
|
* @param handler 触发上下文对象
|
||||||
|
*/
|
||||||
|
trigger<TEnemy = unknown, THero = unknown>(
|
||||||
|
handler: ITriggerHandler<TEnemy, THero>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前触发器包装为单元素触发器集合
|
||||||
|
*/
|
||||||
|
collection(): ITriggerCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITriggerRegistry {
|
||||||
|
/**
|
||||||
|
* 注册一个按类型创建的触发器工厂
|
||||||
|
* @param type 触发器类型
|
||||||
|
* @param factory 触发器工厂函数
|
||||||
|
*/
|
||||||
|
register(type: number, factory: TriggerFactory): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定类型的触发器工厂
|
||||||
|
* @param type 触发器类型
|
||||||
|
*/
|
||||||
|
get(type: number): TriggerFactory | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据触发器类型创建一个触发器实例,如果对应工厂不存在则返回 `null`
|
||||||
|
* @param num 触发器类型
|
||||||
|
*/
|
||||||
|
create(num: number): ITrigger | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个按字符串 id 查询的触发器工厂
|
||||||
|
* @param id 触发器字符串 id
|
||||||
|
* @param factory 触发器工厂函数
|
||||||
|
*/
|
||||||
|
registerString(id: string, factory: TriggerStringFactory): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定字符串 id 对应的触发器工厂
|
||||||
|
* @param id 触发器字符串 id
|
||||||
|
*/
|
||||||
|
getString(id: string): TriggerStringFactory | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据字符串 id 创建一个触发器实例,如果对应工厂不存在则返回 `null`
|
||||||
|
* @param id 触发器字符串 id
|
||||||
|
*/
|
||||||
|
createByString(id: string): ITrigger | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITriggerCollection {
|
||||||
|
/**
|
||||||
|
* 当前集合中的触发器数量
|
||||||
|
*/
|
||||||
|
count(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顺序触发当前集合中的所有触发器
|
||||||
|
* @param handler 初始触发上下文对象
|
||||||
|
*/
|
||||||
|
trigger<TEnemy = unknown, THero = unknown>(
|
||||||
|
handler: ITriggerHandler<TEnemy, THero>
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逐个触发当前集合中的触发器,并允许为下一次推进提供新上下文
|
||||||
|
* @param handler 初始触发上下文对象
|
||||||
|
*/
|
||||||
|
triggerIter<TEnemy = unknown, THero = unknown>(
|
||||||
|
handler: ITriggerHandler<TEnemy, THero>
|
||||||
|
): AsyncGenerator<ITrigger, void, ITriggerHandler<TEnemy, THero> | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代当前集合中的所有触发器
|
||||||
|
*/
|
||||||
|
iterate(): Iterable<ITrigger>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向集合末尾追加一个触发器
|
||||||
|
* @param trigger 要追加的触发器
|
||||||
|
*/
|
||||||
|
push(trigger: ITrigger): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向集合头部插入一个触发器
|
||||||
|
* @param trigger 要插入的触发器
|
||||||
|
*/
|
||||||
|
unshift(trigger: ITrigger): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前集合与其他集合顺序拼接为一个新集合
|
||||||
|
* @param others 要拼接的其他集合
|
||||||
|
*/
|
||||||
|
concat(...others: ITriggerCollection[]): ITriggerCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITriggerCollector {
|
||||||
|
/**
|
||||||
|
* 收集指定图层中某一点的所有触发器
|
||||||
|
* @param x 横坐标
|
||||||
|
* @param y 纵坐标
|
||||||
|
* @param layer 目标图层
|
||||||
|
*/
|
||||||
|
collect(x: number, y: number, layer: IMapLayer): ITriggerCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定或清除当前 collector 使用的注册表
|
||||||
|
* @param registry 触发器注册表
|
||||||
|
*/
|
||||||
|
attachRegistry(registry: ITriggerRegistry | null): void;
|
||||||
|
}
|
||||||
@ -1,22 +1,3 @@
|
|||||||
/**
|
|
||||||
* 滑动数组
|
|
||||||
* @param arr
|
|
||||||
* @param delta
|
|
||||||
*/
|
|
||||||
export function slide<T>(arr: T[], delta: number): T[] {
|
|
||||||
if (delta === 0) return arr;
|
|
||||||
delta %= arr.length;
|
|
||||||
if (delta > 0) {
|
|
||||||
arr.unshift(...arr.splice(arr.length - delta, delta));
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
if (delta < 0) {
|
|
||||||
arr.push(...arr.splice(0, -delta));
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const backDirMap: Record<Dir2, Dir2> = {
|
const backDirMap: Record<Dir2, Dir2> = {
|
||||||
up: 'down',
|
up: 'down',
|
||||||
down: 'up',
|
down: 'up',
|
||||||
@ -38,23 +19,8 @@ export function has<T>(v: T): v is NonNullable<T> {
|
|||||||
return v !== null && v !== void 0;
|
return v !== null && v !== void 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maxGameScale(n: number = 0) {
|
|
||||||
const index = core.domStyle.availableScale.indexOf(core.domStyle.scale);
|
|
||||||
core.control.setDisplayScale(
|
|
||||||
core.domStyle.availableScale.length - 1 - index - n
|
|
||||||
);
|
|
||||||
if (!core.isPlaying() && core.flags.enableHDCanvas) {
|
|
||||||
// @ts-ignore
|
|
||||||
core.domStyle.ratio = Math.max(
|
|
||||||
window.devicePixelRatio || 1,
|
|
||||||
core.domStyle.scale
|
|
||||||
);
|
|
||||||
core.resize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensureArray<T>(arr: T): T extends any[] ? T : T[] {
|
export function ensureArray<T>(arr: T): T extends any[] ? T : T[] {
|
||||||
// @ts-ignore
|
// @ts-expect-error 需要弃用
|
||||||
return arr instanceof Array ? arr : [arr];
|
return arr instanceof Array ? arr : [arr];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,23 +36,6 @@ export function manhattan(x1: number, y1: number, x2: number, y2: number) {
|
|||||||
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
|
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查一个点是否在当前超大地图 v2 优化范围内
|
|
||||||
*/
|
|
||||||
export function checkV2(x?: number, y?: number) {
|
|
||||||
return (
|
|
||||||
has(x) &&
|
|
||||||
has(y) &&
|
|
||||||
!(
|
|
||||||
core.bigmap.v2 &&
|
|
||||||
(x < core.bigmap.posX - core.bigmap.extend ||
|
|
||||||
x > core.bigmap.posX + core._WIDTH_ + core.bigmap.extend ||
|
|
||||||
y < core.bigmap.posY - core.bigmap.extend ||
|
|
||||||
y > core.bigmap.posY + core._HEIGHT_ + core.bigmap.extend)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDamage(damage: number): DamageString {
|
export function formatDamage(damage: number): DamageString {
|
||||||
let dam = '';
|
let dam = '';
|
||||||
let color = '';
|
let color = '';
|
||||||
@ -105,64 +54,6 @@ export function formatDamage(damage: number): DamageString {
|
|||||||
return { damage: dam, color: color as Color };
|
return { damage: dam, color: color as Color };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断一个数组的数值是否全部相等
|
|
||||||
* @param arr 要判断的数组
|
|
||||||
*/
|
|
||||||
export function equal(arr: number[]): boolean;
|
|
||||||
/**
|
|
||||||
* 判断一个数组的元素的某个属性的数值是否全部相等
|
|
||||||
* @param arr 要判断的数组
|
|
||||||
* @param key 要判断的属性名
|
|
||||||
*/
|
|
||||||
export function equal<T>(arr: T[], key: keyof T): boolean;
|
|
||||||
export function equal(arr: any, key?: any) {
|
|
||||||
if (has(key)) {
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
|
||||||
if (arr[i][key] !== arr[0][key]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
|
||||||
if (arr[i] !== arr[0]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获得一个数组的数值的最大值和最小值
|
|
||||||
* @param arr 要获得的数组
|
|
||||||
*/
|
|
||||||
export function boundary(arr: number[]): [number, number];
|
|
||||||
/**
|
|
||||||
* 获得一个数组的元素的某个属性的数值的最大值和最小值
|
|
||||||
* @param arr 要获得的数组
|
|
||||||
* @param key 要获得的属性名
|
|
||||||
*/
|
|
||||||
export function boundary<T>(arr: T[], key: keyof T): [number, number];
|
|
||||||
export function boundary(arr: any, key?: any) {
|
|
||||||
if (has(key)) {
|
|
||||||
let min = arr[0][key];
|
|
||||||
let max = arr[0][key];
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
|
||||||
const ele = arr[i][key];
|
|
||||||
if (ele < min) min = ele;
|
|
||||||
if (ele > max) max = ele;
|
|
||||||
}
|
|
||||||
return [min, max];
|
|
||||||
} else {
|
|
||||||
let min = arr[0];
|
|
||||||
let max = arr[0];
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
|
||||||
const ele = arr[i];
|
|
||||||
if (ele < min) min = ele;
|
|
||||||
if (ele > max) max = ele;
|
|
||||||
}
|
|
||||||
return [min, max];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取两个坐标的相对方向
|
* 获取两个坐标的相对方向
|
||||||
* @param from 初始坐标
|
* @param from 初始坐标
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import type { TimingFn } from 'mutate-animate';
|
import type { TimingFn } from 'mutate-animate';
|
||||||
import {
|
import { heroMoveCollection, MoveStep, state } from '@user/data-state';
|
||||||
fromDirectionString,
|
import { fromDirectionString, hook, loading } from '@user/data-base';
|
||||||
heroMoveCollection,
|
|
||||||
MoveStep,
|
|
||||||
state
|
|
||||||
} from '@user/data-state';
|
|
||||||
import { hook, loading } from '@user/data-base';
|
|
||||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
|
|
||||||
@ -59,8 +54,6 @@ export function initFallback() {
|
|||||||
|
|
||||||
Mota.r(() => {
|
Mota.r(() => {
|
||||||
// ----- 引入
|
// ----- 引入
|
||||||
const { mainRenderer } = Mota.require('@user/client-modules');
|
|
||||||
const Animation = Mota.require('MutateAnimate');
|
|
||||||
|
|
||||||
const patch = new Patch(PatchClass.Control);
|
const patch = new Patch(PatchClass.Control);
|
||||||
const patch2 = new Patch(PatchClass.Events);
|
const patch2 = new Patch(PatchClass.Events);
|
||||||
@ -331,7 +324,7 @@ export function initFallback() {
|
|||||||
callback?.();
|
callback?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const layer = state.layer.getLayerByAlias('event')!;
|
const layer = state.maps.getLayerByAlias('event')!;
|
||||||
layer.openDoor(x, y).then(cb);
|
layer.openDoor(x, y).then(cb);
|
||||||
|
|
||||||
const animate = fallbackIds++;
|
const animate = fallbackIds++;
|
||||||
@ -380,7 +373,7 @@ export function initFallback() {
|
|||||||
cb();
|
cb();
|
||||||
} else {
|
} else {
|
||||||
const num = state.idNumberMap.get(id)!;
|
const num = state.idNumberMap.get(id)!;
|
||||||
const layer = state.layer.getLayerByAlias('event')!;
|
const layer = state.maps.getLayerByAlias('event')!;
|
||||||
layer.closeDoor(num, x, y).then(cb);
|
layer.closeDoor(num, x, y).then(cb);
|
||||||
|
|
||||||
const animate = fallbackIds++;
|
const animate = fallbackIds++;
|
||||||
@ -521,11 +514,11 @@ export function initFallback() {
|
|||||||
// 先使用 mainMapRenderer 妥协
|
// 先使用 mainMapRenderer 妥协
|
||||||
const { client } = Mota.require('@user/client-modules');
|
const { client } = Mota.require('@user/client-modules');
|
||||||
const renderer = client.mainMapRenderer;
|
const renderer = client.mainMapRenderer;
|
||||||
if (renderer.layerState !== state.layer) {
|
if (renderer.layerState !== state.maps) {
|
||||||
callback?.();
|
callback?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const layer = state.layer.getLayerByAlias('event');
|
const layer = state.maps.getLayerByAlias('event');
|
||||||
if (!layer) {
|
if (!layer) {
|
||||||
callback?.();
|
callback?.();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -54,6 +54,7 @@
|
|||||||
"52": "To get divider payload, an excitation binding is expected.",
|
"52": "To get divider payload, an excitation binding is expected.",
|
||||||
"53": "Expected serializable value set as enemy's default attribute.",
|
"53": "Expected serializable value set as enemy's default attribute.",
|
||||||
"54": "Legacy '$1' API has been removed, consider using new APIs: '$2'.",
|
"54": "Legacy '$1' API has been removed, consider using new APIs: '$2'.",
|
||||||
|
"55": "Cannot load MapStore state: reference data (compareWith) has not been set.",
|
||||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
||||||
},
|
},
|
||||||
"warn": {
|
"warn": {
|
||||||
@ -168,6 +169,27 @@
|
|||||||
"109": "Expected a different object reference returned, but got a same reference at modifier '$1' for property '$2'.",
|
"109": "Expected a different object reference returned, but got a same reference at modifier '$1' for property '$2'.",
|
||||||
"110": "Expected a hero attribute binding before executing any enemy context calculation.",
|
"110": "Expected a hero attribute binding before executing any enemy context calculation.",
|
||||||
"111": "Cannot add value to flag field '$1', since the current value is not a number.",
|
"111": "Cannot add value to flag field '$1', since the current value is not a number.",
|
||||||
|
"112": "Cannot add saveable content since id '$1' has already been occupied.",
|
||||||
|
"113": "Cannot bind saveable executor since target saveable content has not been added.",
|
||||||
|
"114": "Save operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.",
|
||||||
|
"115": "Autosave operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.",
|
||||||
|
"116": "Cannot construct modifier of type '$1' since no registry for it.",
|
||||||
|
"117": "EnemyManager.compareWith called more than once. The previous reference will be overridden. Please ensure you intend to do this.",
|
||||||
|
"118": "No enemy comparer attached to EnemyManager. All enemies will be treated as dirty.",
|
||||||
|
"119": "Enemy prefab with code $1 not found during loadState, skipping.",
|
||||||
|
"120": "Special with code $1 not found in enemy '$2' during loadState, skipping.",
|
||||||
|
"121": "MapStore.createLayerState: floor '$1' already exists, the existing floor will be overwritten.",
|
||||||
|
"122": "MapStore.loadState: floor '$1' not found in current map data, skipping.",
|
||||||
|
"123": "MapLayer.setMapRef: array length $1 does not match expected size $2, setMapRef will be ignored.",
|
||||||
|
"124": "MapStore.loadState: floor '$1' or its layer(s) not found in current reference data, skipping.",
|
||||||
|
"125": "Expected sorted floor id array has a same floor id set, but an array with a different floor id set is returned.",
|
||||||
|
"126": "Only move code '$1' is expected, but got '$2'.",
|
||||||
|
"127": "Block at $1,$2 is 0, the dynamic tile will have num 0.",
|
||||||
|
"128": "Position $1,$2 is out of map bounds, operation cancelled.",
|
||||||
|
"129": "Position $1,$2 is not empty, existing block will be overwritten.",
|
||||||
|
"130": "The given tile is not managed by this dynamic layer.",
|
||||||
|
"131": "Event layer to set is not belong to current LayerState.",
|
||||||
|
"132": "Trigger registry entry of $1 '$2' already exists, new factory will override old factory.",
|
||||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { FaceDirection } from '@user/data-base';
|
||||||
|
|
||||||
export interface ISearchable4Dir {
|
export interface ISearchable4Dir {
|
||||||
/** 获取上侧元素 */
|
/** 获取上侧元素 */
|
||||||
up(): ISearchable4Dir | null;
|
up(): ISearchable4Dir | null;
|
||||||
@ -142,6 +144,11 @@ export interface ITileLocator {
|
|||||||
y: number;
|
y: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFacedTileLocator extends ITileLocator {
|
||||||
|
/** 图块朝向 */
|
||||||
|
direction: FaceDirection;
|
||||||
|
}
|
||||||
|
|
||||||
export const enum InternalDirectionGroup {
|
export const enum InternalDirectionGroup {
|
||||||
/** 上下左右四方向 */
|
/** 上下左右四方向 */
|
||||||
Dir4,
|
Dir4,
|
||||||
|
|||||||
@ -29,8 +29,10 @@ export function getDetailedEnemy(
|
|||||||
return typeof func === 'string' ? func : func(enemy);
|
return typeof func === 'string' ? func : func(enemy);
|
||||||
};
|
};
|
||||||
const special: [string, string, string][] = [...enemy.info.special]
|
const special: [string, string, string][] = [...enemy.info.special]
|
||||||
|
// @ts-expect-error 之后修
|
||||||
.filter(v => !enemy.info.specialHalo?.includes(v))
|
.filter(v => !enemy.info.specialHalo?.includes(v))
|
||||||
.map(vv => {
|
.map(vv => {
|
||||||
|
// @ts-expect-error 之后修
|
||||||
const s = Mota.require('@user/data-state').specials[vv];
|
const s = Mota.require('@user/data-state').specials[vv];
|
||||||
return [
|
return [
|
||||||
fromFunc(s.name, enemy.info),
|
fromFunc(s.name, enemy.info),
|
||||||
|
|||||||
@ -315,10 +315,22 @@ importers:
|
|||||||
'@user/data-base':
|
'@user/data-base':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../data-base
|
version: link:../data-base
|
||||||
|
'@user/data-system':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../data-system
|
||||||
'@user/data-utils':
|
'@user/data-utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../data-utils
|
version: link:../data-utils
|
||||||
|
|
||||||
|
packages-user/data-system:
|
||||||
|
dependencies:
|
||||||
|
'@motajs/common':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/common
|
||||||
|
'@user/data-base':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../data-base
|
||||||
|
|
||||||
packages-user/data-utils:
|
packages-user/data-utils:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@user/data-base':
|
'@user/data-base':
|
||||||
@ -409,8 +421,6 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../data-utils
|
version: link:../data-utils
|
||||||
|
|
||||||
packages-user/types: {}
|
|
||||||
|
|
||||||
packages/animate: {}
|
packages/animate: {}
|
||||||
|
|
||||||
packages/audio:
|
packages/audio:
|
||||||
@ -447,12 +457,6 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../common
|
version: link:../common
|
||||||
|
|
||||||
packages/legacy-data:
|
|
||||||
dependencies:
|
|
||||||
'@motajs/common':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../common
|
|
||||||
|
|
||||||
packages/legacy-system:
|
packages/legacy-system:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@motajs/system':
|
'@motajs/system':
|
||||||
|
|||||||
108
prompt.md
108
prompt.md
@ -2,16 +2,106 @@
|
|||||||
|
|
||||||
以下规则必须时刻遵守,任何情况下都不允许违反。
|
以下规则必须时刻遵守,任何情况下都不允许违反。
|
||||||
|
|
||||||
1. 将我已经写好的代码视为绝对正确,除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。如果你认为我的代码中存在逻辑错误,应当在对话中提出,而不是直接修改。
|
1. **不擅自修改已有代码**:将我已经写好的代码视为绝对正确。除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。若认为我的代码存在错误,应在对话中提出,而不是直接修改。
|
||||||
2. 我做的任何代码修改都是有原因的,如果我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
|
2. **不恢复我的修改**:我做的任何代码修改都是有原因的。若我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
|
||||||
3. 时刻以目的进行驱动,想明白我为什么要这么设计接口,这个接口设计的目的是什么,而不是简单地以实现接口为目标。
|
3. **以目的驱动,而非以接口驱动**:实现前先想清楚我为什么要这样设计接口、这个接口设计的目的是什么,而不是单纯地以将接口填满为目标。
|
||||||
4. 如果思考或实现时有任何问题,应该立刻提问,而不是按照自己的想法去写。
|
4. **遇到歧义立即提问**:若思考或实现时遇到任何问题——例如描述模糊、接口不清晰、某些地方存在歧义等——应立即向我提问,而不是按自己的想法去写。
|
||||||
5. 如果我的目标是重构某个接口,按照我说的方式进行重构,如果是彻底性的重构(接口完全没有重合),则按照正常的方式进行实现,旧代码仅做逻辑与思路上的参考;如果是结构性的重构(接口基本一致,但有一些细节上的差距),则应该将旧代码搬到新的接口上,然后进行一些微调,**不要**擅自新增任何参数、任何新的方法或接口,**不要**仅仅通过新增一个兼容层兼容旧代码来实现重构。
|
5. **接口缺失时停止并提问**:若完成需求所需的接口尚不存在,应立即停止实现,向我提出疑问,而不是擅自新增接口来推进。
|
||||||
|
6. **按我说的方式重构**:若目标是重构某个接口,按照我指定的方式执行:
|
||||||
|
- **彻底性重构**(新旧接口完全没有重合):按正常方式全新实现,旧代码仅作逻辑与思路上的参考。
|
||||||
|
- **结构性重构**(新旧接口基本一致,细节有差距):将旧代码搬移到新接口上后进行微调。**不要**擅自新增任何参数、方法或接口,**不要**仅通过新增兼容层的方式应对重构。
|
||||||
|
7. **不要有任何“顺手”的想法**:任何时候,都不要出现顺手的想法,包括但不限于发现了一个 bug,然后**顺手**修复、发现一处类型错误,然后**顺手**修复等,这种情况下应当遵循规则 1。
|
||||||
|
|
||||||
# 建议规则
|
# 建议规则
|
||||||
|
|
||||||
以下规则为建议性,尽量遵守,但是一些特殊情况也可以违反,由你自己把控。
|
以下规则为建议性,尽量遵守,特殊情况下可灵活处理。
|
||||||
|
|
||||||
1. 我有时会在对话中给你提出实现建议,你应该对建议内容进行合理的参考,合理运用建议内容,一定注意不要滥用。
|
1. **合理参考建议**:我有时会在对话中给出实现建议,应合理参考,切忌滥用。
|
||||||
2. 如果实现与类型标注有冲突,应当以类型标注(一般是 `types.ts`)中的内容为参考来源。
|
2. **以类型标注为参考依据**:实现与类型标注有冲突时,以类型标注(一般是 `types.ts`)中的内容为准。
|
||||||
3. 如果你认为类型标注中的接口设计有问题,或在实现中发现其缺少某些接口,应该向我提问是否添加,我同意后方可添加。
|
3. **发现接口问题时提问**:若认为类型标注中的接口设计有问题,或在实现中发现缺少某些接口,应向我提问是否添加,经我同意后方可添加。
|
||||||
|
4. **接口设计兼顾合理性与便捷性**:设计接口时不仅要考虑合理性,还要考虑使用便捷性。罕见场景应当被支持,但不应与常见场景共用同一接口——这只会增加常见场景的使用难度。
|
||||||
|
5. **避免多余的非空判断与类型守卫**:若某个类型已满足目标的类型约束,不应再对其添加任何判断或过滤操作。典型例子:`Promise.all` 对数组元素类型没有任何限制,传入 `(Promise<unknown> | void)[]` 完全合法,无需多此一举地写成 `Promise.all(arr.filter(v => !!v))`。
|
||||||
|
6. **关于整个仓库的类型检查**:一般情况下**不需要**跑全仓库的类型检查,每个需求都是独立的,不会影响外部内容。只有在进行重构的时候才有此需求。
|
||||||
|
|
||||||
|
**时刻谨记上述要求,避免一个需求反复修改仍无法满足预期。**
|
||||||
|
|
||||||
|
# 开发流程
|
||||||
|
|
||||||
|
当我提出需求时,若没有明确说明直接实现或有其他明确要求,则遵循如下开发流程:
|
||||||
|
|
||||||
|
1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,放在 `docs/dev` 目录下。文档中需明确标注需求细节,以及代码实现的大体思路。此阶段需考虑全面,遇到任何问题应向我提问并确认,不得自行假设。
|
||||||
|
2. 我会对文档进行全面阅读,确认实现细节与思路无误后,方允许开始实现。我可能会对文档进行细微调整,请在实现前重新仔细阅读最终版本。实现过程中如有任何问题,应向我提问,而不是自行决定。
|
||||||
|
|
||||||
|
## 示例文档
|
||||||
|
|
||||||
|
对于新增接口/彻底性地重构接口,大致按照以下格式编写,其余需求按照自己的理解去写即可。如某部分需要详细描述,可单独开设标题;若某个接口内容较多,也可以在文档中为其单独开一个章节进行讲述。我会使用引用块的形式在文档中提出建议或回答。Markdown 文档不需要刻意换行,我的编辑器有自动换行功能,正常写没有问题。
|
||||||
|
|
||||||
|
```md
|
||||||
|
# 需求综述
|
||||||
|
|
||||||
|
描述清楚需求的内容、动机与目的。
|
||||||
|
|
||||||
|
# 接口设计与预期
|
||||||
|
|
||||||
|
这是相当重要的一章。需要按接口逐一列出其方法与成员,分析每个成员和方法的预期使用频率(高 / 中 / 低)并说明判断依据;对于中频和高频成员,还需列出至少一个典型使用场景。
|
||||||
|
|
||||||
|
**命名长度原则**:频率越高,成员名应越短。高频成员以一个单词为宜,最多不超过两个单词;中频成员不超过三个单词;低频成员可以稍长,但不宜过长。
|
||||||
|
|
||||||
|
**频率的定义**:此处频率指**用户编写此调用的可能性或频率**,而非运行时的调用频率。例如某成员每秒被调用千次,但整个游戏中只在一处出现过,仍属低频。
|
||||||
|
|
||||||
|
示例如下:
|
||||||
|
|
||||||
|
## IObjectMover
|
||||||
|
|
||||||
|
- `IObjectMover.forward()`:预期频率**高频**。向前移动一格是地图行走、动画演出等场景的核心需求,在逻辑与演出中都会频繁出现,故为高频。典型使用场景:演出中玩家或 NPC 沿某方向连续移动。
|
||||||
|
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
|
||||||
|
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
按照下面的格式分条描述实现思路。
|
||||||
|
|
||||||
|
## 1. 完成 xxx
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## 2. 完成 xxx
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
按照第三方库 → 其他包 → 当前包的其他文件的顺序写。
|
||||||
|
|
||||||
|
- `xxx 库`: 引用第三方库,说明引用目的,以及需要的接口
|
||||||
|
- `@user/xxx`: 引用的目的,需要这个文件的哪些接口
|
||||||
|
- `xxx.ts`: 引用此文件的目的,需要这个文件的哪些接口
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/package/[folder/]file.ts`
|
||||||
|
|
||||||
|
除非必要或明确提出,一般不建议擅自新增公共方法或成员,必要时可以向我提问。
|
||||||
|
|
||||||
|
- [ ] 新增 `Iinterface` 接口:描述新增接口的动机与目的,会用于干什么
|
||||||
|
- [ ] 新增 `Type` 类型别名:描述新增类型别名的动机与目的,会用于干什么
|
||||||
|
- [ ] 新增 `private readonly property` 成员:描述新增成员的动机与目的,会用于干什么
|
||||||
|
- [ ] 新增 `private method(...)` 方法:描述新增方法的动机与目的,会用于干什么
|
||||||
|
- [ ] 编写 `Class.method` 方法:描述实现的大体内容
|
||||||
|
- [ ] 修改 `Class.method` 方法中的部分内容:描述修改哪些内容,修改这些内容的目的
|
||||||
|
- [ ] 重构文件结构,将 `xxx` 与 `yyy` 修改为 `zzz` 与 `www`...
|
||||||
|
...
|
||||||
|
|
||||||
|
### `@motajs/package/[folder/]file.ts`
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
# 问题
|
||||||
|
|
||||||
|
如果描述中有歧义或比较模糊的地方,可以在此列出。
|
||||||
|
|
||||||
|
1. xxxxxx?
|
||||||
|
2. xxxxxx?
|
||||||
|
```
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user