Code/Vehicle/Wheel/PacejkaCurve.cs
using System;
using Sandbox;

namespace Meteor.VehicleTool.Vehicle.Wheel;

public struct PacejkaCurve
{
	// X // B
	[Range( 0, 30 )] public float Stiffnes;

	// Y // C
	[Range( 0, 5 )] public float ShapeFactor;

	// Z // D
	[Range( 0, 2 )] public float PeakValue;

	// W // E
	[Range( 0, 2 )] public float CurvatureFactor;

	public PacejkaCurve( float stiffnes, float shapeFactor, float peakValue, float curvatureFactor ) : this()
	{
		Stiffnes = stiffnes;
		ShapeFactor = shapeFactor;
		PeakValue = peakValue;
		CurvatureFactor = curvatureFactor;
		UpdateFrictionCurve();
	}

	/// <summary>
	/// Slip at which the friction preset has highest friction.
	/// </summary>
	public float PeakSlip;

	private Curve Curve;

	/// <summary>
	/// Gets the slip at which the friction is the highest for this friction curve.
	/// </summary>
	private readonly float GetPeakSlip()
	{
		float peakSlip = -1;
		float yMax = 0;

		for ( float i = 0; i < 1f; i += 0.01f )
		{
			float y = Curve.Evaluate( i );
			if ( y > yMax )
			{
				yMax = y;
				peakSlip = i;
			}
		}

		return peakSlip;
	}
	public readonly float Evaluate( float time ) => Curve.Evaluate( Math.Abs( time ) );


	/// <summary>
	///     Generate Curve from B,C,D and E parameters of Pacejka's simplified magic formula
	/// </summary>
	private void UpdateFrictionCurve()
	{
		Curve.Frame[] frames = new Curve.Frame[20];
		float t = 0;

		for ( int i = 0; i < frames.Length; i++ )
		{
			float v = GetFrictionValue( t );
			frames[i] = new Curve.Frame( t, v );

			if ( i <= 10 )
			{
				t += 0.02f;
			}
			else
			{
				t += 0.1f;
			}
		}
		Curve = new( frames );

		PeakSlip = GetPeakSlip();
	}

	private readonly float GetFrictionValue( float slip )
	{
		float B = Stiffnes;
		float C = ShapeFactor;
		float D = PeakValue;
		float E = CurvatureFactor;
		float t = MathF.Abs( slip );
		return D * MathF.Sin( C * MathF.Atan( B * t - E * (B * t - MathF.Atan( B * t )) ) );
	}

	public static readonly PacejkaCurve Asphalt = new( 9f, 2.15f, 0.933f, 0.971f );
	public static readonly PacejkaCurve AsphaltWet = new( 9f, 2.35f, 0.82f, 0.907f );
	public static readonly PacejkaCurve Generic = new( 8f, 1.9f, 0.8f, 0.99f );
	public static readonly PacejkaCurve Grass = new( 7.38f, 1.1f, 0.538f, 1f );
	public static readonly PacejkaCurve Dirt = new( 7.38f, 1.1f, 0.538f, 1f );
	public static readonly PacejkaCurve Gravel = new( 5.39f, 1.03f, 0.634f, 1f );
	public static readonly PacejkaCurve Ice = new( 1.2f, 2f, 0.16f, 1f );
	public static readonly PacejkaCurve Rock = new( 7.24f, 2.11f, 0.59f, 1f );
	public static readonly PacejkaCurve Sand = new( 5.13f, 1.2f, 0.443f, 0.5f );
	public static readonly PacejkaCurve Snow = new( 8.5f, 1.1f, 0.4f, 0.9f );
	public static readonly PacejkaCurve Tracks = new( 0.1f, 2f, 2f, 1f );

	public static PacejkaCurve GetPreset( PacejkaPreset preset )
	{
		return preset switch
		{
			PacejkaPreset.Asphalt => Asphalt,
			PacejkaPreset.AsphaltWet => AsphaltWet,
			PacejkaPreset.Generic => Generic,
			PacejkaPreset.Grass => Grass,
			PacejkaPreset.Dirt => Dirt,
			PacejkaPreset.Gravel => Gravel,
			PacejkaPreset.Ice => Ice,
			PacejkaPreset.Rock => Rock,
			PacejkaPreset.Sand => Sand,
			PacejkaPreset.Snow => Snow,
			PacejkaPreset.Tracks => Tracks,
			_ => Asphalt,
		};
	}
}

public enum PacejkaPreset
{
	Asphalt,
	AsphaltWet,
	Generic,
	Grass,
	Dirt,
	Gravel,
	Ice,
	Rock,
	Sand,
	Snow,
	Tracks,
}