diff --git a/src/app/App.tsx b/src/app/App.tsx index c04f94ea..d6def651 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -4899,7 +4899,9 @@ export function App({ store, initialStatusMessage }: AppProps) { return; } - const pointerCaptured = firstPersonTelemetry?.pointerLocked === true; + const pointerCaptured = + activeNavigationMode === "firstPerson" && + firstPersonTelemetry?.pointerLocked === true; if (pointerCaptured) { return; @@ -4914,7 +4916,7 @@ export function App({ store, initialStatusMessage }: AppProps) { return () => { window.removeEventListener("keydown", handleWindowKeyDown); }; - }, [editorState.toolMode, firstPersonTelemetry]); + }, [activeNavigationMode, editorState.toolMode, firstPersonTelemetry]); const applyProjectName = () => { const normalizedName = projectNameDraft.trim() || DEFAULT_PROJECT_NAME; @@ -13357,7 +13359,11 @@ export function App({ store, initialStatusMessage }: AppProps) {
Pointer Lock
- {firstPersonTelemetry?.pointerLocked ? "active" : "idle"} + {activeNavigationMode === "firstPerson" + ? firstPersonTelemetry?.pointerLocked + ? "active" + : "idle" + : "not used"}
diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 5bb59a53..91c5e328 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -198,7 +198,6 @@ import { PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION, PLAYER_START_INTERACT_BINDINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION, - PLAYER_START_MOUSE_INVERT_SCENE_DOCUMENT_VERSION, PLAYER_START_TARGETING_SETTINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION, PLAYER_START_PAUSE_BINDINGS_SCENE_DOCUMENT_VERSION, diff --git a/src/runtime-three/first-person-navigation-controller.ts b/src/runtime-three/first-person-navigation-controller.ts index 4517b9d3..274c7ff1 100644 --- a/src/runtime-three/first-person-navigation-controller.ts +++ b/src/runtime-three/first-person-navigation-controller.ts @@ -510,25 +510,23 @@ export class FirstPersonNavigationController implements NavigationController { }; private handleMouseMove = (event: MouseEvent) => { - const context = this.context; - if ( !this.pointerLocked || - context === null || - context.isInputSuspended() === true || - context.isCameraDrivenExternally() === true + this.context?.isInputSuspended() === true || + this.context?.isCameraDrivenExternally() === true ) { return; } const horizontalMouseLookSign = - context.getRuntimeScene().playerStart?.invertMouseCameraHorizontal === true + this.context.getRuntimeScene().playerStart?.invertMouseCameraHorizontal === + true ? -1 : 1; const horizontalMovement = event.movementX * horizontalMouseLookSign; const targetLookResult = - context.handleRuntimeTargetLookInput?.({ + this.context?.handleRuntimeTargetLookInput?.({ horizontal: horizontalMovement, vertical: -event.movementY }) ?? null; diff --git a/src/runtime-three/runtime-host.ts b/src/runtime-three/runtime-host.ts index 4c351967..6872891f 100644 --- a/src/runtime-three/runtime-host.ts +++ b/src/runtime-three/runtime-host.ts @@ -969,56 +969,13 @@ export class RuntimeHost { this.runtimeMessageHandler?.(message); }, setPlayerControllerTelemetry: (telemetry) => { - const pointerLockReleasedWithEscapeTargetClear = - this.currentPlayerControllerTelemetry?.pointerLocked === true && - telemetry !== null && - telemetry.pointerLocked === false && - this.activeController === this.thirdPersonController && - this.activeRuntimeTargetReference !== null && - this.resolveRuntimePlayerInputBindings().keyboard.clearTarget === - "Escape"; - this.currentPlayerControllerTelemetry = telemetry; this.currentPlayerAudioHooks = telemetry?.hooks.audio ?? null; - - if (pointerLockReleasedWithEscapeTargetClear) { - this.clearActiveRuntimeTarget(); - this.requestRuntimePointerLock(); - } - this.playerControllerTelemetryHandler?.(telemetry); } }; } - private requestRuntimePointerLock() { - if ( - document.pointerLockElement === this.domElement || - (this.activeController !== this.firstPersonController && - this.activeController !== this.thirdPersonController) - ) { - return; - } - - const pointerLockCapableElement = this.domElement as HTMLCanvasElement & { - requestPointerLock?: () => void | Promise; - }; - - if (typeof pointerLockCapableElement.requestPointerLock !== "function") { - return; - } - - try { - const pointerLockResult = pointerLockCapableElement.requestPointerLock(); - - if (pointerLockResult instanceof Promise) { - pointerLockResult.catch(() => {}); - } - } catch { - // Browser Escape handling can reject immediate recapture; clearing wins. - } - } - private resolvePlayerVolumeState(feetPosition: { x: number; y: number; diff --git a/tests/domain/scene-document-validation.test.ts b/tests/domain/scene-document-validation.test.ts index 33231a98..cdee47ab 100644 --- a/tests/domain/scene-document-validation.test.ts +++ b/tests/domain/scene-document-validation.test.ts @@ -705,7 +705,6 @@ describe("validateSceneDocument", () => { interactionAngleDegrees: Number.NaN, allowLookInputTargetSwitch: "yes" as unknown as boolean, targetButtonCyclesActiveTarget: 1 as unknown as boolean, - invertMouseCameraHorizontal: "yes" as unknown as boolean, movementTemplate: { kind: "invalidTemplate", moveSpeed: 0, diff --git a/tests/unit/runtime-host.test.ts b/tests/unit/runtime-host.test.ts index d68d9881..8b9749bf 100644 --- a/tests/unit/runtime-host.test.ts +++ b/tests/unit/runtime-host.test.ts @@ -191,7 +191,7 @@ describe("RuntimeHost", () => { message: null }); expect(runtimeMessages).toContain( - "Third Person active. Click inside the runner viewport to capture mouse look, or drag to orbit if pointer lock is unavailable. Scroll to zoom and use the right stick for gamepad camera look." + "Third Person active. Drag to orbit the camera, use the right stick for gamepad camera look, move with your authored bindings, and scroll to zoom." ); }); @@ -3889,66 +3889,6 @@ describe("RuntimeHost", () => { host.dispose(); }); - it("clears active target when third-person Escape releases pointer lock", () => { - const host = new RuntimeHost({ - enableRendering: false - }); - const hostInternals = host as unknown as { - runtimeScene: unknown; - activeController: unknown; - thirdPersonController: unknown; - activeRuntimeTargetReference: { - kind: "npc" | "interactable"; - entityId: string; - } | null; - domElement: HTMLCanvasElement; - controllerContext: { - setPlayerControllerTelemetry(telemetry: unknown): void; - }; - }; - const requestPointerLock = vi.fn(); - - Object.defineProperty(hostInternals.domElement, "requestPointerLock", { - configurable: true, - value: requestPointerLock - }); - - hostInternals.runtimeScene = { - playerInputBindings: { - keyboard: { - clearTarget: "Escape" - } - }, - entities: { - cameraRigs: [], - interactables: [], - npcs: [] - } - } as never; - hostInternals.activeController = hostInternals.thirdPersonController; - hostInternals.activeRuntimeTargetReference = { - kind: "npc", - entityId: "npc-active" - }; - - hostInternals.controllerContext.setPlayerControllerTelemetry({ - pointerLocked: true, - hooks: { - audio: null - } - }); - hostInternals.controllerContext.setPlayerControllerTelemetry({ - pointerLocked: false, - hooks: { - audio: null - } - }); - - expect(hostInternals.activeRuntimeTargetReference).toBeNull(); - expect(requestPointerLock).toHaveBeenCalledTimes(1); - host.dispose(); - }); - it("switches an active target once from directional screen-space look input", () => { const host = new RuntimeHost({ enableRendering: false