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

namespace Bugges.VehicleController;

partial class VehicleController : Component
{
	private const string SKID_MARKS = "Skid Marks";

	[Property, FeatureEnabled( SKID_MARKS )]
	private bool IsSkidMarksEnabled = false;

	[Property( Title = "Decal" ), Feature( SKID_MARKS )]
	private DecalDefinition SkidMark_Decal { get; set; }

	[Property( Title = "Distance Between" ), Feature( SKID_MARKS )]
	private float SkidMark_DistanceBetween { get; set; } = 6.0f;

	[Property( Title = "Tolerance" ), Feature( SKID_MARKS )]
	private float SkidMark_Tolerance { get; set; } = 200.0f;

	private readonly Dictionary<string, Vector3> _lastSkidPositions = [];


	private void UpdateTireMarks( Wheel wheel, SceneTraceResult trace )
	{
		wheel.IsSkidding = ShouldSkid( wheel, trace );
		if ( !IsSkidMarksEnabled ) return;
		if ( !wheel.IsSkidding ) return;

		Vector3 wheelBottom = wheel.Visual.WorldPosition + wheel.Down * wheel.Radius;
		string key = wheel.Visual.Name;
		float distToPlane = Vector3.Dot( wheelBottom - trace.HitPosition, trace.Normal );
		Vector3 position = wheelBottom - trace.Normal * distToPlane + trace.Normal * 1f; bool hasKey = _lastSkidPositions.ContainsKey( key );
		if ( !hasKey ) _lastSkidPositions.Add( key, position );

		float dist = Vector3.DistanceBetween( _lastSkidPositions[key], position );
		if ( dist < SkidMark_DistanceBetween ) return;
		_lastSkidPositions[key] = position;

		Vector3 tireVelocity = Body.GetVelocityAtPoint( wheelBottom );
		float sidewaysVelocity = float.Abs( Vector3.Dot( wheel.Outwards, tireVelocity ) );

		float slipIntensity = sidewaysVelocity;
		if ( wheel.IsBraking ) slipIntensity = float.Max( slipIntensity, tireVelocity.Length );

		float normalizedSlip = float.Clamp( (slipIntensity - SkidMark_Tolerance) / 300f, 0.1f, 1.0f );

		var decal = new GameObject( $"{wheel.Visual.Name}_{SkidMark_Decal.ResourceName}" )
		{
			WorldPosition = position,
			WorldRotation = Rotation.LookAt( -trace.Normal, wheel.Forward )
		}.AddComponent<Decal>();

		decal.Rotation = 0f;
		decal.Transient = true;

		decal.ColorMix = normalizedSlip;

		decal.Decals.Add( SkidMark_Decal );
		decal.Size = new Vector3( wheel.Width, 1, 1 );

		Invoke( 10f, decal.DestroyGameObject );
	}

	private bool ShouldSkid( Wheel wheel, SceneTraceResult trace )
	{
		if ( !trace.Hit ) return false;

		Vector3 wheelBottom = wheel.Visual.WorldPosition + wheel.Down * wheel.Radius;
		Vector3 tireVelocity = Body.GetVelocityAtPoint( wheelBottom );
		if ( tireVelocity.LengthSquared < 10000.0f ) return false;

		float sidewaysVelocity = Vector3.Dot( wheel.Outwards, tireVelocity );
		bool isDrifting = float.Abs( sidewaysVelocity ) > SkidMark_Tolerance;
		if ( !wheel.IsBraking && !isDrifting ) return false;

		return true;
	}
}