PlayerInventory component for a player. Manages the player's carryable items and ammo, spawn loadout, pickups, auto-switch logic, dropping items into a coffin on death, and forwards player event hooks to carried items.
/// <summary>
/// The player's weapon list. <see cref="BaseInventoryComponent"/> owns the slotting, switching, the
/// reserve ammo pool and the host-authoritative networking - this adds the deathmatch policy on top:
/// spawn loadout, pickup notices, auto-switch preferences and dropping everything into a coffin.
/// </summary>
public sealed class PlayerInventory : BaseInventoryComponent, IPlayerEvent
{
[RequireComponent]
public Player Player { get; set; }
/// <summary>
/// The deployed carryable, or null. Passive items are never deployed so this is always a weapon.
/// </summary>
public Carryable Current => ActiveItem as Carryable;
/// <summary>
/// Everything in the inventory that can actually be selected and held.
/// </summary>
public List<Carryable> Carryables => Items.OfType<Carryable>().ToList();
public void GiveDefaultWeapons()
{
// Don't run any pickup notices when spawning in
using var _ = Player.NoNoticeScope();
Pickup( "weapons/crowbar/crowbar.prefab" );
// Pickup( "weapons/hands/hands.prefab" );
// Pickup( "weapons/camera/camera.prefab" );
Pickup( "weapons/glock/glock.prefab" );
GiveAmmo( ResourceLibrary.Get<BaseAmmoResource>( "weapons/ammo/9mm.ammo" ), 100 );
if ( GameSettings.CheatMode )
{
GiveAll();
}
}
public void GiveAll()
{
Pickup( "weapons/mp5/mp5.prefab" );
Pickup( "weapons/gaussgun/gauss.prefab" );
Pickup( "weapons/python/python.prefab" );
Pickup( "weapons/handgrenade/hand_grenade.prefab" );
Pickup( "weapons/tripmine/tripmine.prefab" );
Pickup( "weapons/satchelcharge/satchelcharge.prefab" );
Pickup( "weapons/rpg/rpg.prefab" );
Pickup( "weapons/crossbow/crossbow.prefab" );
Pickup( "weapons/shotgun/shotgun.prefab" );
Pickup( "weapons/ratgun/rat_gun.prefab" );
Pickup( "weapons/gluongun/gluon_gun.prefab" );
Pickup( "weapons/hornetgun/hornetgun.prefab" );
// Only our own ammo types - BaseAmmoResource is shared with the base addon, which ships its
// own .ammo assets that nothing here fires.
foreach ( var ammo in ResourceLibrary.GetAll<BaseAmmoResource>( "weapons/ammo" ) )
{
GiveAmmo( ammo, 100 );
}
}
public bool HasWeapon<T>() where T : Carryable => Carryables.OfType<T>().Any();
public T GetWeapon<T>() where T : Carryable => Carryables.OfType<T>().FirstOrDefault();
public bool HasWeapon( GameObject prefab )
{
var carry = prefab.GetComponent<Carryable>( true );
if ( carry is null )
return false;
return Carryables.Any( x => x.GetType() == carry.GetType() );
}
/// <summary>
/// Switch to a carryable. Kept for readability at the call sites - the engine's
/// <see cref="BaseInventoryComponent.Switch"/> does the work.
/// </summary>
public void SwitchWeapon( Carryable weapon ) => Switch( weapon );
public Carryable GetBestWeapon() => Carryables.OrderByDescending( x => x.Value ).FirstOrDefault();
public Carryable GetBestWeaponHolstered() => GetBestItem() as Carryable;
/// <summary>
/// Give reserve ammo and optionally show the player a pickup notice for it.
/// </summary>
public int GiveAmmo( BaseAmmoResource type, int amount, bool notice )
{
var gained = GiveAmmo( type, amount );
if ( notice && gained > 0 )
Player.ShowNotice( $"{type.Title} x {gained}" );
return gained;
}
/// <summary>
/// An empty gun is worth switching to only when there's nothing better - and a weapon mid-use
/// (a primed grenade, a charging gauss shot) must never be yanked out of the player's hands.
/// </summary>
protected override bool ShouldAutoSwitchTo( BaseInventoryItem item )
{
if ( !item.IsValid() )
return false;
if ( !ActiveItem.IsValid() )
return true;
if ( !GamePreferences.AutoSwitch )
return false;
if ( Current.IsValid() && Current.IsInUse() )
return false;
return base.ShouldAutoSwitchTo( item );
}
protected override void OnItemAdded( BaseInventoryItem item )
{
base.OnItemAdded( item );
IPlayerEvent.PostToGameObject( Player.GameObject, e => e.OnPickup( item ) );
// Only PickupWorldItem auto-switches in the engine - a code-driven Add doesn't, so without
// this a player would spawn into their loadout holding nothing.
if ( !ActiveItem.IsValid() )
SwitchToBest();
NotifyItemPickup( item );
}
[Rpc.Owner]
private void NotifyItemPickup( BaseInventoryItem item )
{
if ( !item.IsValid() )
return;
// Auto-switch is a client-side preference, so the owning client makes the call and asks the
// host to apply it - deciding it host-side would use the host's preference for everyone.
if ( ShouldAutoSwitchTo( item ) )
Switch( item );
if ( Player.IsValid() && Player.IsLocalPlayer )
ILocalPlayerEvent.Post( e => e.OnPickup( item ) );
}
public void DropCoffin()
{
if ( !Networking.IsHost )
return;
var go = GameObject.Clone( "items/coffin/coffin.prefab" );
go.Name = $"Coffin for {GameObject.Name}";
go.WorldPosition = Player.EyeTransform.Position;
go.WorldRotation = Rotation.LookAt( Player.EyeTransform.Forward.WithZ( 0 ), Vector3.Up );
var coffin = go.GetComponent<Coffin>();
Assert.True( coffin.IsValid(), "Coffin not on coffin prefab" );
// The reserve pool is private to the inventory, so hand the coffin a snapshot of what we're
// actually holding rather than the dictionary itself.
coffin.AmmoCounts = new();
foreach ( var ammo in ResourceLibrary.GetAll<BaseAmmoResource>() )
{
var count = GetAmmo( ammo );
if ( count > 0 )
coffin.AmmoCounts[ammo] = count;
}
if ( go.GetComponent<Rigidbody>() is { } rb )
{
rb.Velocity = Player.Controller.Velocity + (Player.EyeTransform.Backward * 128);
}
go.NetworkSpawn( true, null );
var coffinInventory = go.GetComponent<BaseInventoryComponent>();
// Transfer all items to the coffin
foreach ( var item in Items.ToArray() )
{
if ( coffinInventory.IsValid() )
{
Transfer( item, coffinInventory );
}
else
{
Remove( item );
}
}
}
// IPlayerEvent implementation
void IPlayerEvent.OnSpawned() => GiveDefaultWeapons();
void IPlayerEvent.OnDied( IPlayerEvent.DiedParams args )
{
foreach ( var item in Items )
{
switch ( item )
{
case Carryable carryable:
carryable.OnPlayerDeath( args );
break;
case Item passive:
passive.OnPlayerDeath( args );
break;
}
}
}
void IPlayerEvent.OnPickup( BaseInventoryItem item )
{
if ( item is BaseWeapon weapon && weapon.IsSelfAmmo && weapon.AmmoResource.IsValid() )
Player.ShowNotice( $"{weapon.AmmoResource.Title} x {weapon.StartingAmmo}" );
else
Player.ShowNotice( item.DisplayName );
}
void IPlayerEvent.OnCameraMove( ref Angles angles )
{
if ( Current.IsValid() )
Current.OnCameraMove( Player, ref angles );
}
void IPlayerEvent.OnCameraPostSetup( Sandbox.CameraComponent camera )
{
if ( Current.IsValid() )
Current.OnCameraSetup( Player, camera );
}
}