mirror of
https://github.com/motajs/template.git
synced 2026-05-14 04:41:10 +08:00
Compare commits
No commits in common. "476f735adc064248aa66ba687145daf1668c6d7a" and "8c1becc9f12c539e61a11a91f517d4c91be4ab0d" have entirely different histories.
476f735adc
...
8c1becc9f1
@ -1,194 +0,0 @@
|
||||
# 需求综述
|
||||
|
||||
对怪物管理器 `IEnemyManager` 进行存档适配,使其能够参与游戏存档系统。
|
||||
由于大多数情况下怪物模板不会被修改,不需要全量存储,
|
||||
只需对比"参考状态"(游戏加载完成时的初始模板),仅保存发生了变化的模板。
|
||||
|
||||
为此需要:
|
||||
|
||||
- `IEnemy` 和 `ISpecial` 继承 `ISaveableContent` 以支持自身序列化;
|
||||
- 给 `ISpecial` 添加 `deepEqualsTo` 接口用于特殊属性间的深度比较;
|
||||
- `IEnemyManager` 继承 `ISaveableContent`,新增 `compareWith`、`modifyPrefabAttribute`、
|
||||
`attachEnemyComparer`、`getEnemyComparer` 接口;
|
||||
- `IEnemyManager` 内部维护 dirty 集合,以首次 `compareWith` 传入的参考为唯一基准;
|
||||
- `getPrefab` / `getPrefabById` 返回值收窄为 `IReadonlyEnemy<TAttr>`,
|
||||
统一由 `modifyPrefabAttribute` 承担模板修改职责。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 新增存档状态类型
|
||||
|
||||
在 `types.ts` 中新增如下类型,用于序列化怪物与管理器的状态:
|
||||
|
||||
```ts
|
||||
/** 单个 IEnemy 的存档状态 */
|
||||
interface IEnemySaveState<TAttr> {
|
||||
readonly attrs: TAttr;
|
||||
// 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果
|
||||
readonly specials: ReadonlyMap<number, unknown>;
|
||||
}
|
||||
|
||||
/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */
|
||||
interface IEnemyManagerSaveState<TAttr> {
|
||||
// code -> 变更后的 IEnemySaveState
|
||||
readonly modified: ReadonlyMap<number, IEnemySaveState<TAttr>>;
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 新增 `IEnemyComparer<TAttr>` 接口
|
||||
|
||||
由于管理器外部没有比较怪物属性的需求,将比较逻辑封装为独立的比较器,
|
||||
附着在 `EnemyManager` 上。比较器接口如下:
|
||||
|
||||
```ts
|
||||
interface IEnemyComparer<TAttr> {
|
||||
compare(
|
||||
enemyA: IReadonlyEnemy<TAttr>,
|
||||
enemyB: IReadonlyEnemy<TAttr>
|
||||
): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
由用户在初始化时通过 `attachEnemyComparer` 提供。若未提供比较器,
|
||||
在调用 `modifyPrefabAttribute` 或 `changePrefab` 时需发出警告,且视所有怪物均为脏。
|
||||
|
||||
## 3. `ISpecial<T>` 继承 `ISaveableContent<T>`
|
||||
|
||||
- `saveState` 返回 `structuredClone(this.value)`(即 `getValue()` 的深拷贝);
|
||||
- `loadState` 调用 `setValue(state)`;
|
||||
- 新增 `deepEqualsTo(other: ISpecial<T>): boolean`:先对比 `code`,
|
||||
再对 `value` 进行深度比较。
|
||||
|
||||
各内置实现类的比较策略:
|
||||
|
||||
- `NonePropertySpecial`:只需比较 `code`,`value` 为 `void` 无需对比;
|
||||
- `CommonSerializableSpecial`:`value` 为普通可序列化对象,
|
||||
使用 `lodash-es` 的 `isEqual` 进行递归深度比较。
|
||||
|
||||
## 4. `IEnemy<TAttr>` 继承 `ISaveableContent<IEnemySaveState<TAttr>>`
|
||||
|
||||
- `saveState(compression)`:深拷贝 `attrs`,对每个 special 调用
|
||||
`saveState(compression)` 收集到 `specials` Map,返回 `IEnemySaveState<TAttr>`;
|
||||
- `loadState(state, compression)`:以 `state.attrs` 还原属性,
|
||||
然后对已有的每个 special 按 code 查找存档中的对应条目并调用 `loadState`;
|
||||
若存档中出现当前怪物未注册的 special code,发出 logger 警告并跳过。
|
||||
|
||||
## 5. `IEnemyManager<TAttr>` 接口修改
|
||||
|
||||
### 5a. 继承 `ISaveableContent<IEnemyManagerSaveState<TAttr>>`
|
||||
|
||||
- `saveState(compression)`:遍历 dirty 集合,对每个脏模板调用
|
||||
`prefab.saveState(compression)`,汇总为 `IEnemyManagerSaveState<TAttr>` 并返回;
|
||||
- `loadState(state, compression)`:遍历 `state.modified`,
|
||||
找到 code 对应的现有模板,调用 `prefab.loadState(enemyState, compression)` 还原;
|
||||
若某 code 不在当前 prefab 表中,发出 logger 警告并跳过;
|
||||
**不清空 dirty 集合**,始终以首次 `compareWith` 提供的参考为唯一基准;
|
||||
`loadState` 结束后重新用比较器对每个已有脏模板进行比对,
|
||||
刷新 dirty 集合(避免加载后实际已恢复初始值的模板仍停留在 dirty 中)。
|
||||
|
||||
### 5b. 新增 `compareWith`
|
||||
|
||||
```ts
|
||||
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
|
||||
```
|
||||
|
||||
- 由调用方在游戏初始化完成后提供参考快照,外部传入,管理器保存引用;
|
||||
- **首次调用**:直接存储参考,清空 dirty 集合;
|
||||
- **非首次调用**:通过 logger 发出警告,提示此操作风险高,
|
||||
请作者确认操作意图,但仍然执行覆盖(直接替换参考,重置 dirty 集合)。
|
||||
|
||||
### 5c. `getPrefab` / `getPrefabById` 返回值改为 `IReadonlyEnemy<TAttr>`
|
||||
|
||||
原来返回 `IEnemy<TAttr>`,外部可以直接修改模板。
|
||||
改为只读引用,外部不能直接修改,必须通过 `modifyPrefabAttribute` 完成。
|
||||
|
||||
### 5d. 新增 `modifyPrefabAttribute`
|
||||
|
||||
```ts
|
||||
modifyPrefabAttribute(
|
||||
code: number | string,
|
||||
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||
): void;
|
||||
```
|
||||
|
||||
执行流程:
|
||||
|
||||
1. 根据 `code`(数字或 id 字符串)找到对应的模板;
|
||||
2. 将模板以可写引用传入 `modify`,获得修改结果;
|
||||
3. 若 `modify` 返回的是**新引用**(与传入的不同),则将该新对象替换模板表条目
|
||||
(同时更新 `prefabByCode` 与 `prefabById`);
|
||||
4. 将最终生效的模板与 `compareWith` 中提供的参考模板进行 `IEnemyComparer.compare` 比较:
|
||||
- 若不相等,则将此 code 加入 dirty 集合;
|
||||
- 若相等(改回了初始值),则从 dirty 集合中移除;
|
||||
5. 若未附加比较器,则始终视为脏,并发出 logger 警告。
|
||||
|
||||
### 5e. 新增 `attachEnemyComparer` / `getEnemyComparer`
|
||||
|
||||
```ts
|
||||
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void;
|
||||
getEnemyComparer(): IEnemyComparer<TAttr> | null;
|
||||
```
|
||||
|
||||
- `attachEnemyComparer`:设置当前管理器使用的比较器;
|
||||
- `getEnemyComparer`:返回当前比较器,如未设置则返回 `null`,
|
||||
允许外部在特殊场景下借用比较器。
|
||||
|
||||
### 5f. `changePrefab` 也参与 dirty 追踪
|
||||
|
||||
`changePrefab` 直接替换模板表,修改完成后同样与参考模板进行比较,
|
||||
更新 dirty 集合(逻辑与 `modifyPrefabAttribute` 步骤 4 相同)。
|
||||
|
||||
`deletePrefab` 不参与 dirty 追踪,存档时直接跳过被删除的模板。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `lodash-es`:`CommonSerializableSpecial.deepEqualsTo` 中使用 `isEqual` 进行深度比较
|
||||
- `@motajs/common`:引用 `logger` 接口
|
||||
- `@user/data-base/common/types.ts`:引用 `ISaveableContent`、`SaveCompression`
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `packages-user/data-base/src/enemy/types.ts`
|
||||
|
||||
- [ ] 新增 `IEnemySaveState<TAttr>` 类型:单个怪物的存档状态
|
||||
- [ ] 新增 `IEnemyManagerSaveState<TAttr>` 类型:管理器的存档状态
|
||||
- [ ] 新增 `IEnemyComparer<TAttr>` 接口:包含 `compare` 方法,由用户实现
|
||||
- [ ] 修改 `ISpecial<T>`:继承 `ISaveableContent<T>`,
|
||||
新增 `deepEqualsTo(other: ISpecial<T>): boolean`
|
||||
- [ ] 修改 `IEnemy<TAttr>`:继承 `ISaveableContent<IEnemySaveState<TAttr>>`
|
||||
- [ ] 修改 `IEnemyManager<TAttr>`:继承 `ISaveableContent<IEnemyManagerSaveState<TAttr>>`,
|
||||
新增 `compareWith`、`modifyPrefabAttribute`、`attachEnemyComparer`、`getEnemyComparer`;
|
||||
修改 `getPrefab` 与 `getPrefabById` 返回类型为 `IReadonlyEnemy<TAttr>`
|
||||
|
||||
### `packages-user/data-base/src/enemy/enemy.ts`(`Enemy` 类)
|
||||
|
||||
- [ ] 实现 `saveState(compression): IEnemySaveState<TAttr>`
|
||||
- [ ] 实现 `loadState(state, compression): void`
|
||||
|
||||
### `packages-user/data-base/src/enemy/manager.ts`(`EnemyManager` 类)
|
||||
|
||||
- [ ] 新增 `private readonly dirtySet: Set<number>` 成员:记录脏模板的 code
|
||||
- [ ] 新增 `private referenceByCode: Map<number, IReadonlyEnemy<TAttr>>` 成员:
|
||||
保存参考快照
|
||||
- [ ] 新增 `private comparer: IEnemyComparer<TAttr> | null` 成员:比较器
|
||||
- [ ] 新增 `private hasReference: boolean` 成员:标记是否已首次调用 `compareWith`
|
||||
- [ ] 实现 `compareWith`:存储参考快照,非首次调用发出警告,重置 dirty 集合
|
||||
- [ ] 实现 `modifyPrefabAttribute`:调用 modify、处理引用变化、比较、更新 dirty 集合
|
||||
- [ ] 修改 `changePrefab`:替换模板后同步更新 dirty 集合
|
||||
- [ ] 修改 `getPrefab` / `getPrefabById` 返回类型(仅类型,实现无需改动)
|
||||
- [ ] 实现 `attachEnemyComparer` / `getEnemyComparer`
|
||||
- [ ] 实现 `saveState`:遍历 dirty 集合,序列化并返回
|
||||
- [ ] 实现 `loadState`:根据存档恢复脏模板,恢复后重新刷新 dirty 集合
|
||||
|
||||
### 引擎内置特殊属性(当前包内)
|
||||
|
||||
- [ ] `NonePropertySpecial`:实现 `saveState`、`loadState`、`deepEqualsTo`
|
||||
(`value` 为 `void`,`deepEqualsTo` 只比较 `code`)
|
||||
- [ ] `CommonSerializableSpecial`:实现 `saveState`、`loadState`、`deepEqualsTo`
|
||||
(`deepEqualsTo` 的 value 比较使用 `lodash-es` 的 `isEqual`)
|
||||
@ -83,28 +83,28 @@ iterateModifiers(): Iterable<[keyof THero, IHeroModifier]>;
|
||||
|
||||
### `@user/data-base/hero/types.ts`
|
||||
|
||||
- [x] 修改 `IHeroModifier<T, V>` 接口:
|
||||
- [ ] 修改 `IHeroModifier<T, V>` 接口:
|
||||
改为 `IHeroModifier<T, V, S = unknown>`,`identifier` 改名为 `type`,
|
||||
继承 `ISaveableContent<S>`
|
||||
- [x] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式
|
||||
- [x] 修改 `IHeroStateSave<THero>`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段
|
||||
- [x] 修改 `IReadonlyHeroAttribute<THero>`:新增 `iterateModifiers()` 方法签名
|
||||
- [x] 修改 `IHeroState<THero>`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier<V>(type: string): IHeroModifier<unknown, V>` - `createAndInsertModifier<K extends keyof THero, V>(type: string, name: K): IHeroModifier<unknown, V>`
|
||||
- [ ] 新增 `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`
|
||||
|
||||
- [x] 修改 `BaseHeroModifier<T, V>`:
|
||||
- [ ] 修改 `BaseHeroModifier<T, V>`:
|
||||
将 `abstract readonly identifier` 改为 `abstract readonly type`;
|
||||
实现 `saveState` / `loadState`
|
||||
- [x] 修改 `HeroAttribute<THero>`:实现 `iterateModifiers()`
|
||||
- [ ] 修改 `HeroAttribute<THero>`:实现 `iterateModifiers()`
|
||||
|
||||
### `@user/data-base/hero/state.ts`
|
||||
|
||||
- [x] 修改 `HeroState<THero>`:新增 `private readonly registry: Map<string, () => IHeroModifier>` 成员
|
||||
- [x] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry`
|
||||
- [x] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例;
|
||||
- [ ] 修改 `HeroState<THero>`:新增 `private readonly registry: Map<string, () => IHeroModifier>` 成员
|
||||
- [ ] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry`
|
||||
- [ ] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例;
|
||||
若 `type` 未注册则抛出错误
|
||||
- [x] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后,
|
||||
- [ ] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后,
|
||||
再调用 `this.attribute.addModifier(name, modifier)`,返回同一实例
|
||||
- [x] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段
|
||||
- [x] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载
|
||||
- [ ] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段
|
||||
- [ ] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
||||
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
|
||||
import { ILayerState } from '@user/data-base';
|
||||
import { ILayerState } from '@user/data-state';
|
||||
import { IMapExtensionManager, IMapRenderer } from '../map';
|
||||
|
||||
export interface IconProps extends BaseProps {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
|
||||
import { ILayerState } from '@user/data-base';
|
||||
import { ILayerState } from '@user/data-state';
|
||||
import { IMapRenderer } from './types';
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared';
|
||||
|
||||
@ -2,7 +2,7 @@ import {
|
||||
IMapLayer,
|
||||
IMapLayerHookController,
|
||||
IMapLayerHooks
|
||||
} from '@user/data-base';
|
||||
} from '@user/data-state';
|
||||
import { IMapDoorRenderer } from './types';
|
||||
import { IMapRenderer } from '../types';
|
||||
import { sleep } from 'mutate-animate';
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
IHeroMovingHooks,
|
||||
nextFaceDirection
|
||||
} from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-base';
|
||||
import { IMapLayer, state } from '@user/data-state';
|
||||
import { IMapRenderer, IMapRendererTicker, IMovingBlock } from '../types';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { IHookController, logger } from '@motajs/common';
|
||||
@ -15,7 +15,6 @@ 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;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { IHeroMover } from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-state';
|
||||
import {
|
||||
IMapDoorRenderer,
|
||||
IMapExtensionManager,
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
HeroAnimateDirection,
|
||||
IHeroMover
|
||||
} from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-state';
|
||||
|
||||
import { IMapRenderResult } from '../types';
|
||||
|
||||
|
||||
@ -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-base';
|
||||
import { IMapLayer } from '@user/data-state';
|
||||
import { DynamicBlockStatus } from './status';
|
||||
|
||||
export interface IMovingRenderer {
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
MapTileBehavior,
|
||||
MapTileSizeTestMode
|
||||
} from './types';
|
||||
import { ILayerState, ILayerStateHooks, IMapLayer } from '@user/data-base';
|
||||
import { ILayerState, ILayerStateHooks, IMapLayer } from '@user/data-state';
|
||||
import { IHookController, logger } from '@motajs/common';
|
||||
import { compileProgramWith } from '@motajs/client-base';
|
||||
import { isNil, maxBy } from 'lodash-es';
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IMapLayer } from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-state';
|
||||
import { IBlockStatus, IMapVertexStatus } from './types';
|
||||
|
||||
export class StaticBlockStatus implements IBlockStatus {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
IMaterialManager,
|
||||
ITrackedAssetData
|
||||
} from '@user/client-base';
|
||||
import { ILayerState, IMapLayer } from '@user/data-base';
|
||||
import { ILayerState, IMapLayer } from '@user/data-state';
|
||||
import { TimingFn } from 'mutate-animate';
|
||||
|
||||
export const enum MapBackgroundRepeat {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { IMapLayer } from '@user/data-base';
|
||||
import { IMapLayer } from '@user/data-state';
|
||||
import {
|
||||
IBlockData,
|
||||
IBlockSplitter,
|
||||
|
||||
@ -2,12 +2,10 @@ import { logger } from '@motajs/common';
|
||||
import {
|
||||
IEnemy,
|
||||
IEnemyContext,
|
||||
IEnemySaveState,
|
||||
IReadonlyEnemy,
|
||||
ISpecial,
|
||||
IEnemyView
|
||||
} from './types';
|
||||
import { SaveCompression } from '../common/types';
|
||||
|
||||
export class Enemy<TAttr> implements IEnemy<TAttr> {
|
||||
/** 怪物身上的特殊属性列表 */
|
||||
@ -89,29 +87,6 @@ export class Enemy<TAttr> implements IEnemy<TAttr> {
|
||||
this.addSpecial(special.clone());
|
||||
}
|
||||
}
|
||||
|
||||
saveState(_compression: SaveCompression): IEnemySaveState<TAttr> {
|
||||
const specials: Map<number, unknown> = new Map();
|
||||
for (const special of this.specials) {
|
||||
specials.set(special.code, special.saveState(_compression));
|
||||
}
|
||||
return { attrs: structuredClone(this.attributes), specials };
|
||||
}
|
||||
|
||||
loadState(
|
||||
state: IEnemySaveState<TAttr>,
|
||||
compression: SaveCompression
|
||||
): void {
|
||||
this.attributes = structuredClone(state.attrs);
|
||||
for (const special of this.specials) {
|
||||
const saved = state.specials.get(special.code);
|
||||
if (saved === undefined) {
|
||||
logger.warn(120, special.code.toString(), this.id);
|
||||
continue;
|
||||
}
|
||||
special.loadState(saved, compression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EnemyView<TAttr> implements IEnemyView<TAttr> {
|
||||
|
||||
@ -2,15 +2,10 @@ import { logger } from '@motajs/common';
|
||||
import { Enemy as EnemyImpl } from './enemy';
|
||||
import {
|
||||
IEnemy,
|
||||
IEnemyComparer,
|
||||
IEnemyManager,
|
||||
IEnemyManagerSaveState,
|
||||
IEnemyLegacyBridge,
|
||||
IReadonlyEnemy,
|
||||
SpecialCreation,
|
||||
IEnemySaveState
|
||||
SpecialCreation
|
||||
} from './types';
|
||||
import { SaveCompression } from '../common/types';
|
||||
|
||||
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||
/** 特殊属性注册表,code -> 创建函数 */
|
||||
@ -24,18 +19,6 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||
private readonly prefabById: Map<string, IEnemy<TAttr>> = new Map();
|
||||
/** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */
|
||||
private readonly legacyIdToCode: Map<string, number> = new Map();
|
||||
/** 复用映射,reusedCode -> sourceCode */
|
||||
private readonly reuseByCode: Map<number, number> = new Map();
|
||||
/** 复用映射,reusedId -> sourceId */
|
||||
private readonly reuseById: Map<string, string> = new Map();
|
||||
/** 脏模板集合,存储发生了变化的模板 code */
|
||||
private readonly dirtySet: Set<number> = new Set();
|
||||
/** 参考快照,code -> IReadonlyEnemy,由 compareWith 提供 */
|
||||
private referenceByCode: Map<number, IReadonlyEnemy<TAttr>> = new Map();
|
||||
/** 当前附加的怪物比较器 */
|
||||
private comparer: IEnemyComparer<TAttr> | null = null;
|
||||
/** 是否已首次调用 compareWith */
|
||||
private hasReference: boolean = false;
|
||||
|
||||
constructor(readonly bridge: IEnemyLegacyBridge<TAttr>) {}
|
||||
|
||||
@ -128,11 +111,9 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||
|
||||
private internalGetPrefab(code: number | string) {
|
||||
if (typeof code === 'number') {
|
||||
const sourceCode = this.reuseByCode.get(code) ?? code;
|
||||
return this.prefabByCode.get(sourceCode) ?? null;
|
||||
return this.prefabByCode.get(code) ?? null;
|
||||
} else {
|
||||
const sourceId = this.reuseById.get(code) ?? code;
|
||||
return this.prefabById.get(sourceId) ?? null;
|
||||
return this.prefabById.get(code) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +127,6 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||
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 {
|
||||
@ -157,17 +137,14 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||
this.prefabByCode.set(code, prefab);
|
||||
this.prefabById.set(prefab.id, prefab);
|
||||
this.legacyIdToCode.set(enemy.id, code);
|
||||
this.updateDirty(code, prefab);
|
||||
}
|
||||
|
||||
getPrefab(code: number): IReadonlyEnemy<TAttr> | null {
|
||||
const sourceCode = this.reuseByCode.get(code) ?? code;
|
||||
return this.prefabByCode.get(sourceCode) ?? null;
|
||||
getPrefab(code: number): IEnemy<TAttr> | null {
|
||||
return this.prefabByCode.get(code) ?? null;
|
||||
}
|
||||
|
||||
getPrefabById(id: string): IReadonlyEnemy<TAttr> | null {
|
||||
const sourceId = this.reuseById.get(id) ?? id;
|
||||
return this.prefabById.get(sourceId) ?? null;
|
||||
getPrefabById(id: string): IEnemy<TAttr> | null {
|
||||
return this.prefabById.get(id) ?? null;
|
||||
}
|
||||
|
||||
deletePrefab(code: number | string): void {
|
||||
@ -183,126 +160,12 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||
// 再添加新的模板
|
||||
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.reuseByCode.set(code, prefab.code);
|
||||
this.reuseById.set(id, prefab.id);
|
||||
}
|
||||
|
||||
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void {
|
||||
const isSubsequentCall = this.hasReference;
|
||||
if (isSubsequentCall) {
|
||||
logger.warn(117);
|
||||
}
|
||||
this.referenceByCode = new Map();
|
||||
reference.forEach((enemy, key) => {
|
||||
this.referenceByCode.set(key, enemy.clone());
|
||||
});
|
||||
this.hasReference = true;
|
||||
this.dirtySet.clear();
|
||||
if (isSubsequentCall) {
|
||||
this.refreshDirty(reference.keys());
|
||||
}
|
||||
}
|
||||
|
||||
modifyPrefabAttribute(
|
||||
code: number | string,
|
||||
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||
): void {
|
||||
const prefab = this.internalGetPrefab(code);
|
||||
if (!prefab) return;
|
||||
const result = modify(prefab);
|
||||
const prefabCode = prefab.code;
|
||||
if (result !== prefab) {
|
||||
this.prefabByCode.set(result.code, result);
|
||||
this.prefabById.set(result.id, result);
|
||||
if (result.code !== prefabCode) {
|
||||
this.prefabByCode.delete(prefabCode);
|
||||
}
|
||||
if (result.id !== prefab.id) {
|
||||
this.prefabById.delete(prefab.id);
|
||||
}
|
||||
}
|
||||
this.updateDirty(result.code, result);
|
||||
}
|
||||
|
||||
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void {
|
||||
this.comparer = comparer;
|
||||
}
|
||||
|
||||
getEnemyComparer(): IEnemyComparer<TAttr> | null {
|
||||
return this.comparer;
|
||||
}
|
||||
|
||||
saveState(compression: SaveCompression): IEnemyManagerSaveState<TAttr> {
|
||||
const modified: Map<number, IEnemySaveState<TAttr>> = new Map();
|
||||
for (const code of this.dirtySet) {
|
||||
const prefab = this.prefabByCode.get(code);
|
||||
if (!prefab) continue;
|
||||
modified.set(code, prefab.saveState(compression));
|
||||
}
|
||||
return { modified };
|
||||
}
|
||||
|
||||
loadState(
|
||||
state: IEnemyManagerSaveState<TAttr>,
|
||||
compression: SaveCompression
|
||||
): void {
|
||||
for (const [code, enemyState] of state.modified) {
|
||||
const prefab = this.prefabByCode.get(code);
|
||||
if (!prefab) {
|
||||
logger.warn(119, code.toString());
|
||||
continue;
|
||||
}
|
||||
prefab.loadState(enemyState, compression);
|
||||
}
|
||||
// loadState 结束后重新刷新 dirty 集合
|
||||
this.refreshDirty(state.modified.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参考快照更新指定 code 的脏状态
|
||||
* @param code 怪物图块数字
|
||||
* @param current 当前模板对象
|
||||
*/
|
||||
private updateDirty(code: number, current: IEnemy<TAttr>): void {
|
||||
if (!this.hasReference) return;
|
||||
if (!this.comparer) {
|
||||
logger.warn(118);
|
||||
this.dirtySet.add(code);
|
||||
return;
|
||||
}
|
||||
const ref = this.referenceByCode.get(code);
|
||||
if (!ref || !this.comparer.compare(current, ref)) {
|
||||
this.dirtySet.add(code);
|
||||
} else {
|
||||
this.dirtySet.delete(code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有模板加入脏集合,再与参考比较,去除未变化的模板
|
||||
*/
|
||||
private refreshDirty(dirties: Iterable<number>): void {
|
||||
if (!this.hasReference) return;
|
||||
for (const code of dirties) {
|
||||
this.dirtySet.add(code);
|
||||
}
|
||||
if (!this.comparer) return;
|
||||
for (const code of [...this.dirtySet]) {
|
||||
const prefab = this.prefabByCode.get(code);
|
||||
if (!prefab) {
|
||||
this.dirtySet.delete(code);
|
||||
continue;
|
||||
}
|
||||
const ref = this.referenceByCode.get(code);
|
||||
if (ref && this.comparer.compare(prefab, ref)) {
|
||||
this.dirtySet.delete(code);
|
||||
}
|
||||
}
|
||||
this.prefabByCode.set(code, prefab);
|
||||
this.prefabById.set(id, prefab);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { SaveCompression } from '../common/types';
|
||||
import { ISpecial, SpecialCreation } from './types';
|
||||
|
||||
// TODO: 颜色参数
|
||||
@ -47,19 +45,6 @@ export class CommonSerializableSpecial<T> implements ISpecial<T> {
|
||||
this.config
|
||||
);
|
||||
}
|
||||
|
||||
saveState(_compression: SaveCompression): T {
|
||||
return structuredClone(this.value);
|
||||
}
|
||||
|
||||
loadState(state: T, _compression: SaveCompression): void {
|
||||
this.setValue(state);
|
||||
}
|
||||
|
||||
deepEqualsTo(other: ISpecial<T>): boolean {
|
||||
if (this.code !== other.code) return false;
|
||||
return isEqual(this.value, other.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
export class NonePropertySpecial implements ISpecial<void> {
|
||||
@ -93,18 +78,6 @@ export class NonePropertySpecial implements ISpecial<void> {
|
||||
clone(): ISpecial<void> {
|
||||
return new NonePropertySpecial(this.code, this.config);
|
||||
}
|
||||
|
||||
saveState(_compression: SaveCompression): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
loadState(_state: void, _compression: SaveCompression): void {
|
||||
// 无属性,无需操作
|
||||
}
|
||||
|
||||
deepEqualsTo(other: ISpecial<void>): boolean {
|
||||
return this.code === other.code;
|
||||
}
|
||||
}
|
||||
|
||||
export function defineCommonSerializableSpecial<T, TAttr = any>(
|
||||
|
||||
@ -1,36 +1,9 @@
|
||||
import { IRange, ITileLocator } from '@motajs/common';
|
||||
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
|
||||
import { ISaveableContent } from '../common/types';
|
||||
|
||||
//#region 怪物基础
|
||||
|
||||
/** 单个 IEnemy 的存档状态 */
|
||||
export interface IEnemySaveState<TAttr> {
|
||||
/** 怪物属性的深拷贝 */
|
||||
readonly attrs: TAttr;
|
||||
/** 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 */
|
||||
readonly specials: ReadonlyMap<number, unknown>;
|
||||
}
|
||||
|
||||
/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */
|
||||
export interface IEnemyManagerSaveState<TAttr> {
|
||||
/** code -> 变更后的 IEnemySaveState,仅包含脏模板 */
|
||||
readonly modified: ReadonlyMap<number, IEnemySaveState<TAttr>>;
|
||||
}
|
||||
|
||||
export interface IEnemyComparer<TAttr> {
|
||||
/**
|
||||
* 比较两个怪物是否完全相同
|
||||
* @param enemyA 怪物 A
|
||||
* @param enemyB 怪物 B
|
||||
*/
|
||||
compare(
|
||||
enemyA: IReadonlyEnemy<TAttr>,
|
||||
enemyB: IReadonlyEnemy<TAttr>
|
||||
): boolean;
|
||||
}
|
||||
|
||||
export interface ISpecial<T = void> extends ISaveableContent<T> {
|
||||
export interface ISpecial<T = void> {
|
||||
/** 特殊属性代码 */
|
||||
readonly code: number;
|
||||
/** 特殊属性需要的数值 */
|
||||
@ -67,12 +40,6 @@ export interface ISpecial<T = void> extends ISaveableContent<T> {
|
||||
* 深拷贝此特殊属性
|
||||
*/
|
||||
clone(): ISpecial<T>;
|
||||
|
||||
/**
|
||||
* 深度比较此特殊属性与另一特殊属性是否相同
|
||||
* @param other 另一特殊属性
|
||||
*/
|
||||
deepEqualsTo(other: ISpecial<T>): boolean;
|
||||
}
|
||||
|
||||
export interface IReadonlyEnemy<TAttr> {
|
||||
@ -115,8 +82,7 @@ export interface IReadonlyEnemy<TAttr> {
|
||||
clone(): IReadonlyEnemy<TAttr>;
|
||||
}
|
||||
|
||||
export interface IEnemy<TAttr>
|
||||
extends IReadonlyEnemy<TAttr>, ISaveableContent<IEnemySaveState<TAttr>> {
|
||||
export interface IEnemy<TAttr> extends IReadonlyEnemy<TAttr> {
|
||||
/**
|
||||
* 添加特殊属性
|
||||
* @param special 特殊属性对象
|
||||
@ -172,9 +138,7 @@ export interface IEnemyLegacyBridge<TAttr> {
|
||||
fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr;
|
||||
}
|
||||
|
||||
export interface IEnemyManager<TAttr> extends ISaveableContent<
|
||||
IEnemyManagerSaveState<TAttr>
|
||||
> {
|
||||
export interface IEnemyManager<TAttr> {
|
||||
/**
|
||||
* 注册一个特殊属性
|
||||
* @param code 特殊属性代码
|
||||
@ -229,13 +193,13 @@ export interface IEnemyManager<TAttr> extends ISaveableContent<
|
||||
* 获取指定怪物的模板
|
||||
* @param code 怪物图块数字
|
||||
*/
|
||||
getPrefab(code: number): IReadonlyEnemy<TAttr> | null;
|
||||
getPrefab(code: number): IEnemy<TAttr> | null;
|
||||
|
||||
/**
|
||||
* 根据怪物的 `id` 获取对应的怪物模板
|
||||
* @param id 怪物 `id`
|
||||
*/
|
||||
getPrefabById(id: string): IReadonlyEnemy<TAttr> | null;
|
||||
getPrefabById(id: string): IEnemy<TAttr> | null;
|
||||
|
||||
/**
|
||||
* 删除指定的怪物模板
|
||||
@ -257,34 +221,6 @@ export interface IEnemyManager<TAttr> extends ISaveableContent<
|
||||
* @param id 复用怪物 id
|
||||
*/
|
||||
reusePrefab(source: number | string, code: number, id: string): void;
|
||||
|
||||
/**
|
||||
* 设置参考快照,后续对模板的修改将与此比较以确定是否脏。
|
||||
* 非首次调用时会发出警告,但仍执行覆盖
|
||||
* @param reference code -> 参考怪物的 Map
|
||||
*/
|
||||
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
|
||||
|
||||
/**
|
||||
* 修改指定怪物模板的属性,修改完成后自动与参考模板比较并更新 dirty 集合
|
||||
* @param code 怪物的图块数字或 `id`
|
||||
* @param modify 修改函数,传入可写怪物对象,返回修改后的对象
|
||||
*/
|
||||
modifyPrefabAttribute(
|
||||
code: number | string,
|
||||
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 附加怪物比较器,用于 dirty 集合的判断
|
||||
* @param comparer 比较器对象
|
||||
*/
|
||||
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void;
|
||||
|
||||
/**
|
||||
* 获取当前附加的怪物比较器,如未设置则返回 `null`
|
||||
*/
|
||||
getEnemyComparer(): IEnemyComparer<TAttr> | null;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -22,8 +22,7 @@ import {
|
||||
FaceDirection,
|
||||
ISaveableContent,
|
||||
IStateSaveData,
|
||||
SaveCompression,
|
||||
IReadonlyEnemy
|
||||
SaveCompression
|
||||
} from '@user/data-base';
|
||||
import { IEnemyAttr } from './enemy';
|
||||
import {
|
||||
@ -43,7 +42,6 @@ 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 {
|
||||
// 全局内容
|
||||
@ -94,9 +92,7 @@ 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);
|
||||
@ -163,7 +159,6 @@ export class CoreState implements ICoreState {
|
||||
private initEnemyManager(data: Record<EnemyIds, Enemy>) {
|
||||
// TODO: 修改怪物模板并存入存档,即 core.setEnemy
|
||||
const manager = this.enemyManager;
|
||||
const reference = new Map<number, IReadonlyEnemy<IEnemyAttr>>();
|
||||
for (const [id, enemy] of Object.entries(structuredClone(data))) {
|
||||
const num = this.idNumberMap.get(id);
|
||||
if (isNil(num)) continue;
|
||||
@ -174,9 +169,7 @@ export class CoreState implements ICoreState {
|
||||
const upCode = this.idNumberMap.get(up)!;
|
||||
const rightCode = this.idNumberMap.get(right)!;
|
||||
const downCode = this.idNumberMap.get(down)!;
|
||||
const prefab = manager.fromLegacyEnemy(downCode, enemy);
|
||||
reference.set(downCode, prefab);
|
||||
manager.addPrefab(prefab);
|
||||
manager.addPrefabFromLegacy(downCode, enemy);
|
||||
this.roleFace.malloc(downCode, FaceDirection.Down);
|
||||
this.roleFace.bind(leftCode, downCode, FaceDirection.Left);
|
||||
this.roleFace.bind(upCode, downCode, FaceDirection.Up);
|
||||
@ -185,12 +178,9 @@ export class CoreState implements ICoreState {
|
||||
manager.reusePrefab(num, upCode, up);
|
||||
manager.reusePrefab(num, rightCode, right);
|
||||
} else {
|
||||
const prefab = manager.fromLegacyEnemy(num, enemy);
|
||||
reference.set(num, prefab);
|
||||
manager.addPrefab(prefab);
|
||||
manager.addPrefabFromLegacy(num, enemy);
|
||||
}
|
||||
}
|
||||
manager.compareWith(reference);
|
||||
}
|
||||
|
||||
addSaveableContent(id: string, content: ISaveableContent<unknown>): void {
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { IEnemyComparer, IReadonlyEnemy } from '@user/data-base';
|
||||
import { IEnemyAttr } from './types';
|
||||
|
||||
export class MainEnemyComparer implements IEnemyComparer<IEnemyAttr> {
|
||||
compare(
|
||||
enemyA: IReadonlyEnemy<IEnemyAttr>,
|
||||
enemyB: IReadonlyEnemy<IEnemyAttr>
|
||||
): boolean {
|
||||
// 比较基本属性
|
||||
if (
|
||||
enemyA.getAttribute('hp') !== enemyB.getAttribute('hp') ||
|
||||
enemyA.getAttribute('atk') !== enemyB.getAttribute('atk') ||
|
||||
enemyA.getAttribute('def') !== enemyB.getAttribute('def') ||
|
||||
enemyA.getAttribute('money') !== enemyB.getAttribute('money') ||
|
||||
enemyA.getAttribute('exp') !== enemyB.getAttribute('exp') ||
|
||||
enemyA.getAttribute('point') !== enemyB.getAttribute('point')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 比较特殊属性
|
||||
const specialsA = [...enemyA.iterateSpecials()];
|
||||
const specialsB = [...enemyB.iterateSpecials()];
|
||||
if (specialsA.length !== specialsB.length) return false;
|
||||
for (const special of specialsA) {
|
||||
const other = enemyB.getSpecial(special.code);
|
||||
if (!other || !special.deepEqualsTo(other)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { backDir, toDir } from './utils';
|
||||
import { fromDirectionString, loading } from '@user/data-base';
|
||||
import { loading } from '@user/data-base';
|
||||
import type { RenderAdapter } from '@motajs/render';
|
||||
import type { HeroKeyMover } from '@user/client-modules';
|
||||
import { sleep } from '@motajs/common';
|
||||
import { state } from '..';
|
||||
import { fromDirectionString, state } from '..';
|
||||
|
||||
// todo: 转身功能
|
||||
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import type { TimingFn } from 'mutate-animate';
|
||||
import { heroMoveCollection, MoveStep, state } from '@user/data-state';
|
||||
import { fromDirectionString, hook, loading } from '@user/data-base';
|
||||
import {
|
||||
fromDirectionString,
|
||||
heroMoveCollection,
|
||||
MoveStep,
|
||||
state
|
||||
} from '@user/data-state';
|
||||
import { hook, loading } from '@user/data-base';
|
||||
import { Patch, PatchClass } from '@motajs/legacy-common';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
@ -54,6 +59,8 @@ 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);
|
||||
|
||||
@ -173,10 +173,6 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,10 +29,8 @@ 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),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user