From 6d7e57ab344adb5121893383edb7df51def34c01 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 15 Sep 2026 11:22:52 +0800 Subject: [PATCH] test(06-13): tier hero save/load classes across all compressions (G-06-09-B) - EquipmentState percentage round trip per tier (NoCompression green; Low/High correct-expectation it.skip anchored #06-09-1) - HeroItems constant/consumable tables and HeroState attributes/location loop over the three compression tiers - HeroState container covers HeroLocation/HeroRendering across tiers; HeroEquipment container expectation it.skip anchored #06-09-2 - Existing #06-09-1/#06-09-2 skips unchanged --- .../data-base/src/hero/saveLoad.test.ts | 237 ++++++++++++------ 1 file changed, 160 insertions(+), 77 deletions(-) diff --git a/packages-user/data-base/src/hero/saveLoad.test.ts b/packages-user/data-base/src/hero/saveLoad.test.ts index 50e7e89..fe537e9 100644 --- a/packages-user/data-base/src/hero/saveLoad.test.ts +++ b/packages-user/data-base/src/hero/saveLoad.test.ts @@ -1,4 +1,4 @@ -// 测试勇士各子系统存读档:同实例往返、压缩档与码 58/59 +// 测试勇士吁E��系统存读档�E�同实例往返、压缩档与码E58/59 import { afterAll, describe, expect, it, vi } from 'vitest'; import { type IDataCommon, @@ -54,7 +54,7 @@ afterAll(() => { vi.unstubAllGlobals(); }); -/** 存读档测试覆盖的三档压缩级别 */ +/** 存读档测试要E��皁E��档压缩级别 */ const SAVE_COMPRESSIONS = [ SaveCompression.NoCompression, SaveCompression.LowCompression, @@ -71,7 +71,7 @@ interface EquipEnv { equipment: HeroEquipment; } -/** 构造一个含图块、道具存储与假录像系统的公共层假对象 */ +/** 极E��一个含图块、E��具存储与假录像系统的公共层假对象 */ function createState(): IDataCommon { const tileStore = new TileStore(); const itemStore = new ItemStore(); @@ -83,7 +83,7 @@ function createState(): IDataCommon { } as never; } -/** 构造一份合成的勇士基础属性 */ +/** 极E��一份合�E皁E��士基础属性 */ function createBaseAttr(): IHeroAttr { return { name: 'hero', @@ -99,7 +99,7 @@ function createBaseAttr(): IHeroAttr { }; } -/** 构造一个带合成装备属性的道具定义 */ +/** 极E��一个带合�E裁E��E��性皁E��具定乁E*/ function createEquipItem( num: number, id: string, @@ -126,7 +126,7 @@ function createEquipItem( }; } -/** 构造一个非装备类道具定义,用于覆盖背包分表存读档 */ +/** 极E��一个非裁E��E��道�E定义,用于要E��背包刁E��存读档 */ function createPlainItem( num: number, id: string, @@ -151,7 +151,7 @@ function createPlainItem( }; } -/** 向图块与道具存储注册一个道具定义 */ +/** 向图块与道具存储注册一个道�E定乁E*/ function registerItem(env: EquipEnv, item: IItemRawData): void { env.tileStore.addTile({ num: item.num, @@ -164,7 +164,7 @@ function registerItem(env: EquipEnv, item: IItemRawData): void { env.itemStore.addItem(item); } -/** 构造一个装配装备实例存储与勇士装备对象的测试环境 */ +/** 极E��一个裁E�E裁E��E��例存储与勇士裁E��E��象皁E��试环墁E*/ function createEquipEnv(): EquipEnv { const state = createState(); const store = new HeroEquipsStore(state); @@ -181,7 +181,7 @@ function createEquipEnv(): EquipEnv { }; } -/** 构造一个注册了跟随者图块的公共层假对象 */ +/** 极E��一个注册亁E��随老E��块的公共层假对象 */ function createFollowerState(): IDataCommon { const state = createState(); state.tileStore.addTile({ @@ -195,7 +195,7 @@ function createFollowerState(): IDataCommon { return state; } -/** 构造一个装配完成的勇士状态对象 */ +/** 极E��一个裁E�E完�E皁E��士状态对象 */ function createHeroState(): IHeroState { return new HeroState( createState(), @@ -204,8 +204,26 @@ function createHeroState(): IHeroState { ); } +/** 在持E��压缩档下对裁E��E��刁E��加成做一次同实例往返,返回读档后的修饰器值 */ +function roundTripPercentageModifier(compression: SaveCompression): unknown { + const env = createEquipEnv(); + const item = createEquipItem(10, 'sword', [0], [], [['hp', 0.5]]); + registerItem(env, item); + const state = new EquipmentState(0, item); + const modifier = [...state.getModifiers()][0][1]; + modifier.setValue(0.9); + + const saved = state.saveState(compression); + state.loadState(saved, compression); + + const restored = [...state.getModifiers()].find( + ([, current]) => current instanceof PercentageModifier + ); + return restored?.[1].getValue(); +} + describe('Hero modifier save and load round trips', () => { - // 验证 ValueModifier 的数值在三个压缩档下均能同实例恢复 + // 验证EValueModifier 皁E��值在三个压缩档下均能同实例恢夁E it('restores a value modifier across all compressions', () => { for (const compression of SAVE_COMPRESSIONS) { const modifier = new ValueModifier(5); @@ -218,7 +236,7 @@ describe('Hero modifier save and load round trips', () => { } }); - // 验证 PercentageModifier 的数值在三个压缩档下均能同实例恢复 + // 验证EPercentageModifier 皁E��值在三个压缩档下均能同实例恢夁E it('restores a percentage modifier across all compressions', () => { for (const compression of SAVE_COMPRESSIONS) { const modifier = new PercentageModifier(0.25); @@ -233,7 +251,7 @@ describe('Hero modifier save and load round trips', () => { }); describe('HeroLocation save and load round trips', () => { - // 验证坐标、楼层与朝向在同实例上完整恢复 + // 验证坐栁E��楼层与朝向在同实例上完整恢夁E it('restores position, floor and direction on the same instance', () => { const location = new HeroLocation( createState(), @@ -258,7 +276,7 @@ describe('HeroLocation save and load round trips', () => { }); describe('HeroRendering save and load round trips', () => { - // 验证不透明度在同实例上恢复 + // 验证不透�E度在同实例上恢夁E it('restores alpha on the same instance', () => { const rendering = new HeroRendering(createState()); rendering.setAlpha(0.25); @@ -272,7 +290,7 @@ describe('HeroRendering save and load round trips', () => { }); describe('HeroEquipment save and load round trips', () => { - // 验证装备槽与已装备映射在同实例上恢复(存盘经结构化克隆模拟真实序列化) + // 验证裁E��E��与已裁E��E��封E��同实例上恢复(存盘经结构化�E隁E��拟真实序�E化!E it('restores slots and equipped mapping on the same instance', () => { const env = createEquipEnv(); registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]])); @@ -290,7 +308,7 @@ describe('HeroEquipment save and load round trips', () => { expect(env.equipment.slots).toEqual(['weapon']); }); - // 疑似 bug:saveState 直接返回内部 equipped/slots 引用而非深拷贝,详见 06-TEST-FINDINGS.md #06-09-2 + // 疑似 bug�E�saveState 直接返回冁E�� equipped/slots 引用而非深拷贝,详见E06-TEST-FINDINGS.md #06-09-2 it.skip('returns an equipment snapshot independent from the live state', () => { const env = createEquipEnv(); registerItem(env, createEquipItem(10, 'sword', [0], [['atk', 5]])); @@ -308,25 +326,28 @@ describe('HeroEquipment save and load round trips', () => { }); describe('EquipmentState save and load round trips', () => { - // 验证百分比加成的数值在同实例上恢复(无压缩档) - it('restores a percentage modifier on the same instance', () => { - const env = createEquipEnv(); - const item = createEquipItem(10, 'sword', [0], [], [['hp', 0.5]]); - registerItem(env, item); - const state = new EquipmentState(0, item); - const modifier = [...state.getModifiers()][0][1]; - modifier.setValue(0.9); - - const saved = state.saveState(SaveCompression.NoCompression); - state.loadState(saved, SaveCompression.NoCompression); - - const restored = [...state.getModifiers()].find( - ([, current]) => current instanceof PercentageModifier + // 验证百刁E��加成在无压缩档下同实例恢复到存档点 + it('restores a percentage modifier on the same instance in NoCompression', () => { + expect(roundTripPercentageModifier(SaveCompression.NoCompression)).toBe( + 0.5 ); - expect(restored?.[1].getValue()).toBe(0.5); }); - // 疑似 bug:loadNoCompression 误从存档百分比表读取数值表,详见 06-TEST-FINDINGS.md #06-09-1 + // 疑似 bug�E�Low 压缩档读档未回退到裁E��E��始定义,未修改皁E��刁E��加成丢失�E�详见E06-TEST-FINDINGS.md #06-09-1 + it.skip('restores a percentage modifier on the same instance in LowCompression', () => { + expect( + roundTripPercentageModifier(SaveCompression.LowCompression) + ).toBe(0.5); + }); + + // 疑似 bug�E�High 压缩档读档未回退到裁E��E��始定义,未修改皁E��刁E��加成丢失�E�详见E06-TEST-FINDINGS.md #06-09-1 + it.skip('restores a percentage modifier on the same instance in HighCompression', () => { + expect( + roundTripPercentageModifier(SaveCompression.HighCompression) + ).toBe(0.5); + }); + + // 疑似 bug�E�loadNoCompression 误从存档百刁E��表读取数值表�E�详见E06-TEST-FINDINGS.md #06-09-1 it.skip('restores a value modifier on the same instance', () => { const env = createEquipEnv(); const item = createEquipItem(10, 'sword', [0], [['atk', 5]]); @@ -344,7 +365,7 @@ describe('EquipmentState save and load round trips', () => { expect(restored?.[1].getValue()).toBe(5); }); - // 疑似 bug:loadDiff 清空后未回退到装备原始定义,未修改的加成丢失,详见 06-TEST-FINDINGS.md #06-09-1 + // 疑似 bug�E�loadDiff 渁E��后未回退到裁E��E��始定义,未修改皁E��成丢失�E�详见E06-TEST-FINDINGS.md #06-09-1 it.skip('keeps unchanged value modifiers for compressed snapshots', () => { const env = createEquipEnv(); const item = createEquipItem(10, 'sword', [0], [['atk', 5]]); @@ -361,7 +382,7 @@ describe('EquipmentState save and load round trips', () => { }); describe('HeroEquipsStore save and load round trips', () => { - // 验证装备实例在三个压缩档下同实例恢复并续接自增 uid + // 验证裁E��E��例在三个压缩档下同实例恢复并续接自墁Euid it('restores equipment instances and the uid counter across all compressions', () => { for (const compression of SAVE_COMPRESSIONS) { const env = createEquipEnv(); @@ -379,7 +400,7 @@ describe('HeroEquipsStore save and load round trips', () => { } }); - // 验证存档缺失道具原始数据时经 logger.catch 观测到错误码 59 + // 验证存档缺失道�E原始数据时绁Elogger.catch 观测到错误码E59 it('warns code 59 when the item raw data is missing', () => { const env = createEquipEnv(); const saved = { @@ -400,7 +421,7 @@ describe('HeroEquipsStore save and load round trips', () => { expect(result.info.map(info => info.code)).toContain(59); }); - // 验证存档不含任何装备实例时经 logger.catch 观测到错误码 58 + // 验证存档不含任何裁E��E��例时绁Elogger.catch 观测到错误码E58 it('warns code 58 when the max equipment uid cannot be found', () => { const env = createEquipEnv(); @@ -416,31 +437,36 @@ describe('HeroEquipsStore save and load round trips', () => { }); describe('HeroItems save and load round trips', () => { - // 验证永久与消耗道具分表在同实例上恢复 - it('restores constant and consumable item tables on the same instance', () => { - const env = createEquipEnv(); - registerItem(env, createPlainItem(20, 'key', ItemCategory.Constant)); - registerItem( - env, - createPlainItem(21, 'potion', ItemCategory.Consumable) - ); - const items = new HeroItems(env.state); - items.addItem(20, 3); - items.addItem(21, 2); + // 验证永乁E��消耗道具刁E��在三个压缩档下同实例恢夁E + it('restores constant and consumable item tables across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + const env = createEquipEnv(); + registerItem( + env, + createPlainItem(20, 'key', ItemCategory.Constant) + ); + registerItem( + env, + createPlainItem(21, 'potion', ItemCategory.Consumable) + ); + const items = new HeroItems(env.state); + items.addItem(20, 3); + items.addItem(21, 2); - const saved = items.saveState(SaveCompression.NoCompression); - items.addItem(20, -3); - items.addItem(21, -2); - items.loadState(saved, SaveCompression.NoCompression); + const saved = items.saveState(compression); + items.addItem(20, -3); + items.addItem(21, -2); + items.loadState(saved, compression); - expect(items.itemCount(20)).toBe(3); - expect(items.itemCount(21)).toBe(2); - expect(items.getItemState(20)?.id).toBe('key'); + expect(items.itemCount(20)).toBe(3); + expect(items.itemCount(21)).toBe(2); + expect(items.getItemState(20)?.id).toBe('key'); + } }); }); describe('HeroFollower save and load round trips', () => { - // 验证跟随者位置、楼层与渲染在同实例上恢复 + // 验证跟随老E��置、楼层与渲染在同实例上恢夁E it('restores follower location and rendering on the same instance', () => { const state = createFollowerState(); const faceHandler = new Dir8FaceHandler(); @@ -480,32 +506,38 @@ describe('HeroFollower save and load round trips', () => { }); describe('HeroState save and load round trips', () => { - // 验证属性、修饰器与位置在同实例上恢复并重建属性对象 - it('restores attributes, modifiers and location on the same instance', () => { - const hero = createHeroState(); - hero.registerModifier('@system/value', () => new ValueModifier(5)); - hero.createAndInsertModifier('@system/value', 'atk'); - hero.getModifiableAttribute().set('hp', 88); - hero.location.setPos(1, 1); - hero.location.setFloor('F1'); - hero.location.mover.setFaceDir(FaceDirection.Up); + // 验证属性、修饰器与位置在三个压缩档下同实例恢复并重建属性对象 + it('restores attributes, modifiers and location across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + const hero = createHeroState(); + hero.registerModifier('@system/value', () => new ValueModifier(5)); + hero.createAndInsertModifier('@system/value', 'atk'); + hero.getModifiableAttribute().set('hp', 88); + hero.location.setPos(1, 1); + hero.location.setFloor('F1'); + hero.location.mover.setFaceDir(FaceDirection.Up); - const saved = hero.saveState(SaveCompression.NoCompression); - hero.getModifiableAttribute().set('hp', 1); - hero.location.setPos(9, 9); - hero.loadState(saved, SaveCompression.NoCompression); + const saved = hero.saveState(compression); + hero.getModifiableAttribute().set('hp', 1); + hero.location.setPos(9, 9); + hero.loadState(saved, compression); - expect(hero.getModifiableAttribute().getBaseAttribute('hp')).toBe(88); - expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe(15); - expect(hero.getLocation()).toEqual({ - x: 1, - y: 1, - direction: FaceDirection.Up - }); - expect(hero.location.floorId).toBe('F1'); + expect(hero.getModifiableAttribute().getBaseAttribute('hp')).toBe( + 88 + ); + expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe( + 15 + ); + expect(hero.getLocation()).toEqual({ + x: 1, + y: 1, + direction: FaceDirection.Up + }); + expect(hero.location.floorId).toBe('F1'); + } }); - // 验证存盘禁用(save=false)的修饰器不进入存档且读档后不再存在 + // 验证存盘禁用�E�Eave=false�E�的修饰器不进�E存档且读档后不�E存在 it('excludes save-disabled modifiers from the snapshot', () => { const hero = createHeroState(); hero.registerModifier('@system/value', () => new ValueModifier(5)); @@ -525,3 +557,54 @@ describe('HeroState save and load round trips', () => { expect(hero.getModifiableAttribute().getFinalAttribute('atk')).toBe(15); }); }); + +describe('HeroState container save and load coverage for sub systems', () => { + // 验证不接受压缩参数皁E��位与渲染经勇士容器三档往返均恢复到存档点 + it('restores location and rendering through the container across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + const hero = createHeroState(); + hero.location.setPos(2, 3); + hero.location.setFloor('F2'); + hero.location.mover.setFaceDir(FaceDirection.Right); + hero.rendering.setAlpha(0.25); + + const saved = hero.saveState(compression); + hero.location.setPos(9, 9); + hero.location.setFloor('F9'); + hero.location.mover.setFaceDir(FaceDirection.Down); + hero.rendering.setAlpha(1); + hero.loadState(saved, compression); + + expect(hero.location.x).toBe(2); + expect(hero.location.y).toBe(3); + expect(hero.location.floorId).toBe('F2'); + expect(hero.location.getCurrentFaceDirection()).toBe( + FaceDirection.Right + ); + expect(hero.rendering.alpha).toBe(0.25); + } + }); + + // 疑似 bug�E�HeroEquipment.saveState 未深拷贁Eequipped/slots�E�经容器三档往返均丢失已裁E��E��封E��详见E06-TEST-FINDINGS.md #06-09-2 + it.skip('restores the equipped mapping through the container across all compressions', () => { + for (const compression of SAVE_COMPRESSIONS) { + 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']); + const uid = hero.items.equipment.add(10); + hero.equip.equip(uid, 0); + + const saved = hero.saveState(compression); + hero.equip.unequip(0); + hero.loadState(saved, compression); + + expect(hero.equip.getEquipped(0)).toBe(uid); + expect(hero.equip.slots).toEqual(['weapon']); + } + }); +});