🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (32 total matches found)
fieldguide.vehiclephysics / Code/Assembly.cs
Game library
// Global usings for the Vehicle Physics Kit assembly only. These do NOT export the kit's
// FieldGuide.VehiclePhysics namespace anywhere — a consumer references the kit types through
// their real namespace, never through a leaked global using.
global using Sandbox;
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
fieldguide.vehiclephysics / Code/CarDefinition.cs
Game library
namespace FieldGuide.VehiclePhysics;

public enum DriveLayout
{
	FWD,
	RWD,
	AWD
}

public enum BodyStyle
{
	Box,  // sedan/hatch blockout: body + cabin
	Kart  // flat deck + seat back + citizen driver
}

public enum AssistLevel
{
	Casual, // full traction control + yaw-stability damping + ABS
	Sport,  // ABS + optional REDUCED-authority TC / yaw-stability (per-car opt-in; drift feel preserved)
	Sim     // nothing
}

/// <summary>
/// Complete tuning spec for one car. Plain class, instantiated from <see cref="CarDefinitions"/>;
/// promote to a [GameResource] .car asset once an editor workflow is in play so designers can tune
/// without recompiling. All values SI: meters, kg, N, N-m, N/m.
/// </summary>
public class CarDefinition
{
	public string Name { get; set; } = "Car";
	public BodyStyle Style { get; set; } = BodyStyle.Box;
	public bool HasDriver { get; set; }

	/// <summary>Optional opaque body-manifest path for a consumer-supplied custom body builder
	/// (see <see cref="VehicleFactory.CustomBodyBuilder"/>). The kit itself never reads it — with no
	/// custom builder plugged in, the factory builds a primitive blockout body and this value is
	/// inert. It exists so a consumer's builder (e.g. a part-kit assembler) can look up which body to
	/// assemble for this car. null keeps the blockout path byte-for-byte unchanged.</summary>
	public string BodyManifest { get; set; }

	// Chassis
	public float Mass { get; set; } = 1200f;
	public Vector3 BodySize { get; set; } = new( 4.0f, 1.8f, 1.3f ); // length, width, height (m)
	public float Wheelbase { get; set; } = 2.55f;
	public float TrackWidth { get; set; } = 1.55f;
	public float RideHeight { get; set; } = 0.35f; // chassis center above wheel attach plane
	public float GroundClearance { get; set; } = 0.14f; // collider bottom above ground at rest
	public float CenterOfMassDrop { get; set; } = 0.20f; // CoM below chassis center; must stay above wheel plane

	// Wheels & suspension (per wheel)
	public float WheelRadius { get; set; } = 0.31f;
	public float WheelInertia { get; set; } = 1.2f; // kg m^2
	public float SuspensionTravel { get; set; } = 0.20f;
	public float SpringRate { get; set; } = 38000f;
	public float DamperRate { get; set; } = 2800f;

	// Tires
	public TireCurve LongitudinalCurve { get; set; } = TireCurve.Street;
	public TireCurve LateralCurve { get; set; } = new( 0.14f, 1.00f, 0.55f, 0.80f ); // radians: peak ~8 deg
	public float LoadSensitivity { get; set; } = 0.06f;

	// Drivetrain
	public DriveLayout Layout { get; set; } = DriveLayout.FWD;
	// +20% speed pass 2026-07-21: baseline PeakTorque 150->180 and RedlineRpm 6500->7800 so a bare
	// kit consumer starts from the new faster baseline (the demo roster below overrides both per car).
	public float PeakTorque { get; set; } = 180f;
	public float IdleRpm { get; set; } = 900f;
	public float RedlineRpm { get; set; } = 7800f;
	public float EngineInertia { get; set; } = 0.25f; // kg m^2 at crank
	public float EngineBrakeTorque { get; set; } = 40f;
	public float[] GearRatios { get; set; } = { 3.6f, 2.1f, 1.4f, 1.05f, 0.85f };
	public float ReverseRatio { get; set; } = 3.4f;
	public float FinalDrive { get; set; } = 3.9f;
	public float ShiftUpRpm { get; set; } = 5800f;
	public float ShiftDownRpm { get; set; } = 2200f;

	// Engine audio (read by EngineAudio). Per-class dials so a small buzzy kart and a deep
	// sedan/truck share one code path:
	//   EngineSoundEvent     — the LOW-RPM loop this class plays (idle/low, the deep layer).
	//   EngineSoundEventHigh — the HIGH-RPM loop, crossfaded in as revs climb. When null/empty the
	//     class runs the legacy SINGLE-loop model (one handle, wide pitch sweep) — the kart keeps
	//     this so its single synth purr is unchanged. When set, EngineAudio blends the two recorded
	//     layers by RPM, and each layer only pitch-shifts a narrow band around its recorded pitch
	//     (no far stretch = no screech), which is the point of the layered model.
	//   EnginePitchBase      — multiplies the pitch of whichever model runs, so a class reads higher
	//     or lower overall (kart >1 stays buzzy, truck <1 sits deep).
	public string EngineSoundEvent { get; set; } = "sounds/engine/engine_real_low.sound";
	public string EngineSoundEventHigh { get; set; }
	public float EnginePitchBase { get; set; } = 1f;

	// Brakes
	public float BrakeTorque { get; set; } = 2400f; // total, split by bias
	public float BrakeBias { get; set; } = 0.62f;   // fraction to front
	public float HandbrakeTorque { get; set; } = 3000f; // rear wheels only

	// ABS dials (per-car). 0.3/0.55 because telemetry showed the ABS duty cycle, not tire grip, was
	// the brake limiter, and per-car ABS modulation is a legitimate class trait: the locking kart
	// needs a different release than the truck. Active in Casual + Sport.
	public float AbsSlipThreshold { get; set; } = 0.25f; // release when SlipRatio < -this under braking
	public float AbsReleaseFactor { get; set; } = 0.70f; // brake torque multiplier while released

	// Drift-exit soft-lock: cap the handbrake-INDUCED rear slip ratio. When a cap is active and a rear
	// wheel is already sliding PAST it, its handbrake torque is withheld that substep (ABS-style duty
	// cycle) so the rear spins back up toward the cap and keeps some rotation — hence some lateral bite
	// — mid-slide. Default -1.0 = NO effective cap (byte-identical full-lock behavior).
	public float HandbrakeSlipCap { get; set; } = -1.0f;

	// Steering
	public float MaxSteerAngle { get; set; } = 32f; // degrees at standstill
	public float HighSpeedSteerAngle { get; set; } = 8f; // degrees at/above 30 m/s

	// Arcade feel dials (defaults = the original sim-leaning behavior)
	public float SteerRateScale { get; set; } = 1f;    // multiplies how fast steering ramps
	// Reverse speed pass 2026-07-21 (owner feel): raised 4.5 -> 20.0 m/s (~10 -> ~45 mph). This is the
	// ACTUAL reverse cap mechanism (VehicleController cuts reverse throttle above this speed), and the
	// game roster inherits it (only the pickup used to override it). Each car is still additionally
	// limited by its reverse-gear redline-equivalent wheel speed (RedlineRpm/ReverseRatio/FinalDrive/
	// WheelRadius via Drivetrain.RedlineWheelSpeed), so torquey-but-tall reverse gears self-limit below
	// this cap (kart ~8 m/s, pickup ~12 m/s) while the cars reach the high-30s/40s mph.
	public float ReverseSpeedCap { get; set; } = 20.0f; // m/s before reverse throttle cuts
	public float LaunchBoost { get; set; } = 1f;       // torque multiplier at standstill, fades out by ~54 km/h
	public float BrakeAssist { get; set; } = 0f;       // extra chassis-level decel while braking (m/s²)
	// Spin-recovery assist: after a handbrake spin the car keeps rolling BACKWARDS (old travel
	// direction) while the player holds throttle the new way — BrakeAssist can't help there (forward
	// throttle sets Brake=0), so nothing arcade-level arrests the stale velocity. This is extra
	// chassis-level decel along -velocity, applied ONLY when input throttle opposes the ground velocity
	// along the car's facing, fading out as the car rotates to face its motion
	// (VehicleController.ApplySpinRecoveryAssist). Same m/s² unit + never-reverse-within-a-step cap as
	// BrakeAssist; gated Assists != Sim. 0 disables.
	public float SpinRecoveryAssist { get; set; } = 0f; // extra chassis decel killing stale opposing velocity (m/s²)
	public float HandbrakeGripScale { get; set; } = 1f; // rear grip multiplier while handbrake held (<1 = drift button)

	// Wall-glance forgiveness assist: a sustained near-horizontal chassis contact while moving
	// re-projects velocity along the wall tangent and gently yaws the heading to match, scaled by
	// incidence (VehicleController.ApplyWallGlanceAssist). Active only when Assists != Sim. Head-on hits
	// (incidence >= WallGlanceHeadOnDeg) get NO assist — frontal crashes stay hard stops.
	public bool WallGlanceAssist { get; set; } = true;
	public float WallScrubFactor { get; set; } = 0.75f;    // fraction of speed kept along the wall tangent on a shallow graze
	public float WallGlanceShallowDeg { get; set; } = 35f; // at/below this velocity-vs-wall incidence: full assist
	public float WallGlanceHeadOnDeg { get; set; } = 60f;  // at/above this incidence: no assist (hard stop preserved)
	public float WallAlignStrength { get; set; } = 6f;     // yaw-align rate toward the wall tangent (per second)

	// ── Sport-mode stability posture (owner call 2026-07-21) ──
	// Sport historically ran with NO traction control and NO yaw-stability damping (both Casual-only),
	// so a full-throttle RWD car spun the rears to redline and the counter-steer pendulum went divergent
	// — uncatchable spin-outs. These give Sport a REDUCED-authority version of each assist, opt-in per
	// car. Both default to 0 (disabled), so any car that doesn't set them keeps the raw "ABS only" Sport
	// and — critically — Casual and Sim stay byte-identical (the controller multiplies Casual authority
	// by exactly 1 and returns early for Sim, unchanged).

	// Sport traction control. When >0, Sport runs the same proportional TC as Casual
	// (VehicleController.ApplyTractionControl) but holds driven-wheel slip near THIS ratio instead of the
	// Casual 0.14 grip-peak target. Set it LOOSER than the peak (e.g. 0.30-0.45) so the rears still break
	// into a throttle-steerable slide (drift stays alive) while the redline free-spin that torches rear
	// lateral grip is capped. 0 = no Sport TC (raw wheelspin, the old behavior).
	public float SportTcSlipTarget { get; set; } = 0f;

	// Sport yaw-stability. When >0, Sport runs the yaw-rate damper (VehicleController.ApplyStabilityAssist)
	// at THIS fraction of the Casual authority (0-1). It bleeds the stored yaw rate that snaps a
	// counter-steered slide the OTHER way (the pendulum spin-out) so slides stay CATCHABLE, without
	// killing deliberate rotation the way full Casual authority would. 0 = no Sport yaw damping.
	public float SportStabilityScale { get; set; } = 0f;

	// Driver seated pose (citizen animgraph; sit enum: 0 none, 1-3 chair poses, 4-5 ground poses).
	// Defaults = the original upright chair pose. Made per-car so a recumbent kart driver — legs
	// extended forward to the pedals — can be authored without disturbing any upright-seated car.
	public int DriverSit { get; set; } = 1;
	public float DriverSitOffsetHeight { get; set; } = 4f;

	// Defaults
	public AssistLevel DefaultAssists { get; set; } = AssistLevel.Casual;
	public Color Tint { get; set; } = new( 0.85f, 0.55f, 0.35f );
}

/// <summary>Car roster. Hatch is the default/first car; Kart and Coupe round out the roster.
/// These are blockout-body demo definitions — physics is identical whether a car renders as a
/// primitive blockout or a consumer-supplied custom body.</summary>
public static class CarDefinitions
{
	/// <summary>The default/first roster car — an ORANGE hot hatch. FWD, mid-power, street tires.</summary>
	public static CarDefinition Hatch => new()
	{
		Name = "Compact Hatch",
		Mass = 1150f,
		BodySize = new Vector3( 3.9f, 1.75f, 1.45f ),
		Wheelbase = 2.55f,
		TrackWidth = 1.50f,
		WheelRadius = 0.30f,
		Layout = DriveLayout.FWD,
		// +20% speed pass 2026-07-21: PeakTorque 162->194.4, RedlineRpm 6300->7560 (top-gear top speed +20%).
		PeakTorque = 194.4f,
		RedlineRpm = 7560f,
		// Real recorded car set: deep muscle idle crossfaded up to a revving high layer.
		EngineSoundEvent = "sounds/engine/engine_real_low.sound",
		EngineSoundEventHigh = "sounds/engine/engine_real_high.sound",
		LongitudinalCurve = new TireCurve( 0.10f, 1.35f, 0.45f, 1.08f ),
		BrakeTorque = 4300f,
		LateralCurve = new TireCurve( 0.14f, 1.30f, 0.55f, 1.04f ),
		HandbrakeGripScale = 0.55f, // the drift button — rear grip cut while handbrake held; also the FWD J-turn lever
		SpinRecoveryAssist = 7.0f,
		HighSpeedSteerAngle = 9.5f,
		SpringRate = 34000f,
		DamperRate = 2500f,
		Tint = new Color( 0.93f, 0.42f, 0.03f ), // orange
	};

	/// <summary>Full-size pickup. Heaviest in roster, RWD, torquey low-rev engine, longer-travel
	/// softer suspension, offroad-leaning tires, high ride. Signature strength = hill grade.</summary>
	public static CarDefinition Pickup => new()
	{
		Name = "Utility Pickup",
		Mass = 1900f,
		BodySize = new Vector3( 5.2f, 1.95f, 1.55f ),
		Wheelbase = 3.40f,
		TrackWidth = 1.70f,
		RideHeight = 0.44f,          // high ride: the class signature (hatch 0.35)
		GroundClearance = 0.24f,
		CenterOfMassDrop = 0.15f,    // higher CoM than the cars = truck-like roll; track 1.70 keeps rollover margin
		WheelRadius = 0.35f,
		WheelInertia = 2.4f,
		SuspensionTravel = 0.26f,    // longest in roster — washboard/offroad strength
		SpringRate = 42000f,
		DamperRate = 3400f,
		LongitudinalCurve = new TireCurve( 0.14f, 1.25f, 0.60f, 1.05f ),
		LateralCurve = new TireCurve( 0.15f, 1.22f, 0.60f, 1.06f ),
		LoadSensitivity = 0.07f,
		Layout = DriveLayout.RWD,
		// +20% speed pass 2026-07-21: PeakTorque 320->384, RedlineRpm 3900->4680 (top-gear top speed +20%).
		PeakTorque = 384f,           // torquey: >2× hatch; strong hills
		IdleRpm = 650f,
		RedlineRpm = 4680f,          // lowest redline in roster; low-rev truck character (was 3900, +20% pass)
		EngineInertia = 0.5f,
		EngineBrakeTorque = 90f,     // strong engine braking downhill
		// Real recorded truck set: deeper truck idle + revving high layer; base pitch dropped.
		EngineSoundEvent = "sounds/engine/engine_real_truck_low.sound",
		EngineSoundEventHigh = "sounds/engine/engine_real_truck_high.sound",
		EnginePitchBase = 0.85f,
		GearRatios = new[] { 3.8f, 2.3f, 1.5f, 1.1f, 0.85f },
		ReverseRatio = 3.8f,
		FinalDrive = 3.9f,
		ShiftUpRpm = 3500f,
		ShiftDownRpm = 1700f,
		BrakeTorque = 7000f,
		BrakeBias = 0.65f,           // unladen bed = light rear axle
		HandbrakeTorque = 5000f,
		MaxSteerAngle = 27f,         // slow truck steering; long wheelbase stabilizes
		HighSpeedSteerAngle = 8f,
		SteerRateScale = 0.9f,
		// Reverse cap override removed 2026-07-21: inherits the new 20 m/s default; the pickup's tall reverse
		// gear + low redline self-limit it to ~12 m/s (~26 mph) reverse, so it stays the roster's slowest
		// reverse by gearing character rather than an artificial ~9 mph clamp.
		HandbrakeGripScale = 0.45f,  // deepest rear cut in the roster — 1900 kg + 3.4 m wheelbase needs real help
		SpinRecoveryAssist = 7.0f,
		SportTcSlipTarget = 0.30f,   // Sport keeps the torquey RWD truck from lighting the rears to redline
		SportStabilityScale = 0.5f,  // half-authority yaw damp so a Sport slide stays catchable
		DefaultAssists = AssistLevel.Casual,
		Tint = new Color( 0.55f, 0.13f, 0.11f ), // dark brick red
	};

	public static CarDefinition Kart => new()
	{
		Name = "Go-Kart",
		Style = BodyStyle.Kart,
		HasDriver = true,
		Mass = 260f,
		BodySize = new Vector3( 1.9f, 1.15f, 0.30f ),
		Wheelbase = 1.55f,
		TrackWidth = 1.14f,
		RideHeight = 0.17f,
		GroundClearance = 0.08f,
		CenterOfMassDrop = 0.02f, // tiny chassis: a deep drop puts CoM below the wheels
		WheelRadius = 0.16f,
		WheelInertia = 0.18f,
		SuspensionTravel = 0.14f,
		SpringRate = 24000f,
		DamperRate = 1600f,
		Layout = DriveLayout.RWD,
		// +20% speed pass 2026-07-21: PeakTorque 52->62.4, RedlineRpm 9000->10800 (top-gear top speed +20%).
		PeakTorque = 62.4f, // punchy launch, still shifts out of wheelspin quickly
		IdleRpm = 1400f,
		RedlineRpm = 10800f,
		EngineInertia = 0.05f,
		EngineBrakeTorque = 8f,
		// The kart keeps the buzzier single loop; base pitch >1 preserves its whine, while the narrowed
		// band tames the redline chipmunk.
		EngineSoundEvent = "sounds/engine/engine_b_sport_purr.sound",
		EnginePitchBase = 1.2f,
		GearRatios = new[] { 3.4f, 2.4f, 1.8f, 1.4f, 1.1f },
		ReverseRatio = 3.4f,
		FinalDrive = 6.3f,
		ShiftUpRpm = 8000f,
		ShiftDownRpm = 4000f,
		BrakeTorque = 560f,
		HandbrakeTorque = 900f,
		MaxSteerAngle = 31f,
		HighSpeedSteerAngle = 9f,
		// sticky kart slicks
		LongitudinalCurve = new TireCurve( 0.09f, 1.55f, 0.40f, 1.24f ),
		LateralCurve = new TireCurve( 0.12f, 1.66f, 0.45f, 1.32f ),
		AbsSlipThreshold = 0.20f, // earlier release for the lockup-prone kart
		HandbrakeGripScale = 0.70f, // mild rear cut — the light kart already rotates
		SpinRecoveryAssist = 7.0f,
		SportTcSlipTarget = 0.40f,  // loosest Sport TC — the light kart is meant to be playful
		SportStabilityScale = 0.45f, // gentle yaw damp; the kart rotates easily so keep it light
		HandbrakeSlipCap = -0.7f, // keep the rears rotating mid-slide for a cleaner drift exit
		// Recumbent kart driver pose: reclines with legs extended forward to the pedals.
		DriverSit = 4,
		DriverSitOffsetHeight = 0f,
		DefaultAssists = AssistLevel.Casual, // default fun car: keep it catchable
		Tint = new Color( 0.58f, 0.83f, 0.07f ), // acid green
	};

	public static CarDefinition Coupe => new()
	{
		Name = "Sports Coupe",
		Mass = 1420f,
		BodySize = new Vector3( 4.4f, 1.85f, 1.25f ),
		Wheelbase = 2.7f,
		TrackWidth = 1.60f,
		Layout = DriveLayout.RWD,
		// +20% speed pass 2026-07-21: PeakTorque 340->408, RedlineRpm 7200->8640 (top-gear top speed +20%).
		// ShiftUpRpm left at 6600 (unchanged) so the shift ladder holds in ground-speed terms; only the
		// top gear extends to the new redline.
		PeakTorque = 408f,
		RedlineRpm = 8640f,
		ShiftUpRpm = 6600f,
		// Real recorded car set (shared with the hatch): deep muscle idle crossfaded up to a
		// revving high layer; base pitch neutral so it sweeps the band cleanly.
		EngineSoundEvent = "sounds/engine/engine_real_low.sound",
		EngineSoundEventHigh = "sounds/engine/engine_real_high.sound",
		EnginePitchBase = 1.0f,
		LongitudinalCurve = new TireCurve( 0.09f, 1.50f, 0.40f, 1.20f ),
		LateralCurve = new TireCurve( 0.13f, 1.69f, 0.50f, 1.36f ),
		HandbrakeGripScale = 0.55f, // rears break loose under handbrake (J-turn initiation)
		HandbrakeSlipCap = -0.7f,
		SpringRate = 46000f,
		DamperRate = 3600f,
		BrakeTorque = 6200f,
		WheelRadius = 0.33f,
		SpinRecoveryAssist = 7.0f,
		SportTcSlipTarget = 0.35f,   // Sport TC: rears still slide on throttle but can't free-spin to redline
		SportStabilityScale = 0.5f,  // half-authority yaw damp: the counter-steer pendulum stays catchable
		MaxSteerAngle = 30f,
		HighSpeedSteerAngle = 10f, // sportiest car gets the most high-speed turn-in
		Tint = new Color( 0.80f, 0.05f, 0.07f ), // bright signal red
	};
}
fieldguide.vehiclephysics / Code/VehicleWheel.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// One wheel: shapecast ground detection + spring/damper suspension (skeleton pattern from
/// Facepunch/sbox-libwheel, MIT) with the friction model replaced by the spec §5.2.1 slip
/// physics — wheel angular velocity as integrated state, slip ratio/angle into peaked curves,
/// friction ellipse, load sensitivity, low-speed blend.
///
/// Driven by <see cref="VehicleController"/> in substeps; forces are accumulated across
/// substeps and applied once per fixed update (ApplyForceAt applies for the whole step).
/// All internal math in SI.
/// </summary>
public sealed class VehicleWheel : Component
{
	// Configured by VehicleFactory from CarDefinition
	public float Radius { get; set; } = 0.31f;
	public float Inertia { get; set; } = 1.2f;
	public float SuspensionTravel { get; set; } = 0.18f;
	public float SpringRate { get; set; } = 38000f;
	public float DamperRate { get; set; } = 2800f;
	public TireCurve LongitudinalCurve { get; set; }
	public TireCurve LateralCurve { get; set; }
	public float LoadSensitivity { get; set; } = 0.06f;
	public float StaticLoad { get; set; } = 3000f; // N, set by factory: mass·g/4
	public bool IsSteering { get; set; }
	public bool IsDriven { get; set; }
	public bool HasHandbrake { get; set; }
	public float GripScale { get; set; } = 1f; // live multiplier, e.g. handbrake drift cuts rear grip
	public float ParkBrakeScale { get; set; } = 1f; // anti-jitter stiction strength; controller fades it with throttle

	/// <summary>Per-substep DRIVE-side angular-velocity cap (rad/s), set by the controller each
	/// substep to the drivetrain's redline-equivalent wheel speed for the current gear (see
	/// <see cref="Drivetrain.RedlineWheelSpeed"/>). Drive torque can never push the wheel PAST this
	/// within a substep; the ground (tire reaction) still can, and a pre-existing overspeed is never
	/// yanked down (no phantom braking). float.MaxValue = no cap (undriven wheels, neutral).</summary>
	public float DriveOmegaCap { get; set; } = float.MaxValue;

	// State
	public float AngularVelocity { get; private set; } // rad/s, +forward
	public float SteerAngle { get; set; } // degrees, set by controller
	public float SlipRatio { get; private set; }
	public float SlipAngle { get; private set; } // radians
	public float Load { get; private set; } // N
	public bool IsGrounded => _trace.Hit;
	public float GroundSpeed { get; private set; } // m/s along wheel forward
	public float SuspensionLength { get; private set; } // m, attach point to wheel center, for visuals

	public string DebugTrace => !_trace.Hit
		? "miss"
		: $"{_trace.GameObject?.Name ?? "?"}{(_trace.StartedSolid ? "!SOLID" : "")} d{_trace.Distance:F1}u n{_trace.Normal.z:F2} sf{(_trace.Surface?.Friction ?? -1f):F2}";

	const float TraceSphereRadius = 1f * Units.UnitsToMeters; // shapecast sphere, see DoTrace

	Rigidbody _rigidbody;
	SceneTraceResult _trace;
	Vector3 _accumulatedForce; // N, world space
	Vector3 _forcePosition;
	int _substeps;
	float _smoothedSlipAngle;

	protected override void OnEnabled()
	{
		_rigidbody = Components.GetInAncestorsOrSelf<Rigidbody>();
	}

	/// <summary>Trace and reset accumulators. Call once per fixed update, before substeps.</summary>
	public void BeginStep()
	{
		DoTrace();
		_accumulatedForce = Vector3.Zero;
		_substeps = 0;
	}

	/// <summary>
	/// One physics substep. driveTorque/brakeTorque in N·m. Accumulates chassis force,
	/// integrates wheel spin.
	/// </summary>
	public void Substep( float dt, float driveTorque, float brakeTorque )
	{
		_substeps++;

		var up = _rigidbody.WorldRotation.Up;

		// a grazing contact (car tipped over, wheel scraping a wall) is not suspension:
		// generating spring force there self-propels a flipped car along the ground
		bool validContact = IsGrounded && Vector3.Dot( _trace.Normal, up ) > 0.4f;

		if ( !validContact )
		{
			Load = 0f;
			SlipRatio = 0f;
			SlipAngle = 0f;
			SuspensionLength = SuspensionTravel;
			// AIRBORNE DRIVE GATE (ramp lip-cluster fix 2026-07-21, LIVE-UNVERIFIED): a driven wheel
			// with no valid contact gets ZERO drive torque - it freewheels (brakes still act). With
			// drive torque passed through, an unloaded driven wheel flared to redline-equivalent
			// within ~3 ticks of leaving a kicker lip, the engine pinned at 94-98% redline for the
			// whole flight (live capture: rpm 6008-6010 at the lip), the drivetrain's limiter-camp
			// escape upshifted MID-AIR ~0.27 s into every full-throttle flight at 20-30 m/s, and the
			// car touched down at the flared surface speed: slip +0.4..+0.5, a 10-12 kN (~1 g) tire
			// spike for ~2 ticks, the skid chirp, and a landing one gear too tall (post-landing drive
			// force -26..-37% vs pre-lip) - the felt "hitch going off the ramps" at any speed
			// (offline quantification: tools/ramp_lip_drivetrain_port.py). Zeroing drive here starves
			// that whole cascade: omega holds ~rolling speed, rpm stays steady, the escape shift
			// never arms in air, and touchdown slip is ~0. Flat-ground behavior is byte-identical BY
			// CONSTRUCTION (this branch never runs with valid ground contact; grounded-tick A/B in
			// the port asserts bit-identical trajectories). Registered prediction at commit time: the
			// owner's Sim-mode discriminator will NOT kill the hitch (the cascade is
			// assist-independent); result to be recorded next to this comment either way.
			IntegrateWheelSpin( dt, 0f, brakeTorque, 0f );
			// Airborne wind-down is bearing drag only: 2%/s. The previous 0.5f here was 50%/s
			// (documented in round 4 as "0.5%/s", a 100x misread of its own constant): after a
			// 1.2 s flight the wheels arrived at ~55% of road speed and braked the car while
			// spinning back up (flight recorder 2026-07-21: ~2 m/s of the touchdown loss plus
			// the landing skid chirp came from exactly this). Real free wheels barely slow.
			AngularVelocity *= 1f - 0.02f * dt;
			return;
		}

		// --- suspension ---
		// Force acts along the CONTACT NORMAL, not body-up: body-up creates a feedback
		// loop where a tilted car pushes itself sideways, slides, and flips.
		// Compression is clamped to physical travel: bottoming out is a rigid contact
		// for the body collider (bump stop), not unbounded spring force.
		var normal = _trace.Normal;
		float restLength = SuspensionTravel + Radius;
		float hitLength = _trace.Distance * Units.UnitsToMeters + TraceSphereRadius; // sphere stops one radius short
		float compression = Math.Clamp( restLength - hitLength, 0f, SuspensionTravel );
		SuspensionLength = Math.Clamp( hitLength - Radius, 0f, SuspensionTravel );

		var velAtWheel = _rigidbody.GetVelocityAtPoint( WorldPosition ) * Units.UnitsToMeters;
		float compressionSpeed = -Vector3.Dot( velAtWheel, normal );

		float springForce = SpringRate * compression + DamperRate * compressionSpeed;
		Load = Math.Clamp( springForce, 0f, StaticLoad * 4f );

		_accumulatedForce += normal * Load;
		_forcePosition = _trace.EndPosition;

		// --- tire frame on the contact plane ---
		var steerRot = _rigidbody.WorldRotation * Rotation.FromYaw( SteerAngle );
		var forward = (steerRot.Forward - normal * Vector3.Dot( steerRot.Forward, normal )).Normal;
		var side = Vector3.Cross( normal, forward );

		var contactVel = _rigidbody.GetVelocityAtPoint( _trace.EndPosition ) * Units.UnitsToMeters;
		float vLong = Vector3.Dot( contactVel, forward );
		float vLat = Vector3.Dot( contactVel, side );
		GroundSpeed = vLong;

		// --- slip ---
		// longitudinal uses RAW slip + a one-substep force clamp below (smoothing it added
		// ~100 ms of feedback lag that turned wheel spin into a rhythmic surge);
		// lateral keeps light relaxation, its loop is chassis-side and much slower
		float slipVelocity = AngularVelocity * Radius - vLong; // m/s at the contact patch
		SlipRatio = slipVelocity / MathF.Max( MathF.Abs( vLong ), 2.0f );

		float rawSlipAngle = MathF.Atan2( vLat, MathF.Abs( vLong ) + 0.7f );
		float relax = Math.Clamp( (MathF.Abs( vLong ) + 1f) * dt / 0.2f, 0.1f, 1f );
		_smoothedSlipAngle += (rawSlipAngle - _smoothedSlipAngle) * relax;
		SlipAngle = _smoothedSlipAngle;

		// --- forces from curves, load sensitivity, surface grip ---
		float surfaceGrip = _trace.Surface?.Friction ?? 1f;
		float loadFactor = 1f - LoadSensitivity * MathF.Max( 0f, Load / StaticLoad - 1f );
		float maxForce = Load * loadFactor * surfaceGrip * GripScale;

		float fx = LongitudinalCurve.Evaluate( SlipRatio ) * MathF.Sign( SlipRatio ) * maxForce;
		float fy = -LateralCurve.Evaluate( SlipAngle ) * MathF.Sign( SlipAngle ) * maxForce;

		// one-substep stability clamp: never push the wheel past ground-speed match within
		// a single substep (gain*dt/inertia > 2 here, the raw loop oscillates without this)
		float fxStable = MathF.Abs( slipVelocity ) * Inertia / (Radius * Radius * dt);
		fx = Math.Clamp( fx, -fxStable, fxStable );

		// --- friction ellipse (spec §5.2.1.4) ---
		float combined = MathF.Sqrt( fx * fx + fy * fy );
		if ( combined > maxForce && combined > 0.001f )
		{
			float scale = maxForce / combined;
			fx *= scale;
			fy *= scale;
		}

		// --- low-speed parking blend: kill standstill jitter ---
		// force is capped at what stops this wheel's mass share within one fixed frame
		// (spec 5.2.1.6) — uncapped, it overshoots and becomes a self-sustaining oscillator
		var planarVel = forward * vLong + side * vLat;
		float planarSpeed = planarVel.Length;
		float parkOmegaBlend = 0f;
		if ( planarSpeed < 1.5f && MathF.Abs( AngularVelocity * Radius ) < 1.5f && planarSpeed > 0.001f )
		{
			// ParkBrakeScale: throttle dissolves the stiction — steered fronts were
			// "parking" against full-lock standstill launches (2 km/h crawls, tele 07-07)
			float blend = (1f - planarSpeed / 1.5f) * ParkBrakeScale;
			float massShare = StaticLoad / 9.81f; // kg carried by this wheel
			float frameDt = dt * VehicleController.Substeps;
			float stopForce = massShare * planarSpeed / frameDt * 0.8f;
			float parkMag = MathF.Min( MathF.Min( planarSpeed * Load * 1.5f, stopForce ), maxForce );

			var park = -planarVel / planarSpeed * parkMag;
			fx = fx * (1f - blend) + Vector3.Dot( park, forward ) * blend;
			fy = fy * (1f - blend) + Vector3.Dot( park, side ) * blend;
			parkOmegaBlend = blend;
		}

		_accumulatedForce += forward * fx + side * fy;

		IntegrateWheelSpin( dt, driveTorque, brakeTorque, fx );

		// Park the wheel's SPIN, not just the chassis. The parking blend above brakes the chassis, but
		// its reaction torque (-fx*Radius, fed into IntegrateWheelSpin) spins this wheel up and, with
		// nothing anchoring omega at rest, it settles at a nonzero spin that keeps pushing the car: a
		// permanent slow creep with the tyre visibly rotating and the slip ratio pinging the skid
		// threshold (community report: "unless you've perfectly stopped, it infinitely skids and rotates
		// the wheels in place"). Pull omega toward the ground-rolling speed vLong/Radius (= 0 at a true
		// standstill) by the SAME blend, so a parked, unbraked wheel settles to zero spin and zero slip.
		// Offline sim: a 0.2 m/s creep that never stopped and skidded 50-100% of frames now settles to
		// under 1 mm/s with zero skid, while cruise (over 1.5 m/s, blend inert) is byte-identical. Fades
		// out with throttle (blend carries ParkBrakeScale) so standstill launches are unaffected.
		if ( parkOmegaBlend > 0f )
			AngularVelocity = MathX.Lerp( AngularVelocity, vLong / Radius, parkOmegaBlend );
	}

	// Cap-aware drive-torque rolloff onset (kart cap-camping fix 2026-07-18): drive torque begins
	// fading toward zero once the wheel's own spin reaches this fraction of DriveOmegaCap; below it
	// the rolloff is inert so below-cap behavior is byte-identical.
	const float DriveRolloffOnset = 0.90f;

	void IntegrateWheelSpin( float dt, float driveTorque, float brakeTorque, float tireForce )
	{
		float preOmega = AngularVelocity;
		bool driving = driveTorque != 0f;
		float driveDir = driving ? MathF.Sign( driveTorque ) : 0f;

		// Cap-aware drive-torque rolloff (kart "stuck turning" fix 2026-07-18). The per-substep
		// clamp below is a hard backstop, but with the clamp ALONE a driven wheel under sustained
		// full torque CAMPS exactly at the cap; when forward speed then collapses in a corner the
		// slip ratio blows far past the grip peak (live: 7+) and the longitudinal tail force eats the
		// friction ellipse, killing rear lateral grip so the yaw holds against countersteer. Fade
		// drive torque to zero as the wheel approaches the cap so it settles OFF the cap instead of
		// camping on it. Smoothstep, not a hard corner, to avoid a torque-fade limit cycle. Inert
		// below the onset (approach <= DriveRolloffOnset) so below-cap behavior is byte-identical.
		if ( driving && DriveOmegaCap < float.MaxValue )
		{
			float approach = driveDir * preOmega / DriveOmegaCap;
			if ( approach > DriveRolloffOnset )
			{
				float f = Math.Clamp( (1f - approach) / (1f - DriveRolloffOnset), 0f, 1f );
				driveTorque *= f * f * (3f - 2f * f);
			}
		}

		// drive + tire reaction
		float torque = driveTorque - tireForce * Radius;
		AngularVelocity += torque / Inertia * dt;

		// Per-substep drive-side overshoot clamp (kart high-PeakTorque wobble hunt 2026-07-18).
		// The rev limiter zeroes torque only on the substep AFTER wheel-implied rpm crosses redline,
		// so one substep of unlimited drive torque on a light wheel overshoots redline-equivalent
		// omega by up to 6-8x (measured live: kart at 900 N-m spikes to 289-339 rad/s vs the 44 rad/s
		// gear-1 redline equivalent; even stock 52 N-m reaches 141). The spike is what lets an
		// unloaded rear diverge violently from its loaded twin over any perturbation (the felt
		// "individual tires have different traction" left-right wobble). Clamp: DRIVE torque may
		// never push omega past the cap within a substep. Signed by drive direction so reverse works;
		// cap floors at the pre-integration omega so ground-driven overspeed (downhill coast) is
		// never yanked down, and the ground reaction path is untouched. Guarded on the ORIGINAL drive
		// intent so a rolloff that faded torque to zero still cannot let the wheel blow past the cap.
		if ( driving && DriveOmegaCap < float.MaxValue )
		{
			float cap = MathF.Max( DriveOmegaCap, driveDir * preOmega );
			AngularVelocity = driveDir > 0f
				? MathF.Min( AngularVelocity, cap )
				: MathF.Max( AngularVelocity, -cap );
		}

		// brakes can stop the wheel but never reverse it within a step
		if ( brakeTorque > 0f )
		{
			float brakeDelta = brakeTorque / Inertia * dt;
			AngularVelocity = MathF.Abs( AngularVelocity ) <= brakeDelta
				? 0f
				: AngularVelocity - MathF.Sign( AngularVelocity ) * brakeDelta;
		}
	}

	/// <summary>Apply the substep-averaged force to the chassis. Call once per fixed update.</summary>
	public void EndStep()
	{
		if ( _substeps == 0 || _accumulatedForce.IsNearZeroLength )
			return;

		var averaged = _accumulatedForce / _substeps;
		_rigidbody.ApplyForceAt( _forcePosition, averaged * Units.MetersToUnits );
	}

	void DoTrace()
	{
		var down = _rigidbody.WorldRotation.Down;
		float lengthUnits = (SuspensionTravel + Radius) * Units.MetersToUnits;

		_trace = Scene.Trace
			.Radius( 1f )
			.IgnoreGameObjectHierarchy( _rigidbody.GameObject )
			.WithoutTags( "car" )
			.FromTo( WorldPosition, WorldPosition + down * lengthUnits )
			.Run();
	}
}
fieldguide.vehiclephysics / DriveInputs.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Per-frame driver intent, the value-struct swap point at VehicleController's input seam
/// (<see cref="VehicleController.InputOverride"/>). Carries the raw intent the controller samples
/// from a device: the move axis (forward/back + steer), the handbrake action, and the sequential
/// shift requests. A keyboard/gamepad/wheel source or a scripted source (a test pilot, an AI) all
/// produce one of these, so the controller's gear/reverse/steer-ramp logic stays source-agnostic.
///
/// The live-device sampler <see cref="SampleDeviceInputs"/> and its gamepad shaping helpers live
/// here (lifted out of the controller): the controller either consumes an injected override or calls
/// this sampler, and nothing else in the class touches device input.
/// </summary>
public struct DriveInputs
{
	/// <summary>-1..1 signed drive axis: +forward accelerates, -back brakes then engages reverse near a
	/// stop. Built in <see cref="SampleDeviceInputs"/> as (throttle − brake) where each channel is the
	/// MAX of the keyboard/stick component (<c>Input.AnalogMove.x</c>) and the ANALOG gamepad trigger
	/// pull (<c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c>, 0..1) — so a partial trigger
	/// gives a partial pedal. A scripted source sets this float directly.</summary>
	public float MoveForward;

	/// <summary>-1..1, maps to <c>Input.AnalogMove.y</c> (gamepad path reshaped by the gamepad deadzone
	/// + response curve). Note <c>Rotation.FromYaw(+)</c> is a LEFT/CCW turn, so +Steer steers left.</summary>
	public float Steer;

	/// <summary>The handbrake / drift button: keyboard "Jump" (Space), or gamepad A (Jump's own
	/// GamepadCode) or the left bumper ("Handbrake" action).</summary>
	public bool Handbrake;

	/// <summary>Edge-triggered request to shift UP one gear (sequential MANUAL mode). Keyboard E /
	/// gamepad R1 while live; a scripted source pulses it for one tick. The controller rising-edge-
	/// detects it, so a source that holds it across ticks still shifts exactly once.</summary>
	public bool ShiftUp;

	/// <summary>Edge-triggered request to shift DOWN one gear (sequential MANUAL mode). Keyboard Q /
	/// gamepad L1. Same one-shot rising-edge semantics as <see cref="ShiftUp"/>.</summary>
	public bool ShiftDown;

	/// <summary>Edge-triggered request to toggle the transmission mode AUTO↔MANUAL. Keyboard G /
	/// gamepad D-pad down. Same one-shot rising-edge semantics as <see cref="ShiftUp"/>.</summary>
	public bool ShiftModeToggle;

	// Gamepad tier: deadzone + response curve for the analog steer axis.
	const float GamepadSteerDeadzone = 0.12f;
	const float GamepadSteerCurvePower = 1.6f; // >1 softens the center for fine control, still reaches full lock

	// Analog throttle/brake tier: a small trigger deadzone so resting triggers can't creep the pedals.
	// The engine ALSO applies its own 12.5% deadzone one layer down (Controller.SetAxis zeroes any
	// |axis| <= 0.125 before Input.GetAnalog sees it), so values arrive as 0 or >~0.125 and this floor
	// is a belt-and-suspenders guard that also rescales so full pull still reaches 1.0.
	const float GamepadTriggerDeadzone = 0.05f;

	/// <summary>Sample the live input devices into a DriveInputs value (this is the keyboard/gamepad
	/// source; other sources produce the same struct and set <see cref="VehicleController.InputOverride"/>).
	///
	/// Steering rides <c>Input.AnalogMove.y</c> straight off the left stick; keyboard emits exact
	/// -1/0/1 through the same path and passes through <see cref="ApplyGamepadSteerCurve"/> unchanged.
	///
	/// Throttle/brake are VARIABLE per device: keyboard W/S ride <c>Input.AnalogMove.x</c> as an exact
	/// ±1 digital forward/back; the gamepad triggers are read as a true ANALOG 0..1 pull via
	/// <c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c> (right = gas, left = brake). The two
	/// devices combine per channel by MAX — so either device works and neither fights the other — then
	/// the net (throttle − brake) folds into the single signed <see cref="MoveForward"/> scalar. On
	/// keyboard <c>Input.GetAnalog</c> returns 0, so keyboard-only players are byte-identical.</summary>
	public static DriveInputs SampleDeviceInputs()
	{
		var move = Input.AnalogMove;

		// keyboard/stick forward+back split off the shared move axis (W = +x, S = -x)
		float keyThrottle = MathF.Max( 0f, move.x );
		float keyBrake = MathF.Max( 0f, -move.x );

		// gamepad triggers, true analog 0..1 (right = gas, left = brake)
		float triggerThrottle = ReadTrigger( InputAnalog.RightTrigger );
		float triggerBrake = ReadTrigger( InputAnalog.LeftTrigger );

		// MAX blend per channel so either device drives the pedal, neither overrides the other
		float throttle = MathF.Max( keyThrottle, triggerThrottle );
		float brake = MathF.Max( keyBrake, triggerBrake );
		float moveForward = Math.Clamp( throttle - brake, -1f, 1f );

		return new DriveInputs
		{
			MoveForward = moveForward,
			Steer = ApplyGamepadSteerCurve( move.y ),
			Handbrake = Input.Down( "Jump" ) || Input.Down( "Handbrake" ),
		};
	}

	/// <summary>Read a gamepad trigger as a linear 0..1 pull with a small deadzone floor (rescaled so
	/// full pull still reaches 1.0). <c>Input.GetAnalog</c> already returns 0 for a trigger on keyboard,
	/// so this only shapes the gamepad path.</summary>
	static float ReadTrigger( InputAnalog trigger )
	{
		float v = Math.Clamp( Input.GetAnalog( trigger ), 0f, 1f );
		if ( v < GamepadTriggerDeadzone )
			return 0f;
		return (v - GamepadTriggerDeadzone) / (1f - GamepadTriggerDeadzone);
	}

	/// <summary>Deadzone + power curve for the analog steer axis. Values under the deadzone snap to 0;
	/// the remaining range is rescaled so full stick deflection still reaches ±1 (no lost lock), then
	/// raised to <see cref="GamepadSteerCurvePower"/> for a softer center. Keyboard's exact -1/0/1
	/// passes through unaffected (0 is inside the deadzone; 1 rescales to 1 and 1^n == 1).</summary>
	static float ApplyGamepadSteerCurve( float raw )
	{
		float mag = MathF.Abs( raw );
		if ( mag < GamepadSteerDeadzone )
			return 0f;

		float t = Math.Clamp( (mag - GamepadSteerDeadzone) / (1f - GamepadSteerDeadzone), 0f, 1f );
		return MathF.Sign( raw ) * MathF.Pow( t, GamepadSteerCurvePower );
	}
}
fieldguide.vehiclephysics / Code/Demo/DemoTuningPanel.razor
Game library
@namespace FieldGuide.VehiclePhysics
@inherits PanelComponent

@* Kit-native live tuning lab (demo layer). Toggled by the Tune action, bound to the car the chase
   camera is following. Writes tuning changes straight onto the running car through the same paths a
   consumer would use: it mutates the active CarDefinition (read live by the drivetrain and the brake
   model) and pushes suspension/tire values onto the live wheels. This is a demo-scale lab, not the
   full game panel: it exposes the highest-feel dials so the demo reads as a physics lab in the first
   minute. Replace it with your own UI; it lives only in the demo scene. *@

<root>
	@if ( Car.IsValid() )
	{
		@* Key legend, top-right: how to hop between the demo cars. Always visible; the tuning
		   chip/panel top-left covers the T binding. *@
		<div class="legend">
			<div class="mono kbd">[ ]</div>
			<div class="title">switch car</div>
		</div>
	}
	@if ( !IsOpen && Car.IsValid() )
	{
		@* Collapsed legend chip: sits exactly where the expanded panel's top-left corner lands, so
		   pressing T reads as the chip expanding into the lab. Mouse-look stays with the camera. *@
		<div class="panel chip" onclick=@ToggleOpen>
			<div class="head">
				<div class="mono kbd">@ToggleKeyLabel</div>
				<div class="title">tuning</div>
			</div>
		</div>
	}
	@if ( IsOpen && Car.IsValid() )
	{
		<div class="panel">
			<div class="head">
				<div class="title">Tuning lab · @CarName</div>
				<div class="mono kbd">@ToggleKeyLabel</div>
			</div>

			<div class="hint">Tuning the car you are driving. Changes apply live.</div>

			<div class="dials">
				@foreach ( var d in SliderDials )
				{
					var dd = d;
					<div class="dial">
						<div class="drow">
							<div class="dname">@d.Name</div>
							<div class="mono dval">@d.Display()</div>
						</div>
						<div class="dctl">
							<div class="mono step" onclick=@( () => Step( dd, -1 ) )>-</div>
							<div class="track"
								onmousedown=@( e => Scrub( dd, e, true ) )
								onmousemove=@( e => Scrub( dd, e, false ) )>
								<div class="fill" style="width: @WidthPct( dd )%"></div>
							</div>
							<div class="mono step" onclick=@( () => Step( dd, +1 ) )>+</div>
						</div>
					</div>
				}

				<div class="dial">
					<div class="drow">
						<div class="dname">Assists</div>
						<div class="mono cycle" onclick=@CycleAssists>@AssistLabel</div>
					</div>
				</div>

				<div class="dial">
					<div class="drow">
						<div class="dname">Tires</div>
						<div class="mono cycle" onclick=@CycleTires>@TireLabel</div>
					</div>
				</div>
			</div>

			<div class="foot">
				<div class="btn" onclick=@ResetToStock>Reset to stock</div>
			</div>
		</div>
	}
</root>

@code {
	/// <summary>The car this panel tunes: the one the chase camera follows. Set by DemoBootstrap. On
	/// change the panel snapshots the car's pristine (authored) values so Reset-to-stock can restore
	/// them and the grip/torque multipliers rebase to the new car.</summary>
	VehicleController _car;
	public VehicleController Car
	{
		get => _car;
		set
		{
			if ( ReferenceEquals( _car, value ) )
				return;
			_car = value;
			Snapshot();
		}
	}

	/// <summary>Open state. Static so the kit's chase camera can read it through the
	/// <see cref="VehicleCamera.CursorModalOpen"/> seam (DemoBootstrap wires that) without holding a
	/// reference to this panel. The demo runs exactly one panel, so a single flag is enough.</summary>
	public static bool IsOpen;

	// Toggle input action. Documented in the README input table; the host ProjectSettings/Input.config
	// ships it. Never Escape or an F-key.
	const string ToggleAction = "Tune";
	string ToggleKeyLabel => "T";

	string CarName => Car?.Definition?.Name ?? "Car";

	// Live multipliers over the pristine base. Grip scales the (preset-selected) tire curves; torque
	// scales the authored peak engine torque.
	float _gripScale = 1f;
	float _torqueScale = 1f;
	int _tirePreset; // 0 Stock, 1 Street, 2 Sport, 3 Offroad

	// Pristine snapshot, captured value-by-value when the car is bound (its definition is untouched at
	// that point). Value types only (floats, TireCurve struct, enum), so later live tuning of the
	// definition can never corrupt these; Reset restores from here.
	bool _snapped;
	float _stockPeakTorque, _stockSpring, _stockDamper, _stockTravel, _stockBrake;
	TireCurve _stockLat, _stockLong;
	AssistLevel _stockAssists;

	// Current UNSCALED tire base (Stock or a named preset). Grip multiplies these to get the live curves.
	TireCurve _baseLat, _baseLong;

	void Snapshot()
	{
		var def = _car?.Definition;
		_dials = null;
		if ( def is null )
		{
			_snapped = false;
			return;
		}

		_stockPeakTorque = def.PeakTorque;
		_stockSpring = def.SpringRate;
		_stockDamper = def.DamperRate;
		_stockTravel = def.SuspensionTravel;
		_stockBrake = def.BrakeTorque;
		_stockLat = def.LateralCurve;
		_stockLong = def.LongitudinalCurve;
		// Stock assist = the authored definition default. Read it off the definition, not the
		// controller: the controller adopts DefaultAssists in its own OnStart, which may run a frame
		// or two after this bind, so its live Assists is not reliable yet.
		_stockAssists = def.DefaultAssists;

		_baseLat = _stockLat;
		_baseLong = _stockLong;
		_gripScale = 1f;
		_torqueScale = 1f;
		_tirePreset = 0;
		_snapped = true;
		StateHasChanged();
	}

	// ---- dial model ----
	class Dial
	{
		public string Name;
		public float Min, Max, Step;
		public Func<float> Get;
		public Action<float> Set;
		public Func<string> Fmt;
		public string Display() => Fmt();
	}

	List<Dial> _dials;
	List<Dial> SliderDials => _dials ??= BuildDials();

	List<Dial> BuildDials()
	{
		if ( !Car.IsValid() )
			return new List<Dial>();

		var def = Car.Definition;
		return new List<Dial>
		{
			new()
			{
				Name = "Grip", Min = 0.6f, Max = 2.2f, Step = 0.05f,
				Get = () => _gripScale, Set = SetGrip,
				Fmt = () => _gripScale.ToString( "0.00" ) + "x",
			},
			new()
			{
				Name = "Drive torque", Min = 0.5f, Max = 2.0f, Step = 0.05f,
				Get = () => _torqueScale, Set = SetTorqueScale,
				Fmt = () => _torqueScale.ToString( "0.00" ) + "x",
			},
			new()
			{
				Name = "Suspension stiffness", Min = 15000f, Max = 60000f, Step = 2000f,
				Get = () => def.SpringRate, Set = v => { def.SpringRate = v; ApplyWheels(); },
				Fmt = () => def.SpringRate.ToString( "0" ) + " N/m",
			},
			new()
			{
				Name = "Suspension damping", Min = 800f, Max = 6000f, Step = 200f,
				Get = () => def.DamperRate, Set = v => { def.DamperRate = v; ApplyWheels(); },
				Fmt = () => def.DamperRate.ToString( "0" ) + " Ns/m",
			},
			new()
			{
				Name = "Suspension travel", Min = 0.10f, Max = 0.35f, Step = 0.01f,
				Get = () => def.SuspensionTravel, Set = v => { def.SuspensionTravel = v; ApplyWheels(); },
				Fmt = () => (def.SuspensionTravel * 100f).ToString( "0" ) + " cm",
			},
			new()
			{
				Name = "Brake force", Min = 1500f, Max = 8000f, Step = 200f,
				Get = () => def.BrakeTorque, Set = v => def.BrakeTorque = v,
				Fmt = () => def.BrakeTorque.ToString( "0" ) + " Nm",
			},
		};
	}

	float Frac( Dial d )
	{
		if ( d.Max <= d.Min )
			return 0f;
		return Math.Clamp( (d.Get() - d.Min) / (d.Max - d.Min), 0f, 1f );
	}

	string WidthPct( Dial d ) =>
		(Frac( d ) * 100f).ToString( "0.#", System.Globalization.CultureInfo.InvariantCulture );

	void Step( Dial d, int clicks )
	{
		float v = Math.Clamp( d.Get() + clicks * d.Step, d.Min, d.Max );
		d.Set( v );
		StateHasChanged();
	}

	// Click-to-jump + drag-to-scrub on the dial track (mirrors the game panel's proven pattern). jump
	// is true on mousedown (a bare click sets the value at the cursor) and false on mousemove (scrub
	// only while the track owns the press). Value = mouse local x over track width, snapped to the
	// dial's step, pushed through the SAME Set path as the +/- steps.
	void Scrub( Dial d, Sandbox.UI.PanelEvent ev, bool jump )
	{
		if ( ev is not Sandbox.UI.MousePanelEvent e )
			return;

		var track = e.This;
		if ( track is null )
			return;

		// mousemove fires whether or not the button is held; only scrub while the track owns the press.
		if ( !jump && !track.PseudoClass.HasFlag( Sandbox.UI.PseudoClass.Active ) )
			return;

		float w = track.Box.Rect.Width;
		if ( w <= 0f )
			return;

		float frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );
		float v = d.Min + frac * (d.Max - d.Min);
		if ( d.Step > 0f )
			v = MathF.Round( v / d.Step ) * d.Step;
		v = Math.Clamp( v, d.Min, d.Max );
		d.Set( v );
		StateHasChanged();
	}

	// ---- apply paths (same seams a consumer would use) ----
	static TireCurve Scaled( TireCurve c, float k ) =>
		new( c.PeakSlip, c.PeakGrip * k, c.TailSlip, c.TailGrip * k );

	// Push the definition's suspension + tire values onto the live wheels. The factory copies these at
	// spawn; the wheel re-reads them every substep, so writing them here is the live-apply path.
	void ApplyWheels()
	{
		var def = Car.Definition;
		foreach ( var w in Car.Wheels )
		{
			w.SpringRate = def.SpringRate;
			w.DamperRate = def.DamperRate;
			w.SuspensionTravel = def.SuspensionTravel;
			w.LateralCurve = def.LateralCurve;
			w.LongitudinalCurve = def.LongitudinalCurve;
		}
	}

	void SetGrip( float k )
	{
		_gripScale = k;
		var def = Car.Definition;
		def.LateralCurve = Scaled( _baseLat, k );
		def.LongitudinalCurve = Scaled( _baseLong, k );
		ApplyWheels();
	}

	// Drive torque scale multiplies the authored peak torque. Drivetrain reads def.PeakTorque live
	// (it holds the same definition instance), so no drivetrain touch is needed.
	void SetTorqueScale( float k )
	{
		_torqueScale = k;
		Car.Definition.PeakTorque = _stockPeakTorque * k;
	}

	void CycleAssists()
	{
		Car.Assists = Car.Assists switch
		{
			AssistLevel.Casual => AssistLevel.Sport,
			AssistLevel.Sport => AssistLevel.Sim,
			_ => AssistLevel.Casual,
		};
		StateHasChanged();
	}

	string AssistLabel => Car.IsValid() ? Car.Assists.ToString() : "";

	// Tire preset swaps the UNSCALED base curves, then re-applies the current grip multiplier so the
	// grip dial and the preset compose. Stock restores the car's own authored curves.
	void CycleTires()
	{
		_tirePreset = (_tirePreset + 1) % 4;
		(_baseLat, _baseLong) = _tirePreset switch
		{
			1 => (TireCurve.Street, TireCurve.Street),
			2 => (TireCurve.Sport, TireCurve.Sport),
			3 => (TireCurve.Offroad, TireCurve.Offroad),
			_ => (_stockLat, _stockLong),
		};
		SetGrip( _gripScale );
		StateHasChanged();
	}

	string TireLabel => _tirePreset switch
	{
		1 => "Street",
		2 => "Sport",
		3 => "Offroad",
		_ => "Stock",
	};

	// Re-apply the car's pristine authored values (captured at bind). Definitions in the demo roster
	// are fresh per car, so "stock" is unambiguous: the values this car spawned with.
	void ResetToStock()
	{
		if ( !Car.IsValid() || !_snapped )
			return;

		var def = Car.Definition;
		def.PeakTorque = _stockPeakTorque;
		def.SpringRate = _stockSpring;
		def.DamperRate = _stockDamper;
		def.SuspensionTravel = _stockTravel;
		def.BrakeTorque = _stockBrake;
		def.LateralCurve = _stockLat;
		def.LongitudinalCurve = _stockLong;

		_gripScale = 1f;
		_torqueScale = 1f;
		_tirePreset = 0;
		_baseLat = _stockLat;
		_baseLong = _stockLong;

		Car.Assists = _stockAssists;
		ApplyWheels();
		StateHasChanged();
	}

	protected override void OnEnabled()
	{
		// Start COLLAPSED: the chip legend keeps the keybind discoverable while mouse-look stays
		// with the camera (an open lab on spawn captured the cursor before players ever drove,
		// owner call 2026-07-19). Static flag, so reset it here each session.
		IsOpen = false;
	}

	void ToggleOpen()
	{
		IsOpen = !IsOpen;
		Mouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
		StateHasChanged();
	}

	protected override void OnUpdate()
	{
		if ( Input.Pressed( ToggleAction ) )
		{
			IsOpen = !IsOpen;
			Mouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
			StateHasChanged();
		}

		// The chase camera re-hides the cursor every frame while it owns it; hold it visible while open.
		if ( IsOpen )
			Mouse.Visibility = MouseVisibility.Visible;
	}

	protected override int BuildHash()
	{
		// Closed still renders the chip legend, and the chip waits on the car binding, so the
		// hash must move when the car arrives or the first build would stick on the empty tree.
		if ( !IsOpen )
			return Car.IsValid() ? 1 : 0;

		var h = new HashCode();
		h.Add( _gripScale );
		h.Add( _torqueScale );
		h.Add( _tirePreset );
		if ( Car.IsValid() )
		{
			var def = Car.Definition;
			h.Add( def.SpringRate );
			h.Add( def.DamperRate );
			h.Add( def.SuspensionTravel );
			h.Add( def.BrakeTorque );
			h.Add( (int)Car.Assists );
		}
		return h.ToHashCode();
	}
}
fieldguide.vehiclephysics / VehicleController.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Drives one car: input → assists → drivetrain → wheel substeps.
/// Runs 4 internal substeps per fixed update. PRECISION NOTE: this is drivetrain/wheel-STATE
/// substepping, not a full 200 Hz contact simulation — the ground trace happens once per fixed
/// update, the rigidbody does not advance between substeps (velocity-at-contact is re-sampled
/// against the same body state), and the accumulated tire force is applied once, averaged. What the
/// substeps genuinely refine: wheel angular velocity integration (slip-ratio stability at 4× the
/// rate), clutch/RPM coupling, and the TC feedback loop. Contact/chassis transients (washboard, curb
/// strikes, landings) resolve at 50 Hz. Owner-simulated; proxies early-out.
/// </summary>
public sealed class VehicleController : Component, Component.ICollisionListener
{
	public const int Substeps = 4;

	public CarDefinition Definition { get; set; }
	public List<VehicleWheel> Wheels { get; } = new();
	public Drivetrain Drivetrain { get; private set; }

	[Property] public AssistLevel Assists { get; set; } = AssistLevel.Casual;

	/// <summary>Optional assist level a spawn path wants this car to adopt on start, instead of the
	/// definition default. Set by a spawn path that carries a chosen assist level across a respawn or
	/// car swap. It exists because <see cref="OnStart"/> runs a frame or two AFTER <c>Components.Create</c>
	/// — i.e. after the spawning code has already run — so a plain post-spawn <c>Assists = x</c> is
	/// overwritten by the default-init in <see cref="OnStart"/>. When non-null, OnStart adopts THIS
	/// value instead; null keeps the definition default. Harmless regardless of the exact create/start
	/// ordering: if OnStart happened to run first, the spawn path's direct <c>Assists</c> set still wins.</summary>
	public AssistLevel? InitialAssists { get; set; }

	public float Throttle { get; private set; }
	public float Brake { get; private set; }
	public float Steer { get; private set; } // -1..1
	public bool Handbrake { get; private set; }
	public float SpeedMs => _rigidbody.IsValid() ? (_rigidbody.Velocity * Units.UnitsToMeters).Length : 0f;

	/// <summary>
	/// Signed longitudinal speed (m/s) for a HUD speedo: the magnitude equals <see cref="SpeedMs"/>
	/// (forward reads exactly as before), and the sign is the car's travel direction along its own
	/// facing — NEGATIVE while rolling backwards. The sign source is the SAME forward-axis projection
	/// <see cref="ApplySpinRecoveryAssist"/> and gear-engage already use
	/// (velocity · <see cref="Sandbox.GameObject.WorldRotation"/>.Forward). DISPLAY read only — no
	/// physics or telemetry consumes it.
	/// </summary>
	public float SignedSpeedMs
	{
		get
		{
			if ( !_rigidbody.IsValid() )
				return 0f;

			float forward = Vector3.Dot( _rigidbody.Velocity, WorldRotation.Forward );
			return forward < 0f ? -SpeedMs : SpeedMs;
		}
	}

	/// <summary>
	/// Input-source seam (the <see cref="DriveInputs"/> pluggable-source abstraction —
	/// keyboard/gamepad/wheel/scripted pilot as peers at ONE seam). When non-null this value-struct is
	/// consumed by <see cref="ReadInput"/> INSTEAD of sampling live keyboard/gamepad, so whatever set
	/// it drives through the exact same input → assists → drivetrain path a human uses — it never
	/// applies forces itself (an intent-injection pattern). Null = normal keyboard/gamepad. A scripted
	/// source sets it each tick while it drives and clears it when done. This is the ONLY input
	/// addition to VehicleController (a deliberately narrow seam); future device sources plug in here
	/// without touching this class again.
	/// </summary>
	public DriveInputs? InputOverride { get; set; }

	Rigidbody _rigidbody;
	Vector3 _spawnPosition;
	Rotation _spawnRotation;

	protected override void OnStart()
	{
		_rigidbody = Components.Get<Rigidbody>();
		Definition ??= CarDefinitions.Hatch;
		Drivetrain = new Drivetrain( Definition );
		// Adopt a carried session mode if a spawn path staged one (car swap); otherwise the
		// definition default. Because this runs a frame or two after the car is created, a spawn
		// path can't rely on a plain post-spawn Assists set surviving — it stages InitialAssists.
		Assists = InitialAssists ?? Definition.DefaultAssists;
		_spawnPosition = WorldPosition;
		_spawnRotation = WorldRotation;

		// suspension needs continuous simulation — a sleeping body ignores our forces
		if ( _rigidbody.PhysicsBody is not null )
			_rigidbody.PhysicsBody.AutoSleep = false;

		// brief freeze so the car initializes perfectly level and still
		_rigidbody.MotionEnabled = false;
		_settleFreeze = 0.4f;
	}

	TimeSince _telemetry;
	TimeSince _sinceSpawn;
	float _settleFreeze;

	protected override void OnFixedUpdate()
	{
		if ( IsProxy || !_rigidbody.IsValid() || Drivetrain is null )
			return;

		if ( _settleFreeze > 0f )
		{
			_settleFreeze -= Time.Delta;
			if ( _settleFreeze <= 0f )
			{
				_rigidbody.MotionEnabled = true;
				_sinceSpawn = 0;
			}
			return;
		}

		ReadInput();
		ApplySteering();

		// handbrake = drift button: rears instantly lose lateral bite, snap back on release
		foreach ( var wheel in Wheels )
		{
			if ( !wheel.IsSteering )
				wheel.GripScale = Handbrake ? Definition.HandbrakeGripScale : 1f;

			// throttle dissolves low-speed parking stiction on ALL wheels so full-lock
			// launches actually launch (undriven steered fronts were the drag)
			wheel.ParkBrakeScale = 1f - Throttle;
		}

		foreach ( var wheel in Wheels )
			wheel.BeginStep();

		var driven = Wheels.Where( w => w.IsDriven ).ToList();
		float dt = Time.Delta / Substeps;

		// ground-truth wheel speed for shifting: actual forward speed over the tire radius
		float forwardSpeed = Vector3.Dot( _rigidbody.Velocity * Units.UnitsToMeters, WorldRotation.Forward );
		float groundWheelSpeed = forwardSpeed / Definition.WheelRadius;

		// arcade launch boost: extra shove off the line, fully faded by 15 m/s
		float launchBoost = MathX.Lerp( Definition.LaunchBoost, 1f, Math.Clamp( SpeedMs / 15f, 0f, 1f ) );

		// drift-catch assist: arm on the handbrake RELEASE edge, compute this tick's throttle factor.
		if ( _wasHandbrake && !Handbrake )
			_sinceHandbrakeRelease = 0f;
		_wasHandbrake = Handbrake;
		float driftCatch = DriftCatchFactor();

		for ( int step = 0; step < Substeps; step++ )
		{
			float avgDrivenSpeed = driven.Count > 0 ? driven.Average( w => w.AngularVelocity ) : 0f;
			float throttle = ApplyTractionControl( Throttle * driftCatch, driven );
			float torquePerWheel = Drivetrain.Simulate( dt, throttle, avgDrivenSpeed, groundWheelSpeed, driven.Count ) * launchBoost;

			// Drive-side omega cap, read fresh AFTER Simulate (a shift changes the ratio mid-loop):
			// drive torque may never push a driven wheel past redline-equivalent within a substep
			// (the limiter-one-substep-late overshoot defect; see VehicleWheel.IntegrateWheelSpin).
			float omegaCap = Drivetrain.RedlineWheelSpeed;

			foreach ( var wheel in Wheels )
			{
				float drive = wheel.IsDriven ? torquePerWheel : 0f;
				float brake = BrakeTorqueFor( wheel );
				if ( wheel.IsDriven )
					wheel.DriveOmegaCap = omegaCap;
				wheel.Substep( dt, drive, brake );
			}
		}

		foreach ( var wheel in Wheels )
			wheel.EndStep();

		ApplyBrakeAssist();
		ApplySpinRecoveryAssist();
		ApplyStabilityAssist();
		ApplyWallGlanceAssist();
		ApplyAirAttitudeAssist();

		// dense driving telemetry: 2 Hz while moving or on input — parseable for analysis
		if ( _telemetry > 0.5f && (SpeedMs > 0.5f || Throttle > 0f || Brake > 0f) )
		{
			_telemetry = 0;
			float yawRate = _rigidbody.AngularVelocity.z.RadianToDegree();
			var rears = Wheels.Where( w => !w.IsSteering ).ToList();
			var fronts = Wheels.Where( w => w.IsSteering ).ToList();
			Log.Info( $"[vp] tele car={Definition?.Name ?? "?"} v {SpeedMs * 3.6f:F0}kmh rpm {Drivetrain.Rpm:F0} gear {Drivetrain.Gear} | thr {Throttle:F2} brk {Brake:F2} hb {(Handbrake ? 1 : 0)} steer {Steer:F2} | yaw {yawRate:F0}deg/s | rearK {rears.Average( w => w.SlipRatio ):F2} frontA {fronts.Average( w => w.SlipAngle.RadianToDegree() ):F1} rearA {rears.Average( w => w.SlipAngle.RadianToDegree() ):F1}" );

			// per-wheel trace hits whenever contact is abnormal — names what we're driving
			// on (or falling through); this diagnostic has caught three bugs, keep it
			int grounded = Wheels.Count( w => w.IsGrounded );
			if ( grounded < 4 || _sinceSpawn < 6f )
			{
				var detail = string.Join( " | ", Wheels.Select( w =>
					$"{w.GameObject.Name[^2..]} {w.DebugTrace} Fz {w.Load:F0}" ) );
				Log.Info( $"[vp] wheels z {WorldPosition.z * Units.UnitsToMeters:F1}m | {detail}" );
			}
		}
	}

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		if ( Input.Pressed( "Reload" ) )
			Respawn();

		// Drive-mode (assist level) cycle — Casual -> Sport -> Sim -> Casual. A plain property set,
		// so (like Reload) it reads Input.Pressed straight in OnUpdate; no fixed-step edge-latch needed.
		// A plain Assists set survives respawns/car swaps via InitialAssists and never gets clobbered
		// after OnStart.
		if ( Input.Pressed( "DriveMode" ) )
			CycleDriveMode();

		// Sequential-shift edges are latched HERE (per-frame) — Input.Pressed is frame-scoped and
		// unreliable read from OnFixedUpdate, which may run zero or several times a frame. ReadInput
		// (fixed step) consumes + clears these. A frame with no fixed tick still keeps the latch until
		// the next one, so no press is lost at any framerate.
		if ( Input.Pressed( "ShiftUp" ) ) _liveShiftUp = true;
		if ( Input.Pressed( "ShiftDown" ) ) _liveShiftDown = true;
		if ( Input.Pressed( "ShiftMode" ) ) _liveShiftMode = true;
	}

	void ReadInput()
	{
		// one seam: live devices by default, or whatever an input source (a scripted pilot, an input
		// device) staged in InputOverride this tick. The struct carries the SAME raw intent the
		// keyboard sampled, so all the gear/reverse/steer-ramp logic below is source-agnostic.
		var inputs = InputOverride ?? DriveInputs.SampleDeviceInputs();

		// forward stick/W = accelerate, back = brake — unless (near-)stopped, then back = reverse
		float forwardInput = inputs.MoveForward;
		float forwardSpeed = Vector3.Dot( _rigidbody.Velocity * Units.UnitsToMeters, WorldRotation.Forward );

		// direction changes engage while still rolling ~1 m/s the wrong way — waiting
		// for a dead stop is what made K-turns feel like a driving test
		if ( Drivetrain.Gear >= 0 )
		{
			Throttle = MathF.Max( 0f, forwardInput );
			Brake = MathF.Max( 0f, -forwardInput );

			if ( forwardInput < -0.15f && forwardSpeed < 1.0f )
				Drivetrain.EngageReverse();
		}
		else // reverse gear: back = reverse throttle, forward = brake
		{
			Throttle = MathF.Max( 0f, -forwardInput );
			Brake = MathF.Max( 0f, forwardInput );

			if ( forwardInput > 0.15f && forwardSpeed > -1.0f )
				Drivetrain.EngageForward();
		}

		Handbrake = inputs.Handbrake;

		// reverse is for maneuvering, not backward land-speed records
		if ( Drivetrain.Gear == -1 && SpeedMs > Definition.ReverseSpeedCap )
			Throttle = 0f;

		// steering: keyboard ramps, analog is direct; both rate and lock reduce with
		// speed — fast full-lock flicks at 70+ km/h were driver-induced weave
		float speedT = Math.Clamp( SpeedMs / 22f, 0f, 1f );
		float steerTarget = inputs.Steer;
		float rate = (MathF.Abs( steerTarget ) > 0.01f
			? MathX.Lerp( 4.5f, 1.8f, speedT )
			: MathX.Lerp( 6f, 3f, speedT )) * Definition.SteerRateScale;
		Steer = MathX.Lerp( Steer, steerTarget, Math.Clamp( rate * Time.Delta, 0f, 1f ) );

		// ── sequential MANUAL shift requests ──
		// Two sources at ONE seam, exactly like MoveForward/Steer/Handbrake: a scripted InputOverride
		// carries the shift bits in the struct; live play uses the per-frame edges latched in OnUpdate.
		// Live latches are consumed either way so a stray key pressed during a scripted run can't leak
		// into a live shift when the override clears.
		bool reqUp, reqDown, reqMode;
		if ( InputOverride is DriveInputs ov )
		{
			reqUp = ov.ShiftUp;
			reqDown = ov.ShiftDown;
			reqMode = ov.ShiftModeToggle;
		}
		else
		{
			reqUp = _liveShiftUp;
			reqDown = _liveShiftDown;
			reqMode = _liveShiftMode;
		}
		_liveShiftUp = _liveShiftDown = _liveShiftMode = false;

		ApplyShiftRequests( reqUp, reqDown, reqMode, forwardSpeed / Definition.WheelRadius );
	}

	// ── sequential MANUAL shift state ──
	bool _liveShiftUp, _liveShiftDown, _liveShiftMode;   // per-frame edges latched in OnUpdate
	bool _prevReqUp, _prevReqDown, _prevReqMode;          // rising-edge memory across fixed ticks

	/// <summary>Rising-edge-detect this tick's shift requests and drive the drivetrain. The live path's
	/// requests are already one-tick pulses (latched Input.Pressed edges), and this ALSO edge-gates the
	/// override/pilot path so a source that holds a bit across ticks shifts exactly once (the
	/// edge-through-InputOverride trap). <paramref name="groundWheelSpeed"/> (rad/s) feeds the
	/// down-shift over-rev guard.</summary>
	void ApplyShiftRequests( bool up, bool down, bool mode, float groundWheelSpeed )
	{
		if ( mode && !_prevReqMode )
			ToggleShiftMode();
		if ( up && !_prevReqUp )
			TryShiftUp();
		if ( down && !_prevReqDown )
			TryShiftDown( groundWheelSpeed );

		_prevReqUp = up;
		_prevReqDown = down;
		_prevReqMode = mode;
	}

	void ToggleShiftMode()
	{
		Drivetrain.ManualMode = !Drivetrain.ManualMode;
		Log.Info( $"[vp] shiftmode {(Drivetrain.ManualMode ? "MANUAL" : "AUTO")} gear {Drivetrain.Gear}" );
	}

	/// <summary>Cycle the drive mode (assist level) Casual -> Sport -> Sim -> Casual. A HUD assist chip
	/// can read <see cref="Assists"/> and flash on change, so the swap is visible on press.</summary>
	void CycleDriveMode()
	{
		Assists = Assists switch
		{
			AssistLevel.Casual => AssistLevel.Sport,
			AssistLevel.Sport => AssistLevel.Sim,
			_ => AssistLevel.Casual,
		};
		Log.Info( $"[vp] drivemode {Assists.ToString().ToUpper()}" );
	}

	void TryShiftUp()
	{
		// In AUTO a manual shift request is a no-op (the box shifts itself; mode changes only via G).
		if ( !Drivetrain.ManualMode )
			return;
		if ( Drivetrain.ShiftUp() )
			Log.Info( $"[vp] shift UP -> gear {Drivetrain.Gear}" );
	}

	void TryShiftDown( float groundWheelSpeed )
	{
		if ( !Drivetrain.ManualMode )
			return;
		if ( Drivetrain.ShiftDown( groundWheelSpeed ) )
			Log.Info( $"[vp] shift DOWN -> gear {Drivetrain.Gear}" );
		else
			Log.Info( $"[vp] shift DOWN denied gear {Drivetrain.Gear} " +
				$"(predicted {Drivetrain.PredictedDownshiftRpm( groundWheelSpeed ):F0} / redline {Drivetrain.Redline:F0})" );
	}

	void ApplySteering()
	{
		float speedFactor = Math.Clamp( SpeedMs / 22f, 0f, 1f );
		float maxAngle = MathX.Lerp( Definition.MaxSteerAngle, Definition.HighSpeedSteerAngle, speedFactor );
		float angle = Steer * maxAngle;

		foreach ( var wheel in Wheels.Where( w => w.IsSteering ) )
		{
			wheel.SteerAngle = angle;
			wheel.LocalRotation = Rotation.FromYaw( angle );
		}
	}

	float BrakeTorqueFor( VehicleWheel wheel )
	{
		bool isFront = wheel.IsSteering;
		float bias = isFront ? Definition.BrakeBias : 1f - Definition.BrakeBias;
		float torque = Brake * Definition.BrakeTorque * bias * 0.5f; // per wheel (2 per axle)

		if ( Handbrake && wheel.HasHandbrake )
		{
			// Drift-exit soft-lock: when a slip cap is active and this rear is already sliding PAST it
			// (SlipRatio more negative than the cap), withhold the handbrake torque this substep so the
			// wheel spins back up toward the cap — the rears keep rotating and retain lateral authority
			// instead of dead-locking into 60°+ slip angles. Same one-substep-lagged SlipRatio the ABS
			// branch below reads. Default cap -1.0 leaves capActive false, so full lock is byte-identical.
			bool capActive = Definition.HandbrakeSlipCap > -1f;
			if ( !capActive || wheel.SlipRatio > Definition.HandbrakeSlipCap )
				torque += Definition.HandbrakeTorque;
		}

		// ABS: release when the wheel locks under braking (Casual + Sport). Thresholds are
		// per-car dials (see CarDefinition.AbsSlipThreshold).
		if ( Assists != AssistLevel.Sim && torque > 0f && wheel.IsGrounded
			&& wheel.SlipRatio < -Definition.AbsSlipThreshold )
			torque *= Definition.AbsReleaseFactor;

		return torque;
	}

	/// <summary>
	/// Arcade brake assist: extra chassis-level deceleration while braking. The tire
	/// model alone stops at sim rates, which reads as "slow" against arcade expectations.
	/// Capped so it can never reverse the car within a step.
	/// </summary>
	void ApplyBrakeAssist()
	{
		if ( Definition.BrakeAssist <= 0f || Brake < 0.1f || SpeedMs < 0.5f )
			return;

		if ( Wheels.Count( w => w.IsGrounded ) < 2 )
			return;

		var flat = _rigidbody.Velocity.WithZ( 0f );
		if ( flat.IsNearZeroLength )
			return;

		float decel = Definition.BrakeAssist * Brake; // m/s²
		float stopCap = flat.Length * Units.UnitsToMeters / Time.Delta;
		float applied = MathF.Min( decel, stopCap );

		_rigidbody.ApplyForce( -flat.Normal * applied * Definition.Mass * Units.MetersToUnits );
	}

	TimeSince _recoverLog = 999f;

	/// <summary>
	/// Arcade SPIN-RECOVERY assist. After a handbrake flick spins the car ~180°, the player holds full
	/// FORWARD throttle but the car keeps rolling BACKWARDS (its old travel direction) for too long
	/// before the tires pick up and drive it the new way. BrakeAssist can't cover this: with a forward
	/// gear + W held, ReadInput sets Throttle=1, Brake=0 — so the only thing arresting the backward
	/// slide is deep-slip tire tail grip, further scaled down by the friction ellipse sharing with
	/// lateral demand. This adds chassis-level deceleration along -flat-velocity WHENEVER the input
	/// throttle commands the gear's direction while ground velocity along the car's facing opposes it
	/// (the quadrant BrakeAssist's opposing-input→Brake mapping never reaches), fading out via an
	/// opposition ramp as the car rotates to face its motion. Capped by the same never-reverse-within-
	/// a-step stopCap BrakeAssist uses. Sim keeps the raw accepted feel.
	///
	/// Interaction with drift-catch (DriftCatchFactor): drift-catch cuts DRIVETRAIN throttle for ≤0.5 s
	/// after handbrake release while the rear is deeply SIDEWAYS; this reads INPUT throttle and applies
	/// a CHASSIS force. They serve different states — sideways (realign) vs backwards (kill stale
	/// velocity) — and act together in a spin-recovery WITHOUT being merged.
	/// </summary>
	void ApplySpinRecoveryAssist()
	{
		if ( Definition.SpinRecoveryAssist <= 0f || Assists == AssistLevel.Sim )
			return;

		// Throttle is the gear-direction INPUT throttle magnitude set in ReadInput (before the
		// drift-catch / TC drivetrain governors scale it) — exactly the raw request this assist wants.
		if ( Throttle <= 0.1f )
			return;

		if ( Wheels.Count( w => w.IsGrounded ) < 2 )
			return;

		var flat = _rigidbody.Velocity.WithZ( 0f );
		float planarSpeed = flat.Length * Units.UnitsToMeters;
		if ( planarSpeed < 0.5f )
			return;

		// velocity along the car's facing. commandedDir folds forward/reverse gear into one sign: in a
		// forward gear the throttle commands +forward, so a NEGATIVE forwardSpeed (still sliding
		// backwards under forward throttle) is the uncovered case; in reverse gear it commands
		// -forward, so a POSITIVE forwardSpeed (reverse throttle while still rolling forward) mirrors it.
		float forwardSpeed = Vector3.Dot( _rigidbody.Velocity * Units.UnitsToMeters, WorldRotation.Forward );
		float commandedDir = Drivetrain.Gear >= 0 ? 1f : -1f;
		float alongCommanded = forwardSpeed * commandedDir; // <0 ⇒ velocity opposes the throttle direction
		if ( alongCommanded > -0.5f )
			return; // already moving the commanded way (or ~stopped) — nothing to recover

		// opposition ramp: fraction of speed pointing the wrong way — 1 when velocity fully opposes
		// facing, fading to 0 as the car rotates to face its motion (so the assist bows out on its own).
		float oppositionRamp = Math.Clamp( -alongCommanded / planarSpeed, 0f, 1f );

		float decel = Definition.SpinRecoveryAssist * oppositionRamp; // m/s²
		float stopCap = planarSpeed / Time.Delta; // never reverse the flat velocity within a step
		float applied = MathF.Min( decel, stopCap );

		_rigidbody.ApplyForce( -flat.Normal * applied * Definition.Mass * Units.MetersToUnits );

		// throttled ~2 Hz while active — parseable for free-drive sessions.
		if ( _recoverLog > 0.5f )
		{
			_recoverLog = 0f;
			Log.Info( $"[vp] recover v {planarSpeed * 3.6f:F0}kmh along {alongCommanded:F1}m/s ramp {oppositionRamp:F2} decel {applied:F1}m/s2" );
		}
	}

	// ── drift-catch assist ──
	// The measured drift-exit anatomy: on handbrake release the driver goes to full throttle while
	// the rear slip angle is still 60°+, so the drive torque spends the rear tires' friction ellipse
	// LONGITUDINALLY exactly when every newton of lateral force is needed to realign the velocity
	// vector ("stuck sliding sideways" + rearK spike + auto-downshift in the telemetry).
	// For a short window after the handbrake releases, while the rear is still deeply sideways, cut
	// throttle-induced rear slip (ramping to a full cut by DriftCatchFullCutDeg) so the ellipse serves
	// realignment first — mirrors real drift-catch technique (wait for the catch before power).
	// Casual + Sport only; Sim stays the raw accepted feel. The 20° floor keeps deliberate
	// power-oversteer (jturn rotation, small-angle slides) untouched.
	const float DriftCatchWindowS = 0.5f;    // seconds after hb release the assist can act
	const float DriftCatchStartDeg = 20f;    // rear slip angle where the cut starts
	const float DriftCatchFullCutDeg = 35f;  // rear slip angle at/past which throttle is fully cut

	bool _wasHandbrake;
	TimeSince _sinceHandbrakeRelease = 999f;

	float DriftCatchFactor()
	{
		if ( Assists == AssistLevel.Sim || Handbrake )
			return 1f;
		if ( _sinceHandbrakeRelease > DriftCatchWindowS )
			return 1f;

		var rears = Wheels.Where( w => !w.IsSteering && w.IsGrounded ).ToList();
		if ( rears.Count == 0 )
			return 1f;

		float rearADeg = MathF.Abs( rears.Average( w => w.SlipAngle ) ).RadianToDegree();
		if ( rearADeg <= DriftCatchStartDeg )
			return 1f;

		return Math.Clamp( 1f - (rearADeg - DriftCatchStartDeg) / (DriftCatchFullCutDeg - DriftCatchStartDeg), 0f, 1f );
	}

	float ApplyTractionControl( float throttle, List<VehicleWheel> driven )
	{
		// drifting is throttle-steered — TC clamping wheelspin would kill the slide
		if ( Handbrake || Assists == AssistLevel.Sim || throttle <= 0f )
			return throttle;

		// proportional: hold driven-wheel slip near a target. Casual holds the grip PEAK; Sport (when the
		// car opts in via SportTcSlipTarget) holds a LOOSER target well past the peak so the rears still
		// break into a throttle-steerable slide, but bounded so they can't free-spin to redline and torch
		// rear lateral grip (the Sport spin-out, owner 2026-07-21). The longitudinal tire-curve peaks sit
		// at slip 0.09-0.14; a 0.25 target parks the tire in the post-peak slide — slower (less grip than
		// the peak) AND permanently over the wheelspin threshold. Casual 0.14 targets the peak: more launch
		// grip and slip under the counter. Sport, not opted in (target 0), returns raw throttle — the old
		// "ABS only" behavior, byte-identical.
		float slipTarget;
		if ( Assists == AssistLevel.Casual )
			slipTarget = 0.14f;
		else if ( Definition.SportTcSlipTarget > 0f )
			slipTarget = Definition.SportTcSlipTarget;
		else
			return throttle;

		float worstSlip = driven.Where( w => w.IsGrounded ).Select( w => w.SlipRatio ).DefaultIfEmpty( 0f ).Max();
		if ( worstSlip <= slipTarget )
			return throttle;

		// TC floor relaxation (kart cap-camping fix 2026-07-18): the flat 0.2 throttle floor still fed
		// enough torque to sustain a spinning rear on a light car (260 kg kart), so TC could not
		// arrest the wheelspin that pins a collapsing-corner rear far past the grip peak (the "stuck
		// turning" bug, offline: this is the decisive lever, cutting sustained rear slip 3.2 -> 0.4).
		// Once slip is deep past the tail (the longitudinal curve reaches its tail by slip 0.40;
		// TcFloorRelaxStart 1.0 is well beyond it), fade the floor toward 0 so the proportional
		// response can cut throttle to near-zero. Below TcFloorRelaxStart the floor stays 0.2 so all
		// below-threshold behavior is byte-identical.
		const float TcFloorRelaxStart = 1.0f;
		const float TcFloorRelaxEnd = 2.5f;
		float floor = 0.2f;
		if ( worstSlip > TcFloorRelaxStart )
			floor *= Math.Clamp( (TcFloorRelaxEnd - worstSlip) / (TcFloorRelaxEnd - TcFloorRelaxStart), 0f, 1f );

		return throttle * Math.Clamp( slipTarget / worstSlip, floor, 1f );
	}

	/// <summary>
	/// Airborne pitch-rate damping time constant, seconds. Leaving a ramp lip pivots the car over
	/// its rear axle, so every launch departs rotating nose-down (19-46 deg/s measured); with no
	/// air management the car rotates through the whole flight and lands nose-first, digging in.
	/// Flight-recorder capture (2026-07-21, Lad2 at 35.5 m/s): launch attitude 8.6 deg nose-UP,
	/// landing attitude 17.8 deg nose-DOWN, touchdown 35.5 to 31.7 m/s in 40 ms (a ~5 g spike)
	/// then pitch slammed level in 100 ms - the owner's "hitch going off the ramps", reported
	/// identically at every speed because the lip pivot exists at every speed. Damping the
	/// car-local pitch rate while fully airborne holds the launch attitude so the car lands
	/// wheels-matched (slightly tail-first). 0 or negative disables. LIVE-UNVERIFIED.
	/// </summary>
	[Property] public float AirPitchDampTau { get; set; } = 0.30f;

	void ApplyAirAttitudeAssist()
	{
		if ( AirPitchDampTau <= 0f )
			return;

		// fully airborne only: any grounded wheel means suspension owns attitude and this
		// assist is inert, so grounded driving is byte-identical by construction
		foreach ( var w in Wheels )
			if ( w.IsGrounded )
				return;
		if ( Wheels.Count == 0 )
			return;

		// the drift button doubles as "let me rotate" in the air: deliberate flips stay possible
		if ( Handbrake )
			return;

		var local = WorldRotation.Inverse * _rigidbody.AngularVelocity;
		local.y *= MathF.Exp( -Time.Delta / AirPitchDampTau );
		_rigidbody.AngularVelocity = WorldRotation * local;
	}

	void ApplyStabilityAssist()
	{
		// the drift button asks for yaw — don't damp it away while held
		if ( Handbrake )
			return;

		// Casual damps at full authority; Sport (when the car opts in via SportStabilityScale) damps at a
		// FRACTION so the counter-steer pendulum snap (rear regains grip and flings the car the other way,
		// uncatchable — owner 2026-07-21) is bled off while deliberate rotation survives. Sim, and Sport
		// with no opt-in, get nothing. Casual multiplies by exactly 1f so its behavior is byte-identical.
		float authority;
		if ( Assists == AssistLevel.Casual )
			authority = 1f;
		else if ( Assists == AssistLevel.Sport && Definition.SportStabilityScale > 0f )
			authority = Definition.SportStabilityScale;
		else
			return;

		// small corrective action when the rear steps out — damp yaw so slides are catchable
		// instead of divergent (the lift-off L-R flick spin)
		var rears = Wheels.Where( w => !w.IsSteering && w.IsGrounded ).ToList();
		if ( rears.Count == 0 )
			return;

		float rearAlpha = MathF.Abs( rears.Average( w => w.SlipAngle ) );
		if ( rearAlpha < 0.07f ) // ~4 degrees
			return;

		// scales up with speed: the flat 3f cap let a 115 km/h lift-off flick go full 360
		float speedScale = 3f + 3f * Math.Clamp( SpeedMs / 30f, 0f, 1f );
		float strength = Math.Clamp( (rearAlpha - 0.07f) * 8f, 0f, 1f ) * speedScale * authority;
		var angular = _rigidbody.AngularVelocity;
		angular.z *= MathF.Max( 0f, 1f - strength * Time.Delta );
		_rigidbody.AngularVelocity = angular;
	}

	// ── wall-glance forgiveness assist ──
	// Detection surface = the chassis Rigidbody's collision callbacks (Component.ICollisionListener;
	// the ONLY runtime chassis-contact API: OnCollisionStart/Update/Stop carry Sandbox.Collision with
	// .Contact.Point/.Contact.Normal and .Other). The chassis box rests ABOVE the wheels' contact
	// zone, so it never touches flat ground — these fire only on real obstacle contact (wall, cone,
	// bottom-out). We latch ONLY near-horizontal normals (true walls); ground/ramps/banks have
	// near-vertical normals and are ignored.
	const float WallNormalZMax = 0.5f;   // |normal.z| < this ⇒ surface within ~30° of vertical = a wall
	// engage as soon as a wall contact is confirmed (a corner-wedge kills speed in a few frames, so a
	// multi-frame gate arrives after the dead-stop it's meant to prevent). A stray ≤1-frame contact
	// still can't engage: the streak must reach this AND the car must be moving INTO the surface above
	// the speed floor.
	const int WallEngageTicks = 1;
	const float WallGlanceMinSpeed = 3f; // m/s planar floor below which forgiveness is pointless

	Vector3 _wallNormalH;          // horizontal unit wall normal from the most recent contact
	TimeSince _sinceWallContact = 999f;
	int _wallStreak;
	TimeSince _wallGlanceLog = 999f;

	public void OnCollisionStart( Collision o ) => NoteWallContact( o );
	public void OnCollisionUpdate( Collision o ) => NoteWallContact( o );
	public void OnCollisionStop( CollisionStop o ) { }

	void NoteWallContact( Collision c )
	{
		if ( IsProxy )
			return;

		var n = c.Contact.Normal;
		// a wall = near-horizontal contact normal (surface within ~30° of vertical). Ground/ramps/
		// banks report near-vertical normals and never latch, so this can't fire driving over terrain.
		if ( MathF.Abs( n.z ) >= WallNormalZMax )
			return;

		var nH = n.WithZ( 0f );
		if ( nH.IsNearZeroLength )
			return;

		_wallNormalH = nH.Normal;
		_sinceWallContact = 0f;
	}

	/// <summary>
	/// Forgiveness for angled (esp. mid-drift) wall contact: instead of a dead stop, re-project
	/// velocity onto the wall tangent (keeping <see cref="CarDefinition.WallScrubFactor"/> of speed)
	/// and gently yaw the heading to run parallel — both scaled by incidence so HEAD-ON hits stay
	/// hard stops. An assist: gated on <see cref="CarDefinition.WallGlanceAssist"/> and Assists != Sim
	/// (Sim = the accepted raw feel). Runs in OnFixedUpdate BEFORE the physics step, so the solver then
	/// resolves the redirected velocity without penetration — same write-then-step pattern as the
	/// brake/stability assists.
	/// </summary>
	void ApplyWallGlanceAssist()
	{
		bool contactNow = _sinceWallContact <= Time.Delta * 1.5f;
		if ( !contactNow )
		{
			_wallStreak = 0;
			return;
		}
		_wallStreak++;

		if ( Definition is null || !Definition.WallGlanceAssist || Assists == AssistLevel.Sim )
			return;
		if ( _wallStreak < WallEngageTicks )
			return;

		var vel = _rigidbody.Velocity;
		var planar = vel.WithZ( 0f );
		float planarSpeedMs = planar.Length * Units.UnitsToMeters;
		if ( planarSpeedMs < WallGlanceMinSpeed )
			return;

		var nH = _wallNormalH;
		float into = Vector3.Dot( planar.Normal, nH ); // <0 ⇒ moving INTO the wall
		if ( into >= -0.01f )
			return; // already sliding along / peeling away — nothing to catch

		// incidence between velocity and the wall PLANE: 0° = grazing, 90° = straight-on
		float incidenceDeg = MathF.Asin( Math.Clamp( MathF.Abs( into ), 0f, 1f ) ).RadianToDegree();

		// full assist below shallow, none at/above head-on (frontal crashes keep the hard stop)
		float scale = incidenceDeg >= Definition.WallGlanceHeadOnDeg ? 0f
			: incidenceDeg <= Definition.WallGlanceShallowDeg ? 1f
			: (Definition.WallGlanceHeadOnDeg - incidenceDeg)
				/ MathF.Max( 1e-3f, Definition.WallGlanceHeadOnDeg - Definition.WallGlanceShallowDeg );
		if ( scale <= 0f )
			return;

		// tangent = the velocity component that runs ALONG the wall (the slide direction)
		var vTangent = planar - Vector3.Dot( planar, nH ) * nH;
		if ( vTangent.IsNearZeroLength )
			return;
		var tangent = vTangent.Normal;

		// (a) re-project velocity onto the tangent: kill the into-wall component (which would wedge
		// the rigid chassis corner and dead-stop it) and carry the slide forward at
		// max(natural tangential speed, WallScrubFactor·total). Once the car is redirected along the
		// wall the target equals the current tangential speed, so it slides steadily instead of
		// grinding to a halt. Blended by incidence so a near head-on keeps the physics dead-stop.
		float totalSpeed = planar.Length;
		float targetSpeed = MathF.Min( totalSpeed,
			MathF.Max( vTangent.Length, totalSpeed * Definition.WallScrubFactor ) );
		var newPlanar = Vector3.Lerp( planar, tangent * targetSpeed, scale );
		_rigidbody.Velocity = newPlanar.WithZ( vel.z );

		// (b) gentle yaw torque aligning heading to the wall tangent (whichever way the car faces)
		var fwd = WorldRotation.Forward.WithZ( 0f ).Normal;
		var alignTo = Vector3.Dot( fwd, tangent ) < 0f ? -tangent : tangent;
		float cross = fwd.x * alignTo.y - fwd.y * alignTo.x; // +z ⇒ target is to the LEFT (CCW)
		float yawErrRad = MathF.Atan2( cross, Vector3.Dot( fwd, alignTo ) );
		var ang = _rigidbody.AngularVelocity;
		ang.z = MathX.Lerp( ang.z, yawErrRad * Definition.WallAlignStrength,
			Math.Clamp( Definition.WallAlignStrength * scale * Time.Delta, 0f, 1f ) );
		_rigidbody.AngularVelocity = ang;

		// throttled so a multi-tick slide doesn't flood the log
		if ( _wallGlanceLog > 0.2f )
		{
			_wallGlanceLog = 0f;
			Log.Info( $"[vp] wallglance inc {incidenceDeg:F0}deg scale {scale:F2} v {planarSpeedMs * 3.6f:F0}->{newPlanar.Length * Units.UnitsToMeters * 3.6f:F0}kmh" );
		}
	}

	public void Respawn()
	{
		WorldPosition = _spawnPosition + Vector3.Up * 20f;
		WorldRotation = _spawnRotation;
		_rigidbody.Velocity = Vector3.Zero;
		_rigidbody.AngularVelocity = Vector3.Zero;
	}
}
fieldguide.vehiclephysics / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Vehicle Physics Kit" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "vehiclephysics" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "fieldguide" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "fieldguide.vehiclephysics" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "28" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-07-22T03:31:24.7233296Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.120.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.120.0")]
fieldguide.vehiclephysics / Code/TireCurve.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Parametric peaked tire curve (Pacejka-shaped, authored by peak/tail points).
/// Rises smoothly to a grip peak, then falls off to a sliding asymptote.
/// Input is |slip| — slip ratio (unitless) for longitudinal, slip angle (radians) for lateral.
/// Output is a grip coefficient multiplied by tire load to get force.
/// </summary>
public struct TireCurve
{
	/// <summary>Slip value where grip peaks (κ ≈ 0.08–0.12, α ≈ 6–9° in radians).</summary>
	public float PeakSlip { get; set; }

	/// <summary>Grip coefficient at the peak.</summary>
	public float PeakGrip { get; set; }

	/// <summary>Slip value where the curve has fully fallen to the tail.</summary>
	public float TailSlip { get; set; }

	/// <summary>Grip coefficient when fully sliding (typically 75–85% of peak).</summary>
	public float TailGrip { get; set; }

	public TireCurve( float peakSlip, float peakGrip, float tailSlip, float tailGrip )
	{
		PeakSlip = peakSlip;
		PeakGrip = peakGrip;
		TailSlip = tailSlip;
		TailGrip = tailGrip;
	}

	public readonly float Evaluate( float slip )
	{
		slip = MathF.Abs( slip );

		if ( slip <= PeakSlip )
		{
			// parabolic rise with zero slope at the peak
			float n = slip / PeakSlip;
			return PeakGrip * n * (2f - n);
		}

		// smoothstep decay from peak down to the tail
		float t = Math.Clamp( (slip - PeakSlip) / (TailSlip - PeakSlip), 0f, 1f );
		t = t * t * (3f - 2f * t);
		return PeakGrip + (TailGrip - PeakGrip) * t;
	}

	public static TireCurve Street => new( 0.10f, 1.00f, 0.45f, 0.80f );
	public static TireCurve Sport => new( 0.09f, 1.15f, 0.40f, 0.92f );
	public static TireCurve Offroad => new( 0.14f, 0.90f, 0.60f, 0.75f );
}
fieldguide.vehiclephysics / TireCurve.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Parametric peaked tire curve (Pacejka-shaped, authored by peak/tail points).
/// Rises smoothly to a grip peak, then falls off to a sliding asymptote.
/// Input is |slip| — slip ratio (unitless) for longitudinal, slip angle (radians) for lateral.
/// Output is a grip coefficient multiplied by tire load to get force.
/// </summary>
public struct TireCurve
{
	/// <summary>Slip value where grip peaks (κ ≈ 0.08–0.12, α ≈ 6–9° in radians).</summary>
	public float PeakSlip { get; set; }

	/// <summary>Grip coefficient at the peak.</summary>
	public float PeakGrip { get; set; }

	/// <summary>Slip value where the curve has fully fallen to the tail.</summary>
	public float TailSlip { get; set; }

	/// <summary>Grip coefficient when fully sliding (typically 75–85% of peak).</summary>
	public float TailGrip { get; set; }

	public TireCurve( float peakSlip, float peakGrip, float tailSlip, float tailGrip )
	{
		PeakSlip = peakSlip;
		PeakGrip = peakGrip;
		TailSlip = tailSlip;
		TailGrip = tailGrip;
	}

	public readonly float Evaluate( float slip )
	{
		slip = MathF.Abs( slip );

		if ( slip <= PeakSlip )
		{
			// parabolic rise with zero slope at the peak
			float n = slip / PeakSlip;
			return PeakGrip * n * (2f - n);
		}

		// smoothstep decay from peak down to the tail
		float t = Math.Clamp( (slip - PeakSlip) / (TailSlip - PeakSlip), 0f, 1f );
		t = t * t * (3f - 2f * t);
		return PeakGrip + (TailGrip - PeakGrip) * t;
	}

	public static TireCurve Street => new( 0.10f, 1.00f, 0.45f, 0.80f );
	public static TireCurve Sport => new( 0.09f, 1.15f, 0.40f, 0.92f );
	public static TireCurve Offroad => new( 0.14f, 0.90f, 0.60f, 0.75f );
}
fieldguide.vehiclephysics / WheelVisual.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Spins and vertically tracks the wheel's visual mesh from VehicleWheel state.
/// Steering yaw lives on the wheel GameObject itself (set by VehicleController);
/// spin composes with whatever base orientation the factory gave the visual.
/// </summary>
public sealed class WheelVisual : Component
{
	public VehicleWheel Wheel { get; set; }

	Rotation _baseRotation;
	float _spinDegrees;

	protected override void OnStart()
	{
		_baseRotation = LocalRotation;
	}

	protected override void OnUpdate()
	{
		if ( Wheel is null )
			return;

		_spinDegrees += Wheel.AngularVelocity.RadianToDegree() * Time.Delta;
		_spinDegrees %= 360f;

		// Spin about the model-local +Y axle. FromPitch uses the Source pitch sign (positive pitch
		// tilts the local +X forward vector DOWN), so a POSITIVE angle here rolls the top of the wheel
		// toward the car's +X travel direction, the correct rolling sense for forward motion. The old
		// negated angle rolled the tread backwards while driving forward (community report: "wheels
		// rotate the wrong way"). AngularVelocity is +forward, so the pitch angle is used as-is.
		// Per-frame is correct for SPIN: the accumulator integrates with frame dt, and rotation
		// interpolates in its own buffer independent of the fixed-tick position write below.
		LocalRotation = _baseRotation * Rotation.FromPitch( _spinDegrees );
	}

	/// <summary>
	/// Suspension tracking runs at the FIXED tick, not per frame (ramp-hitch fix, 2026-07-21,
	/// LIVE-UNVERIFIED). <see cref="VehicleWheel.SuspensionLength"/> is a raw physics-tick field:
	/// writing it to LocalPosition per RENDER frame is the documented sawtooth anti-pattern (KB
	/// g-game-camera-follows-raw-fixedtick-feet-model-sawtooths): the body renders engine-interpolated
	/// while the wheels step at 50 Hz, so wheels judder against the body exactly where suspension
	/// length changes fast (ramp faces, transients), worse with speed and refresh rate; a wheel that
	/// unloads for one tick snapped 5-10 cm to full droop and back inside 1-2 frames. Owner
	/// discriminator: fps_max 50 (render rate = tick rate) made the felt ramp hitch "a million times
	/// better". Writing inside OnFixedUpdate lets GameTransform interpolation carry the motion per
	/// frame, exactly like the chassis (KB g-game-manual-visual-smoother-fights-fixedupdate-
	/// interpolation: never hand-smooth what engine interpolation already covers).
	/// </summary>
	protected override void OnFixedUpdate()
	{
		if ( Wheel is null )
			return;

		LocalPosition = Vector3.Down * Wheel.SuspensionLength * Units.MetersToUnits;
	}
}
fieldguide.vehiclephysics / CarDefinition.cs
Game library
namespace FieldGuide.VehiclePhysics;

public enum DriveLayout
{
	FWD,
	RWD,
	AWD
}

public enum BodyStyle
{
	Box,  // sedan/hatch blockout: body + cabin
	Kart  // flat deck + seat back + citizen driver
}

public enum AssistLevel
{
	Casual, // full traction control + yaw-stability damping + ABS
	Sport,  // ABS + optional REDUCED-authority TC / yaw-stability (per-car opt-in; drift feel preserved)
	Sim     // nothing
}

/// <summary>
/// Complete tuning spec for one car. Plain class, instantiated from <see cref="CarDefinitions"/>;
/// promote to a [GameResource] .car asset once an editor workflow is in play so designers can tune
/// without recompiling. All values SI: meters, kg, N, N-m, N/m.
/// </summary>
public class CarDefinition
{
	public string Name { get; set; } = "Car";
	public BodyStyle Style { get; set; } = BodyStyle.Box;
	public bool HasDriver { get; set; }

	/// <summary>Optional opaque body-manifest path for a consumer-supplied custom body builder
	/// (see <see cref="VehicleFactory.CustomBodyBuilder"/>). The kit itself never reads it — with no
	/// custom builder plugged in, the factory builds a primitive blockout body and this value is
	/// inert. It exists so a consumer's builder (e.g. a part-kit assembler) can look up which body to
	/// assemble for this car. null keeps the blockout path byte-for-byte unchanged.</summary>
	public string BodyManifest { get; set; }

	// Chassis
	public float Mass { get; set; } = 1200f;
	public Vector3 BodySize { get; set; } = new( 4.0f, 1.8f, 1.3f ); // length, width, height (m)
	public float Wheelbase { get; set; } = 2.55f;
	public float TrackWidth { get; set; } = 1.55f;
	public float RideHeight { get; set; } = 0.35f; // chassis center above wheel attach plane
	public float GroundClearance { get; set; } = 0.14f; // collider bottom above ground at rest
	public float CenterOfMassDrop { get; set; } = 0.20f; // CoM below chassis center; must stay above wheel plane

	// Wheels & suspension (per wheel)
	public float WheelRadius { get; set; } = 0.31f;
	public float WheelInertia { get; set; } = 1.2f; // kg m^2
	public float SuspensionTravel { get; set; } = 0.20f;
	public float SpringRate { get; set; } = 38000f;
	public float DamperRate { get; set; } = 2800f;

	// Tires
	public TireCurve LongitudinalCurve { get; set; } = TireCurve.Street;
	public TireCurve LateralCurve { get; set; } = new( 0.14f, 1.00f, 0.55f, 0.80f ); // radians: peak ~8 deg
	public float LoadSensitivity { get; set; } = 0.06f;

	// Drivetrain
	public DriveLayout Layout { get; set; } = DriveLayout.FWD;
	// +20% speed pass 2026-07-21: baseline PeakTorque 150->180 and RedlineRpm 6500->7800 so a bare
	// kit consumer starts from the new faster baseline (the demo roster below overrides both per car).
	public float PeakTorque { get; set; } = 180f;
	public float IdleRpm { get; set; } = 900f;
	public float RedlineRpm { get; set; } = 7800f;
	public float EngineInertia { get; set; } = 0.25f; // kg m^2 at crank
	public float EngineBrakeTorque { get; set; } = 40f;
	public float[] GearRatios { get; set; } = { 3.6f, 2.1f, 1.4f, 1.05f, 0.85f };
	public float ReverseRatio { get; set; } = 3.4f;
	public float FinalDrive { get; set; } = 3.9f;
	public float ShiftUpRpm { get; set; } = 5800f;
	public float ShiftDownRpm { get; set; } = 2200f;

	// Engine audio (read by EngineAudio). Per-class dials so a small buzzy kart and a deep
	// sedan/truck share one code path:
	//   EngineSoundEvent     — the LOW-RPM loop this class plays (idle/low, the deep layer).
	//   EngineSoundEventHigh — the HIGH-RPM loop, crossfaded in as revs climb. When null/empty the
	//     class runs the legacy SINGLE-loop model (one handle, wide pitch sweep) — the kart keeps
	//     this so its single synth purr is unchanged. When set, EngineAudio blends the two recorded
	//     layers by RPM, and each layer only pitch-shifts a narrow band around its recorded pitch
	//     (no far stretch = no screech), which is the point of the layered model.
	//   EnginePitchBase      — multiplies the pitch of whichever model runs, so a class reads higher
	//     or lower overall (kart >1 stays buzzy, truck <1 sits deep).
	public string EngineSoundEvent { get; set; } = "sounds/engine/engine_real_low.sound";
	public string EngineSoundEventHigh { get; set; }
	public float EnginePitchBase { get; set; } = 1f;

	// Brakes
	public float BrakeTorque { get; set; } = 2400f; // total, split by bias
	public float BrakeBias { get; set; } = 0.62f;   // fraction to front
	public float HandbrakeTorque { get; set; } = 3000f; // rear wheels only

	// ABS dials (per-car). 0.3/0.55 because telemetry showed the ABS duty cycle, not tire grip, was
	// the brake limiter, and per-car ABS modulation is a legitimate class trait: the locking kart
	// needs a different release than the truck. Active in Casual + Sport.
	public float AbsSlipThreshold { get; set; } = 0.25f; // release when SlipRatio < -this under braking
	public float AbsReleaseFactor { get; set; } = 0.70f; // brake torque multiplier while released

	// Drift-exit soft-lock: cap the handbrake-INDUCED rear slip ratio. When a cap is active and a rear
	// wheel is already sliding PAST it, its handbrake torque is withheld that substep (ABS-style duty
	// cycle) so the rear spins back up toward the cap and keeps some rotation — hence some lateral bite
	// — mid-slide. Default -1.0 = NO effective cap (byte-identical full-lock behavior).
	public float HandbrakeSlipCap { get; set; } = -1.0f;

	// Steering
	public float MaxSteerAngle { get; set; } = 32f; // degrees at standstill
	public float HighSpeedSteerAngle { get; set; } = 8f; // degrees at/above 30 m/s

	// Arcade feel dials (defaults = the original sim-leaning behavior)
	public float SteerRateScale { get; set; } = 1f;    // multiplies how fast steering ramps
	// Reverse speed pass 2026-07-21 (owner feel): raised 4.5 -> 20.0 m/s (~10 -> ~45 mph). This is the
	// ACTUAL reverse cap mechanism (VehicleController cuts reverse throttle above this speed), and the
	// game roster inherits it (only the pickup used to override it). Each car is still additionally
	// limited by its reverse-gear redline-equivalent wheel speed (RedlineRpm/ReverseRatio/FinalDrive/
	// WheelRadius via Drivetrain.RedlineWheelSpeed), so torquey-but-tall reverse gears self-limit below
	// this cap (kart ~8 m/s, pickup ~12 m/s) while the cars reach the high-30s/40s mph.
	public float ReverseSpeedCap { get; set; } = 20.0f; // m/s before reverse throttle cuts
	public float LaunchBoost { get; set; } = 1f;       // torque multiplier at standstill, fades out by ~54 km/h
	public float BrakeAssist { get; set; } = 0f;       // extra chassis-level decel while braking (m/s²)
	// Spin-recovery assist: after a handbrake spin the car keeps rolling BACKWARDS (old travel
	// direction) while the player holds throttle the new way — BrakeAssist can't help there (forward
	// throttle sets Brake=0), so nothing arcade-level arrests the stale velocity. This is extra
	// chassis-level decel along -velocity, applied ONLY when input throttle opposes the ground velocity
	// along the car's facing, fading out as the car rotates to face its motion
	// (VehicleController.ApplySpinRecoveryAssist). Same m/s² unit + never-reverse-within-a-step cap as
	// BrakeAssist; gated Assists != Sim. 0 disables.
	public float SpinRecoveryAssist { get; set; } = 0f; // extra chassis decel killing stale opposing velocity (m/s²)
	public float HandbrakeGripScale { get; set; } = 1f; // rear grip multiplier while handbrake held (<1 = drift button)

	// Wall-glance forgiveness assist: a sustained near-horizontal chassis contact while moving
	// re-projects velocity along the wall tangent and gently yaws the heading to match, scaled by
	// incidence (VehicleController.ApplyWallGlanceAssist). Active only when Assists != Sim. Head-on hits
	// (incidence >= WallGlanceHeadOnDeg) get NO assist — frontal crashes stay hard stops.
	public bool WallGlanceAssist { get; set; } = true;
	public float WallScrubFactor { get; set; } = 0.75f;    // fraction of speed kept along the wall tangent on a shallow graze
	public float WallGlanceShallowDeg { get; set; } = 35f; // at/below this velocity-vs-wall incidence: full assist
	public float WallGlanceHeadOnDeg { get; set; } = 60f;  // at/above this incidence: no assist (hard stop preserved)
	public float WallAlignStrength { get; set; } = 6f;     // yaw-align rate toward the wall tangent (per second)

	// ── Sport-mode stability posture (owner call 2026-07-21) ──
	// Sport historically ran with NO traction control and NO yaw-stability damping (both Casual-only),
	// so a full-throttle RWD car spun the rears to redline and the counter-steer pendulum went divergent
	// — uncatchable spin-outs. These give Sport a REDUCED-authority version of each assist, opt-in per
	// car. Both default to 0 (disabled), so any car that doesn't set them keeps the raw "ABS only" Sport
	// and — critically — Casual and Sim stay byte-identical (the controller multiplies Casual authority
	// by exactly 1 and returns early for Sim, unchanged).

	// Sport traction control. When >0, Sport runs the same proportional TC as Casual
	// (VehicleController.ApplyTractionControl) but holds driven-wheel slip near THIS ratio instead of the
	// Casual 0.14 grip-peak target. Set it LOOSER than the peak (e.g. 0.30-0.45) so the rears still break
	// into a throttle-steerable slide (drift stays alive) while the redline free-spin that torches rear
	// lateral grip is capped. 0 = no Sport TC (raw wheelspin, the old behavior).
	public float SportTcSlipTarget { get; set; } = 0f;

	// Sport yaw-stability. When >0, Sport runs the yaw-rate damper (VehicleController.ApplyStabilityAssist)
	// at THIS fraction of the Casual authority (0-1). It bleeds the stored yaw rate that snaps a
	// counter-steered slide the OTHER way (the pendulum spin-out) so slides stay CATCHABLE, without
	// killing deliberate rotation the way full Casual authority would. 0 = no Sport yaw damping.
	public float SportStabilityScale { get; set; } = 0f;

	// Driver seated pose (citizen animgraph; sit enum: 0 none, 1-3 chair poses, 4-5 ground poses).
	// Defaults = the original upright chair pose. Made per-car so a recumbent kart driver — legs
	// extended forward to the pedals — can be authored without disturbing any upright-seated car.
	public int DriverSit { get; set; } = 1;
	public float DriverSitOffsetHeight { get; set; } = 4f;

	// Defaults
	public AssistLevel DefaultAssists { get; set; } = AssistLevel.Casual;
	public Color Tint { get; set; } = new( 0.85f, 0.55f, 0.35f );
}

/// <summary>Car roster. Hatch is the default/first car; Kart and Coupe round out the roster.
/// These are blockout-body demo definitions — physics is identical whether a car renders as a
/// primitive blockout or a consumer-supplied custom body.</summary>
public static class CarDefinitions
{
	/// <summary>The default/first roster car — an ORANGE hot hatch. FWD, mid-power, street tires.</summary>
	public static CarDefinition Hatch => new()
	{
		Name = "Compact Hatch",
		Mass = 1150f,
		BodySize = new Vector3( 3.9f, 1.75f, 1.45f ),
		Wheelbase = 2.55f,
		TrackWidth = 1.50f,
		WheelRadius = 0.30f,
		Layout = DriveLayout.FWD,
		// +20% speed pass 2026-07-21: PeakTorque 162->194.4, RedlineRpm 6300->7560 (top-gear top speed +20%).
		PeakTorque = 194.4f,
		RedlineRpm = 7560f,
		// Real recorded car set: deep muscle idle crossfaded up to a revving high layer.
		EngineSoundEvent = "sounds/engine/engine_real_low.sound",
		EngineSoundEventHigh = "sounds/engine/engine_real_high.sound",
		LongitudinalCurve = new TireCurve( 0.10f, 1.35f, 0.45f, 1.08f ),
		BrakeTorque = 4300f,
		LateralCurve = new TireCurve( 0.14f, 1.30f, 0.55f, 1.04f ),
		HandbrakeGripScale = 0.55f, // the drift button — rear grip cut while handbrake held; also the FWD J-turn lever
		SpinRecoveryAssist = 7.0f,
		HighSpeedSteerAngle = 9.5f,
		SpringRate = 34000f,
		DamperRate = 2500f,
		Tint = new Color( 0.93f, 0.42f, 0.03f ), // orange
	};

	/// <summary>Full-size pickup. Heaviest in roster, RWD, torquey low-rev engine, longer-travel
	/// softer suspension, offroad-leaning tires, high ride. Signature strength = hill grade.</summary>
	public static CarDefinition Pickup => new()
	{
		Name = "Utility Pickup",
		Mass = 1900f,
		BodySize = new Vector3( 5.2f, 1.95f, 1.55f ),
		Wheelbase = 3.40f,
		TrackWidth = 1.70f,
		RideHeight = 0.44f,          // high ride: the class signature (hatch 0.35)
		GroundClearance = 0.24f,
		CenterOfMassDrop = 0.15f,    // higher CoM than the cars = truck-like roll; track 1.70 keeps rollover margin
		WheelRadius = 0.35f,
		WheelInertia = 2.4f,
		SuspensionTravel = 0.26f,    // longest in roster — washboard/offroad strength
		SpringRate = 42000f,
		DamperRate = 3400f,
		LongitudinalCurve = new TireCurve( 0.14f, 1.25f, 0.60f, 1.05f ),
		LateralCurve = new TireCurve( 0.15f, 1.22f, 0.60f, 1.06f ),
		LoadSensitivity = 0.07f,
		Layout = DriveLayout.RWD,
		// +20% speed pass 2026-07-21: PeakTorque 320->384, RedlineRpm 3900->4680 (top-gear top speed +20%).
		PeakTorque = 384f,           // torquey: >2× hatch; strong hills
		IdleRpm = 650f,
		RedlineRpm = 4680f,          // lowest redline in roster; low-rev truck character (was 3900, +20% pass)
		EngineInertia = 0.5f,
		EngineBrakeTorque = 90f,     // strong engine braking downhill
		// Real recorded truck set: deeper truck idle + revving high layer; base pitch dropped.
		EngineSoundEvent = "sounds/engine/engine_real_truck_low.sound",
		EngineSoundEventHigh = "sounds/engine/engine_real_truck_high.sound",
		EnginePitchBase = 0.85f,
		GearRatios = new[] { 3.8f, 2.3f, 1.5f, 1.1f, 0.85f },
		ReverseRatio = 3.8f,
		FinalDrive = 3.9f,
		ShiftUpRpm = 3500f,
		ShiftDownRpm = 1700f,
		BrakeTorque = 7000f,
		BrakeBias = 0.65f,           // unladen bed = light rear axle
		HandbrakeTorque = 5000f,
		MaxSteerAngle = 27f,         // slow truck steering; long wheelbase stabilizes
		HighSpeedSteerAngle = 8f,
		SteerRateScale = 0.9f,
		// Reverse cap override removed 2026-07-21: inherits the new 20 m/s default; the pickup's tall reverse
		// gear + low redline self-limit it to ~12 m/s (~26 mph) reverse, so it stays the roster's slowest
		// reverse by gearing character rather than an artificial ~9 mph clamp.
		HandbrakeGripScale = 0.45f,  // deepest rear cut in the roster — 1900 kg + 3.4 m wheelbase needs real help
		SpinRecoveryAssist = 7.0f,
		SportTcSlipTarget = 0.30f,   // Sport keeps the torquey RWD truck from lighting the rears to redline
		SportStabilityScale = 0.5f,  // half-authority yaw damp so a Sport slide stays catchable
		DefaultAssists = AssistLevel.Casual,
		Tint = new Color( 0.55f, 0.13f, 0.11f ), // dark brick red
	};

	public static CarDefinition Kart => new()
	{
		Name = "Go-Kart",
		Style = BodyStyle.Kart,
		HasDriver = true,
		Mass = 260f,
		BodySize = new Vector3( 1.9f, 1.15f, 0.30f ),
		Wheelbase = 1.55f,
		TrackWidth = 1.14f,
		RideHeight = 0.17f,
		GroundClearance = 0.08f,
		CenterOfMassDrop = 0.02f, // tiny chassis: a deep drop puts CoM below the wheels
		WheelRadius = 0.16f,
		WheelInertia = 0.18f,
		SuspensionTravel = 0.14f,
		SpringRate = 24000f,
		DamperRate = 1600f,
		Layout = DriveLayout.RWD,
		// +20% speed pass 2026-07-21: PeakTorque 52->62.4, RedlineRpm 9000->10800 (top-gear top speed +20%).
		PeakTorque = 62.4f, // punchy launch, still shifts out of wheelspin quickly
		IdleRpm = 1400f,
		RedlineRpm = 10800f,
		EngineInertia = 0.05f,
		EngineBrakeTorque = 8f,
		// The kart keeps the buzzier single loop; base pitch >1 preserves its whine, while the narrowed
		// band tames the redline chipmunk.
		EngineSoundEvent = "sounds/engine/engine_b_sport_purr.sound",
		EnginePitchBase = 1.2f,
		GearRatios = new[] { 3.4f, 2.4f, 1.8f, 1.4f, 1.1f },
		ReverseRatio = 3.4f,
		FinalDrive = 6.3f,
		ShiftUpRpm = 8000f,
		ShiftDownRpm = 4000f,
		BrakeTorque = 560f,
		HandbrakeTorque = 900f,
		MaxSteerAngle = 31f,
		HighSpeedSteerAngle = 9f,
		// sticky kart slicks
		LongitudinalCurve = new TireCurve( 0.09f, 1.55f, 0.40f, 1.24f ),
		LateralCurve = new TireCurve( 0.12f, 1.66f, 0.45f, 1.32f ),
		AbsSlipThreshold = 0.20f, // earlier release for the lockup-prone kart
		HandbrakeGripScale = 0.70f, // mild rear cut — the light kart already rotates
		SpinRecoveryAssist = 7.0f,
		SportTcSlipTarget = 0.40f,  // loosest Sport TC — the light kart is meant to be playful
		SportStabilityScale = 0.45f, // gentle yaw damp; the kart rotates easily so keep it light
		HandbrakeSlipCap = -0.7f, // keep the rears rotating mid-slide for a cleaner drift exit
		// Recumbent kart driver pose: reclines with legs extended forward to the pedals.
		DriverSit = 4,
		DriverSitOffsetHeight = 0f,
		DefaultAssists = AssistLevel.Casual, // default fun car: keep it catchable
		Tint = new Color( 0.58f, 0.83f, 0.07f ), // acid green
	};

	public static CarDefinition Coupe => new()
	{
		Name = "Sports Coupe",
		Mass = 1420f,
		BodySize = new Vector3( 4.4f, 1.85f, 1.25f ),
		Wheelbase = 2.7f,
		TrackWidth = 1.60f,
		Layout = DriveLayout.RWD,
		// +20% speed pass 2026-07-21: PeakTorque 340->408, RedlineRpm 7200->8640 (top-gear top speed +20%).
		// ShiftUpRpm left at 6600 (unchanged) so the shift ladder holds in ground-speed terms; only the
		// top gear extends to the new redline.
		PeakTorque = 408f,
		RedlineRpm = 8640f,
		ShiftUpRpm = 6600f,
		// Real recorded car set (shared with the hatch): deep muscle idle crossfaded up to a
		// revving high layer; base pitch neutral so it sweeps the band cleanly.
		EngineSoundEvent = "sounds/engine/engine_real_low.sound",
		EngineSoundEventHigh = "sounds/engine/engine_real_high.sound",
		EnginePitchBase = 1.0f,
		LongitudinalCurve = new TireCurve( 0.09f, 1.50f, 0.40f, 1.20f ),
		LateralCurve = new TireCurve( 0.13f, 1.69f, 0.50f, 1.36f ),
		HandbrakeGripScale = 0.55f, // rears break loose under handbrake (J-turn initiation)
		HandbrakeSlipCap = -0.7f,
		SpringRate = 46000f,
		DamperRate = 3600f,
		BrakeTorque = 6200f,
		WheelRadius = 0.33f,
		SpinRecoveryAssist = 7.0f,
		SportTcSlipTarget = 0.35f,   // Sport TC: rears still slide on throttle but can't free-spin to redline
		SportStabilityScale = 0.5f,  // half-authority yaw damp: the counter-steer pendulum stays catchable
		MaxSteerAngle = 30f,
		HighSpeedSteerAngle = 10f, // sportiest car gets the most high-speed turn-in
		Tint = new Color( 0.80f, 0.05f, 0.07f ), // bright signal red
	};
}
fieldguide.vehiclephysics / Code/WheelVisual.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Spins and vertically tracks the wheel's visual mesh from VehicleWheel state.
/// Steering yaw lives on the wheel GameObject itself (set by VehicleController);
/// spin composes with whatever base orientation the factory gave the visual.
/// </summary>
public sealed class WheelVisual : Component
{
	public VehicleWheel Wheel { get; set; }

	Rotation _baseRotation;
	float _spinDegrees;

	protected override void OnStart()
	{
		_baseRotation = LocalRotation;
	}

	protected override void OnUpdate()
	{
		if ( Wheel is null )
			return;

		_spinDegrees += Wheel.AngularVelocity.RadianToDegree() * Time.Delta;
		_spinDegrees %= 360f;

		// Spin about the model-local +Y axle. FromPitch uses the Source pitch sign (positive pitch
		// tilts the local +X forward vector DOWN), so a POSITIVE angle here rolls the top of the wheel
		// toward the car's +X travel direction, the correct rolling sense for forward motion. The old
		// negated angle rolled the tread backwards while driving forward (community report: "wheels
		// rotate the wrong way"). AngularVelocity is +forward, so the pitch angle is used as-is.
		// Per-frame is correct for SPIN: the accumulator integrates with frame dt, and rotation
		// interpolates in its own buffer independent of the fixed-tick position write below.
		LocalRotation = _baseRotation * Rotation.FromPitch( _spinDegrees );
	}

	/// <summary>
	/// Suspension tracking runs at the FIXED tick, not per frame (ramp-hitch fix, 2026-07-21,
	/// LIVE-UNVERIFIED). <see cref="VehicleWheel.SuspensionLength"/> is a raw physics-tick field:
	/// writing it to LocalPosition per RENDER frame is the documented sawtooth anti-pattern (KB
	/// g-game-camera-follows-raw-fixedtick-feet-model-sawtooths): the body renders engine-interpolated
	/// while the wheels step at 50 Hz, so wheels judder against the body exactly where suspension
	/// length changes fast (ramp faces, transients), worse with speed and refresh rate; a wheel that
	/// unloads for one tick snapped 5-10 cm to full droop and back inside 1-2 frames. Owner
	/// discriminator: fps_max 50 (render rate = tick rate) made the felt ramp hitch "a million times
	/// better". Writing inside OnFixedUpdate lets GameTransform interpolation carry the motion per
	/// frame, exactly like the chassis (KB g-game-manual-visual-smoother-fights-fixedupdate-
	/// interpolation: never hand-smooth what engine interpolation already covers).
	/// </summary>
	protected override void OnFixedUpdate()
	{
		if ( Wheel is null )
			return;

		LocalPosition = Vector3.Down * Wheel.SuspensionLength * Units.MetersToUnits;
	}
}
fieldguide.vehiclephysics / Demo/DemoBootstrap.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Minimal demo bootstrap for the Vehicle Physics Kit's demo scene. Spawns one blockout car per
/// <see cref="CarDefinitions"/> roster entry in a row on the flat pad, and points the scene's
/// <see cref="VehicleCamera"/> at the first one. Proves the kit drives in isolation; not part of the
/// consumer surface (the demo scene is the only thing that references it).
/// </summary>
public sealed class DemoBootstrap : Component
{
	/// <summary>Spacing between spawned cars along the row (metres).</summary>
	[Property] public float SpacingMeters { get; set; } = 5f;

	/// <summary>Cars falling below this world height (metres) are placed back on their spawn spot.</summary>
	[Property] public float VoidResetHeightM { get; set; } = -20f;

	/// <summary>Held by every non-active car: neutral pedals, handbrake on. A parked car should be parked.</summary>
	static DriveInputs ParkedInputs => new() { Handbrake = true };

	readonly List<VehicleController> _cars = new();
	readonly List<Vector3> _spawns = new();
	VehicleController _active;
	VehicleCamera _camera;
	DemoTuningPanel _panel;
	GameObject _hud;

	protected override void OnStart()
	{
		// ~1.1 g like the source proving ground, so the tuned handling feels right. Read live by the
		// factory's static-load and seat-height math, so setting it here keeps every car level on spawn.
		if ( Scene.PhysicsWorld is not null )
			Scene.PhysicsWorld.Gravity = Vector3.Down * 9.81f * 1.1f * Units.MetersToUnits;

		var roster = new[]
		{
			CarDefinitions.Hatch,
			CarDefinitions.Coupe,
			CarDefinitions.Kart,
			CarDefinitions.Pickup,
		};

		float m = Units.MetersToUnits;
		float startY = -(roster.Length - 1) * SpacingMeters * 0.5f;

		_cars.Clear();
		_spawns.Clear();
		for ( int i = 0; i < roster.Length; i++ )
		{
			var def = roster[i];
			float seatZ = VehicleFactory.SeatHeightM( def );
			var pos = new Vector3( 0f, startY + i * SpacingMeters, seatZ ) * m;

			var go = VehicleFactory.Spawn( Scene, def, pos, Rotation.Identity );
			var controller = go.Components.Get<VehicleController>();
			if ( controller is not null )
			{
				// Park it IMMEDIATELY, before its first physics tick can sample a live device
				// (a resting gamepad trigger past deadzone reads as brake and latches reverse).
				controller.InputOverride = ParkedInputs;
				_cars.Add( controller );
				_spawns.Add( pos );
			}
		}

		_active = _cars.FirstOrDefault();
		if ( _active is not null )
			_active.InputOverride = null;

		// Point the scene's chase camera at the active car so the demo frames it on load.
		_camera = Scene.GetAllComponents<VehicleCamera>().FirstOrDefault();
		if ( _camera is not null )
			_camera.Target = _active;

		MountTuningLab();
	}

	protected override void OnUpdate()
	{
		if ( _cars.Count < 2 )
			return;

		// Bracket cycling, mirroring Vehicle Prototyping's bindings ( [ / ] , d-pad left/right ).
		// OnFixedUpdate's per-tick override pass handles the rest: the old car parks (handbrake),
		// the new one starts listening to the player.
		int step = Input.Pressed( "CycleNext" ) ? 1 : Input.Pressed( "CyclePrev" ) ? -1 : 0;
		if ( step == 0 )
			return;

		int i = _cars.IndexOf( _active );
		i = ((i + step) % _cars.Count + _cars.Count) % _cars.Count;
		_active = _cars[i];

		if ( _camera is not null )
			_camera.Target = _active;
		if ( _panel is not null )
			_panel.Car = _active; // rebinds the lab and re-snapshots stock values for the new car
	}

	/// <summary>Stand up the demo-layer live tuning lab: a ScreenPanel-hosted <see cref="DemoTuningPanel"/>
	/// bound to the active car, and the camera's cursor-yield seam pointed at its open state. This exists
	/// only in the demo scene; consumers that spawn their own cars never get it. It doubles as a working
	/// example of the <see cref="VehicleCamera.CursorModalOpen"/> seam being consumed from the demo side.</summary>
	void MountTuningLab()
	{
		if ( _active is null )
			return;

		// Start collapsed to the chip legend: an open lab on spawn captured the cursor before
		// players ever drove (owner call, 2026-07-19). The chip keeps T discoverable; reset the
		// static flag here each session.
		DemoTuningPanel.IsOpen = false;

		_hud = Scene.CreateObject();
		_hud.Name = "Tuning HUD";
		_hud.Components.GetOrCreate<ScreenPanel>();

		_panel = _hud.Components.Create<DemoTuningPanel>();
		_panel.Car = _active;

		// Dogfood the kit's cursor-yield seam: while the lab is open the chase camera must release the
		// cursor so the player can click dials. This is exactly what the seam is for; wiring it here
		// proves it to consumers.
		VehicleCamera.CursorModalOpen = () => DemoTuningPanel.IsOpen;
	}

	protected override void OnFixedUpdate()
	{
		float m = Units.MetersToUnits;

		for ( int i = 0; i < _cars.Count; i++ )
		{
			var car = _cars[i];
			if ( car is null || !car.IsValid() )
				continue;

			// Only the camera's car listens to the player; parked cars hold the handbrake.
			// (With no override every controller samples the same keyboard, so one W press
			// would launch the whole row at once; without the handbrake a parked car is
			// free-rolling and a device blip or spawn settle can walk it off its mark.)
			car.InputOverride = car == _active ? null : ParkedInputs;

			// Void watchdog: driving off the pad edge otherwise means falling forever.
			if ( car.GameObject.WorldPosition.z < VoidResetHeightM * m )
			{
				car.GameObject.WorldPosition = _spawns[i];
				car.GameObject.WorldRotation = Rotation.Identity;
				var body = car.GameObject.Components.Get<Rigidbody>();
				if ( body is not null )
				{
					body.Velocity = Vector3.Zero;
					body.AngularVelocity = Vector3.Zero;
				}
			}
		}
	}
}
fieldguide.vehiclephysics / Demo/DemoTuningPanel.razor
Game library
@namespace FieldGuide.VehiclePhysics
@inherits PanelComponent

@* Kit-native live tuning lab (demo layer). Toggled by the Tune action, bound to the car the chase
   camera is following. Writes tuning changes straight onto the running car through the same paths a
   consumer would use: it mutates the active CarDefinition (read live by the drivetrain and the brake
   model) and pushes suspension/tire values onto the live wheels. This is a demo-scale lab, not the
   full game panel: it exposes the highest-feel dials so the demo reads as a physics lab in the first
   minute. Replace it with your own UI; it lives only in the demo scene. *@

<root>
	@if ( Car.IsValid() )
	{
		@* Key legend, top-right: how to hop between the demo cars. Always visible; the tuning
		   chip/panel top-left covers the T binding. *@
		<div class="legend">
			<div class="mono kbd">[ ]</div>
			<div class="title">switch car</div>
		</div>
	}
	@if ( !IsOpen && Car.IsValid() )
	{
		@* Collapsed legend chip: sits exactly where the expanded panel's top-left corner lands, so
		   pressing T reads as the chip expanding into the lab. Mouse-look stays with the camera. *@
		<div class="panel chip" onclick=@ToggleOpen>
			<div class="head">
				<div class="mono kbd">@ToggleKeyLabel</div>
				<div class="title">tuning</div>
			</div>
		</div>
	}
	@if ( IsOpen && Car.IsValid() )
	{
		<div class="panel">
			<div class="head">
				<div class="title">Tuning lab · @CarName</div>
				<div class="mono kbd">@ToggleKeyLabel</div>
			</div>

			<div class="hint">Tuning the car you are driving. Changes apply live.</div>

			<div class="dials">
				@foreach ( var d in SliderDials )
				{
					var dd = d;
					<div class="dial">
						<div class="drow">
							<div class="dname">@d.Name</div>
							<div class="mono dval">@d.Display()</div>
						</div>
						<div class="dctl">
							<div class="mono step" onclick=@( () => Step( dd, -1 ) )>-</div>
							<div class="track"
								onmousedown=@( e => Scrub( dd, e, true ) )
								onmousemove=@( e => Scrub( dd, e, false ) )>
								<div class="fill" style="width: @WidthPct( dd )%"></div>
							</div>
							<div class="mono step" onclick=@( () => Step( dd, +1 ) )>+</div>
						</div>
					</div>
				}

				<div class="dial">
					<div class="drow">
						<div class="dname">Assists</div>
						<div class="mono cycle" onclick=@CycleAssists>@AssistLabel</div>
					</div>
				</div>

				<div class="dial">
					<div class="drow">
						<div class="dname">Tires</div>
						<div class="mono cycle" onclick=@CycleTires>@TireLabel</div>
					</div>
				</div>
			</div>

			<div class="foot">
				<div class="btn" onclick=@ResetToStock>Reset to stock</div>
			</div>
		</div>
	}
</root>

@code {
	/// <summary>The car this panel tunes: the one the chase camera follows. Set by DemoBootstrap. On
	/// change the panel snapshots the car's pristine (authored) values so Reset-to-stock can restore
	/// them and the grip/torque multipliers rebase to the new car.</summary>
	VehicleController _car;
	public VehicleController Car
	{
		get => _car;
		set
		{
			if ( ReferenceEquals( _car, value ) )
				return;
			_car = value;
			Snapshot();
		}
	}

	/// <summary>Open state. Static so the kit's chase camera can read it through the
	/// <see cref="VehicleCamera.CursorModalOpen"/> seam (DemoBootstrap wires that) without holding a
	/// reference to this panel. The demo runs exactly one panel, so a single flag is enough.</summary>
	public static bool IsOpen;

	// Toggle input action. Documented in the README input table; the host ProjectSettings/Input.config
	// ships it. Never Escape or an F-key.
	const string ToggleAction = "Tune";
	string ToggleKeyLabel => "T";

	string CarName => Car?.Definition?.Name ?? "Car";

	// Live multipliers over the pristine base. Grip scales the (preset-selected) tire curves; torque
	// scales the authored peak engine torque.
	float _gripScale = 1f;
	float _torqueScale = 1f;
	int _tirePreset; // 0 Stock, 1 Street, 2 Sport, 3 Offroad

	// Pristine snapshot, captured value-by-value when the car is bound (its definition is untouched at
	// that point). Value types only (floats, TireCurve struct, enum), so later live tuning of the
	// definition can never corrupt these; Reset restores from here.
	bool _snapped;
	float _stockPeakTorque, _stockSpring, _stockDamper, _stockTravel, _stockBrake;
	TireCurve _stockLat, _stockLong;
	AssistLevel _stockAssists;

	// Current UNSCALED tire base (Stock or a named preset). Grip multiplies these to get the live curves.
	TireCurve _baseLat, _baseLong;

	void Snapshot()
	{
		var def = _car?.Definition;
		_dials = null;
		if ( def is null )
		{
			_snapped = false;
			return;
		}

		_stockPeakTorque = def.PeakTorque;
		_stockSpring = def.SpringRate;
		_stockDamper = def.DamperRate;
		_stockTravel = def.SuspensionTravel;
		_stockBrake = def.BrakeTorque;
		_stockLat = def.LateralCurve;
		_stockLong = def.LongitudinalCurve;
		// Stock assist = the authored definition default. Read it off the definition, not the
		// controller: the controller adopts DefaultAssists in its own OnStart, which may run a frame
		// or two after this bind, so its live Assists is not reliable yet.
		_stockAssists = def.DefaultAssists;

		_baseLat = _stockLat;
		_baseLong = _stockLong;
		_gripScale = 1f;
		_torqueScale = 1f;
		_tirePreset = 0;
		_snapped = true;
		StateHasChanged();
	}

	// ---- dial model ----
	class Dial
	{
		public string Name;
		public float Min, Max, Step;
		public Func<float> Get;
		public Action<float> Set;
		public Func<string> Fmt;
		public string Display() => Fmt();
	}

	List<Dial> _dials;
	List<Dial> SliderDials => _dials ??= BuildDials();

	List<Dial> BuildDials()
	{
		if ( !Car.IsValid() )
			return new List<Dial>();

		var def = Car.Definition;
		return new List<Dial>
		{
			new()
			{
				Name = "Grip", Min = 0.6f, Max = 2.2f, Step = 0.05f,
				Get = () => _gripScale, Set = SetGrip,
				Fmt = () => _gripScale.ToString( "0.00" ) + "x",
			},
			new()
			{
				Name = "Drive torque", Min = 0.5f, Max = 2.0f, Step = 0.05f,
				Get = () => _torqueScale, Set = SetTorqueScale,
				Fmt = () => _torqueScale.ToString( "0.00" ) + "x",
			},
			new()
			{
				Name = "Suspension stiffness", Min = 15000f, Max = 60000f, Step = 2000f,
				Get = () => def.SpringRate, Set = v => { def.SpringRate = v; ApplyWheels(); },
				Fmt = () => def.SpringRate.ToString( "0" ) + " N/m",
			},
			new()
			{
				Name = "Suspension damping", Min = 800f, Max = 6000f, Step = 200f,
				Get = () => def.DamperRate, Set = v => { def.DamperRate = v; ApplyWheels(); },
				Fmt = () => def.DamperRate.ToString( "0" ) + " Ns/m",
			},
			new()
			{
				Name = "Suspension travel", Min = 0.10f, Max = 0.35f, Step = 0.01f,
				Get = () => def.SuspensionTravel, Set = v => { def.SuspensionTravel = v; ApplyWheels(); },
				Fmt = () => (def.SuspensionTravel * 100f).ToString( "0" ) + " cm",
			},
			new()
			{
				Name = "Brake force", Min = 1500f, Max = 8000f, Step = 200f,
				Get = () => def.BrakeTorque, Set = v => def.BrakeTorque = v,
				Fmt = () => def.BrakeTorque.ToString( "0" ) + " Nm",
			},
		};
	}

	float Frac( Dial d )
	{
		if ( d.Max <= d.Min )
			return 0f;
		return Math.Clamp( (d.Get() - d.Min) / (d.Max - d.Min), 0f, 1f );
	}

	string WidthPct( Dial d ) =>
		(Frac( d ) * 100f).ToString( "0.#", System.Globalization.CultureInfo.InvariantCulture );

	void Step( Dial d, int clicks )
	{
		float v = Math.Clamp( d.Get() + clicks * d.Step, d.Min, d.Max );
		d.Set( v );
		StateHasChanged();
	}

	// Click-to-jump + drag-to-scrub on the dial track (mirrors the game panel's proven pattern). jump
	// is true on mousedown (a bare click sets the value at the cursor) and false on mousemove (scrub
	// only while the track owns the press). Value = mouse local x over track width, snapped to the
	// dial's step, pushed through the SAME Set path as the +/- steps.
	void Scrub( Dial d, Sandbox.UI.PanelEvent ev, bool jump )
	{
		if ( ev is not Sandbox.UI.MousePanelEvent e )
			return;

		var track = e.This;
		if ( track is null )
			return;

		// mousemove fires whether or not the button is held; only scrub while the track owns the press.
		if ( !jump && !track.PseudoClass.HasFlag( Sandbox.UI.PseudoClass.Active ) )
			return;

		float w = track.Box.Rect.Width;
		if ( w <= 0f )
			return;

		float frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );
		float v = d.Min + frac * (d.Max - d.Min);
		if ( d.Step > 0f )
			v = MathF.Round( v / d.Step ) * d.Step;
		v = Math.Clamp( v, d.Min, d.Max );
		d.Set( v );
		StateHasChanged();
	}

	// ---- apply paths (same seams a consumer would use) ----
	static TireCurve Scaled( TireCurve c, float k ) =>
		new( c.PeakSlip, c.PeakGrip * k, c.TailSlip, c.TailGrip * k );

	// Push the definition's suspension + tire values onto the live wheels. The factory copies these at
	// spawn; the wheel re-reads them every substep, so writing them here is the live-apply path.
	void ApplyWheels()
	{
		var def = Car.Definition;
		foreach ( var w in Car.Wheels )
		{
			w.SpringRate = def.SpringRate;
			w.DamperRate = def.DamperRate;
			w.SuspensionTravel = def.SuspensionTravel;
			w.LateralCurve = def.LateralCurve;
			w.LongitudinalCurve = def.LongitudinalCurve;
		}
	}

	void SetGrip( float k )
	{
		_gripScale = k;
		var def = Car.Definition;
		def.LateralCurve = Scaled( _baseLat, k );
		def.LongitudinalCurve = Scaled( _baseLong, k );
		ApplyWheels();
	}

	// Drive torque scale multiplies the authored peak torque. Drivetrain reads def.PeakTorque live
	// (it holds the same definition instance), so no drivetrain touch is needed.
	void SetTorqueScale( float k )
	{
		_torqueScale = k;
		Car.Definition.PeakTorque = _stockPeakTorque * k;
	}

	void CycleAssists()
	{
		Car.Assists = Car.Assists switch
		{
			AssistLevel.Casual => AssistLevel.Sport,
			AssistLevel.Sport => AssistLevel.Sim,
			_ => AssistLevel.Casual,
		};
		StateHasChanged();
	}

	string AssistLabel => Car.IsValid() ? Car.Assists.ToString() : "";

	// Tire preset swaps the UNSCALED base curves, then re-applies the current grip multiplier so the
	// grip dial and the preset compose. Stock restores the car's own authored curves.
	void CycleTires()
	{
		_tirePreset = (_tirePreset + 1) % 4;
		(_baseLat, _baseLong) = _tirePreset switch
		{
			1 => (TireCurve.Street, TireCurve.Street),
			2 => (TireCurve.Sport, TireCurve.Sport),
			3 => (TireCurve.Offroad, TireCurve.Offroad),
			_ => (_stockLat, _stockLong),
		};
		SetGrip( _gripScale );
		StateHasChanged();
	}

	string TireLabel => _tirePreset switch
	{
		1 => "Street",
		2 => "Sport",
		3 => "Offroad",
		_ => "Stock",
	};

	// Re-apply the car's pristine authored values (captured at bind). Definitions in the demo roster
	// are fresh per car, so "stock" is unambiguous: the values this car spawned with.
	void ResetToStock()
	{
		if ( !Car.IsValid() || !_snapped )
			return;

		var def = Car.Definition;
		def.PeakTorque = _stockPeakTorque;
		def.SpringRate = _stockSpring;
		def.DamperRate = _stockDamper;
		def.SuspensionTravel = _stockTravel;
		def.BrakeTorque = _stockBrake;
		def.LateralCurve = _stockLat;
		def.LongitudinalCurve = _stockLong;

		_gripScale = 1f;
		_torqueScale = 1f;
		_tirePreset = 0;
		_baseLat = _stockLat;
		_baseLong = _stockLong;

		Car.Assists = _stockAssists;
		ApplyWheels();
		StateHasChanged();
	}

	protected override void OnEnabled()
	{
		// Start COLLAPSED: the chip legend keeps the keybind discoverable while mouse-look stays
		// with the camera (an open lab on spawn captured the cursor before players ever drove,
		// owner call 2026-07-19). Static flag, so reset it here each session.
		IsOpen = false;
	}

	void ToggleOpen()
	{
		IsOpen = !IsOpen;
		Mouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
		StateHasChanged();
	}

	protected override void OnUpdate()
	{
		if ( Input.Pressed( ToggleAction ) )
		{
			IsOpen = !IsOpen;
			Mouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;
			StateHasChanged();
		}

		// The chase camera re-hides the cursor every frame while it owns it; hold it visible while open.
		if ( IsOpen )
			Mouse.Visibility = MouseVisibility.Visible;
	}

	protected override int BuildHash()
	{
		// Closed still renders the chip legend, and the chip waits on the car binding, so the
		// hash must move when the car arrives or the first build would stick on the empty tree.
		if ( !IsOpen )
			return Car.IsValid() ? 1 : 0;

		var h = new HashCode();
		h.Add( _gripScale );
		h.Add( _torqueScale );
		h.Add( _tirePreset );
		if ( Car.IsValid() )
		{
			var def = Car.Definition;
			h.Add( def.SpringRate );
			h.Add( def.DamperRate );
			h.Add( def.SuspensionTravel );
			h.Add( def.BrakeTorque );
			h.Add( (int)Car.Assists );
		}
		return h.ToHashCode();
	}
}
fieldguide.vehiclephysics / Code/Demo/DemoTuningPanel.razor.scss
Game library
// Kit-native live tuning lab (demo layer). Compact left-anchored card; s&box panels default to flex
// so no display rules are needed (which also keeps this free of inline-flex / inline-block). All text
// is bright: instructional copy never uses dim gray.

DemoTuningPanel {
	position: absolute;
	inset: 0;
	pointer-events: none;
	font-family: Poppins, sans-serif;

	.mono { font-family: "Roboto Mono", monospace; }

	// Top-right key legend (car switching). Mirrors the chip's card language at the opposite corner.
	.legend {
		position: absolute;
		right: 40px;
		top: 40px;
		flex-direction: row;
		align-items: center;
		gap: 8px;
		padding: 10px 14px;
		background-color: rgba( 15, 17, 21, 0.92 );
		border: 1px solid rgba( 255, 255, 255, 0.08 );
		border-radius: 16px;
		box-shadow: 0 20px 60px rgba( 0, 0, 0, 0.6 );

		.title { font-weight: 600; color: #F2F4F7; }
		.kbd {
			color: #F2F4F7;
			border: 1px solid rgba( 255, 255, 255, 0.12 );
			border-radius: 5px;
			padding: 3px 8px;
			background-color: rgba( 255, 255, 255, 0.05 );
		}
	}

	.panel {
		position: absolute;
		left: 40px;
		top: 40px;
		width: 360px;
		flex-direction: column;
		gap: 12px;
		padding: 20px;
		background-color: rgba( 15, 17, 21, 0.92 );
		border: 1px solid rgba( 255, 255, 255, 0.08 );
		border-radius: 16px;
		box-shadow: 0 20px 60px rgba( 0, 0, 0, 0.6 );
		pointer-events: all;

		.head {
			flex-direction: row;
			justify-content: space-between;
			align-items: center;

			.title { font-weight: 700; color: #F2F4F7; }
			.kbd {
				color: #F2F4F7;
				border: 1px solid rgba( 255, 255, 255, 0.12 );
				border-radius: 5px;
				padding: 3px 8px;
				background-color: rgba( 255, 255, 255, 0.05 );
			}
		}

		.hint { color: #F2F4F7; }

		// Collapsed legend chip: same anchor as the expanded card (left/top 40px via .panel), sized
		// to its content so it reads as the card's top-left corner waiting to grow.
		&.chip {
			width: auto;
			padding: 10px 14px;
			gap: 0;

			.head {
				gap: 8px;
				justify-content: flex-start;

				.title { font-weight: 600; color: #F2F4F7; }
			}
		}

		.dials {
			flex-direction: column;
			gap: 12px;

			.dial { flex-direction: column; gap: 6px; }

			.drow {
				flex-direction: row;
				justify-content: space-between;
				align-items: center;

				.dname { color: #F2F4F7; }
				.dval { color: #E8EAED; }
			}

			.dctl {
				flex-direction: row;
				align-items: center;
				gap: 10px;

				.step {
					width: 28px;
					height: 28px;
					justify-content: center;
					align-items: center;
					color: #F2F4F7;
					background-color: rgba( 255, 255, 255, 0.06 );
					border: 1px solid rgba( 255, 255, 255, 0.10 );
					border-radius: 6px;
					cursor: pointer;
					transition: all 0.12s ease;

					&:hover { color: #0A161A; background-color: #4AD9F2; }
				}

				.track {
					// Click-to-jump + drag-to-scrub hit box. Events must land on the track (not the fill)
					// so the mouse local position stays track-relative: pointer-events all here, none on
					// the fill.
					position: relative;
					flex-grow: 1;
					height: 14px;
					border-radius: 99px;
					background-color: rgba( 255, 255, 255, 0.12 );
					pointer-events: all;
					cursor: pointer;

					.fill {
						position: absolute;
						left: 0px;
						height: 100%;
						border-radius: 99px;
						background: linear-gradient( to right, #3AC4DE, #4AD9F2 );
						pointer-events: none;
					}
				}
			}

			.cycle {
				justify-content: center;
				align-items: center;
				font-weight: 600;
				color: #0A161A;
				background-color: #4AD9F2;
				border-radius: 6px;
				padding: 5px 14px;
				cursor: pointer;
				transition: all 0.12s ease;

				&:hover { background-color: #58E1F7; }
			}
		}

		.foot {
			flex-direction: column;
			gap: 8px;

			.btn {
				justify-content: center;
				align-items: center;
				font-weight: 700;
				color: #F2F4F7;
				background-color: rgba( 255, 255, 255, 0.06 );
				border: 1px solid rgba( 255, 255, 255, 0.10 );
				border-radius: 10px;
				padding: 11px 0;
				cursor: pointer;
				transition: all 0.12s ease;

				&:hover { color: #E8EAED; background-color: rgba( 255, 255, 255, 0.10 ); }
			}
		}
	}
}
fieldguide.vehiclephysics / Code/DriveInputs.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Per-frame driver intent, the value-struct swap point at VehicleController's input seam
/// (<see cref="VehicleController.InputOverride"/>). Carries the raw intent the controller samples
/// from a device: the move axis (forward/back + steer), the handbrake action, and the sequential
/// shift requests. A keyboard/gamepad/wheel source or a scripted source (a test pilot, an AI) all
/// produce one of these, so the controller's gear/reverse/steer-ramp logic stays source-agnostic.
///
/// The live-device sampler <see cref="SampleDeviceInputs"/> and its gamepad shaping helpers live
/// here (lifted out of the controller): the controller either consumes an injected override or calls
/// this sampler, and nothing else in the class touches device input.
/// </summary>
public struct DriveInputs
{
	/// <summary>-1..1 signed drive axis: +forward accelerates, -back brakes then engages reverse near a
	/// stop. Built in <see cref="SampleDeviceInputs"/> as (throttle − brake) where each channel is the
	/// MAX of the keyboard/stick component (<c>Input.AnalogMove.x</c>) and the ANALOG gamepad trigger
	/// pull (<c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c>, 0..1) — so a partial trigger
	/// gives a partial pedal. A scripted source sets this float directly.</summary>
	public float MoveForward;

	/// <summary>-1..1, maps to <c>Input.AnalogMove.y</c> (gamepad path reshaped by the gamepad deadzone
	/// + response curve). Note <c>Rotation.FromYaw(+)</c> is a LEFT/CCW turn, so +Steer steers left.</summary>
	public float Steer;

	/// <summary>The handbrake / drift button: keyboard "Jump" (Space), or gamepad A (Jump's own
	/// GamepadCode) or the left bumper ("Handbrake" action).</summary>
	public bool Handbrake;

	/// <summary>Edge-triggered request to shift UP one gear (sequential MANUAL mode). Keyboard E /
	/// gamepad R1 while live; a scripted source pulses it for one tick. The controller rising-edge-
	/// detects it, so a source that holds it across ticks still shifts exactly once.</summary>
	public bool ShiftUp;

	/// <summary>Edge-triggered request to shift DOWN one gear (sequential MANUAL mode). Keyboard Q /
	/// gamepad L1. Same one-shot rising-edge semantics as <see cref="ShiftUp"/>.</summary>
	public bool ShiftDown;

	/// <summary>Edge-triggered request to toggle the transmission mode AUTO↔MANUAL. Keyboard G /
	/// gamepad D-pad down. Same one-shot rising-edge semantics as <see cref="ShiftUp"/>.</summary>
	public bool ShiftModeToggle;

	// Gamepad tier: deadzone + response curve for the analog steer axis.
	const float GamepadSteerDeadzone = 0.12f;
	const float GamepadSteerCurvePower = 1.6f; // >1 softens the center for fine control, still reaches full lock

	// Analog throttle/brake tier: a small trigger deadzone so resting triggers can't creep the pedals.
	// The engine ALSO applies its own 12.5% deadzone one layer down (Controller.SetAxis zeroes any
	// |axis| <= 0.125 before Input.GetAnalog sees it), so values arrive as 0 or >~0.125 and this floor
	// is a belt-and-suspenders guard that also rescales so full pull still reaches 1.0.
	const float GamepadTriggerDeadzone = 0.05f;

	/// <summary>Sample the live input devices into a DriveInputs value (this is the keyboard/gamepad
	/// source; other sources produce the same struct and set <see cref="VehicleController.InputOverride"/>).
	///
	/// Steering rides <c>Input.AnalogMove.y</c> straight off the left stick; keyboard emits exact
	/// -1/0/1 through the same path and passes through <see cref="ApplyGamepadSteerCurve"/> unchanged.
	///
	/// Throttle/brake are VARIABLE per device: keyboard W/S ride <c>Input.AnalogMove.x</c> as an exact
	/// ±1 digital forward/back; the gamepad triggers are read as a true ANALOG 0..1 pull via
	/// <c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c> (right = gas, left = brake). The two
	/// devices combine per channel by MAX — so either device works and neither fights the other — then
	/// the net (throttle − brake) folds into the single signed <see cref="MoveForward"/> scalar. On
	/// keyboard <c>Input.GetAnalog</c> returns 0, so keyboard-only players are byte-identical.</summary>
	public static DriveInputs SampleDeviceInputs()
	{
		var move = Input.AnalogMove;

		// keyboard/stick forward+back split off the shared move axis (W = +x, S = -x)
		float keyThrottle = MathF.Max( 0f, move.x );
		float keyBrake = MathF.Max( 0f, -move.x );

		// gamepad triggers, true analog 0..1 (right = gas, left = brake)
		float triggerThrottle = ReadTrigger( InputAnalog.RightTrigger );
		float triggerBrake = ReadTrigger( InputAnalog.LeftTrigger );

		// MAX blend per channel so either device drives the pedal, neither overrides the other
		float throttle = MathF.Max( keyThrottle, triggerThrottle );
		float brake = MathF.Max( keyBrake, triggerBrake );
		float moveForward = Math.Clamp( throttle - brake, -1f, 1f );

		return new DriveInputs
		{
			MoveForward = moveForward,
			Steer = ApplyGamepadSteerCurve( move.y ),
			Handbrake = Input.Down( "Jump" ) || Input.Down( "Handbrake" ),
		};
	}

	/// <summary>Read a gamepad trigger as a linear 0..1 pull with a small deadzone floor (rescaled so
	/// full pull still reaches 1.0). <c>Input.GetAnalog</c> already returns 0 for a trigger on keyboard,
	/// so this only shapes the gamepad path.</summary>
	static float ReadTrigger( InputAnalog trigger )
	{
		float v = Math.Clamp( Input.GetAnalog( trigger ), 0f, 1f );
		if ( v < GamepadTriggerDeadzone )
			return 0f;
		return (v - GamepadTriggerDeadzone) / (1f - GamepadTriggerDeadzone);
	}

	/// <summary>Deadzone + power curve for the analog steer axis. Values under the deadzone snap to 0;
	/// the remaining range is rescaled so full stick deflection still reaches ±1 (no lost lock), then
	/// raised to <see cref="GamepadSteerCurvePower"/> for a softer center. Keyboard's exact -1/0/1
	/// passes through unaffected (0 is inside the deadzone; 1 rescales to 1 and 1^n == 1).</summary>
	static float ApplyGamepadSteerCurve( float raw )
	{
		float mag = MathF.Abs( raw );
		if ( mag < GamepadSteerDeadzone )
			return 0f;

		float t = Math.Clamp( (mag - GamepadSteerDeadzone) / (1f - GamepadSteerDeadzone), 0f, 1f );
		return MathF.Sign( raw ) * MathF.Pow( t, GamepadSteerCurvePower );
	}
}
fieldguide.vehiclephysics / Code/Units.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// All vehicle math is done in SI units (meters, kg, N); convert only at the engine boundary.
/// s&amp;box uses Source-style inch units.
/// </summary>
public static class Units
{
	public const float MetersToUnits = 39.37f;
	public const float UnitsToMeters = 1f / MetersToUnits;
}
fieldguide.vehiclephysics / Code/VehicleCamera.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Chase camera: spring-arm follow behind the car's flattened heading, FOV widens with speed.
/// Mouse orbits around the car; after a few seconds without mouse input it eases back to the chase
/// position. Mouse wheel zooms distance.
/// </summary>
public sealed class VehicleCamera : Component
{
	/// <summary>
	/// Seam for "a UI cursor modal is currently open" (spec 3.1). Drive mode needs a locked cursor
	/// or AnalogLook stays at zero, so this camera hides the cursor every frame — UNLESS a consumer's
	/// UI has a modal up that owns the cursor. Plug that check in here; null (default) means the kit
	/// never yields the cursor (single-window driving with no UI modals). Null-safe: unset reads false.
	/// </summary>
	public static Func<bool> CursorModalOpen { get; set; }

	static bool AnyCursorModalOpen
	{
		get
		{
			try
			{
				return CursorModalOpen?.Invoke() ?? false;
			}
			catch ( NotImplementedException )
			{
				// Hotload survivor: this static can hold a lambda authored by a REPLACED assembly
				// (s&box hotload copies static state but cannot substitute an orphaned lambda, so
				// invoking it throws). Drop the stale delegate and fall back to "no modal"; whoever
				// owns the seam (the demo bootstrap, or a consumer) re-wires it at session start.
				CursorModalOpen = null;
				return false;
			}
		}
	}

	public VehicleController Target { get; set; }

	[Property] public float Distance { get; set; } = 6.5f; // m
	[Property] public float Height { get; set; } = 2.2f;   // m
	[Property] public float BaseFov { get; set; } = 70f;
	[Property] public float OrbitReturnDelay { get; set; } = 5f;
	[Property] public float MinDistance { get; set; } = 3.0f;  // m — wheel zoom in
	[Property] public float MaxDistance { get; set; } = 14.0f; // m — wheel zoom out
	[Property] public float ZoomPerNotch { get; set; } = 0.85f; // m per wheel tick

	CameraComponent _camera;
	float _orbitYaw;
	float _orbitPitch;
	TimeSince _lastMouseInput = 999f;

	protected override void OnStart()
	{
		_camera = Components.Get<CameraComponent>();
		// Drive mode needs a locked cursor or AnalogLook stays at zero. UI modals unlock
		// it; we re-lock every frame when none are open (panels only set Visible while up).
		if ( !AnyCursorModalOpen )
			Mouse.Visibility = MouseVisibility.Hidden;
	}

	protected override void OnUpdate()
	{
		if ( Target is null || !Target.IsValid() )
			return;

		bool uiOwnsCursor = AnyCursorModalOpen;
		if ( !uiOwnsCursor )
		{
			// Re-assert every frame — panel dismiss paths miss edge cases, and the engine
			// can resurface the cursor after focus changes.
			Mouse.Visibility = MouseVisibility.Hidden;

			var look = Input.AnalogLook;
			if ( MathF.Abs( look.yaw ) > 0.05f || MathF.Abs( look.pitch ) > 0.05f )
			{
				_lastMouseInput = 0;
				_orbitYaw += look.yaw;
				_orbitPitch = Math.Clamp( _orbitPitch + look.pitch, -30f, 12f );
			}

			// Scroll up = closer (smaller Distance). Ignore while UI has the cursor.
			float scroll = Input.MouseWheel.y;
			if ( MathF.Abs( scroll ) > 0.01f )
				Distance = Math.Clamp( Distance - scroll * ZoomPerNotch, MinDistance, MaxDistance );
		}

		if ( !uiOwnsCursor && _lastMouseInput > OrbitReturnDelay )
		{
			// ease back behind the car
			float decay = 1f - MathF.Exp( -3f * Time.Delta );
			_orbitYaw = MathX.Lerp( _orbitYaw, 0f, decay );
			_orbitPitch = MathX.Lerp( _orbitPitch, 0f, decay );
		}

		float m = Units.MetersToUnits;
		var car = Target.GameObject;

		// follow the flattened heading so rolls/flips don't whip the camera
		var flatForward = car.WorldRotation.Forward.WithZ( 0f );
		flatForward = flatForward.IsNearZeroLength ? Vector3.Forward : flatForward.Normal;
		float baseYaw = MathF.Atan2( flatForward.y, flatForward.x ).RadianToDegree();

		var orbit = Rotation.From( _orbitPitch, baseYaw + _orbitYaw, 0f );

		float speedStretch = Math.Clamp( Target.SpeedMs / 40f, 0f, 1f );
		var wantedPos = car.WorldPosition
			- orbit.Forward * (Distance + speedStretch * 1.5f) * m
			+ Vector3.Up * Height * m;

		float smooth = 1f - MathF.Exp( -8f * Time.Delta );
		WorldPosition = Vector3.Lerp( WorldPosition, wantedPos, smooth );

		var lookTarget = car.WorldPosition + Vector3.Up * 0.8f * m + orbit.Forward * 1.5f * m;
		WorldRotation = Rotation.LookAt( (lookTarget - WorldPosition).Normal );

		if ( _camera.IsValid() )
			_camera.FieldOfView = BaseFov + speedStretch * 15f;
	}
}
fieldguide.vehiclephysics / VehicleWheel.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// One wheel: shapecast ground detection + spring/damper suspension (skeleton pattern from
/// Facepunch/sbox-libwheel, MIT) with the friction model replaced by the spec §5.2.1 slip
/// physics — wheel angular velocity as integrated state, slip ratio/angle into peaked curves,
/// friction ellipse, load sensitivity, low-speed blend.
///
/// Driven by <see cref="VehicleController"/> in substeps; forces are accumulated across
/// substeps and applied once per fixed update (ApplyForceAt applies for the whole step).
/// All internal math in SI.
/// </summary>
public sealed class VehicleWheel : Component
{
	// Configured by VehicleFactory from CarDefinition
	public float Radius { get; set; } = 0.31f;
	public float Inertia { get; set; } = 1.2f;
	public float SuspensionTravel { get; set; } = 0.18f;
	public float SpringRate { get; set; } = 38000f;
	public float DamperRate { get; set; } = 2800f;
	public TireCurve LongitudinalCurve { get; set; }
	public TireCurve LateralCurve { get; set; }
	public float LoadSensitivity { get; set; } = 0.06f;
	public float StaticLoad { get; set; } = 3000f; // N, set by factory: mass·g/4
	public bool IsSteering { get; set; }
	public bool IsDriven { get; set; }
	public bool HasHandbrake { get; set; }
	public float GripScale { get; set; } = 1f; // live multiplier, e.g. handbrake drift cuts rear grip
	public float ParkBrakeScale { get; set; } = 1f; // anti-jitter stiction strength; controller fades it with throttle

	/// <summary>Per-substep DRIVE-side angular-velocity cap (rad/s), set by the controller each
	/// substep to the drivetrain's redline-equivalent wheel speed for the current gear (see
	/// <see cref="Drivetrain.RedlineWheelSpeed"/>). Drive torque can never push the wheel PAST this
	/// within a substep; the ground (tire reaction) still can, and a pre-existing overspeed is never
	/// yanked down (no phantom braking). float.MaxValue = no cap (undriven wheels, neutral).</summary>
	public float DriveOmegaCap { get; set; } = float.MaxValue;

	// State
	public float AngularVelocity { get; private set; } // rad/s, +forward
	public float SteerAngle { get; set; } // degrees, set by controller
	public float SlipRatio { get; private set; }
	public float SlipAngle { get; private set; } // radians
	public float Load { get; private set; } // N
	public bool IsGrounded => _trace.Hit;
	public float GroundSpeed { get; private set; } // m/s along wheel forward
	public float SuspensionLength { get; private set; } // m, attach point to wheel center, for visuals

	public string DebugTrace => !_trace.Hit
		? "miss"
		: $"{_trace.GameObject?.Name ?? "?"}{(_trace.StartedSolid ? "!SOLID" : "")} d{_trace.Distance:F1}u n{_trace.Normal.z:F2} sf{(_trace.Surface?.Friction ?? -1f):F2}";

	const float TraceSphereRadius = 1f * Units.UnitsToMeters; // shapecast sphere, see DoTrace

	Rigidbody _rigidbody;
	SceneTraceResult _trace;
	Vector3 _accumulatedForce; // N, world space
	Vector3 _forcePosition;
	int _substeps;
	float _smoothedSlipAngle;

	protected override void OnEnabled()
	{
		_rigidbody = Components.GetInAncestorsOrSelf<Rigidbody>();
	}

	/// <summary>Trace and reset accumulators. Call once per fixed update, before substeps.</summary>
	public void BeginStep()
	{
		DoTrace();
		_accumulatedForce = Vector3.Zero;
		_substeps = 0;
	}

	/// <summary>
	/// One physics substep. driveTorque/brakeTorque in N·m. Accumulates chassis force,
	/// integrates wheel spin.
	/// </summary>
	public void Substep( float dt, float driveTorque, float brakeTorque )
	{
		_substeps++;

		var up = _rigidbody.WorldRotation.Up;

		// a grazing contact (car tipped over, wheel scraping a wall) is not suspension:
		// generating spring force there self-propels a flipped car along the ground
		bool validContact = IsGrounded && Vector3.Dot( _trace.Normal, up ) > 0.4f;

		if ( !validContact )
		{
			Load = 0f;
			SlipRatio = 0f;
			SlipAngle = 0f;
			SuspensionLength = SuspensionTravel;
			// AIRBORNE DRIVE GATE (ramp lip-cluster fix 2026-07-21, LIVE-UNVERIFIED): a driven wheel
			// with no valid contact gets ZERO drive torque - it freewheels (brakes still act). With
			// drive torque passed through, an unloaded driven wheel flared to redline-equivalent
			// within ~3 ticks of leaving a kicker lip, the engine pinned at 94-98% redline for the
			// whole flight (live capture: rpm 6008-6010 at the lip), the drivetrain's limiter-camp
			// escape upshifted MID-AIR ~0.27 s into every full-throttle flight at 20-30 m/s, and the
			// car touched down at the flared surface speed: slip +0.4..+0.5, a 10-12 kN (~1 g) tire
			// spike for ~2 ticks, the skid chirp, and a landing one gear too tall (post-landing drive
			// force -26..-37% vs pre-lip) - the felt "hitch going off the ramps" at any speed
			// (offline quantification: tools/ramp_lip_drivetrain_port.py). Zeroing drive here starves
			// that whole cascade: omega holds ~rolling speed, rpm stays steady, the escape shift
			// never arms in air, and touchdown slip is ~0. Flat-ground behavior is byte-identical BY
			// CONSTRUCTION (this branch never runs with valid ground contact; grounded-tick A/B in
			// the port asserts bit-identical trajectories). Registered prediction at commit time: the
			// owner's Sim-mode discriminator will NOT kill the hitch (the cascade is
			// assist-independent); result to be recorded next to this comment either way.
			IntegrateWheelSpin( dt, 0f, brakeTorque, 0f );
			// Airborne wind-down is bearing drag only: 2%/s. The previous 0.5f here was 50%/s
			// (documented in round 4 as "0.5%/s", a 100x misread of its own constant): after a
			// 1.2 s flight the wheels arrived at ~55% of road speed and braked the car while
			// spinning back up (flight recorder 2026-07-21: ~2 m/s of the touchdown loss plus
			// the landing skid chirp came from exactly this). Real free wheels barely slow.
			AngularVelocity *= 1f - 0.02f * dt;
			return;
		}

		// --- suspension ---
		// Force acts along the CONTACT NORMAL, not body-up: body-up creates a feedback
		// loop where a tilted car pushes itself sideways, slides, and flips.
		// Compression is clamped to physical travel: bottoming out is a rigid contact
		// for the body collider (bump stop), not unbounded spring force.
		var normal = _trace.Normal;
		float restLength = SuspensionTravel + Radius;
		float hitLength = _trace.Distance * Units.UnitsToMeters + TraceSphereRadius; // sphere stops one radius short
		float compression = Math.Clamp( restLength - hitLength, 0f, SuspensionTravel );
		SuspensionLength = Math.Clamp( hitLength - Radius, 0f, SuspensionTravel );

		var velAtWheel = _rigidbody.GetVelocityAtPoint( WorldPosition ) * Units.UnitsToMeters;
		float compressionSpeed = -Vector3.Dot( velAtWheel, normal );

		float springForce = SpringRate * compression + DamperRate * compressionSpeed;
		Load = Math.Clamp( springForce, 0f, StaticLoad * 4f );

		_accumulatedForce += normal * Load;
		_forcePosition = _trace.EndPosition;

		// --- tire frame on the contact plane ---
		var steerRot = _rigidbody.WorldRotation * Rotation.FromYaw( SteerAngle );
		var forward = (steerRot.Forward - normal * Vector3.Dot( steerRot.Forward, normal )).Normal;
		var side = Vector3.Cross( normal, forward );

		var contactVel = _rigidbody.GetVelocityAtPoint( _trace.EndPosition ) * Units.UnitsToMeters;
		float vLong = Vector3.Dot( contactVel, forward );
		float vLat = Vector3.Dot( contactVel, side );
		GroundSpeed = vLong;

		// --- slip ---
		// longitudinal uses RAW slip + a one-substep force clamp below (smoothing it added
		// ~100 ms of feedback lag that turned wheel spin into a rhythmic surge);
		// lateral keeps light relaxation, its loop is chassis-side and much slower
		float slipVelocity = AngularVelocity * Radius - vLong; // m/s at the contact patch
		SlipRatio = slipVelocity / MathF.Max( MathF.Abs( vLong ), 2.0f );

		float rawSlipAngle = MathF.Atan2( vLat, MathF.Abs( vLong ) + 0.7f );
		float relax = Math.Clamp( (MathF.Abs( vLong ) + 1f) * dt / 0.2f, 0.1f, 1f );
		_smoothedSlipAngle += (rawSlipAngle - _smoothedSlipAngle) * relax;
		SlipAngle = _smoothedSlipAngle;

		// --- forces from curves, load sensitivity, surface grip ---
		float surfaceGrip = _trace.Surface?.Friction ?? 1f;
		float loadFactor = 1f - LoadSensitivity * MathF.Max( 0f, Load / StaticLoad - 1f );
		float maxForce = Load * loadFactor * surfaceGrip * GripScale;

		float fx = LongitudinalCurve.Evaluate( SlipRatio ) * MathF.Sign( SlipRatio ) * maxForce;
		float fy = -LateralCurve.Evaluate( SlipAngle ) * MathF.Sign( SlipAngle ) * maxForce;

		// one-substep stability clamp: never push the wheel past ground-speed match within
		// a single substep (gain*dt/inertia > 2 here, the raw loop oscillates without this)
		float fxStable = MathF.Abs( slipVelocity ) * Inertia / (Radius * Radius * dt);
		fx = Math.Clamp( fx, -fxStable, fxStable );

		// --- friction ellipse (spec §5.2.1.4) ---
		float combined = MathF.Sqrt( fx * fx + fy * fy );
		if ( combined > maxForce && combined > 0.001f )
		{
			float scale = maxForce / combined;
			fx *= scale;
			fy *= scale;
		}

		// --- low-speed parking blend: kill standstill jitter ---
		// force is capped at what stops this wheel's mass share within one fixed frame
		// (spec 5.2.1.6) — uncapped, it overshoots and becomes a self-sustaining oscillator
		var planarVel = forward * vLong + side * vLat;
		float planarSpeed = planarVel.Length;
		float parkOmegaBlend = 0f;
		if ( planarSpeed < 1.5f && MathF.Abs( AngularVelocity * Radius ) < 1.5f && planarSpeed > 0.001f )
		{
			// ParkBrakeScale: throttle dissolves the stiction — steered fronts were
			// "parking" against full-lock standstill launches (2 km/h crawls, tele 07-07)
			float blend = (1f - planarSpeed / 1.5f) * ParkBrakeScale;
			float massShare = StaticLoad / 9.81f; // kg carried by this wheel
			float frameDt = dt * VehicleController.Substeps;
			float stopForce = massShare * planarSpeed / frameDt * 0.8f;
			float parkMag = MathF.Min( MathF.Min( planarSpeed * Load * 1.5f, stopForce ), maxForce );

			var park = -planarVel / planarSpeed * parkMag;
			fx = fx * (1f - blend) + Vector3.Dot( park, forward ) * blend;
			fy = fy * (1f - blend) + Vector3.Dot( park, side ) * blend;
			parkOmegaBlend = blend;
		}

		_accumulatedForce += forward * fx + side * fy;

		IntegrateWheelSpin( dt, driveTorque, brakeTorque, fx );

		// Park the wheel's SPIN, not just the chassis. The parking blend above brakes the chassis, but
		// its reaction torque (-fx*Radius, fed into IntegrateWheelSpin) spins this wheel up and, with
		// nothing anchoring omega at rest, it settles at a nonzero spin that keeps pushing the car: a
		// permanent slow creep with the tyre visibly rotating and the slip ratio pinging the skid
		// threshold (community report: "unless you've perfectly stopped, it infinitely skids and rotates
		// the wheels in place"). Pull omega toward the ground-rolling speed vLong/Radius (= 0 at a true
		// standstill) by the SAME blend, so a parked, unbraked wheel settles to zero spin and zero slip.
		// Offline sim: a 0.2 m/s creep that never stopped and skidded 50-100% of frames now settles to
		// under 1 mm/s with zero skid, while cruise (over 1.5 m/s, blend inert) is byte-identical. Fades
		// out with throttle (blend carries ParkBrakeScale) so standstill launches are unaffected.
		if ( parkOmegaBlend > 0f )
			AngularVelocity = MathX.Lerp( AngularVelocity, vLong / Radius, parkOmegaBlend );
	}

	// Cap-aware drive-torque rolloff onset (kart cap-camping fix 2026-07-18): drive torque begins
	// fading toward zero once the wheel's own spin reaches this fraction of DriveOmegaCap; below it
	// the rolloff is inert so below-cap behavior is byte-identical.
	const float DriveRolloffOnset = 0.90f;

	void IntegrateWheelSpin( float dt, float driveTorque, float brakeTorque, float tireForce )
	{
		float preOmega = AngularVelocity;
		bool driving = driveTorque != 0f;
		float driveDir = driving ? MathF.Sign( driveTorque ) : 0f;

		// Cap-aware drive-torque rolloff (kart "stuck turning" fix 2026-07-18). The per-substep
		// clamp below is a hard backstop, but with the clamp ALONE a driven wheel under sustained
		// full torque CAMPS exactly at the cap; when forward speed then collapses in a corner the
		// slip ratio blows far past the grip peak (live: 7+) and the longitudinal tail force eats the
		// friction ellipse, killing rear lateral grip so the yaw holds against countersteer. Fade
		// drive torque to zero as the wheel approaches the cap so it settles OFF the cap instead of
		// camping on it. Smoothstep, not a hard corner, to avoid a torque-fade limit cycle. Inert
		// below the onset (approach <= DriveRolloffOnset) so below-cap behavior is byte-identical.
		if ( driving && DriveOmegaCap < float.MaxValue )
		{
			float approach = driveDir * preOmega / DriveOmegaCap;
			if ( approach > DriveRolloffOnset )
			{
				float f = Math.Clamp( (1f - approach) / (1f - DriveRolloffOnset), 0f, 1f );
				driveTorque *= f * f * (3f - 2f * f);
			}
		}

		// drive + tire reaction
		float torque = driveTorque - tireForce * Radius;
		AngularVelocity += torque / Inertia * dt;

		// Per-substep drive-side overshoot clamp (kart high-PeakTorque wobble hunt 2026-07-18).
		// The rev limiter zeroes torque only on the substep AFTER wheel-implied rpm crosses redline,
		// so one substep of unlimited drive torque on a light wheel overshoots redline-equivalent
		// omega by up to 6-8x (measured live: kart at 900 N-m spikes to 289-339 rad/s vs the 44 rad/s
		// gear-1 redline equivalent; even stock 52 N-m reaches 141). The spike is what lets an
		// unloaded rear diverge violently from its loaded twin over any perturbation (the felt
		// "individual tires have different traction" left-right wobble). Clamp: DRIVE torque may
		// never push omega past the cap within a substep. Signed by drive direction so reverse works;
		// cap floors at the pre-integration omega so ground-driven overspeed (downhill coast) is
		// never yanked down, and the ground reaction path is untouched. Guarded on the ORIGINAL drive
		// intent so a rolloff that faded torque to zero still cannot let the wheel blow past the cap.
		if ( driving && DriveOmegaCap < float.MaxValue )
		{
			float cap = MathF.Max( DriveOmegaCap, driveDir * preOmega );
			AngularVelocity = driveDir > 0f
				? MathF.Min( AngularVelocity, cap )
				: MathF.Max( AngularVelocity, -cap );
		}

		// brakes can stop the wheel but never reverse it within a step
		if ( brakeTorque > 0f )
		{
			float brakeDelta = brakeTorque / Inertia * dt;
			AngularVelocity = MathF.Abs( AngularVelocity ) <= brakeDelta
				? 0f
				: AngularVelocity - MathF.Sign( AngularVelocity ) * brakeDelta;
		}
	}

	/// <summary>Apply the substep-averaged force to the chassis. Call once per fixed update.</summary>
	public void EndStep()
	{
		if ( _substeps == 0 || _accumulatedForce.IsNearZeroLength )
			return;

		var averaged = _accumulatedForce / _substeps;
		_rigidbody.ApplyForceAt( _forcePosition, averaged * Units.MetersToUnits );
	}

	void DoTrace()
	{
		var down = _rigidbody.WorldRotation.Down;
		float lengthUnits = (SuspensionTravel + Radius) * Units.MetersToUnits;

		_trace = Scene.Trace
			.Radius( 1f )
			.IgnoreGameObjectHierarchy( _rigidbody.GameObject )
			.WithoutTags( "car" )
			.FromTo( WorldPosition, WorldPosition + down * lengthUnits )
			.Run();
	}
}
fieldguide.vehiclephysics / Code/Drivetrain.cs
Game library
namespace FieldGuide.VehiclePhysics;

/// <summary>
/// Engine + clutch + gearbox + diff state machine. Plain class owned by
/// VehicleController, simulated per substep. RPM is its own integrated state coupled to
/// the wheels through an auto-clutch, so revving at standstill and shift flare exist.
/// </summary>
public class Drivetrain
{
	readonly CarDefinition _def;

	public float Rpm { get; private set; }
	public int Gear { get; private set; } = 1; // 1-based; 0 = neutral, -1 = reverse
	public bool IsShifting => _shiftTimer > 0f;

	/// <summary>Driver-selectable transmission mode. false (default) = the untuned automatic box
	/// (byte-identical existing behavior); true = sequential MANUAL — the auto-shift block in
	/// <see cref="Simulate"/> is suppressed and gear changes come from <see cref="ShiftUp"/> /
	/// <see cref="ShiftDown"/> driven off input. The rev limiter, shift timer/lockout, and clutch
	/// machinery are untouched by the mode (they model the shift event either way).</summary>
	public bool ManualMode { get; set; }

	/// <summary>Redline (rpm) for this car — read by the manual over-rev guard / UI.</summary>
	public float Redline => _def.RedlineRpm;

	float _shiftTimer;
	float _shiftLockout;
	float _freeRpm; // engine-side rpm when the clutch slips
	float _limiterHold; // seconds spent pinned near redline under power (limiter-camp escape)

	// Downshift arming (anti-hunt hysteresis): a gear entered by UPSHIFT starts unproven and may
	// not auto-downshift under power until groundRpm has first risen past ShiftDownRpm — an escape
	// shift can land below that threshold on purpose (recovery in progress), and a fixed-length
	// lockout can expire before the low-rpm clutch-slip zone climbs out (measured: 2nd entered at
	// ~1150 ground rpm climbs ~575 rpm/s, so 1.5 s ends at ~1980 < the 2200 downshift point —
	// the box bounced straight back into the wheelspin it had escaped). Lifting the throttle
	// re-arms the downshift immediately, so coasting to a stop still steps down normally.
	bool _gearProven = true;

	public Drivetrain( CarDefinition def )
	{
		_def = def;
		Rpm = def.IdleRpm;
		_freeRpm = def.IdleRpm;
	}

	/// <summary>Analytic torque curve: ~50% at idle, peak ~75% of the band, mild falloff at redline.</summary>
	public float EngineTorqueAt( float rpm )
	{
		float n = Math.Clamp( (rpm - _def.IdleRpm) / (_def.RedlineRpm - _def.IdleRpm), 0f, 1f );
		float shape = (0.5f + 1.5f * n - n * n) / 1.0625f;
		return _def.PeakTorque * shape;
	}

	float CurrentRatio => Gear switch
	{
		> 0 => _def.GearRatios[Gear - 1] * _def.FinalDrive,
		-1 => -_def.ReverseRatio * _def.FinalDrive,
		_ => 0f
	};

	/// <summary>Redline-equivalent driven-wheel angular speed (rad/s) for the CURRENT gear ratio;
	/// float.MaxValue in neutral (no drive coupling). Read fresh each substep by the controller
	/// (the ratio changes on a shift) and handed to the wheels as the per-substep drive-side omega
	/// cap: the rev limiter cuts torque only on the substep AFTER wheel-implied rpm crosses redline,
	/// and a light wheel at high PeakTorque can blow 6x past redline-equivalent within that one
	/// substep (measured: kart at 900 N-m hits 289-339 rad/s vs a 44 rad/s gear-1 redline
	/// equivalent). The cap makes the limiter effectively per-substep on the drive side.</summary>
	public float RedlineWheelSpeed
		=> CurrentRatio == 0f ? float.MaxValue : _def.RedlineRpm * MathF.Tau / 60f / MathF.Abs( CurrentRatio );

	/// <summary>
	/// One substep. avgDrivenWheelSpeed and groundWheelSpeed in rad/s. Returns torque per driven
	/// wheel (N·m). Clutch engagement is a continuous blend — a binary locked/slipping switch
	/// sits right at town-driving speeds and judders. Shift decisions use GROUND speed, not
	/// engine/wheel rpm: wheelspin inflates engine rpm, causing 1-2-1-2 shift hunting where
	/// every downshift torque-spikes the rear tires (spin-outs).
	/// </summary>
	public float Simulate( float dt, float throttle, float avgDrivenWheelSpeed, float groundWheelSpeed, int drivenWheelCount )
	{
		if ( _shiftTimer > 0f )
		{
			_shiftTimer -= dt;
			throttle = 0f; // torque cut during shift
		}

		float ratio = CurrentRatio;
		float wheelImpliedRpm = MathF.Abs( avgDrivenWheelSpeed * ratio ) * 60f / MathF.Tau;

		// continuous auto-clutch: 0 at stall rpm, fully locked a few hundred rpm above idle
		float stallRpm = _def.IdleRpm * 1.05f;
		float lockRpm = _def.IdleRpm * 1.7f;
		float engagement = Gear == 0 ? 0f : Math.Clamp( (wheelImpliedRpm - stallRpm) / (lockRpm - stallRpm), 0f, 1f );

		// engine-side rpm when slipping: revs toward the throttle target
		float freeTarget = _def.IdleRpm + throttle * (_def.RedlineRpm * 0.5f - _def.IdleRpm);
		_freeRpm = Math.Clamp( _freeRpm + (freeTarget - _freeRpm) * 5f * dt, _def.IdleRpm, _def.RedlineRpm );

		float lockedRpm = Math.Clamp( wheelImpliedRpm, _def.IdleRpm, _def.RedlineRpm );
		Rpm = MathX.Lerp( _freeRpm, lockedRpm, engagement );

		float engineTorque = EngineTorqueAt( Rpm ) * throttle;
		float engineBrake = _def.EngineBrakeTorque * (1f - throttle) * (Rpm / _def.RedlineRpm) * engagement;
		float slipTransmission = Math.Clamp( throttle * 1.2f, 0f, 1f ) * 0.85f;
		float clutchFactor = MathX.Lerp( slipTransmission, 1f, engagement );
		float torqueOut = (engineTorque * clutchFactor - engineBrake) * ratio;

		// rev limiter
		if ( Rpm >= _def.RedlineRpm && throttle > 0f )
			torqueOut = MathF.Min( torqueOut, 0f );

		// automatic shifting from ground-speed-implied rpm + post-shift lockout
		_shiftLockout -= dt;
		float groundRpm = MathF.Abs( groundWheelSpeed * ratio ) * 60f / MathF.Tau;

		// Limiter-camp escape: the upshift decision reads GROUND-speed rpm (anti-hunt — see the
		// header comment) but the engine + rev limiter run on WHEEL-implied rpm, and wheelspin
		// separates the two. With traction control off, a hard launch inflates engine rpm onto the
		// limiter while groundRpm is still below ShiftUpRpm — the box then bounces on the limiter
		// until ground speed catches up (measured on the hatch: ~3.4 s pinned 5945–6300 in 1st
		// while groundRpm crawled 560→5730). The limiter cut decelerates a spinning wheel within
		// substeps, so the bounce dips a few percent below redline many times a second — the
		// near-limiter window is therefore WIDE (0.94) and the hold DECAYS on dips instead of
		// resetting, or the oscillation zeroes the timer forever and the escape never fires.
		bool nearLimiter = Rpm >= _def.RedlineRpm * 0.94f && throttle > 0.5f;
		_limiterHold = nearLimiter ? _limiterHold + dt : MathF.Max( 0f, _limiterHold - dt * 0.5f );

		// Downshift arming: the current gear proves itself the moment ground rpm clears the
		// downshift threshold; from then on the normal downshift rule applies unchanged.
		if ( !_gearProven && groundRpm >= _def.ShiftDownRpm )
			_gearProven = true;

		if ( !ManualMode && Gear > 0 && !IsShifting && _shiftLockout <= 0f )
		{
			bool wantUp = groundRpm > _def.ShiftUpRpm;
			bool escape = false;
			if ( !wantUp && _limiterHold > 0.25f && Gear < _def.GearRatios.Length )
			{
				// Escape guard: the next gear only needs to be VIABLE, not already above the
				// downshift point — a spinning launch hooks up instantly on the taller gear and
				// recovers under the downshift-arming hysteresis (which holds the box in the new
				// gear through the sub-ShiftDownRpm climb no matter how long it takes).
				float nextRatio = _def.GearRatios[Gear] * _def.FinalDrive; // Gear is 1-based → [Gear] = next gear up
				float postShiftGroundRpm = MathF.Abs( groundWheelSpeed * nextRatio ) * 60f / MathF.Tau;
				wantUp = escape = postShiftGroundRpm >= _def.ShiftDownRpm * 0.5f;
			}

			if ( wantUp && Gear < _def.GearRatios.Length )
			{
				Gear++;
				_shiftTimer = 0.15f;
				// Escape shifts keep a longer settle hold; the arming hysteresis above is the real
				// anti-hunt guard (a fixed timer alone measurably expired mid-recovery).
				_shiftLockout = escape ? 1.5f : 0.8f;
				_limiterHold = 0f;
				_gearProven = false; // entered by upshift → must prove itself before downshifting under power
			}
			else if ( groundRpm < _def.ShiftDownRpm && Gear > 1 && (_gearProven || throttle < 0.5f) )
			{
				Gear--;
				_shiftTimer = 0.12f;
				_shiftLockout = 0.8f;
				_gearProven = true; // entered downward — ground rpm is higher here by ratio; nothing to prove
			}
		}

		// open diff v1: equal split across driven wheels
		return drivenWheelCount > 0 ? torqueOut / drivenWheelCount : 0f;
	}

	public void EngageReverse() { Gear = -1; _shiftTimer = 0f; _gearProven = true; }
	public void EngageForward() { if ( Gear <= 0 ) { Gear = 1; _shiftTimer = 0f; _gearProven = true; } }

	// ── sequential MANUAL shift ──
	// Gated on IsShifting only (the 0.15 s torque-cut window), NOT on _shiftLockout — the 0.8 s
	// lockout is the auto box's anti-hunt guard and would make a hand-shifted sequential box feel
	// sluggish; the player decides when to shift. Both timers are still set so the shift flare /
	// torque cut model exactly as the auto path, and so toggling back to AUTO inherits a clean state.

	/// <summary>Sequential manual up-shift: advance one forward gear. No-op in reverse/neutral, at the
	/// top gear, or mid-shift. Returns true if a gear change happened.</summary>
	public bool ShiftUp()
	{
		if ( IsShifting ) return false;
		if ( Gear <= 0 || Gear >= _def.GearRatios.Length ) return false;
		Gear++;
		_shiftTimer = 0.15f;
		_shiftLockout = 0.8f;
		_gearProven = true; // player-commanded — the auto arming rule never second-guesses manual shifts
		return true;
	}

	/// <summary>Sequential manual down-shift: drop one forward gear — BLOCKED if it would throw engine
	/// rpm past redline at the current ground speed (money-shift / over-rev guard). No-op below 1st, in
	/// reverse/neutral, or mid-shift. <paramref name="groundWheelSpeed"/> is the ground-implied wheel
	/// speed (rad/s), the same quantity the auto path shifts on. Returns true if a gear change happened.</summary>
	public bool ShiftDown( float groundWheelSpeed )
	{
		if ( IsShifting ) return false;
		if ( Gear <= 1 ) return false; // below 1st does nothing; reverse engages automatically (ReadInput)
		if ( PredictedDownshiftRpm( groundWheelSpeed ) > _def.RedlineRpm ) return false; // over-rev guard
		Gear--;
		_shiftTimer = 0.12f;
		_shiftLockout = 0.8f;
		_gearProven = true; // player-commanded — the auto arming rule never second-guesses manual shifts
		return true;
	}

	/// <summary>Engine rpm a one-gear downshift would produce at this ground wheel speed (rad/s), using
	/// the SAME ground-speed→rpm mapping as the auto-shift decision so the over-rev guard and the rev
	/// limiter agree. Returns the current <see cref="Rpm"/> when there is no lower forward gear.</summary>
	public float PredictedDownshiftRpm( float groundWheelSpeed )
	{
		if ( Gear <= 1 ) return Rpm;
		float lowerRatio = _def.GearRatios[Gear - 2] * _def.FinalDrive;
		return MathF.Abs( groundWheelSpeed * lowerRatio ) * 60f / MathF.Tau;
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 32,
    "Files": [
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "// Global usings for the Vehicle Physics Kit assembly only. These do NOT export the kit's\r\n// FieldGuide.VehiclePhysics namespace anywhere \u2014 a consumer references the kit types through\r\n// their real namespace, never through a leaked global using.\r\nglobal using Sandbox;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\nglobal using System.Threading.Tasks;\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/CarDefinition.cs",
            "FileName": "CarDefinition.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\npublic enum DriveLayout\r\n{\r\n\tFWD,\r\n\tRWD,\r\n\tAWD\r\n}\r\n\r\npublic enum BodyStyle\r\n{\r\n\tBox,  // sedan/hatch blockout: body + cabin\r\n\tKart  // flat deck + seat back + citizen driver\r\n}\r\n\r\npublic enum AssistLevel\r\n{\r\n\tCasual, // full traction control + yaw-stability damping + ABS\r\n\tSport,  // ABS + optional REDUCED-authority TC / yaw-stability (per-car opt-in; drift feel preserved)\r\n\tSim     // nothing\r\n}\r\n\r\n/// <summary>\r\n/// Complete tuning spec for one car. Plain class, instantiated from <see cref=\"CarDefinitions\"/>;\r\n/// promote to a [GameResource] .car asset once an editor workflow is in play so designers can tune\r\n/// without recompiling. All values SI: meters, kg, N, N-m, N/m.\r\n/// </summary>\r\npublic class CarDefinition\r\n{\r\n\tpublic string Name { get; set; } = \"Car\";\r\n\tpublic BodyStyle Style { get; set; } = BodyStyle.Box;\r\n\tpublic bool HasDriver { get; set; }\r\n\r\n\t/// <summary>Optional opaque body-manifest path for a consumer-supplied custom body builder\r\n\t/// (see <see cref=\"VehicleFactory.CustomBodyBuilder\"/>). The kit itself never reads it \u2014 with no\r\n\t/// custom builder plugged in, the factory builds a primitive blockout body and this value is\r\n\t/// inert. It exists so a consumer's builder (e.g. a part-kit assembler) can look up which body to\r\n\t/// assemble for this car. null keeps the blockout path byte-for-byte unchanged.</summary>\r\n\tpublic string BodyManifest { get; set; }\r\n\r\n\t// Chassis\r\n\tpublic float Mass { get; set; } = 1200f;\r\n\tpublic Vector3 BodySize { get; set; } = new( 4.0f, 1.8f, 1.3f ); // length, width, height (m)\r\n\tpublic float Wheelbase { get; set; } = 2.55f;\r\n\tpublic float TrackWidth { get; set; } = 1.55f;\r\n\tpublic float RideHeight { get; set; } = 0.35f; // chassis center above wheel attach plane\r\n\tpublic float GroundClearance { get; set; } = 0.14f; // collider bottom above ground at rest\r\n\tpublic float CenterOfMassDrop { get; set; } = 0.20f; // CoM below chassis center; must stay above wheel plane\r\n\r\n\t// Wheels & suspension (per wheel)\r\n\tpublic float WheelRadius { get; set; } = 0.31f;\r\n\tpublic float WheelInertia { get; set; } = 1.2f; // kg m^2\r\n\tpublic float SuspensionTravel { get; set; } = 0.20f;\r\n\tpublic float SpringRate { get; set; } = 38000f;\r\n\tpublic float DamperRate { get; set; } = 2800f;\r\n\r\n\t// Tires\r\n\tpublic TireCurve LongitudinalCurve { get; set; } = TireCurve.Street;\r\n\tpublic TireCurve LateralCurve { get; set; } = new( 0.14f, 1.00f, 0.55f, 0.80f ); // radians: peak ~8 deg\r\n\tpublic float LoadSensitivity { get; set; } = 0.06f;\r\n\r\n\t// Drivetrain\r\n\tpublic DriveLayout Layout { get; set; } = DriveLayout.FWD;\r\n\t// +20% speed pass 2026-07-21: baseline PeakTorque 150->180 and RedlineRpm 6500->7800 so a bare\r\n\t// kit consumer starts from the new faster baseline (the demo roster below overrides both per car).\r\n\tpublic float PeakTorque { get; set; } = 180f;\r\n\tpublic float IdleRpm { get; set; } = 900f;\r\n\tpublic float RedlineRpm { get; set; } = 7800f;\r\n\tpublic float EngineInertia { get; set; } = 0.25f; // kg m^2 at crank\r\n\tpublic float EngineBrakeTorque { get; set; } = 40f;\r\n\tpublic float[] GearRatios { get; set; } = { 3.6f, 2.1f, 1.4f, 1.05f, 0.85f };\r\n\tpublic float ReverseRatio { get; set; } = 3.4f;\r\n\tpublic float FinalDrive { get; set; } = 3.9f;\r\n\tpublic float ShiftUpRpm { get; set; } = 5800f;\r\n\tpublic float ShiftDownRpm { get; set; } = 2200f;\r\n\r\n\t// Engine audio (read by EngineAudio). Per-class dials so a small buzzy kart and a deep\r\n\t// sedan/truck share one code path:\r\n\t//   EngineSoundEvent     \u2014 the LOW-RPM loop this class plays (idle/low, the deep layer).\r\n\t//   EngineSoundEventHigh \u2014 the HIGH-RPM loop, crossfaded in as revs climb. When null/empty the\r\n\t//     class runs the legacy SINGLE-loop model (one handle, wide pitch sweep) \u2014 the kart keeps\r\n\t//     this so its single synth purr is unchanged. When set, EngineAudio blends the two recorded\r\n\t//     layers by RPM, and each layer only pitch-shifts a narrow band around its recorded pitch\r\n\t//     (no far stretch = no screech), which is the point of the layered model.\r\n\t//   EnginePitchBase      \u2014 multiplies the pitch of whichever model runs, so a class reads higher\r\n\t//     or lower overall (kart >1 stays buzzy, truck <1 sits deep).\r\n\tpublic string EngineSoundEvent { get; set; } = \"sounds/engine/engine_real_low.sound\";\r\n\tpublic string EngineSoundEventHigh { get; set; }\r\n\tpublic float EnginePitchBase { get; set; } = 1f;\r\n\r\n\t// Brakes\r\n\tpublic float BrakeTorque { get; set; } = 2400f; // total, split by bias\r\n\tpublic float BrakeBias { get; set; } = 0.62f;   // fraction to front\r\n\tpublic float HandbrakeTorque { get; set; } = 3000f; // rear wheels only\r\n\r\n\t// ABS dials (per-car). 0.3/0.55 because telemetry showed the ABS duty cycle, not tire grip, was\r\n\t// the brake limiter, and per-car ABS modulation is a legitimate class trait: the locking kart\r\n\t// needs a different release than the truck. Active in Casual + Sport.\r\n\tpublic float AbsSlipThreshold { get; set; } = 0.25f; // release when SlipRatio < -this under braking\r\n\tpublic float AbsReleaseFactor { get; set; } = 0.70f; // brake torque multiplier while released\r\n\r\n\t// Drift-exit soft-lock: cap the handbrake-INDUCED rear slip ratio. When a cap is active and a rear\r\n\t// wheel is already sliding PAST it, its handbrake torque is withheld that substep (ABS-style duty\r\n\t// cycle) so the rear spins back up toward the cap and keeps some rotation \u2014 hence some lateral bite\r\n\t// \u2014 mid-slide. Default -1.0 = NO effective cap (byte-identical full-lock behavior).\r\n\tpublic float HandbrakeSlipCap { get; set; } = -1.0f;\r\n\r\n\t// Steering\r\n\tpublic float MaxSteerAngle { get; set; } = 32f; // degrees at standstill\r\n\tpublic float HighSpeedSteerAngle { get; set; } = 8f; // degrees at/above 30 m/s\r\n\r\n\t// Arcade feel dials (defaults = the original sim-leaning behavior)\r\n\tpublic float SteerRateScale { get; set; } = 1f;    // multiplies how fast steering ramps\r\n\t// Reverse speed pass 2026-07-21 (owner feel): raised 4.5 -> 20.0 m/s (~10 -> ~45 mph). This is the\r\n\t// ACTUAL reverse cap mechanism (VehicleController cuts reverse throttle above this speed), and the\r\n\t// game roster inherits it (only the pickup used to override it). Each car is still additionally\r\n\t// limited by its reverse-gear redline-equivalent wheel speed (RedlineRpm/ReverseRatio/FinalDrive/\r\n\t// WheelRadius via Drivetrain.RedlineWheelSpeed), so torquey-but-tall reverse gears self-limit below\r\n\t// this cap (kart ~8 m/s, pickup ~12 m/s) while the cars reach the high-30s/40s mph.\r\n\tpublic float ReverseSpeedCap { get; set; } = 20.0f; // m/s before reverse throttle cuts\r\n\tpublic float LaunchBoost { get; set; } = 1f;       // torque multiplier at standstill, fades out by ~54 km/h\r\n\tpublic float BrakeAssist { get; set; } = 0f;       // extra chassis-level decel while braking (m/s\u00b2)\r\n\t// Spin-recovery assist: after a handbrake spin the car keeps rolling BACKWARDS (old travel\r\n\t// direction) while the player holds throttle the new way \u2014 BrakeAssist can't help there (forward\r\n\t// throttle sets Brake=0), so nothing arcade-level arrests the stale velocity. This is extra\r\n\t// chassis-level decel along -velocity, applied ONLY when input throttle opposes the ground velocity\r\n\t// along the car's facing, fading out as the car rotates to face its motion\r\n\t// (VehicleController.ApplySpinRecoveryAssist). Same m/s\u00b2 unit + never-reverse-within-a-step cap as\r\n\t// BrakeAssist; gated Assists != Sim. 0 disables.\r\n\tpublic float SpinRecoveryAssist { get; set; } = 0f; // extra chassis decel killing stale opposing velocity (m/s\u00b2)\r\n\tpublic float HandbrakeGripScale { get; set; } = 1f; // rear grip multiplier while handbrake held (<1 = drift button)\r\n\r\n\t// Wall-glance forgiveness assist: a sustained near-horizontal chassis contact while moving\r\n\t// re-projects velocity along the wall tangent and gently yaws the heading to match, scaled by\r\n\t// incidence (VehicleController.ApplyWallGlanceAssist). Active only when Assists != Sim. Head-on hits\r\n\t// (incidence >= WallGlanceHeadOnDeg) get NO assist \u2014 frontal crashes stay hard stops.\r\n\tpublic bool WallGlanceAssist { get; set; } = true;\r\n\tpublic float WallScrubFactor { get; set; } = 0.75f;    // fraction of speed kept along the wall tangent on a shallow graze\r\n\tpublic float WallGlanceShallowDeg { get; set; } = 35f; // at/below this velocity-vs-wall incidence: full assist\r\n\tpublic float WallGlanceHeadOnDeg { get; set; } = 60f;  // at/above this incidence: no assist (hard stop preserved)\r\n\tpublic float WallAlignStrength { get; set; } = 6f;     // yaw-align rate toward the wall tangent (per second)\r\n\r\n\t// \u2500\u2500 Sport-mode stability posture (owner call 2026-07-21) \u2500\u2500\r\n\t// Sport historically ran with NO traction control and NO yaw-stability damping (both Casual-only),\r\n\t// so a full-throttle RWD car spun the rears to redline and the counter-steer pendulum went divergent\r\n\t// \u2014 uncatchable spin-outs. These give Sport a REDUCED-authority version of each assist, opt-in per\r\n\t// car. Both default to 0 (disabled), so any car that doesn't set them keeps the raw \"ABS only\" Sport\r\n\t// and \u2014 critically \u2014 Casual and Sim stay byte-identical (the controller multiplies Casual authority\r\n\t// by exactly 1 and returns early for Sim, unchanged).\r\n\r\n\t// Sport traction control. When >0, Sport runs the same proportional TC as Casual\r\n\t// (VehicleController.ApplyTractionControl) but holds driven-wheel slip near THIS ratio instead of the\r\n\t// Casual 0.14 grip-peak target. Set it LOOSER than the peak (e.g. 0.30-0.45) so the rears still break\r\n\t// into a throttle-steerable slide (drift stays alive) while the redline free-spin that torches rear\r\n\t// lateral grip is capped. 0 = no Sport TC (raw wheelspin, the old behavior).\r\n\tpublic float SportTcSlipTarget { get; set; } = 0f;\r\n\r\n\t// Sport yaw-stability. When >0, Sport runs the yaw-rate damper (VehicleController.ApplyStabilityAssist)\r\n\t// at THIS fraction of the Casual authority (0-1). It bleeds the stored yaw rate that snaps a\r\n\t// counter-steered slide the OTHER way (the pendulum spin-out) so slides stay CATCHABLE, without\r\n\t// killing deliberate rotation the way full Casual authority would. 0 = no Sport yaw damping.\r\n\tpublic float SportStabilityScale { get; set; } = 0f;\r\n\r\n\t// Driver seated pose (citizen animgraph; sit enum: 0 none, 1-3 chair poses, 4-5 ground poses).\r\n\t// Defaults = the original upright chair pose. Made per-car so a recumbent kart driver \u2014 legs\r\n\t// extended forward to the pedals \u2014 can be authored without disturbing any upright-seated car.\r\n\tpublic int DriverSit { get; set; } = 1;\r\n\tpublic float DriverSitOffsetHeight { get; set; } = 4f;\r\n\r\n\t// Defaults\r\n\tpublic AssistLevel DefaultAssists { get; set; } = AssistLevel.Casual;\r\n\tpublic Color Tint { get; set; } = new( 0.85f, 0.55f, 0.35f );\r\n}\r\n\r\n/// <summary>Car roster. Hatch is the default/first car; Kart and Coupe round out the roster.\r\n/// These are blockout-body demo definitions \u2014 physics is identical whether a car renders as a\r\n/// primitive blockout or a consumer-supplied custom body.</summary>\r\npublic static class CarDefinitions\r\n{\r\n\t/// <summary>The default/first roster car \u2014 an ORANGE hot hatch. FWD, mid-power, street tires.</summary>\r\n\tpublic static CarDefinition Hatch => new()\r\n\t{\r\n\t\tName = \"Compact Hatch\",\r\n\t\tMass = 1150f,\r\n\t\tBodySize = new Vector3( 3.9f, 1.75f, 1.45f ),\r\n\t\tWheelbase = 2.55f,\r\n\t\tTrackWidth = 1.50f,\r\n\t\tWheelRadius = 0.30f,\r\n\t\tLayout = DriveLayout.FWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 162->194.4, RedlineRpm 6300->7560 (top-gear top speed +20%).\r\n\t\tPeakTorque = 194.4f,\r\n\t\tRedlineRpm = 7560f,\r\n\t\t// Real recorded car set: deep muscle idle crossfaded up to a revving high layer.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_real_low.sound\",\r\n\t\tEngineSoundEventHigh = \"sounds/engine/engine_real_high.sound\",\r\n\t\tLongitudinalCurve = new TireCurve( 0.10f, 1.35f, 0.45f, 1.08f ),\r\n\t\tBrakeTorque = 4300f,\r\n\t\tLateralCurve = new TireCurve( 0.14f, 1.30f, 0.55f, 1.04f ),\r\n\t\tHandbrakeGripScale = 0.55f, // the drift button \u2014 rear grip cut while handbrake held; also the FWD J-turn lever\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tHighSpeedSteerAngle = 9.5f,\r\n\t\tSpringRate = 34000f,\r\n\t\tDamperRate = 2500f,\r\n\t\tTint = new Color( 0.93f, 0.42f, 0.03f ), // orange\r\n\t};\r\n\r\n\t/// <summary>Full-size pickup. Heaviest in roster, RWD, torquey low-rev engine, longer-travel\r\n\t/// softer suspension, offroad-leaning tires, high ride. Signature strength = hill grade.</summary>\r\n\tpublic static CarDefinition Pickup => new()\r\n\t{\r\n\t\tName = \"Utility Pickup\",\r\n\t\tMass = 1900f,\r\n\t\tBodySize = new Vector3( 5.2f, 1.95f, 1.55f ),\r\n\t\tWheelbase = 3.40f,\r\n\t\tTrackWidth = 1.70f,\r\n\t\tRideHeight = 0.44f,          // high ride: the class signature (hatch 0.35)\r\n\t\tGroundClearance = 0.24f,\r\n\t\tCenterOfMassDrop = 0.15f,    // higher CoM than the cars = truck-like roll; track 1.70 keeps rollover margin\r\n\t\tWheelRadius = 0.35f,\r\n\t\tWheelInertia = 2.4f,\r\n\t\tSuspensionTravel = 0.26f,    // longest in roster \u2014 washboard/offroad strength\r\n\t\tSpringRate = 42000f,\r\n\t\tDamperRate = 3400f,\r\n\t\tLongitudinalCurve = new TireCurve( 0.14f, 1.25f, 0.60f, 1.05f ),\r\n\t\tLateralCurve = new TireCurve( 0.15f, 1.22f, 0.60f, 1.06f ),\r\n\t\tLoadSensitivity = 0.07f,\r\n\t\tLayout = DriveLayout.RWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 320->384, RedlineRpm 3900->4680 (top-gear top speed +20%).\r\n\t\tPeakTorque = 384f,           // torquey: >2\u00d7 hatch; strong hills\r\n\t\tIdleRpm = 650f,\r\n\t\tRedlineRpm = 4680f,          // lowest redline in roster; low-rev truck character (was 3900, +20% pass)\r\n\t\tEngineInertia = 0.5f,\r\n\t\tEngineBrakeTorque = 90f,     // strong engine braking downhill\r\n\t\t// Real recorded truck set: deeper truck idle + revving high layer; base pitch dropped.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_real_truck_low.sound\",\r\n\t\tEngineSoundEventHigh = \"sounds/engine/engine_real_truck_high.sound\",\r\n\t\tEnginePitchBase = 0.85f,\r\n\t\tGearRatios = new[] { 3.8f, 2.3f, 1.5f, 1.1f, 0.85f },\r\n\t\tReverseRatio = 3.8f,\r\n\t\tFinalDrive = 3.9f,\r\n\t\tShiftUpRpm = 3500f,\r\n\t\tShiftDownRpm = 1700f,\r\n\t\tBrakeTorque = 7000f,\r\n\t\tBrakeBias = 0.65f,           // unladen bed = light rear axle\r\n\t\tHandbrakeTorque = 5000f,\r\n\t\tMaxSteerAngle = 27f,         // slow truck steering; long wheelbase stabilizes\r\n\t\tHighSpeedSteerAngle = 8f,\r\n\t\tSteerRateScale = 0.9f,\r\n\t\t// Reverse cap override removed 2026-07-21: inherits the new 20 m/s default; the pickup's tall reverse\r\n\t\t// gear + low redline self-limit it to ~12 m/s (~26 mph) reverse, so it stays the roster's slowest\r\n\t\t// reverse by gearing character rather than an artificial ~9 mph clamp.\r\n\t\tHandbrakeGripScale = 0.45f,  // deepest rear cut in the roster \u2014 1900 kg + 3.4 m wheelbase needs real help\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tSportTcSlipTarget = 0.30f,   // Sport keeps the torquey RWD truck from lighting the rears to redline\r\n\t\tSportStabilityScale = 0.5f,  // half-authority yaw damp so a Sport slide stays catchable\r\n\t\tDefaultAssists = AssistLevel.Casual,\r\n\t\tTint = new Color( 0.55f, 0.13f, 0.11f ), // dark brick red\r\n\t};\r\n\r\n\tpublic static CarDefinition Kart => new()\r\n\t{\r\n\t\tName = \"Go-Kart\",\r\n\t\tStyle = BodyStyle.Kart,\r\n\t\tHasDriver = true,\r\n\t\tMass = 260f,\r\n\t\tBodySize = new Vector3( 1.9f, 1.15f, 0.30f ),\r\n\t\tWheelbase = 1.55f,\r\n\t\tTrackWidth = 1.14f,\r\n\t\tRideHeight = 0.17f,\r\n\t\tGroundClearance = 0.08f,\r\n\t\tCenterOfMassDrop = 0.02f, // tiny chassis: a deep drop puts CoM below the wheels\r\n\t\tWheelRadius = 0.16f,\r\n\t\tWheelInertia = 0.18f,\r\n\t\tSuspensionTravel = 0.14f,\r\n\t\tSpringRate = 24000f,\r\n\t\tDamperRate = 1600f,\r\n\t\tLayout = DriveLayout.RWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 52->62.4, RedlineRpm 9000->10800 (top-gear top speed +20%).\r\n\t\tPeakTorque = 62.4f, // punchy launch, still shifts out of wheelspin quickly\r\n\t\tIdleRpm = 1400f,\r\n\t\tRedlineRpm = 10800f,\r\n\t\tEngineInertia = 0.05f,\r\n\t\tEngineBrakeTorque = 8f,\r\n\t\t// The kart keeps the buzzier single loop; base pitch >1 preserves its whine, while the narrowed\r\n\t\t// band tames the redline chipmunk.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_b_sport_purr.sound\",\r\n\t\tEnginePitchBase = 1.2f,\r\n\t\tGearRatios = new[] { 3.4f, 2.4f, 1.8f, 1.4f, 1.1f },\r\n\t\tReverseRatio = 3.4f,\r\n\t\tFinalDrive = 6.3f,\r\n\t\tShiftUpRpm = 8000f,\r\n\t\tShiftDownRpm = 4000f,\r\n\t\tBrakeTorque = 560f,\r\n\t\tHandbrakeTorque = 900f,\r\n\t\tMaxSteerAngle = 31f,\r\n\t\tHighSpeedSteerAngle = 9f,\r\n\t\t// sticky kart slicks\r\n\t\tLongitudinalCurve = new TireCurve( 0.09f, 1.55f, 0.40f, 1.24f ),\r\n\t\tLateralCurve = new TireCurve( 0.12f, 1.66f, 0.45f, 1.32f ),\r\n\t\tAbsSlipThreshold = 0.20f, // earlier release for the lockup-prone kart\r\n\t\tHandbrakeGripScale = 0.70f, // mild rear cut \u2014 the light kart already rotates\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tSportTcSlipTarget = 0.40f,  // loosest Sport TC \u2014 the light kart is meant to be playful\r\n\t\tSportStabilityScale = 0.45f, // gentle yaw damp; the kart rotates easily so keep it light\r\n\t\tHandbrakeSlipCap = -0.7f, // keep the rears rotating mid-slide for a cleaner drift exit\r\n\t\t// Recumbent kart driver pose: reclines with legs extended forward to the pedals.\r\n\t\tDriverSit = 4,\r\n\t\tDriverSitOffsetHeight = 0f,\r\n\t\tDefaultAssists = AssistLevel.Casual, // default fun car: keep it catchable\r\n\t\tTint = new Color( 0.58f, 0.83f, 0.07f ), // acid green\r\n\t};\r\n\r\n\tpublic static CarDefinition Coupe => new()\r\n\t{\r\n\t\tName = \"Sports Coupe\",\r\n\t\tMass = 1420f,\r\n\t\tBodySize = new Vector3( 4.4f, 1.85f, 1.25f ),\r\n\t\tWheelbase = 2.7f,\r\n\t\tTrackWidth = 1.60f,\r\n\t\tLayout = DriveLayout.RWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 340->408, RedlineRpm 7200->8640 (top-gear top speed +20%).\r\n\t\t// ShiftUpRpm left at 6600 (unchanged) so the shift ladder holds in ground-speed terms; only the\r\n\t\t// top gear extends to the new redline.\r\n\t\tPeakTorque = 408f,\r\n\t\tRedlineRpm = 8640f,\r\n\t\tShiftUpRpm = 6600f,\r\n\t\t// Real recorded car set (shared with the hatch): deep muscle idle crossfaded up to a\r\n\t\t// revving high layer; base pitch neutral so it sweeps the band cleanly.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_real_low.sound\",\r\n\t\tEngineSoundEventHigh = \"sounds/engine/engine_real_high.sound\",\r\n\t\tEnginePitchBase = 1.0f,\r\n\t\tLongitudinalCurve = new TireCurve( 0.09f, 1.50f, 0.40f, 1.20f ),\r\n\t\tLateralCurve = new TireCurve( 0.13f, 1.69f, 0.50f, 1.36f ),\r\n\t\tHandbrakeGripScale = 0.55f, // rears break loose under handbrake (J-turn initiation)\r\n\t\tHandbrakeSlipCap = -0.7f,\r\n\t\tSpringRate = 46000f,\r\n\t\tDamperRate = 3600f,\r\n\t\tBrakeTorque = 6200f,\r\n\t\tWheelRadius = 0.33f,\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tSportTcSlipTarget = 0.35f,   // Sport TC: rears still slide on throttle but can't free-spin to redline\r\n\t\tSportStabilityScale = 0.5f,  // half-authority yaw damp: the counter-steer pendulum stays catchable\r\n\t\tMaxSteerAngle = 30f,\r\n\t\tHighSpeedSteerAngle = 10f, // sportiest car gets the most high-speed turn-in\r\n\t\tTint = new Color( 0.80f, 0.05f, 0.07f ), // bright signal red\r\n\t};\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/VehicleWheel.cs",
            "FileName": "VehicleWheel.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// One wheel: shapecast ground detection + spring/damper suspension (skeleton pattern from\r\n/// Facepunch/sbox-libwheel, MIT) with the friction model replaced by the spec \u00a75.2.1 slip\r\n/// physics \u2014 wheel angular velocity as integrated state, slip ratio/angle into peaked curves,\r\n/// friction ellipse, load sensitivity, low-speed blend.\r\n///\r\n/// Driven by <see cref=\"VehicleController\"/> in substeps; forces are accumulated across\r\n/// substeps and applied once per fixed update (ApplyForceAt applies for the whole step).\r\n/// All internal math in SI.\r\n/// </summary>\r\npublic sealed class VehicleWheel : Component\r\n{\r\n\t// Configured by VehicleFactory from CarDefinition\r\n\tpublic float Radius { get; set; } = 0.31f;\r\n\tpublic float Inertia { get; set; } = 1.2f;\r\n\tpublic float SuspensionTravel { get; set; } = 0.18f;\r\n\tpublic float SpringRate { get; set; } = 38000f;\r\n\tpublic float DamperRate { get; set; } = 2800f;\r\n\tpublic TireCurve LongitudinalCurve { get; set; }\r\n\tpublic TireCurve LateralCurve { get; set; }\r\n\tpublic float LoadSensitivity { get; set; } = 0.06f;\r\n\tpublic float StaticLoad { get; set; } = 3000f; // N, set by factory: mass\u00b7g/4\r\n\tpublic bool IsSteering { get; set; }\r\n\tpublic bool IsDriven { get; set; }\r\n\tpublic bool HasHandbrake { get; set; }\r\n\tpublic float GripScale { get; set; } = 1f; // live multiplier, e.g. handbrake drift cuts rear grip\r\n\tpublic float ParkBrakeScale { get; set; } = 1f; // anti-jitter stiction strength; controller fades it with throttle\r\n\r\n\t/// <summary>Per-substep DRIVE-side angular-velocity cap (rad/s), set by the controller each\r\n\t/// substep to the drivetrain's redline-equivalent wheel speed for the current gear (see\r\n\t/// <see cref=\"Drivetrain.RedlineWheelSpeed\"/>). Drive torque can never push the wheel PAST this\r\n\t/// within a substep; the ground (tire reaction) still can, and a pre-existing overspeed is never\r\n\t/// yanked down (no phantom braking). float.MaxValue = no cap (undriven wheels, neutral).</summary>\r\n\tpublic float DriveOmegaCap { get; set; } = float.MaxValue;\r\n\r\n\t// State\r\n\tpublic float AngularVelocity { get; private set; } // rad/s, +forward\r\n\tpublic float SteerAngle { get; set; } // degrees, set by controller\r\n\tpublic float SlipRatio { get; private set; }\r\n\tpublic float SlipAngle { get; private set; } // radians\r\n\tpublic float Load { get; private set; } // N\r\n\tpublic bool IsGrounded => _trace.Hit;\r\n\tpublic float GroundSpeed { get; private set; } // m/s along wheel forward\r\n\tpublic float SuspensionLength { get; private set; } // m, attach point to wheel center, for visuals\r\n\r\n\tpublic string DebugTrace => !_trace.Hit\r\n\t\t? \"miss\"\r\n\t\t: $\"{_trace.GameObject?.Name ?? \"?\"}{(_trace.StartedSolid ? \"!SOLID\" : \"\")} d{_trace.Distance:F1}u n{_trace.Normal.z:F2} sf{(_trace.Surface?.Friction ?? -1f):F2}\";\r\n\r\n\tconst float TraceSphereRadius = 1f * Units.UnitsToMeters; // shapecast sphere, see DoTrace\r\n\r\n\tRigidbody _rigidbody;\r\n\tSceneTraceResult _trace;\r\n\tVector3 _accumulatedForce; // N, world space\r\n\tVector3 _forcePosition;\r\n\tint _substeps;\r\n\tfloat _smoothedSlipAngle;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t_rigidbody = Components.GetInAncestorsOrSelf<Rigidbody>();\r\n\t}\r\n\r\n\t/// <summary>Trace and reset accumulators. Call once per fixed update, before substeps.</summary>\r\n\tpublic void BeginStep()\r\n\t{\r\n\t\tDoTrace();\r\n\t\t_accumulatedForce = Vector3.Zero;\r\n\t\t_substeps = 0;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// One physics substep. driveTorque/brakeTorque in N\u00b7m. Accumulates chassis force,\r\n\t/// integrates wheel spin.\r\n\t/// </summary>\r\n\tpublic void Substep( float dt, float driveTorque, float brakeTorque )\r\n\t{\r\n\t\t_substeps++;\r\n\r\n\t\tvar up = _rigidbody.WorldRotation.Up;\r\n\r\n\t\t// a grazing contact (car tipped over, wheel scraping a wall) is not suspension:\r\n\t\t// generating spring force there self-propels a flipped car along the ground\r\n\t\tbool validContact = IsGrounded && Vector3.Dot( _trace.Normal, up ) > 0.4f;\r\n\r\n\t\tif ( !validContact )\r\n\t\t{\r\n\t\t\tLoad = 0f;\r\n\t\t\tSlipRatio = 0f;\r\n\t\t\tSlipAngle = 0f;\r\n\t\t\tSuspensionLength = SuspensionTravel;\r\n\t\t\t// AIRBORNE DRIVE GATE (ramp lip-cluster fix 2026-07-21, LIVE-UNVERIFIED): a driven wheel\r\n\t\t\t// with no valid contact gets ZERO drive torque - it freewheels (brakes still act). With\r\n\t\t\t// drive torque passed through, an unloaded driven wheel flared to redline-equivalent\r\n\t\t\t// within ~3 ticks of leaving a kicker lip, the engine pinned at 94-98% redline for the\r\n\t\t\t// whole flight (live capture: rpm 6008-6010 at the lip), the drivetrain's limiter-camp\r\n\t\t\t// escape upshifted MID-AIR ~0.27 s into every full-throttle flight at 20-30 m/s, and the\r\n\t\t\t// car touched down at the flared surface speed: slip +0.4..+0.5, a 10-12 kN (~1 g) tire\r\n\t\t\t// spike for ~2 ticks, the skid chirp, and a landing one gear too tall (post-landing drive\r\n\t\t\t// force -26..-37% vs pre-lip) - the felt \"hitch going off the ramps\" at any speed\r\n\t\t\t// (offline quantification: tools/ramp_lip_drivetrain_port.py). Zeroing drive here starves\r\n\t\t\t// that whole cascade: omega holds ~rolling speed, rpm stays steady, the escape shift\r\n\t\t\t// never arms in air, and touchdown slip is ~0. Flat-ground behavior is byte-identical BY\r\n\t\t\t// CONSTRUCTION (this branch never runs with valid ground contact; grounded-tick A/B in\r\n\t\t\t// the port asserts bit-identical trajectories). Registered prediction at commit time: the\r\n\t\t\t// owner's Sim-mode discriminator will NOT kill the hitch (the cascade is\r\n\t\t\t// assist-independent); result to be recorded next to this comment either way.\r\n\t\t\tIntegrateWheelSpin( dt, 0f, brakeTorque, 0f );\r\n\t\t\t// Airborne wind-down is bearing drag only: 2%/s. The previous 0.5f here was 50%/s\r\n\t\t\t// (documented in round 4 as \"0.5%/s\", a 100x misread of its own constant): after a\r\n\t\t\t// 1.2 s flight the wheels arrived at ~55% of road speed and braked the car while\r\n\t\t\t// spinning back up (flight recorder 2026-07-21: ~2 m/s of the touchdown loss plus\r\n\t\t\t// the landing skid chirp came from exactly this). Real free wheels barely slow.\r\n\t\t\tAngularVelocity *= 1f - 0.02f * dt;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// --- suspension ---\r\n\t\t// Force acts along the CONTACT NORMAL, not body-up: body-up creates a feedback\r\n\t\t// loop where a tilted car pushes itself sideways, slides, and flips.\r\n\t\t// Compression is clamped to physical travel: bottoming out is a rigid contact\r\n\t\t// for the body collider (bump stop), not unbounded spring force.\r\n\t\tvar normal = _trace.Normal;\r\n\t\tfloat restLength = SuspensionTravel + Radius;\r\n\t\tfloat hitLength = _trace.Distance * Units.UnitsToMeters + TraceSphereRadius; // sphere stops one radius short\r\n\t\tfloat compression = Math.Clamp( restLength - hitLength, 0f, SuspensionTravel );\r\n\t\tSuspensionLength = Math.Clamp( hitLength - Radius, 0f, SuspensionTravel );\r\n\r\n\t\tvar velAtWheel = _rigidbody.GetVelocityAtPoint( WorldPosition ) * Units.UnitsToMeters;\r\n\t\tfloat compressionSpeed = -Vector3.Dot( velAtWheel, normal );\r\n\r\n\t\tfloat springForce = SpringRate * compression + DamperRate * compressionSpeed;\r\n\t\tLoad = Math.Clamp( springForce, 0f, StaticLoad * 4f );\r\n\r\n\t\t_accumulatedForce += normal * Load;\r\n\t\t_forcePosition = _trace.EndPosition;\r\n\r\n\t\t// --- tire frame on the contact plane ---\r\n\t\tvar steerRot = _rigidbody.WorldRotation * Rotation.FromYaw( SteerAngle );\r\n\t\tvar forward = (steerRot.Forward - normal * Vector3.Dot( steerRot.Forward, normal )).Normal;\r\n\t\tvar side = Vector3.Cross( normal, forward );\r\n\r\n\t\tvar contactVel = _rigidbody.GetVelocityAtPoint( _trace.EndPosition ) * Units.UnitsToMeters;\r\n\t\tfloat vLong = Vector3.Dot( contactVel, forward );\r\n\t\tfloat vLat = Vector3.Dot( contactVel, side );\r\n\t\tGroundSpeed = vLong;\r\n\r\n\t\t// --- slip ---\r\n\t\t// longitudinal uses RAW slip + a one-substep force clamp below (smoothing it added\r\n\t\t// ~100 ms of feedback lag that turned wheel spin into a rhythmic surge);\r\n\t\t// lateral keeps light relaxation, its loop is chassis-side and much slower\r\n\t\tfloat slipVelocity = AngularVelocity * Radius - vLong; // m/s at the contact patch\r\n\t\tSlipRatio = slipVelocity / MathF.Max( MathF.Abs( vLong ), 2.0f );\r\n\r\n\t\tfloat rawSlipAngle = MathF.Atan2( vLat, MathF.Abs( vLong ) + 0.7f );\r\n\t\tfloat relax = Math.Clamp( (MathF.Abs( vLong ) + 1f) * dt / 0.2f, 0.1f, 1f );\r\n\t\t_smoothedSlipAngle += (rawSlipAngle - _smoothedSlipAngle) * relax;\r\n\t\tSlipAngle = _smoothedSlipAngle;\r\n\r\n\t\t// --- forces from curves, load sensitivity, surface grip ---\r\n\t\tfloat surfaceGrip = _trace.Surface?.Friction ?? 1f;\r\n\t\tfloat loadFactor = 1f - LoadSensitivity * MathF.Max( 0f, Load / StaticLoad - 1f );\r\n\t\tfloat maxForce = Load * loadFactor * surfaceGrip * GripScale;\r\n\r\n\t\tfloat fx = LongitudinalCurve.Evaluate( SlipRatio ) * MathF.Sign( SlipRatio ) * maxForce;\r\n\t\tfloat fy = -LateralCurve.Evaluate( SlipAngle ) * MathF.Sign( SlipAngle ) * maxForce;\r\n\r\n\t\t// one-substep stability clamp: never push the wheel past ground-speed match within\r\n\t\t// a single substep (gain*dt/inertia > 2 here, the raw loop oscillates without this)\r\n\t\tfloat fxStable = MathF.Abs( slipVelocity ) * Inertia / (Radius * Radius * dt);\r\n\t\tfx = Math.Clamp( fx, -fxStable, fxStable );\r\n\r\n\t\t// --- friction ellipse (spec \u00a75.2.1.4) ---\r\n\t\tfloat combined = MathF.Sqrt( fx * fx + fy * fy );\r\n\t\tif ( combined > maxForce && combined > 0.001f )\r\n\t\t{\r\n\t\t\tfloat scale = maxForce / combined;\r\n\t\t\tfx *= scale;\r\n\t\t\tfy *= scale;\r\n\t\t}\r\n\r\n\t\t// --- low-speed parking blend: kill standstill jitter ---\r\n\t\t// force is capped at what stops this wheel's mass share within one fixed frame\r\n\t\t// (spec 5.2.1.6) \u2014 uncapped, it overshoots and becomes a self-sustaining oscillator\r\n\t\tvar planarVel = forward * vLong + side * vLat;\r\n\t\tfloat planarSpeed = planarVel.Length;\r\n\t\tfloat parkOmegaBlend = 0f;\r\n\t\tif ( planarSpeed < 1.5f && MathF.Abs( AngularVelocity * Radius ) < 1.5f && planarSpeed > 0.001f )\r\n\t\t{\r\n\t\t\t// ParkBrakeScale: throttle dissolves the stiction \u2014 steered fronts were\r\n\t\t\t// \"parking\" against full-lock standstill launches (2 km/h crawls, tele 07-07)\r\n\t\t\tfloat blend = (1f - planarSpeed / 1.5f) * ParkBrakeScale;\r\n\t\t\tfloat massShare = StaticLoad / 9.81f; // kg carried by this wheel\r\n\t\t\tfloat frameDt = dt * VehicleController.Substeps;\r\n\t\t\tfloat stopForce = massShare * planarSpeed / frameDt * 0.8f;\r\n\t\t\tfloat parkMag = MathF.Min( MathF.Min( planarSpeed * Load * 1.5f, stopForce ), maxForce );\r\n\r\n\t\t\tvar park = -planarVel / planarSpeed * parkMag;\r\n\t\t\tfx = fx * (1f - blend) + Vector3.Dot( park, forward ) * blend;\r\n\t\t\tfy = fy * (1f - blend) + Vector3.Dot( park, side ) * blend;\r\n\t\t\tparkOmegaBlend = blend;\r\n\t\t}\r\n\r\n\t\t_accumulatedForce += forward * fx + side * fy;\r\n\r\n\t\tIntegrateWheelSpin( dt, driveTorque, brakeTorque, fx );\r\n\r\n\t\t// Park the wheel's SPIN, not just the chassis. The parking blend above brakes the chassis, but\r\n\t\t// its reaction torque (-fx*Radius, fed into IntegrateWheelSpin) spins this wheel up and, with\r\n\t\t// nothing anchoring omega at rest, it settles at a nonzero spin that keeps pushing the car: a\r\n\t\t// permanent slow creep with the tyre visibly rotating and the slip ratio pinging the skid\r\n\t\t// threshold (community report: \"unless you've perfectly stopped, it infinitely skids and rotates\r\n\t\t// the wheels in place\"). Pull omega toward the ground-rolling speed vLong/Radius (= 0 at a true\r\n\t\t// standstill) by the SAME blend, so a parked, unbraked wheel settles to zero spin and zero slip.\r\n\t\t// Offline sim: a 0.2 m/s creep that never stopped and skidded 50-100% of frames now settles to\r\n\t\t// under 1 mm/s with zero skid, while cruise (over 1.5 m/s, blend inert) is byte-identical. Fades\r\n\t\t// out with throttle (blend carries ParkBrakeScale) so standstill launches are unaffected.\r\n\t\tif ( parkOmegaBlend > 0f )\r\n\t\t\tAngularVelocity = MathX.Lerp( AngularVelocity, vLong / Radius, parkOmegaBlend );\r\n\t}\r\n\r\n\t// Cap-aware drive-torque rolloff onset (kart cap-camping fix 2026-07-18): drive torque begins\r\n\t// fading toward zero once the wheel's own spin reaches this fraction of DriveOmegaCap; below it\r\n\t// the rolloff is inert so below-cap behavior is byte-identical.\r\n\tconst float DriveRolloffOnset = 0.90f;\r\n\r\n\tvoid IntegrateWheelSpin( float dt, float driveTorque, float brakeTorque, float tireForce )\r\n\t{\r\n\t\tfloat preOmega = AngularVelocity;\r\n\t\tbool driving = driveTorque != 0f;\r\n\t\tfloat driveDir = driving ? MathF.Sign( driveTorque ) : 0f;\r\n\r\n\t\t// Cap-aware drive-torque rolloff (kart \"stuck turning\" fix 2026-07-18). The per-substep\r\n\t\t// clamp below is a hard backstop, but with the clamp ALONE a driven wheel under sustained\r\n\t\t// full torque CAMPS exactly at the cap; when forward speed then collapses in a corner the\r\n\t\t// slip ratio blows far past the grip peak (live: 7+) and the longitudinal tail force eats the\r\n\t\t// friction ellipse, killing rear lateral grip so the yaw holds against countersteer. Fade\r\n\t\t// drive torque to zero as the wheel approaches the cap so it settles OFF the cap instead of\r\n\t\t// camping on it. Smoothstep, not a hard corner, to avoid a torque-fade limit cycle. Inert\r\n\t\t// below the onset (approach <= DriveRolloffOnset) so below-cap behavior is byte-identical.\r\n\t\tif ( driving && DriveOmegaCap < float.MaxValue )\r\n\t\t{\r\n\t\t\tfloat approach = driveDir * preOmega / DriveOmegaCap;\r\n\t\t\tif ( approach > DriveRolloffOnset )\r\n\t\t\t{\r\n\t\t\t\tfloat f = Math.Clamp( (1f - approach) / (1f - DriveRolloffOnset), 0f, 1f );\r\n\t\t\t\tdriveTorque *= f * f * (3f - 2f * f);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// drive + tire reaction\r\n\t\tfloat torque = driveTorque - tireForce * Radius;\r\n\t\tAngularVelocity += torque / Inertia * dt;\r\n\r\n\t\t// Per-substep drive-side overshoot clamp (kart high-PeakTorque wobble hunt 2026-07-18).\r\n\t\t// The rev limiter zeroes torque only on the substep AFTER wheel-implied rpm crosses redline,\r\n\t\t// so one substep of unlimited drive torque on a light wheel overshoots redline-equivalent\r\n\t\t// omega by up to 6-8x (measured live: kart at 900 N-m spikes to 289-339 rad/s vs the 44 rad/s\r\n\t\t// gear-1 redline equivalent; even stock 52 N-m reaches 141). The spike is what lets an\r\n\t\t// unloaded rear diverge violently from its loaded twin over any perturbation (the felt\r\n\t\t// \"individual tires have different traction\" left-right wobble). Clamp: DRIVE torque may\r\n\t\t// never push omega past the cap within a substep. Signed by drive direction so reverse works;\r\n\t\t// cap floors at the pre-integration omega so ground-driven overspeed (downhill coast) is\r\n\t\t// never yanked down, and the ground reaction path is untouched. Guarded on the ORIGINAL drive\r\n\t\t// intent so a rolloff that faded torque to zero still cannot let the wheel blow past the cap.\r\n\t\tif ( driving && DriveOmegaCap < float.MaxValue )\r\n\t\t{\r\n\t\t\tfloat cap = MathF.Max( DriveOmegaCap, driveDir * preOmega );\r\n\t\t\tAngularVelocity = driveDir > 0f\r\n\t\t\t\t? MathF.Min( AngularVelocity, cap )\r\n\t\t\t\t: MathF.Max( AngularVelocity, -cap );\r\n\t\t}\r\n\r\n\t\t// brakes can stop the wheel but never reverse it within a step\r\n\t\tif ( brakeTorque > 0f )\r\n\t\t{\r\n\t\t\tfloat brakeDelta = brakeTorque / Inertia * dt;\r\n\t\t\tAngularVelocity = MathF.Abs( AngularVelocity ) <= brakeDelta\r\n\t\t\t\t? 0f\r\n\t\t\t\t: AngularVelocity - MathF.Sign( AngularVelocity ) * brakeDelta;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Apply the substep-averaged force to the chassis. Call once per fixed update.</summary>\r\n\tpublic void EndStep()\r\n\t{\r\n\t\tif ( _substeps == 0 || _accumulatedForce.IsNearZeroLength )\r\n\t\t\treturn;\r\n\r\n\t\tvar averaged = _accumulatedForce / _substeps;\r\n\t\t_rigidbody.ApplyForceAt( _forcePosition, averaged * Units.MetersToUnits );\r\n\t}\r\n\r\n\tvoid DoTrace()\r\n\t{\r\n\t\tvar down = _rigidbody.WorldRotation.Down;\r\n\t\tfloat lengthUnits = (SuspensionTravel + Radius) * Units.MetersToUnits;\r\n\r\n\t\t_trace = Scene.Trace\r\n\t\t\t.Radius( 1f )\r\n\t\t\t.IgnoreGameObjectHierarchy( _rigidbody.GameObject )\r\n\t\t\t.WithoutTags( \"car\" )\r\n\t\t\t.FromTo( WorldPosition, WorldPosition + down * lengthUnits )\r\n\t\t\t.Run();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "DriveInputs.cs",
            "FileName": "DriveInputs.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Per-frame driver intent, the value-struct swap point at VehicleController's input seam\r\n/// (<see cref=\"VehicleController.InputOverride\"/>). Carries the raw intent the controller samples\r\n/// from a device: the move axis (forward/back + steer), the handbrake action, and the sequential\r\n/// shift requests. A keyboard/gamepad/wheel source or a scripted source (a test pilot, an AI) all\r\n/// produce one of these, so the controller's gear/reverse/steer-ramp logic stays source-agnostic.\r\n///\r\n/// The live-device sampler <see cref=\"SampleDeviceInputs\"/> and its gamepad shaping helpers live\r\n/// here (lifted out of the controller): the controller either consumes an injected override or calls\r\n/// this sampler, and nothing else in the class touches device input.\r\n/// </summary>\r\npublic struct DriveInputs\r\n{\r\n\t/// <summary>-1..1 signed drive axis: +forward accelerates, -back brakes then engages reverse near a\r\n\t/// stop. Built in <see cref=\"SampleDeviceInputs\"/> as (throttle \u2212 brake) where each channel is the\r\n\t/// MAX of the keyboard/stick component (<c>Input.AnalogMove.x</c>) and the ANALOG gamepad trigger\r\n\t/// pull (<c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c>, 0..1) \u2014 so a partial trigger\r\n\t/// gives a partial pedal. A scripted source sets this float directly.</summary>\r\n\tpublic float MoveForward;\r\n\r\n\t/// <summary>-1..1, maps to <c>Input.AnalogMove.y</c> (gamepad path reshaped by the gamepad deadzone\r\n\t/// + response curve). Note <c>Rotation.FromYaw(+)</c> is a LEFT/CCW turn, so +Steer steers left.</summary>\r\n\tpublic float Steer;\r\n\r\n\t/// <summary>The handbrake / drift button: keyboard \"Jump\" (Space), or gamepad A (Jump's own\r\n\t/// GamepadCode) or the left bumper (\"Handbrake\" action).</summary>\r\n\tpublic bool Handbrake;\r\n\r\n\t/// <summary>Edge-triggered request to shift UP one gear (sequential MANUAL mode). Keyboard E /\r\n\t/// gamepad R1 while live; a scripted source pulses it for one tick. The controller rising-edge-\r\n\t/// detects it, so a source that holds it across ticks still shifts exactly once.</summary>\r\n\tpublic bool ShiftUp;\r\n\r\n\t/// <summary>Edge-triggered request to shift DOWN one gear (sequential MANUAL mode). Keyboard Q /\r\n\t/// gamepad L1. Same one-shot rising-edge semantics as <see cref=\"ShiftUp\"/>.</summary>\r\n\tpublic bool ShiftDown;\r\n\r\n\t/// <summary>Edge-triggered request to toggle the transmission mode AUTO\u2194MANUAL. Keyboard G /\r\n\t/// gamepad D-pad down. Same one-shot rising-edge semantics as <see cref=\"ShiftUp\"/>.</summary>\r\n\tpublic bool ShiftModeToggle;\r\n\r\n\t// Gamepad tier: deadzone + response curve for the analog steer axis.\r\n\tconst float GamepadSteerDeadzone = 0.12f;\r\n\tconst float GamepadSteerCurvePower = 1.6f; // >1 softens the center for fine control, still reaches full lock\r\n\r\n\t// Analog throttle/brake tier: a small trigger deadzone so resting triggers can't creep the pedals.\r\n\t// The engine ALSO applies its own 12.5% deadzone one layer down (Controller.SetAxis zeroes any\r\n\t// |axis| <= 0.125 before Input.GetAnalog sees it), so values arrive as 0 or >~0.125 and this floor\r\n\t// is a belt-and-suspenders guard that also rescales so full pull still reaches 1.0.\r\n\tconst float GamepadTriggerDeadzone = 0.05f;\r\n\r\n\t/// <summary>Sample the live input devices into a DriveInputs value (this is the keyboard/gamepad\r\n\t/// source; other sources produce the same struct and set <see cref=\"VehicleController.InputOverride\"/>).\r\n\t///\r\n\t/// Steering rides <c>Input.AnalogMove.y</c> straight off the left stick; keyboard emits exact\r\n\t/// -1/0/1 through the same path and passes through <see cref=\"ApplyGamepadSteerCurve\"/> unchanged.\r\n\t///\r\n\t/// Throttle/brake are VARIABLE per device: keyboard W/S ride <c>Input.AnalogMove.x</c> as an exact\r\n\t/// \u00b11 digital forward/back; the gamepad triggers are read as a true ANALOG 0..1 pull via\r\n\t/// <c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c> (right = gas, left = brake). The two\r\n\t/// devices combine per channel by MAX \u2014 so either device works and neither fights the other \u2014 then\r\n\t/// the net (throttle \u2212 brake) folds into the single signed <see cref=\"MoveForward\"/> scalar. On\r\n\t/// keyboard <c>Input.GetAnalog</c> returns 0, so keyboard-only players are byte-identical.</summary>\r\n\tpublic static DriveInputs SampleDeviceInputs()\r\n\t{\r\n\t\tvar move = Input.AnalogMove;\r\n\r\n\t\t// keyboard/stick forward+back split off the shared move axis (W = +x, S = -x)\r\n\t\tfloat keyThrottle = MathF.Max( 0f, move.x );\r\n\t\tfloat keyBrake = MathF.Max( 0f, -move.x );\r\n\r\n\t\t// gamepad triggers, true analog 0..1 (right = gas, left = brake)\r\n\t\tfloat triggerThrottle = ReadTrigger( InputAnalog.RightTrigger );\r\n\t\tfloat triggerBrake = ReadTrigger( InputAnalog.LeftTrigger );\r\n\r\n\t\t// MAX blend per channel so either device drives the pedal, neither overrides the other\r\n\t\tfloat throttle = MathF.Max( keyThrottle, triggerThrottle );\r\n\t\tfloat brake = MathF.Max( keyBrake, triggerBrake );\r\n\t\tfloat moveForward = Math.Clamp( throttle - brake, -1f, 1f );\r\n\r\n\t\treturn new DriveInputs\r\n\t\t{\r\n\t\t\tMoveForward = moveForward,\r\n\t\t\tSteer = ApplyGamepadSteerCurve( move.y ),\r\n\t\t\tHandbrake = Input.Down( \"Jump\" ) || Input.Down( \"Handbrake\" ),\r\n\t\t};\r\n\t}\r\n\r\n\t/// <summary>Read a gamepad trigger as a linear 0..1 pull with a small deadzone floor (rescaled so\r\n\t/// full pull still reaches 1.0). <c>Input.GetAnalog</c> already returns 0 for a trigger on keyboard,\r\n\t/// so this only shapes the gamepad path.</summary>\r\n\tstatic float ReadTrigger( InputAnalog trigger )\r\n\t{\r\n\t\tfloat v = Math.Clamp( Input.GetAnalog( trigger ), 0f, 1f );\r\n\t\tif ( v < GamepadTriggerDeadzone )\r\n\t\t\treturn 0f;\r\n\t\treturn (v - GamepadTriggerDeadzone) / (1f - GamepadTriggerDeadzone);\r\n\t}\r\n\r\n\t/// <summary>Deadzone + power curve for the analog steer axis. Values under the deadzone snap to 0;\r\n\t/// the remaining range is rescaled so full stick deflection still reaches \u00b11 (no lost lock), then\r\n\t/// raised to <see cref=\"GamepadSteerCurvePower\"/> for a softer center. Keyboard's exact -1/0/1\r\n\t/// passes through unaffected (0 is inside the deadzone; 1 rescales to 1 and 1^n == 1).</summary>\r\n\tstatic float ApplyGamepadSteerCurve( float raw )\r\n\t{\r\n\t\tfloat mag = MathF.Abs( raw );\r\n\t\tif ( mag < GamepadSteerDeadzone )\r\n\t\t\treturn 0f;\r\n\r\n\t\tfloat t = Math.Clamp( (mag - GamepadSteerDeadzone) / (1f - GamepadSteerDeadzone), 0f, 1f );\r\n\t\treturn MathF.Sign( raw ) * MathF.Pow( t, GamepadSteerCurvePower );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/Demo/DemoTuningPanel.razor",
            "FileName": "DemoTuningPanel.razor",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "@namespace FieldGuide.VehiclePhysics\r\n@inherits PanelComponent\r\n\r\n@* Kit-native live tuning lab (demo layer). Toggled by the Tune action, bound to the car the chase\r\n   camera is following. Writes tuning changes straight onto the running car through the same paths a\r\n   consumer would use: it mutates the active CarDefinition (read live by the drivetrain and the brake\r\n   model) and pushes suspension/tire values onto the live wheels. This is a demo-scale lab, not the\r\n   full game panel: it exposes the highest-feel dials so the demo reads as a physics lab in the first\r\n   minute. Replace it with your own UI; it lives only in the demo scene. *@\r\n\r\n<root>\r\n\t@if ( Car.IsValid() )\r\n\t{\r\n\t\t@* Key legend, top-right: how to hop between the demo cars. Always visible; the tuning\r\n\t\t   chip/panel top-left covers the T binding. *@\r\n\t\t<div class=\"legend\">\r\n\t\t\t<div class=\"mono kbd\">[ ]</div>\r\n\t\t\t<div class=\"title\">switch car</div>\r\n\t\t</div>\r\n\t}\r\n\t@if ( !IsOpen && Car.IsValid() )\r\n\t{\r\n\t\t@* Collapsed legend chip: sits exactly where the expanded panel's top-left corner lands, so\r\n\t\t   pressing T reads as the chip expanding into the lab. Mouse-look stays with the camera. *@\r\n\t\t<div class=\"panel chip\" onclick=@ToggleOpen>\r\n\t\t\t<div class=\"head\">\r\n\t\t\t\t<div class=\"mono kbd\">@ToggleKeyLabel</div>\r\n\t\t\t\t<div class=\"title\">tuning</div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t}\r\n\t@if ( IsOpen && Car.IsValid() )\r\n\t{\r\n\t\t<div class=\"panel\">\r\n\t\t\t<div class=\"head\">\r\n\t\t\t\t<div class=\"title\">Tuning lab \u00b7 @CarName</div>\r\n\t\t\t\t<div class=\"mono kbd\">@ToggleKeyLabel</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"hint\">Tuning the car you are driving. Changes apply live.</div>\r\n\r\n\t\t\t<div class=\"dials\">\r\n\t\t\t\t@foreach ( var d in SliderDials )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar dd = d;\r\n\t\t\t\t\t<div class=\"dial\">\r\n\t\t\t\t\t\t<div class=\"drow\">\r\n\t\t\t\t\t\t\t<div class=\"dname\">@d.Name</div>\r\n\t\t\t\t\t\t\t<div class=\"mono dval\">@d.Display()</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t<div class=\"dctl\">\r\n\t\t\t\t\t\t\t<div class=\"mono step\" onclick=@( () => Step( dd, -1 ) )>-</div>\r\n\t\t\t\t\t\t\t<div class=\"track\"\r\n\t\t\t\t\t\t\t\tonmousedown=@( e => Scrub( dd, e, true ) )\r\n\t\t\t\t\t\t\t\tonmousemove=@( e => Scrub( dd, e, false ) )>\r\n\t\t\t\t\t\t\t\t<div class=\"fill\" style=\"width: @WidthPct( dd )%\"></div>\r\n\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"mono step\" onclick=@( () => Step( dd, +1 ) )>+</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t}\r\n\r\n\t\t\t\t<div class=\"dial\">\r\n\t\t\t\t\t<div class=\"drow\">\r\n\t\t\t\t\t\t<div class=\"dname\">Assists</div>\r\n\t\t\t\t\t\t<div class=\"mono cycle\" onclick=@CycleAssists>@AssistLabel</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\r\n\t\t\t\t<div class=\"dial\">\r\n\t\t\t\t\t<div class=\"drow\">\r\n\t\t\t\t\t\t<div class=\"dname\">Tires</div>\r\n\t\t\t\t\t\t<div class=\"mono cycle\" onclick=@CycleTires>@TireLabel</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"foot\">\r\n\t\t\t\t<div class=\"btn\" onclick=@ResetToStock>Reset to stock</div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t}\r\n</root>\r\n\r\n@code {\r\n\t/// <summary>The car this panel tunes: the one the chase camera follows. Set by DemoBootstrap. On\r\n\t/// change the panel snapshots the car's pristine (authored) values so Reset-to-stock can restore\r\n\t/// them and the grip/torque multipliers rebase to the new car.</summary>\r\n\tVehicleController _car;\r\n\tpublic VehicleController Car\r\n\t{\r\n\t\tget => _car;\r\n\t\tset\r\n\t\t{\r\n\t\t\tif ( ReferenceEquals( _car, value ) )\r\n\t\t\t\treturn;\r\n\t\t\t_car = value;\r\n\t\t\tSnapshot();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Open state. Static so the kit's chase camera can read it through the\r\n\t/// <see cref=\"VehicleCamera.CursorModalOpen\"/> seam (DemoBootstrap wires that) without holding a\r\n\t/// reference to this panel. The demo runs exactly one panel, so a single flag is enough.</summary>\r\n\tpublic static bool IsOpen;\r\n\r\n\t// Toggle input action. Documented in the README input table; the host ProjectSettings/Input.config\r\n\t// ships it. Never Escape or an F-key.\r\n\tconst string ToggleAction = \"Tune\";\r\n\tstring ToggleKeyLabel => \"T\";\r\n\r\n\tstring CarName => Car?.Definition?.Name ?? \"Car\";\r\n\r\n\t// Live multipliers over the pristine base. Grip scales the (preset-selected) tire curves; torque\r\n\t// scales the authored peak engine torque.\r\n\tfloat _gripScale = 1f;\r\n\tfloat _torqueScale = 1f;\r\n\tint _tirePreset; // 0 Stock, 1 Street, 2 Sport, 3 Offroad\r\n\r\n\t// Pristine snapshot, captured value-by-value when the car is bound (its definition is untouched at\r\n\t// that point). Value types only (floats, TireCurve struct, enum), so later live tuning of the\r\n\t// definition can never corrupt these; Reset restores from here.\r\n\tbool _snapped;\r\n\tfloat _stockPeakTorque, _stockSpring, _stockDamper, _stockTravel, _stockBrake;\r\n\tTireCurve _stockLat, _stockLong;\r\n\tAssistLevel _stockAssists;\r\n\r\n\t// Current UNSCALED tire base (Stock or a named preset). Grip multiplies these to get the live curves.\r\n\tTireCurve _baseLat, _baseLong;\r\n\r\n\tvoid Snapshot()\r\n\t{\r\n\t\tvar def = _car?.Definition;\r\n\t\t_dials = null;\r\n\t\tif ( def is null )\r\n\t\t{\r\n\t\t\t_snapped = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_stockPeakTorque = def.PeakTorque;\r\n\t\t_stockSpring = def.SpringRate;\r\n\t\t_stockDamper = def.DamperRate;\r\n\t\t_stockTravel = def.SuspensionTravel;\r\n\t\t_stockBrake = def.BrakeTorque;\r\n\t\t_stockLat = def.LateralCurve;\r\n\t\t_stockLong = def.LongitudinalCurve;\r\n\t\t// Stock assist = the authored definition default. Read it off the definition, not the\r\n\t\t// controller: the controller adopts DefaultAssists in its own OnStart, which may run a frame\r\n\t\t// or two after this bind, so its live Assists is not reliable yet.\r\n\t\t_stockAssists = def.DefaultAssists;\r\n\r\n\t\t_baseLat = _stockLat;\r\n\t\t_baseLong = _stockLong;\r\n\t\t_gripScale = 1f;\r\n\t\t_torqueScale = 1f;\r\n\t\t_tirePreset = 0;\r\n\t\t_snapped = true;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\t// ---- dial model ----\r\n\tclass Dial\r\n\t{\r\n\t\tpublic string Name;\r\n\t\tpublic float Min, Max, Step;\r\n\t\tpublic Func<float> Get;\r\n\t\tpublic Action<float> Set;\r\n\t\tpublic Func<string> Fmt;\r\n\t\tpublic string Display() => Fmt();\r\n\t}\r\n\r\n\tList<Dial> _dials;\r\n\tList<Dial> SliderDials => _dials ??= BuildDials();\r\n\r\n\tList<Dial> BuildDials()\r\n\t{\r\n\t\tif ( !Car.IsValid() )\r\n\t\t\treturn new List<Dial>();\r\n\r\n\t\tvar def = Car.Definition;\r\n\t\treturn new List<Dial>\r\n\t\t{\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Grip\", Min = 0.6f, Max = 2.2f, Step = 0.05f,\r\n\t\t\t\tGet = () => _gripScale, Set = SetGrip,\r\n\t\t\t\tFmt = () => _gripScale.ToString( \"0.00\" ) + \"x\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Drive torque\", Min = 0.5f, Max = 2.0f, Step = 0.05f,\r\n\t\t\t\tGet = () => _torqueScale, Set = SetTorqueScale,\r\n\t\t\t\tFmt = () => _torqueScale.ToString( \"0.00\" ) + \"x\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Suspension stiffness\", Min = 15000f, Max = 60000f, Step = 2000f,\r\n\t\t\t\tGet = () => def.SpringRate, Set = v => { def.SpringRate = v; ApplyWheels(); },\r\n\t\t\t\tFmt = () => def.SpringRate.ToString( \"0\" ) + \" N/m\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Suspension damping\", Min = 800f, Max = 6000f, Step = 200f,\r\n\t\t\t\tGet = () => def.DamperRate, Set = v => { def.DamperRate = v; ApplyWheels(); },\r\n\t\t\t\tFmt = () => def.DamperRate.ToString( \"0\" ) + \" Ns/m\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Suspension travel\", Min = 0.10f, Max = 0.35f, Step = 0.01f,\r\n\t\t\t\tGet = () => def.SuspensionTravel, Set = v => { def.SuspensionTravel = v; ApplyWheels(); },\r\n\t\t\t\tFmt = () => (def.SuspensionTravel * 100f).ToString( \"0\" ) + \" cm\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Brake force\", Min = 1500f, Max = 8000f, Step = 200f,\r\n\t\t\t\tGet = () => def.BrakeTorque, Set = v => def.BrakeTorque = v,\r\n\t\t\t\tFmt = () => def.BrakeTorque.ToString( \"0\" ) + \" Nm\",\r\n\t\t\t},\r\n\t\t};\r\n\t}\r\n\r\n\tfloat Frac( Dial d )\r\n\t{\r\n\t\tif ( d.Max <= d.Min )\r\n\t\t\treturn 0f;\r\n\t\treturn Math.Clamp( (d.Get() - d.Min) / (d.Max - d.Min), 0f, 1f );\r\n\t}\r\n\r\n\tstring WidthPct( Dial d ) =>\r\n\t\t(Frac( d ) * 100f).ToString( \"0.#\", System.Globalization.CultureInfo.InvariantCulture );\r\n\r\n\tvoid Step( Dial d, int clicks )\r\n\t{\r\n\t\tfloat v = Math.Clamp( d.Get() + clicks * d.Step, d.Min, d.Max );\r\n\t\td.Set( v );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\t// Click-to-jump + drag-to-scrub on the dial track (mirrors the game panel's proven pattern). jump\r\n\t// is true on mousedown (a bare click sets the value at the cursor) and false on mousemove (scrub\r\n\t// only while the track owns the press). Value = mouse local x over track width, snapped to the\r\n\t// dial's step, pushed through the SAME Set path as the +/- steps.\r\n\tvoid Scrub( Dial d, Sandbox.UI.PanelEvent ev, bool jump )\r\n\t{\r\n\t\tif ( ev is not Sandbox.UI.MousePanelEvent e )\r\n\t\t\treturn;\r\n\r\n\t\tvar track = e.This;\r\n\t\tif ( track is null )\r\n\t\t\treturn;\r\n\r\n\t\t// mousemove fires whether or not the button is held; only scrub while the track owns the press.\r\n\t\tif ( !jump && !track.PseudoClass.HasFlag( Sandbox.UI.PseudoClass.Active ) )\r\n\t\t\treturn;\r\n\r\n\t\tfloat w = track.Box.Rect.Width;\r\n\t\tif ( w <= 0f )\r\n\t\t\treturn;\r\n\r\n\t\tfloat frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );\r\n\t\tfloat v = d.Min + frac * (d.Max - d.Min);\r\n\t\tif ( d.Step > 0f )\r\n\t\t\tv = MathF.Round( v / d.Step ) * d.Step;\r\n\t\tv = Math.Clamp( v, d.Min, d.Max );\r\n\t\td.Set( v );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\t// ---- apply paths (same seams a consumer would use) ----\r\n\tstatic TireCurve Scaled( TireCurve c, float k ) =>\r\n\t\tnew( c.PeakSlip, c.PeakGrip * k, c.TailSlip, c.TailGrip * k );\r\n\r\n\t// Push the definition's suspension + tire values onto the live wheels. The factory copies these at\r\n\t// spawn; the wheel re-reads them every substep, so writing them here is the live-apply path.\r\n\tvoid ApplyWheels()\r\n\t{\r\n\t\tvar def = Car.Definition;\r\n\t\tforeach ( var w in Car.Wheels )\r\n\t\t{\r\n\t\t\tw.SpringRate = def.SpringRate;\r\n\t\t\tw.DamperRate = def.DamperRate;\r\n\t\t\tw.SuspensionTravel = def.SuspensionTravel;\r\n\t\t\tw.LateralCurve = def.LateralCurve;\r\n\t\t\tw.LongitudinalCurve = def.LongitudinalCurve;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid SetGrip( float k )\r\n\t{\r\n\t\t_gripScale = k;\r\n\t\tvar def = Car.Definition;\r\n\t\tdef.LateralCurve = Scaled( _baseLat, k );\r\n\t\tdef.LongitudinalCurve = Scaled( _baseLong, k );\r\n\t\tApplyWheels();\r\n\t}\r\n\r\n\t// Drive torque scale multiplies the authored peak torque. Drivetrain reads def.PeakTorque live\r\n\t// (it holds the same definition instance), so no drivetrain touch is needed.\r\n\tvoid SetTorqueScale( float k )\r\n\t{\r\n\t\t_torqueScale = k;\r\n\t\tCar.Definition.PeakTorque = _stockPeakTorque * k;\r\n\t}\r\n\r\n\tvoid CycleAssists()\r\n\t{\r\n\t\tCar.Assists = Car.Assists switch\r\n\t\t{\r\n\t\t\tAssistLevel.Casual => AssistLevel.Sport,\r\n\t\t\tAssistLevel.Sport => AssistLevel.Sim,\r\n\t\t\t_ => AssistLevel.Casual,\r\n\t\t};\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tstring AssistLabel => Car.IsValid() ? Car.Assists.ToString() : \"\";\r\n\r\n\t// Tire preset swaps the UNSCALED base curves, then re-applies the current grip multiplier so the\r\n\t// grip dial and the preset compose. Stock restores the car's own authored curves.\r\n\tvoid CycleTires()\r\n\t{\r\n\t\t_tirePreset = (_tirePreset + 1) % 4;\r\n\t\t(_baseLat, _baseLong) = _tirePreset switch\r\n\t\t{\r\n\t\t\t1 => (TireCurve.Street, TireCurve.Street),\r\n\t\t\t2 => (TireCurve.Sport, TireCurve.Sport),\r\n\t\t\t3 => (TireCurve.Offroad, TireCurve.Offroad),\r\n\t\t\t_ => (_stockLat, _stockLong),\r\n\t\t};\r\n\t\tSetGrip( _gripScale );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tstring TireLabel => _tirePreset switch\r\n\t{\r\n\t\t1 => \"Street\",\r\n\t\t2 => \"Sport\",\r\n\t\t3 => \"Offroad\",\r\n\t\t_ => \"Stock\",\r\n\t};\r\n\r\n\t// Re-apply the car's pristine authored values (captured at bind). Definitions in the demo roster\r\n\t// are fresh per car, so \"stock\" is unambiguous: the values this car spawned with.\r\n\tvoid ResetToStock()\r\n\t{\r\n\t\tif ( !Car.IsValid() || !_snapped )\r\n\t\t\treturn;\r\n\r\n\t\tvar def = Car.Definition;\r\n\t\tdef.PeakTorque = _stockPeakTorque;\r\n\t\tdef.SpringRate = _stockSpring;\r\n\t\tdef.DamperRate = _stockDamper;\r\n\t\tdef.SuspensionTravel = _stockTravel;\r\n\t\tdef.BrakeTorque = _stockBrake;\r\n\t\tdef.LateralCurve = _stockLat;\r\n\t\tdef.LongitudinalCurve = _stockLong;\r\n\r\n\t\t_gripScale = 1f;\r\n\t\t_torqueScale = 1f;\r\n\t\t_tirePreset = 0;\r\n\t\t_baseLat = _stockLat;\r\n\t\t_baseLong = _stockLong;\r\n\r\n\t\tCar.Assists = _stockAssists;\r\n\t\tApplyWheels();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t// Start COLLAPSED: the chip legend keeps the keybind discoverable while mouse-look stays\r\n\t\t// with the camera (an open lab on spawn captured the cursor before players ever drove,\r\n\t\t// owner call 2026-07-19). Static flag, so reset it here each session.\r\n\t\tIsOpen = false;\r\n\t}\r\n\r\n\tvoid ToggleOpen()\r\n\t{\r\n\t\tIsOpen = !IsOpen;\r\n\t\tMouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Input.Pressed( ToggleAction ) )\r\n\t\t{\r\n\t\t\tIsOpen = !IsOpen;\r\n\t\t\tMouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;\r\n\t\t\tStateHasChanged();\r\n\t\t}\r\n\r\n\t\t// The chase camera re-hides the cursor every frame while it owns it; hold it visible while open.\r\n\t\tif ( IsOpen )\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;\r\n\t}\r\n\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\t// Closed still renders the chip legend, and the chip waits on the car binding, so the\r\n\t\t// hash must move when the car arrives or the first build would stick on the empty tree.\r\n\t\tif ( !IsOpen )\r\n\t\t\treturn Car.IsValid() ? 1 : 0;\r\n\r\n\t\tvar h = new HashCode();\r\n\t\th.Add( _gripScale );\r\n\t\th.Add( _torqueScale );\r\n\t\th.Add( _tirePreset );\r\n\t\tif ( Car.IsValid() )\r\n\t\t{\r\n\t\t\tvar def = Car.Definition;\r\n\t\t\th.Add( def.SpringRate );\r\n\t\t\th.Add( def.DamperRate );\r\n\t\t\th.Add( def.SuspensionTravel );\r\n\t\t\th.Add( def.BrakeTorque );\r\n\t\t\th.Add( (int)Car.Assists );\r\n\t\t}\r\n\t\treturn h.ToHashCode();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "VehicleController.cs",
            "FileName": "VehicleController.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Drives one car: input \u2192 assists \u2192 drivetrain \u2192 wheel substeps.\r\n/// Runs 4 internal substeps per fixed update. PRECISION NOTE: this is drivetrain/wheel-STATE\r\n/// substepping, not a full 200 Hz contact simulation \u2014 the ground trace happens once per fixed\r\n/// update, the rigidbody does not advance between substeps (velocity-at-contact is re-sampled\r\n/// against the same body state), and the accumulated tire force is applied once, averaged. What the\r\n/// substeps genuinely refine: wheel angular velocity integration (slip-ratio stability at 4\u00d7 the\r\n/// rate), clutch/RPM coupling, and the TC feedback loop. Contact/chassis transients (washboard, curb\r\n/// strikes, landings) resolve at 50 Hz. Owner-simulated; proxies early-out.\r\n/// </summary>\r\npublic sealed class VehicleController : Component, Component.ICollisionListener\r\n{\r\n\tpublic const int Substeps = 4;\r\n\r\n\tpublic CarDefinition Definition { get; set; }\r\n\tpublic List<VehicleWheel> Wheels { get; } = new();\r\n\tpublic Drivetrain Drivetrain { get; private set; }\r\n\r\n\t[Property] public AssistLevel Assists { get; set; } = AssistLevel.Casual;\r\n\r\n\t/// <summary>Optional assist level a spawn path wants this car to adopt on start, instead of the\r\n\t/// definition default. Set by a spawn path that carries a chosen assist level across a respawn or\r\n\t/// car swap. It exists because <see cref=\"OnStart\"/> runs a frame or two AFTER <c>Components.Create</c>\r\n\t/// \u2014 i.e. after the spawning code has already run \u2014 so a plain post-spawn <c>Assists = x</c> is\r\n\t/// overwritten by the default-init in <see cref=\"OnStart\"/>. When non-null, OnStart adopts THIS\r\n\t/// value instead; null keeps the definition default. Harmless regardless of the exact create/start\r\n\t/// ordering: if OnStart happened to run first, the spawn path's direct <c>Assists</c> set still wins.</summary>\r\n\tpublic AssistLevel? InitialAssists { get; set; }\r\n\r\n\tpublic float Throttle { get; private set; }\r\n\tpublic float Brake { get; private set; }\r\n\tpublic float Steer { get; private set; } // -1..1\r\n\tpublic bool Handbrake { get; private set; }\r\n\tpublic float SpeedMs => _rigidbody.IsValid() ? (_rigidbody.Velocity * Units.UnitsToMeters).Length : 0f;\r\n\r\n\t/// <summary>\r\n\t/// Signed longitudinal speed (m/s) for a HUD speedo: the magnitude equals <see cref=\"SpeedMs\"/>\r\n\t/// (forward reads exactly as before), and the sign is the car's travel direction along its own\r\n\t/// facing \u2014 NEGATIVE while rolling backwards. The sign source is the SAME forward-axis projection\r\n\t/// <see cref=\"ApplySpinRecoveryAssist\"/> and gear-engage already use\r\n\t/// (velocity \u00b7 <see cref=\"Sandbox.GameObject.WorldRotation\"/>.Forward). DISPLAY read only \u2014 no\r\n\t/// physics or telemetry consumes it.\r\n\t/// </summary>\r\n\tpublic float SignedSpeedMs\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif ( !_rigidbody.IsValid() )\r\n\t\t\t\treturn 0f;\r\n\r\n\t\t\tfloat forward = Vector3.Dot( _rigidbody.Velocity, WorldRotation.Forward );\r\n\t\t\treturn forward < 0f ? -SpeedMs : SpeedMs;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Input-source seam (the <see cref=\"DriveInputs\"/> pluggable-source abstraction \u2014\r\n\t/// keyboard/gamepad/wheel/scripted pilot as peers at ONE seam). When non-null this value-struct is\r\n\t/// consumed by <see cref=\"ReadInput\"/> INSTEAD of sampling live keyboard/gamepad, so whatever set\r\n\t/// it drives through the exact same input \u2192 assists \u2192 drivetrain path a human uses \u2014 it never\r\n\t/// applies forces itself (an intent-injection pattern). Null = normal keyboard/gamepad. A scripted\r\n\t/// source sets it each tick while it drives and clears it when done. This is the ONLY input\r\n\t/// addition to VehicleController (a deliberately narrow seam); future device sources plug in here\r\n\t/// without touching this class again.\r\n\t/// </summary>\r\n\tpublic DriveInputs? InputOverride { get; set; }\r\n\r\n\tRigidbody _rigidbody;\r\n\tVector3 _spawnPosition;\r\n\tRotation _spawnRotation;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_rigidbody = Components.Get<Rigidbody>();\r\n\t\tDefinition ??= CarDefinitions.Hatch;\r\n\t\tDrivetrain = new Drivetrain( Definition );\r\n\t\t// Adopt a carried session mode if a spawn path staged one (car swap); otherwise the\r\n\t\t// definition default. Because this runs a frame or two after the car is created, a spawn\r\n\t\t// path can't rely on a plain post-spawn Assists set surviving \u2014 it stages InitialAssists.\r\n\t\tAssists = InitialAssists ?? Definition.DefaultAssists;\r\n\t\t_spawnPosition = WorldPosition;\r\n\t\t_spawnRotation = WorldRotation;\r\n\r\n\t\t// suspension needs continuous simulation \u2014 a sleeping body ignores our forces\r\n\t\tif ( _rigidbody.PhysicsBody is not null )\r\n\t\t\t_rigidbody.PhysicsBody.AutoSleep = false;\r\n\r\n\t\t// brief freeze so the car initializes perfectly level and still\r\n\t\t_rigidbody.MotionEnabled = false;\r\n\t\t_settleFreeze = 0.4f;\r\n\t}\r\n\r\n\tTimeSince _telemetry;\r\n\tTimeSince _sinceSpawn;\r\n\tfloat _settleFreeze;\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( IsProxy || !_rigidbody.IsValid() || Drivetrain is null )\r\n\t\t\treturn;\r\n\r\n\t\tif ( _settleFreeze > 0f )\r\n\t\t{\r\n\t\t\t_settleFreeze -= Time.Delta;\r\n\t\t\tif ( _settleFreeze <= 0f )\r\n\t\t\t{\r\n\t\t\t\t_rigidbody.MotionEnabled = true;\r\n\t\t\t\t_sinceSpawn = 0;\r\n\t\t\t}\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tReadInput();\r\n\t\tApplySteering();\r\n\r\n\t\t// handbrake = drift button: rears instantly lose lateral bite, snap back on release\r\n\t\tforeach ( var wheel in Wheels )\r\n\t\t{\r\n\t\t\tif ( !wheel.IsSteering )\r\n\t\t\t\twheel.GripScale = Handbrake ? Definition.HandbrakeGripScale : 1f;\r\n\r\n\t\t\t// throttle dissolves low-speed parking stiction on ALL wheels so full-lock\r\n\t\t\t// launches actually launch (undriven steered fronts were the drag)\r\n\t\t\twheel.ParkBrakeScale = 1f - Throttle;\r\n\t\t}\r\n\r\n\t\tforeach ( var wheel in Wheels )\r\n\t\t\twheel.BeginStep();\r\n\r\n\t\tvar driven = Wheels.Where( w => w.IsDriven ).ToList();\r\n\t\tfloat dt = Time.Delta / Substeps;\r\n\r\n\t\t// ground-truth wheel speed for shifting: actual forward speed over the tire radius\r\n\t\tfloat forwardSpeed = Vector3.Dot( _rigidbody.Velocity * Units.UnitsToMeters, WorldRotation.Forward );\r\n\t\tfloat groundWheelSpeed = forwardSpeed / Definition.WheelRadius;\r\n\r\n\t\t// arcade launch boost: extra shove off the line, fully faded by 15 m/s\r\n\t\tfloat launchBoost = MathX.Lerp( Definition.LaunchBoost, 1f, Math.Clamp( SpeedMs / 15f, 0f, 1f ) );\r\n\r\n\t\t// drift-catch assist: arm on the handbrake RELEASE edge, compute this tick's throttle factor.\r\n\t\tif ( _wasHandbrake && !Handbrake )\r\n\t\t\t_sinceHandbrakeRelease = 0f;\r\n\t\t_wasHandbrake = Handbrake;\r\n\t\tfloat driftCatch = DriftCatchFactor();\r\n\r\n\t\tfor ( int step = 0; step < Substeps; step++ )\r\n\t\t{\r\n\t\t\tfloat avgDrivenSpeed = driven.Count > 0 ? driven.Average( w => w.AngularVelocity ) : 0f;\r\n\t\t\tfloat throttle = ApplyTractionControl( Throttle * driftCatch, driven );\r\n\t\t\tfloat torquePerWheel = Drivetrain.Simulate( dt, throttle, avgDrivenSpeed, groundWheelSpeed, driven.Count ) * launchBoost;\r\n\r\n\t\t\t// Drive-side omega cap, read fresh AFTER Simulate (a shift changes the ratio mid-loop):\r\n\t\t\t// drive torque may never push a driven wheel past redline-equivalent within a substep\r\n\t\t\t// (the limiter-one-substep-late overshoot defect; see VehicleWheel.IntegrateWheelSpin).\r\n\t\t\tfloat omegaCap = Drivetrain.RedlineWheelSpeed;\r\n\r\n\t\t\tforeach ( var wheel in Wheels )\r\n\t\t\t{\r\n\t\t\t\tfloat drive = wheel.IsDriven ? torquePerWheel : 0f;\r\n\t\t\t\tfloat brake = BrakeTorqueFor( wheel );\r\n\t\t\t\tif ( wheel.IsDriven )\r\n\t\t\t\t\twheel.DriveOmegaCap = omegaCap;\r\n\t\t\t\twheel.Substep( dt, drive, brake );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tforeach ( var wheel in Wheels )\r\n\t\t\twheel.EndStep();\r\n\r\n\t\tApplyBrakeAssist();\r\n\t\tApplySpinRecoveryAssist();\r\n\t\tApplyStabilityAssist();\r\n\t\tApplyWallGlanceAssist();\r\n\t\tApplyAirAttitudeAssist();\r\n\r\n\t\t// dense driving telemetry: 2 Hz while moving or on input \u2014 parseable for analysis\r\n\t\tif ( _telemetry > 0.5f && (SpeedMs > 0.5f || Throttle > 0f || Brake > 0f) )\r\n\t\t{\r\n\t\t\t_telemetry = 0;\r\n\t\t\tfloat yawRate = _rigidbody.AngularVelocity.z.RadianToDegree();\r\n\t\t\tvar rears = Wheels.Where( w => !w.IsSteering ).ToList();\r\n\t\t\tvar fronts = Wheels.Where( w => w.IsSteering ).ToList();\r\n\t\t\tLog.Info( $\"[vp] tele car={Definition?.Name ?? \"?\"} v {SpeedMs * 3.6f:F0}kmh rpm {Drivetrain.Rpm:F0} gear {Drivetrain.Gear} | thr {Throttle:F2} brk {Brake:F2} hb {(Handbrake ? 1 : 0)} steer {Steer:F2} | yaw {yawRate:F0}deg/s | rearK {rears.Average( w => w.SlipRatio ):F2} frontA {fronts.Average( w => w.SlipAngle.RadianToDegree() ):F1} rearA {rears.Average( w => w.SlipAngle.RadianToDegree() ):F1}\" );\r\n\r\n\t\t\t// per-wheel trace hits whenever contact is abnormal \u2014 names what we're driving\r\n\t\t\t// on (or falling through); this diagnostic has caught three bugs, keep it\r\n\t\t\tint grounded = Wheels.Count( w => w.IsGrounded );\r\n\t\t\tif ( grounded < 4 || _sinceSpawn < 6f )\r\n\t\t\t{\r\n\t\t\t\tvar detail = string.Join( \" | \", Wheels.Select( w =>\r\n\t\t\t\t\t$\"{w.GameObject.Name[^2..]} {w.DebugTrace} Fz {w.Load:F0}\" ) );\r\n\t\t\t\tLog.Info( $\"[vp] wheels z {WorldPosition.z * Units.UnitsToMeters:F1}m | {detail}\" );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tif ( Input.Pressed( \"Reload\" ) )\r\n\t\t\tRespawn();\r\n\r\n\t\t// Drive-mode (assist level) cycle \u2014 Casual -> Sport -> Sim -> Casual. A plain property set,\r\n\t\t// so (like Reload) it reads Input.Pressed straight in OnUpdate; no fixed-step edge-latch needed.\r\n\t\t// A plain Assists set survives respawns/car swaps via InitialAssists and never gets clobbered\r\n\t\t// after OnStart.\r\n\t\tif ( Input.Pressed( \"DriveMode\" ) )\r\n\t\t\tCycleDriveMode();\r\n\r\n\t\t// Sequential-shift edges are latched HERE (per-frame) \u2014 Input.Pressed is frame-scoped and\r\n\t\t// unreliable read from OnFixedUpdate, which may run zero or several times a frame. ReadInput\r\n\t\t// (fixed step) consumes + clears these. A frame with no fixed tick still keeps the latch until\r\n\t\t// the next one, so no press is lost at any framerate.\r\n\t\tif ( Input.Pressed( \"ShiftUp\" ) ) _liveShiftUp = true;\r\n\t\tif ( Input.Pressed( \"ShiftDown\" ) ) _liveShiftDown = true;\r\n\t\tif ( Input.Pressed( \"ShiftMode\" ) ) _liveShiftMode = true;\r\n\t}\r\n\r\n\tvoid ReadInput()\r\n\t{\r\n\t\t// one seam: live devices by default, or whatever an input source (a scripted pilot, an input\r\n\t\t// device) staged in InputOverride this tick. The struct carries the SAME raw intent the\r\n\t\t// keyboard sampled, so all the gear/reverse/steer-ramp logic below is source-agnostic.\r\n\t\tvar inputs = InputOverride ?? DriveInputs.SampleDeviceInputs();\r\n\r\n\t\t// forward stick/W = accelerate, back = brake \u2014 unless (near-)stopped, then back = reverse\r\n\t\tfloat forwardInput = inputs.MoveForward;\r\n\t\tfloat forwardSpeed = Vector3.Dot( _rigidbody.Velocity * Units.UnitsToMeters, WorldRotation.Forward );\r\n\r\n\t\t// direction changes engage while still rolling ~1 m/s the wrong way \u2014 waiting\r\n\t\t// for a dead stop is what made K-turns feel like a driving test\r\n\t\tif ( Drivetrain.Gear >= 0 )\r\n\t\t{\r\n\t\t\tThrottle = MathF.Max( 0f, forwardInput );\r\n\t\t\tBrake = MathF.Max( 0f, -forwardInput );\r\n\r\n\t\t\tif ( forwardInput < -0.15f && forwardSpeed < 1.0f )\r\n\t\t\t\tDrivetrain.EngageReverse();\r\n\t\t}\r\n\t\telse // reverse gear: back = reverse throttle, forward = brake\r\n\t\t{\r\n\t\t\tThrottle = MathF.Max( 0f, -forwardInput );\r\n\t\t\tBrake = MathF.Max( 0f, forwardInput );\r\n\r\n\t\t\tif ( forwardInput > 0.15f && forwardSpeed > -1.0f )\r\n\t\t\t\tDrivetrain.EngageForward();\r\n\t\t}\r\n\r\n\t\tHandbrake = inputs.Handbrake;\r\n\r\n\t\t// reverse is for maneuvering, not backward land-speed records\r\n\t\tif ( Drivetrain.Gear == -1 && SpeedMs > Definition.ReverseSpeedCap )\r\n\t\t\tThrottle = 0f;\r\n\r\n\t\t// steering: keyboard ramps, analog is direct; both rate and lock reduce with\r\n\t\t// speed \u2014 fast full-lock flicks at 70+ km/h were driver-induced weave\r\n\t\tfloat speedT = Math.Clamp( SpeedMs / 22f, 0f, 1f );\r\n\t\tfloat steerTarget = inputs.Steer;\r\n\t\tfloat rate = (MathF.Abs( steerTarget ) > 0.01f\r\n\t\t\t? MathX.Lerp( 4.5f, 1.8f, speedT )\r\n\t\t\t: MathX.Lerp( 6f, 3f, speedT )) * Definition.SteerRateScale;\r\n\t\tSteer = MathX.Lerp( Steer, steerTarget, Math.Clamp( rate * Time.Delta, 0f, 1f ) );\r\n\r\n\t\t// \u2500\u2500 sequential MANUAL shift requests \u2500\u2500\r\n\t\t// Two sources at ONE seam, exactly like MoveForward/Steer/Handbrake: a scripted InputOverride\r\n\t\t// carries the shift bits in the struct; live play uses the per-frame edges latched in OnUpdate.\r\n\t\t// Live latches are consumed either way so a stray key pressed during a scripted run can't leak\r\n\t\t// into a live shift when the override clears.\r\n\t\tbool reqUp, reqDown, reqMode;\r\n\t\tif ( InputOverride is DriveInputs ov )\r\n\t\t{\r\n\t\t\treqUp = ov.ShiftUp;\r\n\t\t\treqDown = ov.ShiftDown;\r\n\t\t\treqMode = ov.ShiftModeToggle;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\treqUp = _liveShiftUp;\r\n\t\t\treqDown = _liveShiftDown;\r\n\t\t\treqMode = _liveShiftMode;\r\n\t\t}\r\n\t\t_liveShiftUp = _liveShiftDown = _liveShiftMode = false;\r\n\r\n\t\tApplyShiftRequests( reqUp, reqDown, reqMode, forwardSpeed / Definition.WheelRadius );\r\n\t}\r\n\r\n\t// \u2500\u2500 sequential MANUAL shift state \u2500\u2500\r\n\tbool _liveShiftUp, _liveShiftDown, _liveShiftMode;   // per-frame edges latched in OnUpdate\r\n\tbool _prevReqUp, _prevReqDown, _prevReqMode;          // rising-edge memory across fixed ticks\r\n\r\n\t/// <summary>Rising-edge-detect this tick's shift requests and drive the drivetrain. The live path's\r\n\t/// requests are already one-tick pulses (latched Input.Pressed edges), and this ALSO edge-gates the\r\n\t/// override/pilot path so a source that holds a bit across ticks shifts exactly once (the\r\n\t/// edge-through-InputOverride trap). <paramref name=\"groundWheelSpeed\"/> (rad/s) feeds the\r\n\t/// down-shift over-rev guard.</summary>\r\n\tvoid ApplyShiftRequests( bool up, bool down, bool mode, float groundWheelSpeed )\r\n\t{\r\n\t\tif ( mode && !_prevReqMode )\r\n\t\t\tToggleShiftMode();\r\n\t\tif ( up && !_prevReqUp )\r\n\t\t\tTryShiftUp();\r\n\t\tif ( down && !_prevReqDown )\r\n\t\t\tTryShiftDown( groundWheelSpeed );\r\n\r\n\t\t_prevReqUp = up;\r\n\t\t_prevReqDown = down;\r\n\t\t_prevReqMode = mode;\r\n\t}\r\n\r\n\tvoid ToggleShiftMode()\r\n\t{\r\n\t\tDrivetrain.ManualMode = !Drivetrain.ManualMode;\r\n\t\tLog.Info( $\"[vp] shiftmode {(Drivetrain.ManualMode ? \"MANUAL\" : \"AUTO\")} gear {Drivetrain.Gear}\" );\r\n\t}\r\n\r\n\t/// <summary>Cycle the drive mode (assist level) Casual -> Sport -> Sim -> Casual. A HUD assist chip\r\n\t/// can read <see cref=\"Assists\"/> and flash on change, so the swap is visible on press.</summary>\r\n\tvoid CycleDriveMode()\r\n\t{\r\n\t\tAssists = Assists switch\r\n\t\t{\r\n\t\t\tAssistLevel.Casual => AssistLevel.Sport,\r\n\t\t\tAssistLevel.Sport => AssistLevel.Sim,\r\n\t\t\t_ => AssistLevel.Casual,\r\n\t\t};\r\n\t\tLog.Info( $\"[vp] drivemode {Assists.ToString().ToUpper()}\" );\r\n\t}\r\n\r\n\tvoid TryShiftUp()\r\n\t{\r\n\t\t// In AUTO a manual shift request is a no-op (the box shifts itself; mode changes only via G).\r\n\t\tif ( !Drivetrain.ManualMode )\r\n\t\t\treturn;\r\n\t\tif ( Drivetrain.ShiftUp() )\r\n\t\t\tLog.Info( $\"[vp] shift UP -> gear {Drivetrain.Gear}\" );\r\n\t}\r\n\r\n\tvoid TryShiftDown( float groundWheelSpeed )\r\n\t{\r\n\t\tif ( !Drivetrain.ManualMode )\r\n\t\t\treturn;\r\n\t\tif ( Drivetrain.ShiftDown( groundWheelSpeed ) )\r\n\t\t\tLog.Info( $\"[vp] shift DOWN -> gear {Drivetrain.Gear}\" );\r\n\t\telse\r\n\t\t\tLog.Info( $\"[vp] shift DOWN denied gear {Drivetrain.Gear} \" +\r\n\t\t\t\t$\"(predicted {Drivetrain.PredictedDownshiftRpm( groundWheelSpeed ):F0} / redline {Drivetrain.Redline:F0})\" );\r\n\t}\r\n\r\n\tvoid ApplySteering()\r\n\t{\r\n\t\tfloat speedFactor = Math.Clamp( SpeedMs / 22f, 0f, 1f );\r\n\t\tfloat maxAngle = MathX.Lerp( Definition.MaxSteerAngle, Definition.HighSpeedSteerAngle, speedFactor );\r\n\t\tfloat angle = Steer * maxAngle;\r\n\r\n\t\tforeach ( var wheel in Wheels.Where( w => w.IsSteering ) )\r\n\t\t{\r\n\t\t\twheel.SteerAngle = angle;\r\n\t\t\twheel.LocalRotation = Rotation.FromYaw( angle );\r\n\t\t}\r\n\t}\r\n\r\n\tfloat BrakeTorqueFor( VehicleWheel wheel )\r\n\t{\r\n\t\tbool isFront = wheel.IsSteering;\r\n\t\tfloat bias = isFront ? Definition.BrakeBias : 1f - Definition.BrakeBias;\r\n\t\tfloat torque = Brake * Definition.BrakeTorque * bias * 0.5f; // per wheel (2 per axle)\r\n\r\n\t\tif ( Handbrake && wheel.HasHandbrake )\r\n\t\t{\r\n\t\t\t// Drift-exit soft-lock: when a slip cap is active and this rear is already sliding PAST it\r\n\t\t\t// (SlipRatio more negative than the cap), withhold the handbrake torque this substep so the\r\n\t\t\t// wheel spins back up toward the cap \u2014 the rears keep rotating and retain lateral authority\r\n\t\t\t// instead of dead-locking into 60\u00b0+ slip angles. Same one-substep-lagged SlipRatio the ABS\r\n\t\t\t// branch below reads. Default cap -1.0 leaves capActive false, so full lock is byte-identical.\r\n\t\t\tbool capActive = Definition.HandbrakeSlipCap > -1f;\r\n\t\t\tif ( !capActive || wheel.SlipRatio > Definition.HandbrakeSlipCap )\r\n\t\t\t\ttorque += Definition.HandbrakeTorque;\r\n\t\t}\r\n\r\n\t\t// ABS: release when the wheel locks under braking (Casual + Sport). Thresholds are\r\n\t\t// per-car dials (see CarDefinition.AbsSlipThreshold).\r\n\t\tif ( Assists != AssistLevel.Sim && torque > 0f && wheel.IsGrounded\r\n\t\t\t&& wheel.SlipRatio < -Definition.AbsSlipThreshold )\r\n\t\t\ttorque *= Definition.AbsReleaseFactor;\r\n\r\n\t\treturn torque;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Arcade brake assist: extra chassis-level deceleration while braking. The tire\r\n\t/// model alone stops at sim rates, which reads as \"slow\" against arcade expectations.\r\n\t/// Capped so it can never reverse the car within a step.\r\n\t/// </summary>\r\n\tvoid ApplyBrakeAssist()\r\n\t{\r\n\t\tif ( Definition.BrakeAssist <= 0f || Brake < 0.1f || SpeedMs < 0.5f )\r\n\t\t\treturn;\r\n\r\n\t\tif ( Wheels.Count( w => w.IsGrounded ) < 2 )\r\n\t\t\treturn;\r\n\r\n\t\tvar flat = _rigidbody.Velocity.WithZ( 0f );\r\n\t\tif ( flat.IsNearZeroLength )\r\n\t\t\treturn;\r\n\r\n\t\tfloat decel = Definition.BrakeAssist * Brake; // m/s\u00b2\r\n\t\tfloat stopCap = flat.Length * Units.UnitsToMeters / Time.Delta;\r\n\t\tfloat applied = MathF.Min( decel, stopCap );\r\n\r\n\t\t_rigidbody.ApplyForce( -flat.Normal * applied * Definition.Mass * Units.MetersToUnits );\r\n\t}\r\n\r\n\tTimeSince _recoverLog = 999f;\r\n\r\n\t/// <summary>\r\n\t/// Arcade SPIN-RECOVERY assist. After a handbrake flick spins the car ~180\u00b0, the player holds full\r\n\t/// FORWARD throttle but the car keeps rolling BACKWARDS (its old travel direction) for too long\r\n\t/// before the tires pick up and drive it the new way. BrakeAssist can't cover this: with a forward\r\n\t/// gear + W held, ReadInput sets Throttle=1, Brake=0 \u2014 so the only thing arresting the backward\r\n\t/// slide is deep-slip tire tail grip, further scaled down by the friction ellipse sharing with\r\n\t/// lateral demand. This adds chassis-level deceleration along -flat-velocity WHENEVER the input\r\n\t/// throttle commands the gear's direction while ground velocity along the car's facing opposes it\r\n\t/// (the quadrant BrakeAssist's opposing-input\u2192Brake mapping never reaches), fading out via an\r\n\t/// opposition ramp as the car rotates to face its motion. Capped by the same never-reverse-within-\r\n\t/// a-step stopCap BrakeAssist uses. Sim keeps the raw accepted feel.\r\n\t///\r\n\t/// Interaction with drift-catch (DriftCatchFactor): drift-catch cuts DRIVETRAIN throttle for \u22640.5 s\r\n\t/// after handbrake release while the rear is deeply SIDEWAYS; this reads INPUT throttle and applies\r\n\t/// a CHASSIS force. They serve different states \u2014 sideways (realign) vs backwards (kill stale\r\n\t/// velocity) \u2014 and act together in a spin-recovery WITHOUT being merged.\r\n\t/// </summary>\r\n\tvoid ApplySpinRecoveryAssist()\r\n\t{\r\n\t\tif ( Definition.SpinRecoveryAssist <= 0f || Assists == AssistLevel.Sim )\r\n\t\t\treturn;\r\n\r\n\t\t// Throttle is the gear-direction INPUT throttle magnitude set in ReadInput (before the\r\n\t\t// drift-catch / TC drivetrain governors scale it) \u2014 exactly the raw request this assist wants.\r\n\t\tif ( Throttle <= 0.1f )\r\n\t\t\treturn;\r\n\r\n\t\tif ( Wheels.Count( w => w.IsGrounded ) < 2 )\r\n\t\t\treturn;\r\n\r\n\t\tvar flat = _rigidbody.Velocity.WithZ( 0f );\r\n\t\tfloat planarSpeed = flat.Length * Units.UnitsToMeters;\r\n\t\tif ( planarSpeed < 0.5f )\r\n\t\t\treturn;\r\n\r\n\t\t// velocity along the car's facing. commandedDir folds forward/reverse gear into one sign: in a\r\n\t\t// forward gear the throttle commands +forward, so a NEGATIVE forwardSpeed (still sliding\r\n\t\t// backwards under forward throttle) is the uncovered case; in reverse gear it commands\r\n\t\t// -forward, so a POSITIVE forwardSpeed (reverse throttle while still rolling forward) mirrors it.\r\n\t\tfloat forwardSpeed = Vector3.Dot( _rigidbody.Velocity * Units.UnitsToMeters, WorldRotation.Forward );\r\n\t\tfloat commandedDir = Drivetrain.Gear >= 0 ? 1f : -1f;\r\n\t\tfloat alongCommanded = forwardSpeed * commandedDir; // <0 \u21d2 velocity opposes the throttle direction\r\n\t\tif ( alongCommanded > -0.5f )\r\n\t\t\treturn; // already moving the commanded way (or ~stopped) \u2014 nothing to recover\r\n\r\n\t\t// opposition ramp: fraction of speed pointing the wrong way \u2014 1 when velocity fully opposes\r\n\t\t// facing, fading to 0 as the car rotates to face its motion (so the assist bows out on its own).\r\n\t\tfloat oppositionRamp = Math.Clamp( -alongCommanded / planarSpeed, 0f, 1f );\r\n\r\n\t\tfloat decel = Definition.SpinRecoveryAssist * oppositionRamp; // m/s\u00b2\r\n\t\tfloat stopCap = planarSpeed / Time.Delta; // never reverse the flat velocity within a step\r\n\t\tfloat applied = MathF.Min( decel, stopCap );\r\n\r\n\t\t_rigidbody.ApplyForce( -flat.Normal * applied * Definition.Mass * Units.MetersToUnits );\r\n\r\n\t\t// throttled ~2 Hz while active \u2014 parseable for free-drive sessions.\r\n\t\tif ( _recoverLog > 0.5f )\r\n\t\t{\r\n\t\t\t_recoverLog = 0f;\r\n\t\t\tLog.Info( $\"[vp] recover v {planarSpeed * 3.6f:F0}kmh along {alongCommanded:F1}m/s ramp {oppositionRamp:F2} decel {applied:F1}m/s2\" );\r\n\t\t}\r\n\t}\r\n\r\n\t// \u2500\u2500 drift-catch assist \u2500\u2500\r\n\t// The measured drift-exit anatomy: on handbrake release the driver goes to full throttle while\r\n\t// the rear slip angle is still 60\u00b0+, so the drive torque spends the rear tires' friction ellipse\r\n\t// LONGITUDINALLY exactly when every newton of lateral force is needed to realign the velocity\r\n\t// vector (\"stuck sliding sideways\" + rearK spike + auto-downshift in the telemetry).\r\n\t// For a short window after the handbrake releases, while the rear is still deeply sideways, cut\r\n\t// throttle-induced rear slip (ramping to a full cut by DriftCatchFullCutDeg) so the ellipse serves\r\n\t// realignment first \u2014 mirrors real drift-catch technique (wait for the catch before power).\r\n\t// Casual + Sport only; Sim stays the raw accepted feel. The 20\u00b0 floor keeps deliberate\r\n\t// power-oversteer (jturn rotation, small-angle slides) untouched.\r\n\tconst float DriftCatchWindowS = 0.5f;    // seconds after hb release the assist can act\r\n\tconst float DriftCatchStartDeg = 20f;    // rear slip angle where the cut starts\r\n\tconst float DriftCatchFullCutDeg = 35f;  // rear slip angle at/past which throttle is fully cut\r\n\r\n\tbool _wasHandbrake;\r\n\tTimeSince _sinceHandbrakeRelease = 999f;\r\n\r\n\tfloat DriftCatchFactor()\r\n\t{\r\n\t\tif ( Assists == AssistLevel.Sim || Handbrake )\r\n\t\t\treturn 1f;\r\n\t\tif ( _sinceHandbrakeRelease > DriftCatchWindowS )\r\n\t\t\treturn 1f;\r\n\r\n\t\tvar rears = Wheels.Where( w => !w.IsSteering && w.IsGrounded ).ToList();\r\n\t\tif ( rears.Count == 0 )\r\n\t\t\treturn 1f;\r\n\r\n\t\tfloat rearADeg = MathF.Abs( rears.Average( w => w.SlipAngle ) ).RadianToDegree();\r\n\t\tif ( rearADeg <= DriftCatchStartDeg )\r\n\t\t\treturn 1f;\r\n\r\n\t\treturn Math.Clamp( 1f - (rearADeg - DriftCatchStartDeg) / (DriftCatchFullCutDeg - DriftCatchStartDeg), 0f, 1f );\r\n\t}\r\n\r\n\tfloat ApplyTractionControl( float throttle, List<VehicleWheel> driven )\r\n\t{\r\n\t\t// drifting is throttle-steered \u2014 TC clamping wheelspin would kill the slide\r\n\t\tif ( Handbrake || Assists == AssistLevel.Sim || throttle <= 0f )\r\n\t\t\treturn throttle;\r\n\r\n\t\t// proportional: hold driven-wheel slip near a target. Casual holds the grip PEAK; Sport (when the\r\n\t\t// car opts in via SportTcSlipTarget) holds a LOOSER target well past the peak so the rears still\r\n\t\t// break into a throttle-steerable slide, but bounded so they can't free-spin to redline and torch\r\n\t\t// rear lateral grip (the Sport spin-out, owner 2026-07-21). The longitudinal tire-curve peaks sit\r\n\t\t// at slip 0.09-0.14; a 0.25 target parks the tire in the post-peak slide \u2014 slower (less grip than\r\n\t\t// the peak) AND permanently over the wheelspin threshold. Casual 0.14 targets the peak: more launch\r\n\t\t// grip and slip under the counter. Sport, not opted in (target 0), returns raw throttle \u2014 the old\r\n\t\t// \"ABS only\" behavior, byte-identical.\r\n\t\tfloat slipTarget;\r\n\t\tif ( Assists == AssistLevel.Casual )\r\n\t\t\tslipTarget = 0.14f;\r\n\t\telse if ( Definition.SportTcSlipTarget > 0f )\r\n\t\t\tslipTarget = Definition.SportTcSlipTarget;\r\n\t\telse\r\n\t\t\treturn throttle;\r\n\r\n\t\tfloat worstSlip = driven.Where( w => w.IsGrounded ).Select( w => w.SlipRatio ).DefaultIfEmpty( 0f ).Max();\r\n\t\tif ( worstSlip <= slipTarget )\r\n\t\t\treturn throttle;\r\n\r\n\t\t// TC floor relaxation (kart cap-camping fix 2026-07-18): the flat 0.2 throttle floor still fed\r\n\t\t// enough torque to sustain a spinning rear on a light car (260 kg kart), so TC could not\r\n\t\t// arrest the wheelspin that pins a collapsing-corner rear far past the grip peak (the \"stuck\r\n\t\t// turning\" bug, offline: this is the decisive lever, cutting sustained rear slip 3.2 -> 0.4).\r\n\t\t// Once slip is deep past the tail (the longitudinal curve reaches its tail by slip 0.40;\r\n\t\t// TcFloorRelaxStart 1.0 is well beyond it), fade the floor toward 0 so the proportional\r\n\t\t// response can cut throttle to near-zero. Below TcFloorRelaxStart the floor stays 0.2 so all\r\n\t\t// below-threshold behavior is byte-identical.\r\n\t\tconst float TcFloorRelaxStart = 1.0f;\r\n\t\tconst float TcFloorRelaxEnd = 2.5f;\r\n\t\tfloat floor = 0.2f;\r\n\t\tif ( worstSlip > TcFloorRelaxStart )\r\n\t\t\tfloor *= Math.Clamp( (TcFloorRelaxEnd - worstSlip) / (TcFloorRelaxEnd - TcFloorRelaxStart), 0f, 1f );\r\n\r\n\t\treturn throttle * Math.Clamp( slipTarget / worstSlip, floor, 1f );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Airborne pitch-rate damping time constant, seconds. Leaving a ramp lip pivots the car over\r\n\t/// its rear axle, so every launch departs rotating nose-down (19-46 deg/s measured); with no\r\n\t/// air management the car rotates through the whole flight and lands nose-first, digging in.\r\n\t/// Flight-recorder capture (2026-07-21, Lad2 at 35.5 m/s): launch attitude 8.6 deg nose-UP,\r\n\t/// landing attitude 17.8 deg nose-DOWN, touchdown 35.5 to 31.7 m/s in 40 ms (a ~5 g spike)\r\n\t/// then pitch slammed level in 100 ms - the owner's \"hitch going off the ramps\", reported\r\n\t/// identically at every speed because the lip pivot exists at every speed. Damping the\r\n\t/// car-local pitch rate while fully airborne holds the launch attitude so the car lands\r\n\t/// wheels-matched (slightly tail-first). 0 or negative disables. LIVE-UNVERIFIED.\r\n\t/// </summary>\r\n\t[Property] public float AirPitchDampTau { get; set; } = 0.30f;\r\n\r\n\tvoid ApplyAirAttitudeAssist()\r\n\t{\r\n\t\tif ( AirPitchDampTau <= 0f )\r\n\t\t\treturn;\r\n\r\n\t\t// fully airborne only: any grounded wheel means suspension owns attitude and this\r\n\t\t// assist is inert, so grounded driving is byte-identical by construction\r\n\t\tforeach ( var w in Wheels )\r\n\t\t\tif ( w.IsGrounded )\r\n\t\t\t\treturn;\r\n\t\tif ( Wheels.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\t// the drift button doubles as \"let me rotate\" in the air: deliberate flips stay possible\r\n\t\tif ( Handbrake )\r\n\t\t\treturn;\r\n\r\n\t\tvar local = WorldRotation.Inverse * _rigidbody.AngularVelocity;\r\n\t\tlocal.y *= MathF.Exp( -Time.Delta / AirPitchDampTau );\r\n\t\t_rigidbody.AngularVelocity = WorldRotation * local;\r\n\t}\r\n\r\n\tvoid ApplyStabilityAssist()\r\n\t{\r\n\t\t// the drift button asks for yaw \u2014 don't damp it away while held\r\n\t\tif ( Handbrake )\r\n\t\t\treturn;\r\n\r\n\t\t// Casual damps at full authority; Sport (when the car opts in via SportStabilityScale) damps at a\r\n\t\t// FRACTION so the counter-steer pendulum snap (rear regains grip and flings the car the other way,\r\n\t\t// uncatchable \u2014 owner 2026-07-21) is bled off while deliberate rotation survives. Sim, and Sport\r\n\t\t// with no opt-in, get nothing. Casual multiplies by exactly 1f so its behavior is byte-identical.\r\n\t\tfloat authority;\r\n\t\tif ( Assists == AssistLevel.Casual )\r\n\t\t\tauthority = 1f;\r\n\t\telse if ( Assists == AssistLevel.Sport && Definition.SportStabilityScale > 0f )\r\n\t\t\tauthority = Definition.SportStabilityScale;\r\n\t\telse\r\n\t\t\treturn;\r\n\r\n\t\t// small corrective action when the rear steps out \u2014 damp yaw so slides are catchable\r\n\t\t// instead of divergent (the lift-off L-R flick spin)\r\n\t\tvar rears = Wheels.Where( w => !w.IsSteering && w.IsGrounded ).ToList();\r\n\t\tif ( rears.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tfloat rearAlpha = MathF.Abs( rears.Average( w => w.SlipAngle ) );\r\n\t\tif ( rearAlpha < 0.07f ) // ~4 degrees\r\n\t\t\treturn;\r\n\r\n\t\t// scales up with speed: the flat 3f cap let a 115 km/h lift-off flick go full 360\r\n\t\tfloat speedScale = 3f + 3f * Math.Clamp( SpeedMs / 30f, 0f, 1f );\r\n\t\tfloat strength = Math.Clamp( (rearAlpha - 0.07f) * 8f, 0f, 1f ) * speedScale * authority;\r\n\t\tvar angular = _rigidbody.AngularVelocity;\r\n\t\tangular.z *= MathF.Max( 0f, 1f - strength * Time.Delta );\r\n\t\t_rigidbody.AngularVelocity = angular;\r\n\t}\r\n\r\n\t// \u2500\u2500 wall-glance forgiveness assist \u2500\u2500\r\n\t// Detection surface = the chassis Rigidbody's collision callbacks (Component.ICollisionListener;\r\n\t// the ONLY runtime chassis-contact API: OnCollisionStart/Update/Stop carry Sandbox.Collision with\r\n\t// .Contact.Point/.Contact.Normal and .Other). The chassis box rests ABOVE the wheels' contact\r\n\t// zone, so it never touches flat ground \u2014 these fire only on real obstacle contact (wall, cone,\r\n\t// bottom-out). We latch ONLY near-horizontal normals (true walls); ground/ramps/banks have\r\n\t// near-vertical normals and are ignored.\r\n\tconst float WallNormalZMax = 0.5f;   // |normal.z| < this \u21d2 surface within ~30\u00b0 of vertical = a wall\r\n\t// engage as soon as a wall contact is confirmed (a corner-wedge kills speed in a few frames, so a\r\n\t// multi-frame gate arrives after the dead-stop it's meant to prevent). A stray \u22641-frame contact\r\n\t// still can't engage: the streak must reach this AND the car must be moving INTO the surface above\r\n\t// the speed floor.\r\n\tconst int WallEngageTicks = 1;\r\n\tconst float WallGlanceMinSpeed = 3f; // m/s planar floor below which forgiveness is pointless\r\n\r\n\tVector3 _wallNormalH;          // horizontal unit wall normal from the most recent contact\r\n\tTimeSince _sinceWallContact = 999f;\r\n\tint _wallStreak;\r\n\tTimeSince _wallGlanceLog = 999f;\r\n\r\n\tpublic void OnCollisionStart( Collision o ) => NoteWallContact( o );\r\n\tpublic void OnCollisionUpdate( Collision o ) => NoteWallContact( o );\r\n\tpublic void OnCollisionStop( CollisionStop o ) { }\r\n\r\n\tvoid NoteWallContact( Collision c )\r\n\t{\r\n\t\tif ( IsProxy )\r\n\t\t\treturn;\r\n\r\n\t\tvar n = c.Contact.Normal;\r\n\t\t// a wall = near-horizontal contact normal (surface within ~30\u00b0 of vertical). Ground/ramps/\r\n\t\t// banks report near-vertical normals and never latch, so this can't fire driving over terrain.\r\n\t\tif ( MathF.Abs( n.z ) >= WallNormalZMax )\r\n\t\t\treturn;\r\n\r\n\t\tvar nH = n.WithZ( 0f );\r\n\t\tif ( nH.IsNearZeroLength )\r\n\t\t\treturn;\r\n\r\n\t\t_wallNormalH = nH.Normal;\r\n\t\t_sinceWallContact = 0f;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Forgiveness for angled (esp. mid-drift) wall contact: instead of a dead stop, re-project\r\n\t/// velocity onto the wall tangent (keeping <see cref=\"CarDefinition.WallScrubFactor\"/> of speed)\r\n\t/// and gently yaw the heading to run parallel \u2014 both scaled by incidence so HEAD-ON hits stay\r\n\t/// hard stops. An assist: gated on <see cref=\"CarDefinition.WallGlanceAssist\"/> and Assists != Sim\r\n\t/// (Sim = the accepted raw feel). Runs in OnFixedUpdate BEFORE the physics step, so the solver then\r\n\t/// resolves the redirected velocity without penetration \u2014 same write-then-step pattern as the\r\n\t/// brake/stability assists.\r\n\t/// </summary>\r\n\tvoid ApplyWallGlanceAssist()\r\n\t{\r\n\t\tbool contactNow = _sinceWallContact <= Time.Delta * 1.5f;\r\n\t\tif ( !contactNow )\r\n\t\t{\r\n\t\t\t_wallStreak = 0;\r\n\t\t\treturn;\r\n\t\t}\r\n\t\t_wallStreak++;\r\n\r\n\t\tif ( Definition is null || !Definition.WallGlanceAssist || Assists == AssistLevel.Sim )\r\n\t\t\treturn;\r\n\t\tif ( _wallStreak < WallEngageTicks )\r\n\t\t\treturn;\r\n\r\n\t\tvar vel = _rigidbody.Velocity;\r\n\t\tvar planar = vel.WithZ( 0f );\r\n\t\tfloat planarSpeedMs = planar.Length * Units.UnitsToMeters;\r\n\t\tif ( planarSpeedMs < WallGlanceMinSpeed )\r\n\t\t\treturn;\r\n\r\n\t\tvar nH = _wallNormalH;\r\n\t\tfloat into = Vector3.Dot( planar.Normal, nH ); // <0 \u21d2 moving INTO the wall\r\n\t\tif ( into >= -0.01f )\r\n\t\t\treturn; // already sliding along / peeling away \u2014 nothing to catch\r\n\r\n\t\t// incidence between velocity and the wall PLANE: 0\u00b0 = grazing, 90\u00b0 = straight-on\r\n\t\tfloat incidenceDeg = MathF.Asin( Math.Clamp( MathF.Abs( into ), 0f, 1f ) ).RadianToDegree();\r\n\r\n\t\t// full assist below shallow, none at/above head-on (frontal crashes keep the hard stop)\r\n\t\tfloat scale = incidenceDeg >= Definition.WallGlanceHeadOnDeg ? 0f\r\n\t\t\t: incidenceDeg <= Definition.WallGlanceShallowDeg ? 1f\r\n\t\t\t: (Definition.WallGlanceHeadOnDeg - incidenceDeg)\r\n\t\t\t\t/ MathF.Max( 1e-3f, Definition.WallGlanceHeadOnDeg - Definition.WallGlanceShallowDeg );\r\n\t\tif ( scale <= 0f )\r\n\t\t\treturn;\r\n\r\n\t\t// tangent = the velocity component that runs ALONG the wall (the slide direction)\r\n\t\tvar vTangent = planar - Vector3.Dot( planar, nH ) * nH;\r\n\t\tif ( vTangent.IsNearZeroLength )\r\n\t\t\treturn;\r\n\t\tvar tangent = vTangent.Normal;\r\n\r\n\t\t// (a) re-project velocity onto the tangent: kill the into-wall component (which would wedge\r\n\t\t// the rigid chassis corner and dead-stop it) and carry the slide forward at\r\n\t\t// max(natural tangential speed, WallScrubFactor\u00b7total). Once the car is redirected along the\r\n\t\t// wall the target equals the current tangential speed, so it slides steadily instead of\r\n\t\t// grinding to a halt. Blended by incidence so a near head-on keeps the physics dead-stop.\r\n\t\tfloat totalSpeed = planar.Length;\r\n\t\tfloat targetSpeed = MathF.Min( totalSpeed,\r\n\t\t\tMathF.Max( vTangent.Length, totalSpeed * Definition.WallScrubFactor ) );\r\n\t\tvar newPlanar = Vector3.Lerp( planar, tangent * targetSpeed, scale );\r\n\t\t_rigidbody.Velocity = newPlanar.WithZ( vel.z );\r\n\r\n\t\t// (b) gentle yaw torque aligning heading to the wall tangent (whichever way the car faces)\r\n\t\tvar fwd = WorldRotation.Forward.WithZ( 0f ).Normal;\r\n\t\tvar alignTo = Vector3.Dot( fwd, tangent ) < 0f ? -tangent : tangent;\r\n\t\tfloat cross = fwd.x * alignTo.y - fwd.y * alignTo.x; // +z \u21d2 target is to the LEFT (CCW)\r\n\t\tfloat yawErrRad = MathF.Atan2( cross, Vector3.Dot( fwd, alignTo ) );\r\n\t\tvar ang = _rigidbody.AngularVelocity;\r\n\t\tang.z = MathX.Lerp( ang.z, yawErrRad * Definition.WallAlignStrength,\r\n\t\t\tMath.Clamp( Definition.WallAlignStrength * scale * Time.Delta, 0f, 1f ) );\r\n\t\t_rigidbody.AngularVelocity = ang;\r\n\r\n\t\t// throttled so a multi-tick slide doesn't flood the log\r\n\t\tif ( _wallGlanceLog > 0.2f )\r\n\t\t{\r\n\t\t\t_wallGlanceLog = 0f;\r\n\t\t\tLog.Info( $\"[vp] wallglance inc {incidenceDeg:F0}deg scale {scale:F2} v {planarSpeedMs * 3.6f:F0}->{newPlanar.Length * Units.UnitsToMeters * 3.6f:F0}kmh\" );\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Respawn()\r\n\t{\r\n\t\tWorldPosition = _spawnPosition + Vector3.Up * 20f;\r\n\t\tWorldRotation = _spawnRotation;\r\n\t\t_rigidbody.Velocity = Vector3.Zero;\r\n\t\t_rigidbody.AngularVelocity = Vector3.Zero;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Vehicle Physics Kit\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"vehiclephysics\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"fieldguide\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"fieldguide.vehiclephysics\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"28\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-07-22T03:31:24.7233296Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.120.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.120.0\")]"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/TireCurve.cs",
            "FileName": "TireCurve.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Parametric peaked tire curve (Pacejka-shaped, authored by peak/tail points).\r\n/// Rises smoothly to a grip peak, then falls off to a sliding asymptote.\r\n/// Input is |slip| \u2014 slip ratio (unitless) for longitudinal, slip angle (radians) for lateral.\r\n/// Output is a grip coefficient multiplied by tire load to get force.\r\n/// </summary>\r\npublic struct TireCurve\r\n{\r\n\t/// <summary>Slip value where grip peaks (\u03ba \u2248 0.08\u20130.12, \u03b1 \u2248 6\u20139\u00b0 in radians).</summary>\r\n\tpublic float PeakSlip { get; set; }\r\n\r\n\t/// <summary>Grip coefficient at the peak.</summary>\r\n\tpublic float PeakGrip { get; set; }\r\n\r\n\t/// <summary>Slip value where the curve has fully fallen to the tail.</summary>\r\n\tpublic float TailSlip { get; set; }\r\n\r\n\t/// <summary>Grip coefficient when fully sliding (typically 75\u201385% of peak).</summary>\r\n\tpublic float TailGrip { get; set; }\r\n\r\n\tpublic TireCurve( float peakSlip, float peakGrip, float tailSlip, float tailGrip )\r\n\t{\r\n\t\tPeakSlip = peakSlip;\r\n\t\tPeakGrip = peakGrip;\r\n\t\tTailSlip = tailSlip;\r\n\t\tTailGrip = tailGrip;\r\n\t}\r\n\r\n\tpublic readonly float Evaluate( float slip )\r\n\t{\r\n\t\tslip = MathF.Abs( slip );\r\n\r\n\t\tif ( slip <= PeakSlip )\r\n\t\t{\r\n\t\t\t// parabolic rise with zero slope at the peak\r\n\t\t\tfloat n = slip / PeakSlip;\r\n\t\t\treturn PeakGrip * n * (2f - n);\r\n\t\t}\r\n\r\n\t\t// smoothstep decay from peak down to the tail\r\n\t\tfloat t = Math.Clamp( (slip - PeakSlip) / (TailSlip - PeakSlip), 0f, 1f );\r\n\t\tt = t * t * (3f - 2f * t);\r\n\t\treturn PeakGrip + (TailGrip - PeakGrip) * t;\r\n\t}\r\n\r\n\tpublic static TireCurve Street => new( 0.10f, 1.00f, 0.45f, 0.80f );\r\n\tpublic static TireCurve Sport => new( 0.09f, 1.15f, 0.40f, 0.92f );\r\n\tpublic static TireCurve Offroad => new( 0.14f, 0.90f, 0.60f, 0.75f );\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "TireCurve.cs",
            "FileName": "TireCurve.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Parametric peaked tire curve (Pacejka-shaped, authored by peak/tail points).\r\n/// Rises smoothly to a grip peak, then falls off to a sliding asymptote.\r\n/// Input is |slip| \u2014 slip ratio (unitless) for longitudinal, slip angle (radians) for lateral.\r\n/// Output is a grip coefficient multiplied by tire load to get force.\r\n/// </summary>\r\npublic struct TireCurve\r\n{\r\n\t/// <summary>Slip value where grip peaks (\u03ba \u2248 0.08\u20130.12, \u03b1 \u2248 6\u20139\u00b0 in radians).</summary>\r\n\tpublic float PeakSlip { get; set; }\r\n\r\n\t/// <summary>Grip coefficient at the peak.</summary>\r\n\tpublic float PeakGrip { get; set; }\r\n\r\n\t/// <summary>Slip value where the curve has fully fallen to the tail.</summary>\r\n\tpublic float TailSlip { get; set; }\r\n\r\n\t/// <summary>Grip coefficient when fully sliding (typically 75\u201385% of peak).</summary>\r\n\tpublic float TailGrip { get; set; }\r\n\r\n\tpublic TireCurve( float peakSlip, float peakGrip, float tailSlip, float tailGrip )\r\n\t{\r\n\t\tPeakSlip = peakSlip;\r\n\t\tPeakGrip = peakGrip;\r\n\t\tTailSlip = tailSlip;\r\n\t\tTailGrip = tailGrip;\r\n\t}\r\n\r\n\tpublic readonly float Evaluate( float slip )\r\n\t{\r\n\t\tslip = MathF.Abs( slip );\r\n\r\n\t\tif ( slip <= PeakSlip )\r\n\t\t{\r\n\t\t\t// parabolic rise with zero slope at the peak\r\n\t\t\tfloat n = slip / PeakSlip;\r\n\t\t\treturn PeakGrip * n * (2f - n);\r\n\t\t}\r\n\r\n\t\t// smoothstep decay from peak down to the tail\r\n\t\tfloat t = Math.Clamp( (slip - PeakSlip) / (TailSlip - PeakSlip), 0f, 1f );\r\n\t\tt = t * t * (3f - 2f * t);\r\n\t\treturn PeakGrip + (TailGrip - PeakGrip) * t;\r\n\t}\r\n\r\n\tpublic static TireCurve Street => new( 0.10f, 1.00f, 0.45f, 0.80f );\r\n\tpublic static TireCurve Sport => new( 0.09f, 1.15f, 0.40f, 0.92f );\r\n\tpublic static TireCurve Offroad => new( 0.14f, 0.90f, 0.60f, 0.75f );\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "WheelVisual.cs",
            "FileName": "WheelVisual.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Spins and vertically tracks the wheel's visual mesh from VehicleWheel state.\r\n/// Steering yaw lives on the wheel GameObject itself (set by VehicleController);\r\n/// spin composes with whatever base orientation the factory gave the visual.\r\n/// </summary>\r\npublic sealed class WheelVisual : Component\r\n{\r\n\tpublic VehicleWheel Wheel { get; set; }\r\n\r\n\tRotation _baseRotation;\r\n\tfloat _spinDegrees;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_baseRotation = LocalRotation;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Wheel is null )\r\n\t\t\treturn;\r\n\r\n\t\t_spinDegrees += Wheel.AngularVelocity.RadianToDegree() * Time.Delta;\r\n\t\t_spinDegrees %= 360f;\r\n\r\n\t\t// Spin about the model-local +Y axle. FromPitch uses the Source pitch sign (positive pitch\r\n\t\t// tilts the local +X forward vector DOWN), so a POSITIVE angle here rolls the top of the wheel\r\n\t\t// toward the car's +X travel direction, the correct rolling sense for forward motion. The old\r\n\t\t// negated angle rolled the tread backwards while driving forward (community report: \"wheels\r\n\t\t// rotate the wrong way\"). AngularVelocity is +forward, so the pitch angle is used as-is.\r\n\t\t// Per-frame is correct for SPIN: the accumulator integrates with frame dt, and rotation\r\n\t\t// interpolates in its own buffer independent of the fixed-tick position write below.\r\n\t\tLocalRotation = _baseRotation * Rotation.FromPitch( _spinDegrees );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Suspension tracking runs at the FIXED tick, not per frame (ramp-hitch fix, 2026-07-21,\r\n\t/// LIVE-UNVERIFIED). <see cref=\"VehicleWheel.SuspensionLength\"/> is a raw physics-tick field:\r\n\t/// writing it to LocalPosition per RENDER frame is the documented sawtooth anti-pattern (KB\r\n\t/// g-game-camera-follows-raw-fixedtick-feet-model-sawtooths): the body renders engine-interpolated\r\n\t/// while the wheels step at 50 Hz, so wheels judder against the body exactly where suspension\r\n\t/// length changes fast (ramp faces, transients), worse with speed and refresh rate; a wheel that\r\n\t/// unloads for one tick snapped 5-10 cm to full droop and back inside 1-2 frames. Owner\r\n\t/// discriminator: fps_max 50 (render rate = tick rate) made the felt ramp hitch \"a million times\r\n\t/// better\". Writing inside OnFixedUpdate lets GameTransform interpolation carry the motion per\r\n\t/// frame, exactly like the chassis (KB g-game-manual-visual-smoother-fights-fixedupdate-\r\n\t/// interpolation: never hand-smooth what engine interpolation already covers).\r\n\t/// </summary>\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( Wheel is null )\r\n\t\t\treturn;\r\n\r\n\t\tLocalPosition = Vector3.Down * Wheel.SuspensionLength * Units.MetersToUnits;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "CarDefinition.cs",
            "FileName": "CarDefinition.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\npublic enum DriveLayout\r\n{\r\n\tFWD,\r\n\tRWD,\r\n\tAWD\r\n}\r\n\r\npublic enum BodyStyle\r\n{\r\n\tBox,  // sedan/hatch blockout: body + cabin\r\n\tKart  // flat deck + seat back + citizen driver\r\n}\r\n\r\npublic enum AssistLevel\r\n{\r\n\tCasual, // full traction control + yaw-stability damping + ABS\r\n\tSport,  // ABS + optional REDUCED-authority TC / yaw-stability (per-car opt-in; drift feel preserved)\r\n\tSim     // nothing\r\n}\r\n\r\n/// <summary>\r\n/// Complete tuning spec for one car. Plain class, instantiated from <see cref=\"CarDefinitions\"/>;\r\n/// promote to a [GameResource] .car asset once an editor workflow is in play so designers can tune\r\n/// without recompiling. All values SI: meters, kg, N, N-m, N/m.\r\n/// </summary>\r\npublic class CarDefinition\r\n{\r\n\tpublic string Name { get; set; } = \"Car\";\r\n\tpublic BodyStyle Style { get; set; } = BodyStyle.Box;\r\n\tpublic bool HasDriver { get; set; }\r\n\r\n\t/// <summary>Optional opaque body-manifest path for a consumer-supplied custom body builder\r\n\t/// (see <see cref=\"VehicleFactory.CustomBodyBuilder\"/>). The kit itself never reads it \u2014 with no\r\n\t/// custom builder plugged in, the factory builds a primitive blockout body and this value is\r\n\t/// inert. It exists so a consumer's builder (e.g. a part-kit assembler) can look up which body to\r\n\t/// assemble for this car. null keeps the blockout path byte-for-byte unchanged.</summary>\r\n\tpublic string BodyManifest { get; set; }\r\n\r\n\t// Chassis\r\n\tpublic float Mass { get; set; } = 1200f;\r\n\tpublic Vector3 BodySize { get; set; } = new( 4.0f, 1.8f, 1.3f ); // length, width, height (m)\r\n\tpublic float Wheelbase { get; set; } = 2.55f;\r\n\tpublic float TrackWidth { get; set; } = 1.55f;\r\n\tpublic float RideHeight { get; set; } = 0.35f; // chassis center above wheel attach plane\r\n\tpublic float GroundClearance { get; set; } = 0.14f; // collider bottom above ground at rest\r\n\tpublic float CenterOfMassDrop { get; set; } = 0.20f; // CoM below chassis center; must stay above wheel plane\r\n\r\n\t// Wheels & suspension (per wheel)\r\n\tpublic float WheelRadius { get; set; } = 0.31f;\r\n\tpublic float WheelInertia { get; set; } = 1.2f; // kg m^2\r\n\tpublic float SuspensionTravel { get; set; } = 0.20f;\r\n\tpublic float SpringRate { get; set; } = 38000f;\r\n\tpublic float DamperRate { get; set; } = 2800f;\r\n\r\n\t// Tires\r\n\tpublic TireCurve LongitudinalCurve { get; set; } = TireCurve.Street;\r\n\tpublic TireCurve LateralCurve { get; set; } = new( 0.14f, 1.00f, 0.55f, 0.80f ); // radians: peak ~8 deg\r\n\tpublic float LoadSensitivity { get; set; } = 0.06f;\r\n\r\n\t// Drivetrain\r\n\tpublic DriveLayout Layout { get; set; } = DriveLayout.FWD;\r\n\t// +20% speed pass 2026-07-21: baseline PeakTorque 150->180 and RedlineRpm 6500->7800 so a bare\r\n\t// kit consumer starts from the new faster baseline (the demo roster below overrides both per car).\r\n\tpublic float PeakTorque { get; set; } = 180f;\r\n\tpublic float IdleRpm { get; set; } = 900f;\r\n\tpublic float RedlineRpm { get; set; } = 7800f;\r\n\tpublic float EngineInertia { get; set; } = 0.25f; // kg m^2 at crank\r\n\tpublic float EngineBrakeTorque { get; set; } = 40f;\r\n\tpublic float[] GearRatios { get; set; } = { 3.6f, 2.1f, 1.4f, 1.05f, 0.85f };\r\n\tpublic float ReverseRatio { get; set; } = 3.4f;\r\n\tpublic float FinalDrive { get; set; } = 3.9f;\r\n\tpublic float ShiftUpRpm { get; set; } = 5800f;\r\n\tpublic float ShiftDownRpm { get; set; } = 2200f;\r\n\r\n\t// Engine audio (read by EngineAudio). Per-class dials so a small buzzy kart and a deep\r\n\t// sedan/truck share one code path:\r\n\t//   EngineSoundEvent     \u2014 the LOW-RPM loop this class plays (idle/low, the deep layer).\r\n\t//   EngineSoundEventHigh \u2014 the HIGH-RPM loop, crossfaded in as revs climb. When null/empty the\r\n\t//     class runs the legacy SINGLE-loop model (one handle, wide pitch sweep) \u2014 the kart keeps\r\n\t//     this so its single synth purr is unchanged. When set, EngineAudio blends the two recorded\r\n\t//     layers by RPM, and each layer only pitch-shifts a narrow band around its recorded pitch\r\n\t//     (no far stretch = no screech), which is the point of the layered model.\r\n\t//   EnginePitchBase      \u2014 multiplies the pitch of whichever model runs, so a class reads higher\r\n\t//     or lower overall (kart >1 stays buzzy, truck <1 sits deep).\r\n\tpublic string EngineSoundEvent { get; set; } = \"sounds/engine/engine_real_low.sound\";\r\n\tpublic string EngineSoundEventHigh { get; set; }\r\n\tpublic float EnginePitchBase { get; set; } = 1f;\r\n\r\n\t// Brakes\r\n\tpublic float BrakeTorque { get; set; } = 2400f; // total, split by bias\r\n\tpublic float BrakeBias { get; set; } = 0.62f;   // fraction to front\r\n\tpublic float HandbrakeTorque { get; set; } = 3000f; // rear wheels only\r\n\r\n\t// ABS dials (per-car). 0.3/0.55 because telemetry showed the ABS duty cycle, not tire grip, was\r\n\t// the brake limiter, and per-car ABS modulation is a legitimate class trait: the locking kart\r\n\t// needs a different release than the truck. Active in Casual + Sport.\r\n\tpublic float AbsSlipThreshold { get; set; } = 0.25f; // release when SlipRatio < -this under braking\r\n\tpublic float AbsReleaseFactor { get; set; } = 0.70f; // brake torque multiplier while released\r\n\r\n\t// Drift-exit soft-lock: cap the handbrake-INDUCED rear slip ratio. When a cap is active and a rear\r\n\t// wheel is already sliding PAST it, its handbrake torque is withheld that substep (ABS-style duty\r\n\t// cycle) so the rear spins back up toward the cap and keeps some rotation \u2014 hence some lateral bite\r\n\t// \u2014 mid-slide. Default -1.0 = NO effective cap (byte-identical full-lock behavior).\r\n\tpublic float HandbrakeSlipCap { get; set; } = -1.0f;\r\n\r\n\t// Steering\r\n\tpublic float MaxSteerAngle { get; set; } = 32f; // degrees at standstill\r\n\tpublic float HighSpeedSteerAngle { get; set; } = 8f; // degrees at/above 30 m/s\r\n\r\n\t// Arcade feel dials (defaults = the original sim-leaning behavior)\r\n\tpublic float SteerRateScale { get; set; } = 1f;    // multiplies how fast steering ramps\r\n\t// Reverse speed pass 2026-07-21 (owner feel): raised 4.5 -> 20.0 m/s (~10 -> ~45 mph). This is the\r\n\t// ACTUAL reverse cap mechanism (VehicleController cuts reverse throttle above this speed), and the\r\n\t// game roster inherits it (only the pickup used to override it). Each car is still additionally\r\n\t// limited by its reverse-gear redline-equivalent wheel speed (RedlineRpm/ReverseRatio/FinalDrive/\r\n\t// WheelRadius via Drivetrain.RedlineWheelSpeed), so torquey-but-tall reverse gears self-limit below\r\n\t// this cap (kart ~8 m/s, pickup ~12 m/s) while the cars reach the high-30s/40s mph.\r\n\tpublic float ReverseSpeedCap { get; set; } = 20.0f; // m/s before reverse throttle cuts\r\n\tpublic float LaunchBoost { get; set; } = 1f;       // torque multiplier at standstill, fades out by ~54 km/h\r\n\tpublic float BrakeAssist { get; set; } = 0f;       // extra chassis-level decel while braking (m/s\u00b2)\r\n\t// Spin-recovery assist: after a handbrake spin the car keeps rolling BACKWARDS (old travel\r\n\t// direction) while the player holds throttle the new way \u2014 BrakeAssist can't help there (forward\r\n\t// throttle sets Brake=0), so nothing arcade-level arrests the stale velocity. This is extra\r\n\t// chassis-level decel along -velocity, applied ONLY when input throttle opposes the ground velocity\r\n\t// along the car's facing, fading out as the car rotates to face its motion\r\n\t// (VehicleController.ApplySpinRecoveryAssist). Same m/s\u00b2 unit + never-reverse-within-a-step cap as\r\n\t// BrakeAssist; gated Assists != Sim. 0 disables.\r\n\tpublic float SpinRecoveryAssist { get; set; } = 0f; // extra chassis decel killing stale opposing velocity (m/s\u00b2)\r\n\tpublic float HandbrakeGripScale { get; set; } = 1f; // rear grip multiplier while handbrake held (<1 = drift button)\r\n\r\n\t// Wall-glance forgiveness assist: a sustained near-horizontal chassis contact while moving\r\n\t// re-projects velocity along the wall tangent and gently yaws the heading to match, scaled by\r\n\t// incidence (VehicleController.ApplyWallGlanceAssist). Active only when Assists != Sim. Head-on hits\r\n\t// (incidence >= WallGlanceHeadOnDeg) get NO assist \u2014 frontal crashes stay hard stops.\r\n\tpublic bool WallGlanceAssist { get; set; } = true;\r\n\tpublic float WallScrubFactor { get; set; } = 0.75f;    // fraction of speed kept along the wall tangent on a shallow graze\r\n\tpublic float WallGlanceShallowDeg { get; set; } = 35f; // at/below this velocity-vs-wall incidence: full assist\r\n\tpublic float WallGlanceHeadOnDeg { get; set; } = 60f;  // at/above this incidence: no assist (hard stop preserved)\r\n\tpublic float WallAlignStrength { get; set; } = 6f;     // yaw-align rate toward the wall tangent (per second)\r\n\r\n\t// \u2500\u2500 Sport-mode stability posture (owner call 2026-07-21) \u2500\u2500\r\n\t// Sport historically ran with NO traction control and NO yaw-stability damping (both Casual-only),\r\n\t// so a full-throttle RWD car spun the rears to redline and the counter-steer pendulum went divergent\r\n\t// \u2014 uncatchable spin-outs. These give Sport a REDUCED-authority version of each assist, opt-in per\r\n\t// car. Both default to 0 (disabled), so any car that doesn't set them keeps the raw \"ABS only\" Sport\r\n\t// and \u2014 critically \u2014 Casual and Sim stay byte-identical (the controller multiplies Casual authority\r\n\t// by exactly 1 and returns early for Sim, unchanged).\r\n\r\n\t// Sport traction control. When >0, Sport runs the same proportional TC as Casual\r\n\t// (VehicleController.ApplyTractionControl) but holds driven-wheel slip near THIS ratio instead of the\r\n\t// Casual 0.14 grip-peak target. Set it LOOSER than the peak (e.g. 0.30-0.45) so the rears still break\r\n\t// into a throttle-steerable slide (drift stays alive) while the redline free-spin that torches rear\r\n\t// lateral grip is capped. 0 = no Sport TC (raw wheelspin, the old behavior).\r\n\tpublic float SportTcSlipTarget { get; set; } = 0f;\r\n\r\n\t// Sport yaw-stability. When >0, Sport runs the yaw-rate damper (VehicleController.ApplyStabilityAssist)\r\n\t// at THIS fraction of the Casual authority (0-1). It bleeds the stored yaw rate that snaps a\r\n\t// counter-steered slide the OTHER way (the pendulum spin-out) so slides stay CATCHABLE, without\r\n\t// killing deliberate rotation the way full Casual authority would. 0 = no Sport yaw damping.\r\n\tpublic float SportStabilityScale { get; set; } = 0f;\r\n\r\n\t// Driver seated pose (citizen animgraph; sit enum: 0 none, 1-3 chair poses, 4-5 ground poses).\r\n\t// Defaults = the original upright chair pose. Made per-car so a recumbent kart driver \u2014 legs\r\n\t// extended forward to the pedals \u2014 can be authored without disturbing any upright-seated car.\r\n\tpublic int DriverSit { get; set; } = 1;\r\n\tpublic float DriverSitOffsetHeight { get; set; } = 4f;\r\n\r\n\t// Defaults\r\n\tpublic AssistLevel DefaultAssists { get; set; } = AssistLevel.Casual;\r\n\tpublic Color Tint { get; set; } = new( 0.85f, 0.55f, 0.35f );\r\n}\r\n\r\n/// <summary>Car roster. Hatch is the default/first car; Kart and Coupe round out the roster.\r\n/// These are blockout-body demo definitions \u2014 physics is identical whether a car renders as a\r\n/// primitive blockout or a consumer-supplied custom body.</summary>\r\npublic static class CarDefinitions\r\n{\r\n\t/// <summary>The default/first roster car \u2014 an ORANGE hot hatch. FWD, mid-power, street tires.</summary>\r\n\tpublic static CarDefinition Hatch => new()\r\n\t{\r\n\t\tName = \"Compact Hatch\",\r\n\t\tMass = 1150f,\r\n\t\tBodySize = new Vector3( 3.9f, 1.75f, 1.45f ),\r\n\t\tWheelbase = 2.55f,\r\n\t\tTrackWidth = 1.50f,\r\n\t\tWheelRadius = 0.30f,\r\n\t\tLayout = DriveLayout.FWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 162->194.4, RedlineRpm 6300->7560 (top-gear top speed +20%).\r\n\t\tPeakTorque = 194.4f,\r\n\t\tRedlineRpm = 7560f,\r\n\t\t// Real recorded car set: deep muscle idle crossfaded up to a revving high layer.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_real_low.sound\",\r\n\t\tEngineSoundEventHigh = \"sounds/engine/engine_real_high.sound\",\r\n\t\tLongitudinalCurve = new TireCurve( 0.10f, 1.35f, 0.45f, 1.08f ),\r\n\t\tBrakeTorque = 4300f,\r\n\t\tLateralCurve = new TireCurve( 0.14f, 1.30f, 0.55f, 1.04f ),\r\n\t\tHandbrakeGripScale = 0.55f, // the drift button \u2014 rear grip cut while handbrake held; also the FWD J-turn lever\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tHighSpeedSteerAngle = 9.5f,\r\n\t\tSpringRate = 34000f,\r\n\t\tDamperRate = 2500f,\r\n\t\tTint = new Color( 0.93f, 0.42f, 0.03f ), // orange\r\n\t};\r\n\r\n\t/// <summary>Full-size pickup. Heaviest in roster, RWD, torquey low-rev engine, longer-travel\r\n\t/// softer suspension, offroad-leaning tires, high ride. Signature strength = hill grade.</summary>\r\n\tpublic static CarDefinition Pickup => new()\r\n\t{\r\n\t\tName = \"Utility Pickup\",\r\n\t\tMass = 1900f,\r\n\t\tBodySize = new Vector3( 5.2f, 1.95f, 1.55f ),\r\n\t\tWheelbase = 3.40f,\r\n\t\tTrackWidth = 1.70f,\r\n\t\tRideHeight = 0.44f,          // high ride: the class signature (hatch 0.35)\r\n\t\tGroundClearance = 0.24f,\r\n\t\tCenterOfMassDrop = 0.15f,    // higher CoM than the cars = truck-like roll; track 1.70 keeps rollover margin\r\n\t\tWheelRadius = 0.35f,\r\n\t\tWheelInertia = 2.4f,\r\n\t\tSuspensionTravel = 0.26f,    // longest in roster \u2014 washboard/offroad strength\r\n\t\tSpringRate = 42000f,\r\n\t\tDamperRate = 3400f,\r\n\t\tLongitudinalCurve = new TireCurve( 0.14f, 1.25f, 0.60f, 1.05f ),\r\n\t\tLateralCurve = new TireCurve( 0.15f, 1.22f, 0.60f, 1.06f ),\r\n\t\tLoadSensitivity = 0.07f,\r\n\t\tLayout = DriveLayout.RWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 320->384, RedlineRpm 3900->4680 (top-gear top speed +20%).\r\n\t\tPeakTorque = 384f,           // torquey: >2\u00d7 hatch; strong hills\r\n\t\tIdleRpm = 650f,\r\n\t\tRedlineRpm = 4680f,          // lowest redline in roster; low-rev truck character (was 3900, +20% pass)\r\n\t\tEngineInertia = 0.5f,\r\n\t\tEngineBrakeTorque = 90f,     // strong engine braking downhill\r\n\t\t// Real recorded truck set: deeper truck idle + revving high layer; base pitch dropped.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_real_truck_low.sound\",\r\n\t\tEngineSoundEventHigh = \"sounds/engine/engine_real_truck_high.sound\",\r\n\t\tEnginePitchBase = 0.85f,\r\n\t\tGearRatios = new[] { 3.8f, 2.3f, 1.5f, 1.1f, 0.85f },\r\n\t\tReverseRatio = 3.8f,\r\n\t\tFinalDrive = 3.9f,\r\n\t\tShiftUpRpm = 3500f,\r\n\t\tShiftDownRpm = 1700f,\r\n\t\tBrakeTorque = 7000f,\r\n\t\tBrakeBias = 0.65f,           // unladen bed = light rear axle\r\n\t\tHandbrakeTorque = 5000f,\r\n\t\tMaxSteerAngle = 27f,         // slow truck steering; long wheelbase stabilizes\r\n\t\tHighSpeedSteerAngle = 8f,\r\n\t\tSteerRateScale = 0.9f,\r\n\t\t// Reverse cap override removed 2026-07-21: inherits the new 20 m/s default; the pickup's tall reverse\r\n\t\t// gear + low redline self-limit it to ~12 m/s (~26 mph) reverse, so it stays the roster's slowest\r\n\t\t// reverse by gearing character rather than an artificial ~9 mph clamp.\r\n\t\tHandbrakeGripScale = 0.45f,  // deepest rear cut in the roster \u2014 1900 kg + 3.4 m wheelbase needs real help\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tSportTcSlipTarget = 0.30f,   // Sport keeps the torquey RWD truck from lighting the rears to redline\r\n\t\tSportStabilityScale = 0.5f,  // half-authority yaw damp so a Sport slide stays catchable\r\n\t\tDefaultAssists = AssistLevel.Casual,\r\n\t\tTint = new Color( 0.55f, 0.13f, 0.11f ), // dark brick red\r\n\t};\r\n\r\n\tpublic static CarDefinition Kart => new()\r\n\t{\r\n\t\tName = \"Go-Kart\",\r\n\t\tStyle = BodyStyle.Kart,\r\n\t\tHasDriver = true,\r\n\t\tMass = 260f,\r\n\t\tBodySize = new Vector3( 1.9f, 1.15f, 0.30f ),\r\n\t\tWheelbase = 1.55f,\r\n\t\tTrackWidth = 1.14f,\r\n\t\tRideHeight = 0.17f,\r\n\t\tGroundClearance = 0.08f,\r\n\t\tCenterOfMassDrop = 0.02f, // tiny chassis: a deep drop puts CoM below the wheels\r\n\t\tWheelRadius = 0.16f,\r\n\t\tWheelInertia = 0.18f,\r\n\t\tSuspensionTravel = 0.14f,\r\n\t\tSpringRate = 24000f,\r\n\t\tDamperRate = 1600f,\r\n\t\tLayout = DriveLayout.RWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 52->62.4, RedlineRpm 9000->10800 (top-gear top speed +20%).\r\n\t\tPeakTorque = 62.4f, // punchy launch, still shifts out of wheelspin quickly\r\n\t\tIdleRpm = 1400f,\r\n\t\tRedlineRpm = 10800f,\r\n\t\tEngineInertia = 0.05f,\r\n\t\tEngineBrakeTorque = 8f,\r\n\t\t// The kart keeps the buzzier single loop; base pitch >1 preserves its whine, while the narrowed\r\n\t\t// band tames the redline chipmunk.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_b_sport_purr.sound\",\r\n\t\tEnginePitchBase = 1.2f,\r\n\t\tGearRatios = new[] { 3.4f, 2.4f, 1.8f, 1.4f, 1.1f },\r\n\t\tReverseRatio = 3.4f,\r\n\t\tFinalDrive = 6.3f,\r\n\t\tShiftUpRpm = 8000f,\r\n\t\tShiftDownRpm = 4000f,\r\n\t\tBrakeTorque = 560f,\r\n\t\tHandbrakeTorque = 900f,\r\n\t\tMaxSteerAngle = 31f,\r\n\t\tHighSpeedSteerAngle = 9f,\r\n\t\t// sticky kart slicks\r\n\t\tLongitudinalCurve = new TireCurve( 0.09f, 1.55f, 0.40f, 1.24f ),\r\n\t\tLateralCurve = new TireCurve( 0.12f, 1.66f, 0.45f, 1.32f ),\r\n\t\tAbsSlipThreshold = 0.20f, // earlier release for the lockup-prone kart\r\n\t\tHandbrakeGripScale = 0.70f, // mild rear cut \u2014 the light kart already rotates\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tSportTcSlipTarget = 0.40f,  // loosest Sport TC \u2014 the light kart is meant to be playful\r\n\t\tSportStabilityScale = 0.45f, // gentle yaw damp; the kart rotates easily so keep it light\r\n\t\tHandbrakeSlipCap = -0.7f, // keep the rears rotating mid-slide for a cleaner drift exit\r\n\t\t// Recumbent kart driver pose: reclines with legs extended forward to the pedals.\r\n\t\tDriverSit = 4,\r\n\t\tDriverSitOffsetHeight = 0f,\r\n\t\tDefaultAssists = AssistLevel.Casual, // default fun car: keep it catchable\r\n\t\tTint = new Color( 0.58f, 0.83f, 0.07f ), // acid green\r\n\t};\r\n\r\n\tpublic static CarDefinition Coupe => new()\r\n\t{\r\n\t\tName = \"Sports Coupe\",\r\n\t\tMass = 1420f,\r\n\t\tBodySize = new Vector3( 4.4f, 1.85f, 1.25f ),\r\n\t\tWheelbase = 2.7f,\r\n\t\tTrackWidth = 1.60f,\r\n\t\tLayout = DriveLayout.RWD,\r\n\t\t// +20% speed pass 2026-07-21: PeakTorque 340->408, RedlineRpm 7200->8640 (top-gear top speed +20%).\r\n\t\t// ShiftUpRpm left at 6600 (unchanged) so the shift ladder holds in ground-speed terms; only the\r\n\t\t// top gear extends to the new redline.\r\n\t\tPeakTorque = 408f,\r\n\t\tRedlineRpm = 8640f,\r\n\t\tShiftUpRpm = 6600f,\r\n\t\t// Real recorded car set (shared with the hatch): deep muscle idle crossfaded up to a\r\n\t\t// revving high layer; base pitch neutral so it sweeps the band cleanly.\r\n\t\tEngineSoundEvent = \"sounds/engine/engine_real_low.sound\",\r\n\t\tEngineSoundEventHigh = \"sounds/engine/engine_real_high.sound\",\r\n\t\tEnginePitchBase = 1.0f,\r\n\t\tLongitudinalCurve = new TireCurve( 0.09f, 1.50f, 0.40f, 1.20f ),\r\n\t\tLateralCurve = new TireCurve( 0.13f, 1.69f, 0.50f, 1.36f ),\r\n\t\tHandbrakeGripScale = 0.55f, // rears break loose under handbrake (J-turn initiation)\r\n\t\tHandbrakeSlipCap = -0.7f,\r\n\t\tSpringRate = 46000f,\r\n\t\tDamperRate = 3600f,\r\n\t\tBrakeTorque = 6200f,\r\n\t\tWheelRadius = 0.33f,\r\n\t\tSpinRecoveryAssist = 7.0f,\r\n\t\tSportTcSlipTarget = 0.35f,   // Sport TC: rears still slide on throttle but can't free-spin to redline\r\n\t\tSportStabilityScale = 0.5f,  // half-authority yaw damp: the counter-steer pendulum stays catchable\r\n\t\tMaxSteerAngle = 30f,\r\n\t\tHighSpeedSteerAngle = 10f, // sportiest car gets the most high-speed turn-in\r\n\t\tTint = new Color( 0.80f, 0.05f, 0.07f ), // bright signal red\r\n\t};\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/WheelVisual.cs",
            "FileName": "WheelVisual.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Spins and vertically tracks the wheel's visual mesh from VehicleWheel state.\r\n/// Steering yaw lives on the wheel GameObject itself (set by VehicleController);\r\n/// spin composes with whatever base orientation the factory gave the visual.\r\n/// </summary>\r\npublic sealed class WheelVisual : Component\r\n{\r\n\tpublic VehicleWheel Wheel { get; set; }\r\n\r\n\tRotation _baseRotation;\r\n\tfloat _spinDegrees;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_baseRotation = LocalRotation;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Wheel is null )\r\n\t\t\treturn;\r\n\r\n\t\t_spinDegrees += Wheel.AngularVelocity.RadianToDegree() * Time.Delta;\r\n\t\t_spinDegrees %= 360f;\r\n\r\n\t\t// Spin about the model-local +Y axle. FromPitch uses the Source pitch sign (positive pitch\r\n\t\t// tilts the local +X forward vector DOWN), so a POSITIVE angle here rolls the top of the wheel\r\n\t\t// toward the car's +X travel direction, the correct rolling sense for forward motion. The old\r\n\t\t// negated angle rolled the tread backwards while driving forward (community report: \"wheels\r\n\t\t// rotate the wrong way\"). AngularVelocity is +forward, so the pitch angle is used as-is.\r\n\t\t// Per-frame is correct for SPIN: the accumulator integrates with frame dt, and rotation\r\n\t\t// interpolates in its own buffer independent of the fixed-tick position write below.\r\n\t\tLocalRotation = _baseRotation * Rotation.FromPitch( _spinDegrees );\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// Suspension tracking runs at the FIXED tick, not per frame (ramp-hitch fix, 2026-07-21,\r\n\t/// LIVE-UNVERIFIED). <see cref=\"VehicleWheel.SuspensionLength\"/> is a raw physics-tick field:\r\n\t/// writing it to LocalPosition per RENDER frame is the documented sawtooth anti-pattern (KB\r\n\t/// g-game-camera-follows-raw-fixedtick-feet-model-sawtooths): the body renders engine-interpolated\r\n\t/// while the wheels step at 50 Hz, so wheels judder against the body exactly where suspension\r\n\t/// length changes fast (ramp faces, transients), worse with speed and refresh rate; a wheel that\r\n\t/// unloads for one tick snapped 5-10 cm to full droop and back inside 1-2 frames. Owner\r\n\t/// discriminator: fps_max 50 (render rate = tick rate) made the felt ramp hitch \"a million times\r\n\t/// better\". Writing inside OnFixedUpdate lets GameTransform interpolation carry the motion per\r\n\t/// frame, exactly like the chassis (KB g-game-manual-visual-smoother-fights-fixedupdate-\r\n\t/// interpolation: never hand-smooth what engine interpolation already covers).\r\n\t/// </summary>\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tif ( Wheel is null )\r\n\t\t\treturn;\r\n\r\n\t\tLocalPosition = Vector3.Down * Wheel.SuspensionLength * Units.MetersToUnits;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Demo/DemoBootstrap.cs",
            "FileName": "DemoBootstrap.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Minimal demo bootstrap for the Vehicle Physics Kit's demo scene. Spawns one blockout car per\r\n/// <see cref=\"CarDefinitions\"/> roster entry in a row on the flat pad, and points the scene's\r\n/// <see cref=\"VehicleCamera\"/> at the first one. Proves the kit drives in isolation; not part of the\r\n/// consumer surface (the demo scene is the only thing that references it).\r\n/// </summary>\r\npublic sealed class DemoBootstrap : Component\r\n{\r\n\t/// <summary>Spacing between spawned cars along the row (metres).</summary>\r\n\t[Property] public float SpacingMeters { get; set; } = 5f;\r\n\r\n\t/// <summary>Cars falling below this world height (metres) are placed back on their spawn spot.</summary>\r\n\t[Property] public float VoidResetHeightM { get; set; } = -20f;\r\n\r\n\t/// <summary>Held by every non-active car: neutral pedals, handbrake on. A parked car should be parked.</summary>\r\n\tstatic DriveInputs ParkedInputs => new() { Handbrake = true };\r\n\r\n\treadonly List<VehicleController> _cars = new();\r\n\treadonly List<Vector3> _spawns = new();\r\n\tVehicleController _active;\r\n\tVehicleCamera _camera;\r\n\tDemoTuningPanel _panel;\r\n\tGameObject _hud;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t// ~1.1 g like the source proving ground, so the tuned handling feels right. Read live by the\r\n\t\t// factory's static-load and seat-height math, so setting it here keeps every car level on spawn.\r\n\t\tif ( Scene.PhysicsWorld is not null )\r\n\t\t\tScene.PhysicsWorld.Gravity = Vector3.Down * 9.81f * 1.1f * Units.MetersToUnits;\r\n\r\n\t\tvar roster = new[]\r\n\t\t{\r\n\t\t\tCarDefinitions.Hatch,\r\n\t\t\tCarDefinitions.Coupe,\r\n\t\t\tCarDefinitions.Kart,\r\n\t\t\tCarDefinitions.Pickup,\r\n\t\t};\r\n\r\n\t\tfloat m = Units.MetersToUnits;\r\n\t\tfloat startY = -(roster.Length - 1) * SpacingMeters * 0.5f;\r\n\r\n\t\t_cars.Clear();\r\n\t\t_spawns.Clear();\r\n\t\tfor ( int i = 0; i < roster.Length; i++ )\r\n\t\t{\r\n\t\t\tvar def = roster[i];\r\n\t\t\tfloat seatZ = VehicleFactory.SeatHeightM( def );\r\n\t\t\tvar pos = new Vector3( 0f, startY + i * SpacingMeters, seatZ ) * m;\r\n\r\n\t\t\tvar go = VehicleFactory.Spawn( Scene, def, pos, Rotation.Identity );\r\n\t\t\tvar controller = go.Components.Get<VehicleController>();\r\n\t\t\tif ( controller is not null )\r\n\t\t\t{\r\n\t\t\t\t// Park it IMMEDIATELY, before its first physics tick can sample a live device\r\n\t\t\t\t// (a resting gamepad trigger past deadzone reads as brake and latches reverse).\r\n\t\t\t\tcontroller.InputOverride = ParkedInputs;\r\n\t\t\t\t_cars.Add( controller );\r\n\t\t\t\t_spawns.Add( pos );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t_active = _cars.FirstOrDefault();\r\n\t\tif ( _active is not null )\r\n\t\t\t_active.InputOverride = null;\r\n\r\n\t\t// Point the scene's chase camera at the active car so the demo frames it on load.\r\n\t\t_camera = Scene.GetAllComponents<VehicleCamera>().FirstOrDefault();\r\n\t\tif ( _camera is not null )\r\n\t\t\t_camera.Target = _active;\r\n\r\n\t\tMountTuningLab();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( _cars.Count < 2 )\r\n\t\t\treturn;\r\n\r\n\t\t// Bracket cycling, mirroring Vehicle Prototyping's bindings ( [ / ] , d-pad left/right ).\r\n\t\t// OnFixedUpdate's per-tick override pass handles the rest: the old car parks (handbrake),\r\n\t\t// the new one starts listening to the player.\r\n\t\tint step = Input.Pressed( \"CycleNext\" ) ? 1 : Input.Pressed( \"CyclePrev\" ) ? -1 : 0;\r\n\t\tif ( step == 0 )\r\n\t\t\treturn;\r\n\r\n\t\tint i = _cars.IndexOf( _active );\r\n\t\ti = ((i + step) % _cars.Count + _cars.Count) % _cars.Count;\r\n\t\t_active = _cars[i];\r\n\r\n\t\tif ( _camera is not null )\r\n\t\t\t_camera.Target = _active;\r\n\t\tif ( _panel is not null )\r\n\t\t\t_panel.Car = _active; // rebinds the lab and re-snapshots stock values for the new car\r\n\t}\r\n\r\n\t/// <summary>Stand up the demo-layer live tuning lab: a ScreenPanel-hosted <see cref=\"DemoTuningPanel\"/>\r\n\t/// bound to the active car, and the camera's cursor-yield seam pointed at its open state. This exists\r\n\t/// only in the demo scene; consumers that spawn their own cars never get it. It doubles as a working\r\n\t/// example of the <see cref=\"VehicleCamera.CursorModalOpen\"/> seam being consumed from the demo side.</summary>\r\n\tvoid MountTuningLab()\r\n\t{\r\n\t\tif ( _active is null )\r\n\t\t\treturn;\r\n\r\n\t\t// Start collapsed to the chip legend: an open lab on spawn captured the cursor before\r\n\t\t// players ever drove (owner call, 2026-07-19). The chip keeps T discoverable; reset the\r\n\t\t// static flag here each session.\r\n\t\tDemoTuningPanel.IsOpen = false;\r\n\r\n\t\t_hud = Scene.CreateObject();\r\n\t\t_hud.Name = \"Tuning HUD\";\r\n\t\t_hud.Components.GetOrCreate<ScreenPanel>();\r\n\r\n\t\t_panel = _hud.Components.Create<DemoTuningPanel>();\r\n\t\t_panel.Car = _active;\r\n\r\n\t\t// Dogfood the kit's cursor-yield seam: while the lab is open the chase camera must release the\r\n\t\t// cursor so the player can click dials. This is exactly what the seam is for; wiring it here\r\n\t\t// proves it to consumers.\r\n\t\tVehicleCamera.CursorModalOpen = () => DemoTuningPanel.IsOpen;\r\n\t}\r\n\r\n\tprotected override void OnFixedUpdate()\r\n\t{\r\n\t\tfloat m = Units.MetersToUnits;\r\n\r\n\t\tfor ( int i = 0; i < _cars.Count; i++ )\r\n\t\t{\r\n\t\t\tvar car = _cars[i];\r\n\t\t\tif ( car is null || !car.IsValid() )\r\n\t\t\t\tcontinue;\r\n\r\n\t\t\t// Only the camera's car listens to the player; parked cars hold the handbrake.\r\n\t\t\t// (With no override every controller samples the same keyboard, so one W press\r\n\t\t\t// would launch the whole row at once; without the handbrake a parked car is\r\n\t\t\t// free-rolling and a device blip or spawn settle can walk it off its mark.)\r\n\t\t\tcar.InputOverride = car == _active ? null : ParkedInputs;\r\n\r\n\t\t\t// Void watchdog: driving off the pad edge otherwise means falling forever.\r\n\t\t\tif ( car.GameObject.WorldPosition.z < VoidResetHeightM * m )\r\n\t\t\t{\r\n\t\t\t\tcar.GameObject.WorldPosition = _spawns[i];\r\n\t\t\t\tcar.GameObject.WorldRotation = Rotation.Identity;\r\n\t\t\t\tvar body = car.GameObject.Components.Get<Rigidbody>();\r\n\t\t\t\tif ( body is not null )\r\n\t\t\t\t{\r\n\t\t\t\t\tbody.Velocity = Vector3.Zero;\r\n\t\t\t\t\tbody.AngularVelocity = Vector3.Zero;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Demo/DemoTuningPanel.razor",
            "FileName": "DemoTuningPanel.razor",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "@namespace FieldGuide.VehiclePhysics\r\n@inherits PanelComponent\r\n\r\n@* Kit-native live tuning lab (demo layer). Toggled by the Tune action, bound to the car the chase\r\n   camera is following. Writes tuning changes straight onto the running car through the same paths a\r\n   consumer would use: it mutates the active CarDefinition (read live by the drivetrain and the brake\r\n   model) and pushes suspension/tire values onto the live wheels. This is a demo-scale lab, not the\r\n   full game panel: it exposes the highest-feel dials so the demo reads as a physics lab in the first\r\n   minute. Replace it with your own UI; it lives only in the demo scene. *@\r\n\r\n<root>\r\n\t@if ( Car.IsValid() )\r\n\t{\r\n\t\t@* Key legend, top-right: how to hop between the demo cars. Always visible; the tuning\r\n\t\t   chip/panel top-left covers the T binding. *@\r\n\t\t<div class=\"legend\">\r\n\t\t\t<div class=\"mono kbd\">[ ]</div>\r\n\t\t\t<div class=\"title\">switch car</div>\r\n\t\t</div>\r\n\t}\r\n\t@if ( !IsOpen && Car.IsValid() )\r\n\t{\r\n\t\t@* Collapsed legend chip: sits exactly where the expanded panel's top-left corner lands, so\r\n\t\t   pressing T reads as the chip expanding into the lab. Mouse-look stays with the camera. *@\r\n\t\t<div class=\"panel chip\" onclick=@ToggleOpen>\r\n\t\t\t<div class=\"head\">\r\n\t\t\t\t<div class=\"mono kbd\">@ToggleKeyLabel</div>\r\n\t\t\t\t<div class=\"title\">tuning</div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t}\r\n\t@if ( IsOpen && Car.IsValid() )\r\n\t{\r\n\t\t<div class=\"panel\">\r\n\t\t\t<div class=\"head\">\r\n\t\t\t\t<div class=\"title\">Tuning lab \u00b7 @CarName</div>\r\n\t\t\t\t<div class=\"mono kbd\">@ToggleKeyLabel</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"hint\">Tuning the car you are driving. Changes apply live.</div>\r\n\r\n\t\t\t<div class=\"dials\">\r\n\t\t\t\t@foreach ( var d in SliderDials )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar dd = d;\r\n\t\t\t\t\t<div class=\"dial\">\r\n\t\t\t\t\t\t<div class=\"drow\">\r\n\t\t\t\t\t\t\t<div class=\"dname\">@d.Name</div>\r\n\t\t\t\t\t\t\t<div class=\"mono dval\">@d.Display()</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t<div class=\"dctl\">\r\n\t\t\t\t\t\t\t<div class=\"mono step\" onclick=@( () => Step( dd, -1 ) )>-</div>\r\n\t\t\t\t\t\t\t<div class=\"track\"\r\n\t\t\t\t\t\t\t\tonmousedown=@( e => Scrub( dd, e, true ) )\r\n\t\t\t\t\t\t\t\tonmousemove=@( e => Scrub( dd, e, false ) )>\r\n\t\t\t\t\t\t\t\t<div class=\"fill\" style=\"width: @WidthPct( dd )%\"></div>\r\n\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"mono step\" onclick=@( () => Step( dd, +1 ) )>+</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t}\r\n\r\n\t\t\t\t<div class=\"dial\">\r\n\t\t\t\t\t<div class=\"drow\">\r\n\t\t\t\t\t\t<div class=\"dname\">Assists</div>\r\n\t\t\t\t\t\t<div class=\"mono cycle\" onclick=@CycleAssists>@AssistLabel</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\r\n\t\t\t\t<div class=\"dial\">\r\n\t\t\t\t\t<div class=\"drow\">\r\n\t\t\t\t\t\t<div class=\"dname\">Tires</div>\r\n\t\t\t\t\t\t<div class=\"mono cycle\" onclick=@CycleTires>@TireLabel</div>\r\n\t\t\t\t\t</div>\r\n\t\t\t\t</div>\r\n\t\t\t</div>\r\n\r\n\t\t\t<div class=\"foot\">\r\n\t\t\t\t<div class=\"btn\" onclick=@ResetToStock>Reset to stock</div>\r\n\t\t\t</div>\r\n\t\t</div>\r\n\t}\r\n</root>\r\n\r\n@code {\r\n\t/// <summary>The car this panel tunes: the one the chase camera follows. Set by DemoBootstrap. On\r\n\t/// change the panel snapshots the car's pristine (authored) values so Reset-to-stock can restore\r\n\t/// them and the grip/torque multipliers rebase to the new car.</summary>\r\n\tVehicleController _car;\r\n\tpublic VehicleController Car\r\n\t{\r\n\t\tget => _car;\r\n\t\tset\r\n\t\t{\r\n\t\t\tif ( ReferenceEquals( _car, value ) )\r\n\t\t\t\treturn;\r\n\t\t\t_car = value;\r\n\t\t\tSnapshot();\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Open state. Static so the kit's chase camera can read it through the\r\n\t/// <see cref=\"VehicleCamera.CursorModalOpen\"/> seam (DemoBootstrap wires that) without holding a\r\n\t/// reference to this panel. The demo runs exactly one panel, so a single flag is enough.</summary>\r\n\tpublic static bool IsOpen;\r\n\r\n\t// Toggle input action. Documented in the README input table; the host ProjectSettings/Input.config\r\n\t// ships it. Never Escape or an F-key.\r\n\tconst string ToggleAction = \"Tune\";\r\n\tstring ToggleKeyLabel => \"T\";\r\n\r\n\tstring CarName => Car?.Definition?.Name ?? \"Car\";\r\n\r\n\t// Live multipliers over the pristine base. Grip scales the (preset-selected) tire curves; torque\r\n\t// scales the authored peak engine torque.\r\n\tfloat _gripScale = 1f;\r\n\tfloat _torqueScale = 1f;\r\n\tint _tirePreset; // 0 Stock, 1 Street, 2 Sport, 3 Offroad\r\n\r\n\t// Pristine snapshot, captured value-by-value when the car is bound (its definition is untouched at\r\n\t// that point). Value types only (floats, TireCurve struct, enum), so later live tuning of the\r\n\t// definition can never corrupt these; Reset restores from here.\r\n\tbool _snapped;\r\n\tfloat _stockPeakTorque, _stockSpring, _stockDamper, _stockTravel, _stockBrake;\r\n\tTireCurve _stockLat, _stockLong;\r\n\tAssistLevel _stockAssists;\r\n\r\n\t// Current UNSCALED tire base (Stock or a named preset). Grip multiplies these to get the live curves.\r\n\tTireCurve _baseLat, _baseLong;\r\n\r\n\tvoid Snapshot()\r\n\t{\r\n\t\tvar def = _car?.Definition;\r\n\t\t_dials = null;\r\n\t\tif ( def is null )\r\n\t\t{\r\n\t\t\t_snapped = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_stockPeakTorque = def.PeakTorque;\r\n\t\t_stockSpring = def.SpringRate;\r\n\t\t_stockDamper = def.DamperRate;\r\n\t\t_stockTravel = def.SuspensionTravel;\r\n\t\t_stockBrake = def.BrakeTorque;\r\n\t\t_stockLat = def.LateralCurve;\r\n\t\t_stockLong = def.LongitudinalCurve;\r\n\t\t// Stock assist = the authored definition default. Read it off the definition, not the\r\n\t\t// controller: the controller adopts DefaultAssists in its own OnStart, which may run a frame\r\n\t\t// or two after this bind, so its live Assists is not reliable yet.\r\n\t\t_stockAssists = def.DefaultAssists;\r\n\r\n\t\t_baseLat = _stockLat;\r\n\t\t_baseLong = _stockLong;\r\n\t\t_gripScale = 1f;\r\n\t\t_torqueScale = 1f;\r\n\t\t_tirePreset = 0;\r\n\t\t_snapped = true;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\t// ---- dial model ----\r\n\tclass Dial\r\n\t{\r\n\t\tpublic string Name;\r\n\t\tpublic float Min, Max, Step;\r\n\t\tpublic Func<float> Get;\r\n\t\tpublic Action<float> Set;\r\n\t\tpublic Func<string> Fmt;\r\n\t\tpublic string Display() => Fmt();\r\n\t}\r\n\r\n\tList<Dial> _dials;\r\n\tList<Dial> SliderDials => _dials ??= BuildDials();\r\n\r\n\tList<Dial> BuildDials()\r\n\t{\r\n\t\tif ( !Car.IsValid() )\r\n\t\t\treturn new List<Dial>();\r\n\r\n\t\tvar def = Car.Definition;\r\n\t\treturn new List<Dial>\r\n\t\t{\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Grip\", Min = 0.6f, Max = 2.2f, Step = 0.05f,\r\n\t\t\t\tGet = () => _gripScale, Set = SetGrip,\r\n\t\t\t\tFmt = () => _gripScale.ToString( \"0.00\" ) + \"x\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Drive torque\", Min = 0.5f, Max = 2.0f, Step = 0.05f,\r\n\t\t\t\tGet = () => _torqueScale, Set = SetTorqueScale,\r\n\t\t\t\tFmt = () => _torqueScale.ToString( \"0.00\" ) + \"x\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Suspension stiffness\", Min = 15000f, Max = 60000f, Step = 2000f,\r\n\t\t\t\tGet = () => def.SpringRate, Set = v => { def.SpringRate = v; ApplyWheels(); },\r\n\t\t\t\tFmt = () => def.SpringRate.ToString( \"0\" ) + \" N/m\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Suspension damping\", Min = 800f, Max = 6000f, Step = 200f,\r\n\t\t\t\tGet = () => def.DamperRate, Set = v => { def.DamperRate = v; ApplyWheels(); },\r\n\t\t\t\tFmt = () => def.DamperRate.ToString( \"0\" ) + \" Ns/m\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Suspension travel\", Min = 0.10f, Max = 0.35f, Step = 0.01f,\r\n\t\t\t\tGet = () => def.SuspensionTravel, Set = v => { def.SuspensionTravel = v; ApplyWheels(); },\r\n\t\t\t\tFmt = () => (def.SuspensionTravel * 100f).ToString( \"0\" ) + \" cm\",\r\n\t\t\t},\r\n\t\t\tnew()\r\n\t\t\t{\r\n\t\t\t\tName = \"Brake force\", Min = 1500f, Max = 8000f, Step = 200f,\r\n\t\t\t\tGet = () => def.BrakeTorque, Set = v => def.BrakeTorque = v,\r\n\t\t\t\tFmt = () => def.BrakeTorque.ToString( \"0\" ) + \" Nm\",\r\n\t\t\t},\r\n\t\t};\r\n\t}\r\n\r\n\tfloat Frac( Dial d )\r\n\t{\r\n\t\tif ( d.Max <= d.Min )\r\n\t\t\treturn 0f;\r\n\t\treturn Math.Clamp( (d.Get() - d.Min) / (d.Max - d.Min), 0f, 1f );\r\n\t}\r\n\r\n\tstring WidthPct( Dial d ) =>\r\n\t\t(Frac( d ) * 100f).ToString( \"0.#\", System.Globalization.CultureInfo.InvariantCulture );\r\n\r\n\tvoid Step( Dial d, int clicks )\r\n\t{\r\n\t\tfloat v = Math.Clamp( d.Get() + clicks * d.Step, d.Min, d.Max );\r\n\t\td.Set( v );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\t// Click-to-jump + drag-to-scrub on the dial track (mirrors the game panel's proven pattern). jump\r\n\t// is true on mousedown (a bare click sets the value at the cursor) and false on mousemove (scrub\r\n\t// only while the track owns the press). Value = mouse local x over track width, snapped to the\r\n\t// dial's step, pushed through the SAME Set path as the +/- steps.\r\n\tvoid Scrub( Dial d, Sandbox.UI.PanelEvent ev, bool jump )\r\n\t{\r\n\t\tif ( ev is not Sandbox.UI.MousePanelEvent e )\r\n\t\t\treturn;\r\n\r\n\t\tvar track = e.This;\r\n\t\tif ( track is null )\r\n\t\t\treturn;\r\n\r\n\t\t// mousemove fires whether or not the button is held; only scrub while the track owns the press.\r\n\t\tif ( !jump && !track.PseudoClass.HasFlag( Sandbox.UI.PseudoClass.Active ) )\r\n\t\t\treturn;\r\n\r\n\t\tfloat w = track.Box.Rect.Width;\r\n\t\tif ( w <= 0f )\r\n\t\t\treturn;\r\n\r\n\t\tfloat frac = Math.Clamp( e.LocalPosition.x / w, 0f, 1f );\r\n\t\tfloat v = d.Min + frac * (d.Max - d.Min);\r\n\t\tif ( d.Step > 0f )\r\n\t\t\tv = MathF.Round( v / d.Step ) * d.Step;\r\n\t\tv = Math.Clamp( v, d.Min, d.Max );\r\n\t\td.Set( v );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\t// ---- apply paths (same seams a consumer would use) ----\r\n\tstatic TireCurve Scaled( TireCurve c, float k ) =>\r\n\t\tnew( c.PeakSlip, c.PeakGrip * k, c.TailSlip, c.TailGrip * k );\r\n\r\n\t// Push the definition's suspension + tire values onto the live wheels. The factory copies these at\r\n\t// spawn; the wheel re-reads them every substep, so writing them here is the live-apply path.\r\n\tvoid ApplyWheels()\r\n\t{\r\n\t\tvar def = Car.Definition;\r\n\t\tforeach ( var w in Car.Wheels )\r\n\t\t{\r\n\t\t\tw.SpringRate = def.SpringRate;\r\n\t\t\tw.DamperRate = def.DamperRate;\r\n\t\t\tw.SuspensionTravel = def.SuspensionTravel;\r\n\t\t\tw.LateralCurve = def.LateralCurve;\r\n\t\t\tw.LongitudinalCurve = def.LongitudinalCurve;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid SetGrip( float k )\r\n\t{\r\n\t\t_gripScale = k;\r\n\t\tvar def = Car.Definition;\r\n\t\tdef.LateralCurve = Scaled( _baseLat, k );\r\n\t\tdef.LongitudinalCurve = Scaled( _baseLong, k );\r\n\t\tApplyWheels();\r\n\t}\r\n\r\n\t// Drive torque scale multiplies the authored peak torque. Drivetrain reads def.PeakTorque live\r\n\t// (it holds the same definition instance), so no drivetrain touch is needed.\r\n\tvoid SetTorqueScale( float k )\r\n\t{\r\n\t\t_torqueScale = k;\r\n\t\tCar.Definition.PeakTorque = _stockPeakTorque * k;\r\n\t}\r\n\r\n\tvoid CycleAssists()\r\n\t{\r\n\t\tCar.Assists = Car.Assists switch\r\n\t\t{\r\n\t\t\tAssistLevel.Casual => AssistLevel.Sport,\r\n\t\t\tAssistLevel.Sport => AssistLevel.Sim,\r\n\t\t\t_ => AssistLevel.Casual,\r\n\t\t};\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tstring AssistLabel => Car.IsValid() ? Car.Assists.ToString() : \"\";\r\n\r\n\t// Tire preset swaps the UNSCALED base curves, then re-applies the current grip multiplier so the\r\n\t// grip dial and the preset compose. Stock restores the car's own authored curves.\r\n\tvoid CycleTires()\r\n\t{\r\n\t\t_tirePreset = (_tirePreset + 1) % 4;\r\n\t\t(_baseLat, _baseLong) = _tirePreset switch\r\n\t\t{\r\n\t\t\t1 => (TireCurve.Street, TireCurve.Street),\r\n\t\t\t2 => (TireCurve.Sport, TireCurve.Sport),\r\n\t\t\t3 => (TireCurve.Offroad, TireCurve.Offroad),\r\n\t\t\t_ => (_stockLat, _stockLong),\r\n\t\t};\r\n\t\tSetGrip( _gripScale );\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tstring TireLabel => _tirePreset switch\r\n\t{\r\n\t\t1 => \"Street\",\r\n\t\t2 => \"Sport\",\r\n\t\t3 => \"Offroad\",\r\n\t\t_ => \"Stock\",\r\n\t};\r\n\r\n\t// Re-apply the car's pristine authored values (captured at bind). Definitions in the demo roster\r\n\t// are fresh per car, so \"stock\" is unambiguous: the values this car spawned with.\r\n\tvoid ResetToStock()\r\n\t{\r\n\t\tif ( !Car.IsValid() || !_snapped )\r\n\t\t\treturn;\r\n\r\n\t\tvar def = Car.Definition;\r\n\t\tdef.PeakTorque = _stockPeakTorque;\r\n\t\tdef.SpringRate = _stockSpring;\r\n\t\tdef.DamperRate = _stockDamper;\r\n\t\tdef.SuspensionTravel = _stockTravel;\r\n\t\tdef.BrakeTorque = _stockBrake;\r\n\t\tdef.LateralCurve = _stockLat;\r\n\t\tdef.LongitudinalCurve = _stockLong;\r\n\r\n\t\t_gripScale = 1f;\r\n\t\t_torqueScale = 1f;\r\n\t\t_tirePreset = 0;\r\n\t\t_baseLat = _stockLat;\r\n\t\t_baseLong = _stockLong;\r\n\r\n\t\tCar.Assists = _stockAssists;\r\n\t\tApplyWheels();\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t// Start COLLAPSED: the chip legend keeps the keybind discoverable while mouse-look stays\r\n\t\t// with the camera (an open lab on spawn captured the cursor before players ever drove,\r\n\t\t// owner call 2026-07-19). Static flag, so reset it here each session.\r\n\t\tIsOpen = false;\r\n\t}\r\n\r\n\tvoid ToggleOpen()\r\n\t{\r\n\t\tIsOpen = !IsOpen;\r\n\t\tMouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Input.Pressed( ToggleAction ) )\r\n\t\t{\r\n\t\t\tIsOpen = !IsOpen;\r\n\t\t\tMouse.Visibility = IsOpen ? MouseVisibility.Visible : MouseVisibility.Hidden;\r\n\t\t\tStateHasChanged();\r\n\t\t}\r\n\r\n\t\t// The chase camera re-hides the cursor every frame while it owns it; hold it visible while open.\r\n\t\tif ( IsOpen )\r\n\t\t\tMouse.Visibility = MouseVisibility.Visible;\r\n\t}\r\n\r\n\tprotected override int BuildHash()\r\n\t{\r\n\t\t// Closed still renders the chip legend, and the chip waits on the car binding, so the\r\n\t\t// hash must move when the car arrives or the first build would stick on the empty tree.\r\n\t\tif ( !IsOpen )\r\n\t\t\treturn Car.IsValid() ? 1 : 0;\r\n\r\n\t\tvar h = new HashCode();\r\n\t\th.Add( _gripScale );\r\n\t\th.Add( _torqueScale );\r\n\t\th.Add( _tirePreset );\r\n\t\tif ( Car.IsValid() )\r\n\t\t{\r\n\t\t\tvar def = Car.Definition;\r\n\t\t\th.Add( def.SpringRate );\r\n\t\t\th.Add( def.DamperRate );\r\n\t\t\th.Add( def.SuspensionTravel );\r\n\t\t\th.Add( def.BrakeTorque );\r\n\t\t\th.Add( (int)Car.Assists );\r\n\t\t}\r\n\t\treturn h.ToHashCode();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/Demo/DemoTuningPanel.razor.scss",
            "FileName": "DemoTuningPanel.razor.scss",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "// Kit-native live tuning lab (demo layer). Compact left-anchored card; s&box panels default to flex\r\n// so no display rules are needed (which also keeps this free of inline-flex / inline-block). All text\r\n// is bright: instructional copy never uses dim gray.\r\n\r\nDemoTuningPanel {\r\n\tposition: absolute;\r\n\tinset: 0;\r\n\tpointer-events: none;\r\n\tfont-family: Poppins, sans-serif;\r\n\r\n\t.mono { font-family: \"Roboto Mono\", monospace; }\r\n\r\n\t// Top-right key legend (car switching). Mirrors the chip's card language at the opposite corner.\r\n\t.legend {\r\n\t\tposition: absolute;\r\n\t\tright: 40px;\r\n\t\ttop: 40px;\r\n\t\tflex-direction: row;\r\n\t\talign-items: center;\r\n\t\tgap: 8px;\r\n\t\tpadding: 10px 14px;\r\n\t\tbackground-color: rgba( 15, 17, 21, 0.92 );\r\n\t\tborder: 1px solid rgba( 255, 255, 255, 0.08 );\r\n\t\tborder-radius: 16px;\r\n\t\tbox-shadow: 0 20px 60px rgba( 0, 0, 0, 0.6 );\r\n\r\n\t\t.title { font-weight: 600; color: #F2F4F7; }\r\n\t\t.kbd {\r\n\t\t\tcolor: #F2F4F7;\r\n\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.12 );\r\n\t\t\tborder-radius: 5px;\r\n\t\t\tpadding: 3px 8px;\r\n\t\t\tbackground-color: rgba( 255, 255, 255, 0.05 );\r\n\t\t}\r\n\t}\r\n\r\n\t.panel {\r\n\t\tposition: absolute;\r\n\t\tleft: 40px;\r\n\t\ttop: 40px;\r\n\t\twidth: 360px;\r\n\t\tflex-direction: column;\r\n\t\tgap: 12px;\r\n\t\tpadding: 20px;\r\n\t\tbackground-color: rgba( 15, 17, 21, 0.92 );\r\n\t\tborder: 1px solid rgba( 255, 255, 255, 0.08 );\r\n\t\tborder-radius: 16px;\r\n\t\tbox-shadow: 0 20px 60px rgba( 0, 0, 0, 0.6 );\r\n\t\tpointer-events: all;\r\n\r\n\t\t.head {\r\n\t\t\tflex-direction: row;\r\n\t\t\tjustify-content: space-between;\r\n\t\t\talign-items: center;\r\n\r\n\t\t\t.title { font-weight: 700; color: #F2F4F7; }\r\n\t\t\t.kbd {\r\n\t\t\t\tcolor: #F2F4F7;\r\n\t\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.12 );\r\n\t\t\t\tborder-radius: 5px;\r\n\t\t\t\tpadding: 3px 8px;\r\n\t\t\t\tbackground-color: rgba( 255, 255, 255, 0.05 );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.hint { color: #F2F4F7; }\r\n\r\n\t\t// Collapsed legend chip: same anchor as the expanded card (left/top 40px via .panel), sized\r\n\t\t// to its content so it reads as the card's top-left corner waiting to grow.\r\n\t\t&.chip {\r\n\t\t\twidth: auto;\r\n\t\t\tpadding: 10px 14px;\r\n\t\t\tgap: 0;\r\n\r\n\t\t\t.head {\r\n\t\t\t\tgap: 8px;\r\n\t\t\t\tjustify-content: flex-start;\r\n\r\n\t\t\t\t.title { font-weight: 600; color: #F2F4F7; }\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.dials {\r\n\t\t\tflex-direction: column;\r\n\t\t\tgap: 12px;\r\n\r\n\t\t\t.dial { flex-direction: column; gap: 6px; }\r\n\r\n\t\t\t.drow {\r\n\t\t\t\tflex-direction: row;\r\n\t\t\t\tjustify-content: space-between;\r\n\t\t\t\talign-items: center;\r\n\r\n\t\t\t\t.dname { color: #F2F4F7; }\r\n\t\t\t\t.dval { color: #E8EAED; }\r\n\t\t\t}\r\n\r\n\t\t\t.dctl {\r\n\t\t\t\tflex-direction: row;\r\n\t\t\t\talign-items: center;\r\n\t\t\t\tgap: 10px;\r\n\r\n\t\t\t\t.step {\r\n\t\t\t\t\twidth: 28px;\r\n\t\t\t\t\theight: 28px;\r\n\t\t\t\t\tjustify-content: center;\r\n\t\t\t\t\talign-items: center;\r\n\t\t\t\t\tcolor: #F2F4F7;\r\n\t\t\t\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\r\n\t\t\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.10 );\r\n\t\t\t\t\tborder-radius: 6px;\r\n\t\t\t\t\tcursor: pointer;\r\n\t\t\t\t\ttransition: all 0.12s ease;\r\n\r\n\t\t\t\t\t&:hover { color: #0A161A; background-color: #4AD9F2; }\r\n\t\t\t\t}\r\n\r\n\t\t\t\t.track {\r\n\t\t\t\t\t// Click-to-jump + drag-to-scrub hit box. Events must land on the track (not the fill)\r\n\t\t\t\t\t// so the mouse local position stays track-relative: pointer-events all here, none on\r\n\t\t\t\t\t// the fill.\r\n\t\t\t\t\tposition: relative;\r\n\t\t\t\t\tflex-grow: 1;\r\n\t\t\t\t\theight: 14px;\r\n\t\t\t\t\tborder-radius: 99px;\r\n\t\t\t\t\tbackground-color: rgba( 255, 255, 255, 0.12 );\r\n\t\t\t\t\tpointer-events: all;\r\n\t\t\t\t\tcursor: pointer;\r\n\r\n\t\t\t\t\t.fill {\r\n\t\t\t\t\t\tposition: absolute;\r\n\t\t\t\t\t\tleft: 0px;\r\n\t\t\t\t\t\theight: 100%;\r\n\t\t\t\t\t\tborder-radius: 99px;\r\n\t\t\t\t\t\tbackground: linear-gradient( to right, #3AC4DE, #4AD9F2 );\r\n\t\t\t\t\t\tpointer-events: none;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t.cycle {\r\n\t\t\t\tjustify-content: center;\r\n\t\t\t\talign-items: center;\r\n\t\t\t\tfont-weight: 600;\r\n\t\t\t\tcolor: #0A161A;\r\n\t\t\t\tbackground-color: #4AD9F2;\r\n\t\t\t\tborder-radius: 6px;\r\n\t\t\t\tpadding: 5px 14px;\r\n\t\t\t\tcursor: pointer;\r\n\t\t\t\ttransition: all 0.12s ease;\r\n\r\n\t\t\t\t&:hover { background-color: #58E1F7; }\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.foot {\r\n\t\t\tflex-direction: column;\r\n\t\t\tgap: 8px;\r\n\r\n\t\t\t.btn {\r\n\t\t\t\tjustify-content: center;\r\n\t\t\t\talign-items: center;\r\n\t\t\t\tfont-weight: 700;\r\n\t\t\t\tcolor: #F2F4F7;\r\n\t\t\t\tbackground-color: rgba( 255, 255, 255, 0.06 );\r\n\t\t\t\tborder: 1px solid rgba( 255, 255, 255, 0.10 );\r\n\t\t\t\tborder-radius: 10px;\r\n\t\t\t\tpadding: 11px 0;\r\n\t\t\t\tcursor: pointer;\r\n\t\t\t\ttransition: all 0.12s ease;\r\n\r\n\t\t\t\t&:hover { color: #E8EAED; background-color: rgba( 255, 255, 255, 0.10 ); }\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/DriveInputs.cs",
            "FileName": "DriveInputs.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Per-frame driver intent, the value-struct swap point at VehicleController's input seam\r\n/// (<see cref=\"VehicleController.InputOverride\"/>). Carries the raw intent the controller samples\r\n/// from a device: the move axis (forward/back + steer), the handbrake action, and the sequential\r\n/// shift requests. A keyboard/gamepad/wheel source or a scripted source (a test pilot, an AI) all\r\n/// produce one of these, so the controller's gear/reverse/steer-ramp logic stays source-agnostic.\r\n///\r\n/// The live-device sampler <see cref=\"SampleDeviceInputs\"/> and its gamepad shaping helpers live\r\n/// here (lifted out of the controller): the controller either consumes an injected override or calls\r\n/// this sampler, and nothing else in the class touches device input.\r\n/// </summary>\r\npublic struct DriveInputs\r\n{\r\n\t/// <summary>-1..1 signed drive axis: +forward accelerates, -back brakes then engages reverse near a\r\n\t/// stop. Built in <see cref=\"SampleDeviceInputs\"/> as (throttle \u2212 brake) where each channel is the\r\n\t/// MAX of the keyboard/stick component (<c>Input.AnalogMove.x</c>) and the ANALOG gamepad trigger\r\n\t/// pull (<c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c>, 0..1) \u2014 so a partial trigger\r\n\t/// gives a partial pedal. A scripted source sets this float directly.</summary>\r\n\tpublic float MoveForward;\r\n\r\n\t/// <summary>-1..1, maps to <c>Input.AnalogMove.y</c> (gamepad path reshaped by the gamepad deadzone\r\n\t/// + response curve). Note <c>Rotation.FromYaw(+)</c> is a LEFT/CCW turn, so +Steer steers left.</summary>\r\n\tpublic float Steer;\r\n\r\n\t/// <summary>The handbrake / drift button: keyboard \"Jump\" (Space), or gamepad A (Jump's own\r\n\t/// GamepadCode) or the left bumper (\"Handbrake\" action).</summary>\r\n\tpublic bool Handbrake;\r\n\r\n\t/// <summary>Edge-triggered request to shift UP one gear (sequential MANUAL mode). Keyboard E /\r\n\t/// gamepad R1 while live; a scripted source pulses it for one tick. The controller rising-edge-\r\n\t/// detects it, so a source that holds it across ticks still shifts exactly once.</summary>\r\n\tpublic bool ShiftUp;\r\n\r\n\t/// <summary>Edge-triggered request to shift DOWN one gear (sequential MANUAL mode). Keyboard Q /\r\n\t/// gamepad L1. Same one-shot rising-edge semantics as <see cref=\"ShiftUp\"/>.</summary>\r\n\tpublic bool ShiftDown;\r\n\r\n\t/// <summary>Edge-triggered request to toggle the transmission mode AUTO\u2194MANUAL. Keyboard G /\r\n\t/// gamepad D-pad down. Same one-shot rising-edge semantics as <see cref=\"ShiftUp\"/>.</summary>\r\n\tpublic bool ShiftModeToggle;\r\n\r\n\t// Gamepad tier: deadzone + response curve for the analog steer axis.\r\n\tconst float GamepadSteerDeadzone = 0.12f;\r\n\tconst float GamepadSteerCurvePower = 1.6f; // >1 softens the center for fine control, still reaches full lock\r\n\r\n\t// Analog throttle/brake tier: a small trigger deadzone so resting triggers can't creep the pedals.\r\n\t// The engine ALSO applies its own 12.5% deadzone one layer down (Controller.SetAxis zeroes any\r\n\t// |axis| <= 0.125 before Input.GetAnalog sees it), so values arrive as 0 or >~0.125 and this floor\r\n\t// is a belt-and-suspenders guard that also rescales so full pull still reaches 1.0.\r\n\tconst float GamepadTriggerDeadzone = 0.05f;\r\n\r\n\t/// <summary>Sample the live input devices into a DriveInputs value (this is the keyboard/gamepad\r\n\t/// source; other sources produce the same struct and set <see cref=\"VehicleController.InputOverride\"/>).\r\n\t///\r\n\t/// Steering rides <c>Input.AnalogMove.y</c> straight off the left stick; keyboard emits exact\r\n\t/// -1/0/1 through the same path and passes through <see cref=\"ApplyGamepadSteerCurve\"/> unchanged.\r\n\t///\r\n\t/// Throttle/brake are VARIABLE per device: keyboard W/S ride <c>Input.AnalogMove.x</c> as an exact\r\n\t/// \u00b11 digital forward/back; the gamepad triggers are read as a true ANALOG 0..1 pull via\r\n\t/// <c>Input.GetAnalog(InputAnalog.RightTrigger|LeftTrigger)</c> (right = gas, left = brake). The two\r\n\t/// devices combine per channel by MAX \u2014 so either device works and neither fights the other \u2014 then\r\n\t/// the net (throttle \u2212 brake) folds into the single signed <see cref=\"MoveForward\"/> scalar. On\r\n\t/// keyboard <c>Input.GetAnalog</c> returns 0, so keyboard-only players are byte-identical.</summary>\r\n\tpublic static DriveInputs SampleDeviceInputs()\r\n\t{\r\n\t\tvar move = Input.AnalogMove;\r\n\r\n\t\t// keyboard/stick forward+back split off the shared move axis (W = +x, S = -x)\r\n\t\tfloat keyThrottle = MathF.Max( 0f, move.x );\r\n\t\tfloat keyBrake = MathF.Max( 0f, -move.x );\r\n\r\n\t\t// gamepad triggers, true analog 0..1 (right = gas, left = brake)\r\n\t\tfloat triggerThrottle = ReadTrigger( InputAnalog.RightTrigger );\r\n\t\tfloat triggerBrake = ReadTrigger( InputAnalog.LeftTrigger );\r\n\r\n\t\t// MAX blend per channel so either device drives the pedal, neither overrides the other\r\n\t\tfloat throttle = MathF.Max( keyThrottle, triggerThrottle );\r\n\t\tfloat brake = MathF.Max( keyBrake, triggerBrake );\r\n\t\tfloat moveForward = Math.Clamp( throttle - brake, -1f, 1f );\r\n\r\n\t\treturn new DriveInputs\r\n\t\t{\r\n\t\t\tMoveForward = moveForward,\r\n\t\t\tSteer = ApplyGamepadSteerCurve( move.y ),\r\n\t\t\tHandbrake = Input.Down( \"Jump\" ) || Input.Down( \"Handbrake\" ),\r\n\t\t};\r\n\t}\r\n\r\n\t/// <summary>Read a gamepad trigger as a linear 0..1 pull with a small deadzone floor (rescaled so\r\n\t/// full pull still reaches 1.0). <c>Input.GetAnalog</c> already returns 0 for a trigger on keyboard,\r\n\t/// so this only shapes the gamepad path.</summary>\r\n\tstatic float ReadTrigger( InputAnalog trigger )\r\n\t{\r\n\t\tfloat v = Math.Clamp( Input.GetAnalog( trigger ), 0f, 1f );\r\n\t\tif ( v < GamepadTriggerDeadzone )\r\n\t\t\treturn 0f;\r\n\t\treturn (v - GamepadTriggerDeadzone) / (1f - GamepadTriggerDeadzone);\r\n\t}\r\n\r\n\t/// <summary>Deadzone + power curve for the analog steer axis. Values under the deadzone snap to 0;\r\n\t/// the remaining range is rescaled so full stick deflection still reaches \u00b11 (no lost lock), then\r\n\t/// raised to <see cref=\"GamepadSteerCurvePower\"/> for a softer center. Keyboard's exact -1/0/1\r\n\t/// passes through unaffected (0 is inside the deadzone; 1 rescales to 1 and 1^n == 1).</summary>\r\n\tstatic float ApplyGamepadSteerCurve( float raw )\r\n\t{\r\n\t\tfloat mag = MathF.Abs( raw );\r\n\t\tif ( mag < GamepadSteerDeadzone )\r\n\t\t\treturn 0f;\r\n\r\n\t\tfloat t = Math.Clamp( (mag - GamepadSteerDeadzone) / (1f - GamepadSteerDeadzone), 0f, 1f );\r\n\t\treturn MathF.Sign( raw ) * MathF.Pow( t, GamepadSteerCurvePower );\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/Units.cs",
            "FileName": "Units.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// All vehicle math is done in SI units (meters, kg, N); convert only at the engine boundary.\r\n/// s&amp;box uses Source-style inch units.\r\n/// </summary>\r\npublic static class Units\r\n{\r\n\tpublic const float MetersToUnits = 39.37f;\r\n\tpublic const float UnitsToMeters = 1f / MetersToUnits;\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/VehicleCamera.cs",
            "FileName": "VehicleCamera.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Chase camera: spring-arm follow behind the car's flattened heading, FOV widens with speed.\r\n/// Mouse orbits around the car; after a few seconds without mouse input it eases back to the chase\r\n/// position. Mouse wheel zooms distance.\r\n/// </summary>\r\npublic sealed class VehicleCamera : Component\r\n{\r\n\t/// <summary>\r\n\t/// Seam for \"a UI cursor modal is currently open\" (spec 3.1). Drive mode needs a locked cursor\r\n\t/// or AnalogLook stays at zero, so this camera hides the cursor every frame \u2014 UNLESS a consumer's\r\n\t/// UI has a modal up that owns the cursor. Plug that check in here; null (default) means the kit\r\n\t/// never yields the cursor (single-window driving with no UI modals). Null-safe: unset reads false.\r\n\t/// </summary>\r\n\tpublic static Func<bool> CursorModalOpen { get; set; }\r\n\r\n\tstatic bool AnyCursorModalOpen\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\treturn CursorModalOpen?.Invoke() ?? false;\r\n\t\t\t}\r\n\t\t\tcatch ( NotImplementedException )\r\n\t\t\t{\r\n\t\t\t\t// Hotload survivor: this static can hold a lambda authored by a REPLACED assembly\r\n\t\t\t\t// (s&box hotload copies static state but cannot substitute an orphaned lambda, so\r\n\t\t\t\t// invoking it throws). Drop the stale delegate and fall back to \"no modal\"; whoever\r\n\t\t\t\t// owns the seam (the demo bootstrap, or a consumer) re-wires it at session start.\r\n\t\t\t\tCursorModalOpen = null;\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tpublic VehicleController Target { get; set; }\r\n\r\n\t[Property] public float Distance { get; set; } = 6.5f; // m\r\n\t[Property] public float Height { get; set; } = 2.2f;   // m\r\n\t[Property] public float BaseFov { get; set; } = 70f;\r\n\t[Property] public float OrbitReturnDelay { get; set; } = 5f;\r\n\t[Property] public float MinDistance { get; set; } = 3.0f;  // m \u2014 wheel zoom in\r\n\t[Property] public float MaxDistance { get; set; } = 14.0f; // m \u2014 wheel zoom out\r\n\t[Property] public float ZoomPerNotch { get; set; } = 0.85f; // m per wheel tick\r\n\r\n\tCameraComponent _camera;\r\n\tfloat _orbitYaw;\r\n\tfloat _orbitPitch;\r\n\tTimeSince _lastMouseInput = 999f;\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\t_camera = Components.Get<CameraComponent>();\r\n\t\t// Drive mode needs a locked cursor or AnalogLook stays at zero. UI modals unlock\r\n\t\t// it; we re-lock every frame when none are open (panels only set Visible while up).\r\n\t\tif ( !AnyCursorModalOpen )\r\n\t\t\tMouse.Visibility = MouseVisibility.Hidden;\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif ( Target is null || !Target.IsValid() )\r\n\t\t\treturn;\r\n\r\n\t\tbool uiOwnsCursor = AnyCursorModalOpen;\r\n\t\tif ( !uiOwnsCursor )\r\n\t\t{\r\n\t\t\t// Re-assert every frame \u2014 panel dismiss paths miss edge cases, and the engine\r\n\t\t\t// can resurface the cursor after focus changes.\r\n\t\t\tMouse.Visibility = MouseVisibility.Hidden;\r\n\r\n\t\t\tvar look = Input.AnalogLook;\r\n\t\t\tif ( MathF.Abs( look.yaw ) > 0.05f || MathF.Abs( look.pitch ) > 0.05f )\r\n\t\t\t{\r\n\t\t\t\t_lastMouseInput = 0;\r\n\t\t\t\t_orbitYaw += look.yaw;\r\n\t\t\t\t_orbitPitch = Math.Clamp( _orbitPitch + look.pitch, -30f, 12f );\r\n\t\t\t}\r\n\r\n\t\t\t// Scroll up = closer (smaller Distance). Ignore while UI has the cursor.\r\n\t\t\tfloat scroll = Input.MouseWheel.y;\r\n\t\t\tif ( MathF.Abs( scroll ) > 0.01f )\r\n\t\t\t\tDistance = Math.Clamp( Distance - scroll * ZoomPerNotch, MinDistance, MaxDistance );\r\n\t\t}\r\n\r\n\t\tif ( !uiOwnsCursor && _lastMouseInput > OrbitReturnDelay )\r\n\t\t{\r\n\t\t\t// ease back behind the car\r\n\t\t\tfloat decay = 1f - MathF.Exp( -3f * Time.Delta );\r\n\t\t\t_orbitYaw = MathX.Lerp( _orbitYaw, 0f, decay );\r\n\t\t\t_orbitPitch = MathX.Lerp( _orbitPitch, 0f, decay );\r\n\t\t}\r\n\r\n\t\tfloat m = Units.MetersToUnits;\r\n\t\tvar car = Target.GameObject;\r\n\r\n\t\t// follow the flattened heading so rolls/flips don't whip the camera\r\n\t\tvar flatForward = car.WorldRotation.Forward.WithZ( 0f );\r\n\t\tflatForward = flatForward.IsNearZeroLength ? Vector3.Forward : flatForward.Normal;\r\n\t\tfloat baseYaw = MathF.Atan2( flatForward.y, flatForward.x ).RadianToDegree();\r\n\r\n\t\tvar orbit = Rotation.From( _orbitPitch, baseYaw + _orbitYaw, 0f );\r\n\r\n\t\tfloat speedStretch = Math.Clamp( Target.SpeedMs / 40f, 0f, 1f );\r\n\t\tvar wantedPos = car.WorldPosition\r\n\t\t\t- orbit.Forward * (Distance + speedStretch * 1.5f) * m\r\n\t\t\t+ Vector3.Up * Height * m;\r\n\r\n\t\tfloat smooth = 1f - MathF.Exp( -8f * Time.Delta );\r\n\t\tWorldPosition = Vector3.Lerp( WorldPosition, wantedPos, smooth );\r\n\r\n\t\tvar lookTarget = car.WorldPosition + Vector3.Up * 0.8f * m + orbit.Forward * 1.5f * m;\r\n\t\tWorldRotation = Rotation.LookAt( (lookTarget - WorldPosition).Normal );\r\n\r\n\t\tif ( _camera.IsValid() )\r\n\t\t\t_camera.FieldOfView = BaseFov + speedStretch * 15f;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "VehicleWheel.cs",
            "FileName": "VehicleWheel.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// One wheel: shapecast ground detection + spring/damper suspension (skeleton pattern from\r\n/// Facepunch/sbox-libwheel, MIT) with the friction model replaced by the spec \u00a75.2.1 slip\r\n/// physics \u2014 wheel angular velocity as integrated state, slip ratio/angle into peaked curves,\r\n/// friction ellipse, load sensitivity, low-speed blend.\r\n///\r\n/// Driven by <see cref=\"VehicleController\"/> in substeps; forces are accumulated across\r\n/// substeps and applied once per fixed update (ApplyForceAt applies for the whole step).\r\n/// All internal math in SI.\r\n/// </summary>\r\npublic sealed class VehicleWheel : Component\r\n{\r\n\t// Configured by VehicleFactory from CarDefinition\r\n\tpublic float Radius { get; set; } = 0.31f;\r\n\tpublic float Inertia { get; set; } = 1.2f;\r\n\tpublic float SuspensionTravel { get; set; } = 0.18f;\r\n\tpublic float SpringRate { get; set; } = 38000f;\r\n\tpublic float DamperRate { get; set; } = 2800f;\r\n\tpublic TireCurve LongitudinalCurve { get; set; }\r\n\tpublic TireCurve LateralCurve { get; set; }\r\n\tpublic float LoadSensitivity { get; set; } = 0.06f;\r\n\tpublic float StaticLoad { get; set; } = 3000f; // N, set by factory: mass\u00b7g/4\r\n\tpublic bool IsSteering { get; set; }\r\n\tpublic bool IsDriven { get; set; }\r\n\tpublic bool HasHandbrake { get; set; }\r\n\tpublic float GripScale { get; set; } = 1f; // live multiplier, e.g. handbrake drift cuts rear grip\r\n\tpublic float ParkBrakeScale { get; set; } = 1f; // anti-jitter stiction strength; controller fades it with throttle\r\n\r\n\t/// <summary>Per-substep DRIVE-side angular-velocity cap (rad/s), set by the controller each\r\n\t/// substep to the drivetrain's redline-equivalent wheel speed for the current gear (see\r\n\t/// <see cref=\"Drivetrain.RedlineWheelSpeed\"/>). Drive torque can never push the wheel PAST this\r\n\t/// within a substep; the ground (tire reaction) still can, and a pre-existing overspeed is never\r\n\t/// yanked down (no phantom braking). float.MaxValue = no cap (undriven wheels, neutral).</summary>\r\n\tpublic float DriveOmegaCap { get; set; } = float.MaxValue;\r\n\r\n\t// State\r\n\tpublic float AngularVelocity { get; private set; } // rad/s, +forward\r\n\tpublic float SteerAngle { get; set; } // degrees, set by controller\r\n\tpublic float SlipRatio { get; private set; }\r\n\tpublic float SlipAngle { get; private set; } // radians\r\n\tpublic float Load { get; private set; } // N\r\n\tpublic bool IsGrounded => _trace.Hit;\r\n\tpublic float GroundSpeed { get; private set; } // m/s along wheel forward\r\n\tpublic float SuspensionLength { get; private set; } // m, attach point to wheel center, for visuals\r\n\r\n\tpublic string DebugTrace => !_trace.Hit\r\n\t\t? \"miss\"\r\n\t\t: $\"{_trace.GameObject?.Name ?? \"?\"}{(_trace.StartedSolid ? \"!SOLID\" : \"\")} d{_trace.Distance:F1}u n{_trace.Normal.z:F2} sf{(_trace.Surface?.Friction ?? -1f):F2}\";\r\n\r\n\tconst float TraceSphereRadius = 1f * Units.UnitsToMeters; // shapecast sphere, see DoTrace\r\n\r\n\tRigidbody _rigidbody;\r\n\tSceneTraceResult _trace;\r\n\tVector3 _accumulatedForce; // N, world space\r\n\tVector3 _forcePosition;\r\n\tint _substeps;\r\n\tfloat _smoothedSlipAngle;\r\n\r\n\tprotected override void OnEnabled()\r\n\t{\r\n\t\t_rigidbody = Components.GetInAncestorsOrSelf<Rigidbody>();\r\n\t}\r\n\r\n\t/// <summary>Trace and reset accumulators. Call once per fixed update, before substeps.</summary>\r\n\tpublic void BeginStep()\r\n\t{\r\n\t\tDoTrace();\r\n\t\t_accumulatedForce = Vector3.Zero;\r\n\t\t_substeps = 0;\r\n\t}\r\n\r\n\t/// <summary>\r\n\t/// One physics substep. driveTorque/brakeTorque in N\u00b7m. Accumulates chassis force,\r\n\t/// integrates wheel spin.\r\n\t/// </summary>\r\n\tpublic void Substep( float dt, float driveTorque, float brakeTorque )\r\n\t{\r\n\t\t_substeps++;\r\n\r\n\t\tvar up = _rigidbody.WorldRotation.Up;\r\n\r\n\t\t// a grazing contact (car tipped over, wheel scraping a wall) is not suspension:\r\n\t\t// generating spring force there self-propels a flipped car along the ground\r\n\t\tbool validContact = IsGrounded && Vector3.Dot( _trace.Normal, up ) > 0.4f;\r\n\r\n\t\tif ( !validContact )\r\n\t\t{\r\n\t\t\tLoad = 0f;\r\n\t\t\tSlipRatio = 0f;\r\n\t\t\tSlipAngle = 0f;\r\n\t\t\tSuspensionLength = SuspensionTravel;\r\n\t\t\t// AIRBORNE DRIVE GATE (ramp lip-cluster fix 2026-07-21, LIVE-UNVERIFIED): a driven wheel\r\n\t\t\t// with no valid contact gets ZERO drive torque - it freewheels (brakes still act). With\r\n\t\t\t// drive torque passed through, an unloaded driven wheel flared to redline-equivalent\r\n\t\t\t// within ~3 ticks of leaving a kicker lip, the engine pinned at 94-98% redline for the\r\n\t\t\t// whole flight (live capture: rpm 6008-6010 at the lip), the drivetrain's limiter-camp\r\n\t\t\t// escape upshifted MID-AIR ~0.27 s into every full-throttle flight at 20-30 m/s, and the\r\n\t\t\t// car touched down at the flared surface speed: slip +0.4..+0.5, a 10-12 kN (~1 g) tire\r\n\t\t\t// spike for ~2 ticks, the skid chirp, and a landing one gear too tall (post-landing drive\r\n\t\t\t// force -26..-37% vs pre-lip) - the felt \"hitch going off the ramps\" at any speed\r\n\t\t\t// (offline quantification: tools/ramp_lip_drivetrain_port.py). Zeroing drive here starves\r\n\t\t\t// that whole cascade: omega holds ~rolling speed, rpm stays steady, the escape shift\r\n\t\t\t// never arms in air, and touchdown slip is ~0. Flat-ground behavior is byte-identical BY\r\n\t\t\t// CONSTRUCTION (this branch never runs with valid ground contact; grounded-tick A/B in\r\n\t\t\t// the port asserts bit-identical trajectories). Registered prediction at commit time: the\r\n\t\t\t// owner's Sim-mode discriminator will NOT kill the hitch (the cascade is\r\n\t\t\t// assist-independent); result to be recorded next to this comment either way.\r\n\t\t\tIntegrateWheelSpin( dt, 0f, brakeTorque, 0f );\r\n\t\t\t// Airborne wind-down is bearing drag only: 2%/s. The previous 0.5f here was 50%/s\r\n\t\t\t// (documented in round 4 as \"0.5%/s\", a 100x misread of its own constant): after a\r\n\t\t\t// 1.2 s flight the wheels arrived at ~55% of road speed and braked the car while\r\n\t\t\t// spinning back up (flight recorder 2026-07-21: ~2 m/s of the touchdown loss plus\r\n\t\t\t// the landing skid chirp came from exactly this). Real free wheels barely slow.\r\n\t\t\tAngularVelocity *= 1f - 0.02f * dt;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// --- suspension ---\r\n\t\t// Force acts along the CONTACT NORMAL, not body-up: body-up creates a feedback\r\n\t\t// loop where a tilted car pushes itself sideways, slides, and flips.\r\n\t\t// Compression is clamped to physical travel: bottoming out is a rigid contact\r\n\t\t// for the body collider (bump stop), not unbounded spring force.\r\n\t\tvar normal = _trace.Normal;\r\n\t\tfloat restLength = SuspensionTravel + Radius;\r\n\t\tfloat hitLength = _trace.Distance * Units.UnitsToMeters + TraceSphereRadius; // sphere stops one radius short\r\n\t\tfloat compression = Math.Clamp( restLength - hitLength, 0f, SuspensionTravel );\r\n\t\tSuspensionLength = Math.Clamp( hitLength - Radius, 0f, SuspensionTravel );\r\n\r\n\t\tvar velAtWheel = _rigidbody.GetVelocityAtPoint( WorldPosition ) * Units.UnitsToMeters;\r\n\t\tfloat compressionSpeed = -Vector3.Dot( velAtWheel, normal );\r\n\r\n\t\tfloat springForce = SpringRate * compression + DamperRate * compressionSpeed;\r\n\t\tLoad = Math.Clamp( springForce, 0f, StaticLoad * 4f );\r\n\r\n\t\t_accumulatedForce += normal * Load;\r\n\t\t_forcePosition = _trace.EndPosition;\r\n\r\n\t\t// --- tire frame on the contact plane ---\r\n\t\tvar steerRot = _rigidbody.WorldRotation * Rotation.FromYaw( SteerAngle );\r\n\t\tvar forward = (steerRot.Forward - normal * Vector3.Dot( steerRot.Forward, normal )).Normal;\r\n\t\tvar side = Vector3.Cross( normal, forward );\r\n\r\n\t\tvar contactVel = _rigidbody.GetVelocityAtPoint( _trace.EndPosition ) * Units.UnitsToMeters;\r\n\t\tfloat vLong = Vector3.Dot( contactVel, forward );\r\n\t\tfloat vLat = Vector3.Dot( contactVel, side );\r\n\t\tGroundSpeed = vLong;\r\n\r\n\t\t// --- slip ---\r\n\t\t// longitudinal uses RAW slip + a one-substep force clamp below (smoothing it added\r\n\t\t// ~100 ms of feedback lag that turned wheel spin into a rhythmic surge);\r\n\t\t// lateral keeps light relaxation, its loop is chassis-side and much slower\r\n\t\tfloat slipVelocity = AngularVelocity * Radius - vLong; // m/s at the contact patch\r\n\t\tSlipRatio = slipVelocity / MathF.Max( MathF.Abs( vLong ), 2.0f );\r\n\r\n\t\tfloat rawSlipAngle = MathF.Atan2( vLat, MathF.Abs( vLong ) + 0.7f );\r\n\t\tfloat relax = Math.Clamp( (MathF.Abs( vLong ) + 1f) * dt / 0.2f, 0.1f, 1f );\r\n\t\t_smoothedSlipAngle += (rawSlipAngle - _smoothedSlipAngle) * relax;\r\n\t\tSlipAngle = _smoothedSlipAngle;\r\n\r\n\t\t// --- forces from curves, load sensitivity, surface grip ---\r\n\t\tfloat surfaceGrip = _trace.Surface?.Friction ?? 1f;\r\n\t\tfloat loadFactor = 1f - LoadSensitivity * MathF.Max( 0f, Load / StaticLoad - 1f );\r\n\t\tfloat maxForce = Load * loadFactor * surfaceGrip * GripScale;\r\n\r\n\t\tfloat fx = LongitudinalCurve.Evaluate( SlipRatio ) * MathF.Sign( SlipRatio ) * maxForce;\r\n\t\tfloat fy = -LateralCurve.Evaluate( SlipAngle ) * MathF.Sign( SlipAngle ) * maxForce;\r\n\r\n\t\t// one-substep stability clamp: never push the wheel past ground-speed match within\r\n\t\t// a single substep (gain*dt/inertia > 2 here, the raw loop oscillates without this)\r\n\t\tfloat fxStable = MathF.Abs( slipVelocity ) * Inertia / (Radius * Radius * dt);\r\n\t\tfx = Math.Clamp( fx, -fxStable, fxStable );\r\n\r\n\t\t// --- friction ellipse (spec \u00a75.2.1.4) ---\r\n\t\tfloat combined = MathF.Sqrt( fx * fx + fy * fy );\r\n\t\tif ( combined > maxForce && combined > 0.001f )\r\n\t\t{\r\n\t\t\tfloat scale = maxForce / combined;\r\n\t\t\tfx *= scale;\r\n\t\t\tfy *= scale;\r\n\t\t}\r\n\r\n\t\t// --- low-speed parking blend: kill standstill jitter ---\r\n\t\t// force is capped at what stops this wheel's mass share within one fixed frame\r\n\t\t// (spec 5.2.1.6) \u2014 uncapped, it overshoots and becomes a self-sustaining oscillator\r\n\t\tvar planarVel = forward * vLong + side * vLat;\r\n\t\tfloat planarSpeed = planarVel.Length;\r\n\t\tfloat parkOmegaBlend = 0f;\r\n\t\tif ( planarSpeed < 1.5f && MathF.Abs( AngularVelocity * Radius ) < 1.5f && planarSpeed > 0.001f )\r\n\t\t{\r\n\t\t\t// ParkBrakeScale: throttle dissolves the stiction \u2014 steered fronts were\r\n\t\t\t// \"parking\" against full-lock standstill launches (2 km/h crawls, tele 07-07)\r\n\t\t\tfloat blend = (1f - planarSpeed / 1.5f) * ParkBrakeScale;\r\n\t\t\tfloat massShare = StaticLoad / 9.81f; // kg carried by this wheel\r\n\t\t\tfloat frameDt = dt * VehicleController.Substeps;\r\n\t\t\tfloat stopForce = massShare * planarSpeed / frameDt * 0.8f;\r\n\t\t\tfloat parkMag = MathF.Min( MathF.Min( planarSpeed * Load * 1.5f, stopForce ), maxForce );\r\n\r\n\t\t\tvar park = -planarVel / planarSpeed * parkMag;\r\n\t\t\tfx = fx * (1f - blend) + Vector3.Dot( park, forward ) * blend;\r\n\t\t\tfy = fy * (1f - blend) + Vector3.Dot( park, side ) * blend;\r\n\t\t\tparkOmegaBlend = blend;\r\n\t\t}\r\n\r\n\t\t_accumulatedForce += forward * fx + side * fy;\r\n\r\n\t\tIntegrateWheelSpin( dt, driveTorque, brakeTorque, fx );\r\n\r\n\t\t// Park the wheel's SPIN, not just the chassis. The parking blend above brakes the chassis, but\r\n\t\t// its reaction torque (-fx*Radius, fed into IntegrateWheelSpin) spins this wheel up and, with\r\n\t\t// nothing anchoring omega at rest, it settles at a nonzero spin that keeps pushing the car: a\r\n\t\t// permanent slow creep with the tyre visibly rotating and the slip ratio pinging the skid\r\n\t\t// threshold (community report: \"unless you've perfectly stopped, it infinitely skids and rotates\r\n\t\t// the wheels in place\"). Pull omega toward the ground-rolling speed vLong/Radius (= 0 at a true\r\n\t\t// standstill) by the SAME blend, so a parked, unbraked wheel settles to zero spin and zero slip.\r\n\t\t// Offline sim: a 0.2 m/s creep that never stopped and skidded 50-100% of frames now settles to\r\n\t\t// under 1 mm/s with zero skid, while cruise (over 1.5 m/s, blend inert) is byte-identical. Fades\r\n\t\t// out with throttle (blend carries ParkBrakeScale) so standstill launches are unaffected.\r\n\t\tif ( parkOmegaBlend > 0f )\r\n\t\t\tAngularVelocity = MathX.Lerp( AngularVelocity, vLong / Radius, parkOmegaBlend );\r\n\t}\r\n\r\n\t// Cap-aware drive-torque rolloff onset (kart cap-camping fix 2026-07-18): drive torque begins\r\n\t// fading toward zero once the wheel's own spin reaches this fraction of DriveOmegaCap; below it\r\n\t// the rolloff is inert so below-cap behavior is byte-identical.\r\n\tconst float DriveRolloffOnset = 0.90f;\r\n\r\n\tvoid IntegrateWheelSpin( float dt, float driveTorque, float brakeTorque, float tireForce )\r\n\t{\r\n\t\tfloat preOmega = AngularVelocity;\r\n\t\tbool driving = driveTorque != 0f;\r\n\t\tfloat driveDir = driving ? MathF.Sign( driveTorque ) : 0f;\r\n\r\n\t\t// Cap-aware drive-torque rolloff (kart \"stuck turning\" fix 2026-07-18). The per-substep\r\n\t\t// clamp below is a hard backstop, but with the clamp ALONE a driven wheel under sustained\r\n\t\t// full torque CAMPS exactly at the cap; when forward speed then collapses in a corner the\r\n\t\t// slip ratio blows far past the grip peak (live: 7+) and the longitudinal tail force eats the\r\n\t\t// friction ellipse, killing rear lateral grip so the yaw holds against countersteer. Fade\r\n\t\t// drive torque to zero as the wheel approaches the cap so it settles OFF the cap instead of\r\n\t\t// camping on it. Smoothstep, not a hard corner, to avoid a torque-fade limit cycle. Inert\r\n\t\t// below the onset (approach <= DriveRolloffOnset) so below-cap behavior is byte-identical.\r\n\t\tif ( driving && DriveOmegaCap < float.MaxValue )\r\n\t\t{\r\n\t\t\tfloat approach = driveDir * preOmega / DriveOmegaCap;\r\n\t\t\tif ( approach > DriveRolloffOnset )\r\n\t\t\t{\r\n\t\t\t\tfloat f = Math.Clamp( (1f - approach) / (1f - DriveRolloffOnset), 0f, 1f );\r\n\t\t\t\tdriveTorque *= f * f * (3f - 2f * f);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// drive + tire reaction\r\n\t\tfloat torque = driveTorque - tireForce * Radius;\r\n\t\tAngularVelocity += torque / Inertia * dt;\r\n\r\n\t\t// Per-substep drive-side overshoot clamp (kart high-PeakTorque wobble hunt 2026-07-18).\r\n\t\t// The rev limiter zeroes torque only on the substep AFTER wheel-implied rpm crosses redline,\r\n\t\t// so one substep of unlimited drive torque on a light wheel overshoots redline-equivalent\r\n\t\t// omega by up to 6-8x (measured live: kart at 900 N-m spikes to 289-339 rad/s vs the 44 rad/s\r\n\t\t// gear-1 redline equivalent; even stock 52 N-m reaches 141). The spike is what lets an\r\n\t\t// unloaded rear diverge violently from its loaded twin over any perturbation (the felt\r\n\t\t// \"individual tires have different traction\" left-right wobble). Clamp: DRIVE torque may\r\n\t\t// never push omega past the cap within a substep. Signed by drive direction so reverse works;\r\n\t\t// cap floors at the pre-integration omega so ground-driven overspeed (downhill coast) is\r\n\t\t// never yanked down, and the ground reaction path is untouched. Guarded on the ORIGINAL drive\r\n\t\t// intent so a rolloff that faded torque to zero still cannot let the wheel blow past the cap.\r\n\t\tif ( driving && DriveOmegaCap < float.MaxValue )\r\n\t\t{\r\n\t\t\tfloat cap = MathF.Max( DriveOmegaCap, driveDir * preOmega );\r\n\t\t\tAngularVelocity = driveDir > 0f\r\n\t\t\t\t? MathF.Min( AngularVelocity, cap )\r\n\t\t\t\t: MathF.Max( AngularVelocity, -cap );\r\n\t\t}\r\n\r\n\t\t// brakes can stop the wheel but never reverse it within a step\r\n\t\tif ( brakeTorque > 0f )\r\n\t\t{\r\n\t\t\tfloat brakeDelta = brakeTorque / Inertia * dt;\r\n\t\t\tAngularVelocity = MathF.Abs( AngularVelocity ) <= brakeDelta\r\n\t\t\t\t? 0f\r\n\t\t\t\t: AngularVelocity - MathF.Sign( AngularVelocity ) * brakeDelta;\r\n\t\t}\r\n\t}\r\n\r\n\t/// <summary>Apply the substep-averaged force to the chassis. Call once per fixed update.</summary>\r\n\tpublic void EndStep()\r\n\t{\r\n\t\tif ( _substeps == 0 || _accumulatedForce.IsNearZeroLength )\r\n\t\t\treturn;\r\n\r\n\t\tvar averaged = _accumulatedForce / _substeps;\r\n\t\t_rigidbody.ApplyForceAt( _forcePosition, averaged * Units.MetersToUnits );\r\n\t}\r\n\r\n\tvoid DoTrace()\r\n\t{\r\n\t\tvar down = _rigidbody.WorldRotation.Down;\r\n\t\tfloat lengthUnits = (SuspensionTravel + Radius) * Units.MetersToUnits;\r\n\r\n\t\t_trace = Scene.Trace\r\n\t\t\t.Radius( 1f )\r\n\t\t\t.IgnoreGameObjectHierarchy( _rigidbody.GameObject )\r\n\t\t\t.WithoutTags( \"car\" )\r\n\t\t\t.FromTo( WorldPosition, WorldPosition + down * lengthUnits )\r\n\t\t\t.Run();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "fieldguide.vehiclephysics",
            "Path": "Code/Drivetrain.cs",
            "FileName": "Drivetrain.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 313829,
            "Code": "namespace FieldGuide.VehiclePhysics;\r\n\r\n/// <summary>\r\n/// Engine + clutch + gearbox + diff state machine. Plain class owned by\r\n/// VehicleController, simulated per substep. RPM is its own integrated state coupled to\r\n/// the wheels through an auto-clutch, so revving at standstill and shift flare exist.\r\n/// </summary>\r\npublic class Drivetrain\r\n{\r\n\treadonly CarDefinition _def;\r\n\r\n\tpublic float Rpm { get; private set; }\r\n\tpublic int Gear { get; private set; } = 1; // 1-based; 0 = neutral, -1 = reverse\r\n\tpublic bool IsShifting => _shiftTimer > 0f;\r\n\r\n\t/// <summary>Driver-selectable transmission mode. false (default) = the untuned automatic box\r\n\t/// (byte-identical existing behavior); true = sequential MANUAL \u2014 the auto-shift block in\r\n\t/// <see cref=\"Simulate\"/> is suppressed and gear changes come from <see cref=\"ShiftUp\"/> /\r\n\t/// <see cref=\"ShiftDown\"/> driven off input. The rev limiter, shift timer/lockout, and clutch\r\n\t/// machinery are untouched by the mode (they model the shift event either way).</summary>\r\n\tpublic bool ManualMode { get; set; }\r\n\r\n\t/// <summary>Redline (rpm) for this car \u2014 read by the manual over-rev guard / UI.</summary>\r\n\tpublic float Redline => _def.RedlineRpm;\r\n\r\n\tfloat _shiftTimer;\r\n\tfloat _shiftLockout;\r\n\tfloat _freeRpm; // engine-side rpm when the clutch slips\r\n\tfloat _limiterHold; // seconds spent pinned near redline under power (limiter-camp escape)\r\n\r\n\t// Downshift arming (anti-hunt hysteresis): a gear entered by UPSHIFT starts unproven and may\r\n\t// not auto-downshift under power until groundRpm has first risen past ShiftDownRpm \u2014 an escape\r\n\t// shift can land below that threshold on purpose (recovery in progress), and a fixed-length\r\n\t// lockout can expire before the low-rpm clutch-slip zone climbs out (measured: 2nd entered at\r\n\t// ~1150 ground rpm climbs ~575 rpm/s, so 1.5 s ends at ~1980 < the 2200 downshift point \u2014\r\n\t// the box bounced straight back into the wheelspin it had escaped). Lifting the throttle\r\n\t// re-arms the downshift immediately, so coasting to a stop still steps down normally.\r\n\tbool _gearProven = true;\r\n\r\n\tpublic Drivetrain( CarDefinition def )\r\n\t{\r\n\t\t_def = def;\r\n\t\tRpm = def.IdleRpm;\r\n\t\t_freeRpm = def.IdleRpm;\r\n\t}\r\n\r\n\t/// <summary>Analytic torque curve: ~50% at idle, peak ~75% of the band, mild falloff at redline.</summary>\r\n\tpublic float EngineTorqueAt( float rpm )\r\n\t{\r\n\t\tfloat n = Math.Clamp( (rpm - _def.IdleRpm) / (_def.RedlineRpm - _def.IdleRpm), 0f, 1f );\r\n\t\tfloat shape = (0.5f + 1.5f * n - n * n) / 1.0625f;\r\n\t\treturn _def.PeakTorque * shape;\r\n\t}\r\n\r\n\tfloat CurrentRatio => Gear switch\r\n\t{\r\n\t\t> 0 => _def.GearRatios[Gear - 1] * _def.FinalDrive,\r\n\t\t-1 => -_def.ReverseRatio * _def.FinalDrive,\r\n\t\t_ => 0f\r\n\t};\r\n\r\n\t/// <summary>Redline-equivalent driven-wheel angular speed (rad/s) for the CURRENT gear ratio;\r\n\t/// float.MaxValue in neutral (no drive coupling). Read fresh each substep by the controller\r\n\t/// (the ratio changes on a shift) and handed to the wheels as the per-substep drive-side omega\r\n\t/// cap: the rev limiter cuts torque only on the substep AFTER wheel-implied rpm crosses redline,\r\n\t/// and a light wheel at high PeakTorque can blow 6x past redline-equivalent within that one\r\n\t/// substep (measured: kart at 900 N-m hits 289-339 rad/s vs a 44 rad/s gear-1 redline\r\n\t/// equivalent). The cap makes the limiter effectively per-substep on the drive side.</summary>\r\n\tpublic float RedlineWheelSpeed\r\n\t\t=> CurrentRatio == 0f ? float.MaxValue : _def.RedlineRpm * MathF.Tau / 60f / MathF.Abs( CurrentRatio );\r\n\r\n\t/// <summary>\r\n\t/// One substep. avgDrivenWheelSpeed and groundWheelSpeed in rad/s. Returns torque per driven\r\n\t/// wheel (N\u00b7m). Clutch engagement is a continuous blend \u2014 a binary locked/slipping switch\r\n\t/// sits right at town-driving speeds and judders. Shift decisions use GROUND speed, not\r\n\t/// engine/wheel rpm: wheelspin inflates engine rpm, causing 1-2-1-2 shift hunting where\r\n\t/// every downshift torque-spikes the rear tires (spin-outs).\r\n\t/// </summary>\r\n\tpublic float Simulate( float dt, float throttle, float avgDrivenWheelSpeed, float groundWheelSpeed, int drivenWheelCount )\r\n\t{\r\n\t\tif ( _shiftTimer > 0f )\r\n\t\t{\r\n\t\t\t_shiftTimer -= dt;\r\n\t\t\tthrottle = 0f; // torque cut during shift\r\n\t\t}\r\n\r\n\t\tfloat ratio = CurrentRatio;\r\n\t\tfloat wheelImpliedRpm = MathF.Abs( avgDrivenWheelSpeed * ratio ) * 60f / MathF.Tau;\r\n\r\n\t\t// continuous auto-clutch: 0 at stall rpm, fully locked a few hundred rpm above idle\r\n\t\tfloat stallRpm = _def.IdleRpm * 1.05f;\r\n\t\tfloat lockRpm = _def.IdleRpm * 1.7f;\r\n\t\tfloat engagement = Gear == 0 ? 0f : Math.Clamp( (wheelImpliedRpm - stallRpm) / (lockRpm - stallRpm), 0f, 1f );\r\n\r\n\t\t// engine-side rpm when slipping: revs toward the throttle target\r\n\t\tfloat freeTarget = _def.IdleRpm + throttle * (_def.RedlineRpm * 0.5f - _def.IdleRpm);\r\n\t\t_freeRpm = Math.Clamp( _freeRpm + (freeTarget - _freeRpm) * 5f * dt, _def.IdleRpm, _def.RedlineRpm );\r\n\r\n\t\tfloat lockedRpm = Math.Clamp( wheelImpliedRpm, _def.IdleRpm, _def.RedlineRpm );\r\n\t\tRpm = MathX.Lerp( _freeRpm, lockedRpm, engagement );\r\n\r\n\t\tfloat engineTorque = EngineTorqueAt( Rpm ) * throttle;\r\n\t\tfloat engineBrake = _def.EngineBrakeTorque * (1f - throttle) * (Rpm / _def.RedlineRpm) * engagement;\r\n\t\tfloat slipTransmission = Math.Clamp( throttle * 1.2f, 0f, 1f ) * 0.85f;\r\n\t\tfloat clutchFactor = MathX.Lerp( slipTransmission, 1f, engagement );\r\n\t\tfloat torqueOut = (engineTorque * clutchFactor - engineBrake) * ratio;\r\n\r\n\t\t// rev limiter\r\n\t\tif ( Rpm >= _def.RedlineRpm && throttle > 0f )\r\n\t\t\ttorqueOut = MathF.Min( torqueOut, 0f );\r\n\r\n\t\t// automatic shifting from ground-speed-implied rpm + post-shift lockout\r\n\t\t_shiftLockout -= dt;\r\n\t\tfloat groundRpm = MathF.Abs( groundWheelSpeed * ratio ) * 60f / MathF.Tau;\r\n\r\n\t\t// Limiter-camp escape: the upshift decision reads GROUND-speed rpm (anti-hunt \u2014 see the\r\n\t\t// header comment) but the engine + rev limiter run on WHEEL-implied rpm, and wheelspin\r\n\t\t// separates the two. With traction control off, a hard launch inflates engine rpm onto the\r\n\t\t// limiter while groundRpm is still below ShiftUpRpm \u2014 the box then bounces on the limiter\r\n\t\t// until ground speed catches up (measured on the hatch: ~3.4 s pinned 5945\u20136300 in 1st\r\n\t\t// while groundRpm crawled 560\u21925730). The limiter cut decelerates a spinning wheel within\r\n\t\t// substeps, so the bounce dips a few percent below redline many times a second \u2014 the\r\n\t\t// near-limiter window is therefore WIDE (0.94) and the hold DECAYS on dips instead of\r\n\t\t// resetting, or the oscillation zeroes the timer forever and the escape never fires.\r\n\t\tbool nearLimiter = Rpm >= _def.RedlineRpm * 0.94f && throttle > 0.5f;\r\n\t\t_limiterHold = nearLimiter ? _limiterHold + dt : MathF.Max( 0f, _limiterHold - dt * 0.5f );\r\n\r\n\t\t// Downshift arming: the current gear proves itself the moment ground rpm clears the\r\n\t\t// downshift threshold; from then on the normal downshift rule applies unchanged.\r\n\t\tif ( !_gearProven && groundRpm >= _def.ShiftDownRpm )\r\n\t\t\t_gearProven = true;\r\n\r\n\t\tif ( !ManualMode && Gear > 0 && !IsShifting && _shiftLockout <= 0f )\r\n\t\t{\r\n\t\t\tbool wantUp = groundRpm > _def.ShiftUpRpm;\r\n\t\t\tbool escape = false;\r\n\t\t\tif ( !wantUp && _limiterHold > 0.25f && Gear < _def.GearRatios.Length )\r\n\t\t\t{\r\n\t\t\t\t// Escape guard: the next gear only needs to be VIABLE, not already above the\r\n\t\t\t\t// downshift point \u2014 a spinning launch hooks up instantly on the taller gear and\r\n\t\t\t\t// recovers under the downshift-arming hysteresis (which holds the box in the new\r\n\t\t\t\t// gear through the sub-ShiftDownRpm climb no matter how long it takes).\r\n\t\t\t\tfloat nextRatio = _def.GearRatios[Gear] * _def.FinalDrive; // Gear is 1-based \u2192 [Gear] = next gear up\r\n\t\t\t\tfloat postShiftGroundRpm = MathF.Abs( groundWheelSpeed * nextRatio ) * 60f / MathF.Tau;\r\n\t\t\t\twantUp = escape = postShiftGroundRpm >= _def.ShiftDownRpm * 0.5f;\r\n\t\t\t}\r\n\r\n\t\t\tif ( wantUp && Gear < _def.GearRatios.Length )\r\n\t\t\t{\r\n\t\t\t\tGear++;\r\n\t\t\t\t_shiftTimer = 0.15f;\r\n\t\t\t\t// Escape shifts keep a longer settle hold; the arming hysteresis above is the real\r\n\t\t\t\t// anti-hunt guard (a fixed timer alone measurably expired mid-recovery).\r\n\t\t\t\t_shiftLockout = escape ? 1.5f : 0.8f;\r\n\t\t\t\t_limiterHold = 0f;\r\n\t\t\t\t_gearProven = false; // entered by upshift \u2192 must prove itself before downshifting under power\r\n\t\t\t}\r\n\t\t\telse if ( groundRpm < _def.ShiftDownRpm && Gear > 1 && (_gearProven || throttle < 0.5f) )\r\n\t\t\t{\r\n\t\t\t\tGear--;\r\n\t\t\t\t_shiftTimer = 0.12f;\r\n\t\t\t\t_shiftLockout = 0.8f;\r\n\t\t\t\t_gearProven = true; // entered downward \u2014 ground rpm is higher here by ratio; nothing to prove\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// open diff v1: equal split across driven wheels\r\n\t\treturn drivenWheelCount > 0 ? torqueOut / drivenWheelCount : 0f;\r\n\t}\r\n\r\n\tpublic void EngageReverse() { Gear = -1; _shiftTimer = 0f; _gearProven = true; }\r\n\tpublic void EngageForward() { if ( Gear <= 0 ) { Gear = 1; _shiftTimer = 0f; _gearProven = true; } }\r\n\r\n\t// \u2500\u2500 sequential MANUAL shift \u2500\u2500\r\n\t// Gated on IsShifting only (the 0.15 s torque-cut window), NOT on _shiftLockout \u2014 the 0.8 s\r\n\t// lockout is the auto box's anti-hunt guard and would make a hand-shifted sequential box feel\r\n\t// sluggish; the player decides when to shift. Both timers are still set so the shift flare /\r\n\t// torque cut model exactly as the auto path, and so toggling back to AUTO inherits a clean state.\r\n\r\n\t/// <summary>Sequential manual up-shift: advance one forward gear. No-op in reverse/neutral, at the\r\n\t/// top gear, or mid-shift. Returns true if a gear change happened.</summary>\r\n\tpublic bool ShiftUp()\r\n\t{\r\n\t\tif ( IsShifting ) return false;\r\n\t\tif ( Gear <= 0 || Gear >= _def.GearRatios.Length ) return false;\r\n\t\tGear++;\r\n\t\t_shiftTimer = 0.15f;\r\n\t\t_shiftLockout = 0.8f;\r\n\t\t_gearProven = true; // player-commanded \u2014 the auto arming rule never second-guesses manual shifts\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// <summary>Sequential manual down-shift: drop one forward gear \u2014 BLOCKED if it would throw engine\r\n\t/// rpm past redline at the current ground speed (money-shift / over-rev guard). No-op below 1st, in\r\n\t/// reverse/neutral, or mid-shift. <paramref name=\"groundWheelSpeed\"/> is the ground-implied wheel\r\n\t/// speed (rad/s), the same quantity the auto path shifts on. Returns true if a gear change happened.</summary>\r\n\tpublic bool ShiftDown( float groundWheelSpeed )\r\n\t{\r\n\t\tif ( IsShifting ) return false;\r\n\t\tif ( Gear <= 1 ) return false; // below 1st does nothing; reverse engages automatically (ReadInput)\r\n\t\tif ( PredictedDownshiftRpm( groundWheelSpeed ) > _def.RedlineRpm ) return false; // over-rev guard\r\n\t\tGear--;\r\n\t\t_shiftTimer = 0.12f;\r\n\t\t_shiftLockout = 0.8f;\r\n\t\t_gearProven = true; // player-commanded \u2014 the auto arming rule never second-guesses manual shifts\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// <summary>Engine rpm a one-gear downshift would produce at this ground wheel speed (rad/s), using\r\n\t/// the SAME ground-speed\u2192rpm mapping as the auto-shift decision so the over-rev guard and the rev\r\n\t/// limiter agree. Returns the current <see cref=\"Rpm\"/> when there is no lower forward gear.</summary>\r\n\tpublic float PredictedDownshiftRpm( float groundWheelSpeed )\r\n\t{\r\n\t\tif ( Gear <= 1 ) return Rpm;\r\n\t\tfloat lowerRatio = _def.GearRatios[Gear - 2] * _def.FinalDrive;\r\n\t\treturn MathF.Abs( groundWheelSpeed * lowerRatio ) * 60f / MathF.Tau;\r\n\t}\r\n}\r\n"
        }
    ]
}