BoardManager.cs
using System;

namespace Battlebugs;

public sealed class BoardManager : Component
{
	// Static Variables
	public static BoardManager Local
	{
		get
		{
			if ( !_local.IsValid() )
			{
				_local = Game.ActiveScene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.IsOwner );
			}
			return _local;
		}
	}
	private static BoardManager _local;
	public static BoardManager Opponent => Game.ActiveScene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.Owner != Connection.Local );

	// Properties
	[Property] public int GridSize { get; set; } = 64;
	[Property] public int Width { get; set; } = 10;
	[Property] public int Height { get; set; } = 10;
	[Property, Group( "References" )] public GameObject CameraPosition { get; set; }

	// Networked Variables
	[Sync] public int Coins { get; set; } = 100;
	[Sync] public bool IsReady { get; set; } = false;
	[Sync] public NetList<BugReference> BugReferences { get; set; } = new();
	[Sync] public int CoinsSpent { get; set; } = 0;
	[Sync] public int BugsKilled { get; set; } = 0;

	// Public Variables
	public WeaponResource SelectedWeapon = null;
	public Dictionary<BugResource, int> BugInventory = new();
	public Dictionary<WeaponResource, int> WeaponInventory = new();
	public int MaxPlaceableSegments => BugInventory.Where( x => x.Value > 0 ).OrderBy( x => x.Key.SegmentCount ).LastOrDefault().Key?.SegmentCount ?? 0;
	TimeSince timeSinceTurnStart = 0;

	protected override void OnStart()
	{
		if ( IsProxy ) return;

		InitBoard();
		ResetBugInventory();
		ResetWeaponInventory();

		if ( Network.Owner is null )
		{
			SetupBoardRandomly();
			IsReady = true;
		}
	}

	protected override void OnFixedUpdate()
	{
		if ( GameManager.Instance.CurrentPlayer != this ) timeSinceTurnStart = 0;
		if ( Network.Owner is null && timeSinceTurnStart > 2.5f && GameManager.Instance.CurrentPlayer == this && GameManager.Instance.State == GameState.Playing && GameManager.Instance.IsFiring )
		{
			AttackRandomly();
		}
	}

	void InitBoard()
	{
		Vector3 startingPosition = WorldPosition + new Vector3( -(Width * GridSize) / 2f + GridSize / 2f, -(Height * GridSize) / 2f + GridSize / 2f, 0 );
		for ( int x = 0; x < Width; x++ )
		{
			for ( int y = 0; y < Height; y++ )
			{
				var cellObj = GameManager.Instance.CellPrefab.Clone( startingPosition + new Vector3( x * GridSize, y * GridSize, 0 ) );
				var cell = cellObj.Components.Get<CellComponent>();
				var index = x + y * (Width + 1);
				cell.Init( this, new Vector2( x, y ), index );
				cellObj.SetParent( GameObject );
				cellObj.Network.SetOrphanedMode( NetworkOrphaned.ClearOwner );
				cellObj.NetworkSpawn( Network.Owner );
			}
		}
	}

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

		var size = new Vector3( Width * GridSize, Height * GridSize, 0 );
		var bounds = new BBox( size / 2f, -size / 2f );
		Gizmo.Draw.LineBBox( bounds );
	}

	public void ClearAllBugs( bool playSound = true )
	{
		if ( playSound ) Sound.Play( "clear-all-bugs" );
		var segments = Scene.GetAllComponents<BugSegment>();
		foreach ( var segment in segments )
		{
			if ( segment.Network.OwnerId != Network.OwnerId ) continue;
			segment.Cell.IsOccupied = false;
			segment.GameObject.Destroy();
		}
		var cells = Components.GetAll<CellComponent>( FindMode.InChildren );
		foreach ( var cell in cells )
		{
			cell.IsOccupied = false;
			cell.BroadcastUpdateHighlight();
		}
		ResetBugInventory();
	}

	public void ToggleReady()
	{
		if ( IsProxy ) return;
		IsReady = !IsReady;
	}

	void ResetBugInventory()
	{
		var allBugs = ResourceLibrary.GetAll<BugResource>();
		BugInventory.Clear();
		foreach ( var bug in allBugs )
		{
			BugInventory[bug] = bug.StartingAmount;
		}
	}

	[Rpc.Owner]
	public void AttackRandomly()
	{
		var opponentBoard = Scene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId );
		if ( opponentBoard is null ) return;

		var opponentPos = opponentBoard.WorldPosition;
		var halfWidth = (Width * GridSize) / 2f;
		var halfHeight = (Height * GridSize) / 2f;

		Vector3 targetPosition;

		// Use cell state to decide where to shoot. Cells that have been hit and still
		// have a living bug (IsHit && IsOccupied) are high-value targets — shoot near them.
		// Otherwise fire at a random cell that hasn't been hit yet to explore the board.
		// Never target cells that are already cleared (WasOccupied = bug dead).
		var opponentCells = opponentBoard.Components.GetAll<CellComponent>( FindMode.InChildren ).ToList();

		var activeBugCells = opponentCells.Where( x => x.IsHit && x.IsOccupied ).ToList();
		var unexploredCells = opponentCells.Where( x => !x.IsHit ).ToList();

		if ( activeBugCells.Count > 0 )
		{
			// Target a known living bug cell with some scatter.
			var target = activeBugCells.OrderBy( x => Random.Shared.Float() ).First();
			targetPosition = target.WorldPosition + (Vector3.Random.WithZ( 0 ) * 48f);
		}
		else if ( unexploredCells.Count > 0 )
		{
			// Shoot at a random unexplored cell.
			var target = unexploredCells.OrderBy( x => Random.Shared.Float() ).First();
			targetPosition = target.WorldPosition;
		}
		else
		{
			// Fallback: random board position.
			targetPosition = opponentPos + new Vector3(
				Random.Shared.Float( -halfWidth, halfWidth ),
				Random.Shared.Float( -halfHeight, halfHeight ),
				0
			);
		}

		targetPosition = new Vector3(
			Math.Clamp( targetPosition.x, opponentPos.x - halfWidth, opponentPos.x + halfWidth ),
			Math.Clamp( targetPosition.y, opponentPos.y - halfHeight, opponentPos.y + halfHeight ),
			0
		);

		var weapon = WeaponInventory.OrderBy( x => Random.Shared.Float() ).FirstOrDefault( x => x.Value != 0 ).Key;

		// CPU spends coins to restock weapons when running low on purchasable ammo.
		if ( Network.Owner is null )
		{
			CpuPurchaseWeapons();
		}

		SelectedWeapon = weapon;
		WeaponInventory[SelectedWeapon]--;
		GameManager.Instance.BroadcastFire( Id, SelectedWeapon.ResourcePath, targetPosition );
	}

	void ResetWeaponInventory()
	{
		var allWeapons = ResourceLibrary.GetAll<WeaponResource>();
		WeaponInventory.Clear();
		foreach ( var weapon in allWeapons )
		{
			WeaponInventory[weapon] = weapon.StartingAmount;
			if ( weapon.StartingAmount < 0 ) SelectedWeapon = weapon;
		}
	}

	/// <summary>
	/// CPU spends coins to buy a random affordable weapon when purchasable ammo is low.
	/// </summary>
	void CpuPurchaseWeapons()
	{
		// Count total non-unlimited ammo remaining (unlimited = negative StartingAmount).
		var purchasableAmmo = WeaponInventory
			.Where( x => x.Key.Cost > 0 )
			.Sum( x => x.Value );

		// Only buy when running low on special ammo.
		if ( purchasableAmmo > 2 ) return;

		// Pick a random weapon the CPU can afford, preferring cheaper ones slightly.
		var affordable = WeaponInventory.Keys
			.Where( x => x.Cost > 0 && Coins >= x.Cost )
			.OrderBy( x => x.Cost )
			.ThenBy( x => Random.Shared.Float() )
			.ToList();

		if ( affordable.Count == 0 ) return;

		// Buy one random affordable weapon.
		var toBuy = affordable.OrderBy( x => Random.Shared.Float() ).First();
		PurchaseWeapon( toBuy );
	}

	public float GetHealthPercent()
	{
		var segments = Scene.GetAllComponents<BugSegment>().Where( x => x.Network.OwnerId == Network.OwnerId );
		var totalSegments = segments.Count();
		var totalHealth = segments.Sum( x => x.Health );
		var totalMaxHealth = BugReferences.Sum( x => ResourceLibrary.Get<BugResource>( x.ResourcePath ).StartingHealth * x.ObjectIds.Count );
		return (float)totalHealth / totalMaxHealth;
	}

	public float GetScorePercent()
	{
		if ( GameManager.Instance.State < GameState.Playing ) return 0.5f;
		var myScore = GetHealthPercent();
		var opponentScore = Scene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId ).GetHealthPercent();
		return myScore / (myScore + opponentScore);

	}

	[Rpc.Owner]
	public void SaveBugReferences()
	{
		BugReferences.Clear();

		// Compose the list of bugs and their individual ids
		var references = new List<BugReference>();
		var segments = Scene.GetAllComponents<BugSegment>();
		foreach ( var segment in segments )
		{
			if ( segment.Network.OwnerId != Network.OwnerId ) continue;
			var existingRef = references.FirstOrDefault( x => x.BugId == segment.GameObject.Name );
			if ( !string.IsNullOrEmpty( existingRef.ResourcePath ) )
			{
				existingRef.Add( segment.GameObject.Id );
			}
			else
			{
				var reference = new BugReference( segment.BugPath, segment.GameObject.Name );
				reference.Add( segment.GameObject.Id );
				references.Add( reference );
			}
		}

		// Add entries to the NetList
		foreach ( var reference in references )
		{
			BugReferences.Add( reference );
		}
	}

	public void PurchaseWeapon( WeaponResource weapon )
	{
		Coins -= weapon.Cost;
		WeaponInventory[weapon]++;
		Sandbox.Services.Stats.Increment( "coins_spent", weapon.Cost );
	}

	[Rpc.Owner]
	public void GiveCoins( int amount )
	{
		Coins += amount;
		CoinsSpent += amount;
		Sandbox.Services.Stats.Increment( "coins_earned", amount );
	}

	[Rpc.Owner]
	public void IncrementBugsKilled()
	{
		BugsKilled++;
		Sandbox.Services.Stats.Increment( "bugs_killed", 1 );
	}

	public void IncrementDamageDealt( float damage )
	{
		Sandbox.Services.Stats.Increment( "damage_dealt", (int)damage );
	}

	[Rpc.Owner]
	public void GiveCellCoins( Vector3 position )
	{
		GiveCoins( 5 );
		GameManager.Instance.SpawnCoins( position, 1 );
		HintPanel.Instance.CellCoinNotification();
	}

	public void SetupBoardRandomly()
	{
		ClearAllBugs( false );
		int attempts = 0;
		foreach ( var entry in BugInventory )
		{
			while ( BugInventory[entry.Key] > 0 )
			{
				while ( !TryPlaceRandomBug( entry.Key ) )
				{
					attempts++;
					if ( attempts > 100 )
					{
						SetupBoardRandomly();
						return;
					}
				}
			}
		}
	}

	bool TryPlaceRandomBug( BugResource bug )
	{
		int attempts = 0;

		var startingCell = Components.GetAll<CellComponent>().Where( x => !x.IsOccupied && x.GameObject.Root == GameObject ).OrderBy( x => Guid.NewGuid() ).FirstOrDefault();
		if ( startingCell is null ) return false;

		var cells = new List<CellComponent> { startingCell };

		while ( cells.Count < bug.SegmentCount )
		{
			var cell = cells.Last();
			var neighbors = cell.GetNeighbors().Where( x => !x.IsOccupied && !cells.Contains( x ) && x.GameObject.Root == GameObject ).ToList();
			if ( neighbors.Count == 0 )
			{
				attempts++;
				if ( attempts > 100 ) return false;
				cells = new List<CellComponent> { startingCell };
				continue;
			}
			var nextCell = neighbors.OrderBy( x => Random.Shared.Float() ).First();
			cells.Add( nextCell );
		}

		GameManager.Instance.CreateBug( this, PlacementInput.Instance.GetPlacementData( cells, bug ), Network.Owner is null );
		return true;
	}

	public struct BugReference
	{
		public string ResourcePath { get; set; }
		public string BugId { get; set; }
		public List<string> ObjectIds { get; set; }

		private BugResource _bug;

		public BugReference( string resourcePath, string bugId )
		{
			ResourcePath = resourcePath;
			BugId = bugId;
			ObjectIds = new List<string>();
		}

		public void Add( Guid objectId )
		{
			ObjectIds.Add( objectId.ToString() );
		}

		public BugResource GetBug()
		{
			if ( _bug is null ) _bug = ResourceLibrary.Get<BugResource>( ResourcePath );
			return _bug;
		}
	}

}