mirror of
https://github.com/motajs/template.git
synced 2026-10-08 13:30:18 +08:00
test(08-04): align data-base hero tests to shipped interfaces
Replay stub route->array; drop addHook().load() chains; ItemStore requires tileStore; item.equip value/percentage as Record; setFloor(IGameMap|null); getItem->addItem. Confirmed production fixes: HeroLocation.setFloor accepts null; HeroEquipment.loadState wraps re-equip in replay disable/revert. Container tests drop rendering/followers. Focused: 114 passed + 1 skipped.
This commit is contained in:
parent
8153d4f419
commit
69cec80c6b
@ -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';
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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<typeof vi.fn>;
|
||||
revert: ReturnType<typeof vi.fn>;
|
||||
}
|
||||
@ -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<IHeroAttr>();
|
||||
// 录像系统桩,用于满足装备/卸下时的 route.add 记录与临时禁用录像
|
||||
const itemStore = new ItemStore<IHeroAttr>(tileStore);
|
||||
// 录像系统桩,用于满足装备/卸下时的 array.add 记录与临时禁用录像
|
||||
const replaySystem = createReplaySystem();
|
||||
const state = { tileStore, itemStore, replaySystem } as never;
|
||||
const attribute = new HeroAttribute<IHeroAttr>(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]]
|
||||
]);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -57,9 +57,9 @@ interface ItemFixture {
|
||||
/** 构造一个装配勇士道具对象的测试环境 */
|
||||
function createEnv(): TestEnv {
|
||||
const tileStore = new TileStore();
|
||||
const itemStore = new ItemStore<IHeroAttr>();
|
||||
// 录像系统桩,仅用于满足道具使用时的 route.add 记录
|
||||
const replaySystem = { route: { add: vi.fn() } };
|
||||
const itemStore = new ItemStore<IHeroAttr>(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);
|
||||
|
||||
@ -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]);
|
||||
});
|
||||
|
||||
// 验证朝向经由共享移动器读写
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<E69E81><45>一个含图块、E<E38081><45>具存储与假录像系统的公共层假对象 */
|
||||
function createState(): IDataCommon {
|
||||
const tileStore = new TileStore();
|
||||
const itemStore = new ItemStore<IHeroAttr>();
|
||||
const route = { add: vi.fn() };
|
||||
const itemStore = new ItemStore<IHeroAttr>(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<E69E81><45>一份合<E4BBBD>E皁E<E79A81><45>士基础属性 */
|
||||
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<IHeroAttr> {
|
||||
);
|
||||
}
|
||||
|
||||
/** 构造一个已注册跟随者图块、可直接添加跟随者的勇士状态对象 */
|
||||
function createFollowerHero(): IHeroState<IHeroAttr> {
|
||||
return new HeroState<IHeroAttr>(
|
||||
createFollowerState(),
|
||||
new Dir8FaceHandler(),
|
||||
new HeroAttribute<IHeroAttr>(createBaseAttr())
|
||||
);
|
||||
}
|
||||
|
||||
/** 构造一个已注册装备定义并设置好装备槽的勇士状态对象(基础 atk 为 10) */
|
||||
function createEquipHero(): IHeroState<IHeroAttr> {
|
||||
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<IHeroAttr>(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<E58881><45>加成在无压缩档下同实例恢复到存档点
|
||||
it('restores a percentage modifier on the same instance in NoCompression', () => {
|
||||
// 验证百刁E<E58881><45>加成在无压缩档下读档到新实例并恢复到存档点
|
||||
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<IHeroAttr>(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<EFBFBD><EFBFBD>位与渲染经勇士容器三档往返均恢复到存档点
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<IHeroAttr>
|
||||
@ -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
|
||||
|
||||
@ -353,8 +353,12 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
|
||||
});
|
||||
// 由于装备修饰器不进存档,所以此时的勇士处于没有任何装备修饰器的状态,故可以安全清除
|
||||
this.equips.clear();
|
||||
// 读档期间的重新装备不应写入录像,故用禁用窗口包裹
|
||||
const replay = this.state.replaySystem;
|
||||
replay.disable();
|
||||
for (const [index, uid] of state.equipped) {
|
||||
this.equip(uid, index);
|
||||
}
|
||||
replay.revert();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user