From 153f4a490aab4723c3969b9916ecd286e614f03e Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 17 Sep 2026 13:47:20 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(07-11):=20#06-17-4=20preserve=20eq?= =?UTF-8?q?uipment=20instances=20across=20loadState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HeroEquipsStore.loadState 按 uid 复用现有 EquipmentState 原地读档 - 同 uid 但装备图块数字不同时才替换该实例 - 存档中不存在的装备实例一律删除(以存档为准) - IHeroEquipsStore.loadState jsDoc 补写同引用与删除语义 - 新增同实例保真、删除多余实例、num 不同替换三条回归用例 --- .../data-base/src/hero/equipStore.test.ts | 90 ++++++++++++++++++- .../data-base/src/hero/equipStore.ts | 26 +++++- .../data-base/src/hero/saveLoad.test.ts | 51 +++++++++++ packages-user/data-base/src/hero/types.ts | 16 +++- 4 files changed, 177 insertions(+), 6 deletions(-) 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 {