From 927482a15c117270271d8fee76f925e2b88af716 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Mon, 27 Apr 2026 18:14:41 +0200 Subject: [PATCH] Refactor player start settings: remove mouse camera inversion and update first-person pointer lock logic --- src/app/App.tsx | 44 +++--------- src/document/migrate-scene-document.ts | 1 - .../first-person-navigation-controller.ts | 12 ++-- .../domain/scene-document-validation.test.ts | 4 -- tests/unit/entity-instances.test.ts | 3 - ...first-person-navigation-controller.test.ts | 37 ---------- ...rt-interaction-reach-serialization.test.ts | 29 -------- tests/unit/runtime-host.test.ts | 70 +------------------ ...third-person-navigation-controller.test.ts | 41 ----------- 9 files changed, 15 insertions(+), 226 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 02f2ff51..f28df4b9 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -3735,9 +3735,6 @@ export function App({ store, initialStatusMessage }: AppProps) { setPlayerStartTargetButtonCyclesActiveTargetDraft( DEFAULT_PLAYER_START_TARGET_BUTTON_CYCLES_ACTIVE_TARGET_VALUE ); - setPlayerStartInvertMouseCameraHorizontalDraft( - DEFAULT_PLAYER_START_INVERT_MOUSE_CAMERA_HORIZONTAL_VALUE - ); setPlayerStartMovementTemplateDraft(createPlayerStartMovementTemplate()); setPlayerStartMovementTemplateNumberDraft( createPlayerStartMovementTemplateNumberDraft( @@ -3907,9 +3904,6 @@ export function App({ store, initialStatusMessage }: AppProps) { setPlayerStartTargetButtonCyclesActiveTargetDraft( selectedEntity.targetButtonCyclesActiveTarget ); - setPlayerStartInvertMouseCameraHorizontalDraft( - selectedEntity.invertMouseCameraHorizontal - ); setPlayerStartMovementTemplateDraft( clonePlayerStartMovementTemplate(selectedEntity.movementTemplate) ); @@ -4910,7 +4904,9 @@ export function App({ store, initialStatusMessage }: AppProps) { return; } - const pointerCaptured = firstPersonTelemetry?.pointerLocked === true; + const pointerCaptured = + activeNavigationMode === "firstPerson" && + firstPersonTelemetry?.pointerLocked === true; if (pointerCaptured) { return; @@ -4925,7 +4921,7 @@ export function App({ store, initialStatusMessage }: AppProps) { return () => { window.removeEventListener("keydown", handleWindowKeyDown); }; - }, [editorState.toolMode, firstPersonTelemetry]); + }, [activeNavigationMode, editorState.toolMode, firstPersonTelemetry]); const applyProjectName = () => { const normalizedName = projectNameDraft.trim() || DEFAULT_PROJECT_NAME; @@ -8907,7 +8903,6 @@ export function App({ store, initialStatusMessage }: AppProps) { overrides: { allowLookInputTargetSwitch?: boolean; colliderMode?: PlayerStartColliderMode; - invertMouseCameraHorizontal?: boolean; movementTemplate?: PlayerStartMovementTemplate; navigationMode?: PlayerStartNavigationMode; inputBindings?: PlayerStartInputBindings; @@ -8947,9 +8942,6 @@ export function App({ store, initialStatusMessage }: AppProps) { const targetButtonCyclesActiveTarget = overrides.targetButtonCyclesActiveTarget ?? playerStartTargetButtonCyclesActiveTargetDraft; - const invertMouseCameraHorizontal = - overrides.invertMouseCameraHorizontal ?? - playerStartInvertMouseCameraHorizontalDraft; const nextEntity = createPlayerStartEntity({ id: selectedPlayerStart.id, name: selectedPlayerStart.name, @@ -8960,7 +8952,6 @@ export function App({ store, initialStatusMessage }: AppProps) { interactionAngleDegrees, allowLookInputTargetSwitch, targetButtonCyclesActiveTarget, - invertMouseCameraHorizontal, movementTemplate, inputBindings, collider: { @@ -13373,7 +13364,11 @@ export function App({ store, initialStatusMessage }: AppProps) {
Pointer Lock
- {firstPersonTelemetry?.pointerLocked ? "active" : "idle"} + {activeNavigationMode === "firstPerson" + ? firstPersonTelemetry?.pointerLocked + ? "active" + : "idle" + : "not used"}
@@ -20842,27 +20837,6 @@ export function App({ store, initialStatusMessage }: AppProps) { }} /> -
diff --git a/src/document/migrate-scene-document.ts b/src/document/migrate-scene-document.ts index 5bb59a53..91c5e328 100644 --- a/src/document/migrate-scene-document.ts +++ b/src/document/migrate-scene-document.ts @@ -198,7 +198,6 @@ import { PLAYER_START_GAMEPAD_CAMERA_LOOK_SCENE_DOCUMENT_VERSION, PLAYER_START_INTERACT_BINDINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION, - PLAYER_START_MOUSE_INVERT_SCENE_DOCUMENT_VERSION, PLAYER_START_TARGETING_SETTINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_NAVIGATION_MODE_SCENE_DOCUMENT_VERSION, PLAYER_START_PAUSE_BINDINGS_SCENE_DOCUMENT_VERSION, diff --git a/src/runtime-three/first-person-navigation-controller.ts b/src/runtime-three/first-person-navigation-controller.ts index 0e3ab539..274c7ff1 100644 --- a/src/runtime-three/first-person-navigation-controller.ts +++ b/src/runtime-three/first-person-navigation-controller.ts @@ -510,25 +510,23 @@ export class FirstPersonNavigationController implements NavigationController { }; private handleMouseMove = (event: MouseEvent) => { - const context = this.context; - if ( !this.pointerLocked || - context?.isInputSuspended() === true || - context?.isCameraDrivenExternally() === true || - context === null + this.context?.isInputSuspended() === true || + this.context?.isCameraDrivenExternally() === true ) { return; } const horizontalMouseLookSign = - context.getRuntimeScene().playerStart?.invertMouseCameraHorizontal === true + this.context.getRuntimeScene().playerStart?.invertMouseCameraHorizontal === + true ? -1 : 1; const horizontalMovement = event.movementX * horizontalMouseLookSign; const targetLookResult = - context.handleRuntimeTargetLookInput?.({ + this.context?.handleRuntimeTargetLookInput?.({ horizontal: horizontalMovement, vertical: -event.movementY }) ?? null; diff --git a/tests/domain/scene-document-validation.test.ts b/tests/domain/scene-document-validation.test.ts index 583739a1..cdee47ab 100644 --- a/tests/domain/scene-document-validation.test.ts +++ b/tests/domain/scene-document-validation.test.ts @@ -705,7 +705,6 @@ describe("validateSceneDocument", () => { interactionAngleDegrees: Number.NaN, allowLookInputTargetSwitch: "yes" as unknown as boolean, targetButtonCyclesActiveTarget: 1 as unknown as boolean, - invertMouseCameraHorizontal: "yes" as unknown as boolean, movementTemplate: { kind: "invalidTemplate", moveSpeed: 0, @@ -793,9 +792,6 @@ describe("validateSceneDocument", () => { expect.objectContaining({ code: "invalid-player-start-target-button-cycles-active-target" }), - expect.objectContaining({ - code: "invalid-player-start-invert-mouse-camera-horizontal" - }), expect.objectContaining({ code: "invalid-player-start-movement-template-kind" }), diff --git a/tests/unit/entity-instances.test.ts b/tests/unit/entity-instances.test.ts index 473ba298..eb7eea2b 100644 --- a/tests/unit/entity-instances.test.ts +++ b/tests/unit/entity-instances.test.ts @@ -19,7 +19,6 @@ import { DEFAULT_PLAYER_START_CROUCH_SETTINGS, DEFAULT_PLAYER_START_EYE_HEIGHT, DEFAULT_PLAYER_START_ALLOW_LOOK_INPUT_TARGET_SWITCH, - DEFAULT_PLAYER_START_INVERT_MOUSE_CAMERA_HORIZONTAL, DEFAULT_PLAYER_START_INTERACTION_ANGLE_DEGREES, DEFAULT_PLAYER_START_INTERACTION_REACH_METERS, DEFAULT_PLAYER_START_JUMP_SETTINGS, @@ -60,8 +59,6 @@ describe("entity registry defaults", () => { DEFAULT_PLAYER_START_ALLOW_LOOK_INPUT_TARGET_SWITCH, targetButtonCyclesActiveTarget: DEFAULT_PLAYER_START_TARGET_BUTTON_CYCLES_ACTIVE_TARGET, - invertMouseCameraHorizontal: - DEFAULT_PLAYER_START_INVERT_MOUSE_CAMERA_HORIZONTAL, movementTemplate: { kind: "default", moveSpeed: DEFAULT_PLAYER_START_MOVE_SPEED, diff --git a/tests/unit/first-person-navigation-controller.test.ts b/tests/unit/first-person-navigation-controller.test.ts index 8ff67184..2737111a 100644 --- a/tests/unit/first-person-navigation-controller.test.ts +++ b/tests/unit/first-person-navigation-controller.test.ts @@ -162,43 +162,6 @@ describe("FirstPersonNavigationController", () => { expect(exitPointerLockSpy).toHaveBeenCalledTimes(1); }); - it("applies authored horizontal mouse inversion while pointer-locked", () => { - const playerStart = createPlayerStartEntity({ - id: "entity-player-start-invert-first-person", - invertMouseCameraHorizontal: true - }); - const { context, domElement } = createRuntimeControllerContext(playerStart); - const controller = new FirstPersonNavigationController(); - const mouseMoveEvent = new MouseEvent("mousemove"); - - Object.defineProperty(mouseMoveEvent, "movementX", { - configurable: true, - value: 20 - }); - Object.defineProperty(mouseMoveEvent, "movementY", { - configurable: true, - value: 0 - }); - Object.defineProperty(document, "pointerLockElement", { - configurable: true, - get: () => domElement - }); - - controller.activate(context); - document.dispatchEvent(mouseMoveEvent); - controller.update(0); - - const telemetry = - context.setPlayerControllerTelemetry.mock.calls.at(-1)?.[0]; - - expect(telemetry?.pointerLocked).toBe(true); - expect(telemetry?.yawDegrees).toBeGreaterThan(0); - - controller.deactivate(context, { - releasePointerLock: false - }); - }); - it("uses authored gamepad bindings instead of the hardcoded stick mapping", () => { const playerStart = createPlayerStartEntity({ id: "entity-player-start-custom-gamepad", diff --git a/tests/unit/player-start-interaction-reach-serialization.test.ts b/tests/unit/player-start-interaction-reach-serialization.test.ts index 96da400e..bcb0b5ba 100644 --- a/tests/unit/player-start-interaction-reach-serialization.test.ts +++ b/tests/unit/player-start-interaction-reach-serialization.test.ts @@ -3,13 +3,11 @@ import { describe, expect, it } from "vitest"; import { migrateSceneDocument } from "../../src/document/migrate-scene-document"; import { createEmptySceneDocument, - PLAYER_START_TARGETING_SETTINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_INTERACTION_REACH_SCENE_DOCUMENT_VERSION, PLAYER_START_INTERACT_BINDINGS_SCENE_DOCUMENT_VERSION } from "../../src/document/scene-document"; import { DEFAULT_PLAYER_START_ALLOW_LOOK_INPUT_TARGET_SWITCH, - DEFAULT_PLAYER_START_INVERT_MOUSE_CAMERA_HORIZONTAL, DEFAULT_PLAYER_START_INTERACTION_ANGLE_DEGREES, DEFAULT_PLAYER_START_INTERACTION_REACH_METERS, DEFAULT_PLAYER_START_TARGET_BUTTON_CYCLES_ACTIVE_TARGET, @@ -54,7 +52,6 @@ describe("Player Start interaction sector persistence", () => { interactionAngleDegrees: 42, allowLookInputTargetSwitch: false, targetButtonCyclesActiveTarget: true, - invertMouseCameraHorizontal: true, inputBindings: { keyboard: { clearTarget: "KeyQ" @@ -79,7 +76,6 @@ describe("Player Start interaction sector persistence", () => { interactionAngleDegrees: 42, allowLookInputTargetSwitch: false, targetButtonCyclesActiveTarget: true, - invertMouseCameraHorizontal: true, inputBindings: { keyboard: { clearTarget: "KeyQ" @@ -151,29 +147,4 @@ describe("Player Start interaction sector persistence", () => { } }); }); - - it("migrates version 83 player starts to include the mouse inversion default", () => { - const playerStart = createPlayerStartEntity({ - id: "entity-player-start-mouse-invert-legacy" - }); - const legacyPlayerStart = { - ...playerStart - } as Record; - - delete legacyPlayerStart.invertMouseCameraHorizontal; - - const migrated = migrateSceneDocument({ - ...createEmptySceneDocument({ name: "Legacy Player Mouse Invert Scene" }), - version: PLAYER_START_TARGETING_SETTINGS_SCENE_DOCUMENT_VERSION, - entities: { - [playerStart.id]: legacyPlayerStart - } - }); - - expect(migrated.entities[playerStart.id]).toMatchObject({ - kind: "playerStart", - invertMouseCameraHorizontal: - DEFAULT_PLAYER_START_INVERT_MOUSE_CAMERA_HORIZONTAL - }); - }); }); diff --git a/tests/unit/runtime-host.test.ts b/tests/unit/runtime-host.test.ts index bddad022..8b9749bf 100644 --- a/tests/unit/runtime-host.test.ts +++ b/tests/unit/runtime-host.test.ts @@ -191,7 +191,7 @@ describe("RuntimeHost", () => { message: null }); expect(runtimeMessages).toContain( - "Third Person active. Click inside the runner viewport to capture mouse look, or drag to orbit if pointer lock is unavailable. Scroll to zoom and use the right stick for gamepad camera look." + "Third Person active. Drag to orbit the camera, use the right stick for gamepad camera look, move with your authored bindings, and scroll to zoom." ); }); @@ -3889,74 +3889,6 @@ describe("RuntimeHost", () => { host.dispose(); }); - it("preserves pointer lock when switching between first- and third-person controllers", () => { - const host = new RuntimeHost({ - enableRendering: false - }); - const runtimeScene = buildRuntimeSceneFromDocument( - { - ...createEmptySceneDocument(), - entities: { - "entity-player-start-switch": createPlayerStartEntity({ - id: "entity-player-start-switch" - }) - } - }, - { - navigationMode: "firstPerson" - } - ); - const hostInternals = host as unknown as { - activeController: { - id: "firstPerson" | "thirdPerson"; - deactivate: ReturnType; - } | null; - controllerContext: unknown; - desiredNavigationMode: "firstPerson" | "thirdPerson"; - runtimeScene: ReturnType | null; - sceneReady: boolean; - thirdPersonController: { - id: "thirdPerson"; - activate: ReturnType; - deactivate: ReturnType; - }; - activateDesiredNavigationController(): void; - }; - const deactivate = vi.fn(); - const activate = vi.fn(); - const nextControllerDeactivate = vi.fn(); - const domElement = ( - host as unknown as { - domElement: HTMLCanvasElement; - } - ).domElement; - - hostInternals.runtimeScene = runtimeScene; - hostInternals.sceneReady = true; - hostInternals.activeController = { - id: "firstPerson", - deactivate - }; - hostInternals.desiredNavigationMode = "thirdPerson"; - hostInternals.thirdPersonController = { - id: "thirdPerson", - activate, - deactivate: nextControllerDeactivate - }; - Object.defineProperty(document, "pointerLockElement", { - configurable: true, - get: () => domElement - }); - - hostInternals.activateDesiredNavigationController(); - - expect(deactivate).toHaveBeenCalledWith(hostInternals.controllerContext, { - releasePointerLock: false - }); - expect(activate).toHaveBeenCalledWith(hostInternals.controllerContext); - host.dispose(); - }); - it("switches an active target once from directional screen-space look input", () => { const host = new RuntimeHost({ enableRendering: false diff --git a/tests/unit/third-person-navigation-controller.test.ts b/tests/unit/third-person-navigation-controller.test.ts index a2a57f92..31a5733b 100644 --- a/tests/unit/third-person-navigation-controller.test.ts +++ b/tests/unit/third-person-navigation-controller.test.ts @@ -171,47 +171,6 @@ describe("ThirdPersonNavigationController", () => { controller.deactivate(context); }); - it("captures pointer-locked third-person mouse look and honors horizontal inversion", () => { - const playerStart = createPlayerStartEntity({ - id: "entity-player-start-invert-third-person", - invertMouseCameraHorizontal: true - }); - const { context } = createRuntimeControllerContext(playerStart); - const controller = new ThirdPersonNavigationController(); - const controllerInternals = controller as unknown as { - pointerLocked: boolean; - handleMouseMove(event: MouseEvent): void; - }; - const requestPointerLock = vi.fn(); - const mouseMoveEvent = new MouseEvent("mousemove"); - - Object.defineProperty(mouseMoveEvent, "movementX", { - configurable: true, - value: 24 - }); - Object.defineProperty(mouseMoveEvent, "movementY", { - configurable: true, - value: 0 - }); - Object.defineProperty(context.domElement, "requestPointerLock", { - configurable: true, - value: requestPointerLock - }); - - controller.activate(context); - expect(requestPointerLock).toHaveBeenCalledTimes(1); - - controllerInternals.pointerLocked = true; - controllerInternals.handleMouseMove(mouseMoveEvent); - controller.update(0); - - expect(context.camera.position.x).toBeLessThan(0); - - controller.deactivate(context, { - releasePointerLock: false - }); - }); - it("smooths the third-person camera back out when collision clears", () => { const { context } = createRuntimeControllerContext(); const controller = new ThirdPersonNavigationController();