Park/GridManager.Regions.cs
using HC3.Terrain;

namespace HC3;

public partial class GridManager : ITerrainEvent
{
	[Property, Hide] public Vector3Int EntranceGridPos { get; private set; }

	[Rpc.Broadcast]
	void BroadcastEntranceMoved( Vector3Int gridPos )
	{
		EntranceGridPos = gridPos;
	}

	void ITerrainEvent.EntranceMoved( Vector3Int gridPos )
	{
		BroadcastEntranceMoved( gridPos );
	}

	public IEnumerable<GridObject> Walkable => _walkableObjects;

	/// <summary>
	/// Assigns every known cell with a region Id.
	/// Every disconnected island forms a new region id, with the entrance at 0.
	/// </summary>
	public void DirtyRegions()
	{
		// TODO: run this on a background thread?

		var pos2d = new Vector2Int( EntranceGridPos );
		if ( !_cells.TryGetValue( pos2d, out GridCell startCell ) )
		{
			return;
		}

		Path startPath = startCell.GetComponents<Path>( EntranceGridPos.z ).FirstOrDefault();
		if ( !startPath.IsValid() )
		{
			return;
		}

		foreach ( var gridObject in _walkableObjects )
			gridObject.RegionId = -1;

		int nextRegionId = 0;

		if ( startCell != null )
			AssignRegionsFrom( startPath.GridObject, ref nextRegionId );

		foreach ( var cell in Walkable.Where( x => x.RegionId == -1 ) )
		{
			AssignRegionsFrom( cell, ref nextRegionId );
		}
	}

	private void AssignRegionsFrom( GridObject current, ref int nextRegionId )
	{
		var queue = new Queue<GridObject>();
		queue.Enqueue( current );

		current.RegionId = nextRegionId;

		while ( queue.Count > 0 )
		{
			var cell = queue.Dequeue();

			Vector3Int cellPos = WorldToGridPosition3D( cell.WorldPosition );

			foreach ( var neighbor in GridNavigation.Instance.GetEnterableNeighbors( cellPos, NavFlags.None )
				.SelectMany( x => Instance.GetCell( new Vector2Int( x ) ).GetComponents<GridObject>( x.z ) )
				.Where( x => x.RegionId == -1 ) )
			{
				neighbor.RegionId = nextRegionId;

				queue.Enqueue( neighbor );
			}
		}

		nextRegionId++;
	}

	public static int GetRegion( Vector3Int pos )
	{
		var pos2d = new Vector2Int( pos.x, pos.y );
		if ( Instance.GetCell( pos2d ) is not { } cell )
		{
			return -1;
		}

		var path = cell.GetComponents<GridObject>( pos.z ).FirstOrDefault();
		return path.IsValid() ? path.RegionId : -1;
	}

	public static bool IsWalkable( Vector3 worldStart, Vector3 worldTarget )
	{
		var startGrid = WorldToGridPosition3D( worldStart );
		var targetGrid = WorldToGridPosition3D( worldTarget );

		return IsWalkable( startGrid, targetGrid );
	}

	public static bool IsWalkable( Vector3Int startGrid, Vector3Int targetGrid )
	{
		var start2d = new Vector2Int( startGrid.x, startGrid.y );
		if ( Instance.GetCell( start2d ) is not { } startCell )
		{
			return false;
		}

		var target2d = new Vector2Int( targetGrid.x, targetGrid.y );
		if ( Instance.GetCell( target2d ) is not { } goalCell )
		{
			return false;
		}

		var startPath = startCell.GetComponents<GridObject>( startGrid.z ).FirstOrDefault();
		var targetPath = goalCell.GetComponents<GridObject>( targetGrid.z ).FirstOrDefault();

		if ( !startPath.IsValid() || !targetPath.IsValid() )
		{
			return false;
		}

		return startPath.RegionId == targetPath.RegionId && startPath.RegionId != -1;
	}
}