refactor: EnemyManager

This commit is contained in:
unanmed 2026-09-24 14:40:38 +08:00
parent 523d19417c
commit 47e71b52ae
3 changed files with 36 additions and 181 deletions

View File

@ -1,16 +1,15 @@
import { logger } from '@motajs/common';
import { Enemy as EnemyImpl } from './enemy';
import {
IEnemy,
IEnemyComparer,
IEnemyManager,
IEnemyManagerSaveState,
IEnemyLegacyBridge,
IReadonlyEnemy,
SpecialCreation,
IEnemySaveState
} from './types';
import { SaveCompression } from '@user/data-common';
import { ITileStore, SaveCompression } from '@user/data-common';
import { isNil } from 'lodash-es';
export class EnemyManager<TEnemy> implements IEnemyManager<TEnemy> {
/** 特殊属性注册表,code -> 创建函数 */
@ -22,14 +21,8 @@ export class EnemyManager<TEnemy> implements IEnemyManager<TEnemy> {
private readonly attributeRegistry: Map<keyof TEnemy, any> = new Map();
/** 怪物模板表,code -> IEnemy */
private readonly prefabByCode: Map<number, IEnemy<TEnemy>> = new Map();
/** 怪物模板表,id -> IEnemy */
private readonly prefabById: Map<string, IEnemy<TEnemy>> = 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 提供 */
@ -39,7 +32,7 @@ export class EnemyManager<TEnemy> implements IEnemyManager<TEnemy> {
/** 是否已首次调用 compareWith */
private hasReference: boolean = false;
constructor(readonly bridge: IEnemyLegacyBridge<TEnemy>) {}
constructor(private readonly tileStore: ITileStore) {}
registerSpecial(code: number, cons: SpecialCreation<any, TEnemy>): void {
this.specialRegistry.set(code, cons);
@ -61,59 +54,16 @@ export class EnemyManager<TEnemy> implements IEnemyManager<TEnemy> {
this.attributeRegistry.set(name, defaultValue);
}
fromLegacyEnemy(code: number, enemy: Enemy): IEnemy<TEnemy> {
// 如果该旧样板怪物已经通过 addPrefabFromLegacy 注册为模板,直接克隆模板
const existingCode = this.legacyIdToCode.get(enemy.id);
if (existingCode) {
const prefab = this.prefabByCode.get(existingCode);
if (prefab) {
return prefab.clone();
}
}
return this.convertLegacyEnemy(code, enemy);
}
/**
* 根据旧样板怪物与注册过的默认属性构造属性对象
* @param enemy 旧样板怪物对象
* 根据怪物的图块数字或 id 获取其模板
* @param token 怪物的图块数字或 id
* @returns
*/
private createAttributes(enemy: Enemy): TEnemy {
const attrs: Partial<TEnemy> = {};
for (const [name, defaultValue] of this.attributeRegistry) {
attrs[name] = structuredClone(defaultValue);
}
Object.assign(attrs, this.bridge.fromLegacyEnemy(enemy, attrs));
return attrs as TEnemy;
}
/**
* 真正执行旧样板怪物到新怪物对象的转换
* @param code 怪物图块数字
* @param enemy 旧样板怪物对象
*/
private convertLegacyEnemy(code: number, enemy: Enemy): IEnemy<TEnemy> {
const attrs = this.createAttributes(enemy);
const result = new EnemyImpl<TEnemy>(
enemy.id,
code,
structuredClone(attrs)
);
// 转换特殊属性
if (enemy.special) {
for (const specialCode of enemy.special) {
const creator = this.specialRegistry.get(specialCode);
if (!creator) continue;
const special = creator(result);
special.fromLegacyEnemy(enemy);
result.addSpecial(special);
}
}
return result;
private internalGetPrefab(token: number | string) {
const num = this.tileStore.num(token);
if (isNil(num)) return null;
const sourceCode = this.reuseByCode.get(num) ?? num;
return this.prefabByCode.get(sourceCode) ?? null;
}
createEnemy(code: number): IEnemy<TEnemy> | null {
@ -122,79 +72,39 @@ export class EnemyManager<TEnemy> implements IEnemyManager<TEnemy> {
return prefab.clone();
}
// 不变式:所有按 code/id 取模板的公开入口都必须经 internalGetPrefab 解析复用映射,
// 复用映射把同一模板的多个朝向 code 别名到来源模板,绕过解析会让这些 code 取不到模板
createEnemyById(id: string): IEnemy<TEnemy> | null {
const prefab = this.internalGetPrefab(id);
if (!prefab) return null;
return prefab.clone();
}
private internalGetPrefab(code: number | string) {
if (typeof code === 'number') {
const sourceCode = this.reuseByCode.get(code) ?? code;
return this.prefabByCode.get(sourceCode) ?? null;
} else {
const sourceId = this.reuseById.get(code) ?? code;
return this.prefabById.get(sourceId) ?? null;
}
}
addPrefab(enemy: IEnemy<TEnemy>): void {
if (
this.prefabByCode.has(enemy.code) ||
this.prefabById.has(enemy.id)
) {
if (this.prefabByCode.has(enemy.code)) {
return;
}
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 {
if (this.prefabByCode.has(code) || this.prefabById.has(enemy.id)) {
return;
}
const prefab = this.convertLegacyEnemy(code, enemy);
this.prefabByCode.set(code, prefab);
this.prefabById.set(prefab.id, prefab);
this.legacyIdToCode.set(enemy.id, code);
this.updateDirty(code, prefab);
getPrefab(token: number | string): IReadonlyEnemy<TEnemy> | null {
return this.internalGetPrefab(token);
}
getPrefab(code: number): IReadonlyEnemy<TEnemy> | null {
const sourceCode = this.reuseByCode.get(code) ?? code;
return this.prefabByCode.get(sourceCode) ?? null;
}
getPrefabById(id: string): IReadonlyEnemy<TEnemy> | null {
const sourceId = this.reuseById.get(id) ?? id;
return this.prefabById.get(sourceId) ?? null;
}
deletePrefab(code: number | string): void {
const prefab = this.internalGetPrefab(code);
deletePrefab(token: number | string): void {
const prefab = this.internalGetPrefab(token);
if (!prefab) return;
this.prefabByCode.delete(prefab.code);
this.prefabById.delete(prefab.id);
}
changePrefab(code: number | string, enemy: IEnemy<TEnemy>): void {
changePrefab(token: number | string, enemy: IEnemy<TEnemy>): void {
// 先删除旧的模板(如果存在)
this.deletePrefab(code);
this.deletePrefab(token);
// 再添加新的模板
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 {
reusePrefab(source: number | string, reuse: number | string): void {
const prefab = this.internalGetPrefab(source);
if (!prefab) return;
this.reuseByCode.set(code, prefab.code);
this.reuseById.set(id, prefab.id);
const num = this.tileStore.num(reuse);
if (isNil(num)) return;
this.reuseByCode.set(num, prefab.code);
}
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TEnemy>>): void {
@ -223,13 +133,9 @@ export class EnemyManager<TEnemy> implements IEnemyManager<TEnemy> {
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);
}

View File

@ -9,8 +9,6 @@ export interface ICommonSpecialConfig<T> {
getSpecialName: (special: ISpecial<T>) => string;
/** 获取特殊属性的描述 */
getDescription: (special: ISpecial<T>) => string;
/** 从旧样板怪物对象获取此特殊属性对应的属性值 */
fromLegacyEnemy: (enemy: Enemy) => T;
}
export class CommonSerializableSpecial<T> implements ISpecial<T> {
@ -36,10 +34,6 @@ export class CommonSerializableSpecial<T> implements ISpecial<T> {
return this.config.getDescription(this);
}
fromLegacyEnemy(enemy: Enemy): void {
this.value = this.config.fromLegacyEnemy(enemy);
}
clone(): ISpecial<T> {
return new CommonSerializableSpecial(
this.code,
@ -86,19 +80,15 @@ export class NonePropertySpecial implements ISpecial<void> {
return this.config.getDescription(this);
}
fromLegacyEnemy(_enemy: Enemy): void {
// unneeded
}
clone(): ISpecial<void> {
return new NonePropertySpecial(this.code, this.config);
}
saveState(_compression: SaveCompression): void {
saveState(): void {
return undefined;
}
loadState(_state: void, _compression: SaveCompression): void {
loadState(): void {
// 无属性,无需操作
}

View File

@ -55,12 +55,6 @@ export interface ISpecial<T = void> extends ISaveableContent<T> {
*/
getDescription(): string;
/**
* 从旧样板的怪物对象中导入此特殊属性
* @param enemy 旧样板怪物对象
*/
fromLegacyEnemy(enemy: Enemy): void;
/**
* 深拷贝此特殊属性
*/
@ -162,14 +156,6 @@ export interface IEnemy<TAttr>
export type SpecialCreation<T, TAttr> = (enemy: IEnemy<TAttr>) => ISpecial<T>;
export interface IEnemyLegacyBridge<TAttr> {
/**
* 从旧样板的怪物对象中获取其属性
* @param enemy 旧样板怪物对象
*/
fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr;
}
export interface IEnemyManager<TAttr> extends ISaveableContent<
IEnemyManagerSaveState<TAttr>
> {
@ -190,24 +176,11 @@ export interface IEnemyManager<TAttr> extends ISaveableContent<
defaultValue: TAttr[K]
): void;
/**
* 根据旧样板怪物对象生成一个新的怪物对象
* @param code 怪物数字
* @param enemy 旧样板怪物对象
*/
fromLegacyEnemy(code: number, enemy: Enemy): IEnemy<TAttr>;
/**
* 创建怪物对象,如果对应数字的怪物不存在则会返回 `null`
* @param code 怪物图块数字
* @param token 怪物图块数字或 id
*/
createEnemy(code: number): IEnemy<TAttr> | null;
/**
* 根据怪物的 `id` 创建怪物对象,如果对应的怪物不存在则会返回 `null`
* @param id 怪物 `id`
*/
createEnemyById(id: string): IEnemy<TAttr> | null;
createEnemy(token: number | string): IEnemy<TAttr> | null;
/**
* 添加怪物模板,如果 `id` 或 `code` 与已有的冲突,则不会做任何操作,
@ -216,45 +189,31 @@ export interface IEnemyManager<TAttr> extends ISaveableContent<
*/
addPrefab(enemy: IEnemy<TAttr>): void;
/**
* 从旧样板的怪物对象中添加怪物模板
* @param code 怪物对象对应的图块数字
* @param enemy 旧样板怪物对象
*/
addPrefabFromLegacy(code: number, enemy: Enemy): void;
/**
* 获取指定怪物的模板
* @param code 怪物图块数字
* @param token 怪物图块数字或id
*/
getPrefab(code: number): IReadonlyEnemy<TAttr> | null;
/**
* 根据怪物的 `id` 获取对应的怪物模板
* @param id 怪物 `id`
*/
getPrefabById(id: string): IReadonlyEnemy<TAttr> | null;
getPrefab(token: number): IReadonlyEnemy<TAttr> | null;
/**
* 删除指定的怪物模板
* @param code 怪物的图块数字或 `id`
* @param token 怪物的图块数字或 `id`
*/
deletePrefab(code: number | string): void;
deletePrefab(token: number | string): void;
/**
* 修改一个已有的怪物模板,如果不存在则会新增
* @param code 怪物的图块数字或 `id`
* @param token 怪物的图块数字或 `id`
* @param enemy 新的怪物模板
*/
changePrefab(code: number | string, enemy: IEnemy<TAttr>): void;
changePrefab(token: number | string, enemy: IEnemy<TAttr>): void;
/**
* 让指定的怪物数字和怪物 id 复用指定的模板
* @param source 怪物模板源
* @param code 复用怪物数字
* @param id 复用怪物 id
* @param reuse 哪个怪物要复用此模板
*/
reusePrefab(source: number | string, code: number, id: string): void;
reusePrefab(source: number | string, reuse: number | string): void;
/**
* 设置参考快照,后续对模板的修改将与此比较以确定是否脏。
@ -265,11 +224,11 @@ export interface IEnemyManager<TAttr> extends ISaveableContent<
/**
* 修改指定怪物模板的属性,修改完成后自动与参考模板比较并更新 dirty 集合
* @param code 怪物的图块数字或 `id`
* @param token 怪物的图块数字或 `id`
* @param modify 修改函数,传入可写怪物对象,返回修改后的对象
*/
modifyPrefabAttribute(
code: number | string,
token: number | string,
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
): void;