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

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

	private Dictionary<IntVector2, Card> _cardAbovePositions = new();

	public override bool ValidateStartingGridPos()
	{
		if ( GridPos.y < Manager.Instance.GridHeight - 1 )
			return true;

		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 ( newGridPos.y < Manager.Instance.GridHeight - 1 && otherCard.CardType != CardType.Balloon && otherCard.CardType != CardType.Rock && otherCard.CardType != CardType.Pawn )
			{
				Manager.Instance.SwapCardPositionsNonAsync( this, otherCard );
				break;
			}
		}

		return false;
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		if ( eventType == EventType.TurnStart )
		{
			foreach( var card in Manager.Instance.GetNearbyCards(GridPos))
			{
				if ( CanRise( card ) )
					return true;
			}
		}

		return false;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		List<Card> potentialCards = new();

		foreach ( var card in Manager.Instance.GetNearbyCards( GridPos, adjacentOnly: true ) )
		{
			if ( CanRise( card ) )
				potentialCards.Add( card );
		}

		if ( potentialCards.Count == 0 )
			return;

		var cardToLift = potentialCards[Game.Random.Int(0, potentialCards.Count - 1)];

		await Task.DelayRealtime( 500 );

		Manager.Instance.PlayCardSfx( "card_move", cardToLift, volume: 1.1f, pitch: Game.Random.Float( 0.85f, 1.15f ) );

		_cardAbovePositions.Clear();

		RemoveCurrentGridPositions( cardToLift );

		await MoveUpward( cardToLift, setGridPos: false );
		await MoveUpward( cardToLift, setGridPos: true );

		await Task.DelayRealtime( 400 );

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

	bool CanRise(Card card)
	{
		// don't lift card directly below
		if ( card.GridPos.y == GridPos.y - 1 )
			return false;

		var currGridPos = card.GridPos + new IntVector2( 0, 1 );

		while ( Manager.Instance.IsGridPosInBounds( currGridPos ) )
		{
			if ( Manager.Instance.IsGridPosEmpty( currGridPos ) )
				return true;

			currGridPos += new IntVector2( 0, 1 );
		}

		return false;
	}

	async Task MoveUpward(Card card, bool setGridPos)
	{
		var gridPosAbove = card.GridPos + new IntVector2( 0, 1 );

		Card cardAbove = _cardAbovePositions.ContainsKey( gridPosAbove ) ? _cardAbovePositions[gridPosAbove] : null;

		if( setGridPos )
		{
			await Manager.Instance.SetCardGridPos( card, gridPosAbove );
			card.IsMovementControlled = false;
		}
		else
		{
			card.MoveToPos( Manager.GetCardPos( gridPosAbove ), 0.3f, EasingType.SineOut );
		}

		if(cardAbove != null )
		{
			await MoveUpward(cardAbove, setGridPos);
		}
	}

	void RemoveCurrentGridPositions(Card card)
	{
		var currGridPos = card.GridPos;
		while ( Manager.Instance.IsGridPosInBounds( currGridPos ) )
		{
			var currCard = Manager.Instance.GetCardAtGridPos( currGridPos );
			_cardAbovePositions.Add( currGridPos, currCard );

			Manager.Instance.RemoveCardGridPos( currCard );

			currGridPos += new IntVector2( 0, 1 );
			var cardAbove = Manager.Instance.GetCardAtGridPos( currGridPos );

			if ( cardAbove == null )
				break;
		}
	}
}