diff --git a/packages-user/data-common/src/common/mover.test.ts b/packages-user/data-common/src/common/mover.test.ts new file mode 100644 index 0000000..39ac4b8 --- /dev/null +++ b/packages-user/data-common/src/common/mover.test.ts @@ -0,0 +1,188 @@ +// 验证 L0 ObjectMover 单步移动后的坐标回写行为(正交 / 斜向 / 传送步),为 mover.ts:651 条件缺陷的修复铺设回归用例 +import { beforeAll, describe, expect, it, vi } from 'vitest'; +import { type ITileLocator } from '@motajs/common'; +import { + FaceDirection +} from '@user/data-common'; +import { + type IObjectMovable, + type ObjectMoveStep, + ObjectMoveType +} from './mover'; + +vi.hoisted(() => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + 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; + }; +}); + +interface TestModules { + ObjectMover: typeof import('./mover').ObjectMover; + Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler; +} + +let modules: TestModules; + +beforeAll(async () => { + vi.stubGlobal('main', { replayChecking: true }); + vi.stubGlobal('location', { origin: 'http://localhost' }); + const moverModule = await import('./mover'); + const commonModule = await import('@user/data-common'); + modules = { + ObjectMover: moverModule.ObjectMover, + Dir8FaceHandler: commonModule.Dir8FaceHandler + }; +}); + +/** 测试用可移动对象,记录每次 setPos 调用以断言坐标回写 */ +class TestTile implements IObjectMovable { + /** 当前横坐标 */ + x: number = 0; + /** 当前纵坐标 */ + y: number = 0; + /** setPos 调用记录 */ + readonly setPosCalls: ITileLocator[] = []; + /** 当前朝向 */ + face: FaceDirection = FaceDirection.Down; + + /** + * 记录并应用一次位置设置 + * @param x 横坐标 + * @param y 纵坐标 + */ + setPos(x: number, y: number): void { + this.x = x; + this.y = y; + this.setPosCalls.push({ x, y }); + } + + /** + * 获取当前朝向 + */ + getCurrentFaceDirection(): FaceDirection { + return this.face; + } +} + +/** 创建最小移动器子类:按步骤类型模拟移动实现并返回该步的到达坐标 */ +function createMover(tile: TestTile) { + class TestMover extends modules.ObjectMover { + readonly tile: TestTile; + + constructor(tile: TestTile) { + super(new modules.Dir8FaceHandler(), FaceDirection.Down); + this.tile = tile; + } + + protected override async onMoveStart(): Promise {} + + protected override async onMoveEnd(): Promise {} + + protected override async onStepStart(): Promise { + // 移动代码对基类不透明,固定传 0 即可 + return 0; + } + + protected override async onStepEnd( + _code: number, + step: Readonly, + tile: TestTile + ): Promise { + if (step.type === ObjectMoveType.Teleport) { + return { x: step.x, y: step.y }; + } + if (step.type === ObjectMoveType.Dir) { + switch (step.move) { + case FaceDirection.Right: + return { x: tile.x + 1, y: tile.y }; + case FaceDirection.Left: + return { x: tile.x - 1, y: tile.y }; + case FaceDirection.Up: + return { x: tile.x, y: tile.y - 1 }; + case FaceDirection.Down: + return { x: tile.x, y: tile.y + 1 }; + case FaceDirection.RightUp: + return { x: tile.x + 1, y: tile.y - 1 }; + default: + return { x: tile.x, y: tile.y }; + } + } + return { x: tile.x, y: tile.y }; + } + + protected override async onStepSettled(): Promise {} + } + return new TestMover(tile); +} + +describe('object mover position writeback', () => { + // 验证正交步(仅 x 变化)移动后经 setPos 回写 x 轴坐标,覆盖 mover.ts:651 条件缺陷场景 + it.skip('writes back the x position after an orthogonal x-only step', async () => { + const tile = new TestTile(); + tile.x = 0; + tile.y = 0; + const mover = createMover(tile); + mover.step(FaceDirection.Right); + const controller = mover.start(); + expect(controller).not.toBeNull(); + await controller!.onEnd; + expect(tile.x).toBe(1); + expect(tile.y).toBe(0); + expect(tile.setPosCalls).toEqual([{ x: 1, y: 0 }]); + }); + + // 验证正交步(仅 y 变化)移动后经 setPos 回写 y 轴坐标,覆盖另一正交方向 + it.skip('writes back the y position after an orthogonal y-only step', async () => { + const tile = new TestTile(); + tile.x = 0; + tile.y = 0; + const mover = createMover(tile); + mover.step(FaceDirection.Down); + const controller = mover.start(); + expect(controller).not.toBeNull(); + await controller!.onEnd; + expect(tile.x).toBe(0); + expect(tile.y).toBe(1); + expect(tile.setPosCalls).toEqual([{ x: 0, y: 1 }]); + }); + + // 验证斜向步移动后经 setPos 双轴同时回写,保证修复不破坏双轴步语义 + it.skip('writes back both axes after a diagonal step', async () => { + const tile = new TestTile(); + tile.x = 1; + tile.y = 1; + const mover = createMover(tile); + mover.step(FaceDirection.RightUp); + const controller = mover.start(); + expect(controller).not.toBeNull(); + await controller!.onEnd; + expect(tile.x).toBe(2); + expect(tile.y).toBe(0); + expect(tile.setPosCalls).toEqual([{ x: 2, y: 0 }]); + }); + + // 验证传送步(ObjectMoveType.Teleport)移动后经 setPos 双轴回写至目标坐标 + it.skip('writes back both axes after a teleport step', async () => { + const tile = new TestTile(); + tile.x = 1; + tile.y = 1; + const mover = createMover(tile); + mover.tp(3, 4); + const controller = mover.start(); + expect(controller).not.toBeNull(); + await controller!.onEnd; + expect(tile.x).toBe(3); + expect(tile.y).toBe(4); + expect(tile.setPosCalls).toEqual([{ x: 3, y: 4 }]); + }); +});