mirror of
https://github.com/motajs/template.git
synced 2026-05-02 12:23:13 +08:00
Merge c43bfa2ab0 into 1a569804d8
This commit is contained in:
commit
54606f22db
22
dev.md
22
dev.md
@ -28,12 +28,16 @@
|
|||||||
- `pnpm build:packages`: 构建所有 `packages` 文件夹下的内容,使用库模式。
|
- `pnpm build:packages`: 构建所有 `packages` 文件夹下的内容,使用库模式。
|
||||||
- `pnpm build:game`: 构建为可以直接部署的构建包。
|
- `pnpm build:game`: 构建为可以直接部署的构建包。
|
||||||
- `pnpm build:lib`: 构建所有 `packages` `packages-user` 文件夹下的内容,使用库模式。
|
- `pnpm build:lib`: 构建所有 `packages` `packages-user` 文件夹下的内容,使用库模式。
|
||||||
|
- `pnpm type`: 对仓库执行类型检查
|
||||||
|
- `pnpm check:circular`: 对仓库执行循环引用检查
|
||||||
|
|
||||||
## 开发原则
|
## 开发原则
|
||||||
|
|
||||||
- 模块无副作用原则:
|
- 模块原则:
|
||||||
- 所有模块不包含副作用内容,全部由函数、类、常量的声明组成,不出现导出的变量声明、代码执行内容,允许但不建议编写类的静态块。
|
- 无副作用原则:所有模块不包含副作用内容,全部由函数、类、常量的声明组成,不出现导出的变量声明、代码执行内容,允许但不建议编写类的静态块。
|
||||||
- 如果需要模块初始化,编写一个 `createXxx` 函数,然后在 `index.ts` 中整合,再逐级向上传递,直至遇到包含 `create` 函数的 `index.ts`,所有初始化将会统一在顶层模块中执行。
|
- 如果需要模块初始化,编写一个 `createXxx` 函数,然后在 `index.ts` 中整合,再逐级向上传递,直至遇到包含 `create` 函数的 `index.ts`,所有初始化将会统一在顶层模块中执行。
|
||||||
|
- 不允许一个文件导出不属于当前 `monorepo` 或当前文件夹的内容。
|
||||||
|
- 不允许出现循环引用,如果不得不进行循环引用,应当首先考虑接口设计是否有问题。
|
||||||
- 命名规则:
|
- 命名规则:
|
||||||
- 变量、成员、一般常量、方法、函数使用小驼峰。
|
- 变量、成员、一般常量、方法、函数使用小驼峰。
|
||||||
- 类、接口、类型别名、命名空间、泛型、枚举、组件使用大驼峰。
|
- 类、接口、类型别名、命名空间、泛型、枚举、组件使用大驼峰。
|
||||||
@ -47,7 +51,7 @@
|
|||||||
- 长文件可使用 `#region` 分段,可以写上 `#endretion` 允许折叠。
|
- 长文件可使用 `#region` 分段,可以写上 `#endretion` 允许折叠。
|
||||||
- TODO 使用 `// TODO:` 或 `// todo:` 格式。
|
- TODO 使用 `// TODO:` 或 `// todo:` 格式。
|
||||||
- 单行注释的双斜杠与注释内容之间添加一个空格,多行注释只允许出现 `jsDoc` 注释,如果需要多行非 `jsDoc` 注释,使用多个单行注释。
|
- 单行注释的双斜杠与注释内容之间添加一个空格,多行注释只允许出现 `jsDoc` 注释,如果需要多行非 `jsDoc` 注释,使用多个单行注释。
|
||||||
- 注释进行合理换行,考虑到中文字符较宽,建议 40-60 个字符进行换行。不允许在句中换行,必须在标点符号后换行。
|
- 注释进行合理换行,考虑到中文字符较宽,建议 40-60 个字符进行换行。不允许在句中换行,必须在标点符号后换行。参数注释换行后保持对齐。
|
||||||
- 单行注释结尾不添加句号,对于多行长注释,可以在结尾添加句号。
|
- 单行注释结尾不添加句号,对于多行长注释,可以在结尾添加句号。
|
||||||
- 类型:
|
- 类型:
|
||||||
- 不允许出现非必要的 `any` 类型。
|
- 不允许出现非必要的 `any` 类型。
|
||||||
@ -62,3 +66,15 @@
|
|||||||
- 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()`,`this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符。
|
- 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()`,`this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符。
|
||||||
- 只进行必要的非空判断,不必要的非空判断直接使用非空断言 `!` 实现。
|
- 只进行必要的非空判断,不必要的非空判断直接使用非空断言 `!` 实现。
|
||||||
- 除非参数要求传入函数等情况,不建议在函数内写任何局部函数。
|
- 除非参数要求传入函数等情况,不建议在函数内写任何局部函数。
|
||||||
|
- 语句尽量不换行,除非必要,尤其注意三元运算符与 `private readonly` 类成员。
|
||||||
|
|
||||||
|
## 双端分离
|
||||||
|
|
||||||
|
样板将渲染端与数据端彻底分离,数据端可以单独在 `node` 环境运行,可以直接用于录像验证。渲染端仅负责向数据端发送消息,不负责任何逻辑运算。
|
||||||
|
|
||||||
|
- `@user/data-base`: 数据端的系统层,负责核心系统。
|
||||||
|
- `@user/data-state`: 数据端的实现层,依靠系统层实现完整的游戏实例。
|
||||||
|
- `@user/client-base`: 渲染端的系统层,负责渲染端的核心系统。
|
||||||
|
- `@user/client-modules`: 渲染端的实现层,依靠系统层实现客户端的渲染与用户交互。
|
||||||
|
|
||||||
|
数据端允许运行渲染端代码,但需要使用全局接口 `Mota.r(() => {})` 包裹。除非必要,否则不建议在数据端调用渲染端代码。
|
||||||
|
|||||||
194
docs/dev/enemy-manager-save.md
Normal file
194
docs/dev/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`)
|
||||||
110
docs/dev/hero-modifier-save.md
Normal file
110
docs/dev/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` 重建修饰器并挂载
|
||||||
294
docs/dev/map-store-save.md
Normal file
294
docs/dev/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`
|
||||||
|
|
||||||
|
- [ ] 新增 `IMapLayerSave` 接口:单个 MapLayer 存档数据格式
|
||||||
|
- [ ] 新增 `ILayerStateSave` 接口:单个楼层存档数据格式
|
||||||
|
- [ ] 新增 `IMapStoreSave` 接口:MapStore 整体存档数据格式
|
||||||
|
- [ ] 修改 `ILayerState`:新增 `readonly active: boolean` 和
|
||||||
|
`setActiveStatus(active: boolean): void`
|
||||||
|
- [ ] 修改 `IMapLayer`:新增 `setMapRef(array: Uint32Array): void`
|
||||||
|
- [ ] 新增 `IMapStore` 接口:继承 `ISaveableContent<IMapStoreSave>`,
|
||||||
|
含全部接口(见第 7 节)
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/layerState.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `active: boolean = false` 成员:楼层激活状态
|
||||||
|
- [ ] 实现 `setActiveStatus(active: boolean): void`
|
||||||
|
- [ ] 新增 `private dirty: boolean = false` 成员:楼层级脏标记
|
||||||
|
- [ ] 修改 `StateMapLayerHook.onUpdateArea`、`onUpdateBlock`、`onResize`:
|
||||||
|
在转发钩子的同时,将 `state.dirty` 置 `true`
|
||||||
|
- [ ] 新增 `isDirty(): boolean` 方法:返回 `this.dirty`,供 `MapStore` 读取
|
||||||
|
- [ ] 新增 `setDirty(dirty: boolean): void` 方法:
|
||||||
|
供 `MapStore.compareWith` 时根据实际比较结果设置
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `setMapRef(array: Uint32Array): void` 方法:
|
||||||
|
直接替换内部图块数组引用,跳过拷贝,供 `MapStore` 读档时使用。
|
||||||
|
需确保传入数组长度与 `width × height` 匹配,
|
||||||
|
并触发必要的钩子通知(不触发 `onResize`,应触发 `onUpdateArea` 通知全区域更新)。
|
||||||
|
在方法注释中明确标注:调用后不得再持有或修改传入的数组。
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapStore.ts`(新文件)
|
||||||
|
|
||||||
|
- [ ] 实现 `MapStore` 类,实现 `IMapStore`
|
||||||
|
- [ ] `private mapData: Map<string, LayerState>`:楼层 id 到状态对象的映射
|
||||||
|
- [ ] `readonly maps: ReadonlySet<string>`:所有楼层 id 的只读集合视图
|
||||||
|
- [ ] `private refData: Map<string, Map<number, Uint32Array>> | null`:参考基准
|
||||||
|
- [ ] 实现 `getLayerState`、`getActiveMap`、`createLayerState`
|
||||||
|
- [ ] 实现 `isMapActive`、`setMapActiveStatus`、`iterateActiveMaps`、`iterateInactiveMaps`、`iterateAllMaps`
|
||||||
|
- [ ] 实现 `compareWith`
|
||||||
|
- [ ] 实现 `saveNoCompression`、`saveLowCompression`、`saveHighCompression`
|
||||||
|
- [ ] 实现 `loadNoCompression`、`loadLowCompression`、`loadHighCompression`
|
||||||
|
- [ ] 实现 `saveState(compression)` 和 `loadState(state, compression)` 分发
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/index.ts`
|
||||||
|
|
||||||
|
- [ ] 补充导出 `mapStore.ts`
|
||||||
|
|
||||||
|
### `@user/data-base/src/types.ts`
|
||||||
|
|
||||||
|
- [ ] 将 `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`
|
||||||
215
docs/dev/save-system.md
Normal file
215
docs/dev/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]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
|
export * from './face';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import { logger } from '@motajs/common';
|
|||||||
import {
|
import {
|
||||||
IEnemy,
|
IEnemy,
|
||||||
IEnemyContext,
|
IEnemyContext,
|
||||||
|
IEnemySaveState,
|
||||||
IReadonlyEnemy,
|
IReadonlyEnemy,
|
||||||
ISpecial,
|
ISpecial,
|
||||||
IEnemyView
|
IEnemyView
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { SaveCompression } from '../common/types';
|
||||||
|
|
||||||
export class Enemy<TAttr> implements IEnemy<TAttr> {
|
export class Enemy<TAttr> implements IEnemy<TAttr> {
|
||||||
/** 怪物身上的特殊属性列表 */
|
/** 怪物身上的特殊属性列表 */
|
||||||
@ -87,6 +89,29 @@ export class Enemy<TAttr> implements IEnemy<TAttr> {
|
|||||||
this.addSpecial(special.clone());
|
this.addSpecial(special.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveState(_compression: SaveCompression): IEnemySaveState<TAttr> {
|
||||||
|
const specials: Map<number, unknown> = new Map();
|
||||||
|
for (const special of this.specials) {
|
||||||
|
specials.set(special.code, special.saveState(_compression));
|
||||||
|
}
|
||||||
|
return { attrs: structuredClone(this.attributes), specials };
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
||||||
|
|||||||
@ -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/types';
|
||||||
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,36 @@
|
|||||||
import { IRange, ITileLocator } from '@motajs/common';
|
import { IRange, ITileLocator } from '@motajs/common';
|
||||||
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
|
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
|
||||||
|
import { ISaveableContent } from '../common/types';
|
||||||
|
|
||||||
//#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 +67,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 +115,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 +172,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 +229,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,6 +257,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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置参考快照,后续对模板的修改将与此比较以确定是否脏。
|
||||||
|
* 非首次调用时会发出警告,但仍执行覆盖
|
||||||
|
* @param reference code -> 参考怪物的 Map
|
||||||
|
*/
|
||||||
|
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改指定怪物模板的属性,修改完成后自动与参考模板比较并更新 dirty 集合
|
||||||
|
* @param code 怪物的图块数字或 `id`
|
||||||
|
* @param modify 修改函数,传入可写怪物对象,返回修改后的对象
|
||||||
|
*/
|
||||||
|
modifyPrefabAttribute(
|
||||||
|
code: number | string,
|
||||||
|
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||||
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加怪物比较器,用于 dirty 集合的判断
|
||||||
|
* @param comparer 比较器对象
|
||||||
|
*/
|
||||||
|
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前附加的怪物比较器,如未设置则返回 `null`
|
||||||
|
*/
|
||||||
|
getEnemyComparer(): IEnemyComparer<TAttr> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './layerState';
|
export * from './layerState';
|
||||||
export * from './mapLayer';
|
export * from './mapLayer';
|
||||||
|
export * from './mapStore';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
@ -29,6 +29,12 @@ export class LayerState
|
|||||||
/** 图层钩子映射 */
|
/** 图层钩子映射 */
|
||||||
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
||||||
|
|
||||||
|
/** 楼层是否处于激活状态 */
|
||||||
|
active: boolean = false;
|
||||||
|
|
||||||
|
/** 楼层级脏标记 */
|
||||||
|
private dirty: boolean = false;
|
||||||
|
|
||||||
addLayer(width: number, height: number): IMapLayer {
|
addLayer(width: number, height: number): IMapLayer {
|
||||||
const array = new Uint32Array(width * height);
|
const array = new Uint32Array(width * height);
|
||||||
const layer = new MapLayer(array, width, height);
|
const layer = new MapLayer(array, width, height);
|
||||||
@ -106,6 +112,18 @@ export class LayerState
|
|||||||
return this.backgroundTile;
|
return this.backgroundTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveStatus(active: boolean): void {
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +138,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);
|
||||||
});
|
});
|
||||||
@ -186,14 +186,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 lineStart = ny * w + x;
|
||||||
const origin = ny * w + nx;
|
const lineEnd = lineStart + width;
|
||||||
const target = (ny - y) * width + (nx - x);
|
const dy = ny - y;
|
||||||
res[target] = arr[origin];
|
res.set(arr.subarray(lineStart, lineEnd), dy * width);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -205,6 +203,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 {
|
||||||
383
packages-user/data-base/src/map/mapStore.ts
Normal file
383
packages-user/data-base/src/map/mapStore.ts
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
|
import {
|
||||||
|
ILayerState,
|
||||||
|
ILayerStateSave,
|
||||||
|
IMapLayer,
|
||||||
|
IMapLayerSave,
|
||||||
|
IMapStore,
|
||||||
|
IMapStoreSave
|
||||||
|
} from './types';
|
||||||
|
import { LayerState } from './layerState';
|
||||||
|
|
||||||
|
export class MapStore implements IMapStore {
|
||||||
|
/** 楼层 id 到状态对象的映射 */
|
||||||
|
private readonly mapData: Map<string, LayerState> = new Map();
|
||||||
|
|
||||||
|
/** 所有楼层 id 的只读集合视图 */
|
||||||
|
readonly maps: Set<string> = new Set();
|
||||||
|
|
||||||
|
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */
|
||||||
|
private refData: Map<string, Map<number, Uint32Array>> | null = null;
|
||||||
|
|
||||||
|
//#region 楼层访问
|
||||||
|
|
||||||
|
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 楼层管理
|
||||||
|
|
||||||
|
createLayerState(id: string): ILayerState {
|
||||||
|
if (this.mapData.has(id)) {
|
||||||
|
logger.warn(121, id);
|
||||||
|
}
|
||||||
|
const state = new LayerState();
|
||||||
|
// 若 refData 已存在,新楼层直接视为全脏
|
||||||
|
if (this.refData !== null) {
|
||||||
|
state.setDirty(true);
|
||||||
|
}
|
||||||
|
this.mapData.set(id, state);
|
||||||
|
this.maps.add(id);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#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
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
||||||
|
import { ISaveableContent } from '../common';
|
||||||
|
|
||||||
export interface IMapLayerData {
|
export interface IMapLayerData {
|
||||||
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
||||||
@ -48,8 +49,7 @@ export interface IMapLayerHooks extends IHookBase {
|
|||||||
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMapLayerHookController
|
export interface IMapLayerHookController extends IHookController<IMapLayerHooks> {
|
||||||
extends IHookController<IMapLayerHooks> {
|
|
||||||
/** 拓展所属的图层对象 */
|
/** 拓展所属的图层对象 */
|
||||||
readonly layer: IMapLayer;
|
readonly layer: IMapLayer;
|
||||||
|
|
||||||
@ -59,8 +59,10 @@ export interface IMapLayerHookController
|
|||||||
getMapData(): Readonly<IMapLayerData>;
|
getMapData(): Readonly<IMapLayerData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMapLayer
|
export interface IMapLayer extends IHookable<
|
||||||
extends IHookable<IMapLayerHooks, IMapLayerHookController> {
|
IMapLayerHooks,
|
||||||
|
IMapLayerHookController
|
||||||
|
> {
|
||||||
/** 地图宽度 */
|
/** 地图宽度 */
|
||||||
readonly width: number;
|
readonly width: number;
|
||||||
/** 地图高度 */
|
/** 地图高度 */
|
||||||
@ -155,6 +157,15 @@ export interface IMapLayer
|
|||||||
* @param y 门纵坐标
|
* @param y 门纵坐标
|
||||||
*/
|
*/
|
||||||
closeDoor(num: number, x: number, y: number): Promise<void>;
|
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||||
|
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||||
|
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||||
|
* 且调用后不得再持有或修改传入的数组。
|
||||||
|
* @param array 地图数组,会直接替换内部引用
|
||||||
|
*/
|
||||||
|
setMapRef(array: Uint32Array): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILayerStateHooks extends IHookBase {
|
export interface ILayerStateHooks extends IHookBase {
|
||||||
@ -212,6 +223,8 @@ export interface ILayerStateHooks extends IHookBase {
|
|||||||
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||||
/** 地图列表 */
|
/** 地图列表 */
|
||||||
readonly layerList: Set<IMapLayer>;
|
readonly layerList: Set<IMapLayer>;
|
||||||
|
/** 此楼层是否处于激活状态 */
|
||||||
|
readonly active: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加图层
|
* 添加图层
|
||||||
@ -275,4 +288,107 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
|
|||||||
* 获取背景图块数字,如果没有设置过,则返回 0
|
* 获取背景图块数字,如果没有设置过,则返回 0
|
||||||
*/
|
*/
|
||||||
getBackground(): number;
|
getBackground(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置楼层激活状态
|
||||||
|
* @param active 激活状态
|
||||||
|
*/
|
||||||
|
setActiveStatus(active: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼层是否被修改过(相对于参考基准)
|
||||||
|
*/
|
||||||
|
isDirty(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置楼层脏标记
|
||||||
|
*/
|
||||||
|
setDirty(dirty: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||||
|
/** 所有楼层的 id 集合 */
|
||||||
|
readonly maps: ReadonlySet<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
|
||||||
|
*/
|
||||||
|
createLayerState(id: string): 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;
|
||||||
}
|
}
|
||||||
43
packages-user/data-base/src/types.ts
Normal file
43
packages-user/data-base/src/types.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { IHeroFollower, IHeroState } from './hero';
|
||||||
|
import { IEnemyManager } from './enemy';
|
||||||
|
import { IFlagSystem } from './flag';
|
||||||
|
import { IRoleFaceBinder, ISaveableContent } from './common';
|
||||||
|
import { IMapStore } from './map';
|
||||||
|
|
||||||
|
export interface IStateSaveData {
|
||||||
|
/** 跟随者列表 */
|
||||||
|
readonly followers: readonly IHeroFollower[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IStateBase<TEnemy, THero> {
|
||||||
|
/** 朝向绑定 */
|
||||||
|
readonly roleFace: IRoleFaceBinder;
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
@ -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,16 @@ import {
|
|||||||
FlagSystem,
|
FlagSystem,
|
||||||
IMotaDataLoader,
|
IMotaDataLoader,
|
||||||
MotaDataLoader,
|
MotaDataLoader,
|
||||||
loading
|
loading,
|
||||||
|
IRoleFaceBinder,
|
||||||
|
RoleFaceBinder,
|
||||||
|
FaceDirection,
|
||||||
|
ISaveableContent,
|
||||||
|
SaveCompression,
|
||||||
|
IReadonlyEnemy,
|
||||||
|
IMapStore,
|
||||||
|
MapStore
|
||||||
} from '@user/data-base';
|
} from '@user/data-base';
|
||||||
import { IEnemyAttr } from './enemy/types';
|
|
||||||
import {
|
import {
|
||||||
CommonAuraConverter,
|
CommonAuraConverter,
|
||||||
EnemyLegacyBridge,
|
EnemyLegacyBridge,
|
||||||
@ -27,31 +32,56 @@ 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 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.roleFace = new RoleFaceBinder();
|
||||||
this.idNumberMap = new Map();
|
this.idNumberMap = new Map();
|
||||||
this.numberIdMap = new Map();
|
this.numberIdMap = new Map();
|
||||||
@ -71,7 +101,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,6 +130,25 @@ 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.flags = new FlagSystem();
|
this.flags = new FlagSystem();
|
||||||
@ -105,8 +156,17 @@ export class CoreState implements ICoreState {
|
|||||||
// 加载先使用兼容层实现
|
// 加载先使用兼容层实现
|
||||||
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 +177,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 +188,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 +199,115 @@ 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);
|
||||||
|
const bg = state.addLayer(floor.width, floor.height);
|
||||||
|
const bg2 = state.addLayer(floor.width, floor.height);
|
||||||
|
const event = state.addLayer(floor.width, floor.height);
|
||||||
|
const fg = state.addLayer(floor.width, floor.height);
|
||||||
|
const fg2 = state.addLayer(floor.width, floor.height);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floor.bg2map && floor.bg2map.length > 0) {
|
||||||
|
const arr = new Uint32Array(floor.bg2map.flat());
|
||||||
|
bg2.setMapRef(arr);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadState(data: IStateSaveData): void {
|
addSaveableContent(id: string, content: ISaveableContent<unknown>): void {
|
||||||
this.hero.mover.removeAllFollowers();
|
if (this.saveables.has(id)) {
|
||||||
data.followers.forEach(v => {
|
logger.warn(112, id);
|
||||||
this.hero.mover.addFollower(v.num, v.identifier);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,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,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: 转身功能
|
||||||
|
|
||||||
|
|||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,19 @@
|
|||||||
"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.",
|
||||||
"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),
|
||||||
|
|||||||
71
prompt.md
71
prompt.md
@ -5,8 +5,8 @@
|
|||||||
1. 将我已经写好的代码视为绝对正确,除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。如果你认为我的代码中存在逻辑错误,应当在对话中提出,而不是直接修改。
|
1. 将我已经写好的代码视为绝对正确,除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。如果你认为我的代码中存在逻辑错误,应当在对话中提出,而不是直接修改。
|
||||||
2. 我做的任何代码修改都是有原因的,如果我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
|
2. 我做的任何代码修改都是有原因的,如果我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
|
||||||
3. 时刻以目的进行驱动,想明白我为什么要这么设计接口,这个接口设计的目的是什么,而不是简单地以实现接口为目标。
|
3. 时刻以目的进行驱动,想明白我为什么要这么设计接口,这个接口设计的目的是什么,而不是简单地以实现接口为目标。
|
||||||
4. 如果思考或实现时有任何问题,应该立刻提问,而不是按照自己的想法去写。
|
4. 如果思考或实现时有任何问题,比如我的描述比较模糊,或接口描述比较模糊,或某些地方会产生歧义等等,应该立刻向我提问,而不是按照自己的想法去写。
|
||||||
5. 如果我的目标是重构某个接口,按照我说的方式进行重构,如果是彻底性的重构(接口完全没有重合),则按照正常的方式进行实现,旧代码仅做逻辑与思路上的参考;如果是结构性的重构(接口基本一致,但有一些细节上的差距),则应该将旧代码搬到新的接口上,然后进行一些微调,**不要**擅自新增任何参数、任何新的方法或接口,**不要**仅仅通过新增一个兼容层兼容旧代码来实现重构。
|
5. 如果我的目标是重构某个接口,按照我说的方式进行重构。如果是彻底性的重构(接口完全没有重合),则按照正常的方式进行实现,旧代码仅做逻辑与思路上的参考;如果是结构性的重构(接口基本一致,但有一些细节上的差距),则应该将旧代码搬到新的接口上,然后进行一些微调,**不要**擅自新增任何参数、任何新的方法或接口,**不要**仅仅通过新增一个兼容层兼容旧代码来实现重构。
|
||||||
|
|
||||||
# 建议规则
|
# 建议规则
|
||||||
|
|
||||||
@ -15,3 +15,70 @@
|
|||||||
1. 我有时会在对话中给你提出实现建议,你应该对建议内容进行合理的参考,合理运用建议内容,一定注意不要滥用。
|
1. 我有时会在对话中给你提出实现建议,你应该对建议内容进行合理的参考,合理运用建议内容,一定注意不要滥用。
|
||||||
2. 如果实现与类型标注有冲突,应当以类型标注(一般是 `types.ts`)中的内容为参考来源。
|
2. 如果实现与类型标注有冲突,应当以类型标注(一般是 `types.ts`)中的内容为参考来源。
|
||||||
3. 如果你认为类型标注中的接口设计有问题,或在实现中发现其缺少某些接口,应该向我提问是否添加,我同意后方可添加。
|
3. 如果你认为类型标注中的接口设计有问题,或在实现中发现其缺少某些接口,应该向我提问是否添加,我同意后方可添加。
|
||||||
|
|
||||||
|
**时刻谨记上述要求,避免一个需求写好几次都写不出来,或写出我不满意的代码而挨骂**
|
||||||
|
|
||||||
|
# 开发流程
|
||||||
|
|
||||||
|
当我提出需求时,如果没有明确说明直接实现或有其他明确要求,则遵循如下开发流程:
|
||||||
|
|
||||||
|
1. 阅读当前代码,分析需求,将需求整理为一个 markdown 文档,文档中明确标记需求细节,以及代码实现的大体思路。这一阶段中应当考虑全面,遇到任何问题应向我提问并确认。文档可以放在 `docs/dev` 目录下。
|
||||||
|
2. 我会对文档进行全面的阅读,确保实现细节与思路没有问题后,允许你开始实现。这一步中我可能会对文档进行细微的调整,确保重新仔细阅读文档。如果实现时遇到了任何问题,应该向我提问,而不是按照自己的想法去写。
|
||||||
|
|
||||||
|
## 示例文档
|
||||||
|
|
||||||
|
大致按照下述示例文档的格式编写,如果某些场景需要详细描述某个东西,可以单独开一个标题来写。
|
||||||
|
|
||||||
|
```md
|
||||||
|
# 需求综述
|
||||||
|
|
||||||
|
描述清楚需求的内容与目的。
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
按照下面的格式分条描述实现思路,可以创建为 `todo list`。
|
||||||
|
|
||||||
|
## 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