cards/CardRabbit.cs
using Sandbox;
using System.Threading.Tasks;
using static Sandbox.Physics.CollisionRules;

public class CardRabbit : Card
{
	public override bool IsAlive => true;
	
	public override bool ValidateStartingGridPos()
	{
		var rabbits = Manager.Instance.Cards.Where( x => x.CardType == CardType.Rabbit ).ToList();

		var rabbit0 = rabbits[0];
		var rabbit1 = rabbits[1];

		int farthestDist = GetFarthestDistance( rabbit0, rabbit1 );

		if ( rabbits.Count < 2 || farthestDist > 3 )
			return true;

		int NUM_TRIES = 100;
		for ( int i = 0; i < NUM_TRIES; i++ )
		{
			var newGridPos = Manager.Instance.GetRandomGridPos( except: rabbit0.GridPos );
			var otherCard = Manager.Instance.GetCardAtGridPos( newGridPos );

			if ( GetFarthestDistance( otherCard, rabbit1 ) > 3 && otherCard.CardType != CardType.Map )
			{
				Manager.Instance.SwapCardPositionsNonAsync( rabbit0, otherCard );
				break;
			}
		}

		if ( !Manager.IsNearby( rabbit0.GridPos, rabbit1.GridPos ) )
			return true;

		NUM_TRIES = 100;
		for ( int i = 0; i < NUM_TRIES; i++ )
		{
			var newGridPos = Manager.Instance.GetRandomGridPos( except: rabbit0.GridPos );
			var otherCard = Manager.Instance.GetCardAtGridPos( newGridPos );

			if ( !Manager.IsNearby( newGridPos, rabbit1.GridPos ) && otherCard.CardType != CardType.Map )
			{
				Manager.Instance.SwapCardPositionsNonAsync( rabbit0, otherCard );
				break;
			}
		}

		return false;
	}

	int GetFarthestDistance(Card card0, Card card1)
	{
		return Math.Max(Math.Abs( card0.GridPos.x - card1.GridPos.x ), Math.Abs( card0.GridPos.y - card1.GridPos.y ));
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		return eventType == EventType.TurnStart && Game.Random.Float(0f, 1f) < 0.5f;
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		var otherRabbits = Manager.Instance.Cards.Where(x => x.CardType == CardType.Rabbit && x != this).ToList();
		if ( otherRabbits.Count == 0 )
			return;
		
		var match = otherRabbits.First();

		if ( Manager.IsAdjacent( GridPos, match.GridPos ) )
			return;

		var path = Manager.Instance.GetPathTo( GridPos, match.GridPos );

		if ( path.Count <= 1 )
			return;

		//Manager.Instance.PushEventMessage( this, eventType );

		await Task.DelayRealtime( 150 );

		IntVector2 targetGridPos = path.FirstOrDefault();

		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( 300 );

		//Manager.Instance.PopEventMessage();

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