diff --git a/packages-user/data-base/src/hero/equipment.ts b/packages-user/data-base/src/hero/equipment.ts index 376eeeb..4143cef 100644 --- a/packages-user/data-base/src/hero/equipment.ts +++ b/packages-user/data-base/src/hero/equipment.ts @@ -78,9 +78,10 @@ export class HeroEquipment implements IHeroEquipment { * @param state 装备状态实例 */ private loadEquipEffect(state: IEquipmentState) { + // 装备修饰器由 HeroEquipment.loadState 的重新装备恢复,故不进入属性存档,避免与属性读档的重建重复计入 for (const [name, modifier] of state.getModifiers()) { // @ts-expect-error 泛型无法推导 - this.attribute.addModifier(name, modifier, true); + this.attribute.addModifier(name, modifier, false); } } diff --git a/packages-user/data-base/src/hero/saveLoad.test.ts b/packages-user/data-base/src/hero/saveLoad.test.ts index 8122945..1f536ce 100644 --- a/packages-user/data-base/src/hero/saveLoad.test.ts +++ b/packages-user/data-base/src/hero/saveLoad.test.ts @@ -204,6 +204,19 @@ function createHeroState(): IHeroState { ); } +/** 构造一个已注册装备定义并设置好装备槽的勇士状态对象(基础 atk 为 10) */ +function createEquipHero(): IHeroState { + const env = createEquipEnv(); + registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]])); + const hero = new HeroState( + env.state, + new Dir8FaceHandler(), + new HeroAttribute(createBaseAttr()) + ); + hero.equip.setSlots(['weapon']); + return hero; +} + /** 在持E��压缩档下对裁E��E��刁E��加成做一次同实例往返,返回读档后的修饰器值 */ function roundTripPercentageModifier(compression: SaveCompression): unknown { const env = createEquipEnv(); @@ -546,7 +559,7 @@ describe('HeroState save and load round trips', () => { hero.getModifiableAttribute().setModifierSaveEnabled(disabled, false); const saved = hero.saveState(SaveCompression.NoCompression); - expect(saved.modifiers).toHaveLength(1); + expect(saved.attribute.modifiers).toHaveLength(1); hero.loadState(saved, SaveCompression.NoCompression); @@ -608,3 +621,72 @@ describe('HeroState container save and load coverage for sub systems', () => { } }); }); + +describe('HeroState same-reference attribute load (#06-17-1)', () => { + // 验证三档压缩读档后装备加成仍在且属性对象为同一实例 + it('keeps equipment bonuses and the attribute instance across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + const hero = createEquipHero(); + const uid = hero.items.equipment.add(10); + hero.equip.equip(uid, 0); + const attrBefore = hero.getModifiableAttribute(); + expect(attrBefore.getFinalAttribute('atk')).toBe(15); + + const saved = hero.saveState(compression); + hero.equip.unequip(0); + hero.getModifiableAttribute().set('atk', 1); + + hero.loadState(saved, compression); + + expect(hero.getModifiableAttribute()).toBe(attrBefore); + expect(attrBefore.getFinalAttribute('atk')).toBe(15); + expect(attrBefore.getBaseAttribute('atk')).toBe(10); + expect(hero.equip.getEquipped(0)).toBe(uid); + } + }); + + // 验证读档后装备增删仍作用于活属性实例 + it('applies equipment changes to the live attribute after load', () => { + const hero = createEquipHero(); + const uid = hero.items.equipment.add(10); + hero.equip.equip(uid, 0); + const attrBefore = hero.getModifiableAttribute(); + + const saved = hero.saveState(SaveCompression.NoCompression); + hero.equip.unequip(0); + hero.loadState(saved, SaveCompression.NoCompression); + expect(attrBefore.getFinalAttribute('atk')).toBe(15); + + hero.equip.unequip(0); + expect(attrBefore.getFinalAttribute('atk')).toBe(10); + + hero.equip.equip(uid, 0); + expect(attrBefore.getFinalAttribute('atk')).toBe(15); + }); + + // 验证修饰器类型命中注册表时装备加成不翻倍(英雄 5 + 装备 5 → 20 而非 25) + it('does not double-count equipment bonuses when the type is registered', () => { + for (const compression of SAVE_COMPRESSIONS) { + const hero = createEquipHero(); + hero.registerModifier('@system/value', () => new ValueModifier(5)); + hero.createAndInsertModifier('@system/value', 'atk'); + const uid = hero.items.equipment.add(10); + hero.equip.equip(uid, 0); + expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe( + 20 + ); + + const saved = hero.saveState(compression); + hero.equip.unequip(0); + hero.getModifiableAttribute().set('atk', 1); + hero.loadState(saved, compression); + + expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe( + 20 + ); + expect([ + ...hero.getModifiableAttribute().getModifiers('atk') + ]).toHaveLength(2); + } + }); +}); diff --git a/packages-user/data-base/src/hero/state.test.ts b/packages-user/data-base/src/hero/state.test.ts index daf2555..0148f04 100644 --- a/packages-user/data-base/src/hero/state.test.ts +++ b/packages-user/data-base/src/hero/state.test.ts @@ -114,17 +114,6 @@ describe('HeroState assembly', () => { isolated.set('hp', 1); expect(attribute.getBaseAttribute('hp')).toBe(100); }); - - // 验证 attachAttribute 替换绑定的属性对象 - it('replaces the bound attribute through attachAttribute', () => { - const hero = createHeroState(); - const replacement = new HeroAttribute(createBaseAttr()); - - hero.attachAttribute(replacement); - - expect(hero.getModifiableAttribute()).toBe(replacement); - expect(hero.getAttribute()).toBe(replacement); - }); }); describe('HeroState modifier registry', () => { diff --git a/packages-user/data-base/src/hero/state.ts b/packages-user/data-base/src/hero/state.ts index 4774131..ffec225 100644 --- a/packages-user/data-base/src/hero/state.ts +++ b/packages-user/data-base/src/hero/state.ts @@ -1,4 +1,3 @@ -import { HeroAttribute } from './attribute'; import { HeroEquipment } from './equipment'; import { HeroFollowersController } from './follower'; import { HeroItems } from './items'; @@ -14,7 +13,6 @@ import { IHeroRendering, IHeroState, IHeroStateSave, - IModifierStateSave, IReadonlyHeroAttribute, IHeroChangeFloorInfo, IHeroStateHooks @@ -26,23 +24,12 @@ import { IFacedTileLocator, SaveCompression } from '@user/data-common'; -import { - Hookable, - HookController, - IHookController, - logger -} from '@motajs/common'; +import { Hookable, HookController, IHookController } from '@motajs/common'; export class HeroState extends Hookable implements IHeroState { - /** 修饰器工厂函数注册表 */ - private readonly registry: Map< - string, - () => IHeroModifier - > = new Map(); - readonly location: IHeroLocation; readonly rendering: IHeroRendering; readonly followers: IHeroFollowersController; @@ -52,7 +39,7 @@ export class HeroState constructor( state: IDataCommon, faceHandler: IFaceHandler, - public attribute: IHeroAttribute + public readonly attribute: IHeroAttribute ) { super(); this.rendering = new HeroRendering(state); @@ -87,10 +74,6 @@ export class HeroState //#region 属性相关 - attachAttribute(attribute: IHeroAttribute): void { - this.attribute = attribute; - } - getModifiableAttribute(): IHeroAttribute { return this.attribute; } @@ -107,26 +90,18 @@ export class HeroState type: string, cons: () => IHeroModifier ): void { - this.registry.set(type, cons); + this.attribute.registerModifier(type, cons); } createModifier(type: string): IHeroModifier | null { - const cons = this.registry.get(type); - if (!cons) { - logger.warn(116, type); - return null; - } - return cons() as IHeroModifier; + return this.attribute.createModifier(type); } createAndInsertModifier( type: string, name: K ): IHeroModifier | null { - const modifier = this.createModifier(type); - if (!modifier) return null; - this.attribute.addModifier(name, modifier); - return modifier; + return this.attribute.createAndInsertModifier(type, name); } //#endregion @@ -146,25 +121,15 @@ export class HeroState } saveState(compression: SaveCompression): IHeroStateSave { - const modifiers: IModifierStateSave[] = []; - for (const [name, modifier] of this.attribute.iterateModifiers()) { - if (!this.attribute.getModifierSaveEnabled(modifier)) continue; - modifiers.push({ - name: name as keyof THero, - type: modifier.type, - state: modifier.saveState(compression) - }); - } const followerSaves = this.followers .getAllFollowers() .map(v => v.saveState(compression)); return { - attribute: this.attribute.toStructured(), + attribute: this.attribute.saveState(compression), location: this.location.saveState(compression), rendering: this.rendering.saveState(compression), followers: followerSaves, - modifiers, items: this.items.saveState(compression), equip: this.equip.saveState(compression) }; @@ -174,15 +139,8 @@ export class HeroState 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, compression); - newAttribute.addModifier(save.name, modifier); - } - this.attribute = newAttribute; + // 属性原地读档:不替换实例,使装备与战斗侧持有的引用跨读档始终有效 + this.attribute.loadState(state.attribute, compression); this.location.loadState(state.location, compression); this.rendering.loadState(state.rendering, compression); this.items.loadState(state.items, compression); diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 1761481..1485425 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -828,16 +828,14 @@ export interface IHeroStateHooks extends IHookBase { } export interface IHeroStateSave { - /** 勇士属性状态 */ - readonly attribute: THero; + /** 勇士属性状态,含基础属性值与保存启用的修饰器 */ + readonly attribute: IHeroAttributeSave; /** 勇士当前位置 */ readonly location: IHeroLocationSave; /** 勇士渲染状态 */ readonly rendering: IHeroRenderingSave; /** 勇士当前的跟随者 */ readonly followers: readonly IHeroFollowerSave[]; - /** 勇士属性修饰器状态 */ - readonly modifiers: readonly IModifierStateSave[]; /** 勇士道具背包状态 */ readonly items: IHeroItemsSave; /** 勇士装备状态 */ @@ -866,12 +864,6 @@ export interface IHeroState */ getLocation(): IFacedTileLocator; - /** - * 绑定勇士属性对象 - * @param attribute 勇士属性对象 - */ - attachAttribute(attribute: IHeroAttribute): void; - /** * 获取可修改勇士对象 */