diff --git a/src/app/App.tsx b/src/app/App.tsx index 094c3ac2..c25c51a1 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -10924,6 +10924,29 @@ export function App({ store, initialStatusMessage }: AppProps) { } /> + + Air Direction Only + + commitPlayerStartMovementTemplateDraft( + { + jump: { + directionOnly: + event.currentTarget.checked + } + }, + { + schedule: true + } + ) + } + /> + Bunny Hopping { variableHeight: "yes", maxHoldMs: 0, moveWhileJumping: "yes", - moveWhileFalling: 1 + moveWhileFalling: 1, + directionOnly: "left" }, sprint: { speedMultiplier: 0 @@ -217,6 +218,9 @@ describe("validateSceneDocument", () => { expect.objectContaining({ code: "invalid-player-start-move-while-falling" }), + expect.objectContaining({ + code: "invalid-player-start-air-direction-only" + }), expect.objectContaining({ code: "invalid-player-start-sprint-speed-multiplier" }), diff --git a/tests/serialization/scene-document-json.test.ts b/tests/serialization/scene-document-json.test.ts index 6cf04b03..7c6a9295 100644 --- a/tests/serialization/scene-document-json.test.ts +++ b/tests/serialization/scene-document-json.test.ts @@ -9,6 +9,7 @@ import { IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION, LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION, MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION, + PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION, PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_INPUT_BINDINGS_SCENE_DOCUMENT_VERSION, PLAYER_START_MOVEMENT_TEMPLATE_SCENE_DOCUMENT_VERSION, @@ -467,7 +468,8 @@ describe("scene document JSON", () => { variableHeight: true, maxHoldMs: 220, moveWhileJumping: false, - moveWhileFalling: false + moveWhileFalling: false, + directionOnly: true }, sprint: { speedMultiplier: 1.8 @@ -951,6 +953,7 @@ describe("scene document JSON", () => { const { moveWhileJumping: _moveWhileJumping, moveWhileFalling: _moveWhileFalling, + directionOnly: _directionOnly, ...legacyJump } = playerStart.movementTemplate.jump; const legacyDocument = { @@ -975,6 +978,39 @@ describe("scene document JSON", () => { expect(migratedDocument.entities[playerStart.id]).toEqual(playerStart); }); + it("migrates version 34 Player Start jump settings to include default air direction mode", () => { + const playerStart = createPlayerStartEntity({ + id: "entity-player-start-legacy-air-direction", + movementTemplate: { + kind: "responsive" + } + }); + const { + directionOnly: _directionOnly, + ...legacyJump + } = playerStart.movementTemplate.jump; + const legacyDocument = { + ...createEmptySceneDocument({ + name: "Legacy Player Air Direction Scene" + }), + version: PLAYER_START_AIR_CONTROL_SCENE_DOCUMENT_VERSION, + entities: { + [playerStart.id]: { + ...playerStart, + movementTemplate: { + ...playerStart.movementTemplate, + jump: legacyJump + } + } + } + }; + + const migratedDocument = migrateSceneDocument(legacyDocument); + + expect(migratedDocument.version).toBe(SCENE_DOCUMENT_VERSION); + expect(migratedDocument.entities[playerStart.id]).toEqual(playerStart); + }); + it("round-trips authored third-person Player Start navigation", () => { const playerStart = createPlayerStartEntity({ id: "entity-player-start-third-person", diff --git a/tests/unit/player-locomotion.test.ts b/tests/unit/player-locomotion.test.ts index 523bfc7d..17c8699c 100644 --- a/tests/unit/player-locomotion.test.ts +++ b/tests/unit/player-locomotion.test.ts @@ -574,6 +574,70 @@ describe("player-locomotion", () => { expect(step?.locomotionState.planarSpeed).toBeCloseTo(0); }); + it("reorients airborne movement using existing speed without adding more", () => { + const step = stepPlayerLocomotion({ + dt: 0.1, + feetPosition: { + x: 0, + y: 1, + z: 0 + }, + movementYawRadians: 0, + airDirectionYawRadians: Math.PI / 2, + standingShape: FIRST_PERSON_PLAYER_SHAPE, + verticalVelocity: -2, + previousLocomotionState: createIdleRuntimeLocomotionState("airborne"), + previousPlanarDisplacement: { + x: 0, + y: 0, + z: 0.45 + }, + jumpBufferRemainingMs: 0, + coyoteTimeRemainingMs: 0, + jumpHoldRemainingMs: 0, + crouched: false, + wasJumpPressed: false, + input: FORWARD_INPUT, + movement: { + ...DEFAULT_MOVEMENT, + jump: { + ...DEFAULT_MOVEMENT.jump, + directionOnly: true + } + }, + resolveMotion: (feetPosition, motion): ResolvedPlayerMotion => ({ + feetPosition: { + x: feetPosition.x + motion.x, + y: feetPosition.y + motion.y, + z: feetPosition.z + motion.z + }, + grounded: false, + collisionCount: 0, + groundCollisionNormal: null, + collidedAxes: { + x: false, + y: false, + z: false + } + }), + resolveVolumeState: () => createVolumeState(), + probeGround: () => ({ + grounded: false, + distance: null, + normal: null, + slopeDegrees: null + }), + canOccupyShape: () => true + }); + + expect(step).not.toBeNull(); + expect(step?.locomotionState.locomotionMode).toBe("airborne"); + expect(step?.planarDisplacement.x).toBeCloseTo(0.45); + expect(step?.planarDisplacement.z).toBeCloseTo(0); + expect(step?.locomotionState.requestedPlanarSpeed).toBeCloseTo(4.5); + expect(step?.locomotionState.planarSpeed).toBeCloseTo(4.5); + }); + it("sinks toward the water surface while keeping the head above water", () => { const step = stepPlayerLocomotion({ dt: 0.1, diff --git a/tests/unit/player-start-inspector.integration.test.tsx b/tests/unit/player-start-inspector.integration.test.tsx index 85c9faab..faafefb1 100644 --- a/tests/unit/player-start-inspector.integration.test.tsx +++ b/tests/unit/player-start-inspector.integration.test.tsx @@ -179,6 +179,9 @@ describe("Player Start inspector", () => { const variableJumpCheckbox = screen.getByTestId( "player-start-movement-variable-jump-enabled" ); + const airDirectionOnlyCheckbox = screen.getByTestId( + "player-start-movement-air-direction-only-enabled" + ); const jumpBufferInput = screen.getByTestId( "player-start-movement-jump-buffer" ); @@ -217,6 +220,10 @@ describe("Player Start inspector", () => { fireEvent.click(variableJumpCheckbox); }); + act(() => { + fireEvent.click(airDirectionOnlyCheckbox); + }); + act(() => { fireEvent.change(jumpBufferInput, { target: { @@ -233,7 +240,8 @@ describe("Player Start inspector", () => { moveSpeed: 5.7, jump: { bufferMs: 75, - variableHeight: false + variableHeight: false, + directionOnly: true } } });