Refactor player start settings: remove mouse camera inversion and update first-person pointer lock logic
This commit is contained in:
@@ -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) {
|
||||
<div className="stat-card">
|
||||
<div className="label">Pointer Lock</div>
|
||||
<div className="value">
|
||||
{firstPersonTelemetry?.pointerLocked ? "active" : "idle"}
|
||||
{activeNavigationMode === "firstPerson"
|
||||
? firstPersonTelemetry?.pointerLocked
|
||||
? "active"
|
||||
: "idle"
|
||||
: "not used"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
@@ -20842,27 +20837,6 @@ export function App({ store, initialStatusMessage }: AppProps) {
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field form-field--toggle">
|
||||
<span className="label">Invert Mouse Camera</span>
|
||||
<input
|
||||
data-testid="player-start-invert-mouse-camera"
|
||||
type="checkbox"
|
||||
checked={
|
||||
playerStartInvertMouseCameraHorizontalDraft
|
||||
}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.currentTarget.checked;
|
||||
setPlayerStartInvertMouseCameraHorizontalDraft(
|
||||
nextValue
|
||||
);
|
||||
scheduleDraftCommit(() =>
|
||||
applyPlayerStartChange({
|
||||
invertMouseCameraHorizontal: nextValue
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<typeof vi.fn>;
|
||||
} | null;
|
||||
controllerContext: unknown;
|
||||
desiredNavigationMode: "firstPerson" | "thirdPerson";
|
||||
runtimeScene: ReturnType<typeof buildRuntimeSceneFromDocument> | null;
|
||||
sceneReady: boolean;
|
||||
thirdPersonController: {
|
||||
id: "thirdPerson";
|
||||
activate: ReturnType<typeof vi.fn>;
|
||||
deactivate: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user