import type { AxisBinding, AxisInput } from "../input/axisInput.js"; import type { ChaseCameraTuning } from "../runtime/cameraDirector.js"; import { type GripCurve } from "./vehicleBody.js"; export interface KinematicVehicleTuning { engineAccel: number; brakeAccel: number; topSpeed: number; reverseSpeed: number; turnRate: number; turnSpeedRef: number; grip?: GripCurve; gripStrength: number; handbrakeGrip: number; /** Forward-speed decay per second when coasting (sleds, boats); default `0`. */ rollingResistance?: number; /** * Constant coast deceleration in world units/s², applied when the throttle is lifted and the brake * is not driving reverse. Unlike `rollingResistance` (a proportional decay, so a fast car sheds * speed quickly and a slow one crawls forever), this is the flat road-load feel karts and arcade * racers want; it is capped at `|speed| / dt` so coasting can never push the vehicle backwards. * The `chassis` block derives this from mass and gearing instead — set one or the other, not both. * Default `0` (coast forever). */ coastDeceleration?: number; /** * Reverse drive force as a fraction of `engineAccel`, for the arcade path. Reverse is intentionally * softer than forward; raise it for karts that need a brisk backup, lower it for heavy vehicles. * Default {@link DEFAULT_REVERSE_FORCE_SCALE}. With a `chassis` block, `chassis.reverseForceScale` * takes precedence. */ reverseForceScale?: number; /** * Downward acceleration in world units/s² applied to the vehicle's hop/air state, enabling * {@link KinematicVehicle.hop}. Omit to disable vertical motion entirely, which keeps every * existing ground vehicle on the flat path with no air state to integrate. */ hopGravity?: number; /** Optional higher-fidelity powertrain. Omit to preserve the direct arcade acceleration model. */ powertrain?: KinematicPowertrainTuning; /** * Optional bicycle-model steering with speed-sensitive lock and smoothed rack response. * @deprecated Heading here is still commanded from steer, so it can't produce real understeer or oversteer. Use `createVehicleDynamics` (`physics/vehicleDynamics`) when handling matters. */ steering?: KinematicSteeringTuning; /** Optional aerodynamic drag/downforce and electronic driver assists. */ dynamics?: KinematicDynamicsTuning; /** * Optional load-transfer handling: the tires' combined grip falls as weight moves off a corner, so a * car on the brakes into a corner holds less slide than the same car coasting, and a hard corner * itself costs grip. Bounded by the geometry (never diverges) and reported per tick as * {@link KinematicVehicleStep.weightTransfer}. Omit to keep the flat-grip model. */ weightTransfer?: KinematicWeightTransferTuning; /** * Optional mass-and-force chassis layer (#1051). When present it supersedes `engineAccel`/`brakeAccel` * with force/mass dynamics — a tire friction budget, lateral weight transfer, and engine braking — so * vehicles of different mass, drive/brake force, and center-of-gravity drive measurably differently. * Omit to preserve the direct arcade acceleration model exactly. * @deprecated The friction budget clamps lateral speed while heading keeps turning at the commanded rate, so a held steer at speed spins the car. Use `createVehicleDynamics` (`physics/vehicleDynamics`), whose yaw comes from tire forces. */ chassis?: KinematicChassisTuning; /** Per-axis bindings for this vehicle — which actions drive throttle/brake/steer/handbrake. Unlisted axes keep car defaults (W/S/A/D/Space). */ bindings?: Partial>; /** Camera POV while this vehicle is piloted. Nothing applies it automatically: pass it to `ctx.camera.setChaseTuning` when the driver enters and `null` when they exit. */ camera?: ChaseCameraTuning | null; } /** * Mass-and-force chassis layer (#1051); when present it supersedes `engineAccel`/`brakeAccel` with * force/mass dynamics. Drive/brake become forces divided by `massKg`, a per-tick tire friction budget * (`tireGrip` * grip curve * surface * downforce * m * g) is split lateral-first then longitudinal so * hard slides and launches saturate, and `comHeight`/`trackWidth` set weight-transfer washout and body * lean. Coasting decelerates from physical road load, not a per-vehicle constant: rolling resistance * `μ_rr · m · g` always, plus engine braking `ENGINE_BRAKE_FRACTION · engineForce` routed through the * current gear (so it strengthens as the car downshifts) whenever the throttle is lifted and a * `powertrain` is configured. All fields are required; omit the whole block for the legacy model. */ export interface KinematicChassisTuning { /** Total vehicle mass, kg (1200 compact … 11000 bus); divides every drive/brake force into accel. */ massKg: number; /** Peak drive force at the wheels, N — scaled by the powertrain torque curve and throttle. Also sets engine-braking magnitude. */ engineForce: number; /** Peak service-brake force, N; still modulated by the existing ABS logic. */ brakeForce: number; /** Peak tire friction coefficient μ on an ideal surface (~0.9 street, 1.1 sport, 0.7 bus). */ tireGrip: number; /** Center-of-mass height, m (0.45 sports car … 1.4 bus); drives weight transfer and body lean. */ comHeight: number; /** Lateral wheel spacing (track width), m; larger resists lateral weight transfer. */ trackWidth: number; /** * Reverse drive force as a fraction of `engineForce` (default {@link DEFAULT_REVERSE_FORCE_SCALE}). * Real gearboxes and games both keep reverse much softer than forward launch. */ reverseForceScale?: number; } /** Data-first gearbox and torque-curve tuning for a kinematic ground vehicle. */ export interface KinematicPowertrainTuning { idleRpm: number; redlineRpm: number; shiftUpRpm: number; shiftDownRpm: number; shiftSeconds: number; finalDrive: number; wheelRadius: number; gears: readonly number[]; torqueCurve: GripCurve; } /** Bicycle-model steering settings; all angles are radians. */ export interface KinematicSteeringTuning { wheelbase: number; maxAngle: number; highSpeedAngle: number; highSpeedAt: number; response: number; yawDamping?: number; } /** Aerodynamic and electronic-assist settings layered over tire grip. */ export interface KinematicDynamicsTuning { aerodynamicDrag?: number; downforce?: number; tractionControl?: number; abs?: number; stabilityControl?: number; bodyPitchFactor?: number; bodyRollFactor?: number; maxBodyPitch?: number; maxBodyRoll?: number; attitudeResponse?: number; } /** * Load-transfer handling for the arcade model. Weight moves under braking, traction and cornering; the * tire that goes light loses more grip than the loaded one gains (load sensitivity), so the axle's * combined grip falls. Every term is bounded by the geometry, and `sensitivity` caps the loss, so the * transfer can never drive grip negative or diverge. For per-axle balance from tire forces, use * `createVehicleDynamics` (`physics/vehicleDynamics`) instead. */ export interface KinematicWeightTransferTuning { /** Centre-of-mass height, m. */ comHeight: number; /** Wheelbase, m; sets longitudinal transfer under braking and traction. */ wheelbase: number; /** Track width, m; sets lateral transfer under cornering. */ trackWidth: number; /** Grip lost per unit of total load transfer (default `0.35`); the tire load sensitivity. */ sensitivity?: number; } export interface KinematicVehicleOptions { position?: readonly [number, number, number]; heading?: number; /** Grip multiplier by world position — off-track gravel, ice patches; default `1` everywhere. */ surfaceFriction?: (x: number, z: number) => number; /** Extra velocity damping per second by world position (rough ground drag); default `0`. */ dragAt?: (x: number, z: number) => number; /** * Veto or clamp a planar move before it commits — walls, closed gates, arena bounds. Given the * attempted `from`→`to` on the XZ plane, return the destination actually allowed; velocity is * rederived from the permitted displacement, so motion blocked on an axis stops on that axis. * A result may carry `motion`, the permitted XZ displacement in world units before position * recovery. Use it when the endpoint also depenetrates the body: recovery must not add velocity. * Plain tuple results retain displacement-based velocity. Called once per tick; no purity required. * Default: no clamp (the move always commits). */ clampMove?: (from: readonly [number, number], to: readonly [number, number]) => readonly [number, number] & { readonly motion?: readonly [number, number]; }; } /** * Per-tick multipliers layered over the base tuning — the transient overrides games apply for one * frame without rebuilding the vehicle: nitro/boost, a braced-plow bonus, or entering a speed zone or * slow field. Each defaults to `1` (no change), so passing nothing leaves the base tuning untouched. */ export interface KinematicVehicleModifiers { /** Scale top speed this tick (sustained boost, speed pads). */ topSpeedScale?: number; /** Scale engine acceleration this tick (boost, brace bonus). */ accelScale?: number; /** Scale turn rate this tick (brace penalty, heavy-load steering). */ turnRateScale?: number; /** * Scale braking force this tick (worn pads, ice, a brace bonus that stiffens the whole drivetrain). * Separate from `accelScale` because stronger drive and stronger brakes are not the same upgrade. */ brakeScale?: number; /** * Scale the tire grip curve this tick (damage, a low-grip zone, a temporary debuff). Mirrors the * `gripScale` on `VehicleDynamicsModifiers`, so a damage layer's per-tick output drives either sim. */ gripScale?: number; } export interface KinematicVehicleStep { position: readonly [number, number, number]; heading: number; /** Signed speed along the heading, world units/s. */ forwardSpeed: number; lateralSpeed: number; /** Lateral/forward slip ratio driving the grip curve — drift indicators key off it. */ slip: number; /** The `surfaceFriction` sample used this tick (`< 1` = off the ideal surface). */ surface: number; /** * Total load transfer this tick, `0..1`: longitudinal (braking/traction) plus lateral (cornering) * weight movement as a fraction of the vehicle's weight. Always `0` without a `weightTransfer` block. */ weightTransfer: number; /** The grip multiplier the load transfer applied this tick (`1` = no loss); always `1` without the block. */ loadGrip: number; /** One-based forward gear, or `0` while reversing/neutral. */ gear: number; rpm: number; steerAngle: number; yawRate: number; longitudinalAcceleration: number; tractionLimited: boolean; absActive: boolean; /** * Demanded drive force exceeded the tire friction budget left after cornering this tick — a * traction-limited launch (#1051). Always `false` without a `chassis` block; distinct from * `tractionLimited`, which stays owned by the traction-control assist. */ wheelspin: boolean; bodyPitch: number; bodyRoll: number; /** * Height above the vehicle's ground plane from an in-progress hop, world units; `0` while grounded * and always `0` without `hopGravity`. `tickDrivableVehicle` adds this on top of the sampled ground * height, so a hopping vehicle still follows terrain underneath it. */ airOffset: number; /** `true` while `airOffset > 0` — gate landing effects, air control, and trick scoring off this. */ airborne: boolean; } /** Serializable integrator state of a {@link KinematicVehicle}: what `restore` needs to resume bit-for-bit. */ export interface KinematicVehicleState { x: number; y: number; z: number; heading: number; vx: number; vz: number; gearIndex: number; rpm: number; shiftRemaining: number; steerAngle: number; yawRate: number; previousForwardSpeed: number; bodyPitch: number; bodyRoll: number; airOffset: number; verticalVelocity: number; } /** * The pure-kinematic arcade car every racing game hand-rolled (#282.1): steer-yaw scaled by speed, * throttle/brake acceleration, and a grip-curve lateral-slip bleed — no `PhysicsWorld`, no wheels, * just the drift-friendly integration the three shipped racers proved out. Games keep their flavor * (drift meters, boost, off-track rules) via `surfaceFriction`/`dragAt` hooks and the returned slip. */ export interface KinematicVehicle { tick(dt: number, axis: AxisInput, modifiers?: KinematicVehicleModifiers): KinematicVehicleStep; pose(): { position: readonly [number, number, number]; heading: number; }; velocity(): readonly [number, number]; /** Scale the current velocity (boost pads, hard collisions). */ scaleVelocity(factor: number): void; /** * Launch the vehicle upward with the given vertical impulse (world units/s). Returns `false` — and * changes nothing — when the vehicle is already airborne or the tuning has no `hopGravity`, so a * held jump key cannot ratchet a vehicle into orbit. */ hop(impulse: number): boolean; /** * Replace the tuning in place, keeping position, heading, velocity and gear state. This is the * seam for vehicles whose stats change during play — an installed part, a damaged engine, a * temporary debuff — which must NOT cost the vehicle its momentum the way rebuilding the sim * would. For a change that lasts a single tick, pass {@link KinematicVehicleModifiers} instead. */ retune(next: KinematicVehicleTuning): void; snapshot(): KinematicVehicleState; restore(state: KinematicVehicleState): void; resetTo(position: readonly [number, number, number], heading: number): void; } /** * Arcade ground-vehicle sim (car, kart, bike) with throttle, brake, steer, drift and retunable handling. * * @capability kinematic-vehicle arcade car/kart driving physics with throttle, brake, steer and retunable handling */ export declare function createKinematicVehicle(initialTuning: KinematicVehicleTuning, options?: KinematicVehicleOptions): KinematicVehicle; /** Default reverse drive force as a fraction of forward `engineForce` / `engineAccel`. */ export declare const DEFAULT_REVERSE_FORCE_SCALE = 0.48;