2026-04-11 18:36:37 +02:00
import { Euler } from "three" ;
2026-03-31 03:04:15 +02:00
import type { Vec3 } from "../core/vector" ;
2026-04-11 12:30:28 +02:00
import {
2026-04-11 18:36:37 +02:00
FIRST_PERSON_PLAYER_SHAPE ,
cloneFirstPersonPlayerShape ,
getFirstPersonPlayerEyeHeight
} from "./player-collision" ;
import {
resolvePlayerStartActionInputs ,
2026-04-11 12:30:28 +02:00
resolvePlayerStartLookInput ,
} from "./player-input-bindings" ;
2026-04-11 18:36:37 +02:00
import {
createIdleRuntimeLocomotionState ,
stepPlayerLocomotion
} from "./player-locomotion" ;
2026-04-11 19:05:36 +02:00
import { createPlayerControllerTelemetry } from "./player-controller-telemetry" ;
2026-04-11 19:05:05 +02:00
import type { PlayerControllerTelemetry } from "./navigation-controller" ;
2026-04-11 04:19:50 +02:00
import type {
2026-04-11 05:16:30 +02:00
NavigationControllerDeactivateOptions ,
2026-04-11 04:19:50 +02:00
NavigationController ,
RuntimeControllerContext ,
RuntimeLocomotionState
} from "./navigation-controller" ;
2026-04-11 18:00:36 +02:00
import type { RuntimePlayerMovement } from "./runtime-scene-build" ;
2026-03-31 03:04:15 +02:00
const LOOK_SENSITIVITY = 0.0022 ;
2026-04-11 12:30:28 +02:00
const GAMEPAD_LOOK_SPEED = 2.4 ;
2026-03-31 03:04:15 +02:00
const MAX_PITCH_RADIANS = Math . PI * 0.48 ;
function clampPitch ( pitchRadians : number ) : number {
2026-04-11 04:19:50 +02:00
return Math . max (
- MAX_PITCH_RADIANS ,
Math . min ( MAX_PITCH_RADIANS , pitchRadians )
) ;
2026-03-31 03:04:15 +02:00
}
2026-04-04 15:53:09 +02:00
function toEyePosition ( feetPosition : Vec3 , eyeHeight : number ) : Vec3 {
2026-03-31 03:04:15 +02:00
return {
x : feetPosition.x ,
2026-04-04 15:53:09 +02:00
y : feetPosition.y + eyeHeight ,
2026-03-31 03:04:15 +02:00
z : feetPosition.z
} ;
}
2026-04-11 18:00:36 +02:00
function cloneRuntimePlayerMovement (
movement : RuntimePlayerMovement
) : RuntimePlayerMovement {
return {
templateKind : movement.templateKind ,
moveSpeed : movement.moveSpeed ,
2026-04-11 20:30:39 +02:00
maxSpeed : movement.maxSpeed ,
2026-04-11 20:59:15 +02:00
maxStepHeight : movement.maxStepHeight ,
2026-04-11 18:00:36 +02:00
capabilities : {
jump : movement.capabilities.jump ,
sprint : movement.capabilities.sprint ,
crouch : movement.capabilities.crouch
2026-04-11 20:10:01 +02:00
} ,
jump : {
speed : movement.jump.speed ,
bufferMs : movement.jump.bufferMs ,
coyoteTimeMs : movement.jump.coyoteTimeMs ,
variableHeight : movement.jump.variableHeight ,
2026-04-11 20:30:39 +02:00
maxHoldMs : movement.jump.maxHoldMs ,
bunnyHop : movement.jump.bunnyHop ,
bunnyHopBoost : movement.jump.bunnyHopBoost
2026-04-11 20:10:01 +02:00
} ,
sprint : {
speedMultiplier : movement.sprint.speedMultiplier
} ,
crouch : {
speedMultiplier : movement.crouch.speedMultiplier
2026-04-11 18:00:36 +02:00
}
} ;
}
2026-04-11 15:23:53 +02:00
function shouldAutoCapturePointerLockOnActivate ( ) : boolean {
if ( typeof navigator === "undefined" ) {
return true ;
}
const userAgent = navigator . userAgent ;
const vendor = navigator . vendor ;
const isSafari =
vendor . includes ( "Apple" ) &&
userAgent . includes ( "Safari/" ) &&
! userAgent . includes ( "Chrome/" ) &&
! userAgent . includes ( "Chromium/" ) &&
! userAgent . includes ( "CriOS/" ) &&
! userAgent . includes ( "Edg/" ) &&
! userAgent . includes ( "OPR/" ) &&
! userAgent . includes ( "Firefox/" ) ;
return ! isSafari ;
}
2026-03-31 03:04:15 +02:00
export class FirstPersonNavigationController implements NavigationController {
readonly id = "firstPerson" as const ;
private context : RuntimeControllerContext | null = null ;
private readonly pressedKeys = new Set < string > ( ) ;
private readonly cameraRotation = new Euler ( 0 , 0 , 0 , "YXZ" ) ;
private feetPosition = {
x : 0 ,
y : 0 ,
z : 0
} ;
2026-04-11 18:36:37 +02:00
private standingPlayerShape = cloneFirstPersonPlayerShape (
FIRST_PERSON_PLAYER_SHAPE
) ;
private activePlayerShape = cloneFirstPersonPlayerShape (
FIRST_PERSON_PLAYER_SHAPE
) ;
2026-03-31 03:04:15 +02:00
private yawRadians = 0 ;
private pitchRadians = 0 ;
private verticalVelocity = 0 ;
private grounded = false ;
2026-04-11 18:36:37 +02:00
private jumpPressed = false ;
private locomotionState : RuntimeLocomotionState =
createIdleRuntimeLocomotionState ( "flying" ) ;
2026-04-06 08:22:12 +02:00
private inWaterVolume = false ;
private inFogVolume = false ;
2026-03-31 03:04:15 +02:00
private pointerLocked = false ;
2026-04-11 15:13:23 +02:00
private suppressNextPointerLockError = false ;
2026-03-31 03:04:15 +02:00
private initializedFromSpawn = false ;
2026-04-11 19:05:05 +02:00
private previousTelemetry : PlayerControllerTelemetry | null = null ;
private latestJumpStarted = false ;
private latestHeadBump = false ;
2026-04-11 21:43:59 +02:00
private previousPlanarDisplacement = {
x : 0 ,
y : 0 ,
z : 0
} ;
2026-04-11 20:10:01 +02:00
private jumpBufferRemainingMs = 0 ;
private coyoteTimeRemainingMs = 0 ;
private jumpHoldRemainingMs = 0 ;
2026-03-31 03:04:15 +02:00
activate ( ctx : RuntimeControllerContext ) : void {
this . context = ctx ;
if ( ! this . initializedFromSpawn ) {
2026-04-11 18:36:46 +02:00
const runtimeScene = ctx . getRuntimeScene ( ) ;
const spawn = runtimeScene . spawn ;
2026-03-31 03:04:15 +02:00
this . feetPosition = {
. . . spawn . position
} ;
2026-04-11 18:36:46 +02:00
this . standingPlayerShape = cloneFirstPersonPlayerShape (
runtimeScene . playerCollider
) ;
this . activePlayerShape = cloneFirstPersonPlayerShape (
runtimeScene . playerCollider
) ;
2026-03-31 03:04:15 +02:00
this . yawRadians = ( spawn . yawDegrees * Math . PI ) / 180 ;
this . pitchRadians = 0 ;
this . verticalVelocity = 0 ;
this . grounded = false ;
2026-04-11 18:36:46 +02:00
this . jumpPressed = false ;
this . locomotionState = createIdleRuntimeLocomotionState (
runtimeScene . playerCollider . mode === "none" ? "flying" : "airborne"
) ;
2026-04-06 08:22:12 +02:00
this . inWaterVolume = false ;
this . inFogVolume = false ;
2026-03-31 03:04:15 +02:00
this . initializedFromSpawn = true ;
}
window . addEventListener ( "keydown" , this . handleKeyDown ) ;
window . addEventListener ( "keyup" , this . handleKeyUp ) ;
window . addEventListener ( "blur" , this . handleBlur ) ;
document . addEventListener ( "mousemove" , this . handleMouseMove ) ;
2026-04-11 04:19:50 +02:00
document . addEventListener (
"pointerlockchange" ,
this . handlePointerLockChange
) ;
2026-03-31 03:04:15 +02:00
document . addEventListener ( "pointerlockerror" , this . handlePointerLockError ) ;
ctx . domElement . addEventListener ( "pointerdown" , this . handlePointerDown ) ;
this . syncPointerLockState ( ) ;
2026-04-11 15:13:23 +02:00
2026-04-11 15:23:53 +02:00
if (
shouldAutoCapturePointerLockOnActivate ( ) &&
document . pointerLockElement !== ctx . domElement
) {
2026-04-11 15:13:23 +02:00
const pointerLockCapableElement = ctx . domElement as HTMLCanvasElement & {
requestPointerLock ? : ( ) = > void | Promise < void > ;
} ;
if ( typeof pointerLockCapableElement . requestPointerLock === "function" ) {
this . suppressNextPointerLockError = true ;
const pointerLockResult = pointerLockCapableElement . requestPointerLock ( ) ;
if ( pointerLockResult instanceof Promise ) {
pointerLockResult . catch ( ( ) = > { } ) ;
}
}
}
2026-03-31 03:04:15 +02:00
this . updateCameraTransform ( ) ;
this . publishTelemetry ( ) ;
}
2026-04-11 05:16:30 +02:00
deactivate (
ctx : RuntimeControllerContext ,
options : NavigationControllerDeactivateOptions = { }
) : void {
2026-03-31 03:04:15 +02:00
window . removeEventListener ( "keydown" , this . handleKeyDown ) ;
window . removeEventListener ( "keyup" , this . handleKeyUp ) ;
window . removeEventListener ( "blur" , this . handleBlur ) ;
document . removeEventListener ( "mousemove" , this . handleMouseMove ) ;
2026-04-11 04:19:50 +02:00
document . removeEventListener (
"pointerlockchange" ,
this . handlePointerLockChange
) ;
document . removeEventListener (
"pointerlockerror" ,
this . handlePointerLockError
) ;
2026-03-31 03:04:15 +02:00
ctx . domElement . removeEventListener ( "pointerdown" , this . handlePointerDown ) ;
this . pressedKeys . clear ( ) ;
2026-04-11 05:16:30 +02:00
if (
( options . releasePointerLock ? ? true ) &&
document . pointerLockElement === ctx . domElement
) {
2026-03-31 03:04:15 +02:00
document . exitPointerLock ( ) ;
}
this . pointerLocked = false ;
2026-04-11 18:37:16 +02:00
this . suppressNextPointerLockError = false ;
this . jumpPressed = false ;
2026-04-11 19:05:36 +02:00
this . latestJumpStarted = false ;
this . latestHeadBump = false ;
2026-04-11 21:43:59 +02:00
this . previousPlanarDisplacement = {
x : 0 ,
y : 0 ,
z : 0
} ;
2026-04-11 20:10:01 +02:00
this . jumpBufferRemainingMs = 0 ;
this . coyoteTimeRemainingMs = 0 ;
this . jumpHoldRemainingMs = 0 ;
2026-04-11 19:05:36 +02:00
this . previousTelemetry = null ;
2026-03-31 03:04:15 +02:00
ctx . setRuntimeMessage ( null ) ;
2026-04-11 19:05:36 +02:00
ctx . setPlayerControllerTelemetry ( null ) ;
2026-03-31 03:04:15 +02:00
this . context = null ;
}
2026-04-11 04:14:48 +02:00
resetSceneState ( ) : void {
this . pressedKeys . clear ( ) ;
this . feetPosition = {
x : 0 ,
y : 0 ,
z : 0
} ;
this . yawRadians = 0 ;
this . pitchRadians = 0 ;
this . verticalVelocity = 0 ;
this . grounded = false ;
2026-04-11 18:36:46 +02:00
this . jumpPressed = false ;
this . standingPlayerShape = cloneFirstPersonPlayerShape (
FIRST_PERSON_PLAYER_SHAPE
) ;
this . activePlayerShape = cloneFirstPersonPlayerShape (
FIRST_PERSON_PLAYER_SHAPE
) ;
this . locomotionState = createIdleRuntimeLocomotionState ( "flying" ) ;
2026-04-11 04:14:48 +02:00
this . inWaterVolume = false ;
this . inFogVolume = false ;
this . pointerLocked = false ;
2026-04-11 15:13:23 +02:00
this . suppressNextPointerLockError = false ;
2026-04-11 04:14:48 +02:00
this . initializedFromSpawn = false ;
2026-04-11 19:05:36 +02:00
this . previousTelemetry = null ;
this . latestJumpStarted = false ;
this . latestHeadBump = false ;
2026-04-11 21:43:59 +02:00
this . previousPlanarDisplacement = {
x : 0 ,
y : 0 ,
z : 0
} ;
2026-04-11 20:10:01 +02:00
this . jumpBufferRemainingMs = 0 ;
this . coyoteTimeRemainingMs = 0 ;
this . jumpHoldRemainingMs = 0 ;
2026-04-11 04:14:48 +02:00
}
2026-03-31 03:04:15 +02:00
update ( dt : number ) : void {
if ( this . context === null ) {
return ;
}
2026-04-11 18:00:36 +02:00
const runtimeScene = this . context . getRuntimeScene ( ) ;
2026-04-11 18:37:16 +02:00
this . standingPlayerShape = cloneFirstPersonPlayerShape (
runtimeScene . playerCollider
) ;
2026-04-11 18:00:36 +02:00
const playerMovement = runtimeScene . playerMovement ;
2026-04-11 12:30:28 +02:00
const lookInput = resolvePlayerStartLookInput (
2026-04-11 18:00:36 +02:00
runtimeScene . playerInputBindings
2026-04-11 12:30:28 +02:00
) ;
2026-04-11 18:37:16 +02:00
const inputState = resolvePlayerStartActionInputs (
2026-04-11 12:13:06 +02:00
this . pressedKeys ,
2026-04-11 18:00:36 +02:00
runtimeScene . playerInputBindings
2026-04-11 12:13:06 +02:00
) ;
2026-04-11 12:30:28 +02:00
if ( lookInput . horizontal !== 0 || lookInput . vertical !== 0 ) {
this . yawRadians -= lookInput . horizontal * GAMEPAD_LOOK_SPEED * dt ;
this . pitchRadians = clampPitch (
this . pitchRadians + lookInput . vertical * GAMEPAD_LOOK_SPEED * dt
) ;
}
2026-04-11 18:37:16 +02:00
const locomotionStep = stepPlayerLocomotion (
2026-03-31 03:04:15 +02:00
{
2026-04-11 18:37:16 +02:00
dt ,
feetPosition : this.feetPosition ,
movementYawRadians : this.yawRadians ,
standingShape : this.standingPlayerShape ,
verticalVelocity : this.verticalVelocity ,
2026-04-11 19:34:22 +02:00
previousLocomotionState : this.locomotionState ,
2026-04-11 21:43:59 +02:00
previousPlanarDisplacement : this.previousPlanarDisplacement ,
2026-04-11 20:10:01 +02:00
jumpBufferRemainingMs : this.jumpBufferRemainingMs ,
coyoteTimeRemainingMs : this.coyoteTimeRemainingMs ,
jumpHoldRemainingMs : this.jumpHoldRemainingMs ,
2026-04-11 18:37:16 +02:00
crouched : this.locomotionState.crouched ,
wasJumpPressed : this.jumpPressed ,
input : inputState ,
movement : playerMovement ,
resolveMotion : ( feetPosition , motion , shape ) = >
this . context ? . resolveFirstPersonMotion ( feetPosition , motion , shape ) ? ?
null ,
resolveVolumeState : ( feetPosition ) = >
this . context ? . resolvePlayerVolumeState ( feetPosition ) ? ? {
inWater : false ,
2026-04-11 22:12:59 +02:00
inFog : false ,
waterSurfaceHeight : null
2026-04-11 18:37:16 +02:00
} ,
probeGround : ( feetPosition , shape , maxDistance ) = >
2026-04-11 18:42:32 +02:00
this . context ? . probePlayerGround ? . ( feetPosition , shape , maxDistance ) ? ? {
2026-04-11 18:37:16 +02:00
grounded : false ,
distance : null ,
normal : null ,
slopeDegrees : null
} ,
canOccupyShape : ( feetPosition , shape ) = >
2026-04-11 18:42:32 +02:00
this . context ? . canOccupyPlayerShape ? . ( feetPosition , shape ) ? ? true
2026-04-11 18:37:16 +02:00
}
2026-03-31 03:04:15 +02:00
) ;
2026-04-11 18:37:16 +02:00
if ( locomotionStep === null ) {
2026-04-04 07:51:38 +02:00
this . updateCameraTransform ( ) ;
this . publishTelemetry ( ) ;
return ;
}
2026-04-11 18:37:16 +02:00
this . feetPosition = locomotionStep . feetPosition ;
this . activePlayerShape = locomotionStep . activeShape ;
this . verticalVelocity = locomotionStep . verticalVelocity ;
2026-04-11 20:10:01 +02:00
this . jumpBufferRemainingMs = locomotionStep . jumpBufferRemainingMs ;
this . coyoteTimeRemainingMs = locomotionStep . coyoteTimeRemainingMs ;
this . jumpHoldRemainingMs = locomotionStep . jumpHoldRemainingMs ;
2026-04-11 18:37:16 +02:00
this . jumpPressed = locomotionStep . jumpPressed ;
2026-04-11 19:05:36 +02:00
this . latestJumpStarted = locomotionStep . jumpStarted ;
this . latestHeadBump = locomotionStep . headBump ;
2026-04-11 18:37:16 +02:00
this . locomotionState = locomotionStep . locomotionState ;
2026-04-11 21:43:59 +02:00
this . previousPlanarDisplacement = locomotionStep . planarDisplacement ;
2026-04-11 18:37:16 +02:00
this . grounded = locomotionStep . locomotionState . grounded ;
this . inWaterVolume = locomotionStep . inWaterVolume ;
this . inFogVolume = locomotionStep . inFogVolume ;
2026-03-31 03:04:15 +02:00
this . updateCameraTransform ( ) ;
this . publishTelemetry ( ) ;
}
2026-03-31 06:17:31 +02:00
teleportTo ( feetPosition : Vec3 , yawDegrees : number ) {
this . feetPosition = {
. . . feetPosition
} ;
this . yawRadians = ( yawDegrees * Math . PI ) / 180 ;
this . pitchRadians = 0 ;
this . verticalVelocity = 0 ;
this . grounded = false ;
2026-04-11 18:37:16 +02:00
this . jumpPressed = false ;
this . activePlayerShape = cloneFirstPersonPlayerShape (
this . context ? . getRuntimeScene ( ) . playerCollider ? ? FIRST_PERSON_PLAYER_SHAPE
) ;
this . standingPlayerShape = cloneFirstPersonPlayerShape (
this . context ? . getRuntimeScene ( ) . playerCollider ? ? FIRST_PERSON_PLAYER_SHAPE
) ;
this . locomotionState = createIdleRuntimeLocomotionState (
this . activePlayerShape . mode === "none" ? "flying" : "airborne"
) ;
2026-04-11 19:05:36 +02:00
this . previousTelemetry = null ;
this . latestJumpStarted = false ;
this . latestHeadBump = false ;
2026-04-11 21:43:59 +02:00
this . previousPlanarDisplacement = {
x : 0 ,
y : 0 ,
z : 0
} ;
2026-04-11 20:10:01 +02:00
this . jumpBufferRemainingMs = 0 ;
this . coyoteTimeRemainingMs = 0 ;
this . jumpHoldRemainingMs = 0 ;
2026-04-06 08:22:12 +02:00
this . inWaterVolume = false ;
this . inFogVolume = false ;
2026-03-31 06:17:31 +02:00
this . updateCameraTransform ( ) ;
this . publishTelemetry ( ) ;
}
2026-03-31 03:04:15 +02:00
private updateCameraTransform() {
if ( this . context === null ) {
return ;
}
2026-04-11 04:19:50 +02:00
const eyePosition = toEyePosition (
this . feetPosition ,
2026-04-11 18:37:16 +02:00
getFirstPersonPlayerEyeHeight ( this . activePlayerShape )
2026-04-11 04:19:50 +02:00
) ;
2026-03-31 03:04:15 +02:00
this . cameraRotation . x = this . pitchRadians ;
2026-03-31 03:21:30 +02:00
// Authoring yaw treats 0 degrees as facing +Z, while a three.js camera
// looks down -Z by default. Offset by 180 degrees so runtime view matches
// the authored PlayerStart marker and movement basis.
this . cameraRotation . y = this . yawRadians + Math . PI ;
2026-03-31 03:04:15 +02:00
this . cameraRotation . z = 0 ;
2026-04-11 04:19:50 +02:00
this . context . camera . position . set (
eyePosition . x ,
eyePosition . y ,
eyePosition . z
) ;
2026-03-31 03:04:15 +02:00
this . context . camera . rotation . copy ( this . cameraRotation ) ;
}
private publishTelemetry() {
if ( this . context === null ) {
return ;
}
2026-04-11 04:19:50 +02:00
const eyePosition = toEyePosition (
this . feetPosition ,
2026-04-11 18:37:30 +02:00
getFirstPersonPlayerEyeHeight ( this . activePlayerShape )
2026-04-11 04:19:50 +02:00
) ;
const cameraVolumeState =
this . context . resolvePlayerVolumeState ( eyePosition ) ;
2026-04-11 22:12:59 +02:00
const cameraSubmerged =
cameraVolumeState . inWater &&
cameraVolumeState . waterSurfaceHeight !== null &&
eyePosition . y < cameraVolumeState . waterSurfaceHeight ;
2026-04-07 05:29:41 +02:00
2026-04-11 19:05:36 +02:00
const telemetry = createPlayerControllerTelemetry ( {
2026-03-31 03:04:15 +02:00
feetPosition : {
. . . this . feetPosition
} ,
2026-04-07 05:29:41 +02:00
eyePosition ,
2026-03-31 03:04:15 +02:00
grounded : this.grounded ,
2026-04-06 08:22:12 +02:00
locomotionState : this.locomotionState ,
2026-04-11 18:00:36 +02:00
movement : cloneRuntimePlayerMovement (
this . context . getRuntimeScene ( ) . playerMovement
) ,
2026-04-06 08:22:12 +02:00
inWaterVolume : this.inWaterVolume ,
2026-04-11 22:12:59 +02:00
cameraSubmerged ,
2026-04-06 08:22:12 +02:00
inFogVolume : this.inFogVolume ,
2026-03-31 03:04:15 +02:00
pointerLocked : this.pointerLocked ,
2026-04-11 19:05:36 +02:00
spawn : this.context.getRuntimeScene ( ) . spawn ,
previousLocomotionState : this.previousTelemetry?.locomotionState ? ? null ,
previousInWaterVolume : this.previousTelemetry?.inWaterVolume ? ? false ,
jumpStarted : this.latestJumpStarted ,
headBump : this.latestHeadBump
2026-03-31 03:04:15 +02:00
} ) ;
2026-04-11 19:05:36 +02:00
this . context . setPlayerControllerTelemetry ( telemetry ) ;
this . previousTelemetry = telemetry ;
this . latestJumpStarted = false ;
this . latestHeadBump = false ;
2026-03-31 03:04:15 +02:00
}
private syncPointerLockState() {
if ( this . context === null ) {
return ;
}
2026-04-11 04:19:50 +02:00
const pointerLocked =
document . pointerLockElement === this . context . domElement ;
2026-03-31 03:04:15 +02:00
this . pointerLocked = pointerLocked ;
this . context . setRuntimeMessage (
pointerLocked
2026-04-11 12:30:28 +02:00
? "Mouse look active. Press Escape to release the cursor or switch to Third Person. The gamepad right stick also controls the camera."
: "Click inside the runner viewport to capture mouse look. If pointer lock fails, the gamepad right stick still controls the camera and Third Person remains available."
2026-03-31 03:04:15 +02:00
) ;
this . publishTelemetry ( ) ;
}
private handleKeyDown = ( event : KeyboardEvent ) = > {
this . pressedKeys . add ( event . code ) ;
} ;
private handleKeyUp = ( event : KeyboardEvent ) = > {
this . pressedKeys . delete ( event . code ) ;
} ;
private handleBlur = ( ) = > {
this . pressedKeys . clear ( ) ;
} ;
private handleMouseMove = ( event : MouseEvent ) = > {
if ( ! this . pointerLocked ) {
return ;
}
this . yawRadians -= event . movementX * LOOK_SENSITIVITY ;
2026-04-11 04:19:50 +02:00
this . pitchRadians = clampPitch (
this . pitchRadians - event . movementY * LOOK_SENSITIVITY
) ;
2026-03-31 03:04:15 +02:00
} ;
private handlePointerLockChange = ( ) = > {
2026-04-11 15:13:23 +02:00
this . suppressNextPointerLockError = false ;
2026-03-31 03:04:15 +02:00
this . syncPointerLockState ( ) ;
} ;
private handlePointerLockError = ( ) = > {
2026-04-11 15:13:23 +02:00
if ( this . suppressNextPointerLockError ) {
this . suppressNextPointerLockError = false ;
return ;
}
2026-03-31 03:04:15 +02:00
this . context ? . setRuntimeMessage (
2026-04-11 11:15:57 +02:00
"Pointer lock was unavailable in this browser context. Third Person remains available as the non-FPS fallback."
2026-03-31 03:04:15 +02:00
) ;
} ;
private handlePointerDown = ( ) = > {
2026-04-11 04:19:50 +02:00
if (
this . context === null ||
document . pointerLockElement === this . context . domElement
) {
2026-03-31 03:04:15 +02:00
return ;
}
2026-04-11 15:13:23 +02:00
this . suppressNextPointerLockError = false ;
2026-04-11 04:19:50 +02:00
const pointerLockCapableElement = this . context
. domElement as HTMLCanvasElement & {
2026-03-31 03:04:44 +02:00
requestPointerLock ( ) : void | Promise < void > ;
} ;
const pointerLockResult = pointerLockCapableElement . requestPointerLock ( ) ;
2026-03-31 03:04:15 +02:00
2026-03-31 03:04:44 +02:00
if ( pointerLockResult instanceof Promise ) {
2026-03-31 03:04:15 +02:00
pointerLockResult . catch ( ( ) = > {
this . context ? . setRuntimeMessage (
2026-04-11 11:15:57 +02:00
"Pointer lock request was denied. Click again or use Third Person for non-locked navigation."
2026-03-31 03:04:15 +02:00
) ;
} ) ;
}
} ;
}