diff --git a/packages-user/data-base/src/hero/equipStore.test.ts b/packages-user/data-base/src/hero/equipStore.test.ts index 85f0020..66cf994 100644 --- a/packages-user/data-base/src/hero/equipStore.test.ts +++ b/packages-user/data-base/src/hero/equipStore.test.ts @@ -6,12 +6,17 @@ import { type IItemRawData, ItemCategory, ItemStore, + SaveCompression, TileStore, TileType } from '@user/data-common'; import { HeroEquipsStore } from './equipStore'; import { PercentageModifier, ValueModifier } from './modifier'; -import { type IEquipmentSortHandler, type IEquipmentSorter } from './types'; +import { + type IEquipmentSortHandler, + type IEquipmentSorter, + type IEquipmentStateSave +} from './types'; vi.hoisted(() => { vi.stubGlobal('main', { replayChecking: true }); @@ -45,6 +50,13 @@ afterAll(() => { type HeroKey = SelectKey; +/** 存读档测试覆盖的三档压缩级别 */ +const SAVE_COMPRESSIONS = [ + SaveCompression.NoCompression, + SaveCompression.LowCompression, + SaveCompression.HighCompression +] as const; + interface TestEnv { state: IDataCommon; tileStore: IDataCommon['tileStore']; @@ -217,6 +229,82 @@ describe('HeroEquipsStore instances', () => { }); }); +/** 构造一份仅指定数值加成的无压缩装备存档,用于把活实例改到指定存档点 */ +function createValueSave( + uid: number, + atk: number +): IEquipmentStateSave { + return { + uid, + num: 10, + value: new Map([['atk', atk]]), + percentage: new Map() + }; +} + +describe('HeroEquipsStore same-reference load (#06-17-4)', () => { + // 验证同 uid 装备实例在三档压缩读档前后为同一实例,且数值恢复到存档点 + it('keeps the same instance and restores values across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + const env = createEnv(); + registerItem(env, createItem(10, 'sword', [['atk', 5]])); + const uid = env.store.add(10); + const before = env.store.get(uid)!; + // 装备数值只能经读档改变,先用一份自定义存档把活实例改到 9 作为存档点 + before.loadState( + createValueSave(uid, 9), + SaveCompression.NoCompression + ); + const saved = env.store.saveState(compression); + before.loadState( + createValueSave(uid, 1), + SaveCompression.NoCompression + ); + + env.store.loadState(saved, compression); + + expect(env.store.get(uid)).toBe(before); + expect([...before.getModifiers()][0][1].getValue()).toBe(9); + } + }); + + // 验证存档中不存在的装备实例在读档后被删除(以存档为准) + it('deletes instances absent from the save', () => { + const env = createEnv(); + registerItem(env, createItem(10, 'sword')); + const kept = env.store.add(10); + const before = env.store.get(kept)!; + + const saved = env.store.saveState(SaveCompression.NoCompression); + const extra = env.store.add(10); + + env.store.loadState(saved, SaveCompression.NoCompression); + + expect(env.store.get(kept)).toBe(before); + expect(env.store.get(extra)).toBeNull(); + expect(env.store.count(10)).toBe(1); + }); + + // 验证同 uid 但装备图块数字不同的存档无法复用实例,改为替换该实例 + it('replaces the instance when the saved item number differs', () => { + const env = createEnv(); + registerItem(env, createItem(10, 'sword', [['atk', 5]])); + registerItem(env, createItem(11, 'axe', [['atk', 3]])); + const uid = env.store.add(10); + const before = env.store.get(uid)!; + + const saved = env.store.saveState(SaveCompression.NoCompression); + const swapped = { + equipments: saved.equipments.map(v => ({ ...v, num: 11 })) + }; + + env.store.loadState(swapped, SaveCompression.NoCompression); + + expect(env.store.get(uid)).not.toBe(before); + expect(env.store.get(uid)?.item.num).toBe(11); + }); +}); + describe('EquipmentState modifiers', () => { // 验证装备实例按定义生成数值与百分比修饰器 it('builds value and percentage modifiers from the item', () => { diff --git a/packages-user/data-base/src/hero/equipStore.ts b/packages-user/data-base/src/hero/equipStore.ts index 96c726e..9a53a61 100644 --- a/packages-user/data-base/src/hero/equipStore.ts +++ b/packages-user/data-base/src/hero/equipStore.ts @@ -268,16 +268,34 @@ export class HeroEquipsStore implements IHeroEquipsStore { state: IHeroEquipsStoreSave, compression: SaveCompression ): void { - this.instanceMap.clear(); + // 按 uid 复用现有实例原地读档,使外部持有的引用跨读档仍然有效; + // 仅当 uid 相同但装备图块数字不同时才替换实例 + const savedUids = new Set(); for (const save of state.equipments) { const raw = this.state.itemStore.getData(save.num); if (!raw) { logger.error(59, save.num.toString()); continue; } - const instance = new EquipmentState(save.uid, raw, false); - instance.loadState(save, compression); - this.instanceMap.set(save.uid, instance); + 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 maxUid = maxBy(state.equipments, 'uid'); if (!maxUid) { diff --git a/packages-user/data-base/src/hero/saveLoad.test.ts b/packages-user/data-base/src/hero/saveLoad.test.ts index 1f536ce..c1f8995 100644 --- a/packages-user/data-base/src/hero/saveLoad.test.ts +++ b/packages-user/data-base/src/hero/saveLoad.test.ts @@ -690,3 +690,54 @@ describe('HeroState same-reference attribute load (#06-17-1)', () => { } }); }); + +describe('HeroState same-reference equipment load (#06-17-4)', () => { + // 验证经勇士容器三档往返后装备实例为同一实例,且数值恢复到存档点 + it('keeps the equipment instance and restores values across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + const hero = createEquipHero(); + const uid = hero.items.equipment.add(10); + const before = hero.items.equipment.get(uid)!; + // 装备数值只能经读档改变,先用一份自定义存档把活实例改到 9 作为存档点 + before.loadState( + { + uid, + num: 10, + value: new Map([['atk', 9]]), + percentage: new Map() + }, + SaveCompression.NoCompression + ); + const saved = hero.saveState(compression); + before.loadState( + { + uid, + num: 10, + value: new Map([['atk', 1]]), + percentage: new Map() + }, + SaveCompression.NoCompression + ); + + hero.loadState(saved, compression); + + expect(hero.items.equipment.get(uid)).toBe(before); + expect([...before.getModifiers()][0][1].getValue()).toBe(9); + } + }); + + // 验证存档中不存在的装备实例在读档后被删除(以存档为准) + it('deletes equipment instances absent from the save', () => { + const hero = createEquipHero(); + const kept = hero.items.equipment.add(10); + const before = hero.items.equipment.get(kept)!; + + const saved = hero.saveState(SaveCompression.NoCompression); + const extra = hero.items.equipment.add(10); + + hero.loadState(saved, SaveCompression.NoCompression); + + expect(hero.items.equipment.get(kept)).toBe(before); + expect(hero.items.equipment.get(extra)).toBeNull(); + }); +}); diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 1485425..9ec8f1b 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -7,7 +7,8 @@ import { IItemRawData, IObjectMovable, IObjectMover, - ISaveableContent + ISaveableContent, + SaveCompression } from '@user/data-common'; import { IPassPredicate } from '../map'; @@ -710,6 +711,19 @@ export interface IHeroEquipsStore * @param item 道具图块数字或字符串 id */ count(item: number | string): number; + + /** + * 读取装备实例仓库存档。存档中已存在的 uid 会复用原实例并在其实例上原地读档, + * 因此外部通过 {@link get} / {@link instancesOf} / {@link instances} 持有的实例跨读档仍然有效; + * 仅当 uid 相同但装备图块数字不同的存档无法复用实例时,才替换该实例。 + * 存档中不存在的实例一律删除,即读档结果以存档为准。 + * @param state 装备实例仓库存档 + * @param compression 压缩级别 + */ + loadState( + state: IHeroEquipsStoreSave, + compression: SaveCompression + ): void; } export interface IHeroEquipmentSave {