Inputs/AttackingInput.cs
using System;

namespace Battlebugs;

public sealed class AttackingInput : Component
{
	public static AttackingInput Instance { get; private set; }

	[Property] GameObject ReticlePrefab { get; set; }

	GameObject Reticle = null;
	Vector3 ReticlePosition = Vector3.Zero;
	Vector3 ReticleOffset = Vector3.Zero;
	public int ReticleState = 0;
	SoundHandle AimingSound = null;

	const float BaseReticleScale = 0.8f;
	const float MinSprayRadius = 48f;

	protected override void OnAwake()
	{
		Instance = this;
		Enabled = false;
	}

	protected override void OnUpdate()
	{
		if ( !BoardManager.Local.IsValid() ) return;

		var tr = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 8000f )
			.WithoutTags( "bug" )
			.Run();

		if ( ReticleState == 0 )
		{
			if ( !ShopPanel.Instance.IsOpen && !PauseMenu.Instance.IsOpen && tr.Hit && tr.GameObject.Components.TryGet<CellComponent>( out var cell ) )
			{
				if ( cell.Board != BoardManager.Local )
				{
					if ( !Reticle.IsValid() )
					{
						if ( ReticlePrefab is not null )
						{
							CreateReticle( tr.HitPosition );
						}
					}
					else
					{
						Reticle.WorldPosition = tr.HitPosition.WithZ( BoardManager.Local.WorldPosition.z ) + Vector3.Up * 4f;
					}
				}
			}
			else if ( Reticle.IsValid() )
			{
				DestroyReticle();
			}
		}
		else if ( Reticle.IsValid() )
		{
			if ( ReticleState == 1 )
			{
				Reticle.WorldPosition = ReticlePosition + Vector3.Forward * MathF.Sin( Time.Now * 5f ) * 72f;
			}
			else if ( ReticleState == 2 )
			{
				Reticle.WorldPosition = ReticlePosition + ReticleOffset + Vector3.Right * MathF.Sin( Time.Now * 5f ) * 72f;
			}
		}

		// Scale the reticle to match the selected weapon's spray radius.
		if ( Reticle.IsValid() )
		{
			UpdateReticleScale();
		}

		if ( Reticle.IsValid() && Input.Pressed( "Attack1" ) && BoardManager.Local.WeaponInventory[BoardManager.Local.SelectedWeapon] != 0 )
		{
			if ( ReticleState == 0 )
			{
				AimingSound?.Stop();
				AimingSound = Sound.Play( "aiming-loop" );
			}
			Sound.Play( "aiming-click" );
			ReticleState++;
			ReticleOffset = Reticle.WorldPosition - ReticlePosition;
			if ( ReticleState < 2 ) ReticlePosition = Reticle.WorldPosition;
			if ( ReticleState == 3 )
			{
				BoardManager.Local.WeaponInventory[BoardManager.Local.SelectedWeapon]--;
				GameManager.Instance.BroadcastFire( BoardManager.Local.Id, BoardManager.Local.SelectedWeapon.ResourcePath, Reticle.WorldPosition );
				DestroyReticle();
			}
		}

		if ( Input.Pressed( "Attack2" ) && ReticleState == 1 )
		{
			Sound.Play( "ui.navigate.back" );
			ReticleState--;
			if ( ReticleState == 0 )
			{
				AimingSound?.Stop();
				AimingSound = null;
			}
		}
	}

	protected override void OnDisabled()
	{
		DestroyReticle();
	}

	void CreateReticle( Vector3 position )
	{
		Reticle = ReticlePrefab.Clone( position );
		UpdateReticleScale();
	}

	void UpdateReticleScale()
	{
		if ( !Reticle.IsValid() || BoardManager.Local?.SelectedWeapon is null ) return;

		var spray = MathF.Max( BoardManager.Local.SelectedWeapon.Spray, MinSprayRadius );
		// The base prefab scale (0.8) represents roughly one cell (~64 units)
		var scale = (spray / 64f) * BaseReticleScale;
		var plane = Reticle.Children.FirstOrDefault();
		if ( plane.IsValid() )
		{
			plane.LocalScale = new Vector3( scale, scale, 1f );
		}
	}

	void DestroyReticle()
	{
		AimingSound?.Stop();
		AimingSound = null;
		Reticle?.Destroy();
		Reticle = null;
		ReticleState = 0;
	}
}