Player/Car/Gameplay/CarReturnToTrack.cs

Component on player cars that lets the local player return to the track or self-destruct and respawn. It detects when the car is off the racing line, handles input for a ReturnToTrack action, shows a hold-progress for self-destruct, spawns an explosion FX and triggers the respawn.

NetworkingFile Access
using Machines.GameModes;
using Machines.Race;

namespace Machines.Player;

/// <summary>
/// Lets the player teleport back to the track
/// </summary>
public sealed class CarReturnToTrack : Component
{
	[RequireComponent]
	public Car Car { get; private set; }

	/// <summary>
	/// Lateral distance from the racing line at which the car is considered off-track.
	/// </summary>
	[Property]
	public float OffTrackDistance { get; set; } = 350f;

	/// <summary>
	/// How long the return button must be held to self-destruct and respawn.
	/// </summary>
	[Property]
	public float ExplodeHoldTime { get; set; } = 2f;

	/// <summary>
	/// Explosion FX spawned when the car self-destructs on a held return.
	/// </summary>
	[Property]
	public GameObject ExplosionPrefab { get; set; }

	/// <summary>
	/// Whether the car is currently off the track
	/// </summary>
	public bool IsOffTrack
	{
		get
		{
			var line = RacingPath.Current?.Optimal;
			if ( line is null || !line.IsValid )
				return false;

			var d = line.GetDistanceAtPosition( WorldPosition );
			var nearest = line.GetPointAtDistance( d );
			var lateral = (WorldPosition - nearest).WithZ( 0f ).Length;
			return lateral > OffTrackDistance;
		}
	}

	/// <summary>
	/// Whether the local player can press the return button right now. Allowed at any time while off-track.
	/// </summary>
	public bool CanReturn =>
		Car.IsValid() && Car.IsLocalPlayer
		&& Car.Respawn.IsValid() && !Car.Respawn.IsRespawning
		&& IsOffTrack;

	/// <summary>
	/// Whether the local player can self-destruct right now: grounded and mid-race.
	/// </summary>
	public bool CanExplode =>
		Car.IsValid() && Car.IsLocalPlayer
		&& Car.Respawn.IsValid() && !Car.Respawn.IsRespawning
		&& (Car.Movement?.IsGrounded ?? false)
		&& BaseGameMode.Current?.State == GameModeState.Playing;

	/// <summary>
	/// Self-destruct hold progress [0..1]; 0 when not actively charging a self-destruct.
	/// </summary>
	public float SelfDestructProgress { get; private set; }

	// When the return button was pressed, and whether the hold already fired.
	private float _holdStart = -1f;
	private bool _holdTriggered;

	protected override void OnUpdate()
	{
		if ( !Car.IsValid() || !Car.IsLocalPlayer || !Car.Respawn.IsValid() )
			return;

		if ( Input.Pressed( "ReturnToTrack" ) )
		{
			_holdStart = Time.Now;
			_holdTriggered = false;
		}

		// Hold to self-destruct: explode, then respawn. Only while grounded and mid-race.
		var charging = !_holdTriggered && _holdStart >= 0f && Input.Down( "ReturnToTrack" ) && CanExplode;
		SelfDestructProgress = charging ? MathX.Clamp( (Time.Now - _holdStart) / ExplodeHoldTime, 0f, 1f ) : 0f;

		if ( charging && Time.Now - _holdStart >= ExplodeHoldTime )
		{
			_holdTriggered = true;
			SelfDestructProgress = 0f;
			ExplodeAndRespawn();
		}

		if ( Input.Released( "ReturnToTrack" ) )
		{
			_holdStart = -1f;
			_holdTriggered = false;
		}
	}

	private void ExplodeAndRespawn()
	{
		SpawnExplosionFx( WorldPosition );
		Car.Respawn.Respawn();
	}

	[Rpc.Broadcast]
	private void SpawnExplosionFx( Vector3 point )
	{
		if ( ExplosionPrefab.IsValid() )
		{
			ExplosionPrefab.Clone( new CloneConfig
			{
				Transform = new Transform( point ),
				StartEnabled = true
			} );
		}
	}
}