mirror of
https://github.com/motajs/template.git
synced 2026-05-02 12:23:13 +08:00
refactor: 存档系统 & 勇士属性存档
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
ec59a44efc
commit
8c1becc9f1
22
dev.md
22
dev.md
@ -28,12 +28,16 @@
|
||||
- `pnpm build:packages`: 构建所有 `packages` 文件夹下的内容,使用库模式。
|
||||
- `pnpm build:game`: 构建为可以直接部署的构建包。
|
||||
- `pnpm build:lib`: 构建所有 `packages` `packages-user` 文件夹下的内容,使用库模式。
|
||||
- `pnpm type`: 对仓库执行类型检查
|
||||
- `pnpm check:circular`: 对仓库执行循环引用检查
|
||||
|
||||
## 开发原则
|
||||
|
||||
- 模块无副作用原则:
|
||||
- 所有模块不包含副作用内容,全部由函数、类、常量的声明组成,不出现导出的变量声明、代码执行内容,允许但不建议编写类的静态块。
|
||||
- 模块原则:
|
||||
- 无副作用原则:所有模块不包含副作用内容,全部由函数、类、常量的声明组成,不出现导出的变量声明、代码执行内容,允许但不建议编写类的静态块。
|
||||
- 如果需要模块初始化,编写一个 `createXxx` 函数,然后在 `index.ts` 中整合,再逐级向上传递,直至遇到包含 `create` 函数的 `index.ts`,所有初始化将会统一在顶层模块中执行。
|
||||
- 不允许一个文件导出不属于当前 `monorepo` 或当前文件夹的内容。
|
||||
- 不允许出现循环引用,如果不得不进行循环引用,应当首先考虑接口设计是否有问题。
|
||||
- 命名规则:
|
||||
- 变量、成员、一般常量、方法、函数使用小驼峰。
|
||||
- 类、接口、类型别名、命名空间、泛型、枚举、组件使用大驼峰。
|
||||
@ -47,7 +51,7 @@
|
||||
- 长文件可使用 `#region` 分段,可以写上 `#endretion` 允许折叠。
|
||||
- TODO 使用 `// TODO:` 或 `// todo:` 格式。
|
||||
- 单行注释的双斜杠与注释内容之间添加一个空格,多行注释只允许出现 `jsDoc` 注释,如果需要多行非 `jsDoc` 注释,使用多个单行注释。
|
||||
- 注释进行合理换行,考虑到中文字符较宽,建议 40-60 个字符进行换行。不允许在句中换行,必须在标点符号后换行。
|
||||
- 注释进行合理换行,考虑到中文字符较宽,建议 40-60 个字符进行换行。不允许在句中换行,必须在标点符号后换行。参数注释换行后保持对齐。
|
||||
- 单行注释结尾不添加句号,对于多行长注释,可以在结尾添加句号。
|
||||
- 类型:
|
||||
- 不允许出现非必要的 `any` 类型。
|
||||
@ -62,3 +66,15 @@
|
||||
- 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `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(() => {})` 包裹。除非必要,否则不建议在数据端调用渲染端代码。
|
||||
|
||||
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`
|
||||
|
||||
- [ ] 修改 `IHeroModifier<T, V>` 接口:
|
||||
改为 `IHeroModifier<T, V, S = unknown>`,`identifier` 改名为 `type`,
|
||||
继承 `ISaveableContent<S>`
|
||||
- [ ] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式
|
||||
- [ ] 修改 `IHeroStateSave<THero>`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段
|
||||
- [ ] 修改 `IReadonlyHeroAttribute<THero>`:新增 `iterateModifiers()` 方法签名
|
||||
- [ ] 修改 `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`
|
||||
|
||||
- [ ] 修改 `BaseHeroModifier<T, V>`:
|
||||
将 `abstract readonly identifier` 改为 `abstract readonly type`;
|
||||
实现 `saveState` / `loadState`
|
||||
- [ ] 修改 `HeroAttribute<THero>`:实现 `iterateModifiers()`
|
||||
|
||||
### `@user/data-base/hero/state.ts`
|
||||
|
||||
- [ ] 修改 `HeroState<THero>`:新增 `private readonly registry: Map<string, () => IHeroModifier>` 成员
|
||||
- [ ] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry`
|
||||
- [ ] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例;
|
||||
若 `type` 未注册则抛出错误
|
||||
- [ ] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后,
|
||||
再调用 `this.attribute.addModifier(name, modifier)`,返回同一实例
|
||||
- [ ] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段
|
||||
- [ ] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载
|
||||
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 序列化
|
||||
@ -52,3 +52,31 @@ export interface IRoleFaceBinder {
|
||||
*/
|
||||
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,7 +2,7 @@ import { IFlagCommonField, IFlagSystem, IFlagSystemSave } from './types';
|
||||
import { FlagCommonField } from './field';
|
||||
|
||||
export class FlagSystem implements IFlagSystem {
|
||||
private readonly fieldMap: Map<PropertyKey, FlagCommonField<any>> =
|
||||
private readonly fieldMap: Map<PropertyKey, IFlagCommonField<any>> =
|
||||
new Map();
|
||||
|
||||
occupied(field: PropertyKey): boolean {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
//#region 字段
|
||||
|
||||
import { ISaveableContent } from '../common';
|
||||
|
||||
export interface IFlagCommonField<T> {
|
||||
/** 此字段所处的 Flag 系统 */
|
||||
readonly system: IFlagSystem;
|
||||
@ -42,7 +44,7 @@ export interface IFlagSystemSave {
|
||||
readonly fields: Map<PropertyKey, any>;
|
||||
}
|
||||
|
||||
export interface IFlagSystem {
|
||||
export interface IFlagSystem extends ISaveableContent<IFlagSystemSave> {
|
||||
/**
|
||||
* 判断一个字段是否被占用,类似于旧样板的 `core.hasFlag`
|
||||
* @param field 字段名称
|
||||
@ -131,17 +133,6 @@ export interface IFlagSystem {
|
||||
* @param defaultValue 字段默认值
|
||||
*/
|
||||
getFieldValueDefaults<T>(field: PropertyKey, defaultValue: T): T;
|
||||
|
||||
/**
|
||||
* 对 Flag 系统进行结构化复制,形成存档对象
|
||||
*/
|
||||
saveState(): IFlagSystemSave;
|
||||
|
||||
/**
|
||||
* 从指定存档对象读取信息
|
||||
* @param state 存档对象
|
||||
*/
|
||||
loadState(state: IFlagSystemSave): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { SaveCompression } from '../common';
|
||||
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;
|
||||
|
||||
owner: IHeroAttribute<unknown> | null = null;
|
||||
@ -25,6 +27,14 @@ export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V> {
|
||||
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 clone(): IHeroModifier<T, V>;
|
||||
@ -161,4 +171,14 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
||||
getModifiableClone(): IHeroAttribute<THero> {
|
||||
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 {
|
||||
IHeroAttribute,
|
||||
IHeroModifier,
|
||||
IHeroMover,
|
||||
IHeroState,
|
||||
IHeroStateSave,
|
||||
IModifierStateSave,
|
||||
IReadonlyHeroAttribute
|
||||
} from './types';
|
||||
import { SaveCompression } from '../common';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class HeroState<THero> implements IHeroState<THero> {
|
||||
/** 修饰器工厂函数注册表 */
|
||||
private readonly registry: Map<string, () => IHeroModifier> = new Map();
|
||||
|
||||
constructor(
|
||||
public mover: IHeroMover,
|
||||
public attribute: IHeroAttribute<THero>
|
||||
@ -34,4 +43,74 @@ export class HeroState<THero> implements IHeroState<THero> {
|
||||
getIsolatedAttribute(): IHeroAttribute<THero> {
|
||||
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 { FaceDirection } from '@user/data-state';
|
||||
import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common';
|
||||
import { FaceDirection, ISaveableContent } from '../common';
|
||||
|
||||
//#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;
|
||||
/** 修饰器参数值 */
|
||||
@ -42,6 +48,15 @@ export interface IHeroModifier<T = unknown, V = unknown> {
|
||||
clone(): IHeroModifier<T, V>;
|
||||
}
|
||||
|
||||
export interface IModifierStateSave {
|
||||
/** 属性名称 */
|
||||
readonly name: PropertyKey;
|
||||
/** 修饰器类型 */
|
||||
readonly type: string;
|
||||
/** 修饰器存档数据 */
|
||||
readonly state: unknown;
|
||||
}
|
||||
|
||||
export interface IReadonlyHeroAttribute<THero> {
|
||||
/**
|
||||
* 获取勇士的基础属性,即未经过任何 Buff 或装备等加成的属性
|
||||
@ -77,6 +92,16 @@ export interface IReadonlyHeroAttribute<THero> {
|
||||
* 获取此勇士属性对象的可修改副本
|
||||
*/
|
||||
getModifiableClone(): IHeroAttribute<THero>;
|
||||
|
||||
/**
|
||||
* 转换为结构化对象
|
||||
*/
|
||||
toStructured(): THero;
|
||||
|
||||
/**
|
||||
* 遍历所有已挂载的属性修饰器
|
||||
*/
|
||||
iterateModifiers(): Iterable<[PropertyKey, IHeroModifier]>;
|
||||
}
|
||||
|
||||
export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
|
||||
@ -332,7 +357,20 @@ export interface IHeroMover extends IHookable<IHeroMovingHooks> {
|
||||
|
||||
//#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;
|
||||
/** 勇士属性对象 */
|
||||
@ -369,6 +407,29 @@ export interface IHeroState<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
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { IMotaDataLoader } from './load';
|
||||
import { ILoadProgressTotal } from '@motajs/loader';
|
||||
import { IHeroFollower, IHeroState } from './hero';
|
||||
import { IEnemyContext, IEnemyManager } from './enemy';
|
||||
import { IEnemyManager } from './enemy';
|
||||
import { IFlagSystem } from './flag';
|
||||
import { IRoleFaceBinder } from './common';
|
||||
import { IRoleFaceBinder, ISaveableContent } from './common';
|
||||
import { ILayerState } from './map';
|
||||
|
||||
export interface IStateSaveData {
|
||||
@ -19,11 +17,6 @@ export interface IStateBase<TEnemy, THero> {
|
||||
/** 图块数字到 id 的映射 */
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
|
||||
/** 加载进度对象 */
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
/** 数据端加载对象 */
|
||||
readonly dataLoader: IMotaDataLoader;
|
||||
|
||||
/** 地图状态 */
|
||||
readonly layer: ILayerState;
|
||||
/** 勇士状态 */
|
||||
@ -31,20 +24,20 @@ export interface IStateBase<TEnemy, THero> {
|
||||
|
||||
/** 怪物管理器 */
|
||||
readonly enemyManager: IEnemyManager<TEnemy>;
|
||||
/** 怪物上下文 */
|
||||
readonly enemyContext: IEnemyContext<TEnemy, THero>;
|
||||
|
||||
/** Flag 系统 */
|
||||
readonly flags: IFlagSystem;
|
||||
|
||||
/**
|
||||
* 保存当前状态
|
||||
* 添加可存档对象,添加后系统将会自动在存档时将对象存储
|
||||
* @param id 可存档对象的 id
|
||||
* @param content 可存档对象
|
||||
*/
|
||||
saveState(): IStateSaveData;
|
||||
addSaveableContent(id: string, content: ISaveableContent<unknown>): void;
|
||||
|
||||
/**
|
||||
* 加载状态
|
||||
* @param state 状态对象
|
||||
* 根据 id 获取对应的可存档对象
|
||||
* @param id 可存档对象的 id
|
||||
*/
|
||||
loadState(state: IStateSaveData): void;
|
||||
getSaveableContent<T>(id: string): ISaveableContent<T> | null;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ICoreState, IStateSaveData } from './types';
|
||||
import { ICoreState, ISaveableExecutor } from './types';
|
||||
import {
|
||||
DamageSystem,
|
||||
EnemyContext,
|
||||
@ -19,9 +19,12 @@ import {
|
||||
ILayerState,
|
||||
LayerState,
|
||||
RoleFaceBinder,
|
||||
FaceDirection
|
||||
FaceDirection,
|
||||
ISaveableContent,
|
||||
IStateSaveData,
|
||||
SaveCompression
|
||||
} from '@user/data-base';
|
||||
import { IEnemyAttr } from './enemy/types';
|
||||
import { IEnemyAttr } from './enemy';
|
||||
import {
|
||||
CommonAuraConverter,
|
||||
EnemyLegacyBridge,
|
||||
@ -36,23 +39,38 @@ import { HERO_DEFAULT_ATTRIBUTE, TILE_HEIGHT, TILE_WIDTH } from './shared';
|
||||
import { IHeroAttr } from './hero';
|
||||
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { logger } from '@motajs/common';
|
||||
import { ISaveSystem } from './save';
|
||||
import { SaveSystem } from './save/system';
|
||||
|
||||
export class CoreState implements ICoreState {
|
||||
// 全局内容
|
||||
readonly roleFace: IRoleFaceBinder;
|
||||
readonly idNumberMap: Map<string, number>;
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
readonly dataLoader: IMotaDataLoader;
|
||||
|
||||
// 可存档内容
|
||||
readonly layer: ILayerState;
|
||||
readonly hero: IHeroState<IHeroAttr>;
|
||||
|
||||
readonly enemyManager: IEnemyManager<IEnemyAttr>;
|
||||
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
||||
|
||||
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() {
|
||||
this.layer = new LayerState();
|
||||
this.roleFace = new RoleFaceBinder();
|
||||
@ -101,6 +119,25 @@ export class CoreState implements ICoreState {
|
||||
|
||||
//#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 其他初始化
|
||||
|
||||
this.flags = new FlagSystem();
|
||||
@ -110,6 +147,8 @@ export class CoreState implements ICoreState {
|
||||
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
|
||||
});
|
||||
|
||||
this.addSaveableContent('flags', this.flags);
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@ -144,6 +183,36 @@ export class CoreState implements ICoreState {
|
||||
}
|
||||
}
|
||||
|
||||
addSaveableContent(id: string, content: ISaveableContent<unknown>): void {
|
||||
if (this.saveables.has(id)) {
|
||||
logger.warn(112, id);
|
||||
return;
|
||||
}
|
||||
this.saveables.set(id, 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);
|
||||
}
|
||||
}
|
||||
|
||||
saveState(): IStateSaveData {
|
||||
return structuredClone({
|
||||
followers: this.hero.mover.followers
|
||||
|
||||
@ -5,3 +5,4 @@ export * from './final';
|
||||
export * from './legacy';
|
||||
export * from './mapDamage';
|
||||
export * from './special';
|
||||
export * from './types';
|
||||
|
||||
1
packages-user/data-state/src/save/index.ts
Normal file
1
packages-user/data-state/src/save/index.ts
Normal file
@ -0,0 +1 @@
|
||||
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,41 @@
|
||||
import { IHeroFollower, IStateBase } from '@user/data-base';
|
||||
import { IEnemyAttr } from './enemy/types';
|
||||
import {
|
||||
IEnemyContext,
|
||||
IMotaDataLoader,
|
||||
ISaveableContent,
|
||||
IStateBase
|
||||
} from '@user/data-base';
|
||||
import { IEnemyAttr } from './enemy';
|
||||
import { IHeroAttr } from './hero';
|
||||
import { ILoadProgressTotal } from '@motajs/loader';
|
||||
import { ISaveSystem } from './save';
|
||||
|
||||
export interface IStateSaveData {
|
||||
/** 跟随者列表 */
|
||||
readonly followers: readonly IHeroFollower[];
|
||||
export interface ISaveableExecutor<T, TEnemy = IEnemyAttr, THero = IHeroAttr> {
|
||||
/**
|
||||
* 当数据读取后执行的函数,允许对其他存档对象进行读取
|
||||
* @param data 对应可存档对象的存档数据
|
||||
* @param state 当前的基础状态
|
||||
*/
|
||||
afterLoad(data: T, state: IStateBase<TEnemy, THero>): void;
|
||||
}
|
||||
|
||||
export interface ICoreState extends IStateBase<IEnemyAttr, IHeroAttr> {}
|
||||
export interface ICoreState extends IStateBase<IEnemyAttr, IHeroAttr> {
|
||||
/** 加载进度对象 */
|
||||
readonly loadProgress: ILoadProgressTotal;
|
||||
/** 数据端加载对象 */
|
||||
readonly dataLoader: IMotaDataLoader;
|
||||
/** 怪物上下文 */
|
||||
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
|
||||
|
||||
/** 存档系统 */
|
||||
readonly saveSystem: ISaveSystem;
|
||||
|
||||
/**
|
||||
* 将某个存档执行器绑定至指定的可存档对象,一个可存档对象只能绑定一个执行器,但一个执行器可以绑定多个可存档对象
|
||||
* @param content 可存档对象或其注册 id
|
||||
* @param executor 可存档对象对应的执行器
|
||||
*/
|
||||
bindSaveableExecuter<T>(
|
||||
content: ISaveableContent<T> | string,
|
||||
executor: ISaveableExecutor<T>
|
||||
): void;
|
||||
}
|
||||
|
||||
@ -168,6 +168,11 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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 {
|
||||
/** 获取上侧元素 */
|
||||
up(): ISearchable4Dir | null;
|
||||
@ -142,6 +144,11 @@ export interface ITileLocator {
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IFacedTileLocator extends ITileLocator {
|
||||
/** 图块朝向 */
|
||||
direction: FaceDirection;
|
||||
}
|
||||
|
||||
export const enum InternalDirectionGroup {
|
||||
/** 上下左右四方向 */
|
||||
Dir4,
|
||||
|
||||
71
prompt.md
71
prompt.md
@ -5,8 +5,8 @@
|
||||
1. 将我已经写好的代码视为绝对正确,除非我**明确允许**,否则**不允许任何修改**,哪怕因为接口变化或其他原因导致其中出现类型错误。如果你认为我的代码中存在逻辑错误,应当在对话中提出,而不是直接修改。
|
||||
2. 我做的任何代码修改都是有原因的,如果我在两次对话期间新增、删除或修改了部分代码,不要将其恢复。
|
||||
3. 时刻以目的进行驱动,想明白我为什么要这么设计接口,这个接口设计的目的是什么,而不是简单地以实现接口为目标。
|
||||
4. 如果思考或实现时有任何问题,应该立刻提问,而不是按照自己的想法去写。
|
||||
5. 如果我的目标是重构某个接口,按照我说的方式进行重构,如果是彻底性的重构(接口完全没有重合),则按照正常的方式进行实现,旧代码仅做逻辑与思路上的参考;如果是结构性的重构(接口基本一致,但有一些细节上的差距),则应该将旧代码搬到新的接口上,然后进行一些微调,**不要**擅自新增任何参数、任何新的方法或接口,**不要**仅仅通过新增一个兼容层兼容旧代码来实现重构。
|
||||
4. 如果思考或实现时有任何问题,比如我的描述比较模糊,或接口描述比较模糊,或某些地方会产生歧义等等,应该立刻向我提问,而不是按照自己的想法去写。
|
||||
5. 如果我的目标是重构某个接口,按照我说的方式进行重构。如果是彻底性的重构(接口完全没有重合),则按照正常的方式进行实现,旧代码仅做逻辑与思路上的参考;如果是结构性的重构(接口基本一致,但有一些细节上的差距),则应该将旧代码搬到新的接口上,然后进行一些微调,**不要**擅自新增任何参数、任何新的方法或接口,**不要**仅仅通过新增一个兼容层兼容旧代码来实现重构。
|
||||
|
||||
# 建议规则
|
||||
|
||||
@ -15,3 +15,70 @@
|
||||
1. 我有时会在对话中给你提出实现建议,你应该对建议内容进行合理的参考,合理运用建议内容,一定注意不要滥用。
|
||||
2. 如果实现与类型标注有冲突,应当以类型标注(一般是 `types.ts`)中的内容为参考来源。
|
||||
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