Manager.cs
using Microsoft.VisualBasic;
using Sandbox;
using Sandbox.Audio;
using Sandbox.Services;
using System.IO.Pipes;
using System.Numerics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

public enum CheckMatchPhase { Begin, Halfway, Finish }

public enum EventType { None, Choose, Match, Mismatch, LevelStartBoss, LevelStart, TurnStart, Reveal, GainHP, LoseHP, OverhealHP, TimerRanOut, GainMoney, LoseMoney, MoveCard, SwapCards, HurtCards, AfterCardsMoved, FinishLevel, Shake,
	ClaimBounty, ClaimBountyAfter, }
public enum GameState { Menu, Playing, BuyPhase, Victory, Failure }

public enum StatType { TurnNum, NumMatches, NumMismatches, ConsecutiveMatches, ConsecutiveMismatches, TimerTotalTime, TimerTotalTimeOverride, KingMoveForced, LatestHealAmount, LatestOverhealAmount, TimerRunOutHPLoss, ShieldAmount,
	TimerResetAfterBothChoices, FreeItem, NumFlags, MaxFlags, MaxCrayonMarks, FoodAdditionalHeal, EarnExtraMoney, ShopExtraItems, ExistingItemExtraChance, BuyItemHealChance, NumAdditionalBounties, NumExtraBountyTurns, ExtraBountyReward,
	StealItemChance, ClaimBountiesBeforeReady, AllItemsCostHP, HPHealedThisLevel,
	CardSpawnVal0, CardSpawnVal1, CardSpawnVal2, CardSpawnVal3, CardSpawnVal4, CardSpawnVal5, CardSpawnVal6, CardSpawnVal7, CardSpawnVal8, CardSpawnVal9,
}

public interface IEventHandler
{
	public bool ShouldHandleEvent( EventType eventType );
	public Task HandleEventAsync( EventType eventType );
	public string GetEventText( EventType eventType );
}

public struct BountyData
{
	public int startTurn;
	public int endTurn;
	public int moneyAmount;
}

public sealed class Manager : Component
{
	public static Manager Instance { get; private set; }

	[Property] public CameraComponent Camera;
	[Property] public GameObject CardPrefab;
	[Property] public GameObject CardOutlinePrefab;
	[Property] public List<GameObject> CardBreakPrefabs;
	[Property] public GameObject CardBreakParticlesPrefab;
	[Property] public GameObject CardBreakShockwavePrefab;
	[Property] public GameObject MenuCardPrefab;
	[Property] public SoundEvent SongStart;
	[Property] public SoundEvent SongB;
	[Property] public SoundEvent SongC;
	[Property] public SoundEvent SongD;
	[Property] public SoundEvent SongE;
	[Property] public SoundEvent SongLastLevel;
	[Property] public SoundEvent SongShop;
	[Property] public SoundEvent SongVictory;
	[Property] public SoundEvent SongDefeat;

	public Hud Hud { get; set; }
	public SoundPointComponent MusicSoundPoint { get; set; }
	public Mixer MusicMixer { get; set; }
	public float MusicVolume { get; set; } = 30f;
	public Mixer SfxMixer { get; set; }
	public float SfxVolume { get; set; } = 50f;
	public bool IsMuted { get; set; }

	public bool IsSettingsOpen { get; set; }
	public bool IsLeaderboardOpen { get; set; }

	public Card HoveredCard { get; private set; }
	public Card ChosenCard { get; private set; }
	public Card RevealedCard { get; private set; }
	public Card MovedCard { get; private set; }
	public Card ShakenCard { get; private set; }
	public List<Card> SwappedCards { get; private set; } = new();
	public bool IsMismatchALockedMatch { get; private set; }

	public int GridWidth { get; set; }
	public int GridHeight { get; set; }

	private GameObject _cardContainer;
	private GameObject _relicContainer;
	private GameObject _statusContainer;

	public const float GRID_X_SPACING = 50f;
	public const float GRID_Y_SPACING = 60f;

	public int NumPairs => (GridWidth * GridHeight) / 2;

	public List<Card> Cards = new();
	private Dictionary<IntVector2, Card> _cardPositions = new();

	public Dictionary<CardType, int> CardPairsExisting = new(); // pairs that exist on the current level
	public Dictionary<CardType, int> CardPairsMatched = new(); // pairs that have been matched on the current level

	public List<Card> ChosenCards = new();

	public bool IsMoveResolving { get; private set; }

	public int CardTypePanelHash { get; set; }

	public int HP { get; set; }
	public int MaxHP { get; set; }
	public TimeSince TimeSinceHPChanged { get; set; }
	public bool IsDead { get; set; }

	private int _currRemainingSeconds;


	public float TimerElapsed { get; set; }
	public float TimerMoveStartTime { get; set; }
	public TimeSince TimeSinceTimerChanged { get; set; }

	private bool _hasMadeFirstMove;

	public Stack<(IEventHandler, EventType)> EventMessageStack = new();
	public int EventMessageHash { get; private set; }

	public CardType HoveredPanelType { get; set; }
	public int HoveredPanelIndex { get; set; }
	public bool IsHoveringBounty { get; set; }

	public RelicType HoveredRelicType { get; set; }

	public int LevelNum { get; private set; }
	public const int MAX_LEVEL = 10;

	public bool IsSpawningLevel { get; private set; }

	public bool IsLevelActive { get; set; }

	public bool ShouldRestart { get; set; }
	public bool ShouldReturnToMenu { get; set; }

	public int Money { get; set; }
	public TimeSince TimeSinceMoneyChanged { get; set; }

	public GameState GameState { get; set; }

	public List<Relic> Relics = new();

	public List<RelicType> BuyPhaseOfferedRelics = new();
	public int NumRelicsToShow { get; set; }
	public int BuyPhaseHash { get; set; }
	public List<bool> BoughtItems = new();

	public int RelicHash { get; set; }

	public Dictionary<StatType, float> Stats = new();

	public TimeSince TimeSinceHoverSfx { get; set; }

	public List<StatusEffect> Statuses = new();

	public Vector3 CenterPos => new Vector3( GridWidth, GridHeight, 0f ) * 20f;

	public bool ShouldShowOverlayColor { get; private set; }
	public TimeSince TimeSinceOverlayStart { get; private set; }
	public float OverlayTime { get; private set; }
	public Color OverlayColor { get; private set; }

	private Vector3 _cameraStartPos;
	private bool _isCameraShaking;
	private TimeSince _timeSinceCameraShakeStart;
	private float _cameraShakeTime;
	private float _cameraShakeStrength;

	public bool ForceCursor { get; set; }
	public int CursorClockNum { get; set; }
	private TimeSince _timeSinceCursorClockChange;
	public TimeSince TimeSinceMoveStartResolving { get; private set; }

	public int NumFloaters { get; set; }

	public Dictionary<CardType, BountyData> Bounties = new();

	public TimeSince TimeSinceTurnStart { get; set; }

	public TimeSince TimeSinceRunStart { get; set; }
	public float FinalRunTime { get; set; }
	public float BeatPreviousLevelTime { get; set; }

	private TimeSince _timeSinceSpawnMenuCard;
	private float _spawnNextMenuCardDelay;

	public bool IsFadingIn { get; set; }
	public TimeSince TimeSinceStartFadingIn { get; set; }
	public const float FADE_IN_TIME = 1f;

	public bool StartFromMenu { get; set; }

	private List<CardType> SboxStatsMatchedCardTypes { get; set; } = new();
	private List<RelicType> SboxStatsBoughtRelicTypes { get; set; } = new();
	private bool _finishedGettingSboxStats;

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

		Hud = Components.Get<Hud>();
		MusicSoundPoint = Scene.Directory.FindByName( "music player" ).First().Components.Get<SoundPointComponent>();
		MusicMixer = Mixer.FindMixerByName( "Music" );
		SfxMixer = Mixer.FindMixerByName( "Game" );
		SfxMixer.Volume = 1f;

		Instance = this;

		_cardContainer = new GameObject();
		_cardContainer.Name = "cards";

		_relicContainer = new GameObject();
		_relicContainer.Name = "relics";

		_statusContainer = new GameObject();
		_statusContainer.Name = "statuses";

		_cameraStartPos = Camera.WorldPosition;

		InitSboxStats();

		StartMenu();
		//StartNewRun();
	}

	public void PlaySong(SoundEvent soundEvent)
	{
		if(soundEvent == null)
		{
			MusicSoundPoint.StopSound();
			return;
		}

		MusicSoundPoint.SoundEvent = soundEvent;
		MusicSoundPoint.StopSound();
		MusicSoundPoint.StartSound();
	}

	public void StartMenu()
	{
		foreach ( var child in _cardContainer.Children )
			child.Destroy();

		IsLevelActive = false;
		GameState = GameState.Menu;
		ForceCursor = true;

		if (!IsMuted)
			MusicMixer.Volume = 0f;

		_timeSinceSpawnMenuCard = 0f;
		_spawnNextMenuCardDelay = 0f;

		int numMenuCards = Game.Random.Int( 10, 20 );
		for ( int i = 0; i < numMenuCards; i++ )
			CreateMenuCard( new Vector3( Game.Random.Float( -275f, 275f ), Game.Random.Float( -150f, 150f ), Game.Random.Float( 10f, 80f ) ) );

		SetBgTint( Color.FromRgb( 0x00972D ) );

		FadeIn();

		PlaySong( SongStart );
	}

	public void StartNewRun()
	{
		foreach ( var component in Scene.GetAllComponents<MenuCard>() )
			component.GameObject.Destroy();

		foreach ( var component in Scene.GetAllComponents<CardBreakPiece>() )
			component.GameObject.Destroy();

		foreach ( var component in Scene.GetAllComponents<ParticleEffect>() )
			component.GameObject.Destroy();

		ResetRunVars();
		ResetLevelVars();

		//HP = 1;
		//Money = 10;

		//IncreaseRelicLevel( RelicType.ShoppingCart );
		//IncreaseRelicLevel( RelicType.ShoppingCart );
		//IncreaseRelicLevel( RelicType.ShoppingCart );

		//GameState = GameState.Menu;
		//GameState = GameState.Failure;
		//GameState = GameState.Victory;
		SetLevel( 1, startNewSong: !StartFromMenu );

		StartFromMenu = false;

		//_ = FinishedLevel();
	}

	public void SetLevel(int levelNum, bool startNewSong = true)
	{
		LevelNum = levelNum;
		GameState = GameState.Playing;
		
		var cardTypes = LevelCreator.GetCardTypesForLevel( levelNum );
		InitializeStage( cardTypes );

		GenerateBounties();

		//PlaySong( SongLevel1 );
		//PlaySong( null );

		if(levelNum == 1)
			SetBgTint( Color.FromRgb( 0x00972D ) );
		else if (levelNum == 3)
			SetBgTint( Color.FromRgb( 0x086D0D ) );
		else if ( levelNum == 5 )
			SetBgTint( Color.FromRgb( 0xA4389D ) );
		else if ( levelNum == 7 )
			SetBgTint( Color.FromRgb( 0xA03B3B ) );
		else if ( levelNum == 9 )
			SetBgTint( Color.FromRgb( 0x2B44B1 ) );

		FadeIn();

		//_ = FinishedLevel();

		if( startNewSong )
		{
			if ( levelNum == 1 || levelNum == 2 )
				PlaySong( SongStart );
			if ( levelNum == 3 || levelNum == 4 )
				PlaySong( SongB );
			else if ( levelNum == 5 || levelNum == 6 )
				PlaySong( SongC );
			else if ( levelNum == 7 || levelNum == 8 )
				PlaySong( SongE );
			else if ( levelNum == 9 )
				PlaySong( SongD );
			else if ( levelNum == 10 )
				PlaySong( SongLastLevel );
		}

		foreach ( var component in Scene.GetAllComponents<CardBreakPiece>() )
			component.GameObject.Destroy();

		foreach ( var component in Scene.GetAllComponents<ParticleEffect>() )
			component.GameObject.Destroy();
	}

	public void InitializeStage( List<CardType> cardPairTypes )
	{
		ResetLevelVars();

		List<CardType> replacementCardTypes = new();
		if ( GetRelic( RelicType.GuideDog ) != null )
			replacementCardTypes.Add( CardType.GuideDog );

		for(int i = 0; i < replacementCardTypes.Count; i++)
			cardPairTypes.RemoveAt( cardPairTypes.Count - 1 );

		foreach(var cardType in replacementCardTypes)
			cardPairTypes.Add( cardType );

		IsLevelActive = true;

		var numCards = cardPairTypes.Count() * 2;
		var gridSize = FindBestGridSize( numCards );

		GridWidth = gridSize.Item1;
		GridHeight = gridSize.Item2;

		List<CardType> cardTypes = new();
		foreach (var cardPairType in cardPairTypes )
		{
			for ( int i = 0; i < 2; i++ )
				cardTypes.Add( cardPairType );

			if ( CardPairsExisting.ContainsKey( cardPairType ) )
				CardPairsExisting[cardPairType]++;
			else
				CardPairsExisting.Add( cardPairType, 1 );
		}
		cardTypes.Shuffle();

		Dictionary<CardType, int> cardTypeIDs = new();

		int count = 0;
		for ( int x = 0; x < GridWidth; x++ )
		{
			for ( int y = 0; y < GridHeight; y++ )
			{
				var gridPos = new IntVector2( x, y );
				var cardType = cardTypes[count];

				if(cardTypeIDs.ContainsKey(cardType))
					cardTypeIDs[cardType]++;
				else
					cardTypeIDs.Add( cardType, 0 );

				CreateCard( gridPos, cardType, cardTypeIDs[cardType] );

				count++;
			}
		}

		_cardContainer.WorldPosition = new Vector3(
			-GridWidth * GRID_X_SPACING * 0.5f + GRID_X_SPACING * 0.5f + (GridWidth >= 10 ? 10f : 0f),
			-GridHeight * GRID_Y_SPACING * 0.5f + GRID_Y_SPACING * 0.5f + 7f,
			0f
		);

		var maxSize = Math.Max( GridWidth * 0.85f, GridHeight * 1.3f );
		//Camera.WorldPosition = new Vector3( 0f, 0f, Utils.Map( maxSize, 2.55f, 8.5f, 360f, 540f, EasingType.QuadOut) );
		Camera.OrthographicHeight = Utils.Map( maxSize, 2.55f, 8.5f, 260f, 345f, EasingType.QuadOut );

		// validate that each card likes its starting position
		int MAX_TRIES = 100;
		bool allValid = false;
		for (int i = 0; i < MAX_TRIES; i++)
		{
			allValid = true;
			foreach ( var card in Cards )
			{
				if ( !card.ValidateStartingGridPos() )
					allValid = false;
			}

			if ( allValid )
				break;
		}

		if(!allValid)
		{
			Log.Error( "Unable to validate card starting positions!" );
		}

		_ = SpawnLevelAsync();
	}

	async Task SpawnLevelAsync()
	{
		IsSpawningLevel = true;
		IsMoveResolving = true;
		await ForceCursorSwitch();

		PlaySfx( "card_shuffle", new Vector3( 0f, 0f, Camera.WorldPosition.z - 100f ), volume: 0.6f, pitch: Game.Random.Float( 1.3f, 1.45f ) );
		var startPos = new Vector3( -GridWidth * GRID_X_SPACING * 0.5f - GRID_X_SPACING, -GridHeight * GRID_Y_SPACING * 0.5f + GRID_Y_SPACING, 10f );

		Cards.Shuffle();

		int count = 0;
		foreach ( var card in Cards )
		{
			card.WorldPosition = startPos + new Vector3(-0.75f, 0.75f, 0.1f) * count;
			card.IsSpawning = true;
			count++;
		}

		await Task.DelayRealtime( 200 );

		// shuffle from deck
		count = 0;
		for(int i = Cards.Count - 1; i >= 0; i--)
		{
			var card = Cards[i];
			card.IsSpawning = false;

			await Task.DelayRealtime( (int)(Utils.Map(Cards.Count, 6, 50, 80f, 20f ) * Utils.Map(count, 0, Cards.Count, 1f, 0.3f)));

			count++;
		}

		await Task.DelayRealtime( 250 );

		IsSpawningLevel = false;

		await EventHappened( EventType.LevelStartBoss );
		await EventHappened( EventType.LevelStart );

		//await EventHappened( EventType.TurnStart );

		//_updateTaskActive = true;

		TimerElapsed = 0f;
		TimerMoveStartTime = Time.Now;

		IsMoveResolving = false;
		await ForceCursorSwitch();

		UpdateAsync();
	}

	void GenerateBounties()
	{
		if ( LevelNum == 1 )
			return;

		List<CardType> cardTypes = new();
		cardTypes.AddRange( CardPairsExisting.Keys.ToList() );

		for( int i = cardTypes.Count - 1; i >= 0; i-- )
		{
			var cardType = cardTypes[i];
			if ( Card.HasHP( cardType ) || cardType == CardType.Skunk || cardType == CardType.Cockroach )
				cardTypes.RemoveAt( i );
		}

		cardTypes.Shuffle();

		int numBounties = (LevelNum < 5 ? 1 : 2) + (int)Stats[StatType.NumAdditionalBounties];
		for( int i = 0; i < Math.Min(numBounties, cardTypes.Count); i++ )
		{
			int start = Game.Random.Int( 2, 4 ) + Game.Random.Int( 0, (int)Math.Round( Utils.Map( LevelNum, 2, 10, 0f, 5f, EasingType.SineIn ) ) );
			int end = start + Game.Random.Int( 5, (int)Math.Round( Utils.Map( LevelNum, 2, 10, 6f, 9f, EasingType.SineIn ) ) ) + (int)Stats[StatType.NumExtraBountyTurns];
			int money = Game.Random.Int( 1, (int)Math.Round( Utils.Map( LevelNum, 2, 10, 3f, 4f, EasingType.QuadIn ) ) ) + (int)Stats[StatType.ExtraBountyReward];

			Bounties.Add( cardTypes[i], new BountyData() { startTurn = start, endTurn = end, moneyAmount = money } );
		}
	}

	async Task ForceCursorSwitch()
	{
		ForceCursor = true;
		await Task.DelayRealtime( 70 );
		ForceCursor = false;
	}

	public string GetCursor()
	{
		return IsMoveResolving && !IsDead && GameState == GameState.Playing && TimeSinceMoveStartResolving > 0.07f
			? $"clock_{CursorClockNum}"
			: (TimeSinceMoveStartResolving < 0.01f ? "pointer3_small" : "pointer3");
			//: "pointer3";
	}

	void ResetRunVars()
	{
		LevelNum = 1;

		HP = MaxHP = 10;

		Money = 0;

		TimeSinceRunStart = 0f;
		TimeSinceHPChanged = 999f;
		TimeSinceMoneyChanged = 999f;
		TimeSinceTurnStart = 999f;
		TimeSinceTimerChanged = 999f;

		Relics.Clear();
		RelicHash = 0;

		var relicComponents = _relicContainer.Components.GetAll();
		for (int i = relicComponents.Count() - 1; i >= 0; i--) 
			relicComponents.ElementAt( i ).Destroy();

		Stats[StatType.TimerTotalTime] = 10f;
		Stats[StatType.TimerRunOutHPLoss] = 1f;
		Stats[StatType.ShieldAmount] = 0f;
		Stats[StatType.TimerResetAfterBothChoices] = 0f;
		Stats[StatType.FreeItem] = 0f;
		Stats[StatType.CardSpawnVal0] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal1] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal2] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal3] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal4] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal5] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal6] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal7] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal8] = Game.Random.Int( 0, 10 );
		Stats[StatType.CardSpawnVal9] = Game.Random.Int( 0, 10 );
		Stats[StatType.MaxFlags] = 0f;
		Stats[StatType.MaxCrayonMarks] = 0f;
		Stats[StatType.FoodAdditionalHeal] = 0f;
		Stats[StatType.EarnExtraMoney] = 0f;
		Stats[StatType.ShopExtraItems] = 0f;
		Stats[StatType.ExistingItemExtraChance] = 0f;
		Stats[StatType.BuyItemHealChance] = 0f;
		Stats[StatType.NumAdditionalBounties] = 0f;
		Stats[StatType.NumExtraBountyTurns] = 0f;
		Stats[StatType.ExtraBountyReward] = 0f;
		Stats[StatType.StealItemChance] = 0f;
		Stats[StatType.ClaimBountiesBeforeReady] = 0f;
		Stats[StatType.AllItemsCostHP] = 0f;
	}

	void ResetLevelVars()
	{
		IsDead = false;

		TimerElapsed = 0f;
		_currRemainingSeconds = (int)Stats[StatType.TimerTotalTime];
		_hasMadeFirstMove = false;
		ShouldShowOverlayColor = false;
		_isCameraShaking = false;

		ChosenCards.Clear();
		ChosenCard = null;
		HoveredCard = null;
		RevealedCard = null;
		MovedCard = null;
		ShakenCard = null;
		SwappedCards.Clear();
		IsMismatchALockedMatch = false;

		IsMoveResolving = false;

		HoveredPanelType = CardType.None;
		HoveredPanelIndex = -1;
		IsHoveringBounty = false;

		foreach ( var child in _cardContainer.Children )
			child.Destroy();

		Cards.Clear();
		_cardPositions.Clear();

		CardPairsExisting.Clear();
		CardPairsMatched.Clear();
		CardTypePanelHash++;

		Bounties.Clear();

		Statuses.Clear();

		var statusComponents = _statusContainer.Components.GetAll();
		for ( int i = statusComponents.Count() - 1; i >= 0; i-- )
			statusComponents.ElementAt( i ).Destroy();

		Stats[StatType.TurnNum] = 0f;
		Stats[StatType.ConsecutiveMatches] = 0f;
		Stats[StatType.ConsecutiveMismatches] = 0f;
		Stats[StatType.NumMatches] = 0f;
		Stats[StatType.NumMismatches] = 0f;
		Stats[StatType.TimerTotalTimeOverride] = 0f;
		Stats[StatType.KingMoveForced] = 0f;
		Stats[StatType.LatestHealAmount] = 0f;
		Stats[StatType.LatestOverhealAmount] = 0f;
		Stats[StatType.NumFlags] = 0f;
		Stats[StatType.HPHealedThisLevel] = 0f;
	}

	protected override void OnUpdate()
	{
		if ( GameState == GameState.Menu )
			HandleMenu();

		if(Input.EscapePressed)
		{
			if ( IsLeaderboardOpen )
				IsLeaderboardOpen = false;
			else
				IsSettingsOpen = !IsSettingsOpen;

			Input.EscapePressed = false;
			PlaySfx( "click_0" );
		}

		if ( Input.Pressed("Mute") )
		{
			IsMuted = !IsMuted;
			SetMute( IsMuted );

			PlaySfx( "click_0" );
		}

		//Gizmo.Draw.ScreenText( $"{IsLevelActive}", new Vector2( 260, 1260 ), size: 16 );

		//if(Input.Pressed("use"))
		//{
		//	DetermineOfferedRelics();
		//}

		//if (Input.Down( "Duck" ) )
		//{
		//	for ( int x = 0; x < GridWidth; x++ )
		//	{
		//		for ( int y = 0; y < GridHeight; y++ )
		//		{
		//			var gridPos = new IntVector2( x, y );

		//			Gizmo.Draw.Color = Color.White.WithAlpha( 0.9f );
		//			var card = GetCardAtGridPos( gridPos );
		//			Gizmo.Draw.Text( $"{gridPos}: {(card != null ? card.CardType : "")}", new global::Transform( _cardContainer.WorldPosition + GetCardPos( gridPos ) + new Vector3( -21f, -26f, 0f ) ), size: 12f, flags: TextFlag.Left );
		//		}
		//	}
		//}

		if ( IsMoveResolving && !IsDead && IsLevelActive )
		{
			if ( _timeSinceCursorClockChange > 0.075f )
			{
				CursorClockNum = (CursorClockNum + 1) % 12;
				_timeSinceCursorClockChange = 0f;
			}

			//if ( Input.Pressed( "attack1" ) )
			//	PlaySfxScreenPos( "error", Mouse.Position, volume: 1f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
		}

		//ChosenCard = null;

		HandleOverlay();
		HandleCameraShake();

		if ( Scene.TimeScale < 1f )
			Scene.TimeScale = Utils.DynamicEaseTo( Scene.TimeScale, 1f, 0.2f, Time.Delta );

		if(IsFadingIn)
		{
			if ( TimeSinceStartFadingIn > FADE_IN_TIME )
				IsFadingIn = false;

			if(!IsMuted)
				RefreshMusicMixerVolume();
		}

		if ( IsSpawningLevel || IsDead || !IsLevelActive )
			return;

		//var prevHoveredCard = HoveredCard;

		if ( HoveredCard != null )
		{
			HoveredCard.IsHovered = false;
			HoveredCard = null;
		}

		var tr = Scene.Trace.Ray( Camera.ScreenPixelToRay( Mouse.Position ), 1000f ).HitTriggersOnly().Run();
		if ( tr.Hit )
		{
			if(tr.GameObject.Parent != null)
			{
				var card = tr.GameObject.Parent.Components.Get<Card>();
				if ( card != null && !card.IsMovementControlled )
				{
					HoveredCard = card;
					card.IsHovered = true;

					//if ( prevHoveredCard != card && !card.IsRevealed && TimeSinceHoverSfx > 0.025f )
					//{
					//	PlayCardSfx( "click_1", card, volume: 0.3f, pitch: 0.4f );
					//	TimeSinceHoverSfx = 0f;
					//}
				}
			}
		}

		if ( Input.Pressed( "attack2" ) && HoveredCard != null && !HoveredCard.IsRevealed && !HoveredCard.IsExploding )
		{
			if ( HoveredCard.IsFlagShown )
			{
				HoveredCard.HideFlag();
				Stats[StatType.NumFlags]--;
				PlayCardSfx( "pop", HoveredCard, volume: 1f, pitch: Game.Random.Float( 0.9f, 0.95f ) );

				HoveredCard.StartScaling( 0.1f, 1.05f, EasingType.SineOut );
			}
			else
			{
				if( (int)Stats[StatType.NumFlags] < (int)Stats[StatType.MaxFlags] )
				{
					HoveredCard.ShowFlag();
					Stats[StatType.NumFlags]++;
					PlayCardSfx( "pop", HoveredCard, volume: 1f, pitch: Game.Random.Float( 1.1f, 1.15f ) );

					HoveredCard.StartScaling( 0.1f, 1.075f, EasingType.SineOut );
				}
				else
				{
					PlayCardSfx( "error_1", HoveredCard, volume: 0.5f, pitch: Game.Random.Float( 0.9f, 0.92f ) );

					HoveredCard.StartScaling( 0.1f, 1.033f, EasingType.SineOut );
				}
			}
		}

		//if ( Input.Pressed( "attack1" ) )
		//{
		//	Log.Info( "1" );
		//	if ( HoveredCard != null )
		//	{
		//		Log.Info( "2" );
		//		if(IsMoveResolving)
		//		{
		//			Log.Info( $"3" );
		//			PlayCardSfx( "error", HoveredCard, volume: 1f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
		//		}
		//	}
		//}

		if ( IsMoveResolving )
			return;

		if ( Input.Pressed( "attack1" ) && HoveredCard != null && !ChosenCards.Contains( HoveredCard ) && !HoveredCard.IsMovementControlled && ChosenCard == null )
		{
			ChosenCard = HoveredCard;
		}

		//if ( Input.Pressed( "Left" ) && Manager.Instance.LevelNum >= 0 )
		//{
		//	SetLevel( LevelNum - 1 );
		//}

		//if ( Input.Pressed( "Right" ) && Manager.Instance.LevelNum < MAX_LEVEL )
		//{
		//	SetLevel( LevelNum + 1 );
		//}

		//if (Input.Pressed("Menu"))
		//{
		//	StartNewRun();
		//}
	}

	void HandleMenu()
	{
		if(TimeSinceStartFadingIn > 0.075f)
			ForceCursor = false;

		if (_timeSinceSpawnMenuCard > _spawnNextMenuCardDelay)
		{
			CreateMenuCard( new Vector3( Game.Random.Float( -275f, 275f ), 230f, Game.Random.Float( 10f, 80f ) ) );

			_timeSinceSpawnMenuCard = 0f;
			_spawnNextMenuCardDelay = Game.Random.Float( 0.05f, 0.5f );
		}

		var mousePos = Mouse.Position * Hud.Panel.ScaleFromScreen;
		//Log.Info( $"{MathF.Round( mousePos.x)}, {MathF.Round(mousePos.y)}" );

		bool allowHover = true;
		//if ( IsLeaderboardOpen )
		//	allowHover = mousePos.x < 479f || mousePos.x > 1437f || mousePos.y < 81f || mousePos.y > 998f;
		//else if ( IsSettingsOpen )
		//	allowHover = mousePos.x < 691f || mousePos.x > 1227f || mousePos.y < 324f || mousePos.y > 754f;
		//else
		//	allowHover = mousePos.x < 650f || mousePos.x > 1265f || mousePos.y < 314f || mousePos.y > 765f;

		if ( allowHover )
		{
			MenuCard hoveredCard = null;

			var tr = Scene.Trace.Ray( Camera.ScreenPixelToRay( Mouse.Position ), 1000f ).HitTriggersOnly().Run();
			if ( tr.Hit )
			{
				if ( tr.GameObject.Parent != null )
				{
					var menuCard = tr.GameObject.Parent.Components.Get<MenuCard>();
					if ( menuCard != null && !menuCard.IsMovementControlled )
					{
						hoveredCard = menuCard;
						hoveredCard.IsHovered = true;
					}
				}
			}

			if ( Input.Pressed( "attack1" ) && hoveredCard.IsValid() )
			{
				if ( hoveredCard.HP > 1 )
				{
					hoveredCard.Shake( strength: Game.Random.Float( 1f, 2f ), time: Game.Random.Float( 0.3f, 0.45f ), easingType: EasingType.QuadOut, playSfx: false );
					hoveredCard.HP--;

					//PlayCardSfx( Card.GetHurtSfxFilename( hoveredCard.CardType ), hoveredCard );
					hoveredCard.PlayMenuHurtSfx();
				}
				else
				{
					hoveredCard.PlayMenuDeathSfx();

					var breakNum = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );
					hoveredCard.Explode( breakNum );
				}

				//Scene.TimeScale = 0.5f;
			}
		}
	}

	async void UpdateAsync()
	{
		while ( IsLevelActive )
		{
			if ( ChosenCard != null && !ChosenCards.Contains( ChosenCard ) )
			{
				//ChosenCard = null;
				await ChooseCardAsync( ChosenCard );
			}

			ChosenCard = null;

			if ( _hasMadeFirstMove && !IsDead )
				await HandleTimerAsync();

			if ( IsDead )
			{
				Manager.Instance.PlaySfxCenter( "player_death", volume: 0.33f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

				foreach ( var card in Cards )
					card.SetRevealedFromAsync( true );

				await Task.DelayRealtime( 1500 );

				GameOver();
			}

			await Task.Frame();
		}

		//Log.Info( "UpdateAsync Ended" );

		if ( ShouldRestart )
		{
			StartNewRun();
			ShouldRestart = false;
		}

		if ( ShouldReturnToMenu )
		{
			StartMenu();
			ShouldReturnToMenu = false;
		}
	}

	async Task HandleTimerAsync()
	{
		TimerElapsed = Time.Now - TimerMoveStartTime;
		float totalTime = GetTimerTotalTime();

		if ( TimerElapsed > (int)totalTime )
		{
			var camera = Scene.Camera;
			//var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width * 0.05f, Screen.Height * 0.85f, 0f ) );
			var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width * 0.5f, Screen.Height * 0.5f, 0f ) );
			var tr = Scene.Trace.Ray( ray, 10000f ).Run();

			int lossAmount = (int)Stats[StatType.TimerRunOutHPLoss];

			SpawnFloaterText(
				pos: (tr.EndPosition + new Vector3( 0f, 25f, 0f )).WithZ( 100f ),
				text: $"Time ran out!",
				emojiText: "",
				lifetime: 2f,
				color: new Color( 1f, 1f, 1f ),
				velocity: new Vector2( 0f, -32f ),
				deceleration: 2.2f,
				fontSize: 50f,
				startScale: 1.1f,
				endScale: 0.9f
			);

			Manager.Instance.SpawnFloaterText(
				pos: tr.EndPosition.WithZ( 100f ),
				text: $"-{lossAmount}",
				emojiText: "❤️",
				lifetime: 2f,
				color: new Color( 0.8f, 0f, 0f ),
				velocity: new Vector2( 0f, -32f ),
				deceleration: 2.2f,
				fontSize: 60f,
				startScale: 1.1f,
				endScale: 0.9f
			);

			TimeSinceTimerChanged = 0f;

			await LoseHP( lossAmount );
			await EventHappened( EventType.TimerRanOut );

			TimerElapsed = 0f;
			TimerMoveStartTime = Time.Now;
			_currRemainingSeconds = (int)totalTime;
		}
		else 
		{
			int remainingSeconds = MathX.FloorToInt(MathF.Max( totalTime - TimerElapsed, 0 ) );
			if ( remainingSeconds != _currRemainingSeconds )
			{
				float volume = Utils.Map( remainingSeconds, totalTime, 0f, 0f, 1f, EasingType.SineIn );
				if( _currRemainingSeconds % 2 == ( (int)totalTime % 2 == 0 ? 1 : 0 ) )
					PlaySfx("clock_tick", new Vector3( -35f, 0f, Camera.WorldPosition.z - 100f ), volume, pitch: 1.1f);
				else
					PlaySfx( "clock_tock", new Vector3( -35f, 0f, Camera.WorldPosition.z - 100f ), volume, pitch: 0.95f );

				_currRemainingSeconds = remainingSeconds;
			}
		}

		//await Task.Frame();
	}

	public async Task EventHappened( EventType eventType )
	{
		//Cards.Shuffle();
		for(int i = Cards.Count - 1; i >= 0; i-- )
		{
			var card = Cards[i];
			if ( card.ShouldHandleEvent( eventType ) )
				await card.HandleEventAsync( eventType );
		}

		await HandleStatusEvent( eventType );
		await HandleRelicEvent( eventType );
	}

	async Task EventHappened( EventType eventType, Card card0 )
	{
		if ( card0.ShouldHandleEvent( eventType ) )
			await card0.HandleEventAsync( eventType );
			
		//Cards.Shuffle();
		for ( int i = Cards.Count - 1; i >= 0; i-- )
		{
			var card = Cards[i];

			if ( card == card0 )
				continue;

			if ( card.ShouldHandleEvent( eventType ) )
				await card.HandleEventAsync( eventType );
		}

		await HandleStatusEvent( eventType );
		await HandleRelicEvent( eventType );
	}

	async Task EventHappened( EventType eventType, Card card0, Card card1 )
	{
		if ( card0.ShouldHandleEvent( eventType ) )
			await card0.HandleEventAsync( eventType );

		if ( card1.ShouldHandleEvent( eventType ) ) 
			await card1.HandleEventAsync( eventType );

		//Cards.Shuffle();

		for ( int i = Cards.Count - 1; i >= 0; i-- )
		{
			var card = Cards[i];

			if ( card == card0 || card == card1 )
				continue;

			if ( card.ShouldHandleEvent( eventType ) )
				await card.HandleEventAsync( eventType );
		}

		await HandleStatusEvent( eventType );
		await HandleRelicEvent( eventType );
	}

	async Task HandleRelicEvent(EventType eventType)
	{
		for ( int i = Relics.Count - 1; i >= 0; i-- )
		{
			var relic = Relics[i];
			if ( relic.ShouldHandleEvent( eventType ) )
				await relic.HandleEventAsync( eventType );
		}
	}

	async Task HandleStatusEvent( EventType eventType )
	{
		for ( int i = Statuses.Count - 1; i >= 0; i-- )
		{
			var status = Statuses[i];
			if ( status.ShouldHandleEvent( eventType ) )
				await status.HandleEventAsync( eventType );
		}
	}

	async Task CheckMatchAsync()
	{
		var card0 = ChosenCards[0];
		var card1 = ChosenCards[1];
		bool isMatch = (card0.CardType == card1.CardType) && !card0.IsLocked && !card1.IsLocked && card0.CanBeMatched() && card1.CanBeMatched();

		await Task.DelayRealtime( 300 );

		if (isMatch)
		{
			Stats[StatType.NumMatches]++;
			Stats[StatType.ConsecutiveMatches]++;
			Stats[StatType.ConsecutiveMismatches] = 0f;

			StatsMatchCard( card0.CardType );

			PlayCardSfxBetween( "match", card0, card1, volume: 1.75f, pitch: Game.Random.Float( 0.95f, 1.1f ), depthDiff: 60f );

			card0.StartScaling( time: 0.25f, amount: 1.1f, easingType: EasingType.SineInOut );
			card1.StartScaling( time: 0.25f, amount: 1.1f, easingType: EasingType.SineInOut );

			await CheckBounty( card0.OriginalCardType );
			await EventHappened( EventType.Match, card0, card1 );
		}
		else
		{
			Stats[StatType.NumMismatches]++;
			Stats[StatType.ConsecutiveMatches] = 0f;
			Stats[StatType.ConsecutiveMismatches]++;

			await Manager.Instance.ShakeCard( card0, time: Game.Random.Float( 0.3f, 0.5f ), easingType: EasingType.QuadOut, playSfx: false );
			await Manager.Instance.ShakeCard( card1, time: Game.Random.Float( 0.3f, 0.5f ), easingType: EasingType.QuadOut, playSfx: false );

			PlayCardSfxBetween( "wrong", card0, card1, volume: 1.75f, pitch: Game.Random.Float( 0.9f, 1.1f ), depthDiff: 150f );

			await Task.DelayRealtime( 250 );

			if( (int)Stats[StatType.NumMismatches] <= (int)Stats[StatType.ShieldAmount] )
			{
				// do nothing
			}
			else
			{
				await LoseHP( 1 );
			}

			await Task.DelayRealtime( 150 );

			IsMismatchALockedMatch = (card0.CardType == card1.CardType) && (card0.IsLocked || card1.IsLocked);

			await EventHappened( EventType.Mismatch, card0, card1 );
		}

		await Task.DelayRealtime( 200 );

		if ( isMatch )
		{
			bool shouldRemove = true;

			if(Card.HasHP(card0.CardType))
			{
				await card0.Hurt();
				await card1.Hurt();

				card0.PlayHurtSfx( card0, card1 );

				await EventHappened( EventType.HurtCards, card0, card1 );

				await Task.DelayRealtime( 300 );

				shouldRemove = card0.HP <= 0 || card1.HP <= 0;
			}

			if(shouldRemove)
			{
				var numRemainingCardPairs = CardPairsExisting.Count - CardPairsMatched.Count;
				//Log.Info( $"numRemainingCardPairs: {numRemainingCardPairs}" );
				int explodeTime = (int)(Utils.Map( Stats[StatType.ConsecutiveMatches], 0f, 4f, Game.Random.Float( 300f, 350f ), 100f, EasingType.SineOut ) * Utils.Map( numRemainingCardPairs, 8, 1, 1f, 0.4f, EasingType.SineIn));

				card0.StartExploding( explodeTime / 1000f );
				card1.StartExploding( explodeTime / 1000f );

				await Task.DelayRealtime( explodeTime );

				if ( CardPairsMatched.ContainsKey( card0.OriginalCardType ) )
					CardPairsMatched[card0.OriginalCardType]++;
				else
					CardPairsMatched.Add( card0.OriginalCardType, 1 );

				CardTypePanelHash++;

				Cards.Remove( card0 );
				Cards.Remove( card1 );
				_cardPositions.Remove( card0.GridPos );
				_cardPositions.Remove( card1.GridPos );

				Scene.TimeScale = 0.15f;

				var breakNum0 = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );
				var breakNum1 = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );

				while ( breakNum1 == breakNum0 )
					breakNum1 = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );

				if ( card0.IsFlagShown )
					Stats[StatType.NumFlags]--;

				if ( card1.IsFlagShown )
					Stats[StatType.NumFlags]--;

				card0.Explode( breakNum0 );
				card1.Explode( breakNum1 );
			}
			else
			{
				if(card0.IsRevealed)
				{
					HideCard( card0 );
					HideCard( card1 );

					PlayCardSfxBetween( "card_flip", card0, card1, volume: 0.9f, pitch: Game.Random.Float( 0.65f, 0.75f ) );
				}
			}
		}
		else
		{
			await Task.DelayRealtime( 150 );

			HideCard( card0 );
			HideCard( card1 );

			PlayCardSfxBetween( "card_flip", card0, card1, volume: 0.9f, pitch: Game.Random.Float( 0.65f, 0.75f ) );
		}

		ChosenCards.Clear();

		Stats[StatType.TurnNum]++;

		UpdateBounties();

		TimeSinceTurnStart = 0f;
		
		if ( !IsDead )
		{
			if ( Cards.Count == 0 )
			{
				await FinishedLevel();
			}
			else
			{
				await CheckLockedCards();
				await EventHappened( EventType.TurnStart );
			}
		}
	}

	void UpdateBounties()
	{
		bool bountyStarted = false;
		bool bountyRemoved = false;
		int turnNum = (int)Stats[StatType.TurnNum];

		for ( int i = Bounties.Count - 1; i >= 0; i-- )
		{
			var pair = Bounties.ElementAt( i );
			var bountyData = pair.Value;

			if(turnNum == bountyData.startTurn)
			{
				bountyStarted = true;
			}

			if ( turnNum >= bountyData.endTurn )
			{
				var cardType = pair.Key;
				Bounties.Remove( cardType );
				bountyRemoved = true;
			}
		}

		if ( bountyStarted )
			PlaySfxScreenPos( "bounty", new Vector2( 1800f, 400f ), volume: 1f, pitch: Game.Random.Float( 1f, 1.05f ) );

		if ( bountyRemoved )
			PlaySfxScreenPos( "bounty", new Vector2( 1800f, 600f ), volume: 0.85f, pitch: Game.Random.Float( 0.8f, 0.85f ) );
	}

	async Task CheckBounty( CardType cardType )
	{
		if ( Bounties.ContainsKey( cardType ) )
		{
			var bountyData = Bounties[cardType];

			if( Stats[StatType.TurnNum] >= bountyData.startTurn || Stats[StatType.ClaimBountiesBeforeReady] > 0f )
			{
				var camera = Scene.Camera;
				var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );
				var tr = Scene.Trace.Ray( ray, 10000f ).Run();

				PlaySfxCenter( "coin", volume: 1.3f, pitch: Game.Random.Float( 0.8f, 0.9f ) );

				await Task.DelayRealtime( 150 );

				int moneyAmount = bountyData.moneyAmount + (int)Stats[StatType.EarnExtraMoney];

				SpawnFloaterText(
					pos: (tr.EndPosition + new Vector3( 0f, 25f, 0f )).WithZ( 100f ),
					text: $"Bounty Claimed",
					emojiText: "",
					lifetime: 2f,
					color: new Color( 1f, 1f, 1f ),
					velocity: new Vector2( 0f, 35f ),
					deceleration: 2.1f,
					fontSize: 50f,
					startScale: 1f,
					endScale: 1.1f
				);

				SpawnGainMoneyFloater( moneyAmount, tr.EndPosition );

				await GainMoney( moneyAmount );

				Bounties.Remove( cardType );

				await Task.DelayRealtime( 500 );

				await EventHappened( EventType.ClaimBounty );
				await EventHappened( EventType.ClaimBountyAfter );

				await Task.DelayRealtime( 250 );
			}
		}
	}

	public async Task RevealCard( Card card )
	{
		card.SetRevealedFromAsync( true );
		RevealedCard = card;

		await EventHappened( EventType.Reveal, card );
	}

	public void HideCard( Card card )
	{
		card.SetRevealedFromAsync( false );
	}

	public async Task ShakeCard( Card card, float time = 0.6f, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
	{
		card.Shake( strength: Game.Random.Float( 20f, 30f ), time, easingType, playSfx );
		ShakenCard = card;

		await EventHappened( EventType.Shake, card );
	}

	public async Task ShakeCard( Card card, float time, float strength, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
	{
		card.Shake( strength, time, easingType, playSfx );
		ShakenCard = card;

		await EventHappened( EventType.Shake, card );
	}

	public async Task LoseHP(int amount)
	{
		HP = Math.Max( HP - amount, 0 );

		if(amount > 0)
		{
			PlaySfx( "hurt", new Vector3( -50f, 0f, Camera.WorldPosition.z - 100f ) );
			ShakeCamera( strength: Utils.Map( HP, MaxHP, 0, 1.5f, 2.5f, EasingType.SineIn ), time: Utils.Map( HP, MaxHP, 0, 0.45f, 0.6f, EasingType.SineIn ) );
			ShowOverlayColor( Color.Red.WithAlpha( Utils.Map( HP, MaxHP, 0, 0.4f, 0.7f, EasingType.SineIn ) ), time: Utils.Map( HP, MaxHP, 0, 0.175f, 0.25f, EasingType.SineIn ) );
		}

		TimeSinceHPChanged = 0f;

		await EventHappened( EventType.LoseHP );

		if (HP <= 0)
		{
			Die();
			//InitializeRandomStageSize();
		}
	}

	public void Die()
	{
		IsDead = true;

		foreach(var card in Cards)
		{
			card.IsHovered = false;
		}
	}

	public async Task GainHP( int amount )
	{
		int overhealAmount = Math.Max( HP + amount - MaxHP, 0 );

		var healAmount = Math.Min( amount, MaxHP - HP );
		Stats[StatType.LatestHealAmount] = healAmount;

		if(healAmount > 0f)
		{
			Stats[StatType.HPHealedThisLevel] += amount;

			if ( Stats[StatType.HPHealedThisLevel] > 10f )
				Sandbox.Services.Achievements.Unlock( "healer" );
		}

		//Log.Info( $"GainHP - HP: {HP} amount: {amount} MaxHP: {MaxHP} healAmount: {Stats[StatType.LatestHealAmount]} overheal: {overhealAmount}" );

		HP = Math.Min( HP + amount, MaxHP );

		TimeSinceHPChanged = 0f;

		await EventHappened( EventType.GainHP );

		if(overhealAmount > 0)
		{
			Stats[StatType.LatestOverhealAmount] = overhealAmount;

			await EventHappened( EventType.OverhealHP );
		}
	}

	public async Task GainMoney( int amount )
	{
		Money += amount;
		TimeSinceMoneyChanged = 0f;

		await EventHappened( EventType.GainMoney );
	}

	public async Task LoseMoney( int amount )
	{
		Money = Math.Max( Money - amount, 0 );
		TimeSinceMoneyChanged = 0f;

		await EventHappened( EventType.LoseMoney );
	}

	public void SpendMoney( int amount )
	{
		if ( amount <= 0 )
			return;

		if(Money < amount)
		{
			Log.Error( $"SpendMoney - not enough money!" );
			return;
		}

		Money -= amount;
		TimeSinceMoneyChanged = 0f;
	}

	//void InitializeRandomStageSize()
	//{
	//	int numCardPairs = Game.Random.Int( 3, 25 );
	//	while(numCardPairs == 11 || numCardPairs == 13 || numCardPairs == 17 || numCardPairs == 18 || numCardPairs == 19 || numCardPairs == 21 || numCardPairs == 22 || numCardPairs == 23 || numCardPairs == 24 )
	//		numCardPairs = Game.Random.Int( 3, 25 );

	//	//numCardPairs = 25;

	//	var cardPairTypes = new List<CardType>();
	//	for (int i = 0; i < numCardPairs; i++ )
	//	{
	//		CardType cardType = CardType.None;
	//		while(!AllowStartingCardType(cardType))
	//			cardType = (CardType)Game.Random.Int( 1, Enum.GetValues( typeof( CardType ) ).Length - 1 );

	//		cardPairTypes.Add( cardType );
	//	}

	//	InitializeStage( cardPairTypes );
	//}

	bool AllowStartingCardType(CardType cardType)
	{
		return cardType != CardType.None && cardType != CardType.UmbrellaClosed;
	}

	public Card CreateCard( IntVector2 gridPos, CardType cardType, int cardTypeID, bool createOutline = true )
	{
		//Log.Info( $"CreateCard: {gridPos} {cardType}" );

		//var cardObj = CardPrefab.Clone( _cardContainer.WorldPosition, Rotation.FromPitch(180f) );
		var cardObj = CardPrefab.Clone( new Vector3( 999f, 999f, 999f ), Rotation.FromPitch( 180f ) );
		cardObj.Parent = _cardContainer;
		//cardObj.LocalPosition = GetCardPos( gridPos );

		cardObj.Name = $"card ({cardType}) - {cardTypeID}";

		var className = $"Card{cardType}";

		var typeDesc = TypeLibrary.GetType<Card>( className );
		var card = (Card)cardObj.Components.Create( typeDesc );

		if ( card == null )
		{
			Log.Error( $"Failed to create card of type {className}!" );
			return null;
		}

		card.Init(cardType);
		card.GridPos = gridPos;
		card.CardTypeID = cardTypeID;

		Cards.Add( card );
		_cardPositions.Add( gridPos, card );

		if(createOutline)
		{
			var outlineObj = CardOutlinePrefab.Clone();
			outlineObj.Parent = _cardContainer;

			var cardPos = GetCardPos( gridPos );
			outlineObj.LocalPosition = new Vector3( cardPos.x, cardPos.y - 1f, 2f );
		}

		return card;
	}

	public MenuCard CreateMenuCard( Vector3 pos )
	{
		var cardObj = MenuCardPrefab.Clone( pos );

		var menuCard = cardObj.Components.Get<MenuCard>();

		return menuCard;
	}

	public async Task ChooseCardAsync( Card card )
	{
		IsMoveResolving = true;
		TimeSinceMoveStartResolving = 0f;
		await ForceCursorSwitch();

		_hasMadeFirstMove = true;

		card.StartScaling( time: 0.25f, amount: 1.15f, easingType: EasingType.SineInOut );

		card.NumTimesChosen++;

		ChosenCards.Add( card );
		PlayCardSfx( "card_flip", card, pitch: Game.Random.Float(0.9f, 1.1f) );

		await RevealCard( card );

		if ( card.IsFlagShown )
		{
			card.HideFlag();
			Stats[StatType.NumFlags]--;
		}

		await EventHappened( EventType.Choose, card );

		bool resetTimer = Stats[StatType.TimerResetAfterBothChoices] > 0f ? (ChosenCards.Count == 2) : true;

		if ( ChosenCards.Count == 2 )
			await CheckMatchAsync();

		IsMoveResolving = false;
		await ForceCursorSwitch();

		if( resetTimer )
		{
			TimerElapsed = 0f;
			TimerMoveStartTime = Time.Now;
		}
	}

	public static Vector3 GetCardPos(IntVector2 gridPos)
	{
		return new Vector3( gridPos.x * GRID_X_SPACING, gridPos.y * GRID_Y_SPACING, Globals.CARD_DEFAULT_HEIGHT);
	}

	public Card GetCardAtGridPos(IntVector2 gridPos)
	{
		if ( !IsGridPosInBounds( gridPos ) )
			return null;

		if(_cardPositions.ContainsKey(gridPos))
			return _cardPositions[gridPos];

		return null;
	}

	public Card GetCardAtGridPos( int x, int y )
	{
		return GetCardAtGridPos( new IntVector2( x, y ) );
	}

	public Card GetRandomCard()
	{
		if ( Cards.Count == 0 )
			return null;

		return Cards[Game.Random.Int( 0, Cards.Count - 1 )];
	}

	public Card GetRandomCard(Card except0, Card except1)
	{
		if ( Cards.Count == 0 )
			return null;

		var cards = Cards.Where(x => x != except0 && x != except1);
		if ( cards.Count() == 0 )
			return null;

		return cards.ElementAt(Game.Random.Int( 0, cards.Count() - 1 ));
	}

	public Card GetRandomCard( Card except0, Card except1, Card except2 )
	{
		if ( Cards.Count == 0 )
			return null;

		var cards = Cards.Where( x => x != except0 && x != except1 && x != except2 );
		if ( cards.Count() == 0 )
			return null;

		return cards.ElementAt( Game.Random.Int( 0, cards.Count() - 1 ) );
	}

	public Card GetRandomCard( List<Card> except )
	{
		if ( Cards.Count == 0 )
			return null;

		var cards = Cards.ToList();
		cards.Shuffle();

		foreach( var card in cards )
		{
			if ( !except.Contains( card ) )
				return card;
		}

		return null;
	}

	public List<IntVector2> GetAllGridPositions(bool empty)
	{
		List<IntVector2> allEmptyGridPositions = new();

		for ( int x = 0; x < GridWidth; x++ )
		{
			for ( int y = 0; y < GridHeight; y++ )
			{
				var gridPos = new IntVector2( x, y );
				var card = GetCardAtGridPos( gridPos );
				if ( (empty && card == null) || (!empty && card != null) )
					allEmptyGridPositions.Add( gridPos );
			}
		}

		return allEmptyGridPositions;
	}

	public List<IntVector2> GetRandomEmptyGridPositions(int num)
	{
		List<IntVector2> output = new();
		List<IntVector2> allEmptyGridPositions = new();

		for ( int x = 0; x < GridWidth; x++ )
		{
			for ( int y = 0; y < GridHeight; y++ )
			{
				var gridPos = new IntVector2( x, y );
				if(GetCardAtGridPos(gridPos) == null )
					allEmptyGridPositions.Add( gridPos );
			}
		}

		allEmptyGridPositions.Shuffle();

		for( int i = 0; i < Math.Min(num, allEmptyGridPositions.Count); i++ )
		{
			output.Add( allEmptyGridPositions[i]);
		}

		return output;
	}

	public IntVector2 GetRandomGridPos(IntVector2 except)
	{
		IntVector2 gridPos = new IntVector2( Game.Random.Int( 0, GridWidth - 1 ), Game.Random.Int( 0, GridHeight - 1 ) );
		
		while(gridPos.x == except.x && gridPos.y == except.y )
			gridPos = new IntVector2( Game.Random.Int( 0, GridWidth - 1 ), Game.Random.Int( 0, GridHeight - 1 ) );

		return gridPos;
	}

	public List<Card> GetCardsOfType(CardType cardType, Card except = null)
	{
		return Cards.Where( x => x.CardType == cardType && x != except ).ToList();
	}

	public bool IsGridPosEmpty( IntVector2 gridPos )
	{
		return !_cardPositions.ContainsKey( gridPos );
	}

	public bool IsGridPosCorner(IntVector2 gridPos)
	{
		return (gridPos.x == 0 && gridPos.y == 0) || (gridPos.x == 0 && gridPos.y == GridHeight - 1) || (gridPos.x == GridWidth - 1 && gridPos.y == 0) || (gridPos.x == GridWidth - 1 && gridPos.y == GridHeight - 1);
	}

	public async Task SetCardGridPos(Card card, IntVector2 gridPos)
	{
		if ( !IsGridPosInBounds( gridPos ) )
		{
			Log.Error( $"SetCardGridPos - gridpos {gridPos} is not in bounds!" );
			return;
		}

		if ( _cardPositions.ContainsKey( gridPos ) )
		{
			Log.Error( $"SetCardGridPos - {gridPos} isn't empty!" );
			return;
		}

		if ( _cardPositions.ContainsKey( card.GridPos ) && _cardPositions[card.GridPos] == card )
		{
			Log.Error( $"SetCardGridPos - remove {card.CardType}'s old gridposition first!" );
			return;
		}

		_cardPositions[gridPos] = card;
		card.GridPos = gridPos;

		MovedCard = card;
		await EventHappened( EventType.MoveCard, card );
	}

	public void SetCardGridPosNonAsync( Card card, IntVector2 gridPos, bool instant = false )
	{
		if ( !IsGridPosInBounds( gridPos ) )
		{
			Log.Error( $"SetCardGridPosNonAsync - gridpos {gridPos} is not in bounds!" );
			return;
		}

		if ( _cardPositions.ContainsKey( gridPos ) )
		{
			Log.Error( $"SetCardGridPosNonAsync - {gridPos} isn't empty!" );
			return;
		}

		if ( _cardPositions.ContainsKey( card.GridPos ) && _cardPositions[card.GridPos] == card )
		{
			Log.Error( $"SetCardGridPosNonAsync - remove {card.CardType}'s old gridposition first!" );
			return;
		}

		_cardPositions[gridPos] = card;
		card.GridPos = gridPos;

		if ( instant )
			card.LocalPosition = GetCardPos( gridPos );
	}

	public void RemoveCardGridPos(Card card)
	{
		if(!_cardPositions.ContainsKey(card.GridPos))
		{
			Log.Error( $"RemoveCardPosition - {card.GridPos} doesn't have a card!" );
			return;
		}

		var existingCard = _cardPositions[card.GridPos];
		if(existingCard != card)
		{
			Log.Error( $"RemoveCardGridPos ({card.CardType}) - A different card ({existingCard.CardType}) exists at {card.GridPos}!" );
		}

		_cardPositions.Remove( card.GridPos );
	}

	public async Task SwapCardPositions( Card card0, Card card1, bool instant = false )
	{
		var gridPos0 = card0.GridPos;
		var gridPos1 = card1.GridPos;

		//Log.Info( $"SwapCardPositions - {card0.CardType} {card0.GridPos} {GetCardAtGridPos( card0.GridPos )}, {card1.CardType} {card1.GridPos} {GetCardAtGridPos( card1.GridPos )}" );

		RemoveCardGridPos( card0 );
		RemoveCardGridPos( card1 );

		SetCardGridPosNonAsync( card0, gridPos1, instant );
		SetCardGridPosNonAsync( card1, gridPos0, instant );

		SwappedCards.Clear();
		SwappedCards.Add( card0 );
		SwappedCards.Add( card1 );
		await EventHappened( EventType.SwapCards, card0, card1 );
	}

	public void SwapCardPositionsNonAsync( Card card0, Card card1, bool instant = false )
	{
		var gridPos0 = card0.GridPos;
		var gridPos1 = card1.GridPos;

		RemoveCardGridPos( card0 );
		RemoveCardGridPos( card1 );

		SetCardGridPosNonAsync( card0, gridPos1, instant );
		SetCardGridPosNonAsync( card1, gridPos0, instant );
	}

	public bool IsGridPosInBounds(IntVector2 gridPos)
	{
		return gridPos.x >= 0 && gridPos.x < GridWidth && gridPos.y >= 0 && gridPos.y < GridHeight;
	}

	public List<IntVector2> GetNearbyGridPositions( IntVector2 gridPos, bool adjacentOnly = false)
	{
		List<IntVector2> offsets = adjacentOnly
			? new() { new IntVector2( -1, 0 ), new IntVector2( 0, 1 ), new IntVector2( 1, 0 ), new IntVector2( 0, -1 ) }
			: new() { new IntVector2( -1, -1 ), new IntVector2( -1, 0 ), new IntVector2( -1, 1 ), new IntVector2( 0, 1 ), new IntVector2( 1, 1 ), new IntVector2( 1, 0 ), new IntVector2( 1, -1 ), new IntVector2( 0, -1 ) };

		List<IntVector2> gridPositions = new();

		foreach ( var offset in offsets )
		{
			if( IsGridPosInBounds( gridPos + offset ) )
				gridPositions.Add( gridPos + offset );
		}

		return gridPositions;
	}

	public List<Card> GetNearbyCards(IntVector2 gridPos, bool adjacentOnly = false )
	{
		List<Card> nearbyCards = new();
		var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );

		foreach ( var nearbyGridPos in nearbyGridPositions )
		{
			var card = GetCardAtGridPos( nearbyGridPos );
			if ( card != null )
				nearbyCards.Add( card );
		}

		return nearbyCards;
	}

	public int GetNumNearbyCards( IntVector2 gridPos, bool adjacentOnly = false )
	{
		var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );

		int count = 0;
		foreach ( var nearbyGridPos in nearbyGridPositions )
		{
			var card = GetCardAtGridPos( nearbyGridPos );
			if ( card != null )
				count++;
		}

		return count;
	}

	public Card GetRandomNearbyCard( IntVector2 gridPos, bool adjacentOnly = false )
	{
		List<Card> nearbyCards = new();
		var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );

		foreach ( var nearbyGridPos in nearbyGridPositions )
		{
			var card = GetCardAtGridPos( nearbyGridPos );
			if ( card != null )
				nearbyCards.Add( card );
		}

		if ( nearbyCards.Count == 0 )
			return null;

		return nearbyCards[Game.Random.Int(0, nearbyCards.Count - 1)];
	}

	public int GetNumNearbyEmptyGridPositions( IntVector2 gridPos, bool adjacentOnly = false )
	{
		var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );

		int count = 0;
		foreach ( var nearbyGridPos in nearbyGridPositions )
		{
			var card = GetCardAtGridPos( nearbyGridPos );
			if ( card == null )
				count++;
		}

		return count;
	}

	public IntVector2 GetRandomNearbyGridPos( IntVector2 gridPos, bool empty, bool adjacentOnly = false )
	{
		List<IntVector2> validGridPositions = new();
		var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );

		foreach ( var nearbyGridPos in nearbyGridPositions )
		{
			var card = GetCardAtGridPos( nearbyGridPos );
			if ( (empty && card == null) || (!empty && card != null) )
				validGridPositions.Add( nearbyGridPos );
		}

		if ( validGridPositions.Count == 0 )
			return new IntVector2(-1, -1);

		return validGridPositions[Game.Random.Int( 0, validGridPositions.Count - 1 )];
	}

	public List<IntVector2> GetNearbyGridPositions( IntVector2 gridPos, bool empty, bool adjacentOnly = false )
	{
		List<IntVector2> validGridPositions = new();
		var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );

		foreach ( var nearbyGridPos in nearbyGridPositions )
		{
			var card = GetCardAtGridPos( nearbyGridPos );
			if ( (empty && card == null) || (!empty && card != null) )
				validGridPositions.Add( nearbyGridPos );
		}

		return validGridPositions;
	}

	public static bool IsAdjacent(IntVector2 posA, IntVector2 posB)
	{
		return (posA - posB).ManhattanLength == 1;
	}

	public static bool IsNearby( IntVector2 posA, IntVector2 posB )
	{
		return MathF.Abs( posA.x - posB.x ) <= 1 && MathF.Abs( posA.y - posB.y ) <= 1;
	}

	static Tuple<int, int> FindBestGridSize( int numCards )
	{
		int maxHeight = 5;
		int maxWidth = 10;

		int bestWidth = 0;
		int bestHeight = 0;
		int bestDiff = int.MaxValue;

		// Loop through possible widths in descending order
		for ( int width = Math.Min( maxWidth, numCards ); width >= 1; width-- )
		{
			if ( numCards % width == 0 )
			{
				int height = numCards / width;
				if ( height <= maxHeight )
				{
					int diff = Math.Abs( width - height );
					if ( diff < bestDiff || (diff == bestDiff && width > bestWidth) )
					{
						bestDiff = diff;
						bestWidth = width;
						bestHeight = height;
					}
				}
			}
		}

		if(bestWidth == 0 || bestHeight == 0)
		{
			Log.Error( $"No grid size found for {numCards / 2} pairs!" );
		}

		return Tuple.Create( bestWidth, bestHeight );
	}

	async Task FinishedLevel()
	{
		await Task.DelayRealtime( 750 );

		BeatPreviousLevelTime = TimeSinceRunStart.Relative;
		//Log.Info( $"BeatPreviousLevelTime: {BeatPreviousLevelTime} LevelNum: {LevelNum}" );

		if (LevelNum == 10)
		{
			foreach ( var child in _cardContainer.Children )
				child.Destroy();

			Victory();
		}
		else
		{
			await EventHappened( EventType.FinishLevel );

			await Task.DelayRealtime( 300 );

			foreach ( var child in _cardContainer.Children )
				child.Destroy();

			switch(LevelNum)
			{
				case 2:
					Sandbox.Services.Achievements.Unlock( "defeat_ogre" );
					break;
				case 4:
					Sandbox.Services.Achievements.Unlock( "defeat_clown" );
					break;
				case 6:
					Sandbox.Services.Achievements.Unlock( "defeat_police" );
					break;
				case 8:
					Sandbox.Services.Achievements.Unlock( "defeat_king" );
					break;
			}

			TimerElapsed = 0f;

			StartBuyPhase();
		}
	}

	public void StartBuyPhase()
	{
		DetermineOfferedRelics();

		IsLevelActive = false;
		GameState = GameState.BuyPhase;

		Manager.Instance.PlaySfxCenter( "shop", volume: 0.3f, pitch: Game.Random.Float( 0.95f, 1.05f ) );

		PlaySong( SongShop );

		FadeIn();
	}

	public void DetermineOfferedRelics()
	{
		BuyPhaseOfferedRelics.Clear();

		List<(RelicType, float)> relicWeights = new();

		bool offerBloodDonation = LevelNum == 1 ? true : Game.Random.Float( 0f, 1f ) < Utils.Map( LevelNum, 2, 9, 0.35f, 0.2f );

		AddRelicWeight( RelicType.Bandage, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 5f, 0.8f, EasingType.QuadOut ), extraMoney: offerBloodDonation ? 3 : 0 );
		//AddRelicWeight( RelicType.Bandage, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.4f, 0.2f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0);
		AddRelicWeight( RelicType.Salad, relicWeights, weight: Utils.Map( LevelNum, 1, 4, 1f, 0.45f ), extraMoney: offerBloodDonation ? 3 : 0 );
		//AddRelicWeight( RelicType.Salad, relicWeights, weight: Utils.Map( LevelNum, 1, 4, 0.25f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0);
		AddRelicWeight( RelicType.Chocolate, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.55f, 2f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.BloodDonation, relicWeights, weight: offerBloodDonation ? float.MaxValue : 0f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Shield, relicWeights, weight: Utils.Map( LevelNum, 1, 8, 0.5f, 0.4f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Socks, relicWeights, weight: Utils.Map( LevelNum, 1, 3, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Pill, relicWeights, minLevel: 2, weight: 0.7f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Butter, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.2f, 0.3f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.ShoppingCart, relicWeights, weight: Utils.Map( LevelNum, 1, 5, 0.15f, 0.4f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Trophy, relicWeights, weight: Utils.Map( LevelNum, 1, 5, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Flag, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.2f, 0.4f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Crayon, relicWeights, minLevel: 2, weight: Utils.Map( LevelNum, 1, 6, 0.1f, 0.5f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Wristwatch, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.4f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.PrayerBeads, relicWeights, minLevel: 2, weight: Utils.Map( LevelNum, 2, 8, 0.2f, 0.6f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.MedicHelmet, relicWeights, weight: Utils.Map( LevelNum, 1, 4, 0.1f, 0.6f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.MagnifyingGlass, relicWeights, weight: Utils.Map( LevelNum, 1, 8, 0.5f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Battery, relicWeights, minLevel: 2, weight: 0.225f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.SlotMachine, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.MedicalLicense, relicWeights, minLevel: 2, weight: 0.25f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Magic8Ball, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.15f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.RoboAssistant, relicWeights, weight: 0.175f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.GuideDog, relicWeights, minLevel: 3, weight: 0.2f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.LoveLetter, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.08f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Taxi, relicWeights, minLevel: 4, weight: 0.2f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Microscope, relicWeights, minLevel: 3, weight: Utils.Map( LevelNum, 3, 0, 0.08f, 0.3f, EasingType.SineIn ), extraMoney: offerBloodDonation ? 3 : 0 );
		//AddRelicWeight( RelicType.Satellite, relicWeights, minLevel: 2, weight: 0.3f, extraMoney: offerBloodDonation ? 3 : 0);
		AddRelicWeight( RelicType.Satellite, relicWeights, minLevel: 2, weight: Utils.Map( LevelNum, 2, 9, 0.1f, 0.6f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Coupon, relicWeights, maxLevel: 8, weight: Utils.Map( LevelNum, 1, 8, 0f, 0.05f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.WarMedal, relicWeights, weight: Utils.Map( LevelNum, 1, 7, 0.25f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.HotPot, relicWeights, weight: Utils.Map( LevelNum, 1, 5, 0.1f, 0.3f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Abacus, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		//AddRelicWeight( RelicType.Briefcase, relicWeights, weight: Utils.Map( LevelNum, 1, 7, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0);
		AddRelicWeight( RelicType.Saw, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Sign, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.12f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.TriangleRuler, relicWeights, minLevel: 3, weight: Utils.Map( LevelNum, 3, 9, 0.1f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Chopsticks, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.RecycleBin, relicWeights, weight: 0.17f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Clipboard, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Juicebox, relicWeights, weight: 0.1f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.SacrificialBlade, relicWeights, weight: 0.125f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Scale, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.075f, 0.2f, EasingType.SineOut ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Telescope, relicWeights, weight: Utils.Map( LevelNum, 3, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.BowAndArrow, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.25f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.MantlepieceClock, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Dartboard, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.25f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.PirateFlag, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.15f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.Revolver, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.MammothMeat, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.8f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.GrandfatherClock, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.8f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.PersonalChef, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.5f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.MouseTrap, relicWeights, weight: 0.15f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.KitchenKnife, relicWeights, weight: 0.15f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.LightBulb, relicWeights, weight: 0.125f, extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.RunningShoes, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
		AddRelicWeight( RelicType.CreditCard, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.025f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );

		NumRelicsToShow = MathX.FloorToInt( Utils.Map( LevelNum, 1, 7, 6f, 12f ) );
		//NumRelicsToShow = 12;
		for ( int i = 0; i < NumRelicsToShow + 3; i++ )
		{
			var relicType = GetOfferedRelicType( relicWeights );
			if ( relicType != RelicType.None )
			{
				BuyPhaseOfferedRelics.Add( relicType );

				int index = relicWeights.FindIndex( x => x.Item1.Equals( relicType ) );

				if ( index >= 0 )
					relicWeights.RemoveAt( index );
			}
		}

		// shuffle BloodDonation
		if( LevelNum == 1 && BuyPhaseOfferedRelics.Count >= 6 && BuyPhaseOfferedRelics[0] == RelicType.BloodDonation )
		{
			int otherRelicIndex = Game.Random.Int( 1, 5 );
			RelicType otherRelicType = BuyPhaseOfferedRelics[otherRelicIndex];
			BuyPhaseOfferedRelics[otherRelicIndex] = RelicType.BloodDonation;
			BuyPhaseOfferedRelics[0] = otherRelicType;
		}

		BuyPhaseHash++;

		BoughtItems.Clear();
		for ( int i = 0; i < BuyPhaseOfferedRelics.Count; i++ )
			BoughtItems.Add( false );
	}

	void AddRelicWeight( RelicType relicType, List<(RelicType, float)> relicWeights, int minLevel = 0, int maxLevel = MAX_LEVEL, float weight = 1f, int extraMoney = 0)
	{
		if ( LevelNum < minLevel || LevelNum > maxLevel )
			return;

		int currLevel = GetRelicLevel( relicType );
		var price = Relic.GetRelicPrice( relicType, currLevel + 1 );

		var costsHP = Relic.DoesRelicCostHP( relicType ) || Stats[StatType.AllItemsCostHP] > 0f;
		int currency = costsHP ? HP - 1: Money;
		if ( currency < (costsHP ? price : price - extraMoney) && currency > 0 )
			return;

		foreach ( var pair in relicWeights )
		{
			if ( pair.Item1 == relicType )
				currLevel++;
		}

		var existingRelic = GetRelic( relicType );
		if ( existingRelic != null && currLevel >= existingRelic.MaxLevel )
			return;

		var existingLevel = existingRelic?.Level ?? 0;
		if(existingLevel > 0 && Stats[StatType.ExistingItemExtraChance] > 0f )
			weight *= (1f + Stats[StatType.ExistingItemExtraChance]);

		relicWeights.Add( (relicType, weight) );
	}

	RelicType GetOfferedRelicType( List<(RelicType, float)> relicWeights )
	{
		var totalWeight = 0f;
		foreach(var pair in relicWeights)
		{
			totalWeight += pair.Item2;
		}

		var randWeight = Game.Random.Float( 0f, totalWeight );

		totalWeight = 0f;
		foreach ( var pair in relicWeights )
		{
			totalWeight += pair.Item2;
			if ( totalWeight > randWeight )
			{
				return pair.Item1;
			}
		}

		return RelicType.None;
	}

	async Task CheckLockedCards()
	{
		foreach(var card in Cards)
		{
			if(card.IsLocked)
			{
				if(card.WasJustLocked)
				{
					card.WasJustLocked = false;
				}
				else
				{
					card.LockTurnsRemaining--;
					if ( card.LockTurnsRemaining <= 0 )
					{
						await UnlockCard( card );
					}
				}
			}
		}
	}

	public void GameOver()
	{
		IsLevelActive = false;
		GameState = GameState.Failure;
		FinalRunTime = LevelNum > 1 ? BeatPreviousLevelTime : TimeSinceRunStart.Relative;

		PlaySong( SongDefeat );

		SubmitScore( levelsBeat: LevelNum - 1, runTime: FinalRunTime );

		Sandbox.Services.Stats.Increment( $"died-level-{LevelNum}", 1 );
	}

	public void Victory()
	{
		IsLevelActive = false;
		GameState = GameState.Victory;
		FinalRunTime = TimeSinceRunStart.Relative;

		PlaySong( SongVictory );

		SubmitScore( levelsBeat: 10, runTime: TimeSinceRunStart.Relative );

		Sandbox.Services.Achievements.Unlock( "defeat_wizard" );
		Sandbox.Services.Stats.Increment( "game_wins", 1 );
	}

	public void SubmitScore(int levelsBeat, float runTime)
	{
		if ( levelsBeat <= 0 )
			return;

		// lower score is better
		float scorePerLevel = 1000000f;
		float baseScore = scorePerLevel * (10 - levelsBeat);
		float totalScore = baseScore + runTime;

		Sandbox.Services.Stats.SetValue( "total_score", totalScore );
		//Log.Info( $"submitting score: {totalScore}" );
	}

	public void PlayCardSfx( string name, Card card, float volume = 1f, float pitch = 1f, float depthDiff = Globals.CARD_SFX_DEPTH_DIFF )
	{
		var pos = card.WorldPosition.WithZ( Camera.WorldPosition.z - depthDiff );
		PlaySfx( name, pos, volume, pitch );
	}

	public void PlayCardSfxBetween( string name, Card card0, Card card1, float volume = 1f, float pitch = 1f, float depthDiff = Globals.CARD_SFX_DEPTH_DIFF )
	{
		var pos = ((card0.WorldPosition + card1.WorldPosition) / 2f).WithZ( Camera.WorldPosition.z - depthDiff );
		PlaySfx( name, pos, volume, pitch );
	}

	public void PlayCardSfxBetweenGridPositions( string name, IntVector2 gridPos0, IntVector2 gridPos1, float volume = 1f, float pitch = 1f, float depthDiff = Globals.CARD_SFX_DEPTH_DIFF )
	{
		var pos = (((GetCardPos(gridPos0) + GetCardPos(gridPos1)) / 2f) + _cardContainer.WorldPosition).WithZ( Camera.WorldPosition.z - depthDiff );
		PlaySfx( name, pos, volume, pitch );
	}

	public void PlaySfxCenter( string name, float volume = 1f, float pitch = 1f )
	{
		var camera = Scene.Camera;
		var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );
		var tr = Scene.Trace.Ray( ray, 10000f ).Run();

		var sfx = Sound.Play( name, SfxMixer );
		if ( sfx != null )
		{
			sfx.Position = tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF );
			sfx.Volume = volume;
			sfx.Pitch = pitch;
		}
	}

	public void PlaySfxScreenPos( string name, Vector2 screenPos, float volume = 1f, float pitch = 1f )
	{
		var camera = Scene.Camera;
		var ray = camera.ScreenPixelToRay( new Vector3(screenPos.x, screenPos.y, 0f) );
		var tr = Scene.Trace.Ray( ray, 10000f ).Run();

		var sfx = Sound.Play( name, SfxMixer );
		if ( sfx != null )
		{
			sfx.Position = tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF );
			sfx.Volume = volume;
			sfx.Pitch = pitch;
		}
	}

	public void PlaySfx( string name, Vector3 pos, float volume = 1f, float pitch = 1f )
	{
		var sfx = Sound.Play( name, SfxMixer );
		if ( sfx != null )
		{
			sfx.Position = pos;
			sfx.Volume = volume;
			sfx.Pitch = pitch;
		}
	}

	public void PlaySfx(string name)
	{
		Sound.Play( name, SfxMixer );
	}

	public void PushEventMessage(IEventHandler eventHandler, EventType eventType)
	{
		EventMessageStack.Push((eventHandler, eventType));
		EventMessageHash++;
	}

	public void PopEventMessage()
	{
		EventMessageStack.Pop();
		EventMessageHash++;
	}

	public void IncreaseRelicLevel(RelicType relicType)
	{
		Relic relic = null;

		foreach( var r in Relics )
		{
			if ( r.RelicType == relicType )
				relic = r;
		}

		if(relic == null)
		{
			var className = $"Relic{relicType}";

			var typeDesc = TypeLibrary.GetType<Relic>( className );
			relic = (Relic)_relicContainer.Components.Create( typeDesc );

			relic.RelicType = relicType;
			relic.Init();

			Relics.Add( relic );

			if(Relics.Count >= 30)
			{
				Sandbox.Services.Achievements.Unlock( "item_hoarder" );
			}
		}

		relic.LevelUp();

		RelicHash++;

		StatsBuyItem( relicType );
	}

	public Relic GetRelic(RelicType relicType)
	{
		foreach(var relic in Relics)
		{
			if ( relic.RelicType == relicType )
				return relic;
		}

		return null;
	}

	public int GetRelicLevel( RelicType relicType )
	{
		foreach ( var relic in Relics )
		{
			if ( relic.RelicType == relicType )
				return relic.Level;
		}

		return 0;
	}

	public void RemoveRelic(Relic relic)
	{
		if ( Relics.Contains( relic ) )
		{
			Relics.Remove( relic );
			RelicHash++;
		}
	}

	public StatusEffect AddStatus(string statusClassName)
	{
		var typeDesc = TypeLibrary.GetType<StatusEffect>( statusClassName );
		var status = (StatusEffect)_statusContainer.Components.Create( typeDesc );
		Statuses.Add( status );

		return status;
	}

	public void RemoveStatus(StatusEffect status)
	{
		if( Statuses.Contains( status ) )
		{
			Statuses.Remove( status );

			var component = _statusContainer.Components.Get(status.GetType());
			component?.Destroy();
		}
	}

	public void ShowOverlayColor(Color color, float time)
	{
		ShouldShowOverlayColor = true;
		TimeSinceOverlayStart = 0f;
		OverlayTime = time;
		OverlayColor = color;
	}

	void HandleOverlay()
	{
		if ( !ShouldShowOverlayColor )
			return;

		if(TimeSinceOverlayStart > OverlayTime)
			ShouldShowOverlayColor = false;
	}

	public void ShakeCamera(float strength, float time)
	{
		_isCameraShaking = true;
		_cameraShakeTime = time;
		_timeSinceCameraShakeStart = 0f;
		_cameraShakeStrength = strength;
	}

	void HandleCameraShake()
	{
		if ( !_isCameraShaking )
			return;

		if(_timeSinceCameraShakeStart >  _cameraShakeTime)
		{
			_isCameraShaking = false;
			Camera.WorldPosition = _cameraStartPos;
		}
		else
		{
			var randomVec = Utils.GetRandomVector() 
				* _cameraShakeStrength 
				* Utils.Map( _timeSinceCameraShakeStart, 0f, 0.05f, 0f, 1f, EasingType.SineIn) 
				* Utils.Map(_timeSinceCameraShakeStart, 0f, _cameraShakeTime, 1f, 0f, EasingType.SineOut);

			Camera.WorldPosition = _cameraStartPos + new Vector3( randomVec.x, randomVec.y, 0f );
		}
	}

	public void SpawnFloaterText( Vector3 pos, string text, string emojiText, float lifetime, Color color, Vector2 velocity, float deceleration, float fontSize, float startScale = 1f, float endScale = 1f )
	{
		var textObj = new GameObject();
		textObj.Name = "floater text";
		textObj.WorldPosition = pos;
		var floaterText = textObj.Components.Create<FloaterText>();
		floaterText.Init( text, emojiText, lifetime, color, velocity, deceleration, fontSize, startScale, endScale );

		NumFloaters++;
	}

	public void SpawnFloaterImage( Vector3 pos, string filename, float lifetime, Vector2 velocity, float deceleration, float startScale = 1f, float endScale = 1f )
	{
		var textObj = new GameObject();
		textObj.Name = filename;
		textObj.WorldPosition = pos;
		var floaterImage = textObj.Components.Create<FloaterImage>();
		floaterImage.Init( filename, lifetime, velocity, deceleration, startScale, endScale );

		NumFloaters++;
	}

	public void SpawnHealHPFloater( int healAmount, Vector3 pos )
	{
		SpawnFloaterText(
			pos: pos.WithZ( 100f ),
			text: $"+{healAmount}",
			emojiText: "❤️",
			lifetime: 2f,
			color: new Color( 1f, 0f, 0f ),
			velocity: new Vector2( 0f, 35f ),
			deceleration: 2.1f,
			fontSize: 60f,
			startScale: 1f,
			endScale: 1.1f
		);
	}

	public void SpawnLoseHPFloater( int lossAmount, Vector3 pos )
	{
		SpawnFloaterText(
			pos: pos.WithZ( 100f ),
			text: $"-{lossAmount}",
			emojiText: "❤️",
			lifetime: 2f,
			color: new Color( 0.8f, 0f, 0f ),
			velocity: new Vector2( 0f, -32f ),
			deceleration: 1.1f,
			fontSize: 60f,
			startScale: 1.1f,
			endScale: 0.9f
		);
	}

	public void SpawnMaxHPFloater( int amount, Vector3 pos )
	{
		SpawnFloaterText(
			pos: pos.WithZ( 100f ),
			text: $"+{amount} Max HP",
			emojiText: "",
			lifetime: 2f,
			color: new Color( 1f, 0f, 0f ),
			velocity: new Vector2( 0f, 35f ),
			deceleration: 2.1f,
			fontSize: 40f,
			startScale: 1f,
			endScale: 1.1f
		);
	}

	public void SpawnGainMoneyFloater( int moneyAmount, Vector3 pos )
	{
		SpawnFloaterText(
			pos: pos.WithZ( 100f ),
			text: $"+${moneyAmount}",
			emojiText: "",
			lifetime: 2f,
			color: new Color( 1f, 1f, 0f ),
			velocity: new Vector2( 0f, 35f ),
			deceleration: 2.1f,
			fontSize: 60f,
			startScale: 1f,
			endScale: 1.1f
		);
	}

	public void SpawnLoseMoneyFloater( int moneyAmount, Vector3 pos )
	{
		SpawnFloaterText(
			pos: pos.WithZ( 100f ),
			text: $"-${moneyAmount}",
			emojiText: "",
			lifetime: 1.5f,
			color: new Color( 0.7f, 0.7f, 0f ),
			velocity: new Vector2( 0f, -25f ),
			deceleration: 2f,
			fontSize: 60f,
			startScale: 1f,
			endScale: 1.1f
		);
	}

	public void SpawnFloaterLock( Vector3 pos, int numTurns )
	{
		var lockObj = new GameObject();
		lockObj.Name = "floater lock";
		lockObj.WorldPosition = pos;
		var floaterLock = lockObj.Components.Create<FloaterLock>();
		floaterLock.Init( numTurns );

		NumFloaters++;
	}

	public async Task LockCard( Card card, int numTurns )
	{
		Manager.Instance.SpawnFloaterLock(
			pos: card.WorldPosition.WithZ( 100f ),
			numTurns
		);

		card.Lock( numTurns );

		await Task.DelayRealtime( 900 );

		await ShakeCard( card );
	}

	public async Task UnlockCard( Card card )
	{
		card.Unlock();

		await Task.DelayRealtime( 0 );
	}

	public float GetTimerTotalTime()
	{
		return Stats[StatType.TimerTotalTimeOverride] > 0f
			? Stats[StatType.TimerTotalTimeOverride]
			: Stats[StatType.TimerTotalTime];
	}

	public void SpawnCardBreak(Vector3 pos, Rotation rot, float scale, Material material, int breakNum)
	{
		//var prefab = CardBreakPrefabs[Game.Random.Int( 0, CardBreakPrefabs.Count - 1 )];
		//var prefab = CardBreakPrefabs[2];
		var prefab = CardBreakPrefabs[breakNum];

		var cardBreakObj = prefab.Clone( pos, rot );
		var cardBreaker = cardBreakObj.Components.Get<CardBreaker>();
		cardBreaker.Init( material, rot, scale );
	}

	private List<IntVector2> _gridPath;
	private List<IntVector2> _walkable;

	public List<IntVector2> GetPathTo( IntVector2 a, IntVector2 b )
	{
		if ( _gridPath == null )
			_gridPath = new List<IntVector2>();
		else
			_gridPath.Clear();

		_gridPath.Clear();

		if ( (a - b).ManhattanLength <= 1 )
		{
			_gridPath.Add( b );
			return _gridPath;
		}

		List<IntVector2> tempPath = new List<IntVector2>();
		if ( Utils.AStar<IntVector2>( a, b, tempPath, GetEdges, GetHScoreFromGridPosToGridPos ) )
		{
			_gridPath.AddRange( tempPath );

			// remove start pos
			_gridPath.RemoveAt( 0 );
		}

		return _gridPath;
	}

	public List<IntVector2> GetWalkableAdjacentGridPositions( IntVector2 start )
	{
		if ( _walkable == null )
			_walkable = new List<IntVector2>();
		else
			_walkable.Clear();

		IntVector2 left = start + new IntVector2( -1, 0 );
		if ( IsGridPosInBounds( left ) )
			_walkable.Add( left );

		IntVector2 right = start + new IntVector2( 1, 0 );
		if ( IsGridPosInBounds( right ) )
			_walkable.Add( right );

		IntVector2 down = start + new IntVector2( 0, -1 );
		if ( IsGridPosInBounds( down ) )
			_walkable.Add( down );

		IntVector2 up = start + new IntVector2( 0, 1 );
		if ( IsGridPosInBounds( up ) )
			_walkable.Add( up );

		return _walkable;
	}

	static float GetHScoreFromGridPosToGridPos( IntVector2 a, IntVector2 b )
	{
		return (b - a).ManhattanLength;
	}

	IEnumerable<AStarEdge<IntVector2>> GetEdges( IntVector2 start )
	{
		var walkable = GetWalkableAdjacentGridPositions( start );
		return walkable.Select( gridPos => Utils.Edge( gridPos, GetCostToMoveFromGridPosToAdjacentGridPos( start, gridPos ) ) );
	}

	float GetCostToMoveFromGridPosToAdjacentGridPos( IntVector2 a, IntVector2 b )
	{
		return 1f;
	}

	public void FadeIn()
	{
		IsFadingIn = true;
		TimeSinceStartFadingIn = 0f;
	}

	public void RefreshMusicMixerVolume()
	{
		float volume = MusicVolume * 0.01f * (IsFadingIn ? Utils.Map( TimeSinceStartFadingIn, 0f, FADE_IN_TIME, 0f, 1f ) : 1f);
		MusicMixer.Volume = Math.Min( volume, 1f );
	}

	public void RefreshSfxMixerVolume()
	{
		float volume = SfxVolume * 0.01f * 2f;
		SfxMixer.Volume = Math.Min( volume, 2f );
	}

	public void SetMute(bool mute)
	{
		if( mute )
		{
			SfxMixer.Volume = 0f;
			MusicMixer.Volume = 0f;
		}
		else
		{
			RefreshMusicMixerVolume();
			RefreshSfxMixerVolume();
		}
	}

	public void SetBgTint(Color color)
	{
		var bg = Scene.Directory.FindByName( "bg" ).First();

		foreach(var child in bg.Children)
		{
			child.Components.Get<ModelRenderer>().Tint = color;
		}
	}

	async void InitSboxStats()
	{
		//Log.Info( "InitSboxStats start" );

		var stats = Sandbox.Services.Stats.GetLocalPlayerStats( "facepunch.memory" );

		await stats.Refresh();

		for ( int i = 1; i < Enum.GetValues( typeof( CardType ) ).Length - 1; i++ )
		{
			var cardType = (CardType)i;
			var value = stats.Get( $"matched-{cardType}" ).Sum;

			if ( value > 0 )
				SboxStatsMatchedCardTypes.Add( cardType );
		}

		for ( int i = 1; i < Enum.GetValues( typeof( RelicType ) ).Length - 1; i++ )
		{
			var relicType = (RelicType)i;
			var value = stats.Get( $"bought-{relicType}" ).Sum;

			if ( value > 0 )
				SboxStatsBoughtRelicTypes.Add( relicType );
		}

		//Log.Info( "InitSboxStats finished" );

		string matched = "";
		foreach ( var cardType in SboxStatsMatchedCardTypes )
			matched += $"{cardType}, ";
		//Log.Info( $"matched: {matched}" );

		string bought = "";
		foreach ( var relicType in SboxStatsBoughtRelicTypes )
			bought += $"{relicType}, ";
		//Log.Info( $"bought: {bought}" );

		_finishedGettingSboxStats = true;
	}

	public void StatsMatchCard(CardType cardType)
	{
		Sandbox.Services.Stats.Increment( "cards_matched", 2 );

		if ( !_finishedGettingSboxStats )
			return;

		if(!SboxStatsMatchedCardTypes.Contains(cardType))
		{
			SboxStatsMatchedCardTypes.Add( cardType );
			Sandbox.Services.Stats.Increment( $"matched-{cardType}", 1 );
			Sandbox.Services.Stats.Increment( "card_types_matched", 1 );
		}
	}

	public void StatsBuyItem( RelicType relicType )
	{
		if ( !_finishedGettingSboxStats )
			return;

		if ( !SboxStatsBoughtRelicTypes.Contains( relicType ) )
		{
			SboxStatsBoughtRelicTypes.Add( relicType );
			Sandbox.Services.Stats.Increment( $"bought-{relicType}", 1 );
			Sandbox.Services.Stats.Increment( "item_types_bought", 1 );
		}
	}
}