diff --git a/tests/unit/pause-navigation-controller.test.ts b/tests/unit/pause-navigation-controller.test.ts new file mode 100644 index 00000000..b742cea0 --- /dev/null +++ b/tests/unit/pause-navigation-controller.test.ts @@ -0,0 +1,149 @@ +import { PerspectiveCamera } from "three"; +import { describe, expect, it, vi } from "vitest"; + +import type { Vec3 } from "../../src/core/vector"; +import { createEmptySceneDocument } from "../../src/document/scene-document"; +import { createPlayerStartEntity } from "../../src/entities/entity-instances"; +import type { + FirstPersonPlayerShape, + PlayerGroundProbeResult, + ResolvedPlayerMotion +} from "../../src/runtime-three/player-collision"; +import { FirstPersonNavigationController } from "../../src/runtime-three/first-person-navigation-controller"; +import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build"; +import { ThirdPersonNavigationController } from "../../src/runtime-three/third-person-navigation-controller"; + +function createSuspendedContext(navigationMode: "firstPerson" | "thirdPerson") { + const playerStart = createPlayerStartEntity({ + id: `entity-player-start-${navigationMode}` + }); + const runtimeScene = buildRuntimeSceneFromDocument( + { + ...createEmptySceneDocument({ name: `Paused ${navigationMode} Scene` }), + entities: { + [playerStart.id]: playerStart + } + }, + { + navigationMode + } + ); + const domElement = document.createElement("canvas"); + + return { + domElement, + context: { + camera: new PerspectiveCamera(70, 1, 0.05, 1000), + domElement, + getRuntimeScene: () => runtimeScene, + resolveFirstPersonMotion: ( + feetPosition: Vec3, + motion: Vec3, + _shape: FirstPersonPlayerShape + ): ResolvedPlayerMotion => ({ + feetPosition: { + x: feetPosition.x + motion.x, + y: feetPosition.y + motion.y, + z: feetPosition.z + motion.z + }, + grounded: false, + collisionCount: 0, + groundCollisionNormal: null, + collidedAxes: { + x: false, + y: false, + z: false + } + }), + probePlayerGround: (): PlayerGroundProbeResult => ({ + grounded: false, + distance: null, + normal: null, + slopeDegrees: null + }), + canOccupyPlayerShape: () => true, + resolvePlayerVolumeState: () => ({ + inWater: false, + inFog: false, + waterSurfaceHeight: null + }), + resolveThirdPersonCameraCollision: ( + _pivot: Vec3, + desiredCameraPosition: Vec3 + ) => ({ ...desiredCameraPosition }), + isInputSuspended: () => true, + setRuntimeMessage: vi.fn(), + setPlayerControllerTelemetry: vi.fn() + } + }; +} + +describe("paused navigation input", () => { + it("ignores first-person mouse look while input is suspended", () => { + const { context, domElement } = createSuspendedContext("firstPerson"); + const controller = new FirstPersonNavigationController(); + + Object.defineProperty(document, "pointerLockElement", { + configurable: true, + get: () => domElement + }); + + controller.activate(context); + const initialQuaternion = context.camera.quaternion.clone(); + + (controller as unknown as { + handleMouseMove(event: { movementX: number; movementY: number }): void; + }).handleMouseMove({ + movementX: 48, + movementY: 24 + }); + controller.update(0); + + expect(context.camera.quaternion.equals(initialQuaternion)).toBe(true); + + controller.deactivate(context, { + releasePointerLock: false + }); + }); + + it("ignores third-person orbit and zoom while input is suspended", () => { + const { context } = createSuspendedContext("thirdPerson"); + const controller = new ThirdPersonNavigationController(); + + controller.activate(context); + const initialPosition = context.camera.position.clone(); + + ( + controller as unknown as { + handlePointerDown(event: { button: number; clientX: number; clientY: number }): void; + handlePointerMove(event: { clientX: number; clientY: number }): void; + handleWheel(event: { preventDefault(): void; deltaY: number }): void; + } + ).handlePointerDown({ + button: 0, + clientX: 100, + clientY: 100 + }); + ( + controller as unknown as { + handlePointerMove(event: { clientX: number; clientY: number }): void; + } + ).handlePointerMove({ + clientX: 180, + clientY: 150 + }); + ( + controller as unknown as { + handleWheel(event: { preventDefault(): void; deltaY: number }): void; + } + ).handleWheel({ + preventDefault() {}, + deltaY: -240 + }); + controller.update(0); + + expect(context.camera.position.equals(initialPosition)).toBe(true); + + controller.deactivate(context); + }); +});