cards/Card.cs
using Microsoft.VisualBasic;
using Sandbox;
using Sandbox.UI;
using System.Runtime;
using System.Threading.Tasks;

public enum CardType { None, Apple, Bomb, Rocket, Telephone, Trumpet, Pawn, Balloon, Spider, Umbrella, UmbrellaClosed, Juggler, Coin, Vampire, Maracas, Microwave, Raccoon, Rock, Bear, Candle, 
	CrystalBall, Snake, Rose, RoseWilted, Bell, Ant, Twins, Compass, Antenna, Wizard, AncientScroll, JumpingSpider, Bento, Ninja, Flashlight, Bodybuilder, Helicopter, FlyingMoney,
	Syringe, Cellphone, Crab, Magnet, Stethoscope, UFO, Teacher, Tractor, FortuneCookie, Dolphin, Detective, Genie, Potion, Mirror, FaxMachine, Dice, Coffee, Tree, Chipmunk,
	Dancer, Wine, King, Police, Ogre, Clown, MoneyBag, Bank, Steak, MagicWand, Carrot, Caterpillar, Butterfly, Tornado, Map, Cockroach, Broccoli, Donut, Cheese, CheeseMoldy, GuideDog, Taco, Burrito,
	Skunk, CarePackage, Cash, BirthdayCake, Gem, Pickaxe, Mountain, DiamondRing, Rabbit, RedEnvelope,
}

// StockMarket, StockMarketDown, Key, AlarmClock, ATM

public class Card : Component, IEventHandler
{
	public GameObject Model { get; set; }
	public ModelRenderer ModelRenderer { get; set; }
	public GameObject IconObj { get; set; }
	public GameObject IconBackObj { get; set; }
	public bool IsFlagShown { get; set; }

	public float CardIconScale { get; set; } = 1f;

	public bool IsHovered { get; set; }
	public bool IsRevealed { get; set; }
	public bool IsSpawning { get; set; }

	public int NumTimesRevealed { get; set; }
	public int NumTimesChosen { get; set; }

	public CardType CardType { get; private set; }
	public CardType OriginalCardType { get; private set; }

	public virtual bool IsFoodOrBeverage => false;
	public virtual bool IsAlive => false;
	public virtual bool CantBeMoved => false;

	public IntVector2 GridPos { get; set; }

	public bool IsShaking { get; private set; }
	private TimeSince _timeSinceShakeStart; // todo: replace with timer? this can become negative somehow
	private float _shakeTime;
	private EasingType _shakeEasingType;
	public Vector2 ShakeOffset { get; set; }
	private float _shakeStrength;

	public bool IsMovementControlled { get; set; }
	private Vector3 _moveStartPos;
	private Vector3 _moveTargetPos;
	private float _moveTime;
	private TimeSince _timeSinceMoveStart;
	private EasingType _moveEasingType;
	private bool _moveRemoveControlAfter;

	public int HP { get; set; }

	public bool IsLocked { get; set; }
	public int LockTurnsRemaining { get; set; }
	public bool WasJustLocked { get; set; }

	public int CardTypeID { get; set; }

	public float IconOpacity { get; set; }
	public bool IsExploding;
	private TimeSince _timeSinceStartExploding;
	private float _explodeTime;
	private EasingType _explodeEasingType;
	private Vector3 _explodeStartPos;
	private EasingType _explodeShakeEasingType;

	public bool IsScaling;
	private TimeSince _timeSinceScale;
	private float _scaleTime;
	private float _scaleAmount;
	private EasingType _scaleEasingType;

	public bool AreRevealMarksShown { get; set; }

	protected override void OnAwake()
	{
		base.OnAwake();

	}

	public virtual void Init(CardType cardType)
	{
		CardType = OriginalCardType = cardType;

		Model = GameObject.Children.Find( x => x.Name == "model" );
		ModelRenderer = Model.Components.Get<ModelRenderer>();

		IconOpacity = 1f;

		IconObj = GameObject.Children.Find( x => x.Name == "icon" );
		var cardIcon = IconObj.Components.Get<CardIcon>( includeDisabled: true );
		cardIcon.Card = this;

		IconBackObj = GameObject.Children.Find( x => x.Name == "icon_back" );
		var cardIconBack = IconBackObj.Components.Get<CardIconBack>( includeDisabled: true );
		cardIconBack.Card = this;

		SetMaterial( cardType );
	}

	public void SetMaterial(CardType cardType)
	{
		var matFilename = GetMaterialFilename( cardType );
		if ( !string.IsNullOrEmpty( matFilename ) )
			ModelRenderer.MaterialOverride = Material.Load( matFilename );
	}

	public void SetCardType(CardType newType)
	{
		CardType = newType;
		SetMaterial( newType );
	}

	protected override void OnUpdate()
	{
		//if ( Input.Down( "Duck" ) )
		//{
		//	var emoji = Card.GetIconEmoji( CardType );
		//	if ( string.IsNullOrEmpty( emoji ) )
		//	{
		//		Gizmo.Draw.Color = Color.Black.WithAlpha( 1f );
		//		Gizmo.Draw.Text( $"{CardType}", new global::Transform( WorldPosition + new Vector3( -0.5f, -0.5f, -1f ) ), size: 24f );

		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( $"{CardType}", new global::Transform( WorldPosition ), size: 24f );
		//	}
		//	else
		//	{
		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( emoji, new global::Transform( WorldPosition ), size: 60f );
		//	}

		//	if ( IsMovementControlled )
		//	{
		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( "➡️", new global::Transform( WorldPosition + new Vector3( -15.5f, -19f, -1f ) ), size: 30f );
		//	}

		//	if ( IsLocked )
		//	{
		//		Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//		Gizmo.Draw.Text( "🔒", new global::Transform( WorldPosition + new Vector3( 15.5f, -19f, -1f ) ), size: 30f );

		//		Gizmo.Draw.Color = Color.Red.WithAlpha( 1f );
		//		Gizmo.Draw.Text( $"{LockTurnsRemaining}", new global::Transform( WorldPosition + new Vector3( 15.5f, -21f, 1f ) ), size: 20f );
		//	}

		//	Gizmo.Draw.Color = Color.Black.WithAlpha( 1f );
		//	Gizmo.Draw.Text( $"ID: {CardTypeID}", new global::Transform( WorldPosition + new Vector3( -13.5f, 19f, -1f ) + new Vector3( -0.5f, -0.5f, -1f ) ), size: 16 );

		//	Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
		//	Gizmo.Draw.Text( $"ID: {CardTypeID}", new global::Transform( WorldPosition + new Vector3( -13.5f, 19f, -1f ) ), size: 16 );
		//}

		HandleMoving();
		HandleShaking();

		if(IsExploding)
		{
			HandleExploding();
			return;
		}

		if ( IsScaling )
		{
			HandleScaling();
		}
		
		//if(!IsScaling && !IsExploding)
		//{
		//	Transform.LocalScale = new Vector3( Utils.DynamicEaseTo( Transform.LocalScale.x, IsHovered ? 1.05f : 1f, 0.3f, Time.Delta ) );
		//}

		if( IsSpawning )
		{
			// do nothing
		}
		else if( IsMovementControlled )
		{
			LocalPosition += new Vector3( ShakeOffset.x, ShakeOffset.y, 0f ) * Time.Delta * 10f;
		}
		else
		{
			var targetPos = Manager.GetCardPos( GridPos );

			if ( IsShaking )
				targetPos += new Vector3( ShakeOffset.x, ShakeOffset.y, 0f );

			var height = 10f + ((IsRevealed || IsHovered) ? Globals.CARD_ADD_HEIGHT_REVEALED_OR_HOVERED : 0f) + ((IsRevealed && IsHovered) ? Globals.CARD_ADD_HEIGHT_REVEALED_AND_HOVERED : 0f);
			var zPos = MathX.Lerp( LocalPosition.z, height, Time.Delta * 20f );

			LocalPosition = Vector3.Lerp( LocalPosition, targetPos, Time.Delta * 10f ).WithZ( zPos );
		}

		var targetRot = Rotation.FromPitch( IsRevealed ? 0f : 180f );
		WorldRotation = Rotation.Lerp( WorldRotation, targetRot, Time.Delta * 15f );
	}

	public void MoveToPos( Vector3 pos, float time, EasingType easingType = EasingType.Linear, bool removeControlAfter = false)
	{
		//Log.Info( $"Card - MoveToPos: {pos}" );

		IsMovementControlled = true;
		_moveStartPos = LocalPosition;
		_moveTargetPos = pos;
		_moveTime = time;
		_timeSinceMoveStart = 0f;
		_moveEasingType = easingType;
		_moveRemoveControlAfter = removeControlAfter;
	}

	void HandleMoving()
	{
		if ( !IsMovementControlled )
			return;

		if(_timeSinceMoveStart > _moveTime)
		{
			LocalPosition = _moveTargetPos;

			if( _moveRemoveControlAfter )
				IsMovementControlled = false;
		}
		else 
		{
			LocalPosition = Vector3.Lerp( _moveStartPos, _moveTargetPos, Utils.Map( _timeSinceMoveStart, 0f, _moveTime, 0f, 1f, _moveEasingType ) );
		}
	}

	protected void HandleShaking()
	{
		if ( IsShaking )
		{
			if ( _timeSinceShakeStart > _shakeTime )
				IsShaking = false;
			else
				ShakeOffset = Utils.GetRandomVector() * _shakeStrength * Utils.Map( _timeSinceShakeStart, 0f, _shakeTime, 1f, 0f, _shakeEasingType );
		}
	}

	void HandleExploding()
	{
		LocalScale = new Vector3( Utils.Map(_timeSinceStartExploding, 0f, _explodeTime, 1f, Globals.CARD_EXPLODE_SCALE, _explodeEasingType) );
		IconOpacity = Utils.Map( _timeSinceStartExploding, 0f, _explodeTime, 1f, 0f, EasingType.SineIn );

		LocalPosition = _explodeStartPos + Vector3.Random * Utils.Map( _timeSinceStartExploding, 0f, _explodeTime, 0f, 2f, _explodeShakeEasingType) * Utils.Map( _timeSinceStartExploding, _explodeTime * 0.9f, _explodeTime, 1f, 0f );
	}

	void HandleScaling()
	{
		if(_timeSinceScale > 0.25f)
		{
			IsScaling = false;
			LocalScale = new Vector3( 1f );
		}
		else
		{
			LocalScale = Utils.MapReturn( _timeSinceScale, 0f, _scaleTime, 1f, _scaleAmount, _scaleEasingType);
		}
	}

	public static string GetName( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.CrystalBall: return "Crystal Ball";
			case CardType.AncientScroll: return "Ancient Scroll";
			case CardType.JumpingSpider: return "Jumping Spider";
			case CardType.Rock: return "Boulder";
			case CardType.Syringe: return "Morphine";
			case CardType.FlyingMoney: return "Flying Money";
			case CardType.FortuneCookie: return "Fortune Cookie";
			//case CardType.AlarmClock: return "Alarm Clock";
			case CardType.FaxMachine: return "Fax Machine";
			case CardType.MoneyBag: return "Money Bag";
			case CardType.MagicWand: return "Magic Wand";
			//case CardType.StockMarket: return "Stock Market";
			case CardType.GuideDog: return "Guide Dog";
			case CardType.CarePackage: return "Care Package";
			case CardType.BirthdayCake: return "Birthday Cake";
			case CardType.DiamondRing: return "Diamond Ring";
			case CardType.RedEnvelope: return "Red Envelope";
		}

		return cardType.ToString();
	}

	public static string GetFilename( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Apple: return "apple";
			case CardType.Bomb: return "bomb";
			case CardType.Rocket: return "rocket";
			case CardType.Telephone: return "telephone";
			case CardType.Trumpet: return "trumpet";
			case CardType.Pawn: return "pawn";
			case CardType.Balloon: return "balloon";
			case CardType.Spider: return "spider";
			case CardType.Umbrella: return "umbrella";
			case CardType.UmbrellaClosed: return "umbrella_closed";
			case CardType.Juggler: return "juggler";
			case CardType.Coin: return "coin";
			case CardType.Vampire: return "vampire";
			case CardType.Maracas: return "maracas";
			case CardType.Microwave: return "microwave";
			case CardType.Raccoon: return "raccoon";
			case CardType.Rock: return "rock";
			case CardType.Bear: return "bear";
			case CardType.Candle: return "candle";
			case CardType.CrystalBall: return "crystal_ball";
			case CardType.Snake: return "snake";
			case CardType.Rose: return "rose";
			case CardType.RoseWilted: return "rose_wilted";
			case CardType.Bell: return "bell";
			case CardType.Ant: return "ant";
			case CardType.Twins: return "twins";
			case CardType.Compass: return "compass";
			case CardType.Antenna: return "antenna";
			case CardType.Wizard: return "wizard";
			case CardType.AncientScroll: return "scroll";
			case CardType.JumpingSpider: return "spider_jumping";
			case CardType.Bento: return "bento";
			//case CardType.StockMarket: return "investment";
			//case CardType.StockMarketDown: return "investment_down";
			case CardType.Ninja: return "ninja";
			case CardType.Flashlight: return "flashlight";
			case CardType.Bodybuilder: return "bodybuilder";
			case CardType.Helicopter: return "helicopter";
			case CardType.FlyingMoney: return "flying_money";
			case CardType.Syringe: return "syringe";
			case CardType.Cellphone: return "cellphone";
			case CardType.Crab: return "crab";
			case CardType.Magnet: return "magnet";
			case CardType.Stethoscope: return "stethoscope";
			case CardType.UFO: return "ufo";
			case CardType.Teacher: return "teacher";
			case CardType.Tractor: return "tractor";
			case CardType.FortuneCookie: return "fortune_cookie";
			case CardType.Dolphin: return "dolphin";
			case CardType.Detective: return "detective";
			case CardType.Genie: return "genie";
			case CardType.Potion: return "potion";
			case CardType.Mirror: return "mirror";
			//case CardType.AlarmClock: return "alarm_clock";
			case CardType.FaxMachine: return "fax_machine";
			case CardType.Dice: return "dice";
			case CardType.Coffee: return "coffee";
			case CardType.Tree: return "tree";
			case CardType.Chipmunk: return "chipmunk";
			case CardType.Dancer: return "dancer";
			case CardType.Wine: return "wine";
			case CardType.King: return "king";
			case CardType.Police: return "police";
			case CardType.Ogre: return "ogre";
			case CardType.Clown: return "clown";
			case CardType.MoneyBag: return "money_bag";
			case CardType.Bank: return "bank";
			case CardType.Steak: return "steak";
			case CardType.MagicWand: return "magic_wand";
			case CardType.Carrot: return "carrot";
			case CardType.Caterpillar: return "caterpillar";
			case CardType.Butterfly: return "butterfly";
			case CardType.Tornado: return "tornado";
			case CardType.Map: return "map";
			case CardType.Cockroach: return "cockroach";
			//case CardType.Key: return "key";
			case CardType.Broccoli: return "broccoli";
			//case CardType.ATM: return "atm";
			case CardType.Donut: return "donut";
			case CardType.Cheese: return "cheese";
			case CardType.CheeseMoldy: return "cheese_moldy";
			case CardType.GuideDog: return "guide_dog";
			case CardType.Taco: return "taco";
			case CardType.Burrito: return "burrito";
			case CardType.Skunk: return "skunk";
			case CardType.CarePackage: return "care_package";
			case CardType.Cash: return "cash";
			case CardType.BirthdayCake: return "birthday_cake";
			case CardType.Gem: return "gem";
			case CardType.Pickaxe: return "pickaxe";
			case CardType.Mountain: return "mountain";
			case CardType.DiamondRing: return "diamond_ring";
			case CardType.Rabbit: return "rabbit";
			case CardType.RedEnvelope: return "red_envelope";
		}

		return "";
	}

	public static string GetIconFilename( CardType cardType )
	{
		return $"textures/{GetFilename( cardType )}.png";
	}

	public static string GetMaterialFilename( CardType cardType )
	{
		return $"materials/cards/{GetFilename( cardType )}.vmat";
	}

	public static string GetIconEmoji( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Apple: return "🍎";
			case CardType.Bomb: return "💣";
			case CardType.Rocket: return "🚀";
			case CardType.Telephone: return "☎️";
			case CardType.Trumpet: return "🎺";
			case CardType.Pawn: return "♟️";
			case CardType.Balloon: return "🎈";
			case CardType.Spider: return "🕷️";
			case CardType.Umbrella: return "☂️";
			case CardType.UmbrellaClosed: return "🌂";
			case CardType.Juggler: return "🤹";
			case CardType.Coin: return "🟡";
			case CardType.Vampire: return "🧛";
			case CardType.Raccoon: return "🦝";
			case CardType.Rock: return "🗿";
			case CardType.Bear: return "🐻";
			case CardType.Candle: return "🕯️";
			case CardType.CrystalBall: return "🔮";
			case CardType.Snake: return "🐍";
			case CardType.Rose: return "🌹";
			case CardType.RoseWilted: return "🥀";
			case CardType.Bell: return "🔔";
			case CardType.Ant: return "🐜";
			case CardType.Twins: return "👯";
			case CardType.Compass: return "🧭";
			case CardType.Antenna: return "📡";
			case CardType.Wizard: return "🧙";
			case CardType.AncientScroll: return "📜";
			case CardType.JumpingSpider: return "🕷️";
			case CardType.Bento: return "🍱";
			//case CardType.StockMarket: return "📈";
			//case CardType.StockMarketDown: return "📉";
			case CardType.Ninja: return "🐱‍👤";
			case CardType.Flashlight: return "🔦";
			case CardType.Bodybuilder: return "🏋️";
			case CardType.Helicopter: return "🚁";
			case CardType.FlyingMoney: return "💸";
			case CardType.Syringe: return "💉";
			case CardType.Cellphone: return "📱";
			case CardType.Crab: return "🦀";
			case CardType.Magnet: return "🧲";
			case CardType.Stethoscope: return "🩺";
			case CardType.UFO: return "🛸";
			case CardType.Teacher: return "👩‍🏫";
			case CardType.Tractor: return "🚜";
			case CardType.FortuneCookie: return "🥠";
			case CardType.Dolphin: return "🐬";
			case CardType.Detective: return "🕵️";
			case CardType.Genie: return "🧞";
			case CardType.Potion: return "🧪";
			case CardType.Mirror: return "🔘";
			//case CardType.AlarmClock: return "⏰";
			case CardType.FaxMachine: return "📠";
			case CardType.Dice: return "🎲";
			case CardType.Coffee: return "☕";
			case CardType.Tree: return "🌳";
			case CardType.Chipmunk: return "🐿️";
			case CardType.Dancer: return "💃";
			case CardType.Wine: return "🍷";
			case CardType.King: return "🤴";
			case CardType.Police: return "👮";
			case CardType.Ogre: return "👹";
			case CardType.Clown: return "🤡";
			case CardType.MoneyBag: return "💰";
			case CardType.Bank: return "🏦";
			case CardType.Steak: return "🥩";
			case CardType.MagicWand: return "✨";
			case CardType.Carrot: return "🥕";
			case CardType.Caterpillar: return "🐛";
			case CardType.Butterfly: return "🦋";
			case CardType.Tornado: return "🌪️";
			case CardType.Map: return "🗺️";
			case CardType.Cockroach: return "𓆣";
			//case CardType.Key: return "🔑";
			case CardType.Broccoli: return "🥦";
			//case CardType.ATM: return "🏧";
			case CardType.Donut: return "🍩";
			case CardType.Cheese: return "🧀";
			case CardType.CheeseMoldy: return "🤮";
			case CardType.GuideDog: return "🦮";
			case CardType.Taco: return "🌮";
			case CardType.Burrito: return "🌯";
			case CardType.Skunk: return "🦨";
			case CardType.CarePackage: return "📦";
			case CardType.Cash: return "💵";
			case CardType.BirthdayCake: return "🎂";
			case CardType.Gem: return "💎";
			case CardType.Pickaxe: return "⛏️";
			case CardType.Mountain: return "⛰️";
			case CardType.DiamondRing: return "💍";
			case CardType.Rabbit: return "🐇";
			case CardType.RedEnvelope: return "🧧";
		}

		return "";
	}

	public static string GetCardDescription( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Apple: return "✅Match: +1 HP";
			case CardType.Bomb: return "✅Match: reveal nearby cards then shuffle them";
			case CardType.Rocket: return "❌Mismatch: swap position";
			case CardType.Telephone: return "✅Match: shake nearby pairs";
			case CardType.Trumpet: return "Match other cards: 30% chance to reveal";
			case CardType.Pawn: return "➡️Turn start: 50% to try to move upward";
			case CardType.Balloon: return "Floats upward";
			case CardType.Spider: return "Starts in a corner";
			case CardType.Umbrella: return "👁️Revealed: toggle open/closed";
			case CardType.Juggler: return "❌Mismatch: rearrange nearby cards";
			case CardType.Coin: return "✅Match: +$2";
			case CardType.Vampire: return "While hovered: shake if you lose HP";
			case CardType.Maracas: return "Makes a distinctive sound when shook";
			case CardType.Microwave: return "➡️Turn start: shake a nearby food/drink";
			case CardType.Raccoon: return "➡️Turn start: shake an adjacent card";
			case CardType.Rock: return "Falls downward, pushing other cards";
			case CardType.Bear: return "Food/drink revealed: 20% chance to shake";
			case CardType.Candle: return "✅Match: each reveal a nearby card";
			case CardType.CrystalBall: return "❌Mismatch: shake cards, including other card's match";
			case CardType.Snake: return "Shake when an adjacent card is matched";
			case CardType.Rose: return "When moved: shake and wilt";
			case CardType.Bell: return "Rings when moved or shook";
			case CardType.Ant: return "➡️Turn start: move to nearby empty space";
			case CardType.Twins: return "👁️Revealed: nudge toward match";
			case CardType.Compass: return "❌Mismatch: nudge toward other card's match";
			case CardType.Antenna: return "👁️Revealed: sound indicates distance to match";
			case CardType.Wizard: return "⏩Every 3 turns: shuffle some cards\n💔Damage Wizard: respawn some cards and shuffle them";
			case CardType.AncientScroll: return "✅Match: reveal all cards (after next turn, shuffle all cards)";
			case CardType.JumpingSpider: return "Starts in a corner\n❌Mismatch: swaps with an unknown card";
			case CardType.Bento: return "✅Match: +1 HP for each consecutive match";
			//case CardType.StockMarket: return "✅Match: gain half your money if the market is up, lose half if the market is down";
			case CardType.Ninja: return "➡️Turn start: 25% chance to swap position";
			case CardType.Flashlight: return "👆Chosen: reveal a nearby card";
			case CardType.Bodybuilder: return "➡️Turn start: move adjacent card upwards, pushing other cards";
			case CardType.Helicopter: return "❌Mismatch: relocate along with an adjacent card";
			case CardType.FlyingMoney: return "✅Match: +$3\n❌Mismatch: shuffle with some nearby cards";
			case CardType.Syringe: return "✅Match: heal all HP, but time is halved until end of level";
			case CardType.Cellphone: return "❌Mismatch: vibrate nearest cellphone";
			case CardType.Crab: return "Moves sideways to empty spaces";
			case CardType.Magnet: return "➡️Turn start: pull card toward it";
			case CardType.Stethoscope: return "✅Match/❌Mismatch: shake nearby cards with a heartbeat";
			case CardType.UFO: return "✅Match: abduct and shuffle nearby cards";
			case CardType.Teacher: return "✅Match: arrange row alphabetically";
			case CardType.Tractor: return "✅Match/❌Mismatch: shift row one space";
			case CardType.FortuneCookie: return "✅Match: +1 HP, and reveal 5 cards that you haven't seen before";
			case CardType.Dolphin: return "Revealed on 4 consecutive mismatches";
			case CardType.Detective: return "➡️Turn start: 50% chance to reveal a nearby card";
			case CardType.Genie: return "✅Match: next card chosen shakes its match";
			case CardType.Potion: return "✅Match: your HP is set to half";
			case CardType.Mirror: return "✅Match: flip the level horizontally";
			//case CardType.AlarmClock: return "70% chance to shake when timer goes below 1s";
			case CardType.FaxMachine: return "👆Chosen: shake 3 cards, including another fax machine";
			case CardType.Dice: return "❌Mismatch: roll a 6 to get +$2 (rolling 1 loses $1)\n✅Match: Roll a 12 to get +$5 (rolling 2 loses $2)";
			case CardType.Coffee: return "✅Match: +2 HP, and your next turn lasts 3 seconds";
			case CardType.Tree: return "✅Match: reveal an apple";
			case CardType.Chipmunk: return "➡️Turn start: move toward a tree";
			case CardType.Dancer: return "❌Mismatch: dance with multiple partners";
			case CardType.Wine: return "✅Match: +2 HP\nShake if an adjacent card shakes";
			case CardType.King: return "💔Damage King: he decides your next choice";
			case CardType.Police: return "❌Mismatch: Reveal and lock a nearby card for 2 turns\n💔Damage Police: Lock some nearby cards for 1 turn, then shuffle";
			case CardType.Ogre: return "💔Damage Ogre: shuffle with nearby cards";
			case CardType.Clown: return "💔Damage Clown: swap multiple times";
			case CardType.MoneyBag: return "✅Match: +$5";
			case CardType.Bank: return "✅Match: transform all coins into money bags";
			case CardType.Steak: return "✅Match: -$1 and +4 HP";
			case CardType.MagicWand: return "❌Mismatch: send other card adjacent to its match";
			case CardType.Carrot: return "✅Match: +3 HP, and timer is doubled next turn";
			case CardType.Caterpillar: return "➡️Turn start: 20% chance to shake and transform into a butterfly";
			case CardType.Tornado: return "➡️Turn start: swap 2 adjacent cards";
			case CardType.Map: return "✅Match: reveal all cards between";
			case CardType.Cockroach: return "Can't be matched until no other cards remain";
			//case CardType.Key: return "❌Mismatch: unlock other card\n✅Match: unlock all cards";
			case CardType.Broccoli: return "✅Match: +3 HP";
			//case CardType.ATM: return "✅Match: +$1 for every 2 matches you've made this level (max: $7)";
			case CardType.Donut: return "✅Match: +1 HP for each adjacent empty space";
			case CardType.Cheese: return "✅Match: +3 HP if not moldy\nGoes moldy after 7 turns";
			case CardType.GuideDog: return "When you mismatch other cards, 50% chance to point toward one of their matches";
			case CardType.Taco: return "✅Match: +1 Max HP";
			case CardType.Burrito: return "✅Match: +2 Max HP";
			case CardType.Skunk: return "Can't be matched while adjacent to any non-skunks";
			case CardType.CarePackage: return "✅Match: +$1 for every 2 HP missing";
			case CardType.Cash: return "✅Match: +$3 if both cash are nearby eachother, otherwise +$1";
			case CardType.BirthdayCake: return "✅Match: +1 HP and +$2";
			case CardType.Gem: return "✅Match: +$4";
			case CardType.Pickaxe: return "✅Match: transform all boulders into gems";
			case CardType.Mountain: return "Can't be moved";
			case CardType.DiamondRing: return "✅Match: +$3 if no other cards remain, otherwise +$1";
			case CardType.Rabbit: return "➡️Turn start: 50% chance to move towards its match";
			case CardType.RedEnvelope: return "✅Match: if you have less than $5, set your money to $5";
		}

		return "";
	}

	public static string GetCardExplanation( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Ant: return "\"nearby\" means 1 space away\n(including diagonal)";
			case CardType.Snake: return "\"adjacent\" means 1 space away\n(NOT including diagonal)";
		}

		return "";
	}

	public static bool HasHP( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Ogre: return true;
			case CardType.Clown: return true;
			case CardType.Police: return true;
			case CardType.Wizard: return true;
			case CardType.King: return true;
		}

		return false;
	}

	public static int GetMaxHP( CardType cardType )
	{
		switch ( cardType )
		{
			case CardType.Ogre: return 2;
			case CardType.Clown: return 2;
			case CardType.Police: return 2;
			case CardType.Wizard: return 3;
			case CardType.King: return 3;
		}

		return 0;
	}

	public void SetRevealedFromAsync(bool revealed)
	{
		IsRevealed = revealed;

		if( Card.HasHP(CardType) || IsLocked )
			IconObj.Enabled = revealed;

		if ( revealed )
		{
			NumTimesRevealed++;

			if(Manager.Instance.Stats[StatType.MaxCrayonMarks] > 0f && !AreRevealMarksShown)
				ShowRevealMarks();
		}
	}

	public void SetRevealedInstant()
	{
		IsRevealed = true;

		if ( Card.HasHP( CardType ) || IsLocked )
			IconObj.Enabled = true;

		WorldRotation = Rotation.FromPitch( 0f );

		NumTimesRevealed++;

		if ( Manager.Instance.Stats[StatType.MaxCrayonMarks] > 0f && !AreRevealMarksShown )
			ShowRevealMarks();
	}

	public void SetHiddenInstant()
	{
		IsRevealed = false;

		if ( Card.HasHP( CardType ) )
			IconObj.Enabled = false;

		WorldRotation = Rotation.FromPitch( 180f );
	}

	public void Shake( float strength, float time = 0.6f, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
	{
		IsShaking = true;
		_timeSinceShakeStart = 0f;
		_shakeTime = time;
		_shakeEasingType = easingType;
		_shakeStrength = strength;

		if ( playSfx || ( OverrideShakeSfx && ( IsRevealed || !OnlyOverrideShakeSfxWhenRevealed ) )  )
			PlayShakeSfx();
	}

	public virtual void PlayShakeSfx()
	{
		//Manager.Instance.PlayCardSfx( "shake_1", this, volume: 1.1f, pitch: Game.Random.Float( 1.4f, 1.6f ) );
		Manager.Instance.PlayCardSfx( "shake_3", this, volume: 1.35f, pitch: Game.Random.Float( 0.94f, 1.06f ) );
	}

	public virtual bool OverrideShakeSfx => false;
	public virtual bool OnlyOverrideShakeSfxWhenRevealed => false;

	public virtual void PlayHurtSfx(Card card0, Card card1)
	{
		Manager.Instance.PlayCardSfxBetween( "shake_1", card0, card1, volume: 1.3f, pitch: Game.Random.Float( 1.4f, 1.6f ) );
	}

	// return false if needed to change start pos
	public virtual bool ValidateStartingGridPos()
	{
		return true;
	}

	public virtual bool ShouldHandleEvent( EventType eventType )
	{
		return false;
	}

	public virtual async Task HandleEventAsync(EventType eventType)
	{
		await Task.Frame();
	}

	public virtual string GetEventText( EventType eventType )
	{
		return GetCardDescription( CardType );
	}

	public async Task Hurt()
	{
		if(!Card.HasHP(CardType))
		{
			Log.Error( $"Trying to hurt card {CardType} with no HP!" );
			return;
		}

		HP--;
		await Manager.Instance.ShakeCard(this, playSfx: false);
	}

	public void Lock(int numTurns)
	{
		IsLocked = true;
		LockTurnsRemaining = numTurns;
		WasJustLocked = true;
	}

	public void Unlock()
	{
		IsLocked = false;
	}

	public virtual bool CanBeMatched()
	{
		return true;
	}

	public void StartExploding(float explodeTime)
	{
		IsExploding = true;
		_timeSinceStartExploding = 0f;
		_explodeTime = explodeTime;
		_explodeStartPos = LocalPosition;

		int rand = Game.Random.Int( 0, 5 );
		switch(rand)
		{
			case 0: _explodeEasingType = EasingType.SineIn; break;
			case 1: _explodeEasingType = EasingType.SineIn; break;
			case 2: _explodeEasingType = EasingType.QuadIn; break;
			case 3: _explodeEasingType = EasingType.QuadIn; break;
			case 4: _explodeEasingType = EasingType.QuartIn; break;
			case 5: _explodeEasingType = EasingType.Linear; break;
		}

		rand = Game.Random.Int( 0, 7 );
		switch ( rand )
		{
			case 0: _explodeShakeEasingType = EasingType.SineIn; break;
			case 1: _explodeShakeEasingType = EasingType.QuadIn; break;
			case 2: _explodeShakeEasingType = EasingType.QuartIn; break;
			case 3: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 4: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 5: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 6: _explodeShakeEasingType = EasingType.ExpoIn; break;
			case 7: _explodeShakeEasingType = EasingType.Linear; break;
		}
	}

	public void Explode( int breakNum )
	{
		var scale = LocalScale.x * Model.LocalScale.x;
		Manager.Instance.SpawnCardBreak( WorldPosition, LocalRotation, scale, ModelRenderer.MaterialOverride, breakNum );
		Manager.Instance.PlayCardSfx( "explosion", this, volume: Game.Random.Float( 0.4f, 0.55f ), pitch: Game.Random.Float( 0.4f, 2f ) );

		Manager.Instance.CardBreakParticlesPrefab.Clone( WorldPosition.WithZ( 25f ) );
		Manager.Instance.CardBreakShockwavePrefab.Clone( WorldPosition.WithZ( 55f ) );

		GameObject.Destroy();
	}

	public void StartScaling(float time, float amount, EasingType easingType)
	{
		IsScaling = true;
		_timeSinceScale = 0f;
		_scaleTime = time;
		_scaleAmount = amount;
		_scaleEasingType = easingType;
	}

	public void ShowFlag()
	{
		IsFlagShown = true;
		IconBackObj.Enabled = true;
	}

	public void HideFlag()
	{
		IsFlagShown = false;

		if ( !AreRevealMarksShown )
			IconBackObj.Enabled = false;
	}

	public void ShowRevealMarks()
	{
		AreRevealMarksShown = true;
		IconBackObj.Enabled = true;
	}
}