diff --git a/dev.md b/dev.md index d739fd1..50b1504 100644 --- a/dev.md +++ b/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(() => {})` 包裹。除非必要,否则不建议在数据端调用渲染端代码。 diff --git a/docs/dev/enemy-manager-save.md b/docs/dev/enemy-manager-save.md new file mode 100644 index 0000000..c08cf0c --- /dev/null +++ b/docs/dev/enemy-manager-save.md @@ -0,0 +1,194 @@ +# 需求综述 + +对怪物管理器 `IEnemyManager` 进行存档适配,使其能够参与游戏存档系统。 +由于大多数情况下怪物模板不会被修改,不需要全量存储, +只需对比"参考状态"(游戏加载完成时的初始模板),仅保存发生了变化的模板。 + +为此需要: + +- `IEnemy` 和 `ISpecial` 继承 `ISaveableContent` 以支持自身序列化; +- 给 `ISpecial` 添加 `deepEqualsTo` 接口用于特殊属性间的深度比较; +- `IEnemyManager` 继承 `ISaveableContent`,新增 `compareWith`、`modifyPrefabAttribute`、 + `attachEnemyComparer`、`getEnemyComparer` 接口; +- `IEnemyManager` 内部维护 dirty 集合,以首次 `compareWith` 传入的参考为唯一基准; +- `getPrefab` / `getPrefabById` 返回值收窄为 `IReadonlyEnemy`, + 统一由 `modifyPrefabAttribute` 承担模板修改职责。 + +--- + +# 实现思路 + +## 1. 新增存档状态类型 + +在 `types.ts` 中新增如下类型,用于序列化怪物与管理器的状态: + +```ts +/** 单个 IEnemy 的存档状态 */ +interface IEnemySaveState { + readonly attrs: TAttr; + // 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 + readonly specials: ReadonlyMap; +} + +/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */ +interface IEnemyManagerSaveState { + // code -> 变更后的 IEnemySaveState + readonly modified: ReadonlyMap>; +} +``` + +## 2. 新增 `IEnemyComparer` 接口 + +由于管理器外部没有比较怪物属性的需求,将比较逻辑封装为独立的比较器, +附着在 `EnemyManager` 上。比较器接口如下: + +```ts +interface IEnemyComparer { + compare( + enemyA: IReadonlyEnemy, + enemyB: IReadonlyEnemy + ): boolean; +} +``` + +由用户在初始化时通过 `attachEnemyComparer` 提供。若未提供比较器, +在调用 `modifyPrefabAttribute` 或 `changePrefab` 时需发出警告,且视所有怪物均为脏。 + +## 3. `ISpecial` 继承 `ISaveableContent` + +- `saveState` 返回 `structuredClone(this.value)`(即 `getValue()` 的深拷贝); +- `loadState` 调用 `setValue(state)`; +- 新增 `deepEqualsTo(other: ISpecial): boolean`:先对比 `code`, + 再对 `value` 进行深度比较。 + +各内置实现类的比较策略: + +- `NonePropertySpecial`:只需比较 `code`,`value` 为 `void` 无需对比; +- `CommonSerializableSpecial`:`value` 为普通可序列化对象, + 使用 `lodash-es` 的 `isEqual` 进行递归深度比较。 + +## 4. `IEnemy` 继承 `ISaveableContent>` + +- `saveState(compression)`:深拷贝 `attrs`,对每个 special 调用 + `saveState(compression)` 收集到 `specials` Map,返回 `IEnemySaveState`; +- `loadState(state, compression)`:以 `state.attrs` 还原属性, + 然后对已有的每个 special 按 code 查找存档中的对应条目并调用 `loadState`; + 若存档中出现当前怪物未注册的 special code,发出 logger 警告并跳过。 + +## 5. `IEnemyManager` 接口修改 + +### 5a. 继承 `ISaveableContent>` + +- `saveState(compression)`:遍历 dirty 集合,对每个脏模板调用 + `prefab.saveState(compression)`,汇总为 `IEnemyManagerSaveState` 并返回; +- `loadState(state, compression)`:遍历 `state.modified`, + 找到 code 对应的现有模板,调用 `prefab.loadState(enemyState, compression)` 还原; + 若某 code 不在当前 prefab 表中,发出 logger 警告并跳过; + **不清空 dirty 集合**,始终以首次 `compareWith` 提供的参考为唯一基准; + `loadState` 结束后重新用比较器对每个已有脏模板进行比对, + 刷新 dirty 集合(避免加载后实际已恢复初始值的模板仍停留在 dirty 中)。 + +### 5b. 新增 `compareWith` + +```ts +compareWith(reference: ReadonlyMap>): void; +``` + +- 由调用方在游戏初始化完成后提供参考快照,外部传入,管理器保存引用; +- **首次调用**:直接存储参考,清空 dirty 集合; +- **非首次调用**:通过 logger 发出警告,提示此操作风险高, + 请作者确认操作意图,但仍然执行覆盖(直接替换参考,重置 dirty 集合)。 + +### 5c. `getPrefab` / `getPrefabById` 返回值改为 `IReadonlyEnemy` + +原来返回 `IEnemy`,外部可以直接修改模板。 +改为只读引用,外部不能直接修改,必须通过 `modifyPrefabAttribute` 完成。 + +### 5d. 新增 `modifyPrefabAttribute` + +```ts +modifyPrefabAttribute( + code: number | string, + modify: (prefab: IEnemy) => IEnemy +): 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): void; +getEnemyComparer(): IEnemyComparer | 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` 类型:单个怪物的存档状态 +- [ ] 新增 `IEnemyManagerSaveState` 类型:管理器的存档状态 +- [ ] 新增 `IEnemyComparer` 接口:包含 `compare` 方法,由用户实现 +- [ ] 修改 `ISpecial`:继承 `ISaveableContent`, + 新增 `deepEqualsTo(other: ISpecial): boolean` +- [ ] 修改 `IEnemy`:继承 `ISaveableContent>` +- [ ] 修改 `IEnemyManager`:继承 `ISaveableContent>`, + 新增 `compareWith`、`modifyPrefabAttribute`、`attachEnemyComparer`、`getEnemyComparer`; + 修改 `getPrefab` 与 `getPrefabById` 返回类型为 `IReadonlyEnemy` + +### `packages-user/data-base/src/enemy/enemy.ts`(`Enemy` 类) + +- [ ] 实现 `saveState(compression): IEnemySaveState` +- [ ] 实现 `loadState(state, compression): void` + +### `packages-user/data-base/src/enemy/manager.ts`(`EnemyManager` 类) + +- [ ] 新增 `private readonly dirtySet: Set` 成员:记录脏模板的 code +- [ ] 新增 `private referenceByCode: Map>` 成员: + 保存参考快照 +- [ ] 新增 `private comparer: IEnemyComparer | 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`) diff --git a/docs/dev/hero-modifier-save.md b/docs/dev/hero-modifier-save.md new file mode 100644 index 0000000..b5a87e1 --- /dev/null +++ b/docs/dev/hero-modifier-save.md @@ -0,0 +1,110 @@ +# 需求综述 + +当前勇士属性修饰器 `IHeroModifier` 是非注册式的,无法直接存档。 +目标是将其改为注册式,每种修饰器在使用前需先在属性对象上注册, +并实现 `ISaveableContent` 接口以支持存档读档。 + +注册接口签名:`registerModifier(identifier: string, cons: () => IHeroModifier): void` + +# 实现思路 + +## 1. 修改 `IHeroModifier` 接口 + +- 将 `identifier` 改为 `type`:一类修饰器可能被添加多次, + `type` 比 `identifier` 更准确地表达"修饰器类型"的含义。 +- 新增泛型参数 `S = unknown` 作为存档类型,让接口继承 `ISaveableContent`: + `IHeroModifier` +- 大多数修饰器的可变状态只有 `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` + +- 改为 `BaseHeroModifier` 隐式以 `S = V` 实现 `ISaveableContent` +- 将 `abstract readonly identifier: string` 改为 `abstract readonly type: string` +- `saveState()` 直接返回 `this.currentValue` +- `loadState(state)` 调用 `this.setValue(state)` 恢复值 + +## 6. 修改 `HeroState` + +修饰器注册表移至 `HeroState`: + +- 新增 `private readonly registry: Map IHeroModifier>` 存储工厂函数 +- 实现 `registerModifier(type, cons)`:向 `registry` 写入工厂, + 并同步注册到接口签名中 +- 实现 `createModifier(type)`:从 `registry` 取出对应工厂, + 调用工厂函数创建并返回修饰器实例,类型为 `IHeroModifier` +- 实现 `createAndInsertModifier(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` 接口: + 改为 `IHeroModifier`,`identifier` 改名为 `type`, + 继承 `ISaveableContent` +- [x] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式 +- [x] 修改 `IHeroStateSave`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段 +- [x] 修改 `IReadonlyHeroAttribute`:新增 `iterateModifiers()` 方法签名 +- [x] 修改 `IHeroState`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier(type: string): IHeroModifier` - `createAndInsertModifier(type: string, name: K): IHeroModifier` + +### `@user/data-base/hero/attribute.ts` + +- [x] 修改 `BaseHeroModifier`: + 将 `abstract readonly identifier` 改为 `abstract readonly type`; + 实现 `saveState` / `loadState` +- [x] 修改 `HeroAttribute`:实现 `iterateModifiers()` + +### `@user/data-base/hero/state.ts` + +- [x] 修改 `HeroState`:新增 `private readonly registry: Map 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` 重建修饰器并挂载 diff --git a/docs/dev/save-system.md b/docs/dev/save-system.md new file mode 100644 index 0000000..21e9e13 --- /dev/null +++ b/docs/dev/save-system.md @@ -0,0 +1,215 @@ +# 需求综述 + +实现游戏引擎存档系统(`SaveSystem`)及全局事务(`GlobalTransaction`)两个类的完整逻辑。 +存档系统分为两部分: + +- **存档内容**:按 slot id 保存/读取游戏当前状态(`Map>`), + 使用 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 }`, + 与 `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; +} +``` + +- `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`,构建 `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`, + 构建 `{ 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(key)` / `setGlobal(key, value)` + +- `getGlobal`:从 Dexie `global` 表读取 `key` 对应的 `value` 字段并返回,类型断言为 `T`; +- `setGlobal`:将 `{ key, value }` 直接写入 Dexie `global` 表,无需 JSON 序列化。 + +### `startGlobalTransaction(handle)` + +使用 `Dexie.transaction('rw', this.db.table('global'), ...)` 包裹 `handle` 调用, +传入 `GlobalTransaction` 实例,出错时自动回滚。 + +### `GlobalTransaction.get(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 序列化 diff --git a/packages-user/client-modules/src/render/elements/props.ts b/packages-user/client-modules/src/render/elements/props.ts index ce4da18..a3c7a7d 100644 --- a/packages-user/client-modules/src/render/elements/props.ts +++ b/packages-user/client-modules/src/render/elements/props.ts @@ -1,6 +1,6 @@ import { BaseProps, TagDefine } from '@motajs/render-vue'; import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render'; -import { ILayerState } from '@user/data-state'; +import { ILayerState } from '@user/data-base'; import { IMapExtensionManager, IMapRenderer } from '../map'; export interface IconProps extends BaseProps { diff --git a/packages-user/client-modules/src/render/map/element.ts b/packages-user/client-modules/src/render/map/element.ts index 11487d8..a2352dd 100644 --- a/packages-user/client-modules/src/render/map/element.ts +++ b/packages-user/client-modules/src/render/map/element.ts @@ -1,5 +1,5 @@ import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render'; -import { ILayerState } from '@user/data-state'; +import { ILayerState } from '@user/data-base'; import { IMapRenderer } from './types'; import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared'; diff --git a/packages-user/client-modules/src/render/map/extension/door.ts b/packages-user/client-modules/src/render/map/extension/door.ts index ce08fc5..7420ddc 100644 --- a/packages-user/client-modules/src/render/map/extension/door.ts +++ b/packages-user/client-modules/src/render/map/extension/door.ts @@ -2,7 +2,7 @@ import { IMapLayer, IMapLayerHookController, IMapLayerHooks -} from '@user/data-state'; +} from '@user/data-base'; import { IMapDoorRenderer } from './types'; import { IMapRenderer } from '../types'; import { sleep } from 'mutate-animate'; diff --git a/packages-user/client-modules/src/render/map/extension/hero.ts b/packages-user/client-modules/src/render/map/extension/hero.ts index 039ed77..e28d05a 100644 --- a/packages-user/client-modules/src/render/map/extension/hero.ts +++ b/packages-user/client-modules/src/render/map/extension/hero.ts @@ -7,7 +7,7 @@ import { IHeroMovingHooks, nextFaceDirection } from '@user/data-base'; -import { IMapLayer, state } from '@user/data-state'; +import { IMapLayer } from '@user/data-base'; import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types'; import { isNil } from 'lodash-es'; 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 { IMapHeroRenderer } from './types'; import { TimingFn } from 'mutate-animate'; +import { state } from '@user/data-state'; /** 默认的移动时长 */ const DEFAULT_TIME = 100; diff --git a/packages-user/client-modules/src/render/map/extension/manager.ts b/packages-user/client-modules/src/render/map/extension/manager.ts index 41c6481..4a01b30 100644 --- a/packages-user/client-modules/src/render/map/extension/manager.ts +++ b/packages-user/client-modules/src/render/map/extension/manager.ts @@ -1,5 +1,5 @@ import { IHeroMover } from '@user/data-base'; -import { IMapLayer } from '@user/data-state'; +import { IMapLayer } from '@user/data-base'; import { IMapDoorRenderer, IMapExtensionManager, diff --git a/packages-user/client-modules/src/render/map/extension/types.ts b/packages-user/client-modules/src/render/map/extension/types.ts index 82577d0..bd685cc 100644 --- a/packages-user/client-modules/src/render/map/extension/types.ts +++ b/packages-user/client-modules/src/render/map/extension/types.ts @@ -4,7 +4,7 @@ import { HeroAnimateDirection, IHeroMover } from '@user/data-base'; -import { IMapLayer } from '@user/data-state'; +import { IMapLayer } from '@user/data-base'; import { IMapRenderResult } from '../types'; diff --git a/packages-user/client-modules/src/render/map/moving.ts b/packages-user/client-modules/src/render/map/moving.ts index 8c89b9b..ea464cb 100644 --- a/packages-user/client-modules/src/render/map/moving.ts +++ b/packages-user/client-modules/src/render/map/moving.ts @@ -2,7 +2,7 @@ import { linear, TimingFn } from 'mutate-animate'; import { IMapRenderer, IMapVertexGenerator, IMovingBlock } from './types'; import { IMaterialFramedData, IMaterialManager } from '@user/client-base'; import { logger } from '@motajs/common'; -import { IMapLayer } from '@user/data-state'; +import { IMapLayer } from '@user/data-base'; import { DynamicBlockStatus } from './status'; export interface IMovingRenderer { diff --git a/packages-user/client-modules/src/render/map/renderer.ts b/packages-user/client-modules/src/render/map/renderer.ts index df54f0d..6292c82 100644 --- a/packages-user/client-modules/src/render/map/renderer.ts +++ b/packages-user/client-modules/src/render/map/renderer.ts @@ -30,7 +30,7 @@ import { MapTileBehavior, MapTileSizeTestMode } 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 { compileProgramWith } from '@motajs/client-base'; import { isNil, maxBy } from 'lodash-es'; diff --git a/packages-user/client-modules/src/render/map/status.ts b/packages-user/client-modules/src/render/map/status.ts index 922b14b..979abaa 100644 --- a/packages-user/client-modules/src/render/map/status.ts +++ b/packages-user/client-modules/src/render/map/status.ts @@ -1,4 +1,4 @@ -import { IMapLayer } from '@user/data-state'; +import { IMapLayer } from '@user/data-base'; import { IBlockStatus, IMapVertexStatus } from './types'; export class StaticBlockStatus implements IBlockStatus { diff --git a/packages-user/client-modules/src/render/map/types.ts b/packages-user/client-modules/src/render/map/types.ts index 8b93aa0..438586c 100644 --- a/packages-user/client-modules/src/render/map/types.ts +++ b/packages-user/client-modules/src/render/map/types.ts @@ -10,7 +10,7 @@ import { IMaterialManager, ITrackedAssetData } from '@user/client-base'; -import { ILayerState, IMapLayer } from '@user/data-state'; +import { ILayerState, IMapLayer } from '@user/data-base'; import { TimingFn } from 'mutate-animate'; export const enum MapBackgroundRepeat { diff --git a/packages-user/client-modules/src/render/map/vertex.ts b/packages-user/client-modules/src/render/map/vertex.ts index e9bb9a5..a8ca1af 100644 --- a/packages-user/client-modules/src/render/map/vertex.ts +++ b/packages-user/client-modules/src/render/map/vertex.ts @@ -1,4 +1,4 @@ -import { IMapLayer } from '@user/data-state'; +import { IMapLayer } from '@user/data-base'; import { IBlockData, IBlockSplitter, diff --git a/packages-user/data-state/src/common/face.ts b/packages-user/data-base/src/common/face.ts similarity index 100% rename from packages-user/data-state/src/common/face.ts rename to packages-user/data-base/src/common/face.ts diff --git a/packages-user/data-base/src/common/index.ts b/packages-user/data-base/src/common/index.ts index 6d5a6ef..5fcbbcb 100644 --- a/packages-user/data-base/src/common/index.ts +++ b/packages-user/data-base/src/common/index.ts @@ -1,2 +1,3 @@ +export * from './face'; export * from './types'; export * from './utils'; diff --git a/packages-user/data-base/src/common/types.ts b/packages-user/data-base/src/common/types.ts index 196a18f..fbe4822 100644 --- a/packages-user/data-base/src/common/types.ts +++ b/packages-user/data-base/src/common/types.ts @@ -16,3 +16,67 @@ export interface IFaceData { /** 图块朝向 */ 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 { + /** + * 保存对象状态,返回的对象应该经过深拷贝(即 `structuedClone`) + * @param compression 压缩级别 + */ + saveState(compression: SaveCompression): T; + + /** + * 读取对象状态 + * @param state 状态对象 + * @param compression 压缩级别 + */ + loadState(state: T, compression: SaveCompression): void; +} + +//#endregion diff --git a/packages-user/data-base/src/enemy/enemy.ts b/packages-user/data-base/src/enemy/enemy.ts index 501da36..189e2f1 100644 --- a/packages-user/data-base/src/enemy/enemy.ts +++ b/packages-user/data-base/src/enemy/enemy.ts @@ -2,10 +2,12 @@ import { logger } from '@motajs/common'; import { IEnemy, IEnemyContext, + IEnemySaveState, IReadonlyEnemy, ISpecial, IEnemyView } from './types'; +import { SaveCompression } from '../common/types'; export class Enemy implements IEnemy { /** 怪物身上的特殊属性列表 */ @@ -87,6 +89,29 @@ export class Enemy implements IEnemy { this.addSpecial(special.clone()); } } + + saveState(_compression: SaveCompression): IEnemySaveState { + const specials: Map = new Map(); + for (const special of this.specials) { + specials.set(special.code, special.saveState(_compression)); + } + return { attrs: structuredClone(this.attributes), specials }; + } + + loadState( + state: IEnemySaveState, + 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 implements IEnemyView { diff --git a/packages-user/data-base/src/enemy/manager.ts b/packages-user/data-base/src/enemy/manager.ts index 2f69c34..915bd56 100644 --- a/packages-user/data-base/src/enemy/manager.ts +++ b/packages-user/data-base/src/enemy/manager.ts @@ -2,10 +2,15 @@ import { logger } from '@motajs/common'; import { Enemy as EnemyImpl } from './enemy'; import { IEnemy, + IEnemyComparer, IEnemyManager, + IEnemyManagerSaveState, IEnemyLegacyBridge, - SpecialCreation + IReadonlyEnemy, + SpecialCreation, + IEnemySaveState } from './types'; +import { SaveCompression } from '../common/types'; export class EnemyManager implements IEnemyManager { /** 特殊属性注册表,code -> 创建函数 */ @@ -19,6 +24,18 @@ export class EnemyManager implements IEnemyManager { private readonly prefabById: Map> = new Map(); /** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */ private readonly legacyIdToCode: Map = new Map(); + /** 复用映射,reusedCode -> sourceCode */ + private readonly reuseByCode: Map = new Map(); + /** 复用映射,reusedId -> sourceId */ + private readonly reuseById: Map = new Map(); + /** 脏模板集合,存储发生了变化的模板 code */ + private readonly dirtySet: Set = new Set(); + /** 参考快照,code -> IReadonlyEnemy,由 compareWith 提供 */ + private referenceByCode: Map> = new Map(); + /** 当前附加的怪物比较器 */ + private comparer: IEnemyComparer | null = null; + /** 是否已首次调用 compareWith */ + private hasReference: boolean = false; constructor(readonly bridge: IEnemyLegacyBridge) {} @@ -111,9 +128,11 @@ export class EnemyManager implements IEnemyManager { private internalGetPrefab(code: number | string) { if (typeof code === 'number') { - return this.prefabByCode.get(code) ?? null; + const sourceCode = this.reuseByCode.get(code) ?? code; + return this.prefabByCode.get(sourceCode) ?? null; } 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 implements IEnemyManager { const cloned = enemy.clone(); this.prefabByCode.set(enemy.code, cloned); this.prefabById.set(enemy.id, cloned); + this.updateDirty(cloned.code, cloned); } addPrefabFromLegacy(code: number, enemy: Enemy): void { @@ -137,14 +157,17 @@ export class EnemyManager implements IEnemyManager { this.prefabByCode.set(code, prefab); this.prefabById.set(prefab.id, prefab); this.legacyIdToCode.set(enemy.id, code); + this.updateDirty(code, prefab); } - getPrefab(code: number): IEnemy | null { - return this.prefabByCode.get(code) ?? null; + getPrefab(code: number): IReadonlyEnemy | null { + const sourceCode = this.reuseByCode.get(code) ?? code; + return this.prefabByCode.get(sourceCode) ?? null; } - getPrefabById(id: string): IEnemy | null { - return this.prefabById.get(id) ?? null; + getPrefabById(id: string): IReadonlyEnemy | null { + const sourceId = this.reuseById.get(id) ?? id; + return this.prefabById.get(sourceId) ?? null; } deletePrefab(code: number | string): void { @@ -160,12 +183,126 @@ export class EnemyManager implements IEnemyManager { // 再添加新的模板 this.prefabByCode.set(enemy.code, enemy); this.prefabById.set(enemy.id, enemy); + this.updateDirty(enemy.code, enemy); } reusePrefab(source: number | string, code: number, id: string): void { const prefab = this.internalGetPrefab(source); if (!prefab) return; - this.prefabByCode.set(code, prefab); - this.prefabById.set(id, prefab); + this.reuseByCode.set(code, prefab.code); + this.reuseById.set(id, prefab.id); + } + + compareWith(reference: ReadonlyMap>): 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) => IEnemy + ): 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): void { + this.comparer = comparer; + } + + getEnemyComparer(): IEnemyComparer | null { + return this.comparer; + } + + saveState(compression: SaveCompression): IEnemyManagerSaveState { + const modified: Map> = 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, + 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): 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): 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); + } + } } } diff --git a/packages-user/data-base/src/enemy/special.ts b/packages-user/data-base/src/enemy/special.ts index 5072728..59c1083 100644 --- a/packages-user/data-base/src/enemy/special.ts +++ b/packages-user/data-base/src/enemy/special.ts @@ -1,3 +1,5 @@ +import { isEqual } from 'lodash-es'; +import { SaveCompression } from '../common/types'; import { ISpecial, SpecialCreation } from './types'; // TODO: 颜色参数 @@ -45,6 +47,19 @@ export class CommonSerializableSpecial implements ISpecial { this.config ); } + + saveState(_compression: SaveCompression): T { + return structuredClone(this.value); + } + + loadState(state: T, _compression: SaveCompression): void { + this.setValue(state); + } + + deepEqualsTo(other: ISpecial): boolean { + if (this.code !== other.code) return false; + return isEqual(this.value, other.getValue()); + } } export class NonePropertySpecial implements ISpecial { @@ -78,6 +93,18 @@ export class NonePropertySpecial implements ISpecial { clone(): ISpecial { return new NonePropertySpecial(this.code, this.config); } + + saveState(_compression: SaveCompression): void { + return undefined; + } + + loadState(_state: void, _compression: SaveCompression): void { + // 无属性,无需操作 + } + + deepEqualsTo(other: ISpecial): boolean { + return this.code === other.code; + } } export function defineCommonSerializableSpecial( diff --git a/packages-user/data-base/src/enemy/types.ts b/packages-user/data-base/src/enemy/types.ts index 210381a..849a7a2 100644 --- a/packages-user/data-base/src/enemy/types.ts +++ b/packages-user/data-base/src/enemy/types.ts @@ -1,9 +1,36 @@ import { IRange, ITileLocator } from '@motajs/common'; import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero'; +import { ISaveableContent } from '../common/types'; //#region 怪物基础 -export interface ISpecial { +/** 单个 IEnemy 的存档状态 */ +export interface IEnemySaveState { + /** 怪物属性的深拷贝 */ + readonly attrs: TAttr; + /** 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 */ + readonly specials: ReadonlyMap; +} + +/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */ +export interface IEnemyManagerSaveState { + /** code -> 变更后的 IEnemySaveState,仅包含脏模板 */ + readonly modified: ReadonlyMap>; +} + +export interface IEnemyComparer { + /** + * 比较两个怪物是否完全相同 + * @param enemyA 怪物 A + * @param enemyB 怪物 B + */ + compare( + enemyA: IReadonlyEnemy, + enemyB: IReadonlyEnemy + ): boolean; +} + +export interface ISpecial extends ISaveableContent { /** 特殊属性代码 */ readonly code: number; /** 特殊属性需要的数值 */ @@ -40,6 +67,12 @@ export interface ISpecial { * 深拷贝此特殊属性 */ clone(): ISpecial; + + /** + * 深度比较此特殊属性与另一特殊属性是否相同 + * @param other 另一特殊属性 + */ + deepEqualsTo(other: ISpecial): boolean; } export interface IReadonlyEnemy { @@ -82,7 +115,8 @@ export interface IReadonlyEnemy { clone(): IReadonlyEnemy; } -export interface IEnemy extends IReadonlyEnemy { +export interface IEnemy + extends IReadonlyEnemy, ISaveableContent> { /** * 添加特殊属性 * @param special 特殊属性对象 @@ -138,7 +172,9 @@ export interface IEnemyLegacyBridge { fromLegacyEnemy(enemy: Enemy, defaultValue: Partial): TAttr; } -export interface IEnemyManager { +export interface IEnemyManager extends ISaveableContent< + IEnemyManagerSaveState +> { /** * 注册一个特殊属性 * @param code 特殊属性代码 @@ -193,13 +229,13 @@ export interface IEnemyManager { * 获取指定怪物的模板 * @param code 怪物图块数字 */ - getPrefab(code: number): IEnemy | null; + getPrefab(code: number): IReadonlyEnemy | null; /** * 根据怪物的 `id` 获取对应的怪物模板 * @param id 怪物 `id` */ - getPrefabById(id: string): IEnemy | null; + getPrefabById(id: string): IReadonlyEnemy | null; /** * 删除指定的怪物模板 @@ -221,6 +257,34 @@ export interface IEnemyManager { * @param id 复用怪物 id */ reusePrefab(source: number | string, code: number, id: string): void; + + /** + * 设置参考快照,后续对模板的修改将与此比较以确定是否脏。 + * 非首次调用时会发出警告,但仍执行覆盖 + * @param reference code -> 参考怪物的 Map + */ + compareWith(reference: ReadonlyMap>): void; + + /** + * 修改指定怪物模板的属性,修改完成后自动与参考模板比较并更新 dirty 集合 + * @param code 怪物的图块数字或 `id` + * @param modify 修改函数,传入可写怪物对象,返回修改后的对象 + */ + modifyPrefabAttribute( + code: number | string, + modify: (prefab: IEnemy) => IEnemy + ): void; + + /** + * 附加怪物比较器,用于 dirty 集合的判断 + * @param comparer 比较器对象 + */ + attachEnemyComparer(comparer: IEnemyComparer): void; + + /** + * 获取当前附加的怪物比较器,如未设置则返回 `null` + */ + getEnemyComparer(): IEnemyComparer | null; } //#endregion diff --git a/packages-user/data-base/src/flag/system.ts b/packages-user/data-base/src/flag/system.ts index 7063691..ed98767 100644 --- a/packages-user/data-base/src/flag/system.ts +++ b/packages-user/data-base/src/flag/system.ts @@ -2,7 +2,7 @@ import { IFlagCommonField, IFlagSystem, IFlagSystemSave } from './types'; import { FlagCommonField } from './field'; export class FlagSystem implements IFlagSystem { - private readonly fieldMap: Map> = + private readonly fieldMap: Map> = new Map(); occupied(field: PropertyKey): boolean { diff --git a/packages-user/data-base/src/flag/types.ts b/packages-user/data-base/src/flag/types.ts index 7f77a28..a7cc428 100644 --- a/packages-user/data-base/src/flag/types.ts +++ b/packages-user/data-base/src/flag/types.ts @@ -1,5 +1,7 @@ //#region 字段 +import { ISaveableContent } from '../common'; + export interface IFlagCommonField { /** 此字段所处的 Flag 系统 */ readonly system: IFlagSystem; @@ -42,7 +44,7 @@ export interface IFlagSystemSave { readonly fields: Map; } -export interface IFlagSystem { +export interface IFlagSystem extends ISaveableContent { /** * 判断一个字段是否被占用,类似于旧样板的 `core.hasFlag` * @param field 字段名称 @@ -131,17 +133,6 @@ export interface IFlagSystem { * @param defaultValue 字段默认值 */ getFieldValueDefaults(field: PropertyKey, defaultValue: T): T; - - /** - * 对 Flag 系统进行结构化复制,形成存档对象 - */ - saveState(): IFlagSystemSave; - - /** - * 从指定存档对象读取信息 - * @param state 存档对象 - */ - loadState(state: IFlagSystemSave): void; } //#endregion diff --git a/packages-user/data-base/src/hero/attribute.ts b/packages-user/data-base/src/hero/attribute.ts index e58c2ad..6a1f6f0 100644 --- a/packages-user/data-base/src/hero/attribute.ts +++ b/packages-user/data-base/src/hero/attribute.ts @@ -1,7 +1,9 @@ import { logger } from '@motajs/common'; +import { SaveCompression } from '../common'; import { IHeroAttribute, IHeroModifier } from './types'; -export abstract class BaseHeroModifier implements IHeroModifier { +export abstract class BaseHeroModifier implements IHeroModifier { + abstract readonly type: string; abstract readonly priority: number; owner: IHeroAttribute | null = null; @@ -25,6 +27,14 @@ export abstract class BaseHeroModifier implements IHeroModifier { 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; @@ -161,4 +171,14 @@ export class HeroAttribute implements IHeroAttribute { getModifiableClone(): IHeroAttribute { return this.clone(); } + + toStructured(): THero { + return structuredClone(this.attribute); + } + + *iterateModifiers(): IterableIterator<[keyof THero, IHeroModifier]> { + for (const [modifier, name] of this.modifierName) { + yield [name, modifier]; + } + } } diff --git a/packages-user/data-base/src/hero/state.ts b/packages-user/data-base/src/hero/state.ts index a202710..37a690d 100644 --- a/packages-user/data-base/src/hero/state.ts +++ b/packages-user/data-base/src/hero/state.ts @@ -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 implements IHeroState { + /** 修饰器工厂函数注册表 */ + private readonly registry: Map IHeroModifier> = new Map(); + constructor( public mover: IHeroMover, public attribute: IHeroAttribute @@ -34,4 +43,74 @@ export class HeroState implements IHeroState { getIsolatedAttribute(): IHeroAttribute { return this.attribute.getModifiableClone(); } + + registerModifier(type: string, cons: () => IHeroModifier): void { + this.registry.set(type, cons); + } + + createModifier(type: string): IHeroModifier | null { + const cons = this.registry.get(type); + if (!cons) { + logger.warn(116, type); + return null; + } else { + return cons() as IHeroModifier; + } + } + + createAndInsertModifier( + type: string, + name: K + ): IHeroModifier | null { + const modifier = this.createModifier(type); + if (!modifier) return null; + this.attribute.addModifier(name, modifier); + return modifier; + } + + saveState(compression: SaveCompression): IHeroStateSave { + 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, + compression: SaveCompression + ): void { + const newAttribute = new HeroAttribute(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 + ); + } + 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); + }); + } } diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index c87daaa..a35902b 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -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 { +export interface IHeroModifier< + T = unknown, + V = unknown, + S = unknown +> extends ISaveableContent { + /** 修饰器类型 */ + readonly type: string; /** 修饰器优先级 */ readonly priority: number; /** 修饰器参数值 */ @@ -42,6 +48,15 @@ export interface IHeroModifier { clone(): IHeroModifier; } +export interface IModifierStateSave { + /** 属性名称 */ + readonly name: PropertyKey; + /** 修饰器类型 */ + readonly type: string; + /** 修饰器存档数据 */ + readonly state: unknown; +} + export interface IReadonlyHeroAttribute { /** * 获取勇士的基础属性,即未经过任何 Buff 或装备等加成的属性 @@ -77,6 +92,16 @@ export interface IReadonlyHeroAttribute { * 获取此勇士属性对象的可修改副本 */ getModifiableClone(): IHeroAttribute; + + /** + * 转换为结构化对象 + */ + toStructured(): THero; + + /** + * 遍历所有已挂载的属性修饰器 + */ + iterateModifiers(): Iterable<[PropertyKey, IHeroModifier]>; } export interface IHeroAttribute extends IReadonlyHeroAttribute { @@ -332,7 +357,20 @@ export interface IHeroMover extends IHookable { //#region 勇士状态 -export interface IHeroState { +export interface IHeroStateSave { + /** 勇士属性状态 */ + readonly attribute: THero; + /** 勇士当前位置 */ + readonly locator: IFacedTileLocator; + /** 勇士当前的跟随者 */ + readonly followers: readonly Readonly[]; + /** 勇士属性修饰器状态 */ + readonly modifiers: readonly IModifierStateSave[]; +} + +export interface IHeroState extends ISaveableContent< + IHeroStateSave +> { /** 勇士移动对象 */ readonly mover: IHeroMover; /** 勇士属性对象 */ @@ -369,6 +407,29 @@ export interface IHeroState { * 获取独立勇士属性对象,修改此对象不会影响勇士本身的属性 */ getIsolatedAttribute(): IHeroAttribute; + + /** + * 注册一个修饰器工厂函数 + * @param type 修饰器类型 + * @param cons 工厂函数 + */ + registerModifier(type: string, cons: () => IHeroModifier): void; + + /** + * 创建指定类型的修饰器实例 + * @param type 修饰器类型 + */ + createModifier(type: string): IHeroModifier | null; + + /** + * 创建指定类型的修饰器实例并插入至勇士属性对象 + * @param type 修饰器类型 + * @param name 属性名称 + */ + createAndInsertModifier( + type: string, + name: K + ): IHeroModifier | null; } //#endregion diff --git a/packages-user/data-base/src/index.ts b/packages-user/data-base/src/index.ts index 09b3249..0be7276 100644 --- a/packages-user/data-base/src/index.ts +++ b/packages-user/data-base/src/index.ts @@ -3,5 +3,7 @@ export * from './enemy'; export * from './flag'; export * from './hero'; export * from './load'; +export * from './map'; export * from './game'; +export * from './types'; diff --git a/packages-user/data-state/src/map/index.ts b/packages-user/data-base/src/map/index.ts similarity index 100% rename from packages-user/data-state/src/map/index.ts rename to packages-user/data-base/src/map/index.ts diff --git a/packages-user/data-state/src/map/layerState.ts b/packages-user/data-base/src/map/layerState.ts similarity index 100% rename from packages-user/data-state/src/map/layerState.ts rename to packages-user/data-base/src/map/layerState.ts diff --git a/packages-user/data-state/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts similarity index 100% rename from packages-user/data-state/src/map/mapLayer.ts rename to packages-user/data-base/src/map/mapLayer.ts diff --git a/packages-user/data-state/src/map/types.ts b/packages-user/data-base/src/map/types.ts similarity index 100% rename from packages-user/data-state/src/map/types.ts rename to packages-user/data-base/src/map/types.ts diff --git a/packages-user/data-base/src/types.ts b/packages-user/data-base/src/types.ts new file mode 100644 index 0000000..bd52a39 --- /dev/null +++ b/packages-user/data-base/src/types.ts @@ -0,0 +1,43 @@ +import { IHeroFollower, IHeroState } from './hero'; +import { IEnemyManager } from './enemy'; +import { IFlagSystem } from './flag'; +import { IRoleFaceBinder, ISaveableContent } from './common'; +import { ILayerState } from './map'; + +export interface IStateSaveData { + /** 跟随者列表 */ + readonly followers: readonly IHeroFollower[]; +} + +export interface IStateBase { + /** 朝向绑定 */ + readonly roleFace: IRoleFaceBinder; + /** id 到图块数字的映射 */ + readonly idNumberMap: Map; + /** 图块数字到 id 的映射 */ + readonly numberIdMap: Map; + + /** 地图状态 */ + readonly layer: ILayerState; + /** 勇士状态 */ + readonly hero: IHeroState; + + /** 怪物管理器 */ + readonly enemyManager: IEnemyManager; + + /** Flag 系统 */ + readonly flags: IFlagSystem; + + /** + * 添加可存档对象,添加后系统将会自动在存档时将对象存储 + * @param id 可存档对象的 id + * @param content 可存档对象 + */ + addSaveableContent(id: string, content: ISaveableContent): void; + + /** + * 根据 id 获取对应的可存档对象 + * @param id 可存档对象的 id + */ + getSaveableContent(id: string): ISaveableContent | null; +} diff --git a/packages-user/data-state/src/common/index.ts b/packages-user/data-state/src/common/index.ts deleted file mode 100644 index 5fcbbcb..0000000 --- a/packages-user/data-state/src/common/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './face'; -export * from './types'; -export * from './utils'; diff --git a/packages-user/data-state/src/common/types.ts b/packages-user/data-state/src/common/types.ts deleted file mode 100644 index b256db0..0000000 --- a/packages-user/data-state/src/common/types.ts +++ /dev/null @@ -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; -} diff --git a/packages-user/data-state/src/common/utils.ts b/packages-user/data-state/src/common/utils.ts deleted file mode 100644 index 2e7a185..0000000 --- a/packages-user/data-state/src/common/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - degradeFace, - fromDirectionString, - getFaceMovement, - nextFaceDirection -} from '@user/data-base'; diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 4f1b491..d1eaa10 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -1,6 +1,4 @@ -import { ICoreState, IStateSaveData } from './types'; -import { ILayerState, LayerState } from './map'; -import { FaceDirection, IRoleFaceBinder, RoleFaceBinder } from './common'; +import { ICoreState, ISaveableExecutor } from './types'; import { DamageSystem, EnemyContext, @@ -16,9 +14,18 @@ import { FlagSystem, IMotaDataLoader, MotaDataLoader, - loading + loading, + IRoleFaceBinder, + ILayerState, + LayerState, + RoleFaceBinder, + FaceDirection, + ISaveableContent, + IStateSaveData, + SaveCompression, + IReadonlyEnemy } from '@user/data-base'; -import { IEnemyAttr } from './enemy/types'; +import { IEnemyAttr } from './enemy'; import { CommonAuraConverter, EnemyLegacyBridge, @@ -33,23 +40,39 @@ 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'; +import { MainEnemyComparer } from './enemy/comparer'; export class CoreState implements ICoreState { + // 全局内容 readonly roleFace: IRoleFaceBinder; readonly idNumberMap: Map; readonly numberIdMap: Map; - readonly loadProgress: ILoadProgressTotal; - readonly dataLoader: IMotaDataLoader; - + // 可存档内容 readonly layer: ILayerState; readonly hero: IHeroState; - readonly enemyManager: IEnemyManager; - readonly enemyContext: IEnemyContext; - readonly flags: IFlagSystem; + // 状态内容 + readonly loadProgress: ILoadProgressTotal; + readonly dataLoader: IMotaDataLoader; + readonly enemyContext: IEnemyContext; + readonly saveSystem: ISaveSystem; + + /** 可存档对象映射 */ + private readonly saveables: Map> = new Map(); + /** 所有已添加的可存档对象 */ + private readonly addedSaveables: Set> = new Set(); + /** 已绑定的存档执行器 */ + private readonly executors: Map< + ISaveableContent, + ISaveableExecutor + > = new Map(); + constructor() { this.layer = new LayerState(); this.roleFace = new RoleFaceBinder(); @@ -71,7 +94,9 @@ export class CoreState implements ICoreState { //#region 怪物初始化 // 怪物管理器初始化 + const comparer = new MainEnemyComparer(); const enemyManager = new EnemyManager(new EnemyLegacyBridge()); + enemyManager.attachEnemyComparer(comparer); enemyManager.setAttributeDefaults('hp', 0); enemyManager.setAttributeDefaults('atk', 0); enemyManager.setAttributeDefaults('def', 0); @@ -98,6 +123,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(); @@ -107,6 +151,8 @@ export class CoreState implements ICoreState { this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80); }); + this.addSaveableContent('flags', this.flags); + //#endregion } @@ -117,6 +163,7 @@ export class CoreState implements ICoreState { private initEnemyManager(data: Record) { // TODO: 修改怪物模板并存入存档,即 core.setEnemy const manager = this.enemyManager; + const reference = new Map>(); for (const [id, enemy] of Object.entries(structuredClone(data))) { const num = this.idNumberMap.get(id); if (isNil(num)) continue; @@ -127,7 +174,9 @@ export class CoreState implements ICoreState { const upCode = this.idNumberMap.get(up)!; const rightCode = this.idNumberMap.get(right)!; 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.bind(leftCode, downCode, FaceDirection.Left); this.roleFace.bind(upCode, downCode, FaceDirection.Up); @@ -136,9 +185,42 @@ export class CoreState implements ICoreState { manager.reusePrefab(num, upCode, up); manager.reusePrefab(num, rightCode, right); } else { - manager.addPrefabFromLegacy(num, enemy); + const prefab = manager.fromLegacyEnemy(num, enemy); + reference.set(num, prefab); + manager.addPrefab(prefab); } } + manager.compareWith(reference); + } + + addSaveableContent(id: string, content: ISaveableContent): void { + if (this.saveables.has(id)) { + logger.warn(112, id); + return; + } + this.saveables.set(id, content); + } + + getSaveableContent(id: string): ISaveableContent | null { + const content = this.saveables.get(id); + return (content as ISaveableContent) ?? null; + } + + bindSaveableExecuter( + content: ISaveableContent | string, + executor: ISaveableExecutor + ): 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 { diff --git a/packages-user/data-state/src/enemy/comparer.ts b/packages-user/data-state/src/enemy/comparer.ts new file mode 100644 index 0000000..a04bef7 --- /dev/null +++ b/packages-user/data-state/src/enemy/comparer.ts @@ -0,0 +1,31 @@ +import { IEnemyComparer, IReadonlyEnemy } from '@user/data-base'; +import { IEnemyAttr } from './types'; + +export class MainEnemyComparer implements IEnemyComparer { + compare( + enemyA: IReadonlyEnemy, + enemyB: IReadonlyEnemy + ): 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; + } +} diff --git a/packages-user/data-state/src/enemy/index.ts b/packages-user/data-state/src/enemy/index.ts index c923f4c..5998ef6 100644 --- a/packages-user/data-state/src/enemy/index.ts +++ b/packages-user/data-state/src/enemy/index.ts @@ -5,3 +5,4 @@ export * from './final'; export * from './legacy'; export * from './mapDamage'; export * from './special'; +export * from './types'; diff --git a/packages-user/data-state/src/index.ts b/packages-user/data-state/src/index.ts index aa2296a..bc01750 100644 --- a/packages-user/data-state/src/index.ts +++ b/packages-user/data-state/src/index.ts @@ -1,6 +1,5 @@ -import { loading } from '@user/data-base'; +import { FaceDirection, loading } from '@user/data-base'; import { isNil } from 'lodash-es'; -import { FaceDirection } from './common'; import { ICoreState } from './types'; import { TILE_HEIGHT, TILE_WIDTH } from './shared'; import { state } from './ins'; @@ -62,11 +61,9 @@ export function create() { }); } -export * from './common'; export * from './enemy'; export * from './hero'; export * from './legacy'; -export * from './map'; export * from './core'; export * from './ins'; diff --git a/packages-user/data-state/src/legacy/move.ts b/packages-user/data-state/src/legacy/move.ts index 51e4509..48c3aa3 100644 --- a/packages-user/data-state/src/legacy/move.ts +++ b/packages-user/data-state/src/legacy/move.ts @@ -1,10 +1,9 @@ import EventEmitter from 'eventemitter3'; import { backDir, toDir } from './utils'; -import { loading } from '@user/data-base'; -import type { RenderAdapter } from '@motajs/render'; +import { fromDirectionString, loading } from '@user/data-base'; import type { HeroKeyMover } from '@user/client-modules'; import { sleep } from '@motajs/common'; -import { fromDirectionString, state } from '..'; +import { state } from '..'; // todo: 转身功能 diff --git a/packages-user/data-state/src/save/index.ts b/packages-user/data-state/src/save/index.ts new file mode 100644 index 0000000..fcb073f --- /dev/null +++ b/packages-user/data-state/src/save/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/packages-user/data-state/src/save/system.ts b/packages-user/data-state/src/save/system.ts new file mode 100644 index 0000000..8b5fecd --- /dev/null +++ b/packages-user/data-state/src/save/system.ts @@ -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; +} + +interface IGlobalRecord { + /** 全局存储的键名 */ + readonly key: string; + /** 全局存储的内容 */ + readonly value: unknown; +} + +export class GlobalTransaction implements IGlobalTrasaction { + constructor(readonly table: Table) {} + + async get(key: string): Promise { + const record = await this.table.get(key); + return record!.value as T; + } + + async set(key: string, value: unknown): Promise { + 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>): 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> + ): ISaveRead | null { + if (this.undoStack.length === 0) return null; + const data = new Map(); + 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> + ): ISaveRead | null { + if (this.redoStack.length === 0) return null; + const data = new Map(); + 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>): void { + const data = new Map(); + 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 { + if (this.undoStack.length === 0) return; + const t0 = performance.now(); + const top = this.undoStack[this.undoStack.length - 1]; + const table = this.db.table('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> + ): Promise { + const t0 = performance.now(); + const data = new Map(); + for (const [key, content] of state) { + data.set(key, content.saveState(this.commonSaveLevel)); + } + const table = this.db.table('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 { + const table = this.db.table('saves'); + const record = await table.get(id); + if (record === undefined) return null; + return { compression: record.compression, data: record.data }; + } + + async deleteSave(id: number): Promise { + const table = this.db.table('saves'); + await table.delete(id); + } + + async getLastSlot(): Promise { + const value = await this.getGlobal('lastSlot'); + return value ?? 0; + } + + async getGlobal(key: string): Promise { + const table = this.db.table('global'); + const record = await table.get(key); + if (!record) return null; + else return record.value as T; + } + + async setGlobal(key: string, value: unknown): Promise { + const table = this.db.table('global'); + await table.put({ key, value }); + } + + async startGlobalTransaction( + handle: (transaction: IGlobalTrasaction) => PromiseLike + ): Promise { + const globalTable = this.db.table('global'); + return this.db.transaction('rw', globalTable, () => { + return handle(new GlobalTransaction(globalTable)); + }); + } +} diff --git a/packages-user/data-state/src/save/types.ts b/packages-user/data-state/src/save/types.ts new file mode 100644 index 0000000..034db4b --- /dev/null +++ b/packages-user/data-state/src/save/types.ts @@ -0,0 +1,142 @@ +import { ISaveableContent, SaveCompression } from '@user/data-base'; +import { Dexie, Table } from 'dexie'; + +export interface IGlobalTrasaction { + /** 全局存储对应的表 */ + readonly table: Table; + + /** + * 获取指定键值对应的数据 + * @param key 全局键值 + */ + get(key: string): Promise; + + /** + * 设置指定键值存储的数据 + * @param key 全局键值 + * @param value 存储数据 + */ + set(key: string, value: unknown): Promise; +} + +export interface ISaveSystemConfig { + /** 自动存档使用的压缩等级 */ + autosaveLevel: SaveCompression; + /** 普通存档使用的压缩等级 */ + commonSaveLevel: SaveCompression; + /** 可容忍的最大存档耗时,超过此值会抛出警告 */ + saveTimeTolerance: number; + /** 可容忍的最大自动存档耗时,超过此值会抛出警告 */ + autosaveTimeTolerance: number; + /** 自动存档栈最大大小 */ + autosaveStackSize: number; +} + +export interface ISaveRead { + /** 该存档的压缩等级 */ + readonly compression: SaveCompression; + /** 该存档的数据 */ + readonly data: Map; +} + +export interface ISaveSystem { + /** Dexie 数据库 */ + readonly db: Dexie; + + /** + * 初始化存档数据库 + * @param name 数据库名称 + */ + init(name: string): void; + + /** + * 配置此存档系统 + * @param config 配置对象 + */ + config(config: Readonly>): void; + + /** + * 从 `undo` 栈读取上一个自动存档,然后将当前状态加入 `redo` 栈 + * @param current 当前游戏状态,需要加入 `redo` 栈 + */ + undoAutosave( + current: Map> + ): ISaveRead | null; + + /** + * 从 `redo` 栈读取自动存档,并将当前状态加入 `undo` 栈 + * @param current 当前游戏状态,需要加入 `undo` 栈 + */ + redoAutosave( + current: Map> + ): ISaveRead | null; + + /** + * 获取当前的撤回栈 + */ + getUndoStack(): ISaveRead[]; + + /** + * 获取当前的重做栈 + */ + getRedoStack(): ISaveRead[]; + + /** + * 进行自动存档,加入撤回栈 + * @param state 状态对象 + */ + autosave(state: Map>): void; + + /** + * 将 `undo` 栈顶的自动存档真正存入 `IndexedDB` + */ + saveAutosaveToDB(): Promise; + + /** + * 将状态对象存入存档 + * @param id 存档 id,用于建立存档索引及查询 + * @param state 状态对象 + */ + save( + id: number, + state: Map> + ): Promise; + + /** + * 根据 id 读取指定存档 + * @param id 存档 id + */ + load(id: number): Promise; + + /** + * 删除指定存档 + * @param id 存档 id + */ + deleteSave(id: number): Promise; + + /** + * 获取最后一次存档的存档栏位 + */ + getLastSlot(): Promise; + + /** + * 获取指定键值对应的全局存储 + * @param key 全局键值 + */ + getGlobal(key: string): Promise; + + /** + * 设置指定键值对应的全局存储 + * @param key 全局键值 + * @param value 存储数据 + */ + setGlobal(key: string, value: unknown): Promise; + + /** + * 进行全局存储的事务处理,适用于多内容查询与设置,当出现错误时其中的任何写入操作都不会真正存储 + * @param handle 事务处理函数 + */ + startGlobalTransaction( + handle: (transaction: IGlobalTrasaction) => PromiseLike + ): Promise; +} diff --git a/packages-user/data-state/src/types.ts b/packages-user/data-state/src/types.ts index 754ca9d..318816e 100644 --- a/packages-user/data-state/src/types.ts +++ b/packages-user/data-state/src/types.ts @@ -1,61 +1,41 @@ -import { ILayerState } from './map'; -import { IRoleFaceBinder } from './common'; import { IEnemyContext, - IEnemyManager, - IHeroFollower, - IHeroState, - IMotaDataLoader + IMotaDataLoader, + ISaveableContent, + IStateBase } from '@user/data-base'; -import { IEnemyAttr } from './enemy/types'; +import { IEnemyAttr } from './enemy'; import { IHeroAttr } from './hero'; -import { IFlagSystem } from '../../data-base/src/flag/types'; import { ILoadProgressTotal } from '@motajs/loader'; +import { ISaveSystem } from './save'; -export interface IGameDataState { - /** 怪物管理器 */ - readonly enemyManager: IEnemyManager; +export interface ISaveableExecutor { + /** + * 当数据读取后执行的函数,允许对其他存档对象进行读取 + * @param data 对应可存档对象的存档数据 + * @param state 当前的基础状态 + */ + afterLoad(data: T, state: IStateBase): void; } -export interface IStateSaveData { - /** 跟随者列表 */ - readonly followers: readonly IHeroFollower[]; -} - -export interface ICoreState { - /** 朝向绑定 */ - readonly roleFace: IRoleFaceBinder; - /** id 到图块数字的映射 */ - readonly idNumberMap: Map; - /** 图块数字到 id 的映射 */ - readonly numberIdMap: Map; - +export interface ICoreState extends IStateBase { /** 加载进度对象 */ readonly loadProgress: ILoadProgressTotal; /** 数据端加载对象 */ readonly dataLoader: IMotaDataLoader; - - /** 地图状态 */ - readonly layer: ILayerState; - /** 勇士状态 */ - readonly hero: IHeroState; - - /** 怪物管理器 */ - readonly enemyManager: IEnemyManager; /** 怪物上下文 */ readonly enemyContext: IEnemyContext; - /** Flag 系统 */ - readonly flags: IFlagSystem; + /** 存档系统 */ + readonly saveSystem: ISaveSystem; /** - * 保存状态 + * 将某个存档执行器绑定至指定的可存档对象,一个可存档对象只能绑定一个执行器,但一个执行器可以绑定多个可存档对象 + * @param content 可存档对象或其注册 id + * @param executor 可存档对象对应的执行器 */ - saveState(): IStateSaveData; - - /** - * 加载状态 - * @param data 状态对象 - */ - loadState(data: IStateSaveData): void; + bindSaveableExecuter( + content: ISaveableContent | string, + executor: ISaveableExecutor + ): void; } diff --git a/packages-user/legacy-plugin-data/src/fallback.ts b/packages-user/legacy-plugin-data/src/fallback.ts index 8519db7..1271d38 100644 --- a/packages-user/legacy-plugin-data/src/fallback.ts +++ b/packages-user/legacy-plugin-data/src/fallback.ts @@ -1,11 +1,6 @@ import type { TimingFn } from 'mutate-animate'; -import { - fromDirectionString, - heroMoveCollection, - MoveStep, - state -} from '@user/data-state'; -import { hook, loading } from '@user/data-base'; +import { heroMoveCollection, MoveStep, state } from '@user/data-state'; +import { fromDirectionString, hook, loading } from '@user/data-base'; import { Patch, PatchClass } from '@motajs/legacy-common'; import { isNil } from 'lodash-es'; @@ -59,8 +54,6 @@ export function initFallback() { Mota.r(() => { // ----- 引入 - const { mainRenderer } = Mota.require('@user/client-modules'); - const Animation = Mota.require('MutateAnimate'); const patch = new Patch(PatchClass.Control); const patch2 = new Patch(PatchClass.Events); diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 74e1cc6..2028acd 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -168,6 +168,15 @@ "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.", + "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.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } } diff --git a/packages/common/src/utils/types.ts b/packages/common/src/utils/types.ts index 39784d6..8334175 100644 --- a/packages/common/src/utils/types.ts +++ b/packages/common/src/utils/types.ts @@ -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, diff --git a/packages/legacy-ui/src/tools/fixed.ts b/packages/legacy-ui/src/tools/fixed.ts index 2383108..ba05235 100644 --- a/packages/legacy-ui/src/tools/fixed.ts +++ b/packages/legacy-ui/src/tools/fixed.ts @@ -29,8 +29,10 @@ export function getDetailedEnemy( return typeof func === 'string' ? func : func(enemy); }; const special: [string, string, string][] = [...enemy.info.special] + // @ts-expect-error 之后修 .filter(v => !enemy.info.specialHalo?.includes(v)) .map(vv => { + // @ts-expect-error 之后修 const s = Mota.require('@user/data-state').specials[vv]; return [ fromFunc(s.name, enemy.info), diff --git a/prompt.md b/prompt.md index c4da0e7..e683987 100644 --- a/prompt.md +++ b/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? +```