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.
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;
}
}