Code/Vehicle/VehicleController.cs
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.VR;

namespace Meteor.VehicleTool.Vehicle;

[Category( "Physics" )]
[Title( "Vehicle Controller" )]
[EditorHandle( Icon = "directions_car" )]
[Icon( "directions_car" )]
public partial class VehicleController : Component
{
	[Property]
	[Hide]
	[RequireComponent]
	public Rigidbody Body { get; set; }
	public float CurrentSpeed { get; private set; }
	public Vector3 LocalVelocity { get; private set; }

	private bool _showRigidBodyComponent;

	[Property]
	[Group( "Components" )]
	[Title( "Show Rigidbody" )]
	public bool ShowRigidbodyComponent
	{
		get => _showRigidBodyComponent;
		set
		{
			_showRigidBodyComponent = value;
			if ( Body.IsValid() )
				Body.Flags = Body.Flags.WithFlag( ComponentFlags.Hidden, !value );
		}
	}

	protected override void OnDisabled()
	{
		VerticalInput = 0;
		Handbrake = 0;
		SteeringAngle = 0;
		CurrentSteerAngle = 0;
		Throttle = 0;
		Brakes = 0;
		if ( UseSteering )
			SetSteerAngle( 0 );

		foreach ( var item in Wheels )
		{
			item.BrakeTorque = 200f;
			item.MotorTorque = 0;
		}
	}

	protected override void OnDestroy()
	{
		RemoveSounds();
	}
	protected override void OnAwake()
	{
		LoadSoundsAsync();
		EnsureComponentsCreated();
		Transmission.OnGearUpShift += OnGearUp;
		Transmission.OnGearDownShift += OnGearDown;
	}
	protected override void OnStart()
	{
		base.OnStart();

		FindWheels();
	}
	protected override void OnUpdate()
	{

		if ( UseAudio )
			UpdateSound();

		if ( IsProxy )
			return;

		if ( UseCameraControls )
			UpdateCameraPosition();

		if ( UseLookControls )
			UpdateEyeAngles();

	}

	protected override void OnFixedUpdate()
	{
		if ( !IsProxy && UseInputControls )
			UpdateInput();

		LocalVelocity = WorldTransform.PointToLocal( Body.GetVelocityAtPoint( WorldPosition ) + WorldPosition );
		CurrentSpeed = Body.Velocity.Length.InchToMeter();
		if ( UseSteering )
			UpdateSteerAngle();
		if ( UsePowertrain )
			UpdateBrakes();

		if ( !IsOnGround )
			AirControl();

		SimulateAerodinamics();
	}

	[Property, Group( "Air Control" )] public float YawTorque { get; set; } = 10000;
	[Property, Group( "Air Control" )] public float PitchTorque { get; set; } = 20000;

	private void AirControl()
	{
		Vector3 torque = Vector3.Zero;
		torque.y = VerticalInput * PitchTorque / Time.Delta;
		torque.x = -SteeringAngle * YawTorque / Time.Delta;
		Body.ApplyTorque( torque * WorldRotation );
	}
	private class DownforcePoint
	{
		public float MaxForce { get; set; }
		public Vector3 Position { get; set; }
	}
	public const float RHO = 1.225f;
	[Property, Group( "Aerodinamics" )] public Vector3 Dimensions = new( 2f, 4.5f, 1.5f );
	[Property, Group( "Aerodinamics" )] public float FrontalCd { get; set; } = 0.35f;
	[Property, Group( "Aerodinamics" )] public float SideCd { get; set; } = 1.05f;
	[Property, Group( "Aerodinamics" )] public float MaxDownforceSpeed { get; set; } = 80f;
	[Property, Group( "Aerodinamics" )] private List<DownforcePoint> DownforcePoints { get; set; } = [];

	private float _forwardSpeed;
	private float _frontalArea;

	private float _sideArea;
	private float _sideSpeed;
	private float lateralDragForce;
	private float longitudinalDragForce;

	private void SimulateAerodinamics()
	{
		if ( CurrentSpeed < 1f )
		{
			longitudinalDragForce = 0;
			lateralDragForce = 0;
			return;
		}

		_frontalArea = Dimensions.x * Dimensions.z * 0.85f;
		_sideArea = Dimensions.y * Dimensions.z * 0.8f;
		_forwardSpeed = LocalVelocity.x.InchToMeter();
		_sideSpeed = LocalVelocity.y.InchToMeter();
		longitudinalDragForce = 0.5f * RHO * _frontalArea * FrontalCd * (_forwardSpeed * _forwardSpeed) * (_forwardSpeed > 0 ? -1f : 1f);
		lateralDragForce = 0.5f * RHO * _sideArea * SideCd * (_sideSpeed * _sideSpeed) * (_sideSpeed > 0 ? -1f : 1f);

		Body.ApplyForce( new Vector3( longitudinalDragForce.MeterToInch(), lateralDragForce.MeterToInch(), 0 ).RotateAround( Vector3.Zero, WorldRotation ) );

		float speedPercent = CurrentSpeed / MaxDownforceSpeed;
		float forceCoeff = 1f - (1f - MathF.Pow( speedPercent, 2f ));

		foreach ( DownforcePoint dp in DownforcePoints )
			Body.ApplyForceAt( Transform.World.PointToWorld( dp.Position ), forceCoeff.MeterToInch() * dp.MaxForce.MeterToInch() * -WorldRotation.Up );
	}
}