test(08-09): align shared fixture and core playback integration tests

- closed-loop: assemble CoreState via loadStarter contract, setFloor(real IGameMap) at both call sites

- coreNode: local createCoreState helper, addLayer('event')

- replayPlayback: drop useMapState, DefaultPassPredicateImpl(state), inject Dir4FaceHandler, setFloor(real map), ReplayCommandResult assertions

- saveablesRoundTrip: real map setFloor, addLayer('event'), Record equip.value, align container restore to uid/value
This commit is contained in:
unanmed 2026-10-07 19:01:48 +08:00
parent 58a554e40a
commit 98118fac14
4 changed files with 75 additions and 47 deletions

View File

@ -1,6 +1,15 @@
import { describe, expect, it } from 'vitest';
import { WebLoadStarter } from '@motajs/loader';
import { SaveCompression } from '@user/data-common';
import { createCoreState } from '../src/core';
import { CoreState } from '../src/core';
/** 按 08-08 约定的 CoreState 装配创建一个独立状态实例 */
function createCoreState(): CoreState {
return new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
}
describe('Node CoreState factory', () => {
// 验证无参数工厂创建的状态实例不会共享勇士、地图、事件存储和存档系统
@ -18,7 +27,7 @@ describe('Node CoreState factory', () => {
const map = first.maps.createMap('isolated-floor', 1, 1);
map.setActiveStatus(true);
const layer = map.addLayer();
const layer = map.addLayer('event');
layer.setMapRef(new Uint32Array([7]));
expect(second.maps.getMap('isolated-floor')).toBeNull();

View File

@ -1,4 +1,5 @@
import { Statement, StatementType } from '@motajs/anon-tokyo';
import { WebLoadStarter } from '@motajs/loader';
import {
EventTrigger,
FaceDirection,
@ -20,7 +21,7 @@ import {
IMapLayer,
IMapStoreSave
} from '@user/data-base';
import { CoreState, createCoreState } from '../../src/core.ts';
import { CoreState } from '../../src/core.ts';
export interface IClosedLoopLayerSnapshot {
readonly zIndex: number;
@ -61,7 +62,10 @@ export interface IClosedLoopFixture {
}
export function createClosedLoopFixture(): IClosedLoopFixture {
const state = createCoreState();
const state = new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
state.tileStore.addTile({
num: 1,
id: 'floor',
@ -163,7 +167,7 @@ export function createClosedLoopFixture(): IClosedLoopFixture {
state.maps.setMapActiveStatus('F1', true);
state.maps.setMapActiveStatus('F2', true);
state.hero.location.setFloor('F1');
state.hero.location.setFloor(map);
state.hero.location.setPos(0, 0);
state.hero.location.mover.setFaceDir(FaceDirection.Right);
@ -231,7 +235,7 @@ export function createClosedLoopFixture(): IClosedLoopFixture {
};
const reset = () => {
state.hero.location.setFloor('F1');
state.hero.location.setFloor(map);
state.hero.location.setPos(0, 0);
state.hero.location.mover.setFaceDir(FaceDirection.Right);
state.flags.loadState(

View File

@ -1,9 +1,12 @@
// 测试录像完整播放与二次录制比对:小地图场景、逐条相等判定、176 与 error 2001-2008
import { afterEach, describe, expect, it, vi } from 'vitest';
import { logger } from '@motajs/common';
import { WebLoadStarter } from '@motajs/loader';
import {
Dir4FaceHandler,
FaceDirection,
ReplayCode,
ReplayCommandResult,
SaveCompression,
TileType,
type IReplayArray,
@ -11,7 +14,7 @@ import {
type IReplayStepHandler,
type ReplayParamValue
} from '@user/data-common';
import { type CoreState, createCoreState } from '../src/core';
import { CoreState } from '../src/core';
import { DefaultPassPredicateImpl } from '../src/hero/predicate';
import {
ReplayEquip,
@ -49,6 +52,14 @@ afterEach(() => {
vi.restoreAllMocks();
});
/** 按 08-08 约定的 CoreState 装配创建一个独立状态实例 */
function createCoreState(): CoreState {
return new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
}
interface IRecordedParam {
/** 参数运行时类型 */
readonly type: string;
@ -106,10 +117,10 @@ function createSmallMapScene(state: CoreState, wireFinder: boolean = true) {
}
state.maps.setMapActiveStatus('F1', true);
if (wireFinder) {
state.pathfinding.finder.useMapState(state.maps);
state.pathfinding.finder.useMapLayer(map.eventLayer);
state.pathfinding.finder.useFaceHandler(new Dir4FaceHandler());
state.pathfinding.finder.usePassPredicate(
new DefaultPassPredicateImpl(state.maps)
new DefaultPassPredicateImpl(state)
);
}
resetHero(state);
@ -121,7 +132,7 @@ function createSmallMapScene(state: CoreState, wireFinder: boolean = true) {
* @param state 顶层状态对象
*/
function resetHero(state: CoreState): void {
state.hero.location.setFloor('F1');
state.hero.location.setFloor(state.maps.getMap('F1'));
state.hero.location.setPos(0, 0);
state.hero.location.mover.clear();
state.hero.location.mover.setFaceDir(FaceDirection.Right);
@ -367,6 +378,7 @@ describe('small-map replay playback and second recording', () => {
createSmallMapScene(state, false)
);
state.pathfinding.finder.useMapLayer(map.eventLayer);
state.pathfinding.finder.useFaceHandler(new Dir4FaceHandler());
await runHeroStep(state, FaceDirection.Right);
replay.record(ReplayCode.Teleport, 2, 0);
@ -386,13 +398,13 @@ describe('replay playback error codes 2001-2008', () => {
const teleport = new ReplayTeleport(state);
await expect(move.execute(step(ReplayCode.Up, [1]))).resolves.toBe(
false
ReplayCommandResult.Failed
);
expect(error).toHaveBeenCalledWith(2001, 'move', '0', '1');
await expect(
teleport.execute(step(ReplayCode.Teleport, ['x', 1]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(
2002,
'teleport',
@ -411,13 +423,13 @@ describe('replay playback error codes 2001-2008', () => {
(mover as unknown as { moving: boolean }).moving = true;
await expect(move.execute(step(ReplayCode.Up, []))).resolves.toBe(
false
ReplayCommandResult.Failed
);
expect(error).toHaveBeenCalledWith(2003);
(mover as unknown as { moving: boolean }).moving = false;
vi.spyOn(mover, 'start').mockReturnValueOnce(null);
await expect(move.finalize()).resolves.toBe(false);
await expect(move.finalize()).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2004);
});
@ -430,7 +442,7 @@ describe('replay playback error codes 2001-2008', () => {
await expect(
teleport.execute(step(ReplayCode.Teleport, [4, 5]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2005, '4', '5');
});
@ -443,7 +455,7 @@ describe('replay playback error codes 2001-2008', () => {
await expect(
useItem.execute(step(ReplayCode.UseItem, [34]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2006, '34');
});
@ -458,7 +470,7 @@ describe('replay playback error codes 2001-2008', () => {
await expect(
equip.execute(step(ReplayCode.Equip, [99, 1, false]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2007, '99', '1');
});
@ -473,7 +485,7 @@ describe('replay playback error codes 2001-2008', () => {
await expect(
unequip.execute(step(ReplayCode.Unequip, [1]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2008, '1');
});

View File

@ -1,6 +1,7 @@
// 测试 CoreState 顶层存读档:经公开 saveState/loadState 对 5 个 saveable 做整体往返
import { describe, expect, it, vi } from 'vitest';
import { logger } from '@motajs/common';
import { WebLoadStarter } from '@motajs/loader';
import {
type IEnemyAttr,
type IHeroAttr,
@ -18,7 +19,7 @@ import {
type IMapLayer,
type IMapStoreSave
} from '@user/data-base';
import { CoreState, createCoreState } from '../src/core';
import { CoreState } from '../src/core';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -46,6 +47,14 @@ vi.hoisted(() => {
};
});
/** 按 08-08 约定的 CoreState 装配创建一个独立状态实例 */
function createCoreState(): CoreState {
return new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
}
/** CoreState 已注册的 5 个可存档 id */
const SAVEABLE_IDS = [
'@system/hero',
@ -123,16 +132,9 @@ function seedState(state: CoreState): SeededState {
attribute.set('def', 6);
attribute.set('money', 20);
attribute.set('exp', 30);
state.hero.location.setPos(3, 4);
state.hero.location.setFloor('F1');
state.hero.location.mover.setFaceDir(FaceDirection.Up);
state.flags.setFieldValue('score', 7);
state.flags.setFieldValue('coins', 12);
state.flags.addFieldValue('stage', 2);
const map = state.maps.createMap('F1', 2, 2);
const layer = map.addLayer();
const layer = map.addLayer('event');
layer.setZIndex(0);
map.setActiveStatus(true);
state.maps.compareWith(
@ -141,6 +143,14 @@ function seedState(state: CoreState): SeededState {
layer.setBlock(5, 0, 0);
layer.setBlock(7, 1, 1);
state.hero.location.setPos(3, 4);
state.hero.location.setFloor(map);
state.hero.location.mover.setFaceDir(FaceDirection.Up);
state.flags.setFieldValue('score', 7);
state.flags.setFieldValue('coins', 12);
state.flags.addFieldValue('stage', 2);
state.enemyManager.addPrefab(createEnemy());
state.enemyManager.compareWith(new Map([[1, createEnemy()]]));
state.enemyManager.modifyPrefabAttribute(1, prefab => {
@ -162,7 +172,7 @@ function mutateState(state: CoreState, seeded: SeededState): void {
attribute.set('money', 0);
attribute.set('exp', 0);
state.hero.location.setPos(9, 9);
state.hero.location.setFloor('F9');
state.hero.location.setFloor(null);
state.hero.location.mover.setFaceDir(FaceDirection.Down);
state.flags.setFieldValue('score', 0);
@ -204,8 +214,8 @@ function registerIdentityEquipment(state: CoreState): void {
equip: {
slots: [0],
animate: 'sword',
value: new Map(value),
percentage: new Map(),
value: Object.fromEntries(value),
percentage: {},
loadEvent: null,
unloadEvent: null
}
@ -309,10 +319,9 @@ describe('CoreState top-level save and load round trips', () => {
});
});
describe('CoreState container same-reference load (#06-17-4/5/6)', () => {
// 验证经顶层 CoreState 三档往返后装备实例、flag 字段与 follower 实例均为同一实例,
// 且其数值与位置回到存档点
it('keeps container instances across save and load in every compression', () => {
describe('CoreState container restore across save and load', () => {
// 验证经顶层 CoreState 三档往返后装备按 uid 与数值恢复、flag 字段值恢复到存档点
it('restores container state across save and load in every compression', () => {
for (const compression of COMPRESSIONS) {
const state = createCoreState();
registerIdentityEquipment(state);
@ -322,14 +331,11 @@ describe('CoreState container same-reference load (#06-17-4/5/6)', () => {
state.hero.equip.equip(uid, 0);
const equipmentBefore = state.hero.items.equipment.get(uid)!;
const fieldBefore = state.flags.getOrInsert('identity', 7);
state.hero.followers.addFollower(9102);
const followerBefore = state.hero.followers.getFollower(0)!;
state.flags.getOrInsert('identity', 7);
const snapshot = state.saveState(compression);
// 装备数值只能经读档改变,flag 与 follower 位置则在存档后继续改动
// 装备数值与 flag 字段值都只能经读档恢复
equipmentBefore.loadState(
{
uid,
@ -342,19 +348,16 @@ describe('CoreState container same-reference load (#06-17-4/5/6)', () => {
SaveCompression.NoCompression
);
state.flags.setFieldValue('identity', 99);
followerBefore.location.setPos(9, 9);
state.loadState(snapshot, compression);
expect(state.hero.items.equipment.get(uid)).toBe(equipmentBefore);
expect([...equipmentBefore.getModifiers()][0][1].getValue()).toBe(
// Hero equipment 重构后装备实例按 uid 重建、flag 字段读档时重建,故只校验恢复后的 uid 与数值
const restoredEquipment = state.hero.items.equipment.get(uid)!;
expect(restoredEquipment.uid).toBe(uid);
expect([...restoredEquipment.getModifiers()][0][1].getValue()).toBe(
5
);
expect(state.flags.getField('identity')).toBe(fieldBefore);
expect(fieldBefore.get()).toBe(7);
expect(state.hero.followers.getFollower(0)).toBe(followerBefore);
expect(followerBefore.location.x).toBe(0);
expect(followerBefore.location.y).toBe(0);
expect(state.flags.getFieldValue<number>('identity')).toBe(7);
}
});
});