Player/Car/Gameplay/CarBoost.cs

CarBoost component for player cars. Manages boost fuel, dash input, cooldowns and regen, applies impulse to the car movement, spawns a visual effect, and syncs IsDashing and CurrentFuel for UI.

using Machines.Components;

namespace Machines.Player;

/// <summary>
/// Dash mechanic: press Boost to consume fuel and dash in a steered direction (default: forward).
/// </summary>
public sealed class CarBoost : Component
{
	[RequireComponent]
	public Car Car { get; private set; }

	/// <summary>
	/// Maximum boost fuel (seconds-equivalent of capacity).
	/// </summary>
	[Property]
	public float MaxFuel { get; set; } = 3f;

	/// <summary>
	/// How quickly fuel regenerates per second (when not dashing).
	/// </summary>
	[Property]
	public float RegenRate { get; set; } = 0.5f;

	/// <summary>
	/// How long after a dash before fuel starts regenerating again (seconds).
	/// </summary>
	[Property]
	public float RegenDelay { get; set; } = 1f;

	/// <summary>
	/// Prefab spawned on dash; auto-destroyed after EffectLifetime.
	/// </summary>
	[Property]
	public GameObject BoostEffectPrefab { get; set; }

	/// <summary>
	/// How long the dash effect lives before being destroyed.
	/// </summary>
	[Property]
	public float EffectLifetime { get; set; } = 0.4f;

	/// <summary>
	/// Cooldown between dashes in seconds.
	/// </summary>
	[Property]
	public float DashCooldown { get; set; } = 0.5f;

	/// <summary>
	/// Current fuel remaining (0 to MaxFuel). Synced for UI.
	/// </summary>
	[Sync]
	public float CurrentFuel { get; set; }

	/// <summary>
	/// True briefly after a dash, synced for visuals.
	/// </summary>
	[Sync]
	public bool IsDashing { get; private set; }

	/// <summary>
	/// Boost fraction (0 to 1) for UI display.
	/// </summary>
	public float BoostFraction => MaxFuel > 0f ? CurrentFuel / MaxFuel : 0f;

	/// <summary>
	/// True while the race-start boost lockout is active.
	/// </summary>
	public bool IsLockedOut => Time.Now < _boostEnabledTime;

	/// <summary>
	/// Seconds after race start before boost is allowed.
	/// </summary>
	[ConVar( "game_boost_lockout", Saved = true, Min = 0, Max = 30, Flags = ConVarFlags.Replicated | ConVarFlags.GameSetting )]
	public static float RaceStartLockout { get; set; } = 5f;

	/// <summary>
	/// Fraction of max fuel consumed per dash (0-1). Same for every car.
	/// </summary>
	[ConVar( "dash_fuel_cost", Flags = ConVarFlags.Replicated )]
	public static float DashFuelCost { get; set; } = 0.5f;

	private bool _boostHeldLastFrame;
	private float _dashVisualEndTime;
	private float _nextDashTime;
	private float _regenResumeTime;
	private float _boostEnabledTime = float.MaxValue;

	protected override void OnStart()
	{
		CurrentFuel = MaxFuel;
	}

	protected override void OnFixedUpdate()
	{
		if ( !Car.IsValid() || !Car.IsAuthority )
			return;

		if ( !Car.Input.IsValid() || !Car.Movement.IsValid() )
			return;

		// Set lockout timer when the race enters Playing state.
		if ( _boostEnabledTime == float.MaxValue )
		{
			var gm = GameModes.BaseGameMode.Current;
			if ( gm == null )
				_boostEnabledTime = Time.Now; // No game mode: allow immediately.
			else if ( gm.State == GameModes.GameModeState.Playing )
				_boostEnabledTime = Time.Now + RaceStartLockout;
		}

		var wantsBoost = Car.Input.Current.Boost;
		var stats = Car.ActiveStats;
		var fuelCost = DashFuelCost * MaxFuel;

		// Rising-edge press (not hold) and race-start lockout check.
		if ( wantsBoost && !_boostHeldLastFrame && CurrentFuel >= fuelCost && Time.Now >= _nextDashTime && Time.Now >= _boostEnabledTime )
		{
			var isDrifting = Car.Drift.IsValid() && Car.Drift.IsDrifting;
			if ( !isDrifting )
			{
				PerformDash( stats, fuelCost );
			}
		}

		_boostHeldLastFrame = wantsBoost;

		// Regen fuel (paused for RegenDelay after the last dash).
		if ( !wantsBoost && Time.Now >= _regenResumeTime )
			CurrentFuel = MathF.Min( MaxFuel, CurrentFuel + RegenRate * Time.Delta );

		// Clear dash visual flag.
		if ( IsDashing && Time.Now >= _dashVisualEndTime )
			IsDashing = false;
	}

	private void PerformDash( Resources.CarStatValues stats, float fuelCost )
	{
		var throttle = Car.Input.Current.Throttle;
		var yaw = Car.Movement.Yaw;
		var direction = Rotation.FromYaw( yaw ).Forward;

		// Reverse dash direction when braking.
		if ( throttle < -0.1f )
			direction = -direction;

		// Boost straight forward only, no sideways component.
		Car.Movement.ApplyImpulse( direction * stats.BoostImpulse, 0f );
		CurrentFuel -= fuelCost;

		IsDashing = true;
		_dashVisualEndTime = Time.Now + 0.2f;
		_nextDashTime = Time.Now + DashCooldown;
		_regenResumeTime = Time.Now + RegenDelay;

		SpawnEffect();
	}

	[Rpc.Broadcast( NetFlags.OwnerOnly )]
	private void SpawnEffect()
	{
		if ( !BoostEffectPrefab.IsValid() )
			return;

		var effect = BoostEffectPrefab.Clone( new CloneConfig
		{
			Parent = GameObject,
			Transform = new Transform( Vector3.Zero ),
			StartEnabled = true
		} );
	}

	protected override void OnDisabled()
	{
		IsDashing = false;
	}
}