From 7743795948b0c0649981aca32496ccfb37eb8341 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Wed, 23 Sep 2026 13:09:19 +0800 Subject: [PATCH] refactor: Hero equipment system --- packages-user/data-base/src/hero/attribute.ts | 48 +---- .../data-base/src/hero/equipStore.ts | 197 ++++++++++++------ packages-user/data-base/src/hero/equipment.ts | 4 +- packages-user/data-base/src/hero/modifier.ts | 42 +++- packages-user/data-base/src/hero/types.ts | 52 ++++- packages-user/data-common/src/replay/func.ts | 2 +- 6 files changed, 225 insertions(+), 120 deletions(-) diff --git a/packages-user/data-base/src/hero/attribute.ts b/packages-user/data-base/src/hero/attribute.ts index 4e9a12d..01b1d0d 100644 --- a/packages-user/data-base/src/hero/attribute.ts +++ b/packages-user/data-base/src/hero/attribute.ts @@ -1,53 +1,14 @@ import { logger } from '@motajs/common'; -import { SaveCompression } from '@user/data-common'; +import { SaveCompression, shouldReplay } from '@user/data-common'; import { IHeroAttribute, IHeroAttributeCloneOption, IHeroAttributeSave, IHeroModifier, - IHeroModifierOwner, IModifierStateSave } from './types'; import { isNil } from 'lodash-es'; -export abstract class BaseHeroModifier implements IHeroModifier { - abstract readonly type: string; - abstract readonly priority: number; - - owner: IHeroModifierOwner | null = null; - - constructor(private currentValue: V) {} - - get value(): V { - return this.currentValue; - } - - setValue(value: V): void { - this.currentValue = value; - this.owner?.markModifierDirty(this); - } - - getValue(): V { - return this.currentValue; - } - - bindAttribute(attribute: IHeroModifierOwner | null): void { - 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; -} - export class HeroAttribute implements IHeroAttribute { /** 当前勇士属性修饰器 */ private readonly modifier: Map = new Map(); @@ -142,21 +103,25 @@ export class HeroAttribute implements IHeroAttribute { //#region 属性操作 + @shouldReplay('Hero attribute change should be replayed.') set(name: K, value: THero[K]): void { this.attribute[name] = value; this.markDirty(name); } + @shouldReplay('Hero attribute change should be replayed.') add(name: SelectKey, value: number): void { (this.attribute[name] as number) += value; this.markDirty(name); } + @shouldReplay('Hero attribute change should be replayed.') mul(name: SelectKey, value: number): void { (this.attribute[name] as number) *= value; this.markDirty(name); } + @shouldReplay('Hero attribute change should be replayed.') div(name: SelectKey, value: number): void { (this.attribute[name] as number) /= value; this.markDirty(name); @@ -187,6 +152,7 @@ export class HeroAttribute implements IHeroAttribute { return arr.indexOf(modifier); } + @shouldReplay('Changing hero attrubute modifier should be replayed.') addModifier( name: K, modifier: IHeroModifier, @@ -210,6 +176,7 @@ export class HeroAttribute implements IHeroAttribute { this.markDirty(name); } + @shouldReplay('Changing hero attrubute modifier should be replayed.') deleteModifier( name: K, modifier: IHeroModifier @@ -227,6 +194,7 @@ export class HeroAttribute implements IHeroAttribute { this.markDirty(name); } + @shouldReplay('Changing hero attrubute modifier should be replayed.') deleteModifierByIndex( name: K, index: number diff --git a/packages-user/data-base/src/hero/equipStore.ts b/packages-user/data-base/src/hero/equipStore.ts index e9253f9..6fbe81c 100644 --- a/packages-user/data-base/src/hero/equipStore.ts +++ b/packages-user/data-base/src/hero/equipStore.ts @@ -1,4 +1,9 @@ -import { IItemRawData, SaveCompression, IDataCommon } from '@user/data-common'; +import { + IItemRawData, + SaveCompression, + IDataCommon, + shouldReplay +} from '@user/data-common'; import { isNil, maxBy } from 'lodash-es'; import { ValueModifier, PercentageModifier } from './modifier'; import { @@ -8,63 +13,131 @@ import { IHeroEquipsStore, IEquipmentSorter, IEquipmentSortHandler, - IHeroEquipsStoreSave + IHeroEquipsStoreSave, + IEquipmentStateHooks } from './types'; -import { logger } from '@motajs/common'; +import { + Hookable, + HookController, + IHookController, + logger +} from '@motajs/common'; -export class EquipmentState implements IEquipmentState { +type Modifiers = Map, IHeroModifier>; + +export class EquipmentState + extends Hookable> + implements IEquipmentState +{ readonly item: IItemRawData; - /** 装备产生的所有修饰器,每个元素为 [属性名, 修饰器] */ - readonly modifiers: [SelectKey, IHeroModifier][]; + /** 数值修饰器列表 */ + readonly valueModifiers: Modifiers = new Map(); + /** 百分比修饰器列表 */ + readonly perModifiers: Modifiers = new Map(); /** 装备的数值属性 */ - private readonly value: Map, number>; + private readonly value: Map, number> = new Map(); /** 装备的百分比属性 */ - private readonly percentage: Map, number>; + private readonly per: Map, number> = new Map(); + + /** 是否已经构建过修饰器 */ + private built: boolean = false; constructor( readonly uid: number, - item: IItemRawData, - buildModifier: boolean = true + item: IItemRawData ) { + super(); this.item = item; - this.modifiers = []; const equip = item.equip; - this.value = new Map( - Object.entries(equip.value) as [SelectKey, number][] - ); - this.percentage = new Map( - Object.entries(equip.percentage) as [ - SelectKey, - number - ][] - ); - - if (buildModifier) { - this.rebuildModifiers(); + for (const [key, value] of Object.entries(equip.value)) { + this.value.set(key as SelectKey, value as number); } + for (const [key, value] of Object.entries(equip.percentage)) { + this.per.set(key as SelectKey, value as number); + } + } + + protected createController( + hook: Partial> + ): IHookController> { + return new HookController(this, hook); } /** - * 从装备属性重新生成装备修饰器 + * 触发修饰器变动钩子 */ - private rebuildModifiers() { - this.modifiers.length = 0; - - for (const [name, baseVal] of this.value) { - this.modifiers.push([name, new ValueModifier(baseVal)]); - } - for (const [name, basePct] of this.percentage) { - this.modifiers.push([name, new PercentageModifier(basePct)]); - } + private notifyModifierChange() { + this.forEachHook(hook => hook.onChangeModifier?.(this.getModifiers())); } - getModifiers(): Iterable< - [keyof SelectType, IHeroModifier] + @shouldReplay('Setting equipment value should be replayed.') + setValue(name: keyof SelectType, value: number): void { + if (value === 0) { + this.value.delete(name); + this.valueModifiers.delete(name); + } else { + this.value.set(name, value); + const modifier = this.valueModifiers.getOrInsertComputed( + name, + () => new ValueModifier(value, 0) + ); + modifier.setValue(value); + } + this.notifyModifierChange(); + } + + @shouldReplay('Setting equipment percentage value should be replayed.') + setPercentage(name: keyof SelectType, value: number): void { + if (value === 0) { + this.per.delete(name); + this.perModifiers.delete(name); + } else { + this.per.set(name, value); + const modifier = this.perModifiers.getOrInsertComputed( + name, + () => new PercentageModifier(value, 10) + ); + modifier.setValue(value); + } + this.notifyModifierChange(); + } + + getValue(name: keyof SelectType): number { + return this.value.get(name) ?? 0; + } + + getPercentage(name: keyof SelectType): number { + return this.per.get(name) ?? 0; + } + + buildModifiers() { + if (this.built) return; + this.built = true; + + this.valueModifiers.clear(); + this.perModifiers.clear(); + + for (const [name, baseVal] of this.value) { + this.valueModifiers.set(name, new ValueModifier(baseVal, 0)); + } + for (const [name, basePct] of this.per) { + this.perModifiers.set(name, new PercentageModifier(basePct, 10)); + } + + this.notifyModifierChange(); + } + + *getModifiers(): Iterable< + [SelectKey, IHeroModifier] > { - return this.modifiers; + if (!this.built) { + this.buildModifiers(); + } + yield* this.valueModifiers; + yield* this.perModifiers; } /** @@ -76,7 +149,7 @@ export class EquipmentState implements IEquipmentState { uid: this.uid, num: this.item.num, value: new Map(this.value), - percentage: new Map(this.percentage) + percentage: new Map(this.per) }; } @@ -94,7 +167,7 @@ export class EquipmentState implements IEquipmentState { } } const perDiff = new Map, number>(); - for (const [name, equipPer] of this.percentage) { + for (const [name, equipPer] of this.per) { const base = percentage[name]; if (base !== equipPer) { perDiff.set(name, equipPer); @@ -122,14 +195,14 @@ export class EquipmentState implements IEquipmentState { */ private loadNoCompression(state: IEquipmentStateSave): void { this.value.clear(); - this.percentage.clear(); + this.per.clear(); for (const [name, value] of state.value) { this.value.set(name, value); } for (const [name, value] of state.percentage) { - this.percentage.set(name, value); + this.per.set(name, value); } - this.rebuildModifiers(); + this.buildModifiers(); } /** @@ -138,7 +211,7 @@ export class EquipmentState implements IEquipmentState { */ private loadDiff(state: IEquipmentStateSave): void { this.value.clear(); - this.percentage.clear(); + this.per.clear(); // 基准为装备原始定义,再叠加存档中的差异条目 for (const [name, value] of Object.entries( @@ -149,7 +222,7 @@ export class EquipmentState implements IEquipmentState { for (const [name, value] of Object.entries( this.item.equip.percentage )) { - this.percentage.set(name as SelectKey, value); + this.per.set(name as SelectKey, value); } // 差异内容 @@ -157,10 +230,10 @@ export class EquipmentState implements IEquipmentState { this.value.set(name, value); } for (const [name, value] of state.percentage) { - this.percentage.set(name, value); + this.per.set(name, value); } - this.rebuildModifiers(); + this.buildModifiers(); } loadState( @@ -192,6 +265,7 @@ export class HeroEquipsStore implements IHeroEquipsStore { this.state = state; } + @shouldReplay('Adding equipment to equip store should be replayed.') add(item: number | string): number { const num = this.state.tileStore.num(item); if (isNil(num)) return -1; @@ -202,9 +276,11 @@ export class HeroEquipsStore implements IHeroEquipsStore { const uid = this.nextUid++; const state = new EquipmentState(uid, raw); this.instanceMap.set(uid, state); + state.buildModifiers(); return uid; } + @shouldReplay('Deleting equipment from equip store should be replayed.') delete(uid: number): void { this.instanceMap.delete(uid); } @@ -279,34 +355,16 @@ export class HeroEquipsStore implements IHeroEquipsStore { state: IHeroEquipsStoreSave, compression: SaveCompression ): void { - // 按 uid 复用现有实例原地读档,使外部持有的引用跨读档仍然有效; - // 仅当 uid 相同但装备图块数字不同时才替换实例 - const savedUids = new Set(); + this.instanceMap.clear(); for (const save of state.equipments) { const raw = this.state.itemStore.getData(save.num); if (!raw) { logger.error(59, save.num.toString()); continue; } - savedUids.add(save.uid); - const existing = this.instanceMap.get(save.uid); - if (existing && existing.item.num === save.num) { - existing.loadState(save, compression); - } else { - const instance = new EquipmentState( - save.uid, - raw, - false - ); - instance.loadState(save, compression); - this.instanceMap.set(save.uid, instance); - } - } - // 以存档为准:存档中不存在的装备实例一律删除 - for (const uid of this.instanceMap.keys()) { - if (!savedUids.has(uid)) { - this.instanceMap.delete(uid); - } + const ins = new EquipmentState(save.uid, raw); + ins.loadState(save, compression); + this.instanceMap.set(save.uid, ins); } if (state.equipments.length === 0) { this.nextUid = 0; @@ -314,9 +372,10 @@ export class HeroEquipsStore implements IHeroEquipsStore { const maxUid = maxBy(state.equipments, 'uid'); if (!maxUid) { logger.error(58); - return; + this.nextUid = 0; + } else { + this.nextUid = maxUid.uid + 1; } - this.nextUid = maxUid.uid + 1; } } } diff --git a/packages-user/data-base/src/hero/equipment.ts b/packages-user/data-base/src/hero/equipment.ts index 6b16472..42e7386 100644 --- a/packages-user/data-base/src/hero/equipment.ts +++ b/packages-user/data-base/src/hero/equipment.ts @@ -1,5 +1,5 @@ import { isNil } from 'lodash-es'; -import { IDataCommon, ReplayCode } from '@user/data-common'; +import { IDataCommon, ReplayCode, shouldReplay } from '@user/data-common'; import { EquipStatus, IEquipmentState, @@ -150,6 +150,7 @@ export class HeroEquipment implements IHeroEquipment { } } + @shouldReplay('Equiping equipment should be replayed.') equip( uid: number, slot: number | string, @@ -197,6 +198,7 @@ export class HeroEquipment implements IHeroEquipment { return curr; } + @shouldReplay('Unequiping equipment should be replayed.') unequip(slot: number): number | undefined { const uid = this.equips.get(slot); if (isNil(uid)) return void 0; diff --git a/packages-user/data-base/src/hero/modifier.ts b/packages-user/data-base/src/hero/modifier.ts index f8eba54..0e99e34 100644 --- a/packages-user/data-base/src/hero/modifier.ts +++ b/packages-user/data-base/src/hero/modifier.ts @@ -1,5 +1,43 @@ -import { BaseHeroModifier } from './attribute'; -import { IHeroModifier } from './types'; +import { SaveCompression } from '@user/data-common'; +import { IHeroModifier, IHeroModifierOwner } from './types'; + +export abstract class BaseHeroModifier implements IHeroModifier { + abstract readonly type: string; + abstract readonly priority: number; + + owner: IHeroModifierOwner | null = null; + + constructor(private currentValue: V) {} + + get value(): V { + return this.currentValue; + } + + setValue(value: V): void { + this.currentValue = value; + this.owner?.markModifierDirty(this); + } + + getValue(): V { + return this.currentValue; + } + + bindAttribute(attribute: IHeroModifierOwner | null): void { + 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; +} export class ValueModifier extends BaseHeroModifier { readonly type = '@system/value'; diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 54abe15..036c620 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -662,18 +662,56 @@ export interface IHeroEquipsStoreSave { readonly equipments: readonly IEquipmentStateSave[]; } -/** - * 装备实例。其修饰器由装备自身加成重建而来,是派生视图: - * 跨读档不保留旧的修饰器对象引用属预期行为,读档后它们会被重新创建 - */ -export interface IEquipmentState extends ISaveableContent< - IEquipmentStateSave -> { +export interface IEquipmentStateHooks extends IHookBase { + /** + * 当装备的修饰器发生变化时执行,当发生修饰器增加、减少、数值修改时都会触发 + * @param modifiers 当前装备所拥有的所有修饰器 + */ + onChangeModifier?( + modifiers: Iterable<[SelectKey, IHeroModifier]> + ): void; +} + +export interface IEquipmentState + extends + ISaveableContent>, + IHookable> { /** 装备实例 uid */ readonly uid: number; /** 装备的定义数据引用 */ readonly item: IItemRawData; + /** + * 设置此装备对指定勇士属性值的增加量 + * @param name 勇士属性名 + * @param value 要将装备的此属性增量设置为的值 + */ + setValue(name: SelectKey, value: number): void; + + /** + * 设置此装备对指定勇士属性值的百分比增加量 + * @param name 勇士属性名 + * @param value 要将装备的此属性百分比增量设置为的值 + */ + setPercentage(name: SelectKey, value: number): void; + + /** + * 获取装备对指定勇士属性值的增加量,若装备不增加对应属性,则返回 `0` + * @param name 勇士属性名 + */ + getValue(name: SelectKey): number; + + /** + * 获取装备对指定勇士属性值的百分比增加量,若装备不增加对应属性,则返回 `0` + * @param name 勇士属性名 + */ + getPercentage(name: SelectKey): number; + + /** + * 构建所有的修饰器,每个装备实例只需要进行一次构建 + */ + buildModifiers(): void; + /** * 获取装备产生的所有修饰器,每个元素为 [属性名, 修饰器] */ diff --git a/packages-user/data-common/src/replay/func.ts b/packages-user/data-common/src/replay/func.ts index 4c1c623..3dd1428 100644 --- a/packages-user/data-common/src/replay/func.ts +++ b/packages-user/data-common/src/replay/func.ts @@ -18,7 +18,7 @@ interface IReplaySafetyDetailQueue { type ReplayMethod = ( this: This, ...args: Args -) => Return; +) => Return | any; type ReplayDecorator = ( method: ReplayMethod,