Weapons/M4a1/M4a1Weapon.cs

Weapon component for an M4A1 that adds an underbarrel grenade launcher on secondary fire. It handles seeding grenade reserve ammo, consuming secondary ammo, spawning a grenade prefab, setting its explosive attacker, applying initial velocity, adding a physics filter to avoid colliding with the weapon, and network-spawning the grenade. Also draws a simple crosshair.

NetworkingNative Interop
using Sandbox.Rendering;

/// <summary>
/// M4 with an underbarrel grenade launcher on secondary fire. Grenades feed from the secondary
/// magazine (Clip2), topped back up from its reserve when reloading.
/// </summary>
public sealed class M4a1Weapon : BaseBulletWeapon
{
	/// <summary>
	/// The grenade the launcher fires.
	/// </summary>
	[Property, Feature( "Grenade Launcher" )] public GameObject GrenadePrefab { get; set; }

	/// <summary>
	/// Launch speed of the grenade.
	/// </summary>
	[Property, Feature( "Grenade Launcher" )] public float GrenadeSpeed { get; set; } = 1500f;

	/// <summary>
	/// Reserve grenades granted on first pickup - reloads top the launcher's magazine up from these.
	/// </summary>
	[Property, Feature( "Grenade Launcher" )] public int StartingGrenades { get; set; } = 6;

	protected override void OnAdded( Sandbox.BaseInventoryComponent inventory )
	{
		// Seed the grenade reserve once, like the bullet reserve.
		if ( StartingGrenades > 0 && SecondaryAmmoType is not null && !inventory.HasAmmo( SecondaryAmmoType ) )
			inventory.GiveAmmo( SecondaryAmmoType, StartingGrenades );

		base.OnAdded( inventory );
	}

	public override void SecondaryAttack()
	{
		if ( !TakeSecondaryAmmo() )
			return;

		ShootEffects();

		// The attack runs once on the owner - LaunchGrenade is a host RPC, so the grenade spawns
		// exactly once, on the host.
		LaunchGrenade( GetMuzzleTransform().Position + AimRay.Forward * 16f, AimRay.Forward );
	}

	/// <summary>
	/// Spawns and launches the grenade with the host's permission.
	/// </summary>
	[Rpc.Host]
	void LaunchGrenade( Vector3 start, Vector3 direction )
	{
		if ( !GrenadePrefab.IsValid() )
			return;

		var go = GrenadePrefab.Clone( start );

		var explosive = go.GetOrAddComponent<TimedExplosive>();
		explosive.Attacker = Attacker;

		if ( go.GetComponent<Rigidbody>() is { } rb )
			rb.Velocity = direction * GrenadeSpeed + Vector3.Up * 100f;

		// Don't collide with the weapon it launched from
		var filter = go.AddComponent<PhysicsFilter>();
		filter.Body = GameObject;

		go.NetworkSpawn();
	}

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var gap = 16 + SpreadBloom * 32;
		var len = 12;
		var w = 2f;

		var color = !HasPrimaryAmmo() || IsReloading || NextPrimaryFire > 0 ? CrosshairNoShoot : CrosshairCanShoot;

		hud.SetBlendMode( BlendMode.Lighten );
		hud.DrawLine( center + Vector2.Left * (len + gap), center + Vector2.Left * gap, w, color );
		hud.DrawLine( center - Vector2.Left * (len + gap), center - Vector2.Left * gap, w, color );
		hud.DrawLine( center + Vector2.Up * (len + gap), center + Vector2.Up * gap, w, color );
		hud.DrawLine( center - Vector2.Up * (len + gap), center - Vector2.Up * gap, w, color );
	}
}