diff --git a/packages-user/data-common/src/common/mover.test.ts b/packages-user/data-common/src/common/mover.test.ts index c396286..1c7f07b 100644 --- a/packages-user/data-common/src/common/mover.test.ts +++ b/packages-user/data-common/src/common/mover.test.ts @@ -1,9 +1,10 @@ -// 验证 L0 ObjectMover 单步移动后的坐标回写行为(正交 / 斜向 / 传送步),为 mover.ts:651 条件缺陷的修复铺设回归用例 +// 测试 L0 ObjectMover:既有坐标回写回归用例 + 全部公开移动方法的正常与边界行为 import { beforeAll, describe, expect, it, vi } from 'vitest'; import { type ITileLocator } from '@motajs/common'; import { FaceDirection } from '@user/data-common'; import { type IObjectMovable, + ObjectAnimDirection, type ObjectMoveStep, ObjectMoveType } from './mover'; @@ -72,6 +73,30 @@ class TestTile implements IObjectMovable { } } +/** 根据朝向返回从当前位置移动一步后的坐标,未知朝向原地不动 */ +function nextLocator(dir: FaceDirection, tile: TestTile): ITileLocator { + switch (dir) { + 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 }; + case FaceDirection.LeftUp: + return { x: tile.x - 1, y: tile.y - 1 }; + case FaceDirection.RightDown: + return { x: tile.x + 1, y: tile.y + 1 }; + case FaceDirection.LeftDown: + return { x: tile.x - 1, y: tile.y + 1 }; + default: + return { x: tile.x, y: tile.y }; + } +} + /** 创建最小移动器子类:按步骤类型模拟移动实现并返回该步的到达坐标 */ function createMover(tile: TestTile) { class TestMover extends modules.ObjectMover { @@ -96,24 +121,22 @@ function createMover(tile: TestTile) { step: Readonly, tile: TestTile ): Promise { - if (step.type === ObjectMoveType.Teleport) { - return { x: step.x, y: step.y }; + if ( + step.type === ObjectMoveType.Teleport || + step.type === ObjectMoveType.Jump + ) { + return step.rel + ? { x: tile.x + step.x, y: tile.y + step.y } + : { 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 }; - } + if ( + step.type === ObjectMoveType.Dir || + step.type === ObjectMoveType.DirFace + ) { + return nextLocator(step.move, tile); + } + if (step.type === ObjectMoveType.Special) { + return nextLocator(this.moveDirection, tile); } return { x: tile.x, y: tile.y }; } @@ -184,3 +207,213 @@ describe('object mover position writeback', () => { expect(tile.setPosCalls).toEqual([{ x: 3, y: 4 }]); }); }); + +describe('object mover public surface', () => { + // 验证 setPos 立即写入绑定对象的位置 + it('sets the position of the bound object', () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.setPos(4, 6); + + expect(tile.x).toBe(4); + expect(tile.y).toBe(6); + expect(tile.setPosCalls).toEqual([{ x: 4, y: 6 }]); + }); + + // 验证 setFaceDir 与 setMoveDir 分别更新朝向与移动方向 + it('sets face and move directions independently', () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.setFaceDir(FaceDirection.Left); + mover.setMoveDir(FaceDirection.Up); + + expect(mover.faceDirection).toBe(FaceDirection.Left); + expect(mover.moveDirection).toBe(FaceDirection.Up); + }); + + // 验证 jump 绝对模式把对象跳跃到目标坐标 + it('jumps to an absolute target', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.jump(3, 4); + const controller = mover.start(); + expect(controller).not.toBeNull(); + await controller!.onEnd; + + expect(tile.x).toBe(3); + expect(tile.y).toBe(4); + }); + + // 验证 jump 相对模式在当前坐标基础上偏移 + it('jumps by a relative offset', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.jump(2, 3, true); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.x).toBe(2); + expect(tile.y).toBe(3); + }); + + // 验证 stepFace 按指定移动方向移动并采用显式朝向 + it('moves by the step move direction and applies the explicit face', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.stepFace(FaceDirection.Right, FaceDirection.Up); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.x).toBe(1); + expect(tile.y).toBe(0); + expect(mover.faceDirection).toBe(FaceDirection.Up); + }); + + // 验证 forward 沿当前朝向连续前进 + it('moves forward along the current face direction', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.forward(2); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.x).toBe(0); + expect(tile.y).toBe(2); + expect(mover.faceDirection).toBe(FaceDirection.Down); + }); + + // 验证 backward 沿当前朝向的反方向后退一步 + it('moves backward against the current face direction', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.backward(1); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.x).toBe(0); + expect(tile.y).toBe(-1); + expect(mover.faceDirection).toBe(FaceDirection.Down); + expect(mover.moveDirection).toBe(FaceDirection.Up); + }); + + // 疑似 bug:连续后退时上一步会把移动方向翻转为反方向,导致方向来回摆动,详见 06-TEST-FINDINGS.md #06-08-1,修复后取消 skip + it.skip('keeps retreating along the same axis across multiple backward steps', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.backward(2); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.x).toBe(0); + expect(tile.y).toBe(-2); + expect(mover.faceDirection).toBe(FaceDirection.Down); + expect(mover.moveDirection).toBe(FaceDirection.Up); + }); + + // 验证 speed 步更新后续移动速度 + it('applies a speed step to the mover', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.speed(250).step(FaceDirection.Right); + const controller = mover.start(); + await controller!.onEnd; + + expect(mover.currentSpeed).toBe(250); + expect(tile.x).toBe(1); + }); + + // 验证 face 步只改变朝向而不产生位移 + it('changes only the face direction without moving', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.face(FaceDirection.Left); + const controller = mover.start(); + await controller!.onEnd; + + expect(mover.faceDirection).toBe(FaceDirection.Left); + expect(tile.setPosCalls).toEqual([]); + }); + + // 验证 animDir 步切换动画播放方向 + it('switches the animation direction', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.animDir(ObjectAnimDirection.Backward); + const controller = mover.start(); + await controller!.onEnd; + + expect(mover.currAnimDir).toBe(ObjectAnimDirection.Backward); + }); + + // 验证 push 追加的多个步骤按传入顺序依次执行 + it('executes pushed steps in order', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.push([ + { type: ObjectMoveType.Dir, move: FaceDirection.Right }, + { type: ObjectMoveType.Teleport, x: 5, y: 5, rel: false } + ]); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.setPosCalls).toEqual([ + { x: 1, y: 0 }, + { x: 5, y: 5 } + ]); + }); + + // 验证 clear 清空尚未执行的步骤队列 + it('clears queued steps before starting', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.step(FaceDirection.Right).clear(); + const controller = mover.start(); + await controller!.onEnd; + + expect(tile.setPosCalls).toEqual([]); + }); + + // 验证已有移动进行中时 start 返回 null,结束后 moving 复位 + it('returns null while a move is already in progress', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + const first = mover.step(FaceDirection.Right).start(); + expect(first).not.toBeNull(); + + const second = mover.start(); + expect(second).toBeNull(); + + await first!.onEnd; + expect(first!.done).toBe(true); + expect(mover.moving).toBe(false); + }); + + // 验证控制器 stop 会阻止队列中剩余步骤执行 + it('stops the move before executing queued steps', async () => { + const tile = new TestTile(); + const mover = createMover(tile); + + mover.step(FaceDirection.Right, 3); + const controller = mover.start(); + expect(controller).not.toBeNull(); + + await controller!.stop(); + + expect(tile.setPosCalls).toEqual([]); + expect(controller!.done).toBe(true); + }); +});