Carryable weapon base class and a TraceAttackInfo record. TraceAttackInfo packages trace hit data. Carryable extends BaseCombatWeapon, integrates with Player owner, handles per-frame updates, input control, equip/holster hooks, camera hooks, RPC TraceAttack to apply damage and force, and inventory switching.
using Sandbox.Rendering;
/// <summary>
/// Info about a trace attack. It's a struct so we can add to it without updating params everywhere.
/// </summary>
/// <param name="Target"></param>
/// <param name="Damage"></param>
/// <param name="Tags"></param>
/// <param name="Position"></param>
/// <param name="Origin"></param>
public record struct TraceAttackInfo( GameObject Target, float Damage, TagSet Tags = null, Vector3 Position = default, Vector3 Origin = default )
{
/// <summary>
/// Constructs a <see cref="TraceAttackInfo"/> from a trace and input damage.
/// </summary>
public static TraceAttackInfo From( SceneTraceResult tr, float damage, TagSet tags = default, bool localise = true )
{
tags ??= new();
if ( localise && tr.Hitbox?.Tags is not null )
{
tags.Add( tr.Hitbox?.Tags );
}
return new TraceAttackInfo( tr.GameObject, damage, tags, tr.HitPosition, tr.StartPosition );
}
}
/// <summary>
/// A carryable is an item that can be carried in a player's inventory, such as a weapon or tool. This can be selected and equipped by the player.
/// The most notable difference between an item and carryable is that carryables have viewmodels and world models and can be selected in the weapon list.
/// </summary>
/// <remarks>
/// The inventory, slotting, view model, world model, magazine and reload plumbing all come from
/// <see cref="BaseCombatWeapon"/>. What lives here is the deathmatch-specific layer on top: our own
/// <see cref="Player"/> owner, the crosshair HUD, camera hooks and the <see cref="TraceAttack"/>
/// damage path.
/// </remarks>
public partial class Carryable : BaseCombatWeapon, IKillIcon
{
/// <summary>
/// The player that owns this. Deliberately hides <see cref="BaseCombatWeapon.Owner"/> (a
/// <see cref="PlayerController"/>) - our game code wants the <see cref="Player"/> that sits
/// alongside it, and reaches the controller through <see cref="Player.Controller"/>.
/// </summary>
public new Player Owner => GetComponentInParent<Player>( true );
/// <summary>
/// The engine draws a weapon's HUD for any holder that isn't a proxy, which on the host includes
/// every bot it simulates - so their crosshairs pile up on the host's screen. Only the human
/// playing on this client gets a HUD; everyone else still needs their body animated.
/// </summary>
protected override void OnUpdate()
{
var player = Owner;
if ( player.IsValid() && !player.IsLocalPlayer )
{
if ( HolderRenderer is { } body && body.IsValid() )
UpdateBodyAnimations( body );
return;
}
base.OnUpdate();
}
/// <summary>
/// Can we switch to this?
/// </summary>
public virtual bool CanSwitch()
{
return true;
}
protected override bool OnCanSwitchTo() => CanSwitch();
/// <summary>
/// The engine pumps this once per frame on the client that owns the inventory, for the deployed
/// item only. Bots are owned by the host but drive themselves through
/// <see cref="PlayerBotController"/>, not input - so keep them out of the input path entirely.
/// </summary>
protected override void OnControl()
{
var player = Owner;
if ( !player.IsValid() || !player.IsLocalPlayer )
return;
if ( !player.Controller.IsValid() )
return;
OnPlayerUpdate( player );
}
/// <summary>
/// Called every update for the owning client while this is deployed.
/// </summary>
public virtual void OnPlayerUpdate( Player player )
{
OnControl( player );
}
/// <summary>
/// Called every update, scoped to the owning player
/// </summary>
/// <param name="player"></param>
public virtual void OnControl( Player player )
{
}
/// <summary>
/// Called when this is pulled out
/// </summary>
public virtual void OnEquipped( Player player )
{
}
/// <summary>
/// Called when this is put away
/// </summary>
public virtual void OnHolstered( Player player )
{
}
protected override void OnEquipped()
{
base.OnEquipped();
// The engine leaves interpolation on, which makes a hand-attached world model swim.
GameObject.Network.Interpolation = false;
if ( Owner.IsValid() )
OnEquipped( Owner );
}
protected override void OnHolstered()
{
base.OnHolstered();
if ( Owner.IsValid() )
OnHolstered( Owner );
}
/// <summary>
/// Called when setting up the camera - use this to apply effects on the camera based on this carriable
/// </summary>
/// <param name="player"></param>
/// <param name="camera"></param>
public virtual void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
{
}
/// <summary>
/// Can directly influence the player's eye angles here
/// </summary>
/// <param name="player"></param>
/// <param name="angles"></param>
public virtual void OnCameraMove( Player player, ref Angles angles )
{
}
/// <summary>
/// Run a trace related attack with some set information.
/// This is targeted to the host who then does things.
/// </summary>
/// <param name="attack"></param>
[Rpc.Host]
public void TraceAttack( TraceAttackInfo attack )
{
if ( !attack.Target.IsValid() )
return;
if ( !Owner.IsValid() )
return;
var damagable = attack.Target.GetComponentInParent<IDamageable>();
if ( damagable is not null )
{
var info = new DamageInfo( attack.Damage, Owner.GameObject, GameObject );
info.Position = attack.Position;
info.Origin = attack.Origin;
info.Tags = attack.Tags;
damagable.OnDamage( info );
}
if ( attack.Target.GetComponentInChildren<Rigidbody>() is var rb && rb.IsValid() )
{
rb.ApplyForce( (attack.Position - attack.Origin) * 1000f );
}
}
/// <summary>
/// Tell the player to switch away from us, to the next best weapon
/// </summary>
public void SwitchAway()
{
// ShouldAvoid keeps GetBestItem from handing us straight back.
Inventory?.SwitchToBest();
}
/// <summary>
/// Is this item currently being used? When true, prevents auto-switching away on item pickup etc.
/// </summary>
public virtual bool IsInUse()
{
return false;
}
/// <summary>
/// Called when the owner dies
/// </summary>
public virtual void OnPlayerDeath( IPlayerEvent.DiedParams args )
{
}
}