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

public class CardRock : Card
{
	private Dictionary<IntVector2, Card> _cardBelowPositions = new();

	public override bool ValidateStartingGridPos()
	{
		if ( GridPos.y > 0 )
			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 > 0 && otherCard.CardType != CardType.Chipmunk && otherCard.CardType != CardType.Spider && otherCard.CardType != CardType.Rock && otherCard.CardType != CardType.Balloon && otherCard.CardType != CardType.Pawn )
			{
				Manager.Instance.SwapCardPositionsNonAsync( this, otherCard );
				break;
			}
		}

		return false;
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		if ( CardType == CardType.Rock && ( eventType == EventType.TurnStart || eventType == EventType.AfterCardsMoved ) )
		{
			return ShouldFall();
		}

		if ( CardType == CardType.Gem && eventType == EventType.Match && Manager.Instance.ChosenCards[1] == this )
			return true;

		return false;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		if( eventType == EventType.TurnStart || eventType == EventType.AfterCardsMoved )
		{
			if ( IsRevealed )
			{
				Manager.Instance.PushEventMessage( this, eventType );
				await Task.DelayRealtime( 300 );
			}

			await Task.DelayRealtime( 300 );

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

				_cardBelowPositions.Clear();

				RemoveCurrentGridPositions();

				await MoveDownward( this, setGridPos: false );
				await MoveDownward( this, setGridPos: true );

				await Task.DelayRealtime( 400 );
			}

			await Task.DelayRealtime( 300 );

			if ( IsRevealed )
				Manager.Instance.PopEventMessage();

			await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
		}
		else if(eventType == EventType.Match)
		{
			Manager.Instance.PushEventMessage( this, eventType );

			await Task.DelayRealtime( 150 );

			Manager.Instance.PlayCardSfxBetween( "gem", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.3f, pitch: Game.Random.Float( 0.9f, 1.1f ) );

			await Task.DelayRealtime( 50 );

			int moneyAmount = 4 + (int)Manager.Instance.Stats[StatType.EarnExtraMoney];

			Manager.Instance.SpawnGainMoneyFloater( moneyAmount, WorldPosition );

			await Manager.Instance.GainMoney( moneyAmount );

			await Task.DelayRealtime( 700 );

			Manager.Instance.PopEventMessage();
		}
	}

	bool ShouldFall()
	{
		var currGridPos = GridPos + new IntVector2( 0, -1 );

		while ( Manager.Instance.IsGridPosInBounds( currGridPos ) )
		{
			var existingCard = Manager.Instance.GetCardAtGridPos( currGridPos );
			if ( existingCard != null && existingCard.CantBeMoved )
				return false;

			if ( Manager.Instance.IsGridPosEmpty( currGridPos ) )
				return true;

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

		return false;
	}

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

		Card cardBelow = _cardBelowPositions.ContainsKey( gridPosBelow ) ? _cardBelowPositions[gridPosBelow] : null;

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

		if(cardBelow != null )
		{
			await MoveDownward(cardBelow, setGridPos);
		}
	}

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

			Manager.Instance.RemoveCardGridPos( card );

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

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