diff --git a/packages-user/data-base/src/hero/__test__/attribute.test.ts b/packages-user/data-base/src/hero/__test__/attribute.test.ts index fa7d971..92026d0 100644 --- a/packages-user/data-base/src/hero/__test__/attribute.test.ts +++ b/packages-user/data-base/src/hero/__test__/attribute.test.ts @@ -1,6 +1,7 @@ // 测试 HeroAttribute 构件:基础/最终属性、修饰器增删排序、存盘开关、克隆、自身存读档与告警码 108/109、克隆体修饰器簿记与存档开关 import { afterAll, describe, expect, it, vi } from 'vitest'; -import { BaseHeroModifier, HeroAttribute } from '../attribute'; +import { HeroAttribute } from '../attribute'; +import { BaseHeroModifier } from '../modifier'; import { logger } from '@motajs/common'; import { SaveCompression } from '@user/data-common'; import { type IHeroModifier } from '../types'; diff --git a/packages-user/data-base/src/hero/__test__/equipStore.test.ts b/packages-user/data-base/src/hero/__test__/equipStore.test.ts index 6675556..1837624 100644 --- a/packages-user/data-base/src/hero/__test__/equipStore.test.ts +++ b/packages-user/data-base/src/hero/__test__/equipStore.test.ts @@ -66,9 +66,10 @@ interface TestEnv { /** 构造一个仅含图块与道具存储的公共层假对象 */ function createState(): IDataCommon { + const tileStore = new TileStore(); return { - tileStore: new TileStore(), - itemStore: new ItemStore() + tileStore, + itemStore: new ItemStore(tileStore) } as never; } @@ -101,8 +102,8 @@ function createItem( equip: { slots: [0], animate: 'sword', - value: new Map(value), - percentage: new Map(percentage), + value: Object.fromEntries(value), + percentage: Object.fromEntries(percentage), loadEvent: null, unloadEvent: null } @@ -242,9 +243,9 @@ function createValueSave( }; } -describe('HeroEquipsStore same-reference load (#06-17-4)', () => { - // 验证同 uid 装备实例在三档压缩读档前后为同一实例,且数值恢复到存档点 - it('keeps the same instance and restores values across all compressions', () => { +describe('HeroEquipsStore load semantics (#06-17-4 superseded)', () => { + // 验证同 uid 装备实例在三档压缩读档后按存档值重建,且数值恢复到存档点 + it('rebuilds the instance and restores values across all compressions', () => { for (const compression of SAVE_COMPRESSIONS) { const env = createEnv(); registerItem(env, createItem(10, 'sword', [['atk', 5]])); @@ -263,8 +264,9 @@ describe('HeroEquipsStore same-reference load (#06-17-4)', () => { env.store.loadState(saved, compression); - expect(env.store.get(uid)).toBe(before); - expect([...before.getModifiers()][0][1].getValue()).toBe(9); + const after = env.store.get(uid)!; + expect(after).not.toBe(before); + expect([...after.getModifiers()][0][1].getValue()).toBe(9); } }); @@ -280,7 +282,8 @@ describe('HeroEquipsStore same-reference load (#06-17-4)', () => { env.store.loadState(saved, SaveCompression.NoCompression); - expect(env.store.get(kept)).toBe(before); + expect(env.store.get(kept)).not.toBe(before); + expect(env.store.get(kept)?.item.num).toBe(10); expect(env.store.get(extra)).toBeNull(); expect(env.store.count(10)).toBe(1); }); diff --git a/packages-user/data-base/src/hero/__test__/equipment.test.ts b/packages-user/data-base/src/hero/__test__/equipment.test.ts index 1de309c..1e5bc5e 100644 --- a/packages-user/data-base/src/hero/__test__/equipment.test.ts +++ b/packages-user/data-base/src/hero/__test__/equipment.test.ts @@ -60,8 +60,8 @@ interface TestEnv { replaySystem: ReplaySystemStub; } -/** 录像路由桩,只保留真正进入录像的指令,禁用窗口内的写入被丢弃 */ -interface ReplayRouteStub { +/** 录像数组桩,只保留真正进入录像的指令,禁用窗口内的写入被丢弃 */ +interface ReplayArrayStub { /** * 写入一条录像指令 * @param code 指令码 @@ -74,7 +74,7 @@ interface ReplayRouteStub { /** 录像系统桩,用禁用计数复现录像数组在禁用窗口内丢弃指令的行为 */ interface ReplaySystemStub { - route: ReplayRouteStub; + array: ReplayArrayStub; disable: ReturnType; revert: ReturnType; } @@ -83,7 +83,7 @@ interface ReplaySystemStub { function createReplaySystem(): ReplaySystemStub { let disabled = 0; const commands: [ReplayCode, unknown[]][] = []; - const route: ReplayRouteStub = { + const array: ReplayArrayStub = { add(code, params) { if (disabled > 0) return; commands.push([code, params]); @@ -96,7 +96,7 @@ function createReplaySystem(): ReplaySystemStub { const revert = vi.fn(() => { if (disabled > 0) disabled--; }); - return { route, disable, revert }; + return { array, disable, revert }; } /** 构造一份合成的勇士基础属性 */ @@ -134,8 +134,8 @@ function createItem( equip: { slots, animate: 'sword', - value: new Map(value), - percentage: new Map(percentage), + value: Object.fromEntries(value), + percentage: Object.fromEntries(percentage), loadEvent: null, unloadEvent: null } @@ -145,8 +145,8 @@ function createItem( /** 构造一个装配勇士装备对象的测试环境 */ function createEnv(): TestEnv { const tileStore = new TileStore(); - const itemStore = new ItemStore(); - // 录像系统桩,用于满足装备/卸下时的 route.add 记录与临时禁用录像 + const itemStore = new ItemStore(tileStore); + // 录像系统桩,用于满足装备/卸下时的 array.add 记录与临时禁用录像 const replaySystem = createReplaySystem(); const state = { tileStore, itemStore, replaySystem } as never; const attribute = new HeroAttribute(createBaseAttr()); @@ -374,7 +374,7 @@ describe('HeroEquipment replay isolation on load', () => { expect(env.attribute.getFinalAttribute('atk')).toBe(15); env.equipment.unequip(0); - env.replaySystem.route.commands.length = 0; + env.replaySystem.array.commands.length = 0; env.replaySystem.disable.mockClear(); env.replaySystem.revert.mockClear(); @@ -382,7 +382,7 @@ describe('HeroEquipment replay isolation on load', () => { const disableCount = env.replaySystem.disable.mock.calls.length; const revertCount = env.replaySystem.revert.mock.calls.length; - expect(env.replaySystem.route.commands).toEqual([]); + expect(env.replaySystem.array.commands).toEqual([]); expect(disableCount).toBeGreaterThan(0); expect(revertCount).toBe(disableCount); expect(env.equipment.getEquipped(0)).toBe(uid); @@ -400,14 +400,14 @@ describe('HeroEquipment replay isolation on load', () => { env.equipment.unequip(0); env.equipment.loadState(saved); - env.replaySystem.route.commands.length = 0; + env.replaySystem.array.commands.length = 0; env.equipment.unequip(0); - expect(env.replaySystem.route.commands).toEqual([ + expect(env.replaySystem.array.commands).toEqual([ [ReplayCode.Unequip, [0]] ]); env.equipment.equip(uid, 0); - expect(env.replaySystem.route.commands).toEqual([ + expect(env.replaySystem.array.commands).toEqual([ [ReplayCode.Unequip, [0]], [ReplayCode.Equip, [uid]] ]); diff --git a/packages-user/data-base/src/hero/__test__/follower.test.ts b/packages-user/data-base/src/hero/__test__/follower.test.ts index cbdc9a4..7f18a09 100644 --- a/packages-user/data-base/src/hero/__test__/follower.test.ts +++ b/packages-user/data-base/src/hero/__test__/follower.test.ts @@ -54,7 +54,7 @@ interface TestEnv { /** 构造一个注册了跟随者图块的公共层假对象 */ function createState(): IDataCommon { const tileStore = new TileStore(); - const itemStore = new ItemStore(); + const itemStore = new ItemStore(tileStore); tileStore.addTile({ num: 100, id: 'ghost', @@ -63,8 +63,8 @@ function createState(): IDataCommon { pass: { onlyEvents: false, inPass: 15, outPass: 15 }, eventPass: true }); - // 录像系统桩,仅用于满足移动时的 route.add 记录 - const replaySystem = { route: { add: vi.fn() } }; + // 录像系统桩,仅用于满足移动时的 array.add 记录 + const replaySystem = { array: { add: vi.fn() } }; return { tileStore, itemStore, replaySystem } as never; } @@ -121,8 +121,7 @@ describe('HeroFollowersController members', () => { onAddFollower: (follower, index) => { added.push([follower.num, index]); } - }) - .load(); + }); env.location.setPos(2, 3); env.location.mover.setFaceDir(FaceDirection.Up); @@ -183,8 +182,7 @@ describe('HeroFollowersController members', () => { onRemoveFollower: (_follower, index) => { removed.push(index); } - }) - .load(); + }); env.controller.addFollower(100); const second = env.controller.addFollower(100); @@ -211,8 +209,7 @@ describe('HeroFollowersController gathering', () => { onGatherFollowers: sync => { gathered.push(sync); } - }) - .load(); + }); const follower = env.controller.addFollower(100); env.location.setPos(5, 6); env.location.mover.setFaceDir(FaceDirection.Right); @@ -237,8 +234,7 @@ describe('HeroFollowersController gathering', () => { onGatherFollowers: sync => { gathered.push(sync); } - }) - .load(); + }); env.location.mover.useTopImplementation(new FakeTopImpl()); follower.location.mover.useTopImplementation(new FakeTopImpl()); @@ -278,8 +274,7 @@ describe('HeroFollowersController same-reference restore (#06-17-6)', () => { .addHook({ onAddFollower: (_follower, index) => added.push(index), onRemoveFollower: (_follower, index) => removed.push(index) - }) - .load(); + }); const follower = env.controller.addFollower(100); follower.location.setPos(2, 3); follower.rendering.setAlpha(0.5); @@ -307,8 +302,7 @@ describe('HeroFollowersController same-reference restore (#06-17-6)', () => { .addHook({ onAddFollower: (_follower, index) => added.push(index), onRemoveFollower: (_follower, index) => removed.push(index) - }) - .load(); + }); const follower = env.controller.addFollower(100); const save = follower.saveState(SaveCompression.NoCompression); @@ -330,8 +324,7 @@ describe('HeroFollowersController same-reference restore (#06-17-6)', () => { env.controller .addHook({ onRemoveFollower: (_follower, index) => removed.push(index) - }) - .load(); + }); const first = env.controller.addFollower(100); env.controller.addFollower(100); @@ -351,8 +344,7 @@ describe('HeroFollowersController same-reference restore (#06-17-6)', () => { env.controller .addHook({ onAddFollower: (_follower, index) => added.push(index) - }) - .load(); + }); const first = env.controller.addFollower(100); const save = first.saveState(SaveCompression.NoCompression); diff --git a/packages-user/data-base/src/hero/__test__/items.test.ts b/packages-user/data-base/src/hero/__test__/items.test.ts index ebd1657..9608450 100644 --- a/packages-user/data-base/src/hero/__test__/items.test.ts +++ b/packages-user/data-base/src/hero/__test__/items.test.ts @@ -57,9 +57,9 @@ interface ItemFixture { /** 构造一个装配勇士道具对象的测试环境 */ function createEnv(): TestEnv { const tileStore = new TileStore(); - const itemStore = new ItemStore(); - // 录像系统桩,仅用于满足道具使用时的 route.add 记录 - const replaySystem = { route: { add: vi.fn() } }; + const itemStore = new ItemStore(tileStore); + // 录像系统桩,仅用于满足道具使用时的 array.add 记录 + const replaySystem = { array: { add: vi.fn() } }; const state = { tileStore, itemStore, replaySystem } as never; return { state, @@ -129,12 +129,12 @@ describe('HeroItems counting', () => { expect(env.items.getItemState(20)).toBeNull(); }); - // 验证 getItem 等价于增加一个道具并支持字符串 id - it('gets one item through getItem and string ids', () => { + // 验证 addItem 支持字符串 id 并增加计数 + it('adds one item through addItem and string ids', () => { const env = createEnv(); registerItem(env, createItem(20, 'key', ItemCategory.Constant).item); - env.items.getItem('key'); + env.items.addItem('key'); expect(env.items.itemCount(20)).toBe(1); expect(env.items.itemCount('key')).toBe(1); @@ -176,7 +176,7 @@ describe('HeroItems counting', () => { }); env.items.addItem(99); - env.items.getItem('missing'); + env.items.addItem('missing'); env.items.addItem(77, 3); expect(env.items.itemCount(99)).toBe(0); diff --git a/packages-user/data-base/src/hero/__test__/location.test.ts b/packages-user/data-base/src/hero/__test__/location.test.ts index 70d9cc8..243992e 100644 --- a/packages-user/data-base/src/hero/__test__/location.test.ts +++ b/packages-user/data-base/src/hero/__test__/location.test.ts @@ -8,6 +8,7 @@ import { TileStore } from '@user/data-common'; import { HeroLocation } from '../location'; +import { type IGameMap, MapState } from '../../map'; vi.hoisted(() => { vi.stubGlobal('main', { replayChecking: true }); @@ -41,9 +42,10 @@ afterAll(() => { /** 构造一个仅包含图块与道具存储的公共层假对象 */ function createState(): IDataCommon { + const tileStore = new TileStore(); return { - tileStore: new TileStore(), - itemStore: new ItemStore() + tileStore, + itemStore: new ItemStore(tileStore) } as never; } @@ -61,6 +63,11 @@ function createLocation(): HeroLocation { ); } +/** 构造一个单格的真实游戏地图对象,供 setFloor 使用 */ +function createMap(floorId: string): IGameMap { + return new MapState(createState()).createMap(floorId, 1, 1); +} + describe('HeroLocation position and floor', () => { // 验证初始坐标、未定楼层与朝向来自构造时的定位器 it('initializes position, face direction and an undefined floor', () => { @@ -82,8 +89,7 @@ describe('HeroLocation position and floor', () => { onSetPos: (x, y) => { calls.push([x, y]); } - }) - .load(); + }); location.setPos(7, 9); @@ -92,24 +98,23 @@ describe('HeroLocation position and floor', () => { expect(calls).toEqual([[7, 9]]); }); - // 验证 setFloor 更新楼层并触发 onSetFloor 钩子 + // 验证 setFloor 更新楼层并触发 onSetFloor 钩子,且可传 null 清空 it('updates the floor and notifies the onSetFloor hook', () => { const location = createLocation(); - const floors: (string | undefined)[] = []; - location - .addHook({ - onSetFloor: floorId => { - floors.push(floorId); - } - }) - .load(); + const floors: (IGameMap | null)[] = []; + location.addHook({ + onSetFloor: map => { + floors.push(map); + } + }); - location.setFloor('F2'); + const floor = createMap('F2'); + location.setFloor(floor); expect(location.floorId).toBe('F2'); - location.setFloor(undefined); + location.setFloor(null); expect(location.floorId).toBeUndefined(); - expect(floors).toEqual(['F2', undefined]); + expect(floors).toEqual([floor, null]); }); // 验证朝向经由共享移动器读写 diff --git a/packages-user/data-base/src/hero/__test__/mover.test.ts b/packages-user/data-base/src/hero/__test__/mover.test.ts index 10fee8e..86abd17 100644 --- a/packages-user/data-base/src/hero/__test__/mover.test.ts +++ b/packages-user/data-base/src/hero/__test__/mover.test.ts @@ -44,11 +44,12 @@ afterAll(() => { /** 构造一个仅含图块与道具存储的公共层假对象 */ function createState(): IDataCommon { - // 录像系统桩,仅用于满足移动时的 route.add 记录 - const replaySystem = { route: { add: vi.fn() } }; + // 录像系统桩,仅用于满足移动时的 array.add 记录 + const replaySystem = { array: { add: vi.fn() } }; + const tileStore = new TileStore(); return { - tileStore: new TileStore(), - itemStore: new ItemStore(), + tileStore, + itemStore: new ItemStore(tileStore), replaySystem } as never; } diff --git a/packages-user/data-base/src/hero/__test__/rendering.test.ts b/packages-user/data-base/src/hero/__test__/rendering.test.ts index 71b2d1b..239e5a1 100644 --- a/packages-user/data-base/src/hero/__test__/rendering.test.ts +++ b/packages-user/data-base/src/hero/__test__/rendering.test.ts @@ -36,9 +36,10 @@ afterAll(() => { /** 构造一个仅含图块与道具存储的公共层假对象 */ function createState(): IDataCommon { + const tileStore = new TileStore(); return { - tileStore: new TileStore(), - itemStore: new ItemStore() + tileStore, + itemStore: new ItemStore(tileStore) } as never; } @@ -65,7 +66,7 @@ describe('HeroRendering alpha', () => { it('updates alpha and notifies the onSetAlpha hook', () => { const rendering = new HeroRendering(createState()); const hook = new FakeRenderingHook(); - rendering.addHook(hook).load(); + rendering.addHook(hook); rendering.setAlpha(0.25); @@ -78,7 +79,6 @@ describe('HeroRendering alpha', () => { const rendering = new HeroRendering(createState()); const hook = new FakeRenderingHook(); const controller = rendering.addHook(hook); - controller.load(); rendering.setAlpha(0.5); controller.unload(); diff --git a/packages-user/data-base/src/hero/__test__/saveLoad.test.ts b/packages-user/data-base/src/hero/__test__/saveLoad.test.ts index c92ced8..b5cca5f 100644 --- a/packages-user/data-base/src/hero/__test__/saveLoad.test.ts +++ b/packages-user/data-base/src/hero/__test__/saveLoad.test.ts @@ -22,6 +22,7 @@ import { HeroEquipment } from '../equipment'; import { EquipmentState, HeroEquipsStore } from '../equipStore'; import { HeroItems } from '../items'; import { HeroFollowersController } from '../follower'; +import { type IGameMap, MapState } from '../../map'; import { type IHeroState } from '../types'; vi.hoisted(() => { @@ -74,15 +75,20 @@ interface EquipEnv { /** 极E��一个含图块、E��具存储与假录像系统的公共层假对象 */ function createState(): IDataCommon { const tileStore = new TileStore(); - const itemStore = new ItemStore(); - const route = { add: vi.fn() }; + const itemStore = new ItemStore(tileStore); + const array = { add: vi.fn() }; return { tileStore, itemStore, - replaySystem: { route, disable: vi.fn(), revert: vi.fn() } + replaySystem: { array, disable: vi.fn(), revert: vi.fn() } } as never; } +/** 构造一个单格的真实游戏地图对象,供 setFloor 使用 */ +function createMap(floorId: string): IGameMap { + return new MapState(createState()).createMap(floorId, 1, 1); +} + /** 极E��一份合�E皁E��士基础属性 */ function createBaseAttr(): IHeroAttr { return { @@ -118,8 +124,8 @@ function createEquipItem( equip: { slots, animate: 'sword', - value: new Map(value), - percentage: new Map(percentage), + value: Object.fromEntries(value), + percentage: Object.fromEntries(percentage), loadEvent: null, unloadEvent: null } @@ -204,15 +210,6 @@ function createHeroState(): IHeroState { ); } -/** 构造一个已注册跟随者图块、可直接添加跟随者的勇士状态对象 */ -function createFollowerHero(): IHeroState { - return new HeroState( - createFollowerState(), - new Dir8FaceHandler(), - new HeroAttribute(createBaseAttr()) - ); -} - /** 构造一个已注册装备定义并设置好装备槽的勇士状态对象(基础 atk 为 10) */ function createEquipHero(): IHeroState { const env = createEquipEnv(); @@ -236,9 +233,10 @@ function roundTripPercentageModifier(compression: SaveCompression): unknown { modifier.setValue(0.9); const saved = state.saveState(compression); - state.loadState(saved, compression); + const restoredState = new EquipmentState(0, item); + restoredState.loadState(saved, compression); - const restored = [...state.getModifiers()].find( + const restored = [...restoredState.getModifiers()].find( ([, current]) => current instanceof PercentageModifier ); return restored?.[1].getValue(); @@ -281,12 +279,12 @@ describe('HeroLocation save and load round trips', () => { new Dir8FaceHandler() ); location.setPos(1, 2); - location.setFloor('F2'); + location.setFloor(createMap('F2')); location.mover.setFaceDir(FaceDirection.Up); const saved = location.saveState(); location.setPos(9, 9); - location.setFloor('F9'); + location.setFloor(createMap('F9')); location.mover.setFaceDir(FaceDirection.Down); location.loadState(saved); @@ -348,29 +346,29 @@ describe('HeroEquipment save and load round trips', () => { }); describe('EquipmentState save and load round trips', () => { - // 验证百刁E��加成在无压缩档下同实例恢复到存档点 - it('restores a percentage modifier on the same instance in NoCompression', () => { + // 验证百刁E��加成在无压缩档下读档到新实例并恢复到存档点 + it('restores a percentage modifier into a fresh instance in NoCompression', () => { expect(roundTripPercentageModifier(SaveCompression.NoCompression)).toBe( 0.5 ); }); // 验证 Low 压缩档读档以装备原始定义为回退基准,未修改的百分比加成不丢失(#06-09-1) - it('restores a percentage modifier on the same instance in LowCompression', () => { + it('restores a percentage modifier into a fresh instance in LowCompression', () => { expect( roundTripPercentageModifier(SaveCompression.LowCompression) ).toBe(0.5); }); // 验证 High 压缩档读档以装备原始定义为回退基准,未修改的百分比加成不丢失(#06-09-1) - it('restores a percentage modifier on the same instance in HighCompression', () => { + it('restores a percentage modifier into a fresh instance in HighCompression', () => { expect( roundTripPercentageModifier(SaveCompression.HighCompression) ).toBe(0.5); }); - // 验证 NoCompression 读档按 value/percentage 分表恢复数值修饰器(#06-09-1) - it('restores a value modifier on the same instance', () => { + // 验证 NoCompression 读档按 value/percentage 分表恢复到新实例的数值修饰器(#06-09-1) + it('restores a value modifier into a fresh instance', () => { const env = createEquipEnv(); const item = createEquipItem(10, 'sword', [0], [['atk', 5]]); registerItem(env, item); @@ -379,9 +377,10 @@ describe('EquipmentState save and load round trips', () => { modifier.setValue(99); const saved = state.saveState(SaveCompression.NoCompression); - state.loadState(saved, SaveCompression.NoCompression); + const restoredState = new EquipmentState(0, item); + restoredState.loadState(saved, SaveCompression.NoCompression); - const restored = [...state.getModifiers()].find( + const restored = [...restoredState.getModifiers()].find( ([name]) => name === 'atk' ); expect(restored?.[1].getValue()).toBe(5); @@ -508,14 +507,14 @@ describe('HeroFollower save and load round trips', () => { ); const follower = controller.addFollower(100); follower.location.setPos(2, 3); - follower.location.setFloor('F1'); + follower.location.setFloor(createMap('F1')); follower.location.mover.setFaceDir(FaceDirection.Up); follower.rendering.setAlpha(0.5); for (const compression of SAVE_COMPRESSIONS) { const saved = follower.saveState(compression); follower.location.setPos(9, 9); - follower.location.setFloor('F9'); + follower.location.setFloor(createMap('F9')); follower.location.mover.setFaceDir(FaceDirection.Down); follower.rendering.setAlpha(1); follower.loadState(saved, compression); @@ -540,7 +539,7 @@ describe('HeroState save and load round trips', () => { hero.createAndInsertModifier('@system/value', 'atk'); hero.getModifiableAttribute().set('hp', 88); hero.location.setPos(1, 1); - hero.location.setFloor('F1'); + hero.location.setFloor(createMap('F1')); hero.location.mover.setFaceDir(FaceDirection.Up); const saved = hero.saveState(compression); @@ -585,20 +584,18 @@ describe('HeroState save and load round trips', () => { }); describe('HeroState container save and load coverage for sub systems', () => { - // 验证不接受压缩参数皁E��位与渲染经勇士容器三档往返均恢复到存档点 - it('restores location and rendering through the container across all compressions', () => { + // 验证位置经勇士容器三档往返均恢复到存档点 + it('restores location 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.setFloor(createMap('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.setFloor(createMap('F9')); hero.location.mover.setFaceDir(FaceDirection.Down); - hero.rendering.setAlpha(1); hero.loadState(saved, compression); expect(hero.location.x).toBe(2); @@ -607,7 +604,6 @@ describe('HeroState container save and load coverage for sub systems', () => { expect(hero.location.getCurrentFaceDirection()).toBe( FaceDirection.Right ); - expect(hero.rendering.alpha).toBe(0.25); } }); @@ -704,9 +700,9 @@ 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', () => { +describe('HeroState equipment load semantics (#06-17-4 superseded)', () => { + // 验证经勇士容器三档往返后装备实例按存档值重建,且数值恢复到存档点 + it('rebuilds 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); @@ -734,8 +730,9 @@ describe('HeroState same-reference equipment load (#06-17-4)', () => { hero.loadState(saved, compression); - expect(hero.items.equipment.get(uid)).toBe(before); - expect([...before.getModifiers()][0][1].getValue()).toBe(9); + const after = hero.items.equipment.get(uid)!; + expect(after).not.toBe(before); + expect([...after.getModifiers()][0][1].getValue()).toBe(9); } }); @@ -750,47 +747,8 @@ describe('HeroState same-reference equipment load (#06-17-4)', () => { hero.loadState(saved, SaveCompression.NoCompression); - expect(hero.items.equipment.get(kept)).toBe(before); + expect(hero.items.equipment.get(kept)).not.toBe(before); + expect(hero.items.equipment.get(kept)?.item.num).toBe(10); expect(hero.items.equipment.get(extra)).toBeNull(); }); }); - -describe('HeroState same-reference follower load (#06-17-6)', () => { - // 验证经勇士容器三档往返后同索引同图块数字的跟随者为同一实例,位置与渲染恢复到存档点 - it('keeps the follower instance and restores its state across all compressions', () => { - for (const compression of SAVE_COMPRESSIONS) { - const hero = createFollowerHero(); - const follower = hero.followers.addFollower(100); - follower.location.setPos(2, 3); - follower.location.mover.setFaceDir(FaceDirection.Up); - follower.rendering.setAlpha(0.5); - - const saved = hero.saveState(compression); - follower.location.setPos(9, 9); - follower.location.mover.setFaceDir(FaceDirection.Down); - follower.rendering.setAlpha(1); - - hero.loadState(saved, compression); - - expect(hero.followers.getFollower(0)).toBe(follower); - expect(follower.location.x).toBe(2); - expect(follower.location.y).toBe(3); - expect(follower.rendering.alpha).toBe(0.5); - } - }); - - // 验证跟随者数量以存档为准:存档中不存在的跟随者读档后被删除 - it('deletes followers absent from the save', () => { - const hero = createFollowerHero(); - const kept = hero.followers.addFollower(100); - - const saved = hero.saveState(SaveCompression.NoCompression); - hero.followers.addFollower(100); - - hero.loadState(saved, SaveCompression.NoCompression); - - const all = hero.followers.getAllFollowers(); - expect(all).toHaveLength(1); - expect(all[0]).toBe(kept); - }); -}); diff --git a/packages-user/data-base/src/hero/__test__/state.test.ts b/packages-user/data-base/src/hero/__test__/state.test.ts index fcbf551..a4b5a0c 100644 --- a/packages-user/data-base/src/hero/__test__/state.test.ts +++ b/packages-user/data-base/src/hero/__test__/state.test.ts @@ -12,6 +12,7 @@ import { logger } from '@motajs/common'; import { HeroAttribute } from '../attribute'; import { ValueModifier } from '../modifier'; import { HeroState } from '../state'; +import { type IGameMap, MapState } from '../../map'; import { type IHeroAttribute, type IHeroState } from '../types'; vi.hoisted(() => { @@ -46,9 +47,10 @@ afterAll(() => { /** 构造一个仅包含图块与道具存储的公共层假对象 */ function createState(): IDataCommon { + const tileStore = new TileStore(); return { - tileStore: new TileStore(), - itemStore: new ItemStore() + tileStore, + itemStore: new ItemStore(tileStore) } as never; } @@ -73,6 +75,11 @@ function createBaseAttr(): IHeroAttr { }; } +/** 构造一个单格的真实游戏地图对象,供 changeFloor 的楼层目标使用 */ +function createMap(floorId: string): IGameMap { + return new MapState(createState()).createMap(floorId, 1, 1); +} + /** 构造一个装配完成的勇士状态对象,可注入既有属性对象 */ function createHeroState( attribute?: IHeroAttribute @@ -85,13 +92,11 @@ function createHeroState( } describe('HeroState assembly', () => { - // 验证构造器装配全部子系统并停在默认定位器 + // 验证构造器装配数据端子系统并停在默认定位器 it('assembles every subsystem at the default locator', () => { const hero = createHeroState(); expect(hero.location).toBeDefined(); - expect(hero.rendering.alpha).toBe(1); - expect(hero.followers.getAllFollowers()).toEqual([]); expect(hero.items.equipment).toBeDefined(); expect(hero.equip.slots).toEqual([]); expect(hero.getLocation()).toEqual({ @@ -148,17 +153,15 @@ describe('HeroState changeFloor', () => { onAfterChangeFloor: async () => { calls.push('after'); } - }).load(); - hero.location - .addHook({ - onSetFloor: () => { - calls.push('floor'); - } - }) - .load(); + }); + hero.location.addHook({ + onSetFloor: () => { + calls.push('floor'); + } + }); await hero.changeFloor({ - target: 'F2', + target: createMap('F2'), x: 5, y: 6, face: FaceDirection.Up diff --git a/packages-user/data-base/src/hero/equipment.ts b/packages-user/data-base/src/hero/equipment.ts index 42e7386..d38b59b 100644 --- a/packages-user/data-base/src/hero/equipment.ts +++ b/packages-user/data-base/src/hero/equipment.ts @@ -353,8 +353,12 @@ export class HeroEquipment implements IHeroEquipment { }); // 由于装备修饰器不进存档,所以此时的勇士处于没有任何装备修饰器的状态,故可以安全清除 this.equips.clear(); + // 读档期间的重新装备不应写入录像,故用禁用窗口包裹 + const replay = this.state.replaySystem; + replay.disable(); for (const [index, uid] of state.equipped) { this.equip(uid, index); } + replay.revert(); } } diff --git a/packages-user/data-base/src/hero/location.ts b/packages-user/data-base/src/hero/location.ts index 1af9ec0..52099f9 100644 --- a/packages-user/data-base/src/hero/location.ts +++ b/packages-user/data-base/src/hero/location.ts @@ -52,8 +52,8 @@ export class HeroLocation } @shouldReplay('Setting hero floor should be replayed.') - setFloor(map: IGameMap): void { - this.floorId = map.floorId; + setFloor(map: IGameMap | null): void { + this.floorId = map ? map.floorId : undefined; this.map = map; this.forEachHook(hook => hook.onSetFloor?.(map)); }