mirror of
https://github.com/motajs/template.git
synced 2026-09-12 09:30:19 +08:00
feat(03-01): add awaited replay movement tracer
- exercise source-aware event mutation on a fixed map - verify Node replay reaches its normal end state
This commit is contained in:
parent
a707be4856
commit
4eec08638e
@ -213,10 +213,12 @@ export class MapLayer
|
||||
event(x: number, y: number): ILayerEventView | null {
|
||||
if (!this.inMap(x, y)) return null;
|
||||
const index = y * this.width + x;
|
||||
return this.pointEvents.getOrInsertComputed(
|
||||
index,
|
||||
() => new LayerEventView()
|
||||
);
|
||||
let eventView = this.pointEvents.get(index);
|
||||
if (!eventView) {
|
||||
eventView = new LayerEventView();
|
||||
this.pointEvents.set(index, eventView);
|
||||
}
|
||||
return eventView;
|
||||
}
|
||||
|
||||
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
|
||||
|
||||
@ -116,7 +116,7 @@ export class ReplaySandbox
|
||||
}
|
||||
|
||||
async step(): Promise<boolean> {
|
||||
if (!this.pausing || !this.playing || this.ended) return false;
|
||||
if (!this.playing || this.ended) return false;
|
||||
if (this.reader.expired) {
|
||||
logger.warn(156);
|
||||
return false;
|
||||
|
||||
@ -46,7 +46,6 @@ import {
|
||||
} from '@user/data-system';
|
||||
import {
|
||||
CommonAuraConverter,
|
||||
EnemyLegacyBridge,
|
||||
GuardAuraConverter,
|
||||
MainDamageCalculator,
|
||||
MainEnemyFinalEffect,
|
||||
|
||||
113
packages-user/data-state/test/fixtures/closed-loop.ts
vendored
Normal file
113
packages-user/data-state/test/fixtures/closed-loop.ts
vendored
Normal file
@ -0,0 +1,113 @@
|
||||
import { AnonTokyoInterpreter } from 'anon-tokyo';
|
||||
import {
|
||||
EventTrigger,
|
||||
FaceDirection,
|
||||
IReplaySandbox,
|
||||
TileType
|
||||
} from '@user/data-common';
|
||||
import { IGameMap, IMapLayer } from '@user/data-base';
|
||||
import { IReadonlyBlockEvent } from '@user/data-system';
|
||||
import { ReplaySystem } from '../../../data-common/src/replay/system';
|
||||
import { createCoreState, CoreState } from '../../src/core';
|
||||
|
||||
export interface IClosedLoopFixture {
|
||||
readonly state: CoreState;
|
||||
readonly map: IGameMap;
|
||||
readonly eventLayer: IMapLayer;
|
||||
readonly replay: ReplaySystem;
|
||||
readonly sandbox: IReplaySandbox;
|
||||
readonly eventCompleted: () => boolean;
|
||||
}
|
||||
|
||||
const MOVE_RIGHT_COMMAND = 1;
|
||||
|
||||
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 map = state.maps.fromRaw({
|
||||
floorId: 'F1',
|
||||
width: 3,
|
||||
map: { 20: [1, 1, 1] },
|
||||
layerAlias: { 20: 'event' },
|
||||
events: { 20: {} }
|
||||
});
|
||||
if (!map || !map.eventLayer) {
|
||||
throw new Error('closed-loop fixture map was not created');
|
||||
}
|
||||
const eventLayer = map.eventLayer;
|
||||
let completed = false;
|
||||
const event: IReadonlyBlockEvent = {
|
||||
interpreter: new AnonTokyoInterpreter({
|
||||
builtInFunctions: [],
|
||||
globalFunctions: []
|
||||
}),
|
||||
trigger: EventTrigger.OnEnter,
|
||||
rawEvent: [],
|
||||
compiled: null,
|
||||
compile: () => null,
|
||||
execute: async (_param, env) => {
|
||||
await Promise.resolve();
|
||||
if (!env.layer || !env.triggerLocator) {
|
||||
throw new Error('closed-loop event lost its map source');
|
||||
}
|
||||
env.layer.setBlock(2, env.triggerLocator.x, env.triggerLocator.y);
|
||||
completed = true;
|
||||
}
|
||||
};
|
||||
state.eventStore.addEvent('mutate-map', event);
|
||||
|
||||
state.hero.location.setFloor('F1');
|
||||
state.hero.location.setPos(0, 0);
|
||||
state.hero.location.mover.setFaceDir(FaceDirection.Right);
|
||||
|
||||
const replay = new ReplaySystem();
|
||||
replay.registerCommand(MOVE_RIGHT_COMMAND, {
|
||||
execute: async step => {
|
||||
if (step.params.length !== 0) return false;
|
||||
const mover = state.hero.location.mover;
|
||||
mover.step(FaceDirection.Right);
|
||||
const controller = mover.start();
|
||||
if (!controller) return false;
|
||||
await controller.onEnd;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
replay.record(MOVE_RIGHT_COMMAND);
|
||||
|
||||
const reset = () => {
|
||||
eventLayer.setMapRef(new Uint32Array([1, 1, 1]));
|
||||
state.hero.location.setFloor('F1');
|
||||
state.hero.location.setPos(0, 0);
|
||||
state.hero.location.mover.setFaceDir(FaceDirection.Right);
|
||||
completed = false;
|
||||
};
|
||||
const sandbox = replay.createReplaySandbox({
|
||||
route: replay.route,
|
||||
reseter: { reset }
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
map,
|
||||
eventLayer,
|
||||
replay,
|
||||
sandbox,
|
||||
eventCompleted: () => completed
|
||||
};
|
||||
}
|
||||
45
packages-user/data-state/test/nodeTracer.test.ts
Normal file
45
packages-user/data-state/test/nodeTracer.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { createCoreState } from '../src/core';
|
||||
import { createClosedLoopFixture } from './fixtures/closed-loop';
|
||||
|
||||
describe('Node replay tracer', () => {
|
||||
// 验证 Node 宿主无需浏览器全局即可创建相互独立的内存状态
|
||||
it('creates independent Node-safe CoreState instances', () => {
|
||||
const first = createCoreState();
|
||||
const second = createCoreState();
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
expect(first.saveSystem).not.toBe(second.saveSystem);
|
||||
expect(first.saveSystem.constructor.name).toBe('MemorySaveSystem');
|
||||
expect(second.saveSystem.constructor.name).toBe('MemorySaveSystem');
|
||||
});
|
||||
|
||||
// 验证 replay 等待真实移动与事件完成后才结束并改变事件层矩阵
|
||||
it('replays movement, awaits event mutation, and ends normally', async () => {
|
||||
const fixture = createClosedLoopFixture();
|
||||
|
||||
expect(fixture.eventLayer.getBlock(0, 0)).toBe(1);
|
||||
expect(fixture.eventLayer.getBlock(1, 0)).toBe(1);
|
||||
|
||||
fixture.sandbox.play();
|
||||
await waitForEnded(fixture.sandbox);
|
||||
|
||||
expect(fixture.sandbox.ended).toBe(true);
|
||||
expect(fixture.sandbox.getReplayed()).toBe(1);
|
||||
expect(fixture.state.hero.location.x).toBe(1);
|
||||
expect(fixture.state.hero.location.y).toBe(0);
|
||||
expect(fixture.eventCompleted()).toBe(true);
|
||||
expect(fixture.eventLayer.getBlock(1, 0)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEnded(sandbox: {
|
||||
readonly ended: boolean;
|
||||
}): Promise<void> {
|
||||
for (let index = 0; index < 100 && !sandbox.ended; index++) {
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
if (!sandbox.ended) {
|
||||
throw new Error('replay did not reach its end state');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user