From c9327d6239be352b3bdaae180809968a98e8e4c0 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sun, 26 Apr 2026 21:42:57 +0200 Subject: [PATCH] Implement manual pointer look input handling and update target assist logic --- .../third-person-navigation-controller.ts | 18 +++++-- ...third-person-navigation-controller.test.ts | 54 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/runtime-three/third-person-navigation-controller.ts b/src/runtime-three/third-person-navigation-controller.ts index 165c9bef..ffd592d9 100644 --- a/src/runtime-three/third-person-navigation-controller.ts +++ b/src/runtime-three/third-person-navigation-controller.ts @@ -179,6 +179,7 @@ export class ThirdPersonNavigationController implements NavigationController { private inWaterVolume = false; private inFogVolume = false; private dragging = false; + private pointerLookInputPending = false; private lastPointerClientX = 0; private lastPointerClientY = 0; private initializedFromSpawn = false; @@ -276,6 +277,7 @@ export class ThirdPersonNavigationController implements NavigationController { this.jumpHoldRemainingMs = 0; this.previousTelemetry = null; this.smoothedCameraCollisionDistance = null; + this.pointerLookInputPending = false; ctx.setRuntimeMessage(null); ctx.setPlayerControllerTelemetry(null); this.context = null; @@ -310,6 +312,7 @@ export class ThirdPersonNavigationController implements NavigationController { this.inWaterVolume = false; this.inFogVolume = false; this.dragging = false; + this.pointerLookInputPending = false; this.lastPointerClientX = 0; this.lastPointerClientY = 0; this.initializedFromSpawn = false; @@ -346,6 +349,9 @@ export class ThirdPersonNavigationController implements NavigationController { const cameraDrivenExternally = this.context.isCameraDrivenExternally() === true; const lookInputActive = lookInput.horizontal !== 0 || lookInput.vertical !== 0; + const manualLookInputActive = + lookInputActive || this.pointerLookInputPending; + this.pointerLookInputPending = false; let targetLookResult: RuntimeTargetLookInputResult | null = null; if (!cameraDrivenExternally && lookInputActive) { @@ -385,7 +391,7 @@ export class ThirdPersonNavigationController implements NavigationController { if ( cameraDrivenExternally || - !lookInputActive || + !manualLookInputActive || targetLookResult?.activeTargetLocked !== true || targetLookResult.switchedTarget === true || targetLookResult.switchInputHeld === true @@ -404,7 +410,7 @@ export class ThirdPersonNavigationController implements NavigationController { ); } - if (!cameraDrivenExternally) { + if (!cameraDrivenExternally && !manualLookInputActive) { const targetAssist = this.context.resolveThirdPersonTargetAssist?.() ?? null; @@ -449,7 +455,7 @@ export class ThirdPersonNavigationController implements NavigationController { dt ); } - } else { + } else if (cameraDrivenExternally) { this.targetAssistLookOffsetY = dampScalar( this.targetAssistLookOffsetY, 0, @@ -817,6 +823,12 @@ export class ThirdPersonNavigationController implements NavigationController { this.lastPointerClientX = event.clientX; this.lastPointerClientY = event.clientY; + if (deltaX === 0 && deltaY === 0) { + return; + } + + this.pointerLookInputPending = true; + const targetLookResult = this.context?.handleRuntimeTargetLookInput?.({ horizontal: deltaX * POINTER_TARGET_LOOK_INPUT_SCALE, diff --git a/tests/unit/third-person-navigation-controller.test.ts b/tests/unit/third-person-navigation-controller.test.ts index 467d421f..6725d35d 100644 --- a/tests/unit/third-person-navigation-controller.test.ts +++ b/tests/unit/third-person-navigation-controller.test.ts @@ -454,6 +454,60 @@ describe("ThirdPersonNavigationController", () => { controller.deactivate(targetContext); }); + it("pauses third-person target assist while the camera is actively moved with pointer drag", () => { + const { context } = createRuntimeControllerContext(); + const controller = new ThirdPersonNavigationController(); + const controllerInternals = controller as unknown as { + cameraYawRadians: number; + pitchRadians: number; + targetAssistLookOffsetY: number; + }; + const targetContext = { + ...context, + resolveThirdPersonTargetAssist: () => ({ + targetPosition: { + x: 0, + y: 4, + z: 5 + }, + strength: 1 + }), + handleRuntimeTargetLookInput: vi.fn(() => ({ + activeTargetLocked: true, + switchedTarget: false, + switchInputHeld: false + })) + }; + + controller.activate(targetContext); + controllerInternals.cameraYawRadians = 1.1; + controllerInternals.pitchRadians = 1.1; + controllerInternals.targetAssistLookOffsetY = 0; + + targetContext.domElement.dispatchEvent( + new PointerEvent("pointerdown", { + button: 0, + clientX: 0, + clientY: 0 + }) + ); + window.dispatchEvent( + new PointerEvent("pointermove", { + clientX: 30, + clientY: 12 + }) + ); + + controller.update(0.016); + + expect(controllerInternals.cameraYawRadians).toBeCloseTo(1.1, 5); + expect(controllerInternals.pitchRadians).toBeCloseTo(1.1, 5); + expect(controllerInternals.targetAssistLookOffsetY).toBeCloseTo(0, 5); + + window.dispatchEvent(new PointerEvent("pointerup")); + controller.deactivate(targetContext); + }); + it("fades vertical target assist when camera collision pushes the camera close", () => { const { context } = createRuntimeControllerContext(); const controller = new ThirdPersonNavigationController();