using System; namespace FluidSim.Components { public class Vehicle { // ---- Gearbox ---- public int CurrentGear { get; private set; } = 0; public readonly float[] GearRatios = { 2.5f, 1.8f, 1.4f, 1.1f, 0.9f, 0.75f }; public float FinalDriveRatio = 3.0f; public float PrimaryReduction = 2.5f; // ---- Clutch ---- public float ClutchInput { get; set; } public float ClutchDisengageTime = 0.15f; private float _clutchTimer; private float _currentEngagement = 0f; /// Time constant for clutch engagement smoothing (seconds). public float EngagementSmoothTime = 0.5f; // longer, gentler bite private float TargetEngagement { get { if (ClutchInput > 0.01f) return 1f - ClutchInput; if (CurrentGear == 0 || _clutchTimer > 0f) return 0f; return 1f; } } public float Engagement => _currentEngagement; // ---- Clutch torque model ---- /// Peak clutch friction torque (Nm) when fully engaged at high RPM. public float BaseMaxTorque = 80f; // much lower than before /// Stiffness when slipping (Nm per rad/s). Lower = softer engagement. public float ClutchStiffness = 50f; // very soft /// Below this engine RPM, the clutch torque is progressively reduced to prevent stalling. public float IdleRpm = 1200f; public float StallPreventionRamp = 300f; // RPM band above idle where torque ramps up // ---- Physical constants ---- public float Mass = 160f; public float WheelRadius = 0.32f; public float DragCoefficient = 0.35f; public float FrontalArea = 0.8f; public float AirDensity = 1.225f; public float RollingFrictionCoeff = 0.01f; public float Gravity = 9.81f; // ---- State ---- public float Speed { get; private set; } public (float clutchTorqueOnEngine, float effectiveEngineInertia) Update(float engineRpm, float engineInertia, float dt) { if (_clutchTimer > 0f) { _clutchTimer -= dt; if (_clutchTimer < 0f) _clutchTimer = 0f; } float target = TargetEngagement; float smoothing = 1f - MathF.Exp(-dt / Math.Max(EngagementSmoothTime, 0.001f)); _currentEngagement += (target - _currentEngagement) * smoothing; if (MathF.Abs(_currentEngagement - target) < 0.001f) _currentEngagement = target; float engagement = _currentEngagement; float totalGear = 1f; if (CurrentGear > 0) totalGear = GearRatios[CurrentGear - 1] * FinalDriveRatio * PrimaryReduction; float engineRadPerSec = engineRpm * 2f * MathF.PI / 60f; float v = MathF.Max(Speed, 0f); float drag = 0.5f * AirDensity * DragCoefficient * FrontalArea * v * v; float rolling = RollingFrictionCoeff * Mass * Gravity; float resistanceForce = drag + rolling; float clutchTorque = 0f; float effectiveInertia = engineInertia; if (engagement > 0f && CurrentGear > 0) { float vehicleReflectedRadPerSec = (Speed / WheelRadius) * totalGear; float slip = engineRadPerSec - vehicleReflectedRadPerSec; // Stall prevention: reduce max torque when engine RPM is near idle float torqueLimit = BaseMaxTorque * engagement; if (engineRpm < IdleRpm + StallPreventionRamp) { float factor = Math.Clamp((engineRpm - IdleRpm) / StallPreventionRamp, 0f, 1f); torqueLimit *= factor; } float stiffnessTorque = ClutchStiffness * engagement * slip; clutchTorque = Math.Clamp(stiffnessTorque, -torqueLimit, torqueLimit); // Lock if slip negligible and engagement high if (engagement >= 0.99f && MathF.Abs(slip) < 1.0f) { float vehicleInertia = Mass * WheelRadius * WheelRadius; float reflectedVehicleInertia = vehicleInertia / (totalGear * totalGear); effectiveInertia = engineInertia + reflectedVehicleInertia; Speed = engineRadPerSec * WheelRadius / totalGear; float loadTorque = resistanceForce * WheelRadius / totalGear; return (loadTorque, effectiveInertia); } } float driveTorqueAtWheel = clutchTorque * totalGear; float driveForce = driveTorqueAtWheel / WheelRadius; float netForce = driveForce - resistanceForce; float acceleration = netForce / Mass; Speed += acceleration * dt; if (Speed < 0f) Speed = 0f; return (clutchTorque, engineInertia); } public void ShiftUp() { if (CurrentGear < GearRatios.Length) { CurrentGear++; AutoDisengageClutch(); } } public void ShiftDown() { if (CurrentGear > 1) { CurrentGear--; AutoDisengageClutch(); } } public void SetNeutral() { CurrentGear = 0; _clutchTimer = 0f; } public void SetFirstGear() { if (CurrentGear == 0) { CurrentGear = 1; AutoDisengageClutch(); } } private void AutoDisengageClutch() { _clutchTimer = ClutchDisengageTime; } public float SpeedKmh => Speed * 3.6f; } }