Player/Car/Gameplay/CarInventory.cs

Component attached to a car that manages the single-slot item and shield state. It stores the currently held PickupDef, handles granting items (authority-only), consuming/activating items on input, spawning activation effects and sounds, shield activation/consumption, and helper logic to find a target car ahead for homing items.

Networking
using System.Text.Json.Serialization;
using Machines.Components;
using Machines.GameModes;
using Machines.Player;
using Machines.Systems;

namespace Machines.Items;

/// <summary>
/// Holds the currently held item and shield state for a car.
/// </summary>
public sealed class CarInventory : Component
{
	[RequireComponent]
	public Car Car { get; private set; }

	/// <summary>
	/// The currently held item (the asset is the identity). Synced for the HUD.
	/// </summary>
	[Property, Sync, JsonIgnore]
	public PickupDef Held { get; set; }

	/// <summary>
	/// Time.Now the active shield expires at (0 = none). Synced for visuals.
	/// </summary>
	[Sync]
	public float ShieldUntil { get; set; }

	/// <summary>
	/// True when an item is held.
	/// </summary>
	public bool HasItem => Held is not null;

	/// <summary>
	/// True while a shield is up.
	/// </summary>
	public bool ShieldActive => Time.Now < ShieldUntil;

	/// <summary>
	/// Car facing yaw (helper for behaviours that spawn relative to the car).
	/// </summary>
	public float Yaw => Car.Movement.IsValid() ? Car.Movement.Yaw : WorldRotation.Yaw();

	private bool _useHeldLastFrame;

	private bool IsAuthority => Car.IsValid() && Car.IsAuthority;

	/// <summary>
	/// Award an item. Single-slot: ignored if one is already held. Authority-only.
	/// </summary>
	public void Grant( PickupDef def )
	{
		if ( !IsAuthority || def is null || HasItem )
			return;

		Held = def;
	}

	/// <summary>
	/// Host-acknowledged pickup: runs on the owner, grants the item and records the stat.
	/// </summary>
	[Rpc.Owner]
	public void GrantPickup( PickupDef def )
	{
		Grant( def );
		GameStats.Increment( "pickups", car: Car );
	}

	/// <summary>
	/// Raise a shield for <paramref name="duration"/> seconds (used by ShieldBehavior).
	/// </summary>
	public void ActivateShield( float duration )
	{
		ShieldUntil = Time.Now + duration;
	}

	/// <summary>
	/// If a shield is up, consume it and return true (the incoming hit is absorbed). Authority-only.
	/// </summary>
	public bool TryAbsorbHit()
	{
		if ( !IsAuthority || !ShieldActive )
			return false;

		ShieldUntil = 0f;
		return true;
	}

	/// <summary>
	/// Nearest valid car ahead in standings for homing; null if leading or no valid target ahead.
	/// </summary>
	public GameObject FindCarAhead()
	{
		var standings = BaseGameMode.Current?.GetComponent<RaceStandings>();
		if ( !standings.IsValid() || Car.Slot < 0 )
			return null;

		var ordered = standings.GetStandings()
			.Where( s => !s.IsGhost )
			.OrderBy( s => s.Position )
			.ToList();

		var myIndex = ordered.FindIndex( s => s.Slot == Car.Slot );
		if ( myIndex < 0 )
			return null;

		// Walk toward the leader and lock onto the first valid target.
		for ( var i = myIndex - 1; i >= 0; i-- )
		{
			var go = TargetableCar( ordered[i] );
			if ( go.IsValid() )
				return go;
		}

		return null;
	}

	/// <summary>
	/// Returns the car GameObject for a standing, or null if finished, respawning, or disabled.
	/// </summary>
	private GameObject TargetableCar( RaceStandings.Standing standing )
	{
		if ( BaseGameMode.Current is RaceMode race && race.GetPlayerState( standing.Slot ).HasFinished )
			return null;

		var car = standing.Car;
		if ( !car.IsValid() || !car.GameObject.Enabled )
			return null;

		if ( car.Respawn.IsValid() && car.Respawn.IsRespawning )
			return null;

		return car.GameObject;
	}

	// Consume input in OnUpdate so taps aren't dropped (CarInput runs before us there).
	protected override void OnUpdate()
	{
		if ( !IsAuthority || !Car.Input.IsValid() )
			return;

		var wantsUse = Car.Input.Current.UseItem;

		if ( wantsUse && !_useHeldLastFrame && HasItem )
		{
			var def = Held;
			if ( Activate( def ) )
			{
				SpawnFx( def );
				Held = null;
			}
		}

		_useHeldLastFrame = wantsUse;
	}

	/// <summary>
	/// Spawn the item prefab and activate its <see cref="IPickupItem"/>. Returns false if activation is vetoed (item is kept).
	/// </summary>
	private bool Activate( PickupDef def )
	{
		if ( def is null || !def.Prefab.IsValid() )
			return false;

		var go = def.Prefab.Clone( new CloneConfig
		{
			Transform = new Transform( WorldPosition, Rotation.FromYaw( Yaw ) ),
			StartEnabled = true,
			Name = def.Prefab.Name
		} );

		var item = go.Components.Get<IPickupItem>();
		if ( item is null || !item.Activate( Car ) )
		{
			go.Destroy();
			return false;
		}

		return true;
	}

	[Rpc.Broadcast( NetFlags.OwnerOnly )]
	private void SpawnFx( PickupDef def )
	{
		if ( def is null )
			return;

		if ( def.ActivateEffect.IsValid() )
		{
			def.ActivateEffect.Clone( new CloneConfig
			{
				Parent = GameObject,
				Transform = new Transform( Vector3.Zero ),
				StartEnabled = true
			} );
		}

		if ( def.ActivateSound.IsValid() )
			Sound.Play( def.ActivateSound, WorldPosition );
	}

	protected override void OnDisabled()
	{
		Held = null;
		ShieldUntil = 0f;
	}
}