cards/CardWizard.cs
using System.Threading.Tasks;

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

	public static int NumWizards { get; set; }

	public int WizardNum { get; set; }

	public override void Init( CardType cardType )
	{
		base.Init( cardType );

		HP = GetMaxHP( CardType );
		WizardNum = CardWizard.NumWizards++;
	}

	public override bool ShouldHandleEvent( EventType eventType )
	{
		if ( eventType == EventType.TurnStart )
		{
			var turnVal = Manager.Instance.Stats[StatType.TurnNum] % 4 < 2 ? 0 : 1;
			return (Manager.Instance.Stats[StatType.TurnNum] % 3 == 2) && (turnVal == WizardNum % 2) && Manager.Instance.Cards.Count > 2;
		}

		return (eventType == EventType.LevelStartBoss && CardTypeID == 0) || (eventType == EventType.HurtCards && Manager.Instance.ChosenCards[0] == this);
	}

	public override async Task HandleEventAsync( EventType eventType )
	{
		if ( eventType == EventType.LevelStartBoss )
		{
			var camera = Scene.Camera;
			var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height * 0.65f, 0f ) );
			var tr = Scene.Trace.Ray( ray, 10000f ).Run();

			Manager.Instance.SpawnFloaterImage(
				pos: tr.EndPosition,
				filename: Card.GetIconFilename( CardType.Wizard ),
				lifetime: 1f,
				velocity: new Vector2( 0f, 100f ),
				deceleration: 0.5f
			);

			Manager.Instance.SpawnFloaterText(
				pos: tr.EndPosition + new Vector3( 0f, -70f, 0f ),
				text: $"Final Boss",
				emojiText: "",
				lifetime: 1f,
				color: new Color( 1f, 1f, 1f ),
				velocity: new Vector2( 0f, 100f ),
				deceleration: 0.5f,
				fontSize: 45f
			);

			Manager.Instance.SpawnFloaterText(
				pos: tr.EndPosition + new Vector3( 0f, -86f, 0f ),
				text: $"{Card.GetName( CardType.Wizard )}",
				emojiText: "",
				lifetime: 1f,
				color: new Color( 0.6f, 0.2f, 1f ),
				velocity: new Vector2( 0f, 100f ),
				deceleration: 0.5f,
				fontSize: 60f
			);

			var hpText = "";
			for ( int i = 0; i < Card.GetMaxHP( CardType.Wizard ); i++ )
				hpText += "❤️";

			Manager.Instance.SpawnFloaterText(
				pos: tr.EndPosition + new Vector3( 0f, -103f, 0f ),
				text: $"",
				emojiText: hpText,
				lifetime: 1f,
				color: new Color( 1f, 1f, 1f ),
				velocity: new Vector2( 0f, 100f ),
				deceleration: 0.5f,
				fontSize: 35f
			);

			Manager.Instance.PlaySfxCenter( "wizard_laugh", volume: 1f, pitch: Game.Random.Float( 0.9f, 0.95f ) );

			await Task.DelayRealtime( 875 );
		}
		else if (eventType == EventType.TurnStart)
		{
			Manager.Instance.PushEventMessage( this, eventType );

			await Task.DelayRealtime( 200 );

			Manager.Instance.PlayCardSfx( "wizard_laugh", this, volume: 1f, pitch: Game.Random.Float( 0.97f, 1.03f ) );

			await Manager.Instance.RevealCard( this );

			await Task.DelayRealtime( 400 );

			MoveToPos( Manager.GetCardPos( GridPos ).WithZ( 100f ), 0.3f, EasingType.SineOut );

			await Task.DelayRealtime( 300 );

			Manager.Instance.PlayCardSfx( "wizard_spell_0", this, volume: 1.3f, pitch: Game.Random.Float( 0.85f, 0.95f ) );

			// choose other cards
			int numOtherCards = Math.Min( 5 - HP, Manager.Instance.Cards.Count - 1 );

			List<Card> cardsToShuffle = new() { this };

			for ( int i = 0; i < numOtherCards; i++ )
			{
				var card = Manager.Instance.GetRandomCard( except: cardsToShuffle );
				cardsToShuffle.Add( card );
			}

			// move other cards to self
			int cardNum = 0;
			foreach ( var card in cardsToShuffle )
			{
				if ( card == this )
					continue;

				card.MoveToPos( Manager.GetCardPos( GridPos ).WithZ( 50f - cardNum * 0.1f ), 0.7f, EasingType.SineInOut );
				cardNum++;
			}

			await Task.DelayRealtime( 850 );

			Manager.Instance.HideCard( this );
			Manager.Instance.PlayCardSfx( "card_flip", this, volume: 0.9f, pitch: Game.Random.Float( 0.65f, 0.75f ) );

			await Task.DelayRealtime( 200 );

			MoveToPos( Manager.GetCardPos( GridPos ).WithZ( Game.Random.Float( 45f, 50f ) ), 0.3f, EasingType.SineOut );

			await Task.DelayRealtime( 400 );

			List<IntVector2> newGridPositions = new();
			foreach ( var card in cardsToShuffle )
				newGridPositions.Add( card.GridPos );
			newGridPositions.Shuffle();

			foreach ( var card in cardsToShuffle )
				Manager.Instance.RemoveCardGridPos( card );

			// move self and others to new positions
			int count = 0;
			foreach ( Card card in cardsToShuffle )
			{
				card.MoveToPos( Manager.GetCardPos( newGridPositions[count] ), Game.Random.Float( 0.4f, 0.8f ), EasingType.QuadInOut );
				count++;
			}

			Manager.Instance.PlayCardSfx( "wizard_spell_1", this, volume: 1f, pitch: Game.Random.Float( 1.13f, 1.18f ) );

			await Task.DelayRealtime( 900 );

			count = 0;
			foreach ( Card card in cardsToShuffle )
			{
				await Manager.Instance.SetCardGridPos( card, newGridPositions[count] );
				card.IsMovementControlled = false;
				count++;
			}

			await Task.DelayRealtime( 200 );

			Manager.Instance.PopEventMessage();

			await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
		}
		else if(eventType == EventType.HurtCards)
		{
			if(HP > 0)
			{
				Manager.Instance.PushEventMessage( this, eventType );

				await Task.DelayRealtime( 800 );

				var wizard0 = Manager.Instance.ChosenCards[0];
				var wizard1 = Manager.Instance.ChosenCards[1];

				List<Card> respawnedCards = new();

				if ( Manager.Instance.CardPairsMatched.Count > 0 )
				{
					var cardTypeToRespawn = Manager.Instance.CardPairsMatched.Keys.ElementAt( Game.Random.Int( 0, Manager.Instance.CardPairsMatched.Count - 1 ) );

					Manager.Instance.CardPairsMatched[cardTypeToRespawn]--;
					if ( Manager.Instance.CardPairsMatched[cardTypeToRespawn] == 0 )
						Manager.Instance.CardPairsMatched.Remove( cardTypeToRespawn );

					Manager.Instance.CardTypePanelHash++;

					var emptyGridPositions = Manager.Instance.GetRandomEmptyGridPositions( 2 );

					respawnedCards.AddRange( RespawnCardType( cardTypeToRespawn, emptyGridPositions[0], emptyGridPositions[1], offset: 0f ) );
				}

				for ( int i = 0; i < 2; i++ )
				{
					var wizard = Manager.Instance.ChosenCards[i];
					wizard.MoveToPos( Manager.GetCardPos( wizard.GridPos ).WithZ( 100f ), 0.3f, EasingType.SineOut );
				}

				await Task.DelayRealtime( 300 );

				List<Card> cardsToShuffle = new() { wizard0, wizard1 };

				if ( respawnedCards.Count > 0 )
				{
					Manager.Instance.PlayCardSfxBetween( "wizard_spell_0", wizard0, wizard1, volume: 1.3f, pitch: Game.Random.Float( 0.65f, 0.75f ) );

					await Task.DelayRealtime( 1300 );

					foreach ( var card in respawnedCards )
						Manager.Instance.HideCard( card );

					Manager.Instance.PlayCardSfxBetween( "card_flip", wizard0, wizard1, volume: 0.7f, pitch: Game.Random.Float( 0.65f, 0.75f ) );

					await Task.DelayRealtime( 800 );

					foreach ( var card in respawnedCards )
						cardsToShuffle.Add( card );
				}

				// non-respawned cards to shuffle
				Manager.Instance.PlayCardSfx( "wizard_spell_0", this, volume: 1.3f, pitch: Game.Random.Float( 0.85f, 0.95f ) );

				int numOtherCards = 5 - HP;
				for ( int i = 0; i < Math.Min(numOtherCards, Manager.Instance.Cards.Count - 2); i++ )
				{
					var card = Manager.Instance.GetRandomCard( except: cardsToShuffle );
					if(card != null)
						cardsToShuffle.Add( card );
				}

				// move non-respawned cards cards to wizards
				int cardNum = 0;
				foreach ( var card in cardsToShuffle )
				{
					if ( card == wizard0 || card == wizard1 )
						continue;

					card.MoveToPos( Manager.GetCardPos( (cardNum % 2 == 0 ? wizard0 : wizard1).GridPos ).WithZ( 50f - cardNum * 0.1f ), 0.7f, EasingType.SineInOut );
					cardNum++;
				}

				await Task.DelayRealtime( 850 );

				Manager.Instance.HideCard( wizard0 );
				Manager.Instance.HideCard( wizard1 );

				Manager.Instance.PlayCardSfxBetween( "card_flip", wizard0, wizard1, volume: 0.9f, pitch: Game.Random.Float( 0.65f, 0.75f ) );

				wizard0.MoveToPos( Manager.GetCardPos( wizard0.GridPos ).WithZ( Game.Random.Float( 45f, 50f ) ), 0.3f, EasingType.SineOut );
				wizard1.MoveToPos( Manager.GetCardPos( wizard1.GridPos ).WithZ( Game.Random.Float( 45f, 50f ) ), 0.3f, EasingType.SineOut );

				await Task.DelayRealtime( 600 );

				List<IntVector2> newGridPositions = new();
				foreach ( var card in cardsToShuffle )
					newGridPositions.Add( card.GridPos );
				newGridPositions.Shuffle();

				foreach ( var card in cardsToShuffle )
					Manager.Instance.RemoveCardGridPos( card );

				int count = 0;
				foreach ( Card card in cardsToShuffle )
				{
					card.MoveToPos( Manager.GetCardPos( newGridPositions[count] ), Game.Random.Float( 0.4f, 0.8f ), EasingType.QuadInOut );
					count++;
				}

				Manager.Instance.PlayCardSfxBetween( "wizard_spell_1", wizard0, wizard1, volume: 1f, pitch: Game.Random.Float( 1.13f, 1.18f ) );

				await Task.DelayRealtime( 900 );

				count = 0;
				foreach ( Card card in cardsToShuffle )
				{
					await Manager.Instance.SetCardGridPos( card, newGridPositions[count] );
					card.IsMovementControlled = false;
					count++;
				}

				await Task.DelayRealtime( 200 );

				Manager.Instance.PopEventMessage();

				await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
			}
			else
			{
				await Task.DelayRealtime( 800 );

				// todo: death sfx
			}
		}
	}

	List<Card> RespawnCardType(CardType cardType, IntVector2 gridPos0, IntVector2 gridPos1, float offset)
	{
		List<Card> cards = new();

		for ( int i = 0; i < 2; i++ )
		{
			var wizard = Manager.Instance.ChosenCards[i];
			var card = Manager.Instance.CreateCard( i == 0 ? gridPos0 : gridPos1, cardType, cardTypeID: i, createOutline: false );

			var randomVec2 = Utils.GetRandomVector();
			card.LocalPosition = (wizard.LocalPosition + new Vector3( randomVec2.x, randomVec2.y, 0f ) * 300f).WithZ( wizard.LocalPosition.z - 10f );
			card.SetRevealedInstant();

			card.MoveToPos( Manager.GetCardPos( wizard.GridPos ).WithZ( 50f - i * 0.1f - offset ), 2f, EasingType.SineOut );

			cards.Add( card );
		}

		return cards;
	}

	public override string GetEventText( EventType eventType )
	{
		if(eventType == EventType.TurnStart)
		{
			return "⏩Every 3 turns: shuffle some cards";
		}
		else
		{
			return "💔Damage Wizard: respawn some cards and shuffle them";
		}
	}

	public override void PlayHurtSfx( Card card0, Card card1 )
	{
		Manager.Instance.PlayCardSfxBetween( "wizard_hurt", card0, card1, volume: 2.6f, pitch: Utils.Map(HP, GetMaxHP(CardType), 0, 1.2f, 0.7f));
	}
}