Grid/Cell.cs

Grid cell and tags types for an A* nav grid. CellTags is a copy-on-write tag container safe for single-writer / multi-reader use. Cell implements geometry, connectivity, generation helpers (traces/tests for walkability, steps, clearance), neighbor/jump queries, occupancy checking and debug drawing.

Native Interop
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace GridAStar;

/// <summary>
/// Tags on a grid cell. COPY-ON-WRITE + a reference type: tags are written on the MAIN thread (Door.OccupyCells,
/// NPC.AssignNearbyTags) while the A* pathfinder reads/enumerates them on background worker threads. Mutations
/// build a NEW list and swap the reference, so readers always enumerate a stable (immutable) snapshot - this
/// fixes the "Collection was modified; enumeration operation may not execute" crash in ComputePathInternal.
/// Single-writer (main) / multi-reader (path threads), so no lock is needed; a reference read/write is atomic,
/// and a momentarily stale snapshot is harmless (the NPC re-paths next tick). NOTE: s&box's code whitelist
/// forbids the `volatile` keyword, so we rely on plain reference atomicity rather than volatile visibility.
/// </summary>
public class CellTags
{
	// Shared immutable empty snapshot, returned for reads when `all` is (transiently) null.
	private static readonly List<string> Empty = new();

	// Field initializer (runs for every ctor) + null-guards below: cells are built on background generation
	// threads, so the main thread can momentarily observe the Tags reference before this inner list write is
	// visible. Treating null as empty makes every operation safe regardless.
	private List<string> all = new();

	/// <summary>Current snapshot. Safe to enumerate from any thread; do NOT mutate it directly (use Add/Remove).</summary>
	public List<string> All => all ?? Empty;

	public CellTags()
	{
		all = new List<string>();
	}

	public CellTags( List<string> tags )
	{
		all = tags is null ? new List<string>() : new List<string>( tags );
	}

	public bool Has( string tag ) => (all ?? Empty).Contains( tag );

	public bool Has( params string[] tags )
	{
		var snapshot = all ?? Empty;
		foreach ( string tag in tags )
			if ( !snapshot.Contains( tag ) )
				return false;
		return true;
	}

	public bool Has( List<string> tags )
	{
		var snapshot = all ?? Empty;
		foreach ( string tag in tags )
			if ( snapshot.Contains( tag ) )
				return true;
		return false;
	}

	public bool Has( IEnumerable<string> tags )
	{
		var snapshot = all ?? Empty;
		foreach ( string tag in tags )
			if ( snapshot.Contains( tag ) )
				return true;
		return false;
	}

	public void Add( string tag )
	{
		var current = all;
		if ( current is not null && current.Contains( tag ) )
			return;

		all = current is null ? new List<string> { tag } : new List<string>( current ) { tag };
	}

	public void Remove( string tag )
	{
		var current = all;
		if ( current is null || !current.Contains( tag ) )
			return;

		var copy = new List<string>( current );
		copy.Remove( tag );
		all = copy;
	}

	public void Clear()
	{
		all = new List<string>();
	}
}

public struct CellConnection
{
	public Cell Cell { get; private set; }
	public string ConnectionTag { get; private set; } = string.Empty;

	public CellConnection( Cell cell )
	{
		Cell = cell;
	}

	public CellConnection( Cell cell, string tag )
	{
		Cell = cell;
		ConnectionTag = tag;
	}
}

public partial class Cell : IEquatable<Cell>, IValid
{
	/// <summary>The parent grid</summary>
	public Grid Grid { get; set; }
	public Rotation Rotation => Grid.AxisAligned ? new Rotation() : Grid.Rotation;
	public Vector3 Position { get; set; }
	public IntVector2 GridPosition { get; set; }
	/// <summary>
	/// 0 = Bottom Left, 1 = Bottom Right, 2 = Top Left, 3 = Top Right
	/// </summary>
	public float[] Vertices = new float[4];
	public Vector3 BottomLeft => Position.WithZ( Vertices[0] ) + new Vector3( -Grid.CellSize / 2, -Grid.CellSize / 2, 0f ) * Rotation;
	public Vector3 BottomRight => Position.WithZ( Vertices[1] ) + new Vector3( -Grid.CellSize / 2, Grid.CellSize / 2, 0f ) * Rotation;
	public Vector3 TopLeft => Position.WithZ( Vertices[2] ) + new Vector3( Grid.CellSize / 2, -Grid.CellSize / 2, 0f ) * Rotation;
	public Vector3 TopRight => Position.WithZ( Vertices[3] ) + new Vector3( Grid.CellSize / 2, Grid.CellSize / 2, 0f ) * Rotation;
	public float Height => Vertices.Max() - Vertices.Min();
	public Vector3 Bottom => Position.WithZ( Vertices.Min() );
	public BBox Bounds => new BBox( new Vector3( -Grid.WidthClearance, -Grid.WidthClearance, 0f ), new Vector3( Grid.WidthClearance, Grid.WidthClearance, Grid.HeightClearance ) );
	public BBox WorldBounds => new BBox( (Position + Bounds.Mins).WithZ( Vertices.Min() ), Position + Bounds.Maxs );
	public CellTags Tags { get; private set; }
	public List<AStarNode> CellConnections { get; private set; } = new();
	private List<AStarNode> connectedCells = new();

	public bool Occupied
	{
		get => Tags.Has( "occupied" );
		set
		{
			if ( value )
				Tags.Add( "occupied" );
			else
				Tags.Remove( "occupied" );
		}
	}
	public Component OccupyingEntity { get; set; } = null;
	internal Transform currentOccupyingTransform { get; set; } = Transform.Zero;
	bool IValid.IsValid { get; }

	// --- Generation diagnostics (reset in Level.GenerateGrid, logged after) ---
	public static int DebugRejectCoords;
	public static int DebugRejectClearance;
	public static int DebugCreated;

	/// <summary>
	/// Try to create a new cell with the given position and the max standing angle
	/// </summary>
	public static Cell TryCreate( Grid grid, Vector3 position )
	{
		float[] validCoordinates = new float[4];
		var height = position.z - validCoordinates.Min();

		var coordinatesAndStairs = TraceCoordinates( grid, position, ref validCoordinates );
		if ( !coordinatesAndStairs.Item1 )
		{
			DebugRejectCoords++;
			return null;
		}

		if ( !TestForClearance( grid, position, height ) )
		{
			DebugRejectClearance++;
			return null;
		}

		var cell = new Cell( grid, position, validCoordinates );
		if ( coordinatesAndStairs.Item2 )
			cell.Tags.Add( "step" );

		DebugCreated++;
		return cell;
	}

	//(IsWalkable, IsSteps)
	private static (bool, bool) TraceCoordinates( Grid grid, Vector3 position, ref float[] validCoordinates )
	{
		Vector3[] testCoordinates = new Vector3[4] {
			new Vector3( -grid.CellSize / 2, -grid.CellSize / 2 ) * grid.AxisRotation,
			new Vector3( -grid.CellSize / 2, grid.CellSize / 2 ) * grid.AxisRotation,
			new Vector3( grid.CellSize / 2, -grid.CellSize / 2 ) * grid.AxisRotation,
			new Vector3( grid.CellSize / 2, grid.CellSize / 2 ) * grid.AxisRotation
		};

		var maxHeight = Math.Max( grid.CellSize * MathF.Tan( MathX.DegreeToRadian( grid.StandableAngle ) ), grid.StepSize );

		for ( int i = 0; i < 4; i++ )
		{
			var centerDir = testCoordinates[i].Normal;
			// Scene-System fix: start the corner ray a step-height ABOVE the floor. Starting it ON the surface
			// (legacy did) reports StartedSolid in the Scene physics and rejected ~98% of floor cells.
			var startTestPos = position + testCoordinates[i] + Vector3.Up * grid.StepSize - centerDir * grid.Tolerance;
			var endTestPos = position + testCoordinates[i].WithZ( -maxHeight * 2f ) - centerDir * grid.Tolerance;
			var testTrace = grid.Scene.Trace.Ray( startTestPos, endTestPos )
				.WithGridSettings( grid.Settings );
			var testResult = testTrace.Run();

			if ( testResult.StartedSolid ) return (false, false);
			if ( !testResult.Hit ) return (false, false);
			if ( testResult.HitPosition.z > position.z + grid.StepSize ) return (false, false); // corner significantly higher than the cell centre

			validCoordinates[i] = testResult.HitPosition.z;
			testCoordinates[i] = testResult.HitPosition;
		}

		var orderedByHeight = testCoordinates.OrderBy( x => x.z );
		var lowest = orderedByHeight.First();
		var highest = orderedByHeight.Last();

		if ( IsCliff( grid, lowest, highest ) || IsCliff( grid, lowest, position ) )
			return (false, false);

		return TestForSteps( grid, position, testCoordinates );
	}

	private static bool IsCliff( Grid grid, Vector3 from, Vector3 to )
	{
		var trace = grid.Scene.Trace.Ray( from, to )
			.WithGridSettings( grid.Settings );
		var result = trace.Run();

		// A ray that starts embedded in the surface (Scene physics reports this when from/to sit on the
		// floor) isn't detecting a cliff edge - don't reject the cell for it.
		if ( result.StartedSolid )
			return false;

		if ( result.Hit )
			if ( Vector3.GetAngle( Vector3.Up, result.Normal ) > 90 )
				return true;

		return false;
	}

	private static bool TestForClearance( Grid grid, Vector3 position, float height )
	{
		var clearanceBBox = new BBox( new Vector3( -grid.WidthClearance / 2f, -grid.WidthClearance / 2f, 0f ), new Vector3( grid.WidthClearance / 2f, grid.WidthClearance / 2f, 1f ) );
		var startPos = position + Vector3.Up * grid.HeightClearance;
		var clearanceTrace = grid.Scene.Trace.Box( clearanceBBox, startPos, position + Vector3.Up * grid.StepSize )
			.WithGridSettings( grid.Settings );

		var clearanceResult = clearanceTrace.Run();
		var heightDifference = clearanceResult.EndPosition.z - (position.z - height);

		return heightDifference <= grid.StepSize + height;
	}

	//(IsWalkable, IsSteps)
	private static (bool, bool) TestForSteps( Grid grid, Vector3 position, Vector3[] testCoordinates )
	{
		if ( grid.StepSize <= 0.1f )
			return (true, true);

		var lowestToHighest = testCoordinates
			.OrderBy( x => x.z )
			.ToArray();

		var stepTestMin = TestForStep( grid, lowestToHighest[0], lowestToHighest[3], position, lowestToHighest[0] );

		if ( !stepTestMin.Item1 )
			return (false, stepTestMin.Item2);

		var stepTestMid = TestForStep( grid, lowestToHighest[1], lowestToHighest[3], position, lowestToHighest[1] );

		if ( !stepTestMid.Item1 )
			return (false, stepTestMid.Item2);

		return (true, stepTestMin.Item2 || stepTestMid.Item2);
	}

	//(IsWalkable, IsSteps)
	private static (bool, bool) TestForStep( Grid grid, Vector3 startPosition, Vector3 endPosition, Vector3 highestPosition, Vector3 lowestPosition )
	{
		var stepsTried = 0;
		var maxSteps = (int)Math.Max( (Math.Abs( highestPosition.z - lowestPosition.z ) / (grid.StepSize / 2f)) + 1, 3 );
		var stepDistances = new float[maxSteps];

		if ( highestPosition.z - lowestPosition.z <= grid.StepSize / 2 )
			return (true, false);

		while ( stepsTried < maxSteps )
		{
			var tolerance = 0.01f;
			var stepPositionStart = startPosition + Vector3.Up * (grid.StepSize / 4f + grid.StepSize / 2f * stepsTried + tolerance);
			var stepPositionEnd = endPosition.WithZ( stepPositionStart.z );
			var stepDirection = (stepPositionEnd - stepPositionStart).Normal;
			var stepDistance = stepPositionStart.Distance( stepPositionEnd );
			var stepTrace = grid.Scene.Trace.Ray( stepPositionStart, stepPositionStart + stepDirection * (stepDistance + tolerance * 2f) )
				.Size( new Vector3( grid.StepSize / 2f ) )
				.WithGridSettings( grid.Settings );

			var stepResult = stepTrace.Run();
			var stepAngle = Vector3.GetAngle( Vector3.Up, stepResult.Normal );

			if ( stepsTried == 0 )
				if ( stepResult.EndPosition.Distance( endPosition ) <= tolerance * 3f )
					return (true, false);

			if ( stepResult.Hit && stepAngle > grid.StandableAngle && stepAngle < 89.9f )
				return (false, false);

			if ( stepResult.Hit && stepAngle < grid.StandableAngle )
				return (true, false);

			var distanceFromStart = startPosition.Distance( stepResult.EndPosition.WithZ( startPosition.z ) );

			if ( stepsTried >= 2 )
			{
				var distanceDifference = Math.Abs( distanceFromStart - stepDistances[stepsTried - 2] );

				if ( distanceDifference < tolerance )
					return (false, true);
			}

			stepDistances[stepsTried] = distanceFromStart;
			stepsTried++;
		}

		return (true, true);
	}

	public AStarNode AddConnection( Cell other, string tag = "" )
	{
		var node = new AStarNode( other, new AStarNode( this ), tag == "" ? string.Empty : tag );
		CellConnections.Add( node );
		other.connectedCells.Add( node );
		return node;
	}

	public void RemoveConnection( AStarNode connection )
	{
		CellConnections.Remove( connection );
		connection.Current.connectedCells.Remove( connection );
	}

	public void RemoveConnections( Cell other )
	{
		var foundConnections = CellConnections.Where( x => x.Current == other ).ToList();

		foreach ( var connection in foundConnections )
			RemoveConnection( connection );
	}

	public IEnumerable<AStarNode> GetConnections( string movementTag ) => CellConnections.Where( x => x.MovementTag == movementTag );

	public void SetOccupant( Component entity )
	{
		OccupyingEntity = entity;
		currentOccupyingTransform = entity.WorldTransform;
	}

	public void RemoveOccupant()
	{
		OccupyingEntity = null;
		currentOccupyingTransform = Transform.Zero;
	}

	public bool TestForOccupancy( string tag )
	{
		if ( OccupyingEntity != null && OccupyingEntity.WorldTransform == currentOccupyingTransform ) return Occupied;

		var occupyTrace = Grid.Scene.Trace.Box( Bounds, Position, Position )
			.IgnoreStatic()
			.WithTag( tag );

		var occupyResult = occupyTrace.Run();

		if ( occupyResult.Component != null )
			SetOccupant( occupyResult.Component );

		return occupyResult.Hit;
	}

	public Cell( Grid grid, Vector3 position, float[] vertices, List<string> tags = null )
	{
		Grid = grid;
		Position = position;
		GridPosition = (Position - Grid.WorldBounds.Mins - grid.CellSize / 2).ToIntVector2( grid.CellSize );
		Vertices = vertices;

		if ( tags != null && tags.Count() > 0 )
			Tags = new CellTags( tags );
		else
			Tags = new CellTags();
	}

	public void Delete( bool deleteConnections = true )
	{
		if ( deleteConnections )
		{
			var connections = connectedCells.ToList();
			foreach ( var connectedCell in connections )
				connectedCell.Parent.Current.RemoveConnections( this );
			connectedCells.Clear();
			CellConnections.Clear();
		}

		Grid.CellStacks[GridPosition].Remove( this );
	}

	internal static Dictionary<IntVector2, List<IntVector2>> CompareVertices = new()
	{
		[new IntVector2( -1, -1 )] = new List<IntVector2>() { new IntVector2( 0, 3 ) },
		[new IntVector2( -1, 0 )] = new List<IntVector2>() { new IntVector2( 1, 3 ), new IntVector2( 0, 2 ) },
		[new IntVector2( -1, 1 )] = new List<IntVector2>() { new IntVector2( 1, 2 ) },
		[new IntVector2( 0, -1 )] = new List<IntVector2>() { new IntVector2( 0, 1 ), new IntVector2( 2, 3 ) },
		[new IntVector2( 0, 1 )] = new List<IntVector2>() { new IntVector2( 1, 0 ), new IntVector2( 3, 2 ) },
		[new IntVector2( 1, -1 )] = new List<IntVector2>() { new IntVector2( 2, 1 ) },
		[new IntVector2( 1, 0 )] = new List<IntVector2>() { new IntVector2( 3, 1 ), new IntVector2( 2, 0 ) },
		[new IntVector2( 1, 1 )] = new List<IntVector2>() { new IntVector2( 3, 0 ) },
	};

	public bool IsNeighbour( Cell cell )
	{
		var xDistance = cell.GridPosition.x - GridPosition.x;
		var yDistance = cell.GridPosition.y - GridPosition.y;

		if ( xDistance < -1 || xDistance > 1 || yDistance < -1 || yDistance > 1 ) return false;
		if ( cell == this ) return true;
		if ( xDistance == 0 && yDistance == 0 ) return false;

		var verticesToCompare = CompareVertices[new IntVector2( xDistance, yDistance )];

		foreach ( var comparePair in verticesToCompare )
		{
			var heightDifference = Math.Abs( Vertices[comparePair[0]] - cell.Vertices[comparePair[1]] );
			if ( heightDifference > Grid.StepSize ) return false;
		}

		return true;
	}

	public IEnumerable<Cell> GetNeighbours( bool ignoreHeight = false )
	{
		var height = ignoreHeight ? float.MaxValue : Position.z;

		for ( int y = -1; y <= 1; y++ )
		{
			for ( int x = -1; x <= 1; x++ )
			{
				if ( x == 0 && y == 0 ) continue;

				var cellFound = Grid.GetCell( new IntVector2( GridPosition.x + x, GridPosition.y + y ), height );
				if ( cellFound == null ) continue;

				if ( IsNeighbour( cellFound ) )
					yield return cellFound;
			}
		}
	}

	public Cell GetClosestNeighbour( Vector3 position ) => GetNeighbours().OrderBy( x => x.Position.Distance( position ) ).FirstOrDefault();
	public Cell GetClosestNeighbourAndConnection( Vector3 position ) => GetNeighbourAndConnections().OrderBy( x => x.Current.Position.Distance( position ) ).FirstOrDefault()?.Current ?? null;

	public IEnumerable<AStarNode> GetNeighbourAndConnections( bool ignoreHeight = false ) => GetNeighbourConnections( ignoreHeight ).Concat( CellConnections );

	public IEnumerable<AStarNode> GetNeighbourConnections( bool ignoreHeight = false ) => GetNeighbours( ignoreHeight ).Select( x => new AStarNode( x ) );

	/// <summary>
	/// Return the first cell below spaces where a neighbour is missing
	/// </summary>
	public Cell GetFirstValidDroppable( int minCellDistance = 1, int maxCellsDistance = 3, float maxHeightDistance = GridSettings.DEFAULT_DROP_HEIGHT )
	{
		for ( int y = 0; y <= maxCellsDistance * 2; y++ )
		{
			var spiralY = MathAStar.SpiralPattern( y );
			for ( int x = 0; x <= maxCellsDistance * 2; x++ )
			{
				var spiralX = MathAStar.SpiralPattern( x );
				if ( spiralX == 0 && spiralY == 0 ) continue;
				if ( Math.Abs( spiralX ) <= minCellDistance && Math.Abs( spiralY ) <= minCellDistance ) continue;

				var cellFound = Grid.GetCell( new IntVector2( GridPosition.x + spiralX, GridPosition.y + spiralY ), Position.z );

				if ( cellFound == null ) continue;
				if ( cellFound == this ) continue;
				if ( IsNeighbour( cellFound ) ) continue;

				var verticalDistance = Position.z - cellFound.Position.z;
				if ( verticalDistance > maxHeightDistance ) continue;

				var horizontalDistance = new Vector2( spiralX, spiralY ).Length - 1f;
				if ( verticalDistance < Grid.StepSize * horizontalDistance ) continue;

				if ( Grid.LineOfSight( this, cellFound ) ) continue;

				var clearanceBBox = new BBox( new Vector3( -Grid.WidthClearance / 2f, -Grid.WidthClearance / 2f, 0f ), new Vector3( Grid.WidthClearance / 2f, Grid.WidthClearance / 2f, Grid.HeightClearance - Grid.StepSize ) );
				var horizontalClearanceTrace = Grid.Scene.Trace.Box( clearanceBBox, Position + Vector3.Up * Grid.StepSize, cellFound.Position.WithZ( Position.z + Grid.StepSize ) )
					.WithGridSettings( Grid.Settings )
					.Run();
				if ( horizontalClearanceTrace.Hit ) continue;

				var verticalClearanceTrace = Grid.Scene.Trace.Box( clearanceBBox, cellFound.Position.WithZ( Position.z + Grid.StepSize ), cellFound.Position + Vector3.Up * Grid.StepSize )
					.WithGridSettings( Grid.Settings )
					.Run();
				if ( verticalClearanceTrace.Hit ) continue;

				return cellFound;
			}
		}

		return null;
	}

	public IEnumerable<Cell> GetValidJumpables( JumpDefinition definition, float maxHeightDistance = GridSettings.DEFAULT_DROP_HEIGHT, bool ignoreConnections = false, bool ignoreLOS = false )
	{
		var jumpableCells = new List<Cell>();

		for ( int side = 0; side < definition.SidesToCheck; side++ )
		{
			var directionToCheck = Rotation.FromYaw( definition.AngleOffset + 360 / definition.SidesToCheck * side ).Forward;
			var horizontalVelocity = directionToCheck * definition.HorizontalSpeed;

			var endPosition = Grid.TraceParabola( Position, horizontalVelocity, definition.VerticalSpeed, definition.Gravity, maxHeightDistance );
			var cell = Grid.GetCellInArea( endPosition, Grid.WidthClearance );

			if ( cell == null || cell == this ) continue;

			if ( ignoreLOS || !Grid.IsDirectlyWalkable( this, cell, withConnections: !ignoreConnections ) && (ignoreConnections ? true : !Grid.IsDirectlyWalkable( this, cell, withConnections: false )) )
				if ( ignoreLOS || !jumpableCells.Any( otherCell => Grid.IsDirectlyWalkable( otherCell, cell, withConnections: !ignoreConnections ) ) && (ignoreConnections ? true : !jumpableCells.Any( otherCell => Grid.IsDirectlyWalkable( otherCell, cell, withConnections: false ) )) )
					if ( ignoreLOS || !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: !ignoreConnections ) ) && (ignoreConnections ? true : !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: false ) )) )
					{
						var clearanceBBox = new BBox( new Vector3( -Grid.WidthClearance / 2f, -Grid.WidthClearance / 2f, Grid.StepSize ), new Vector3( Grid.WidthClearance / 2f, Grid.WidthClearance / 2f, Grid.HeightClearance ) );
						var jumpTrace = Grid.Scene.Trace.Box( clearanceBBox, endPosition, cell.Position )
							.WithGridSettings( Grid.Settings )
							.Run();

						if ( !jumpTrace.Hit )
							jumpableCells.Add( cell );
					}

			if ( jumpableCells.Count() >= definition.MaxPerCell )
				break;
		}

		return jumpableCells;
	}

	public Cell GetValidJumpable( JumpDefinition definition, Vector3 directionToCheck, float maxHeightDistance = GridSettings.DEFAULT_DROP_HEIGHT, bool ignoreConnections = false, bool ignoreLOS = false )
	{
		var horizontalVelocity = directionToCheck * definition.HorizontalSpeed;

		var endPosition = Grid.TraceParabola( Position, horizontalVelocity, definition.VerticalSpeed, definition.Gravity, maxHeightDistance );
		var cell = Grid.GetCellInArea( endPosition, Grid.WidthClearance );

		if ( cell == null ) return null;

		if ( ignoreLOS || !Grid.IsDirectlyWalkable( this, cell, withConnections: !ignoreConnections ) && (ignoreConnections ? true : !Grid.IsDirectlyWalkable( this, cell, withConnections: false )) )
			if ( ignoreLOS || !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: !ignoreConnections ) ) && (ignoreConnections ? true : !CellConnections.Any( otherNode => Grid.IsDirectlyWalkable( otherNode.Current, cell, withConnections: false ) )) )
			{
				var clearanceBBox = new BBox( new Vector3( -Grid.WidthClearance / 2f, -Grid.WidthClearance / 2f, Grid.StepSize ), new Vector3( Grid.WidthClearance / 2f, Grid.WidthClearance / 2f, Grid.HeightClearance ) );
				var jumpTrace = Grid.Scene.Trace.Box( clearanceBBox, endPosition, cell.Position )
					.WithGridSettings( Grid.Settings )
					.Run();

				if ( !jumpTrace.Hit )
					return cell;
			}

		return null;
	}

	/// <summary>Draw this cell's outline (debug). Scene-System port routes through Scene.DebugOverlay.</summary>
	public void Draw( Color color, float duration = 0f, bool depthTest = true, bool drawCenter = false, bool drawCross = false, bool drawCoordinates = false )
	{
		var overlay = Grid?.Scene?.DebugOverlay;
		if ( overlay is null ) return;

		overlay.Line( BottomLeft, BottomRight, color, duration, default, !depthTest );
		overlay.Line( BottomRight, TopRight, color, duration, default, !depthTest );
		overlay.Line( TopRight, TopLeft, color, duration, default, !depthTest );
		overlay.Line( TopLeft, BottomLeft, color, duration, default, !depthTest );

		if ( drawCross )
		{
			overlay.Line( BottomLeft, TopRight, color, duration, default, !depthTest );
			overlay.Line( TopLeft, BottomRight, color, duration, default, !depthTest );
		}

		if ( drawCenter )
			overlay.Sphere( new Sphere( Position, 5f ), color, duration, default, !depthTest );
	}

	public void Draw( float duration = 0f, bool depthTest = true, bool drawCenter = false, bool drawCross = false )
		=> Draw( Occupied ? Color.Red : Color.White, duration, depthTest, drawCenter, drawCross );

	public override bool Equals( object obj )
	{
		return Equals( obj as Cell );
	}

	public bool Equals( Cell obj )
	{
		return obj != null && obj.GetHashCode() == this.GetHashCode();
	}

	public override int GetHashCode()
	{
		var gridHash = Grid.GetHashCode();
		var positionHash = Position.GetHashCode();

		return gridHash + positionHash;
	}
}