cards/CardChipmunk.cs
using Sandbox;
using System.Threading.Tasks;

public class CardChipmunk : Card
{
	public override bool IsAlive => true;

	public override bool ValidateStartingGridPos()
	{
		foreach(var card in Manager.Instance.GetNearbyCards(GridPos, adjacentOnly: true))
		{
			if ( card.CardType == CardType.Tree )
			{
				int NUM_TRIES = 100;
				for ( int i = 0; i < NUM_TRIES; i++ )
				{
					var newGridPos = Manager.Instance.GetRandomGridPos( except: GridPos );
					var otherCard = Manager.Instance.GetCardAtGridPos( newGridPos );

					if ( otherCard.CardType != CardType.Chipmunk && otherCard.CardType != CardType.Spider )
					{
						Manager.Instance.SwapCardPositionsNonAsync( this, otherCard );
						break;
					}
				}

				return false;
			}
		}

		return true;
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		if( eventType == EventType.TurnStart )
		{
			var trees = Manager.Instance.Cards.Where( x => x.CardType == CardType.Tree ).ToList();
			if ( trees.Count == 0 )
				return false;

			foreach(var tree in trees)
			{
				if ( Manager.IsAdjacent( GridPos, tree.GridPos ) )
					return false;
			}

			return true;
		}

		return false;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		var trees = Manager.Instance.Cards.Where( x => x.CardType == CardType.Tree ).ToList();
		if ( trees.Count == 0 )
			return;

		foreach ( var tree in trees )
		{
			if ( Manager.IsAdjacent( GridPos, tree.GridPos ) )
				return;
		}

		List<IntVector2> validGridPositions = new();

		IntVector2 currGridPos;
		foreach ( var tree in trees )
		{
			currGridPos = tree.GridPos + new IntVector2( -1, 0 );
			if( IsGridPosValid(currGridPos) && !validGridPositions.Contains( currGridPos ) )
				validGridPositions.Add( currGridPos );

			currGridPos = tree.GridPos + new IntVector2( 1, 0 );
			if ( IsGridPosValid( currGridPos ) && !validGridPositions.Contains( currGridPos ) )
				validGridPositions.Add( currGridPos );

			currGridPos = tree.GridPos + new IntVector2( 0, -1 );
			if ( IsGridPosValid( currGridPos ) && !validGridPositions.Contains( currGridPos ) )
				validGridPositions.Add( currGridPos );

			currGridPos = tree.GridPos + new IntVector2( 0, 1 );
			if ( IsGridPosValid( currGridPos ) && !validGridPositions.Contains( currGridPos ) )
				validGridPositions.Add( currGridPos );
		}

		if ( validGridPositions.Count == 0 )
			return;

		validGridPositions.Shuffle();
		validGridPositions = validGridPositions.OrderBy( x => (GridPos - x).ManhattanLength ).ToList();
		var destination = validGridPositions.FirstOrDefault();

		var path = GetPathTo( GridPos, destination );
		if ( path == null || path.Count == 0 )
			return;

		IntVector2 targetGridPos = path.FirstOrDefault();

		await Task.DelayRealtime( 150 );

		var targetCard = Manager.Instance.GetCardAtGridPos( targetGridPos );

		if(targetCard != null)
			Manager.Instance.PlayCardSfxBetween( "card_move", this, targetCard, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 1.15f ) );
		else
			Manager.Instance.PlayCardSfx( "card_move", this, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 1.15f ) );

		this.MoveToPos( this.LocalPosition.WithZ( Game.Random.Float( 70f, 90f ) ), 0.3f, EasingType.SineOut );
		targetCard?.MoveToPos( targetCard.LocalPosition.WithZ( Game.Random.Float( 70f, 90f ) ), 0.25f, EasingType.SineOut );

		await Task.DelayRealtime( 250 );

		this.MoveToPos( Manager.GetCardPos( targetGridPos ).WithZ( this.LocalPosition.z ), 0.45f, EasingType.SineInOut );
		targetCard?.MoveToPos( Manager.GetCardPos( this.GridPos ).WithZ( targetCard.LocalPosition.z ), 0.45f, EasingType.SineInOut );

		await Task.DelayRealtime( 450 );

		this.MoveToPos( this.LocalPosition.WithZ( Globals.CARD_DEFAULT_HEIGHT + Globals.CARD_ADD_HEIGHT_REVEALED_OR_HOVERED ), 0.2f, EasingType.SineOut );
		targetCard?.MoveToPos( targetCard.LocalPosition.WithZ( Globals.CARD_DEFAULT_HEIGHT ), 0.2f, EasingType.SineOut );

		await Task.DelayRealtime( 200 );

		if ( targetCard != null )
		{
			await Manager.Instance.SwapCardPositions( this, targetCard );
		}
		else
		{
			Manager.Instance.RemoveCardGridPos( this );
			await Manager.Instance.SetCardGridPos( this, targetGridPos );
		}

		this.IsMovementControlled = false;
		if ( targetCard != null )
			targetCard.IsMovementControlled = false;

		await Task.DelayRealtime( 200 );

		await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
	}

	bool IsGridPosValid( IntVector2 gridPos )
	{
		if ( !Manager.Instance.IsGridPosInBounds( gridPos ) )
			return false;

		return !ContainsChipmunk( gridPos );
	}

	bool ContainsChipmunk( IntVector2 gridPos )
	{
		var card = Manager.Instance.GetCardAtGridPos( gridPos );
		return card != null && card.CardType == CardType.Chipmunk;
	}

	bool ContainsTree( IntVector2 gridPos )
	{
		var card = Manager.Instance.GetCardAtGridPos( gridPos );
		return card != null && card.CardType == CardType.Tree;
	}

	private List<IntVector2> _gridPath;
	private List<IntVector2> _walkable;

	public List<IntVector2> GetPathTo( IntVector2 a, IntVector2 b )
	{
		if ( _gridPath == null )
			_gridPath = new List<IntVector2>();
		else
			_gridPath.Clear();

		_gridPath.Clear();

		if ( (a - b).ManhattanLength <= 1 )
		{
			_gridPath.Add( b );
			return _gridPath;
		}

		List<IntVector2> tempPath = new List<IntVector2>();
		if ( Utils.AStar<IntVector2>( a, b, tempPath, GetEdges, GetHScoreFromGridPosToGridPos ) )
		{
			_gridPath.AddRange( tempPath );

			// remove start pos
			_gridPath.RemoveAt( 0 );
		}

		return _gridPath;
	}

	public List<IntVector2> GetWalkableAdjacentGridPositions( IntVector2 start )
	{
		if ( _walkable == null )
			_walkable = new List<IntVector2>();
		else
			_walkable.Clear();

		IntVector2 left = start + new IntVector2( -1, 0 );
		if ( Manager.Instance.IsGridPosInBounds( left ) && !ContainsChipmunk(left) && !ContainsTree(left) )
			_walkable.Add( left );

		IntVector2 right = start + new IntVector2( 1, 0 );
		if ( Manager.Instance.IsGridPosInBounds( right ) && !ContainsChipmunk( right ) && !ContainsTree( right ) )
			_walkable.Add( right );

		IntVector2 down = start + new IntVector2( 0, -1 );
		if ( Manager.Instance.IsGridPosInBounds( down ) && !ContainsChipmunk( down ) && !ContainsTree( down ) )
			_walkable.Add( down );

		IntVector2 up = start + new IntVector2( 0, 1 );
		if ( Manager.Instance.IsGridPosInBounds( up ) && !ContainsChipmunk( up ) && !ContainsTree( up ) )
			_walkable.Add( up );

		return _walkable;
	}


	static float GetHScoreFromGridPosToGridPos( IntVector2 a, IntVector2 b )
	{
		return (b - a).ManhattanLength;
	}

	IEnumerable<AStarEdge<IntVector2>> GetEdges( IntVector2 start )
	{
		var walkable = GetWalkableAdjacentGridPositions( start );
		return walkable.Select( gridPos => Utils.Edge( gridPos, GetCostToMoveFromGridPosToAdjacentGridPos( start, gridPos ) ) );
	}

	float GetCostToMoveFromGridPosToAdjacentGridPos( IntVector2 a, IntVector2 b )
	{
		return 1f;
	}
}