template/packages-user/data-state/test/fixtures/closed-loop.ts
unanmed 45e3b1f05d fix(03-10): restore direct replay fixture path
- Register the fixture event and maps through existing data APIs
- Await the synchronous mutation marker before the final snapshot
2026-09-11 14:00:02 +08:00

268 lines
7.4 KiB
TypeScript

import { Statement, StatementType } from 'anon-tokyo';
import {
EventTrigger,
FaceDirection,
GameEvent,
IReplaySandbox,
IReplaySystem,
IReplayArray,
IEnemyAttr,
IHeroAttr,
SaveCompression,
TileType
} from '@user/data-common';
import {
IEnemyManagerSaveState,
IFlagSystemSave,
IGameMap,
IHeroStateSave,
IMapLayer,
IMapStoreSave
} from '@user/data-base';
import { CoreState, createCoreState } from '../../src/core.ts';
import { ReplayCommandCode } from '../../src/replay/types.ts';
export interface IClosedLoopLayerSnapshot {
readonly zIndex: number;
readonly matrix: Uint32Array;
}
export interface IClosedLoopFloorSnapshot {
readonly floorId: string;
readonly layers: readonly IClosedLoopLayerSnapshot[];
}
export interface IClosedLoopExpectedSnapshot {
readonly hero: IHeroAttr;
readonly maps: readonly IClosedLoopFloorSnapshot[];
}
export interface IClosedLoopInitialState {
readonly hero: IHeroStateSave<IHeroAttr>;
readonly flags: IFlagSystemSave;
readonly maps: IMapStoreSave;
readonly enemy: IEnemyManagerSaveState<IEnemyAttr>;
}
export interface IClosedLoopFixture {
readonly state: CoreState;
readonly map: IGameMap;
readonly eventLayer: IMapLayer;
readonly replay: IReplaySystem;
readonly route: IReplayArray;
readonly sandbox: IReplaySandbox;
readonly initialState: IClosedLoopInitialState;
readonly expected: IClosedLoopExpectedSnapshot;
readonly eventId: string;
readonly rawEvent: Statement[];
readonly eventCompletion: Promise<void>;
readonly reset: () => void;
readonly eventCompleted: () => boolean;
}
export function createClosedLoopFixture(): IClosedLoopFixture {
const state = createCoreState();
state.tileStore.addTile({
num: 1,
id: 'floor',
events: { 10: 'mutate-map' },
type: TileType.Terrain,
pass: { onlyEvents: false, outPass: 0b1111, inPass: 0b1111 },
eventPass: true
});
state.tileStore.addTile({
num: 2,
id: 'changed-floor',
events: {},
type: TileType.Terrain,
pass: { onlyEvents: false, outPass: 0b1111, inPass: 0b1111 },
eventPass: true
});
const rawEvent: Statement[] = [
{
type: StatementType.Call,
functionName: 'eventSetBlock',
builtIn: true,
async: false,
parameters: { x: 1, y: 0, tile: 2 }
}
];
const map = state.maps.fromRaw({
floorId: 'F1',
width: 3,
map: {
0: [7, 7, 7],
10: [8, 8, 8],
20: [1, 1, 1],
30: [9, 9, 9],
40: [10, 10, 10]
},
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: {},
30: {},
40: {}
}
});
if (!map || !map.eventLayer) {
throw new Error('closed-loop fixture map was not created');
}
const secondMap = state.maps.fromRaw({
floorId: 'F2',
width: 2,
map: {
0: [11, 11, 11, 11],
10: [12, 12, 12, 12],
20: [13, 13, 13, 13],
30: [14, 14, 14, 14],
40: [15, 15, 15, 15]
},
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: {},
30: {},
40: {}
}
});
if (!secondMap || !secondMap.eventLayer) {
throw new Error('closed-loop fixture second map was not created');
}
const eventLayer = map.eventLayer;
const eventCompletion = Promise.withResolvers<void>();
const eventHook = eventLayer.addHook({
onUpdateBlock: (block, x, y) => {
if (block === 2 && x === 1 && y === 0) {
eventCompletion.resolve();
}
}
});
eventHook.load();
const event = new GameEvent(
state.eventSystem.executor.interpreter,
rawEvent
);
event.setTrigger(EventTrigger.OnEnter);
state.eventStore.addEvent('mutate-map', event);
state.maps.setMapActiveStatus('F1', true);
state.maps.setMapActiveStatus('F2', true);
state.hero.location.setFloor('F1');
state.hero.location.setPos(0, 0);
state.hero.location.mover.setFaceDir(FaceDirection.Right);
const replay = state.replaySystem;
replay.record(ReplayCommandCode.Right);
const route = replay.route;
const initialState: IClosedLoopInitialState = {
hero: state.hero.saveState(SaveCompression.NoCompression),
flags: state.flags.saveState(SaveCompression.NoCompression),
maps: state.maps.saveState(SaveCompression.NoCompression),
enemy: state.enemyManager.saveState(SaveCompression.NoCompression)
};
const expected: IClosedLoopExpectedSnapshot = {
hero: {
name: '',
hp: 1,
hpmax: 0,
atk: 0,
def: 0,
mdef: 0,
mana: 0,
manamax: 0,
money: 0,
exp: 0
},
maps: [
{
floorId: 'F1',
layers: [
{ zIndex: 0, matrix: new Uint32Array([7, 7, 7]) },
{ zIndex: 10, matrix: new Uint32Array([8, 8, 8]) },
{ zIndex: 20, matrix: new Uint32Array([1, 2, 1]) },
{ zIndex: 30, matrix: new Uint32Array([9, 9, 9]) },
{ zIndex: 40, matrix: new Uint32Array([10, 10, 10]) }
]
},
{
floorId: 'F2',
layers: [
{
zIndex: 0,
matrix: new Uint32Array([11, 11, 11, 11])
},
{
zIndex: 10,
matrix: new Uint32Array([12, 12, 12, 12])
},
{
zIndex: 20,
matrix: new Uint32Array([13, 13, 13, 13])
},
{
zIndex: 30,
matrix: new Uint32Array([14, 14, 14, 14])
},
{
zIndex: 40,
matrix: new Uint32Array([15, 15, 15, 15])
}
]
}
]
};
const reset = () => {
state.hero.location.setFloor('F1');
state.hero.location.setPos(0, 0);
state.hero.location.mover.setFaceDir(FaceDirection.Right);
state.flags.loadState(
initialState.flags,
SaveCompression.NoCompression
);
state.maps.loadState(initialState.maps, SaveCompression.NoCompression);
state.enemyManager.loadState(
initialState.enemy,
SaveCompression.NoCompression
);
};
const sandbox = replay.createReplaySandbox({
route: replay.route,
reseter: { reset }
});
return {
state,
map,
eventLayer,
replay,
route,
sandbox,
initialState,
expected,
eventId: 'mutate-map',
rawEvent,
eventCompletion: eventCompletion.promise,
reset,
eventCompleted: () => eventLayer.getBlock(1, 0) === 2
};
}