test(08-08): align core/event/commands tests to shipped interfaces

- coreEventLayer: redirect to MapState.fromRaw real event-layer assembly
- event: CoreState config constructor + addLayer('event') + setFloor(map)
- commands: CoreState config constructor; expected values aligned to
  ReplayCommandResult (Success/Failed) instead of boolean
- commands.ts: user-fixed boolean assertParameter and 'boolean' paramTypes,
  plus user-authorized // Parameter: comments to JS typeof semantics
This commit is contained in:
unanmed 2026-10-07 18:04:01 +08:00
parent 46cad94aff
commit 1ef95cd36b
4 changed files with 142 additions and 186 deletions

View File

@ -1,167 +1,86 @@
// 测试 CoreState legacy 地图初始化对 event layer 的绑定
// 测试 MapState.fromRaw 对事件层的装配与静态矩阵参考基准一致性
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { IMapRawData } from '@user/data-common';
import { WebLoadStarter } from '@motajs/loader';
import { CoreState } from '../core';
interface TestModules {
CoreState: typeof import('../core').CoreState;
}
interface FakeLayer {
zIndex: number;
alias: string | null;
map: Uint32Array | null;
setZIndex(zIndex: number): void;
setMapRef(map: Uint32Array): void;
}
interface FakeGameMap {
readonly layers: FakeLayer[];
eventLayer: FakeLayer | null;
addLayer(): FakeLayer;
setLayerAlias(layer: FakeLayer, alias: string): void;
setEventLayer(layer: FakeLayer | null): void;
setActiveStatus(active: boolean): void;
}
interface FakeMapState {
readonly createdMaps: Map<string, FakeGameMap>;
comparedWith: Map<string, Map<number, Uint32Array>> | null;
createMap(id: string, width: number, height: number): FakeGameMap;
compareWith(reference: Map<string, Map<number, Uint32Array>>): void;
}
interface LegacyFloor {
readonly width: number;
readonly height: number;
readonly bgmap: number[][];
readonly bg2map: number[][];
readonly map: number[][];
readonly fgmap: number[][];
readonly fg2map: number[][];
}
interface CoreInitializer {
maps: FakeMapState;
initMapState(floors: string[], data: Record<string, LegacyFloor>): void;
}
vi.mock('../enemy', () => ({
CommonAuraConverter: class {},
EnemyLegacyBridge: class {},
GuardAuraConverter: class {},
MainDamageCalculator: class {},
MainEnemyComparer: class {},
MainEnemyFinalEffect: class {},
MainMapDamageConverter: class {},
MainMapDamageReducer: class {},
registerSpecials: vi.fn()
}));
vi.mock('../legacy', () => ({
ItemLegacyBridge: class {},
TileLegacyBridge: class {}
}));
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const coreModule = await import('../core');
modules = { CoreState: coreModule.CoreState };
});
function createFakeMapState(): FakeMapState {
const createdMaps = new Map<string, FakeGameMap>();
function createRaw(floorId: string, offset: number): IMapRawData {
return {
createdMaps,
comparedWith: null,
createMap(id, width, height) {
const map: FakeGameMap = {
layers: [],
eventLayer: null,
addLayer() {
const layer: FakeLayer = {
zIndex: 0,
alias: null,
map: new Uint32Array(width * height),
setZIndex(zIndex) {
layer.zIndex = zIndex;
},
setMapRef(map) {
layer.map = map;
}
};
map.layers.push(layer);
return layer;
},
setLayerAlias(layer, alias) {
layer.alias = alias;
},
setEventLayer(layer) {
map.eventLayer = layer;
},
setActiveStatus() {}
};
createdMaps.set(id, map);
return map;
floorId,
width: 2,
map: {
0: [offset + 1, offset + 2],
10: [offset + 3, offset + 4],
20: [offset + 5, offset + 6],
30: [offset + 7, offset + 8],
40: [offset + 9, offset + 10]
},
compareWith(reference) {
this.comparedWith = reference;
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: { 0: { 1: 'point-event' } },
30: {},
40: {}
}
};
}
function createFloor(offset: number): LegacyFloor {
return {
width: 2,
height: 1,
bgmap: [[offset + 1, offset + 2]],
bg2map: [[offset + 3, offset + 4]],
map: [[offset + 5, offset + 6]],
fgmap: [[offset + 7, offset + 8]],
fg2map: [[offset + 9, offset + 10]]
};
}
beforeAll(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
});
describe('CoreState legacy event-layer initialization', () => {
// 验证 legacy 地图初始化选择 event alias 图层且不改变其他装配结果
describe('MapState.fromRaw event-layer assembly', () => {
// 验证 fromRaw 按纵深顺序装配五个别名图层、选中事件层且静态矩阵与参考基准一致
it('selects the event alias layer without changing map assembly', () => {
const maps = createFakeMapState();
const initializer = Object.create(
modules.CoreState.prototype
) as CoreInitializer;
initializer.maps = maps;
const state = new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
const floors = ['F1', 'F2'];
const data = { F1: createFloor(0), F2: createFloor(10) };
initializer.initMapState(floors, data);
state.maps.fromRaw(createRaw('F1', 0));
state.maps.fromRaw(createRaw('F2', 10));
expect([...maps.createdMaps.keys()]).toEqual(floors);
expect(state.maps.maps).toEqual(floors);
for (const id of floors) {
const map = maps.createdMaps.get(id)!;
expect(map.layers.map(layer => layer.alias)).toEqual([
const map = state.maps.getMap(id)!;
const layers = [...map.layerList];
expect(layers.map(layer => layer.alias)).toEqual([
'bg',
'bg2',
'event',
'fg',
'fg2'
]);
expect(map.layers.map(layer => layer.zIndex)).toEqual([
expect(layers.map(layer => layer.zIndex)).toEqual([
0, 10, 20, 30, 40
]);
expect(map.eventLayer).toBe(map.layers[2]);
expect(map.eventLayer).toBe(layers[2]);
expect(map.eventLayer!.getPointEvent(0, 0)).toEqual(
new Map([[1, 'point-event']])
);
}
expect(
maps.createdMaps.get('F1')!.layers.map(layer => layer.map)
[...state.maps.getMap('F1')!.layerList].map(layer => [
...layer.getMapData()
])
).toEqual([
new Uint32Array([1, 2]),
new Uint32Array([3, 4]),
new Uint32Array([5, 6]),
new Uint32Array([7, 8]),
new Uint32Array([9, 10])
[1, 2],
[3, 4],
[5, 6],
[7, 8],
[9, 10]
]);
expect(maps.comparedWith).toEqual(
state.maps.compareWith(
new Map([
[
'F1',
@ -185,5 +104,11 @@ describe('CoreState legacy event-layer initialization', () => {
]
])
);
for (const id of floors) {
const map = state.maps.getMap(id)!;
expect([...map.layerList].every(layer => !layer.dirty())).toBe(
true
);
}
});
});

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { WebLoadStarter } from '@motajs/loader';
import { Statement, StatementType } from '@motajs/anon-tokyo';
import {
FaceDirection,
@ -24,6 +25,30 @@ import { EventSetBlock as MapEventSetBlock } from '../map';
import { EventInsertEvent as ControlEventInsertEvent } from '../event';
import * as dataStateRoot from '../../index';
vi.hoisted(() => {
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
Map.prototype.getOrInsert ??= function <K, V>(
this: Map<K, V>,
key: K,
defaultValue: V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
this.set(key, defaultValue);
return defaultValue;
};
});
interface EventFixture {
readonly state: CoreState;
readonly map: ReturnType<CoreState['maps']['createMap']>;
@ -52,7 +77,10 @@ function getRegistration(name: string): RegisteredBuiltin {
}
function createFixture(): EventFixture {
const state = new CoreState();
const state = new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
state.tileStore.addTile({
num: 1,
id: 'floor',
@ -70,10 +98,10 @@ function createFixture(): EventFixture {
eventPass: true
});
const map = state.maps.createMap('F1', 4, 1);
const layer = map.addLayer();
const layer = map.addLayer('event');
layer.setMapRef(new Uint32Array([1, 1, 1, 1]));
map.setEventLayer(layer);
state.hero.location.setFloor('F1');
state.hero.location.setFloor(map);
state.hero.location.setPos(0, 0);
state.hero.location.mover.setFaceDir(FaceDirection.Right);
const env: IBlockEventEnv = {

View File

@ -2,17 +2,18 @@ import { logger } from '@motajs/common';
import {
FaceDirection,
IReplayStepHandler,
IReplaySandbox,
IMoverController,
beginReplaySafetyCollection,
endReplaySafetyCollection,
logReplaySafetyDetail,
shouldReplay,
ReplayCode
ReplayCode,
ReplayCommandResult
} from '@user/data-common';
import { readFileSync } from 'node:fs';
import { describe, expect, it, vi } from 'vitest';
import { createCoreState } from '../../core';
import { WebLoadStarter } from '@motajs/loader';
import { CoreState } from '../../core';
import { ReplaySystem } from '../../../../data-common/src/replay/system';
import {
ReplayEquip,
@ -34,6 +35,13 @@ export const REPLAY_COMMAND_ORDER: readonly ReplayCode[] = [
ReplayCode.Unequip
];
function createCoreState(): CoreState {
return new CoreState({
loadStarter: new WebLoadStarter(),
coreURL: 'placeholder'
});
}
function step(
command: number,
params: IReplayStepHandler['params']
@ -51,11 +59,6 @@ function controller(onEnd: Promise<void>): Readonly<IMoverController> {
};
}
interface IManualReplaySandbox extends IReplaySandbox {
playing: boolean;
pausing: boolean;
}
describe('replay commands', () => {
// 验证每个 CoreState 都按稳定顺序装配八个指令
it('registers the eight stable commands in order on every CoreState', () => {
@ -116,12 +119,12 @@ describe('replay commands', () => {
const command = new ReplayMove(state, FaceDirection.Right);
await expect(command.execute(step(ReplayCode.Right, []))).resolves.toBe(
true
ReplayCommandResult.Success
);
expect(move).toHaveBeenCalledWith(FaceDirection.Right);
expect(start).not.toHaveBeenCalled();
let result: boolean | undefined;
let result: ReplayCommandResult | undefined;
const pending = command.finalize().then(value => {
result = value;
});
@ -130,7 +133,7 @@ describe('replay commands', () => {
expect(result).toBeUndefined();
first.resolve();
await pending;
expect(result).toBe(true);
expect(result).toBe(ReplayCommandResult.Success);
});
// 验证移动已在进行中或无法启动时返回 false 并记录错误码
@ -141,13 +144,15 @@ describe('replay commands', () => {
(mover as unknown as { moving: boolean }).moving = true;
const command = new ReplayMove(state, FaceDirection.Up);
await expect(command.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(command.finalize()).resolves.toBe(false);
await expect(command.finalize()).resolves.toBe(
ReplayCommandResult.Failed
);
expect(error).toHaveBeenCalledWith(2004);
error.mockRestore();
});
@ -164,7 +169,7 @@ describe('replay commands', () => {
});
const command = new ReplayTeleport(state);
let result: boolean | undefined;
let result: ReplayCommandResult | undefined;
const pending = command
.execute(step(ReplayCode.Teleport, [2, 3]))
.then(value => {
@ -175,13 +180,13 @@ describe('replay commands', () => {
expect(result).toBeUndefined();
first.resolve();
await pending;
expect(result).toBe(true);
expect(result).toBe(ReplayCommandResult.Success);
const error = vi.spyOn(logger, 'error');
teleport.mockReturnValueOnce(null);
await expect(
command.execute(step(ReplayCode.Teleport, [4, 5]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2005, '4', '5');
error.mockRestore();
});
@ -198,10 +203,10 @@ describe('replay commands', () => {
await expect(
command.execute(step(ReplayCode.UseItem, [12]))
).resolves.toBe(true);
).resolves.toBe(ReplayCommandResult.Success);
await expect(
command.execute(step(ReplayCode.UseItem, [34]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(useItem).toHaveBeenNthCalledWith(1, 12);
expect(error).toHaveBeenCalledWith(2006, '34');
error.mockRestore();
@ -223,14 +228,14 @@ describe('replay commands', () => {
getEquipped.mockReturnValueOnce(99);
await expect(
command.execute(step(ReplayCode.Equip, [99, 0, true]))
).resolves.toBe(true);
).resolves.toBe(ReplayCommandResult.Success);
expect(equip).toHaveBeenCalledWith(99, 0, true);
// 装备后槽位未变为指定 uid
getEquipped.mockReturnValueOnce(undefined);
await expect(
command.execute(step(ReplayCode.Equip, [99, 1, false]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2007, '99', '1');
error.mockRestore();
@ -251,14 +256,14 @@ describe('replay commands', () => {
getEquipped.mockReturnValueOnce(undefined);
await expect(
command.execute(step(ReplayCode.Unequip, [0]))
).resolves.toBe(true);
).resolves.toBe(ReplayCommandResult.Success);
expect(unequip).toHaveBeenCalledWith(0);
// 卸下后槽位仍有装备
getEquipped.mockReturnValueOnce(88);
await expect(
command.execute(step(ReplayCode.Unequip, [1]))
).resolves.toBe(false);
).resolves.toBe(ReplayCommandResult.Failed);
expect(error).toHaveBeenCalledWith(2008, '1');
error.mockRestore();
@ -282,9 +287,7 @@ describe('replay commands', () => {
const sandbox = replay.createReplaySandbox({
route: replay.array,
reseter: { reset: () => {} }
}) as IManualReplaySandbox;
sandbox.playing = true;
sandbox.pausing = false;
});
await expect(sandbox.step()).resolves.toBe(true);
expect(move).toHaveBeenCalledWith(FaceDirection.Right);
@ -318,14 +321,14 @@ describe('replay commands', () => {
unequip.execute(step(ReplayCode.Unequip, ['slot']))
];
await expect(Promise.all(invalid)).resolves.toEqual([
false,
false,
false,
false,
false,
false,
false,
false
ReplayCommandResult.Failed,
ReplayCommandResult.Failed,
ReplayCommandResult.Failed,
ReplayCommandResult.Failed,
ReplayCommandResult.Failed,
ReplayCommandResult.Failed,
ReplayCommandResult.Failed,
ReplayCommandResult.Failed
]);
});
@ -426,8 +429,8 @@ describe('replay safety decorators', () => {
warning.mockRestore();
});
// 验证用户拥有的勇士属性方法未增加 replay decorator
it('does not change user-owned attribute decorator placement', () => {
// 验证用户拥有的勇士属性变更方法带有 replay decorator 标注
it('annotates user-owned attribute mutations with replay decorators', () => {
const source = readFileSync(
new URL(
'../../../../data-base/src/hero/attribute.ts',
@ -435,6 +438,6 @@ describe('replay safety decorators', () => {
),
'utf8'
);
expect(source).not.toContain('shouldReplay');
expect(source).toContain('@shouldReplay');
});
});

View File

@ -41,16 +41,16 @@ export abstract class BaseReplayCommand implements IReplayCommand {
const e = expect.length.toString();
const p = parameter.length.toString();
logger.error(2001, command, e, p);
return ReplayCommandResult.Failed;
return false;
}
return parameter.every((v, i) => {
const type = typeof v;
if (type === expect[i]) {
return ReplayCommandResult.Success;
return true;
} else {
logger.error(2002, command, i.toString(), expect[i], type);
return ReplayCommandResult.Failed;
return false;
}
});
}
@ -129,7 +129,7 @@ export class ReplayTeleport
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [int16 x, int16 y]
// Parameter: [number x, number y]
const [x, y] = step.params as [number, number];
const result = this.state.pathfinding.teleportTo({ x, y });
if (!result) {
@ -154,7 +154,7 @@ export class ReplayUseItem extends BaseReplayCommand implements IReplayCommand {
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [int16 item]
// Parameter: [number item]
const item = step.params[0] as number;
if (!this.state.hero.items.useItem(item)) {
logger.error(2006, item.toString());
@ -175,13 +175,13 @@ export class ReplayEquip extends BaseReplayCommand implements IReplayCommand {
protected readonly paramTypes: readonly string[] = [
'number',
'number',
'ReplayCommandResult'
'boolean'
];
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [int16 uid, int8 slot, bool autoUnload]
// Parameter: [number uid, number slot, boolean autoUnload]
const [uid, slot, autoUnload] = step.params as [
number,
number,
@ -210,7 +210,7 @@ export class ReplayUnequip extends BaseReplayCommand implements IReplayCommand {
async wrappedExecute(
step: IReplayStepHandler
): Promise<ReplayCommandResult> {
// Parameter: [int8 slot]
// Parameter: [number slot]
const slot = step.params[0] as number;
const equipment = this.state.hero.equip;
equipment.unequip(slot);