import type { Vec3 } from "../core/vector";

import type {
  RuntimeLocomotionContactState,
  RuntimeLocomotionMode,
  RuntimeLocomotionState,
  RuntimePlayerVolumeState
} from "./navigation-controller";
import type { PlayerStartActionInputState } from "./player-input-bindings";
import {
  cloneFirstPersonPlayerShape,
  createCrouchedFirstPersonPlayerShape,
  type FirstPersonPlayerShape,
  type PlayerGroundProbeResult,
  type ResolvedPlayerMotion
} from "./player-collision";
import type { RuntimePlayerMovement } from "./runtime-scene-build";

const ACTION_ACTIVE_THRESHOLD = 0.5;
const GRAVITY = 22;
const GROUND_PROBE_DISTANCE = 0.12;
const VERTICAL_ASCENT_EPSILON = 1e-4;
const IDLE_SPEED_EPSILON = 0.05;
const VARIABLE_JUMP_HOLD_GRAVITY_FACTOR = 0.45;
const VARIABLE_JUMP_RELEASE_VELOCITY_FACTOR = 0.45;
const SWIM_HEAD_CLEARANCE = 0.04;

function clampPlanarSpeed(speed: number, maxSpeed: number): number {
  if (maxSpeed <= 0) {
    return speed;
  }

  return Math.min(speed, maxSpeed);
}

function clampUnitInterval(value: number): number {
  return Math.max(0, Math.min(1, value));
}

function createEmptyLocomotionContactState(): RuntimeLocomotionContactState {
  return {
    collisionCount: 0,
    collidedAxes: {
      x: false,
      y: false,
      z: false
    },
    groundNormal: null,
    groundDistance: null,
    slopeDegrees: null
  };
}

export function createIdleRuntimeLocomotionState(
  locomotionMode: RuntimeLocomotionMode = "flying"
): RuntimeLocomotionState {
  return {
    locomotionMode,
    airborneKind:
      locomotionMode === "airborne" ? "falling" : null,
    gait: "idle",
    grounded: locomotionMode === "grounded",
    crouched: false,
    sprinting: false,
    inputMagnitude: 0,
    requestedPlanarSpeed: 0,
    planarSpeed: 0,
    verticalVelocity: 0,
    contact: createEmptyLocomotionContactState()
  };
}

export interface StepPlayerLocomotionOptions {
  dt: number;
  feetPosition: Vec3;
  movementYawRadians: number;
  airDirectionYawRadians?: number;
  standingShape: FirstPersonPlayerShape;
  verticalVelocity: number;
  previousLocomotionState?: RuntimeLocomotionState;
  previousPlanarDisplacement: Vec3;
  jumpBufferRemainingMs: number;
  coyoteTimeRemainingMs: number;
  jumpHoldRemainingMs: number;
  crouched: boolean;
  wasJumpPressed: boolean;
  input: PlayerStartActionInputState;
  movement: RuntimePlayerMovement;
  resolveMotion(
    feetPosition: Vec3,
    motion: Vec3,
    shape: FirstPersonPlayerShape
  ): ResolvedPlayerMotion | null;
  resolveVolumeState(feetPosition: Vec3): RuntimePlayerVolumeState;
  probeGround(
    feetPosition: Vec3,
    shape: FirstPersonPlayerShape,
    maxDistance: number
  ): PlayerGroundProbeResult;
  canOccupyShape(
    feetPosition: Vec3,
    shape: FirstPersonPlayerShape
  ): boolean;
}

export interface StepPlayerLocomotionResult {
  feetPosition: Vec3;
  activeShape: FirstPersonPlayerShape;
  verticalVelocity: number;
  jumpBufferRemainingMs: number;
  coyoteTimeRemainingMs: number;
  jumpHoldRemainingMs: number;
  crouched: boolean;
  jumpPressed: boolean;
  jumpStarted: boolean;
  headBump: boolean;
  locomotionState: RuntimeLocomotionState;
  inWaterVolume: boolean;
  inFogVolume: boolean;
  planarDisplacement: Vec3;
}

function readGroundProbe(
  feetPosition: Vec3,
  shape: FirstPersonPlayerShape,
  probeGround: StepPlayerLocomotionOptions["probeGround"]
): PlayerGroundProbeResult {
  if (shape.mode === "none") {
    return {
      grounded: false,
      distance: null,
      normal: null,
      slopeDegrees: null
    };
  }

  return probeGround(feetPosition, shape, GROUND_PROBE_DISTANCE);
}

function resolveLocomotionMode(
  shape: FirstPersonPlayerShape,
  inWaterVolume: boolean,
  grounded: boolean,
  headSubmerged: boolean
): RuntimeLocomotionMode {
  if (shape.mode === "none") {
    return "flying";
  }

  if (inWaterVolume) {
    return headSubmerged ? "diving" : "swimming";
  }

  if (grounded) {
    return "grounded";
  }

  return "airborne";
}

function resolveAirborneKind(
  locomotionMode: RuntimeLocomotionMode,
  verticalVelocity: number
): RuntimeLocomotionState["airborneKind"] {
  if (locomotionMode !== "airborne") {
    return null;
  }

  return verticalVelocity > 0 ? "jumping" : "falling";
}

function resolveGait(
  crouched: boolean,
  sprinting: boolean,
  planarSpeed: number
): RuntimeLocomotionState["gait"] {
  if (crouched) {
    return "crouch";
  }

  if (planarSpeed <= IDLE_SPEED_EPSILON) {
    return "idle";
  }

  return sprinting ? "sprint" : "walk";
}

function computePlanarMotion(
  movementYawRadians: number,
  input: PlayerStartActionInputState,
  requestedPlanarSpeed: number,
  dt: number
): { motion: Vec3; inputMagnitude: number } {
  const directionResult = computePlanarInputDirection(
    movementYawRadians,
    input
  );

  if (
    directionResult.direction === null ||
    requestedPlanarSpeed <= 0 ||
    dt <= 0
  ) {
    return {
      motion: {
        x: 0,
        y: 0,
        z: 0
      },
      inputMagnitude: directionResult.inputMagnitude
    };
  }

  const planarDistance = requestedPlanarSpeed * dt;

  return {
    motion: {
      x: directionResult.direction.x * planarDistance,
      y: 0,
      z: directionResult.direction.z * planarDistance
    },
    inputMagnitude: directionResult.inputMagnitude
  };
}

function computePlanarInputDirection(
  movementYawRadians: number,
  input: PlayerStartActionInputState
): { direction: Vec3 | null; inputMagnitude: number } {
  const inputX = input.moveRight - input.moveLeft;
  const inputZ = input.moveForward - input.moveBackward;
  const rawMagnitude = Math.hypot(inputX, inputZ);
  const inputMagnitude = clampUnitInterval(rawMagnitude);

  if (rawMagnitude <= 0) {
    return {
      direction: null,
      inputMagnitude
    };
  }

  const normalizedInputX = inputX / rawMagnitude;
  const normalizedInputZ = inputZ / rawMagnitude;
  const forwardX = Math.sin(movementYawRadians);
  const forwardZ = Math.cos(movementYawRadians);
  const rightX = -Math.cos(movementYawRadians);
  const rightZ = Math.sin(movementYawRadians);
  const directionX =
    forwardX * normalizedInputZ + rightX * normalizedInputX;
  const directionZ =
    forwardZ * normalizedInputZ + rightZ * normalizedInputX;
  const directionMagnitude = Math.hypot(directionX, directionZ);

  if (directionMagnitude <= 0) {
    return {
      direction: null,
      inputMagnitude
    };
  }

  return {
    direction: {
      x: directionX / directionMagnitude,
      y: 0,
      z: directionZ / directionMagnitude
    },
    inputMagnitude
  };
}

function computePlanarSpeedFromDisplacement(
  displacement: Vec3,
  dt: number
): number {
  if (dt <= 0) {
    return 0;
  }

  return Math.hypot(displacement.x, displacement.z) / dt;
}

function clearPlanarMovementInput(
  input: PlayerStartActionInputState
): PlayerStartActionInputState {
  return {
    ...input,
    moveForward: 0,
    moveBackward: 0,
    moveLeft: 0,
    moveRight: 0
  };
}

function computeDirectionalAirMotion(options: {
  directionYawRadians: number;
  input: PlayerStartActionInputState;
  previousPlanarDisplacement: Vec3;
  dt: number;
}): { motion: Vec3; inputMagnitude: number } {
  const directionResult = computePlanarInputDirection(
    options.directionYawRadians,
    options.input
  );
  const planarSpeed = computePlanarSpeedFromDisplacement(
    options.previousPlanarDisplacement,
    options.dt
  );

  if (
    directionResult.direction === null ||
    planarSpeed <= 0 ||
    options.dt <= 0
  ) {
    return {
      motion: {
        x: 0,
        y: 0,
        z: 0
      },
      inputMagnitude: directionResult.inputMagnitude
    };
  }

  const planarDistance = planarSpeed * options.dt;

  return {
    motion: {
      x: directionResult.direction.x * planarDistance,
      y: 0,
      z: directionResult.direction.z * planarDistance
    },
    inputMagnitude: directionResult.inputMagnitude
  };
}

function isWaterLocomotionMode(
  locomotionMode: RuntimeLocomotionMode | null | undefined
): boolean {
  return locomotionMode === "swimming" || locomotionMode === "diving";
}

function isShallowWater(options: {
  inWater: boolean;
  waterSurfaceHeight: number | null;
  feetPosition: Vec3;
  shape: FirstPersonPlayerShape;
  groundProbe: PlayerGroundProbeResult;
}): boolean {
  if (
    !options.inWater ||
    options.waterSurfaceHeight === null ||
    options.groundProbe.grounded !== true ||
    options.groundProbe.distance === null
  ) {
    return false;
  }

  const waterDepth =
    options.waterSurfaceHeight -
    (options.feetPosition.y - options.groundProbe.distance);

  return waterDepth <= options.shape.eyeHeight + SWIM_HEAD_CLEARANCE;
}

function shouldPreserveAirborneWaterCrossing(options: {
  inWater: boolean;
  waterSurfaceHeight: number | null;
  feetPosition: Vec3;
  shape: FirstPersonPlayerShape;
  verticalVelocity: number;
  previousLocomotionState?: RuntimeLocomotionState;
}): boolean {
  if (
    !options.inWater ||
    options.waterSurfaceHeight === null ||
    options.verticalVelocity <= VERTICAL_ASCENT_EPSILON ||
    isWaterLocomotionMode(options.previousLocomotionState?.locomotionMode)
  ) {
    return false;
  }

  const headHeight = options.feetPosition.y + options.shape.eyeHeight;

  return headHeight >= options.waterSurfaceHeight;
}

function alignPlanarMotionToGround(
  planarMotion: Vec3,
  groundNormal: Vec3 | null
): Vec3 {
  if (groundNormal === null || groundNormal.y <= VERTICAL_ASCENT_EPSILON) {
    return planarMotion;
  }

  return {
    x: planarMotion.x,
    y:
      -(
        groundNormal.x * planarMotion.x +
        groundNormal.z * planarMotion.z
      ) / groundNormal.y,
    z: planarMotion.z
  };
}

function resolveContactState(
  resolvedMotion: ResolvedPlayerMotion,
  groundProbe: PlayerGroundProbeResult,
  grounded: boolean
): RuntimeLocomotionContactState {
  const groundNormal = groundProbe.normal ?? resolvedMotion.groundCollisionNormal;
  const groundDistance =
    groundProbe.distance ?? (grounded ? 0 : null);

  return {
    collisionCount: resolvedMotion.collisionCount,
    collidedAxes: {
      x: resolvedMotion.collidedAxes.x,
      y: resolvedMotion.collidedAxes.y,
      z: resolvedMotion.collidedAxes.z
    },
    groundNormal,
    groundDistance,
    slopeDegrees:
      groundProbe.slopeDegrees ??
      (groundNormal === null
        ? null
        : (Math.acos(Math.max(-1, Math.min(1, groundNormal.y))) * 180) /
          Math.PI)
  };
}

export function stepPlayerLocomotion(
  options: StepPlayerLocomotionOptions
): StepPlayerLocomotionResult | null {
  const currentVolumeState = options.resolveVolumeState(options.feetPosition);
  const dtMs = Math.max(0, options.dt * 1000);
  const jumpPressed = options.input.jump > ACTION_ACTIVE_THRESHOLD;
  const sprintPressed = options.input.sprint > ACTION_ACTIVE_THRESHOLD;
  const crouchPressed = options.input.crouch > ACTION_ACTIVE_THRESHOLD;
  const jumpJustPressed = jumpPressed && !options.wasJumpPressed;
  const canCrouch =
    options.movement.capabilities.crouch &&
    options.standingShape.mode !== "none";
  let jumpBufferRemainingMs = Math.max(
    0,
    options.jumpBufferRemainingMs - dtMs
  );
  let coyoteTimeRemainingMs = Math.max(
    0,
    options.coyoteTimeRemainingMs - dtMs
  );
  let jumpHoldRemainingMs = Math.max(0, options.jumpHoldRemainingMs - dtMs);

  let crouched = options.crouched && canCrouch;

  if (canCrouch) {
    if (crouchPressed) {
      crouched = true;
    } else if (crouched) {
      crouched = !options.canOccupyShape(
        options.feetPosition,
        options.standingShape
      );
    }
  } else {
    crouched = false;
  }

  const activeShape = crouched
    ? createCrouchedFirstPersonPlayerShape(options.standingShape)
    : cloneFirstPersonPlayerShape(options.standingShape);
  const currentGroundProbe = readGroundProbe(
    options.feetPosition,
    activeShape,
    options.probeGround
  );
  const currentShallowWater = isShallowWater({
    inWater: currentVolumeState.inWater,
    waterSurfaceHeight: currentVolumeState.waterSurfaceHeight,
    feetPosition: options.feetPosition,
    shape: activeShape,
    groundProbe: currentGroundProbe
  });
  const currentSwimmableWater =
    currentVolumeState.inWater &&
    currentVolumeState.waterSurfaceHeight !== null &&
    !currentShallowWater &&
    !shouldPreserveAirborneWaterCrossing({
      inWater: currentVolumeState.inWater,
      waterSurfaceHeight: currentVolumeState.waterSurfaceHeight,
      feetPosition: options.feetPosition,
      shape: activeShape,
      verticalVelocity: options.verticalVelocity,
      previousLocomotionState: options.previousLocomotionState
    });
  // The probe can still see nearby floor on the frame after takeoff. While a
  // jump is still carrying positive upward velocity, don't let that probe pull
  // the controller back into grounded/stick-to-ground logic.
  const ascendingFromPreviousFrame =
    options.verticalVelocity > VERTICAL_ASCENT_EPSILON;
  const canUseImmediateGrounding =
    options.previousLocomotionState === undefined ||
    options.previousLocomotionState.grounded ||
    Math.abs(options.verticalVelocity) <= VERTICAL_ASCENT_EPSILON;
  const currentlyGrounded =
    currentGroundProbe.grounded &&
    !ascendingFromPreviousFrame &&
    canUseImmediateGrounding;

  if (currentlyGrounded) {
    coyoteTimeRemainingMs = options.movement.jump.coyoteTimeMs;
  }

  if (
    options.movement.capabilities.jump &&
    jumpJustPressed &&
    !currentSwimmableWater
  ) {
    jumpBufferRemainingMs = options.movement.jump.bufferMs;
  }

  const sprinting =
    options.movement.capabilities.sprint &&
    sprintPressed &&
    !crouched &&
    currentlyGrounded &&
    !currentSwimmableWater;
  const previousRequestedPlanarSpeed = Math.max(
    0,
    options.previousLocomotionState?.requestedPlanarSpeed ?? 0
  );
  const groundedRequestedPlanarSpeed = clampPlanarSpeed(
    options.movement.moveSpeed *
      (crouched
        ? options.movement.crouch.speedMultiplier
        : sprinting
          ? options.movement.sprint.speedMultiplier
          : 1),
    options.movement.maxSpeed
  );
  const airborneRequestedPlanarSpeed = clampPlanarSpeed(
    Math.max(options.movement.moveSpeed, previousRequestedPlanarSpeed),
    options.movement.maxSpeed
  );
  const jumpTriggered =
    options.movement.capabilities.jump &&
    (jumpJustPressed || jumpBufferRemainingMs > 0) &&
    (currentlyGrounded || coyoteTimeRemainingMs > 0) &&
    !currentSwimmableWater &&
    activeShape.mode !== "none";
  const bufferedJumpTriggered =
    jumpTriggered && !jumpJustPressed && jumpBufferRemainingMs > 0;
  const preservePreviousJumpSpeed =
    !currentlyGrounded || bufferedJumpTriggered;
  const jumpRequestedPlanarSpeedBase = clampPlanarSpeed(
    Math.max(
      options.movement.moveSpeed,
      groundedRequestedPlanarSpeed,
      preservePreviousJumpSpeed ? previousRequestedPlanarSpeed : 0
    ),
    options.movement.maxSpeed
  );
  const jumpRequestedPlanarSpeed =
    bufferedJumpTriggered && options.movement.jump.bunnyHop
      ? clampPlanarSpeed(
          jumpRequestedPlanarSpeedBase *
            (1 + options.movement.jump.bunnyHopBoost),
          options.movement.maxSpeed
        )
      : jumpRequestedPlanarSpeedBase;

  if (jumpTriggered) {
    jumpBufferRemainingMs = 0;
    coyoteTimeRemainingMs = 0;
  }

  const airMovementAllowed =
    currentlyGrounded ||
    jumpTriggered ||
    currentSwimmableWater ||
    (options.verticalVelocity > VERTICAL_ASCENT_EPSILON
      ? options.movement.jump.moveWhileJumping
      : options.movement.jump.moveWhileFalling);
  const planarInput = airMovementAllowed
    ? options.input
    : clearPlanarMovementInput(options.input);
  const directionalAirControlActive =
    !currentlyGrounded &&
    !jumpTriggered &&
    !currentSwimmableWater &&
    airMovementAllowed &&
    options.movement.jump.directionOnly;
  const previousPlanarSpeed = computePlanarSpeedFromDisplacement(
    options.previousPlanarDisplacement,
    options.dt
  );

  const requestedPlanarSpeed =
    activeShape.mode !== "none" && !currentSwimmableWater
      ? jumpTriggered
        ? jumpRequestedPlanarSpeed
        : currentlyGrounded
          ? groundedRequestedPlanarSpeed
          : airborneRequestedPlanarSpeed
      : groundedRequestedPlanarSpeed;
  const planarMotionFromInput = directionalAirControlActive
    ? computeDirectionalAirMotion({
        directionYawRadians:
          options.airDirectionYawRadians ?? options.movementYawRadians,
        input: planarInput,
        previousPlanarDisplacement: options.previousPlanarDisplacement,
        dt: options.dt
      })
    : computePlanarMotion(
        options.movementYawRadians,
        planarInput,
        requestedPlanarSpeed,
        options.dt
      );
  const preserveAirborneMomentum =
    activeShape.mode !== "none" &&
    !currentSwimmableWater &&
    (jumpTriggered || !currentlyGrounded) &&
    planarMotionFromInput.inputMagnitude <= 0;
  const planarMotion = preserveAirborneMomentum
    ? {
        motion: {
          x: options.previousPlanarDisplacement.x,
          y: 0,
          z: options.previousPlanarDisplacement.z
        },
        inputMagnitude: 0
      }
    : planarMotionFromInput;

  const groundedPlanarMotion =
    currentlyGrounded && !jumpTriggered
      ? alignPlanarMotionToGround(
          planarMotion.motion,
          currentGroundProbe.normal
        )
      : planarMotion.motion;

  let verticalVelocity = options.verticalVelocity;
  let verticalDisplacement = 0;
  const waterVerticalInput =
    (jumpPressed ? 1 : 0) - (sprintPressed ? 1 : 0);
  const swimVerticalSpeed = options.movement.moveSpeed;

  if (activeShape.mode === "none") {
    verticalVelocity = 0;
    jumpHoldRemainingMs = 0;
  } else if (currentSwimmableWater) {
    const waterSurfaceHeight =
      currentVolumeState.waterSurfaceHeight ??
      options.feetPosition.y + activeShape.eyeHeight;
    const targetFeetY =
      waterSurfaceHeight +
      SWIM_HEAD_CLEARANCE -
      activeShape.eyeHeight;
    const currentHeadSubmerged =
      options.feetPosition.y + activeShape.eyeHeight <
      waterSurfaceHeight;

    if (waterVerticalInput !== 0) {
      verticalVelocity = waterVerticalInput * swimVerticalSpeed;
      verticalDisplacement = verticalVelocity * options.dt;
    } else if (!currentHeadSubmerged) {
      const targetDelta = targetFeetY - options.feetPosition.y;
      verticalDisplacement = Math.max(
        -swimVerticalSpeed * options.dt,
        Math.min(swimVerticalSpeed * options.dt, targetDelta)
      );
      verticalVelocity =
        options.dt > 0 ? verticalDisplacement / options.dt : 0;
    } else {
      verticalVelocity = 0;
      verticalDisplacement = 0;
    }

    jumpHoldRemainingMs = 0;
  } else if (jumpTriggered) {
    verticalVelocity = options.movement.jump.speed;
    verticalDisplacement = verticalVelocity * options.dt;
    jumpHoldRemainingMs = options.movement.jump.variableHeight
      ? options.movement.jump.maxHoldMs
      : 0;
  } else if (currentlyGrounded) {
    verticalVelocity = 0;
    verticalDisplacement = 0;
    jumpHoldRemainingMs = 0;
  } else {
    if (
      options.movement.jump.variableHeight &&
      !jumpPressed &&
      jumpHoldRemainingMs > 0 &&
      verticalVelocity > VERTICAL_ASCENT_EPSILON
    ) {
      verticalVelocity *= VARIABLE_JUMP_RELEASE_VELOCITY_FACTOR;
      jumpHoldRemainingMs = 0;
    }

    const variableJumpActive =
      options.movement.jump.variableHeight &&
      jumpPressed &&
      jumpHoldRemainingMs > 0 &&
      verticalVelocity > VERTICAL_ASCENT_EPSILON;
    const gravityScale = variableJumpActive
      ? VARIABLE_JUMP_HOLD_GRAVITY_FACTOR
      : 1;

    verticalVelocity -= GRAVITY * gravityScale * options.dt;
    verticalDisplacement = verticalVelocity * options.dt;
  }

  const resolvedMotion = options.resolveMotion(
    options.feetPosition,
    {
      x: groundedPlanarMotion.x,
      y: verticalDisplacement + groundedPlanarMotion.y,
      z: groundedPlanarMotion.z
    },
    activeShape
  );

  if (resolvedMotion === null) {
    return null;
  }

  const nextVolumeState = options.resolveVolumeState(resolvedMotion.feetPosition);
  const groundProbe = readGroundProbe(
    resolvedMotion.feetPosition,
    activeShape,
    options.probeGround
  );
  const nextShallowWater = isShallowWater({
    inWater: nextVolumeState.inWater,
    waterSurfaceHeight: nextVolumeState.waterSurfaceHeight,
    feetPosition: resolvedMotion.feetPosition,
    shape: activeShape,
    groundProbe
  });
  const nextSwimmableWater =
    nextVolumeState.inWater &&
    nextVolumeState.waterSurfaceHeight !== null &&
    !nextShallowWater &&
    !shouldPreserveAirborneWaterCrossing({
      inWater: nextVolumeState.inWater,
      waterSurfaceHeight: nextVolumeState.waterSurfaceHeight,
      feetPosition: resolvedMotion.feetPosition,
      shape: activeShape,
      verticalVelocity,
      previousLocomotionState: options.previousLocomotionState
    });
  const nextWaterSurfaceHeight =
    nextVolumeState.waterSurfaceHeight ??
    resolvedMotion.feetPosition.y + activeShape.eyeHeight;
  const headSubmerged =
    nextSwimmableWater &&
    resolvedMotion.feetPosition.y + activeShape.eyeHeight <
      nextWaterSurfaceHeight;
  const headBump =
    verticalDisplacement > 0 &&
    resolvedMotion.collidedAxes.y &&
    resolvedMotion.feetPosition.y <= options.feetPosition.y + 1e-4;
  const ascending = verticalVelocity > VERTICAL_ASCENT_EPSILON && !headBump;
  const grounded =
    !jumpTriggered &&
    !ascending &&
    activeShape.mode !== "none" &&
    !nextSwimmableWater &&
    groundProbe.grounded;

  if (activeShape.mode === "none" || grounded || headBump) {
    verticalVelocity = 0;
    jumpHoldRemainingMs = 0;
  }

  const locomotionMode = resolveLocomotionMode(
    activeShape,
    nextSwimmableWater,
    grounded,
    headSubmerged
  );
  const actualPlanarSpeed =
    options.dt > 0
      ? Math.hypot(
          resolvedMotion.feetPosition.x - options.feetPosition.x,
          resolvedMotion.feetPosition.z - options.feetPosition.z
        ) / options.dt
      : 0;

  return {
    feetPosition: resolvedMotion.feetPosition,
    activeShape,
    verticalVelocity,
    jumpBufferRemainingMs,
    coyoteTimeRemainingMs,
    jumpHoldRemainingMs,
    crouched,
    jumpPressed,
    jumpStarted: jumpTriggered,
    headBump,
    locomotionState: {
      locomotionMode,
      airborneKind: resolveAirborneKind(locomotionMode, verticalVelocity),
      gait: resolveGait(crouched, sprinting, actualPlanarSpeed),
      grounded,
      crouched,
      sprinting,
      inputMagnitude: planarMotion.inputMagnitude,
      requestedPlanarSpeed: preserveAirborneMomentum
        ? previousPlanarSpeed
        : (directionalAirControlActive
            ? previousPlanarSpeed
            : requestedPlanarSpeed) * planarMotion.inputMagnitude,
      planarSpeed: actualPlanarSpeed,
      verticalVelocity,
      contact: resolveContactState(resolvedMotion, groundProbe, grounded)
    },
    inWaterVolume: nextSwimmableWater,
    inFogVolume: nextVolumeState.inFog,
    planarDisplacement: {
      x: resolvedMotion.feetPosition.x - options.feetPosition.x,
      y: 0,
      z: resolvedMotion.feetPosition.z - options.feetPosition.z
    }
  };
}
