From 2fae0158558c4cbfdd6b6c230a1c482aca50d3d6 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 11 Apr 2026 19:14:01 +0200 Subject: [PATCH] Update tests for player controller telemetry and refactor navigation controller tests --- ...first-person-navigation-controller.test.ts | 15 +- .../unit/player-controller-telemetry.test.ts | 169 ++++++++++++++++++ tests/unit/runner-canvas.test.tsx | 40 ++++- ...third-person-navigation-controller.test.ts | 8 +- 4 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 tests/unit/player-controller-telemetry.test.ts diff --git a/tests/unit/first-person-navigation-controller.test.ts b/tests/unit/first-person-navigation-controller.test.ts index 96c7bf43..45a09bb1 100644 --- a/tests/unit/first-person-navigation-controller.test.ts +++ b/tests/unit/first-person-navigation-controller.test.ts @@ -103,7 +103,7 @@ function createRuntimeControllerContext( ...desiredCameraPosition }), setRuntimeMessage: vi.fn(), - setFirstPersonTelemetry: vi.fn() + setPlayerControllerTelemetry: vi.fn() } }; } @@ -250,7 +250,8 @@ describe("FirstPersonNavigationController", () => { window.dispatchEvent(new KeyboardEvent("keydown", { code: "KeyW" })); controller.update(1); - const telemetry = context.setFirstPersonTelemetry.mock.calls.at(-1)?.[0]; + const telemetry = + context.setPlayerControllerTelemetry.mock.calls.at(-1)?.[0]; expect(telemetry?.feetPosition.z).toBeCloseTo(2.25); expect(telemetry?.movement).toMatchObject({ @@ -345,13 +346,18 @@ describe("FirstPersonNavigationController", () => { window.dispatchEvent(new KeyboardEvent("keydown", { code: "Space" })); controller.update(0.1); - const telemetry = context.setFirstPersonTelemetry.mock.calls.at(-1)?.[0]; + const telemetry = + context.setPlayerControllerTelemetry.mock.calls.at(-1)?.[0]; expect(telemetry?.grounded).toBe(false); expect(telemetry?.locomotionState.locomotionMode).toBe("airborne"); expect(telemetry?.locomotionState.airborneKind).toBe("jumping"); expect(telemetry?.locomotionState.verticalVelocity).toBeGreaterThan(0); expect(telemetry?.feetPosition.y ?? 0).toBeGreaterThan(0); + expect(telemetry?.signals.jumpStarted).toBe(true); + expect(telemetry?.signals.leftGround).toBe(true); + expect(telemetry?.hooks.camera.jumping).toBe(true); + expect(telemetry?.hooks.animation.airborneKind).toBe("jumping"); window.dispatchEvent(new KeyboardEvent("keyup", { code: "Space" })); controller.deactivate(context, { @@ -397,7 +403,8 @@ describe("FirstPersonNavigationController", () => { ); controller.update(0.1); - const telemetry = context.setFirstPersonTelemetry.mock.calls.at(-1)?.[0]; + const telemetry = + context.setPlayerControllerTelemetry.mock.calls.at(-1)?.[0]; expect(telemetry?.locomotionState.gait).toBe("crouch"); expect(telemetry?.locomotionState.crouched).toBe(true); diff --git a/tests/unit/player-controller-telemetry.test.ts b/tests/unit/player-controller-telemetry.test.ts new file mode 100644 index 00000000..19b992da --- /dev/null +++ b/tests/unit/player-controller-telemetry.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it } from "vitest"; + +import { createEmptySceneDocument } from "../../src/document/scene-document"; +import type { RuntimeLocomotionState } from "../../src/runtime-three/navigation-controller"; +import { + createPlayerControllerTelemetry, + resolveRuntimeMovementTransitionSignals +} from "../../src/runtime-three/player-controller-telemetry"; +import { buildRuntimeSceneFromDocument } from "../../src/runtime-three/runtime-scene-build"; + +function createLocomotionState( + overrides: Partial = {} +): RuntimeLocomotionState { + return { + locomotionMode: "grounded", + airborneKind: null, + gait: "idle", + grounded: true, + crouched: false, + sprinting: false, + inputMagnitude: 0, + requestedPlanarSpeed: 0, + planarSpeed: 0, + verticalVelocity: 0, + contact: { + collisionCount: 0, + collidedAxes: { + x: false, + y: false, + z: false + }, + groundNormal: null, + groundDistance: null, + slopeDegrees: null, + ...overrides.contact + }, + ...overrides + }; +} + +describe("player-controller-telemetry", () => { + it("derives jump lift-off and wall-contact transitions", () => { + const previousLocomotionState = createLocomotionState({ + locomotionMode: "grounded", + grounded: true, + gait: "walk", + planarSpeed: 1.4, + inputMagnitude: 1 + }); + const locomotionState = createLocomotionState({ + locomotionMode: "airborne", + airborneKind: "jumping", + grounded: false, + gait: "walk", + planarSpeed: 1.6, + inputMagnitude: 1, + verticalVelocity: 5.2, + contact: { + collisionCount: 1, + collidedAxes: { + x: true, + y: false, + z: false + }, + groundNormal: null, + groundDistance: null, + slopeDegrees: null + } + }); + + expect( + resolveRuntimeMovementTransitionSignals({ + previousLocomotionState, + previousInWaterVolume: false, + locomotionState, + inWaterVolume: false, + jumpStarted: true, + headBump: false + }) + ).toEqual({ + jumpStarted: true, + leftGround: true, + startedFalling: false, + landed: false, + enteredWater: false, + exitedWater: false, + wallContactStarted: true, + headBump: false + }); + }); + + it("derives falling, water-entry, and head-bump transitions", () => { + const previousLocomotionState = createLocomotionState({ + locomotionMode: "airborne", + airborneKind: "jumping", + grounded: false, + verticalVelocity: 1.8 + }); + const locomotionState = createLocomotionState({ + locomotionMode: "airborne", + airborneKind: "falling", + grounded: false, + verticalVelocity: -3.4 + }); + + expect( + resolveRuntimeMovementTransitionSignals({ + previousLocomotionState, + previousInWaterVolume: false, + locomotionState, + inWaterVolume: true, + jumpStarted: false, + headBump: true + }) + ).toEqual({ + jumpStarted: false, + leftGround: false, + startedFalling: true, + landed: false, + enteredWater: true, + exitedWater: false, + wallContactStarted: false, + headBump: true + }); + }); + + it("packages movement hooks for camera, audio, and animation consumers", () => { + const runtimeScene = buildRuntimeSceneFromDocument( + createEmptySceneDocument() + ); + const telemetry = createPlayerControllerTelemetry({ + feetPosition: { x: 1, y: 2, z: 3 }, + eyePosition: { x: 1, y: 3.6, z: 3 }, + grounded: false, + locomotionState: createLocomotionState({ + locomotionMode: "swimming", + airborneKind: null, + grounded: false, + gait: "walk", + inputMagnitude: 0.35, + planarSpeed: 0.6, + verticalVelocity: 0 + }), + movement: runtimeScene.playerMovement, + inWaterVolume: true, + cameraSubmerged: true, + inFogVolume: false, + pointerLocked: false, + spawn: runtimeScene.spawn, + previousLocomotionState: createLocomotionState({ + locomotionMode: "grounded", + grounded: true + }), + previousInWaterVolume: false, + jumpStarted: false, + headBump: false + }); + + expect(telemetry.signals.enteredWater).toBe(true); + expect(telemetry.hooks.camera.swimming).toBe(true); + expect(telemetry.hooks.camera.underwaterAmount).toBe(1); + expect(telemetry.hooks.audio.underwaterAmount).toBe(1); + expect(telemetry.hooks.animation.locomotionMode).toBe("swimming"); + expect(telemetry.hooks.animation.moving).toBe(true); + expect(telemetry.hooks.animation.movementAmount).toBeCloseTo(0.6); + expect(telemetry.hooks.animation.inWater).toBe(true); + expect(telemetry.hooks.animation.signals.enteredWater).toBe(true); + }); +}); \ No newline at end of file diff --git a/tests/unit/runner-canvas.test.tsx b/tests/unit/runner-canvas.test.tsx index fcc3536f..a4f176bb 100644 --- a/tests/unit/runner-canvas.test.tsx +++ b/tests/unit/runner-canvas.test.tsx @@ -15,6 +15,17 @@ function createSwimmingTelemetry( runtimeScene: ReturnType, cameraSubmerged: boolean ): FirstPersonTelemetry { + const signals = { + jumpStarted: false, + leftGround: false, + startedFalling: false, + landed: false, + enteredWater: false, + exitedWater: false, + wallContactStarted: false, + headBump: false + }; + return { feetPosition: { x: 0, y: 0, z: 0 }, eyePosition: { x: 0, y: cameraSubmerged ? 0.4 : 1.7, z: 0 }, @@ -47,7 +58,34 @@ function createSwimmingTelemetry( cameraSubmerged, inFogVolume: false, pointerLocked: true, - spawn: runtimeScene.spawn + spawn: runtimeScene.spawn, + signals, + hooks: { + camera: { + jumping: false, + falling: false, + landing: false, + swimming: true, + underwaterAmount: cameraSubmerged ? 1 : 0.55 + }, + audio: { + underwaterAmount: cameraSubmerged ? 1 : 0.55, + enteredWater: false, + exitedWater: false + }, + animation: { + locomotionMode: "swimming", + airborneKind: null, + gait: "walk", + moving: true, + movementAmount: 1, + grounded: false, + crouched: false, + sprinting: false, + inWater: true, + signals + } + } }; } diff --git a/tests/unit/third-person-navigation-controller.test.ts b/tests/unit/third-person-navigation-controller.test.ts index 0973ce64..bee4e19e 100644 --- a/tests/unit/third-person-navigation-controller.test.ts +++ b/tests/unit/third-person-navigation-controller.test.ts @@ -100,7 +100,7 @@ function createRuntimeControllerContext( ...desiredCameraPosition }), setRuntimeMessage: vi.fn(), - setFirstPersonTelemetry: vi.fn() + setPlayerControllerTelemetry: vi.fn() } }; } @@ -184,7 +184,8 @@ describe("ThirdPersonNavigationController", () => { window.dispatchEvent(new KeyboardEvent("keydown", { code: "ArrowUp" })); controller.update(1); - const telemetry = context.setFirstPersonTelemetry.mock.calls.at(-1)?.[0]; + const telemetry = + context.setPlayerControllerTelemetry.mock.calls.at(-1)?.[0]; expect( Math.hypot( @@ -254,7 +255,8 @@ describe("ThirdPersonNavigationController", () => { ); controller.update(1); - const telemetry = context.setFirstPersonTelemetry.mock.calls.at(-1)?.[0]; + const telemetry = + context.setPlayerControllerTelemetry.mock.calls.at(-1)?.[0]; expect(telemetry?.locomotionState.gait).toBe("sprint"); expect(telemetry?.locomotionState.sprinting).toBe(true);