using Car_simulation.Core.Physics; namespace Car_simulation.Core.Components { public class Engine : ICarComponent { // Energy state public float FlywheelEnergy { get; set; } // Physical properties public float MomentOfInertia { get; set; } = 0.25f; public float IdleRPM { get; set; } = 800f; public float RevLimit { get; set; } = 7000; public float StallSpeed { get; set; } = 200f; public float Throttle { get; set; } = 0f; public bool IsRunning => RPM > StallSpeed; private float _cutoffUntil = 0; private bool _cutoff = false; // Torque curve private TorqueCurve _torqueCurve; public Engine() { FlywheelEnergy = GetEnergyFromRPM(IdleRPM); _torqueCurve = new TorqueCurve(); InitializeDefaultCurve(); } private void InitializeDefaultCurve() { _torqueCurve.AddPoint(0f, 0f); _torqueCurve.AddPoint(800f, 95f); _torqueCurve.AddPoint(1500f, 160f); _torqueCurve.AddPoint(2500f, 200f); _torqueCurve.AddPoint(4000f, 235f); _torqueCurve.AddPoint(5000f, 230f); _torqueCurve.AddPoint(6000f, 210f); _torqueCurve.AddPoint(6800f, 185f); _torqueCurve.AddPoint(7200f, 170f); } public float RPM => GetRPM(); public float AngularVelocity => GetOmega(); public float CurrentPower { get; private set; } public void Update(float deltaTime) { // Engine updates are now handled through Car with totalTime parameter } public void UpdateWithTime(float deltaTime, float totalTime) { UpdateRevLimiter(totalTime); float combustionEnergy = CalculateCombustionPower(deltaTime); float frictionLoss = CalculateFrictionLoss(deltaTime); float netEnergy = combustionEnergy - frictionLoss; CurrentPower = netEnergy / deltaTime; FlywheelEnergy += netEnergy; // Stall protection float stallEnergy = GetEnergyFromRPM(StallSpeed); if (FlywheelEnergy < stallEnergy && Throttle > 0.1f) { FlywheelEnergy = stallEnergy * 1.2f; } FlywheelEnergy = Math.Max(FlywheelEnergy, 0); } private void UpdateRevLimiter(float totalTime) { if (RPM > RevLimit) { _cutoffUntil = totalTime + 0.01f; } _cutoff = (totalTime < _cutoffUntil); } private float CalculateFrictionLoss(float deltaTime) { float frictionTorque = GetFrictionTorque(); float frictionPower = frictionTorque * AngularVelocity; return frictionPower * deltaTime; } private float GetFrictionTorque() { return RPM switch { < 500 => 15f, < 1000 => 14f, < 2000 => 16f, < 3000 => 18f, < 4000 => 21f, < 5000 => 25f, < 6000 => 30f, < 7000 => 36f, _ => 44f }; } private float CalculateCombustionPower(float deltaTime) { float throttle = GetActualThrottle(); if (_cutoff) throttle = 0; float torque = GetTorqueOutput() * throttle; return torque * AngularVelocity * deltaTime; } public float GetActualThrottle() { if (RPM < IdleRPM && Throttle < 0.1f) { float idleThrottle = (IdleRPM - RPM) / 200f; return Math.Clamp(idleThrottle, 0.1f, 0.3f); } return Throttle; } public float GetOmega() => MathF.Sqrt(2f * FlywheelEnergy / MomentOfInertia); public float GetRPM() => GetOmega() * PhysicsUtil.RAD_PER_SEC_TO_RPM; public float GetEnergyFromRPM(float rpm) { float omega = rpm * PhysicsUtil.RPM_TO_RAD_PER_SEC; return 0.5f * MomentOfInertia * omega * omega; } public float GetTorqueOutput() => _torqueCurve.GetTorqueAtRPM(RPM); } public class TorqueCurve { private List<(float RPM, float Torque)> _points = new List<(float, float)>(); public void AddPoint(float rpm, float torque) => _points.Add((rpm, torque)); public float GetTorqueAtRPM(float rpm) { if (rpm <= 400) return 0; if (_points.Count == 0) return 0; var orderedPoints = _points.OrderBy(p => p.RPM).ToList(); if (rpm <= orderedPoints.First().RPM) return orderedPoints.First().Torque; if (rpm >= orderedPoints.Last().RPM) return orderedPoints.Last().Torque; for (int i = 0; i < orderedPoints.Count - 1; i++) { if (rpm >= orderedPoints[i].RPM && rpm <= orderedPoints[i + 1].RPM) { float t = (rpm - orderedPoints[i].RPM) / (orderedPoints[i + 1].RPM - orderedPoints[i].RPM); return PhysicsUtil.Lerp(orderedPoints[i].Torque, orderedPoints[i + 1].Torque, t); } } return 0f; } } }