diff --git a/packages-user/data-state/src/__test__/coreEventLayer.test.ts b/packages-user/data-state/src/__test__/coreEventLayer.test.ts index 495319c..8eafffe 100644 --- a/packages-user/data-state/src/__test__/coreEventLayer.test.ts +++ b/packages-user/data-state/src/__test__/coreEventLayer.test.ts @@ -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; - comparedWith: Map> | null; - createMap(id: string, width: number, height: number): FakeGameMap; - compareWith(reference: Map>): 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): 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(); +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 + ); + } }); }); diff --git a/packages-user/data-state/src/event/__test__/event.test.ts b/packages-user/data-state/src/event/__test__/event.test.ts index 38c7aa3..f12336b 100644 --- a/packages-user/data-state/src/event/__test__/event.test.ts +++ b/packages-user/data-state/src/event/__test__/event.test.ts @@ -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 ( + this: Map, + 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 ( + this: Map, + 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; @@ -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 = { diff --git a/packages-user/data-state/src/replay/__test__/commands.test.ts b/packages-user/data-state/src/replay/__test__/commands.test.ts index 66ea4c6..5606891 100644 --- a/packages-user/data-state/src/replay/__test__/commands.test.ts +++ b/packages-user/data-state/src/replay/__test__/commands.test.ts @@ -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): Readonly { }; } -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'); }); }); diff --git a/packages-user/data-state/src/replay/commands.ts b/packages-user/data-state/src/replay/commands.ts index aa7f767..d577e7b 100644 --- a/packages-user/data-state/src/replay/commands.ts +++ b/packages-user/data-state/src/replay/commands.ts @@ -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 { - // 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 { - // 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 { - // 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 { - // Parameter: [int8 slot] + // Parameter: [number slot] const slot = step.params[0] as number; const equipment = this.state.hero.equip; equipment.unequip(slot);