From f022dac380077ae0128f97f4215f1e8745845d1e Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Thu, 30 Apr 2026 00:35:42 +0200 Subject: [PATCH] Improve third-person climbing logic and planar movement detection --- .../third-person-navigation-controller.ts | 38 +++++++++++++------ tests/unit/player-climbing.test.ts | 2 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/runtime-three/third-person-navigation-controller.ts b/src/runtime-three/third-person-navigation-controller.ts index 3113b3b4..1369d440 100644 --- a/src/runtime-three/third-person-navigation-controller.ts +++ b/src/runtime-three/third-person-navigation-controller.ts @@ -15,6 +15,8 @@ import { CLIMB_INPUT_ACTIVE_THRESHOLD, CLIMB_SPEED_METERS_PER_SECOND, computeClimbPlaneMovement, + isClimbMovementIntoSurface, + resolveClimbPlanarInputDirection, shouldEnterClimbing, shouldExitClimbing, type RuntimePlayerClimbSurface @@ -665,7 +667,10 @@ export class ThirdPersonNavigationController implements NavigationController { this.publishTelemetry(); } - private resolveClimbProbeDirection(movementYawRadians: number): Vec3 { + private resolveClimbProbeDirection( + movementYawRadians: number, + inputState: ReturnType + ): Vec3 { if (this.climbSurface !== null) { return { x: -this.climbSurface.normal.x, @@ -674,6 +679,15 @@ export class ThirdPersonNavigationController implements NavigationController { }; } + const inputDirection = resolveClimbPlanarInputDirection( + inputState, + movementYawRadians + ); + + if (inputDirection.direction !== null) { + return inputDirection.direction; + } + return { x: Math.sin(movementYawRadians), y: 0, @@ -731,22 +745,27 @@ export class ThirdPersonNavigationController implements NavigationController { const climbPressed = inputState.climb > CLIMB_INPUT_ACTIVE_THRESHOLD; const jumpPressed = inputState.jump > CLIMB_INPUT_ACTIVE_THRESHOLD; - if (!climbPressed) { - this.climbLatchBlocked = false; - } - const climbSurface = this.context.resolvePlayerClimbSurface?.( this.feetPosition, - this.resolveClimbProbeDirection(movementYawRadians), + this.resolveClimbProbeDirection(movementYawRadians, inputState), this.standingPlayerShape, this.climbSurface ) ?? null; + const movementIntoSurface = isClimbMovementIntoSurface({ + input: inputState, + movementYawRadians, + surface: climbSurface + }); + const climbIntentActive = climbPressed || movementIntoSurface; + + if (!climbIntentActive) { + this.climbLatchBlocked = false; + } if ( this.climbSurface !== null && shouldExitClimbing({ - climbInput: inputState.climb, surface: climbSurface, jumpPressed }) @@ -812,10 +831,6 @@ export class ThirdPersonNavigationController implements NavigationController { return true; } - if (climbSurface === null && climbPressed) { - this.climbLatchBlocked = true; - } - return false; } @@ -824,6 +839,7 @@ export class ThirdPersonNavigationController implements NavigationController { (this.climbLatchBlocked || !shouldEnterClimbing({ climbInput: inputState.climb, + movementIntoSurface, surface: climbSurface, jumpPressed })) diff --git a/tests/unit/player-climbing.test.ts b/tests/unit/player-climbing.test.ts index e39b6212..8bbc155b 100644 --- a/tests/unit/player-climbing.test.ts +++ b/tests/unit/player-climbing.test.ts @@ -143,7 +143,7 @@ describe("player climbing helpers", () => { isClimbMovementIntoSurface({ input: createInputState({ moveForward: 1 }), movementYawRadians: 0, - surface, + surface }) ).toBe(true); expect(