From 66e1b78ca13c2d5a51e1a78c5473a46924771c9e Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 6 Apr 2026 08:25:36 +0200 Subject: [PATCH] Add volume state tracking and update locomotion logic in first-person navigation controller --- .../first-person-navigation-controller.js | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/runtime-three/first-person-navigation-controller.js b/src/runtime-three/first-person-navigation-controller.js index 49ca6cbb..4f781afa 100644 --- a/src/runtime-three/first-person-navigation-controller.js +++ b/src/runtime-three/first-person-navigation-controller.js @@ -30,6 +30,9 @@ export class FirstPersonNavigationController { pitchRadians = 0; verticalVelocity = 0; grounded = false; + locomotionState = "flying"; + inWaterVolume = false; + inFogVolume = false; pointerLocked = false; initializedFromSpawn = false; activate(ctx) { @@ -43,6 +46,9 @@ export class FirstPersonNavigationController { this.pitchRadians = 0; this.verticalVelocity = 0; this.grounded = false; + this.locomotionState = "flying"; + this.inWaterVolume = false; + this.inFogVolume = false; this.initializedFromSpawn = true; } window.addEventListener("keydown", this.handleKeyDown); @@ -78,6 +84,7 @@ export class FirstPersonNavigationController { return; } const playerShape = this.context.getRuntimeScene().playerCollider; + const currentVolumeState = this.context.resolvePlayerVolumeState(this.feetPosition); const inputX = (this.pressedKeys.has("KeyD") ? 1 : 0) - (this.pressedKeys.has("KeyA") ? 1 : 0); const inputZ = (this.pressedKeys.has("KeyW") ? 1 : 0) - (this.pressedKeys.has("KeyS") ? 1 : 0); const inputLength = Math.hypot(inputX, inputZ); @@ -95,12 +102,15 @@ export class FirstPersonNavigationController { if (playerShape.mode === "none") { this.verticalVelocity = 0; } + else if (currentVolumeState.inWater) { + this.verticalVelocity = 0; + } else { this.verticalVelocity -= GRAVITY * dt; } const resolvedMotion = this.context.resolveFirstPersonMotion(this.feetPosition, { x: horizontalX, - y: playerShape.mode === "none" ? 0 : this.verticalVelocity * dt, + y: playerShape.mode === "none" || currentVolumeState.inWater ? 0 : this.verticalVelocity * dt, z: horizontalZ }, playerShape); if (resolvedMotion === null) { @@ -109,10 +119,28 @@ export class FirstPersonNavigationController { return; } this.feetPosition = resolvedMotion.feetPosition; - this.grounded = resolvedMotion.grounded; + const nextVolumeState = this.context.resolvePlayerVolumeState(this.feetPosition); + this.inWaterVolume = nextVolumeState.inWater; + this.inFogVolume = nextVolumeState.inFog; + this.grounded = nextVolumeState.inWater ? false : resolvedMotion.grounded; + if (playerShape.mode === "none") { + this.locomotionState = "flying"; + } + else if (this.inWaterVolume) { + this.locomotionState = "swimming"; + } + else if (this.grounded) { + this.locomotionState = "grounded"; + } + else { + this.locomotionState = "flying"; + } if (this.grounded && this.verticalVelocity < 0) { this.verticalVelocity = 0; } + else if (this.inWaterVolume) { + this.verticalVelocity = 0; + } this.updateCameraTransform(); this.publishTelemetry(); } @@ -124,6 +152,9 @@ export class FirstPersonNavigationController { this.pitchRadians = 0; this.verticalVelocity = 0; this.grounded = false; + this.locomotionState = "flying"; + this.inWaterVolume = false; + this.inFogVolume = false; this.updateCameraTransform(); this.publishTelemetry(); } @@ -151,6 +182,9 @@ export class FirstPersonNavigationController { }, eyePosition: toEyePosition(this.feetPosition, getFirstPersonPlayerEyeHeight(this.context.getRuntimeScene().playerCollider)), grounded: this.grounded, + locomotionState: this.locomotionState, + inWaterVolume: this.inWaterVolume, + inFogVolume: this.inFogVolume, pointerLocked: this.pointerLocked, spawn: this.context.getRuntimeScene().spawn });