import type { AxisInput } from "../input/axisInput.js"; import type { KinematicVehicleOptions } from "./kinematicVehicle.js"; import type { GripCurve } from "./vehicleBody.js"; /** * One axle's tire, in physical terms. Lateral force follows a simplified Pacejka curve * `μ·Fz·sin(C·atan(B·α))` whose `B`/`C` are solved from `peakSlipAngle` and `slideGrip`, so the three * numbers read the way a driver feels them: how much grip, how early it peaks, how much is left sliding. */ export interface VehicleTireTuning { /** Peak friction coefficient μ on an ideal surface (0.7 worn street … 1.1 sport … 1.6 slick/arcade). */ peakGrip: number; /** Slip angle in radians where lateral force peaks (0.07 stiff race tire … 0.2 soft, forgiving tire). */ peakSlipAngle: number; /** Fraction of peak force left at a full 90° slide, `0..1` (0.6 snappy … 0.95 drift-friendly). */ slideGrip: number; } /** Engine and gearbox. Drive force at the wheels = torque · curve(rpm) · gear · finalDrive · efficiency / wheelRadius. */ export interface VehicleGearboxTuning { kind: "gearbox"; /** Peak engine torque, N·m. */ peakTorque: number; /** Torque multiplier by normalized rpm (`0` = idle, `1` = redline); same shape as a grip curve. */ torqueCurve: GripCurve; idleRpm: number; redlineRpm: number; shiftUpRpm: number; shiftDownRpm: number; /** Seconds of reduced drive during a shift. */ shiftSeconds: number; gears: readonly number[]; reverseGear: number; finalDrive: number; /** Driven wheel radius, m. */ wheelRadius: number; /** Driveline efficiency `0..1` (default `0.85`). */ efficiency?: number; /** Engine-braking torque at the crank with the throttle closed, N·m (default `0.12 · peakTorque`). */ engineBrakeTorque?: number; /** Clutch-slip rpm the engine holds while launching from rest (default halfway between idle and peak). */ launchRpm?: number; } /** Single-speed drive (electric, kart, arcade ball-car): force capped by power, so top speed comes out of drag. */ export interface VehicleDirectDriveTuning { kind: "direct"; /** Maximum tractive force at the wheels, N. */ maxForce: number; /** Maximum power, W; force falls off as `power / speed` above the crossover. */ maxPower: number; /** Reverse force as a fraction of `maxForce` (default `0.5`). */ reverseScale?: number; /** Throttle-off drag force at the wheels, N (default `0.08 · maxForce`). */ coastForce?: number; } /** Steering rack: lock, speed-sensitive lock, rack speed, and caster self-alignment. */ export interface VehicleSteeringTuning { /** Front-wheel lock at parking speed, rad. */ maxAngle: number; /** Lock at and above `highSpeedAt`, rad; lower = calmer at speed. */ highSpeedAngle: number; /** Speed where lock reaches `highSpeedAngle`, m/s. */ highSpeedAt: number; /** Rack speed toward the requested angle, rad/s — a keyboard tap and a stick flick both pass through this. */ rate: number; /** Rack speed back to centre when the request is released or reversed, rad/s (default `rate`). */ returnRate?: number; /** * Caster self-alignment `0..1`: the fraction of the car's sideslip the front wheels steer into on their * own. `0` leaves every catch to the driver (sim), `~0.3` is a real street car, `0.8+` catches slides for * the player (arcade). */ selfAlign?: number; } /** Electronic and arcade assists; each is a strength `0..1`, `0`/omitted = off. */ export interface VehicleAssistTuning { /** Anti-lock: caps brake force below the tire's remaining grip so the front keeps steering. */ abs?: number; /** Traction control: caps drive force below the driven axle's remaining grip. */ tractionControl?: number; /** * Stability (yaw) control: a corrective yaw torque once sideslip exceeds `maxSideslip`. Real ESC is subtle * (`~0.3`); arcade street cars that can't spin out sit near `1`. */ stability?: number; /** Sideslip in rad that `stability` allows before it intervenes (default `0.2`, ≈11°). */ maxSideslip?: number; /** Anti-wheelie: caps drive below the acceleration that lifts the front (`g·b/h`); reports as `tractionLimited`. */ antiWheelie?: number; /** Anti-stoppie: caps braking below the deceleration that lifts the rear (`g·a/h`); reports as `absActive`. */ antiStoppie?: number; } /** Aero and resistance. Uses air density 1.225 kg/m³. */ export interface VehicleAeroTuning { /** Drag coefficient × frontal area, m² (0.6 sports car … 1.0 SUV). */ dragArea?: number; /** Lift coefficient × area producing downforce, m² (0 road car … 3 race car). */ downforceArea?: number; /** Share of downforce on the front axle, `0..1` (default `0.45`). */ downforceFront?: number; /** Rolling-resistance coefficient (default `0.015`). */ rollingResistance?: number; } /** * Four sprung corners. With this block the chassis heaves, pitches and rolls on real springs, axle loads come * from spring forces instead of a filtered estimate, slopes pull the car downhill, and the car can leave * the ground and land. Omit it for the flat-ground model. */ export interface VehicleSuspensionTuning { /** Spring rate per wheel, N/m (20 000 soft road car … 60 000 race car; monster trucks go lower with more travel). */ springRate: number; /** Damper rate per wheel in compression, N·s/m (roughly 2·√(k·m/4)·0.3 for a comfortable car, higher for a racer). */ damperRate: number; /** Damper rate per wheel in extension, N·s/m (default `1.5 · damperRate`); higher keeps a landing from bouncing. */ reboundRate?: number; /** Compression available before the bump stop, m (0.08 race car … 0.6 monster truck). */ travel: number; /** Centre-of-mass height above level ground at static rest, m. */ rideHeight: number; /** Total anti-roll bar stiffness, N·m/rad, split between axles by `rollStiffnessFront` (default `0`). */ antiRoll?: number; /** Pitch inertia as a multiple of `m·a·b` (default `0.9`). */ pitchInertiaIndex?: number; } /** Control of the body while airborne (needs `suspension`, the only way to leave the ground). */ export interface VehicleAirTuning { /** Angular acceleration at full input, rad/s², per axis. */ pitchAccel: number; yawAccel: number; rollAccel: number; /** Angular damping while airborne, 1/s (default `1.5`); higher stops rotation when input is released. */ damping?: number; /** Largest angular rate input can build, rad/s (default `5.5`). */ maxRate?: number; } /** * Single-track lean for motorcycles, bicycles and scooters. Steer input asks for a lean angle; the front wheel * then takes whatever angle a balanced turn at that lean needs (curvature = g·tan(lean) / v²), with a brief * countersteer as the lean starts. Tire grip still limits the turn, so over-leaning on a low-grip surface slides. */ export interface VehicleLeanTuning { /** Largest lean the rider asks for at full steer, rad (0.7 ≈ 40° road bike … 0.95 ≈ 55° race bike). */ maxLean: number; /** How fast the bike can change lean, rad/s (2 heavy tourer … 5 light sport bike). */ leanRate: number; /** Seconds of opposite bar per rad/s of lean change — the countersteer that starts a lean (default `0.012`, about 3° of bar on a quick sport bike). */ countersteer?: number; /** Below this speed the bar steers directly, like walking a bike (default `3` m/s). */ directSteerBelow?: number; } /** Jumps from the ground and in the air. */ export interface VehicleJumpTuning { /** Upward velocity a jump adds, m/s. */ speed: number; /** Jumps allowed per flight, counting the first from the ground (default `1`; `2` = double jump). */ count?: number; /** Seconds after leaving the ground during which later jumps are allowed (default `Infinity`). */ window?: number; } /** * Tuning for {@link createVehicleDynamics}. Every number is a physical quantity in SI units (kg, m, N, * N·m, rad, s), so a feel target maps to a knob a person can reason about: more rear grip or a lower * centre of mass for stability, softer tires for forgiveness, more drive to the rear for power oversteer. */ export interface VehicleDynamicsTuning { /** Total mass, kg. */ massKg: number; /** Axle-to-axle distance, m. */ wheelbase: number; /** Share of static weight on the front axle, `0..1` (0.6 front-engined FWD … 0.45 mid-engine). */ frontWeight: number; /** Centre-of-mass height, m; drives longitudinal and lateral load transfer and body attitude. */ comHeight: number; /** Track width, m. */ trackWidth: number; /** Yaw inertia as a dynamic index `Iz / (m·a·b)` (default `1`; `<1` rotates eagerly, `>1` lazily). */ yawInertiaIndex?: number; /** * Share of lateral load transfer taken by the front axle, `0..1` (default `0.5`). The classic balance knob: * more front → the front saturates first (understeer), less → the rear does (oversteer). */ rollStiffnessFront?: number; /** Tire load sensitivity `0..0.5`: how much an axle's grip drops as load moves across it (default `0.2`). */ loadSensitivity?: number; front: VehicleTireTuning; /** Rear tire; defaults to `front`. */ rear?: VehicleTireTuning; /** Share of drive force to the front axle, `0..1` (`0` RWD, `1` FWD, `0.35` rear-biased AWD). */ driveFront: number; powertrain: VehicleGearboxTuning | VehicleDirectDriveTuning; /** Peak service-brake force across all wheels, N. */ brakeForce: number; /** Share of brake force on the front axle, `0..1` (default `0.65`). */ brakeFront?: number; /** Rear lateral grip multiplier while the handbrake is fully on (default `0.35`). */ handbrakeGrip?: number; steering: VehicleSteeringTuning; assists?: VehicleAssistTuning; aero?: VehicleAeroTuning; /** * Drive governor, m/s: drive force fades out over the last 0.5 m/s below it (arcade caps, electronic * limiters). Thrust, slopes and impulses can still carry the car past it. Omit to let power and drag decide. */ speedLimit?: number; /** Reverse speed cap, m/s (default `8`). */ reverseSpeed?: number; /** Body roll stiffness, N·m/rad, for the visual lean (default derived for ~3° at 1 g). */ rollStiffness?: number; /** Body pitch stiffness, N·m/rad, for dive and squat (default derived for ~2° at 1 g). */ pitchStiffness?: number; /** Suspension settle rate for load transfer and body attitude, 1/s (default `8`). */ suspensionResponse?: number; /** Largest integration substep, s (default `1/240`); ticks are split so the result does not depend on frame rate. */ maxSubstep?: number; /** Brake held near standstill drives in reverse (default `true`). Set `false` for a separate reverse action. */ reverseOnBrake?: boolean; /** Sprung corners, slopes and air; omit for the flat-ground model. */ suspension?: VehicleSuspensionTuning; /** Air control; needs `suspension`. Input comes from `modifiers.air`, else throttle−brake pitches and steer yaws. */ air?: VehicleAirTuning; /** Enables {@link VehicleDynamics.jump}; needs `suspension`. */ jump?: VehicleJumpTuning; /** Single-track lean (motorcycle); omit for a four-wheeled car. `trackWidth` then only sets the render width. */ lean?: VehicleLeanTuning; } /** Air-control input, each `-1..1`: `pitch > 0` noses down, `yaw > 0` turns like `steer > 0`, `roll > 0` drops the left side. */ export interface VehicleAirInput { pitch: number; yaw: number; roll: number; } /** Per-tick overrides layered over tuning: boost, grip zones, damage. Each scale defaults to `1`. */ export interface VehicleDynamicsModifiers { driveScale?: number; brakeScale?: number; gripScale?: number; steerScale?: number; /** Thrust along the heading that bypasses the tires, N — boost, a rocket, a tow. */ thrust?: number; /** Air-control input this tick; omitted, throttle−brake pitches and steer yaws while airborne. */ air?: VehicleAirInput; } /** World hooks for one vehicle instance. */ export interface VehicleDynamicsOptions { position?: readonly [number, number, number]; heading?: number; /** Grip multiplier by world position (gravel, ice, wet paint); default `1`. */ surfaceFriction?: (x: number, z: number) => number; /** Clamp a planar move; optional result `motion` separates permitted displacement from position recovery. Called once per physics substep. */ clampMove?: KinematicVehicleOptions["clampMove"]; /** Terrain height under a point; read per wheel when `suspension` is set (ramps, hills, ledges). Default flat at the spawn height. */ groundHeight?: (x: number, z: number) => number; } /** Serializable integrator state: everything {@link VehicleDynamics.restore} needs to resume bit-for-bit. */ export interface VehicleDynamicsState { x: number; y: number; z: number; heading: number; vx: number; vz: number; yawRate: number; steerAngle: number; gearIndex: number; reversing: boolean; shiftRemaining: number; rpm: number; longitudinalAccel: number; lateralAccel: number; bodyPitch: number; bodyRoll: number; /** Vertical velocity of the chassis, m/s (suspension only). */ vy: number; pitchRate: number; rollRate: number; /** Per-corner spring compression, m, in order front-left, front-right, rear-left, rear-right. */ compression: [number, number, number, number]; airborne: boolean; /** The lean the rider is asking for, rad, positive right; it sets the turn the bar steers for. */ lean: number; /** The rider's bar correction on top of the balanced-turn angle, rad. */ leanTrim: number; /** Jumps used since last on the ground. */ jumpsUsed: number; /** Seconds since leaving the ground (`0` while grounded). */ airTime: number; } /** Result of one {@link VehicleDynamics.tick}: pose plus the telemetry camera, audio, haptics and HUD read. */ export interface VehicleDynamicsStep { position: readonly [number, number, number]; heading: number; /** Signed speed along the heading, m/s. */ forwardSpeed: number; /** Lateral speed, m/s (same sign convention as `KinematicVehicleStep.lateralSpeed`). */ lateralSpeed: number; yawRate: number; /** Front-wheel angle, rad; positive steers the heading down, like `AxisInput.steer`. */ steerAngle: number; /** Body sideslip β, rad: angle between heading and velocity. */ sideslip: number; slipAngleFront: number; slipAngleRear: number; /** Share of each axle's friction budget in use, `0..1+`; `≥1` means that axle is sliding. Squeal, rumble and HUD key off it. */ frontSaturation: number; rearSaturation: number; /** Lateral acceleration, m/s²; positive toward the car's right, the way `steer > 0` turns. */ lateralAccel: number; longitudinalAccel: number; frontLoad: number; rearLoad: number; /** Surface multiplier sampled this tick. */ surface: number; /** One-based forward gear; `-1` in reverse. */ gear: number; rpm: number; /** Throttle `0..1` actually applied after traction control and the rev limiter — engine audio load. */ engineLoad: number; wheelspin: boolean; wheelLock: boolean; absActive: boolean; tractionLimited: boolean; stabilityActive: boolean; bodyPitch: number; bodyRoll: number; /** Height of the car's base above the terrain under it, m; `0` on the ground and always `0` without `suspension`. */ airOffset: number; /** No wheel touches the ground. */ airborne: boolean; /** Vertical speed at the moment of touchdown this tick, m/s (`0` otherwise) — landing shake, thud, rumble. */ landingSpeed: number; /** Per-corner spring load, N (front-left, front-right, rear-left, rear-right); all `0` without `suspension`. */ wheelLoads: readonly [number, number, number, number]; /** Balance lean the bike actually shows, `atan(lateral g)`, rad, positive leaning right; `0` without a `lean` block. */ lean: number; /** Acceleration has taken all the load off the front axle. */ wheelie: boolean; /** Braking has taken all the load off the rear axle. */ stoppie: boolean; } /** * Force-based planar car: a two-axle ("bicycle") model where yaw comes from tire forces rather than being * commanded, so understeer, oversteer, lift-off rotation, trail-braking, power slides and handbrake turns * emerge from the numbers instead of from special cases. Deterministic for a given `dt` sequence; ticks are * split into fixed substeps. Pairs with `tickDrivableVehicle` for the entity pose and `measureHandling` * for asserting feel. */ export interface VehicleDynamics { tick(dt: number, input: AxisInput, modifiers?: VehicleDynamicsModifiers): VehicleDynamicsStep; pose(): { position: readonly [number, number, number]; heading: number; }; /** World velocity on the ground plane, `[vx, vz]`, m/s. */ velocity(): readonly [number, number]; /** Scale planar velocity and yaw rate (crash, boost pad). */ scaleVelocity(factor: number): void; /** Add a world-space velocity change, m/s (impacts, knockback, a jump pad's `dvy` with `suspension`). */ applyImpulse(dvx: number, dvz: number, dvy?: number): void; /** * Jump if the tuning's `jump` block allows it now: from the ground, or in the air while jumps remain inside * the window. Returns `false` and changes nothing otherwise, so a held key cannot stack jumps. */ jump(): boolean; /** Add angular velocity to the body, rad/s: `pitch > 0` noses down, `yaw` in heading sense, `roll > 0` drops the left side (dodges, flips, hits). */ applyAngularImpulse(pitch: number, yaw: number, roll: number): void; /** Swap tuning in place (upgrades, damage, a live tuning panel) without losing momentum. */ retune(next: VehicleDynamicsTuning): void; tuning(): VehicleDynamicsTuning; snapshot(): VehicleDynamicsState; restore(state: VehicleDynamicsState): void; resetTo(position: readonly [number, number, number], heading: number): void; } /** * Creates a {@link VehicleDynamics}. * @capability vehicle-dynamics force-based car handling — tires, load transfer, drivetrain, assists — tuned with physical numbers */ export declare function createVehicleDynamics(initialTuning: VehicleDynamicsTuning, options?: VehicleDynamicsOptions): VehicleDynamics;