diff --git a/src/runtime-three/third-person-navigation-controller.ts b/src/runtime-three/third-person-navigation-controller.ts index 2b4a3780..a7ebb4e5 100644 --- a/src/runtime-three/third-person-navigation-controller.ts +++ b/src/runtime-three/third-person-navigation-controller.ts @@ -181,6 +181,7 @@ export class ThirdPersonNavigationController implements NavigationController { private inFogVolume = false; private pointerLocked = false; private suppressNextPointerLockError = false; + private escapeLockedForTargeting = false; private dragging = false; private pointerLookInputPending = false; private lastPointerClientX = 0; @@ -307,6 +308,7 @@ export class ThirdPersonNavigationController implements NavigationController { this.pointerLocked = false; this.suppressNextPointerLockError = false; + this.releaseEscapeKeyLock(); this.dragging = false; this.jumpPressed = false; this.latestJumpStarted = false; @@ -357,6 +359,7 @@ export class ThirdPersonNavigationController implements NavigationController { this.inFogVolume = false; this.pointerLocked = false; this.suppressNextPointerLockError = false; + this.escapeLockedForTargeting = false; this.dragging = false; this.pointerLookInputPending = false; this.lastPointerClientX = 0; @@ -510,6 +513,8 @@ export class ThirdPersonNavigationController implements NavigationController { ); } + this.syncEscapeKeyLock(); + const movementYawRadians = cameraDrivenExternally ? this.context.getCameraYawRadians() @@ -790,6 +795,11 @@ export class ThirdPersonNavigationController implements NavigationController { document.pointerLockElement === this.context.domElement; this.pointerLocked = pointerLocked; this.dragging = false; + + if (!pointerLocked) { + this.releaseEscapeKeyLock(); + } + this.context.setRuntimeMessage( pointerLocked ? "Third Person mouse look active. Scroll to zoom, use the right stick for gamepad camera look, and press Escape to release the cursor." @@ -798,6 +808,85 @@ export class ThirdPersonNavigationController implements NavigationController { this.publishTelemetry(); } + private syncEscapeKeyLock() { + if (typeof navigator === "undefined") { + return; + } + + const keyboard = ( + navigator as Navigator & { + keyboard?: { + lock?: (keys?: string[]) => Promise; + unlock?: () => void; + }; + } + ).keyboard; + + const shouldLockEscape = + this.pointerLocked && + this.context?.resolveThirdPersonTargetAssist?.() !== null && + typeof keyboard?.lock === "function"; + + if (!shouldLockEscape) { + this.releaseEscapeKeyLock(); + return; + } + + if (this.escapeLockedForTargeting) { + return; + } + + void keyboard + .lock?.(["Escape"]) + .then(() => { + this.escapeLockedForTargeting = true; + }) + .catch(() => {}); + } + + private releaseEscapeKeyLock() { + if (!this.escapeLockedForTargeting || typeof navigator === "undefined") { + this.escapeLockedForTargeting = false; + return; + } + + const keyboard = ( + navigator as Navigator & { + keyboard?: { + unlock?: () => void; + }; + } + ).keyboard; + + keyboard?.unlock?.(); + this.escapeLockedForTargeting = false; + } + + private requestPointerLock() { + if (this.context === null) { + return; + } + + const pointerLockCapableElement = this.context + .domElement as HTMLCanvasElement & { + requestPointerLock?: () => void | Promise; + }; + + if (typeof pointerLockCapableElement.requestPointerLock !== "function") { + return; + } + + const pointerLockResult = pointerLockCapableElement.requestPointerLock(); + + if (pointerLockResult instanceof Promise) { + pointerLockResult.catch(() => { + this.context?.setRuntimeMessage( + "Pointer lock request was denied. Drag orbit remains available in Third Person." + ); + }); + } + } + private resolveHorizontalMouseLookSign() { return this.context?.getRuntimeScene().playerStart ?.invertMouseCameraHorizontal === true @@ -877,22 +966,7 @@ export class ThirdPersonNavigationController implements NavigationController { if (document.pointerLockElement !== this.context.domElement) { this.suppressNextPointerLockError = false; - const pointerLockCapableElement = this.context - .domElement as HTMLCanvasElement & { - requestPointerLock?: () => void | Promise; - }; - - if (typeof pointerLockCapableElement.requestPointerLock === "function") { - const pointerLockResult = pointerLockCapableElement.requestPointerLock(); - - if (pointerLockResult instanceof Promise) { - pointerLockResult.catch(() => { - this.context?.setRuntimeMessage( - "Pointer lock request was denied. Drag orbit remains available in Third Person." - ); - }); - } - } + this.requestPointerLock(); } if ( @@ -1024,8 +1098,17 @@ export class ThirdPersonNavigationController implements NavigationController { }; private handlePointerLockChange = () => { + const wasPointerLocked = this.pointerLocked; this.suppressNextPointerLockError = false; this.syncPointerLockState(); + + if ( + wasPointerLocked && + !this.pointerLocked && + this.context?.handleThirdPersonPointerLockReleased?.() === true + ) { + this.requestPointerLock(); + } }; private handlePointerLockError = () => {