Weapons/HandGrenadeWeapon.cs
using Sandbox.Rendering;

public sealed class HandGrenadeWeapon : BaseWeapon
{
	[Property]
	public GameObject Prefab { get; set; }

	[Property]
	public float ThrowPower { get; set; } = 1200f;

	/// <summary>
	/// How long does this explosive last until exploding
	/// </summary>
	[Property]
	public float Lifetime { get; set; } = 3f;

	/// <summary>
	/// Damage radius
	/// </summary>
	[Property]
	public float Radius { get; set; }

	/// <summary>
	/// Max damage, respecting <see cref="DamageFalloff"/>
	/// </summary>
	[Property]
	public float MaxDamage { get; set; } = 125;

	/// <summary>
	/// A falloff curve to dictate damage inflicted to damageables in the area.
	/// </summary>
	[Property]
	public Curve DamageFalloff { get; set; } = new Curve( new Curve.Frame( 1.0f, 1.0f ), new Curve.Frame( 0.0f, 0.0f ) );

	[Sync]
	TimeSince TimeSinceCooked { get; set; }

	[Sync]
	bool IsThrowing { get; set; } = false;

	[Sync]
	bool IsCooking { get; set; } = false;

	[Sync]
	TimeUntil TimeUntilThrown { get; set; }

	public override void OnControl( Player player )
	{
		if ( IsThrowing )
		{
			if ( TimeUntilThrown )
			{
				if ( player.GetAmmoCount( AmmoResource ) < 1 )
				{
					GameObject.Destroy();
					// Delete weapon, switch to best
					SwitchAway();
				}
				else
				{
					IsThrowing = false;
				}
			}

			return;
		}

		if ( Input.Pressed( "Attack1" ) )
		{
			IsCooking = true;
			TimeSinceCooked = 0;

			StartAttack();
		}

		if ( Input.Down( "Attack1" ) && TimeSinceCooked > Lifetime )
		{
			IsCooking = false;
			Explode( WorldPosition );

			if ( !TakeAmmo( 1 ) )
			{
				DestroyGameObject();
			}

			return;
		}

		if ( Input.Released( "Attack1" ) )
		{
			IsCooking = false;
			Throw( player, player.EyeTransform.Rotation.Forward );

			if ( !HasAmmo() )
			{
				SwitchAway();
				GameObject.Destroy();
			}

			StopAttack();
		}
	}

	public override bool IsInUse() => IsCooking;

	public override void OnPlayerDeath( IPlayerEvent.DiedParams args )
	{
		if ( !IsCooking ) return;

		if ( Owner.IsValid() )
		{
			Throw( Owner, Vector3.Down, 0.2f );
		}
	}

	protected override void OnEnabled()
	{
		base.OnEnabled();

		AddShootDelay( 0.5f );
	}

	[Rpc.Host]
	private void TryThrow( Player player, Vector3 startPos, Vector3 direction, float powerScale = 1f )
	{
		if ( !player.IsValid() )
			return;

		var go = Prefab.Clone( startPos );

		var explosive = go.GetComponent<Explosive>();
		explosive.Instigator = player.PlayerData;
		explosive.Lifetime = Lifetime - TimeSinceCooked;
		explosive.MaxDamage = MaxDamage;
		explosive.DamageFalloff = DamageFalloff;
		explosive.Radius = Radius;

		var rb = go.GetComponent<Rigidbody>();

		var baseVelocity = player.GetComponent<PlayerController>().Velocity;
		rb.AngularVelocity = go.WorldRotation.Right * 10f;
		rb.Velocity = baseVelocity + direction * (ThrowPower * powerScale) + Vector3.Up * 100f;

		go.NetworkSpawn();
	}

	public void Throw( Player player, Vector3 direction, float powerScale = 1f )
	{
		if ( !CanShoot() )
			return;

		if ( !TakeAmmo( 1 ) )
		{
			DestroyGameObject();
			// Delete weapon, switch to best
			SwitchAway();
			return;
		}

		var ev = new IWeaponEvent.AttackEvent( ViewModel.IsValid() );
		IWeaponEvent.PostToGameObject( GameObject.Root, x => x.OnAttack( ev ) );

		AddShootDelay( 1f );

		IsThrowing = true;
		TimeUntilThrown = 0.25f;

		var forward = direction;
		var right = player.EyeTransform.Rotation.Right;
		var initialPos = player.EyeTransform.ForwardRay.Position + (forward * 18.0f) + (right * 8.0f);

		initialPos = CheckThrowPosition( player, player.EyeTransform.Position, initialPos );

		TryThrow( player, initialPos, direction, powerScale );

		if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
		{
			new Sandbox.CameraNoise.Punch( new Vector3( Random.Shared.Float( -10, -15 ), Random.Shared.Float( -10, 0 ), 0 ), 1.0f, 3, 0.5f );
			new Sandbox.CameraNoise.Shake( 0.3f, 1.2f );
		}
	}

	private Vector3 CheckThrowPosition( Player player, Vector3 eyePosition, Vector3 grenadePosition )
	{
		var tr = Scene.Trace.Box( BBox.FromPositionAndSize( Vector3.Zero, 8.0f ), eyePosition, grenadePosition )
			.WithoutTags( "trigger", "ragdoll" )
			.IgnoreGameObjectHierarchy( player.GameObject )
			.Run();

		if ( tr.Hit )
		{
			return tr.EndPosition;
		}

		return grenadePosition;
	}

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var gap = 6;
		var len = 4;
		var w = 2;

		Color color = !CanShoot() ? UI.CrosshairInactive : UI.CrosshairActive;

		hud.DrawCircle( center, len, color );
		hud.DrawLine( center + Vector2.Left * (len + gap) * 2, center + Vector2.Left * gap * 2, w, color );
		hud.DrawLine( center - Vector2.Left * (len + gap) * 2, center - Vector2.Left * gap * 2, w, color );

		center += new Vector2( 0, 10 );
		gap -= 1;

		hud.DrawLine( center + Vector2.Left * (len + gap) * 2, center + Vector2.Left * gap * 2, w, color );
		hud.DrawLine( center - Vector2.Left * (len + gap) * 2, center - Vector2.Left * gap * 2, w, color );

		center += new Vector2( 0, 10 );
		gap -= 1;

		hud.DrawLine( center + Vector2.Left * (len + gap) * 2, center + Vector2.Left * gap * 2, w, color );
		hud.DrawLine( center - Vector2.Left * (len + gap) * 2, center - Vector2.Left * gap * 2, w, color );

		center += new Vector2( 0, 10 );
		gap -= 1;

		hud.DrawLine( center + Vector2.Left * (len + gap) * 2, center + Vector2.Left * gap * 2, w, color );
		hud.DrawLine( center - Vector2.Left * (len + gap) * 2, center - Vector2.Left * gap * 2, w, color );
	}

	[Rpc.Broadcast]
	private void CreateEffects()
	{
		if ( Application.IsDedicatedServer ) return;

		Effects.SpawnScorch( WorldPosition, Vector3.Up, GameObject );
		Effects.SpawnExplosion( WorldPosition );
	}

	[Rpc.Host]
	public void Explode( Vector3 pos )
	{
		TagSet tags = [DamageTags.Explosion];

		Damage.Radius( pos, Radius, MaxDamage, tags, GameObject, GameObject, DamageFalloff );
		CreateEffects();
	}
}