Manager.cs
using Sandbox.Audio;
using SS1;

public enum FloaterType { Damage, Heal, Xp }
public sealed class Manager : Component, Component.INetworkListener
{
	public static Manager Instance { get; private set; }

	[Property] public GameObject PlayerPrefab { get; set; }
	[Property] public GameObject ShadowPrefab { get; set; }
	[Property] public GameObject CoinPrefab { get; set; }
	[Property] public GameObject MagnetPrefab { get; set; }
	[Property] public GameObject BloodSplatterPrefab { get; set; }
	[Property] public GameObject LavaPuddlePrefab { get; set; }
	[Property] public GameObject LavaBlobPrefab { get; set; }
	[Property] public GameObject CloudPrefab { get; set; }
	[Property] public GameObject BurningVfxPrefab { get; set; }
	[Property] public GameObject FrozenVfxPrefab { get; set; }
	[Property] public GameObject FearVfxPrefab { get; set; }
	[Property] public GameObject CharmVfxPrefab { get; set; }
	[Property] public GameObject GrenadePrefab { get; set; }
	[Property] public GameObject ExplosionEffectPrefab { get; set; }
	[Property] public GameObject RingEffectPrefab { get; set; }
	[Property] public GameObject ReviveSoulPrefab { get; set; }
	[Property] public GameObject HealthPackPrefab { get; set; }
	[Property] public GameObject RerollPickupPrefab { get; set; }
	[Property] public GameObject EnemyBulletPrefab { get; set; }
	[Property] public GameObject EnemySpikePrefab { get; set; }
	[Property] public GameObject EnemySpikeBgPrefab { get; set; }
	[Property] public GameObject EnemySpikeElitePrefab { get; set; }
	[Property] public GameObject EnemySpikeSpecialPrefab { get; set; }
	[Property] public GameObject FirePrefab { get; set; }
	[Property] public GameObject ShieldVfxPrefab { get; set; }
	[Property] public GameObject CratePrefab { get; set; }
	[Property] public GameObject ZombiePrefab { get; set; }
	[Property] public GameObject ZombieElitePrefab { get; set; }
	[Property] public GameObject ExploderPrefab { get; set; }
	[Property] public GameObject ExploderElitePrefab { get; set; }
	[Property] public GameObject ExploderSpecialPrefab { get; set; }
	[Property] public GameObject SpitterPrefab { get; set; }
	[Property] public GameObject SpitterElitePrefab { get; set; }
	[Property] public GameObject SpitterSpecialPrefab { get; set; }
	[Property] public GameObject SpitterEliteSpecialPrefab { get; set; }
	[Property] public GameObject SpikerPrefab { get; set; }
	[Property] public GameObject SpikerElitePrefab { get; set; }
	[Property] public GameObject SpikerSpecialPrefab { get; set; }
	[Property] public GameObject ChargerPrefab { get; set; }
	[Property] public GameObject ChargerElitePrefab { get; set; }
	[Property] public GameObject ChargerSpecialPrefab { get; set; }
	[Property] public GameObject RunnerPrefab { get; set; }
	[Property] public GameObject RunnerElitePrefab { get; set; }
	[Property] public GameObject RunnerEliteSpecialPrefab { get; set; }
	[Property] public GameObject BossPrefab { get; set; }
	[Property] public GameObject CrownPrefab { get; set; }
	[Property] public GameObject WarningPrefab { get; set; }
	[Property] public GameObject SatelliteLaserPrefab { get; set; }
	[Property] public GameObject FloaterParticleTextPrefab { get; set; }

	[Property] public CameraComponent Camera { get; private set; }
	[Property] public Camera2D Camera2D { get; set; }

	[Property] public MusicManager MusicManager { get; set; }

	public Mixer SfxMixer { get; set; }

	public int EnemyCount { get; private set; }
	public const float MAX_ENEMY_COUNT = 350;

	public int CharmedEnemyCount { get; set; }

	public int CrateCount { get; private set; }
	public const float MAX_CRATE_COUNT = 7;

	public int CoinCount { get; private set; }
	public const float MAX_COIN_COUNT = 200;
	private int _coinDebt; // coin value not spawned because too many coins exist

	public record struct GridSquare( int x, int y );
	public Dictionary<GridSquare, List<Thing>> ThingGridPositions = new Dictionary<GridSquare, List<Thing>>();

	public float GRID_SIZE = 1f;
	public Vector2 BOUNDS_MIN;
	public Vector2 BOUNDS_MAX;
	public Vector2 BOUNDS_MIN_SPAWN;
	public Vector2 BOUNDS_MAX_SPAWN;

	private TimeSince _timeSinceEnemySpawn;
	[Sync] public TimeSince ElapsedTime { get; set; }
	[Sync] public RealTimeSince TimeSinceRunStart { get; set; }
	[Sync] public float FinalRunTime { get; set; }

	public bool ShouldUpdateThings => !(IsGameOver && ShowFinalPanel);
	public bool ShouldUpdatePlayer => !IsGameOver;
	[Sync] public bool IsGameOver { get; private set; }
	[Sync] public bool IsVictory { get; private set; }
	public bool IsWaitingForFinalPanel { get; private set; }
	public bool ShowFinalPanel { get; private set; }
	private RealTimeSince _realTimeSinceFinalPanelWait;
	public const float FINAL_PANEL_WAIT_TIME = 1.7f;

	[Sync] public bool IsPauseMenuOpen { get; set; }

	public Vector2 MouseWorldPos { get; private set; }

	public bool HasSpawnedBoss { get; private set; }
	public bool IsBossDead { get; set; }
	public Boss Boss { get; set; }
	public Boss OtherBoss { get; set; }
	public int NumBossesKilled { get; set; }
	public bool IsBoss0Dead { get; set; }
	public bool IsBoss1Dead { get; set; }

	public TimeSince TimeSinceMagnet { get; set; }

	public List<BloodSplatter> _bloodSplatters = new();
	public List<LavaPuddle> _lavaPuddles = new();
	public List<LavaBlob> _lavaBlobs = new();
	public List<Cloud> _clouds = new();
	public List<ExplosionEffect> _explosions = new();
	public List<RingEffect> _ringEffects = new();
	public List<Warning> _warnings = new();
	public List<SatelliteLaser> _satelliteLasers = new();

	public Status HoveredStatus { get; set; }

	private int _numEnemyDeathSfxs;

	public int NumPlayers { get; private set; }
	public List<Player> Players { get; private set; } = new();

	public Player Player { get; private set; }

	public bool UnlockedBulletDmgAchievement { get; set; }

	public int Difficulty { get; private set; }
	public static int MinDifficulty = -1;
	public static int MaxDifficulty = 15;

	private int _currEnemyIdNum;

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

		Instance = this;

		var difficultyTracker = Scene.GetAllComponents<DifficultyTracker>().FirstOrDefault();
		Difficulty = difficultyTracker?.Difficulty ?? 0;

		//Difficulty = 4;
		//Log.Info( $"Difficulty: {Difficulty}" );

		if ( difficultyTracker != null )
			difficultyTracker.GameObject.Flags &= ~GameObjectFlags.DontDestroyOnLoad;

		BOUNDS_MIN = new Vector2( -16f, -12.35f );
		BOUNDS_MAX = new Vector2( 16f, 12f );
		BOUNDS_MIN_SPAWN = new Vector2( -15.5f, -11.75f );
		BOUNDS_MAX_SPAWN = new Vector2( 15.5f, 11.5f );

		ElapsedTime = 0f;
		//ElapsedTime = 15 * 60f - 20;

		TimeSinceRunStart = 0f;

		for ( float x = BOUNDS_MIN.x; x < BOUNDS_MAX.x; x += GRID_SIZE )
		{
			for ( float y = BOUNDS_MIN.y; y < BOUNDS_MAX.y; y += GRID_SIZE )
			{
				ThingGridPositions.Add( GetGridSquareForPos( new Vector2( x, y ) ), new List<Thing>() );
			}
		}

		SfxMixer = Mixer.FindMixerByName( "Game" );
		SfxMixer.Volume = 0.75f;

		//MusicSoundPointDrums = Scene.Directory.FindByName( "MusicDrums" ).First().Components.Get<SoundPointComponent>();


		//Music.Seek( MathF.Max( CurrentTime, 0f ) );
	}


	protected override void OnStart()
	{
		//if ( !Networking.IsActive )
		//{
		//	if ( MainMenu.IsSingleplayerGame )
		//		CreatePlayer( Connection.Local );
		//	else
		//		Networking.CreateLobby();
		//}

		CreatePlayer( Connection.Local );

		if ( IsProxy )
			return;

		SpawnStartingThings();
	}

	public void SpawnStartingThings()
	{
		for ( int i = 0; i < 3; i++ )
		{
			var pos = new Vector2( Game.Random.Float( BOUNDS_MIN_SPAWN.x, BOUNDS_MAX_SPAWN.x ), Game.Random.Float( BOUNDS_MIN_SPAWN.y, BOUNDS_MAX_SPAWN.y ) );
			SpawnEnemy( TypeLibrary.GetType( typeof( Crate ) ), pos );
		}

		for ( int i = 0; i < 150; i++ )
		{
			//var zombie = SpawnEnemy( TypeLibrary.GetType( typeof( Zombie ) ), new Vector2( Game.Random.Float( -10, 10f ), Game.Random.Float( -10, 10f ) ), forceSpawn: true ) as Zombie;
			//zombie.Target = Player;
			//zombie.IsWandering = false;
		}

		//float amount = 2f;
		//for ( int i = 0; i < 10; i++ )
		//{
		//	//SpawnEnemy( TypeLibrary.GetType( typeof( Zombie ) ), new Vector2( Game.Random.Float( -amount, amount ), Game.Random.Float( -amount, amount ) ), forceSpawn: true );
		//	//SpawnEnemy( TypeLibrary.GetType( typeof( ZombieElite ) ), new Vector2( Game.Random.Float( -amount, amount ), Game.Random.Float( -amount, amount ) ), forceSpawn: true );
		//	SpawnEnemy( TypeLibrary.GetType( typeof( Exploder ) ), new Vector2( Game.Random.Float( -amount, amount ), Game.Random.Float( -amount, amount ) ), forceSpawn: true );
		//	//SpawnEnemy( TypeLibrary.GetType( typeof( ExploderElite ) ), new Vector2( Game.Random.Float( -amount, amount ), Game.Random.Float( -amount, amount ) ), forceSpawn: true );
		//}

		//var enemy = SpawnEnemy( TypeLibrary.GetType( typeof( Exploder ) ), new Vector2( Game.Random.Float( -2f, 2f ), Game.Random.Float( -2f, 2f ) ), forceSpawn: true );
		//enemy.Charm();
		//enemy.Flash( 0f );

		//SpawnBoss( new Vector2( 3f, 3f ) );
		//HasSpawnedBoss = true;

		//SpawnCrown( new Vector2( 4f, 4f ) );
		//SpawnRerollPickup( 0.6f, 0.6f );

		//SpawnHealthPack( new Vector2( 3f, 3f ), Vector2.Zero );
	}

	public void SetPaused( bool paused )
	{
		IsPauseMenuOpen = paused;

		MusicManager.SetPaused( paused );
	}

	protected override void OnUpdate()
	{
		//for ( float x = BOUNDS_MIN.x; x < BOUNDS_MAX.x; x += GRID_SIZE )
		//{
		//	for ( float y = BOUNDS_MIN.y; y < BOUNDS_MAX.y; y += GRID_SIZE )
		//	{
		//		var pos = new Vector2( x, y );
		//		Gizmo.Draw.LineBBox( new BBox( pos, new Vector2( x + GRID_SIZE, y + GRID_SIZE ) ) );

		//		//var gridSquare = GetGridSquareForPos( pos );
		//		//Gizmo.Draw.Text( (new Vector2( gridSquare.x, gridSquare.y )).ToString(), new global::Transform( new Vector3( x + 7f, y + 7f, 0f ) ) );
		//	}
		//}

		if ( Input.EscapePressed )
		{
			SetPaused( !IsPauseMenuOpen );
			Input.EscapePressed = false;
		}

		NumPlayers = Players.Count();

		if ( IsGameOver )
		{
			if ( IsWaitingForFinalPanel )
			{
				if ( _realTimeSinceFinalPanelWait > FINAL_PANEL_WAIT_TIME )
				{
					IsWaitingForFinalPanel = false;
					ShowFinalPanel = true;

					Scene.TimeScale = 1f;

					if ( !IsVictory && !IsBossDead )
					{
						foreach ( var enemy in Scene.GetAll<Enemy>() )
						{
							if ( !enemy.IsDying && !enemy.IsCharmed )
								enemy.Celebrate();
						}
					}
				}
				else
				{
					Scene.TimeScale = Utils.Map( _realTimeSinceFinalPanelWait, 0f, FINAL_PANEL_WAIT_TIME, 0.0001f, 0.25f, EasingType.SineIn );

					float zoom = 1f + Player.Stats[PlayerStat.ZoomAmount];
					Camera.OrthographicHeight = Utils.Map( _realTimeSinceFinalPanelWait, 0f, 0.5f, 10f * zoom, 8f, EasingType.SineInOut );
				}
			}

			return;
		}

		var tr = Scene.Trace.Ray( Camera.ScreenPixelToRay( Mouse.Position ), 1500f ).Run();
		if ( tr.Hit )
		{
			MouseWorldPos = (Vector2)tr.HitPosition;
		}

		if ( _numEnemyDeathSfxs > 0 )
			_numEnemyDeathSfxs--;

		if ( IsPauseMenuOpen )
		{
			Scene.TimeScale = 0f;
		}
		else
		{
			if ( Player.Stats[PlayerStat.PauseWhileChoosing] > 0f )
			{
				if ( Player.IsChoosingLevelUpReward )
				{
					Scene.TimeScale = Utils.Map( Player.TimeSinceLevelUp, 0f, 0.5f, 0.7f, 0f, EasingType.QuadOut );
				}
				else
				{
					Scene.TimeScale = Player.Statuses.Count > 1
						? Utils.Map( Player.RealTimeSinceChoseUpgrade, 0f, 0.5f, 0f, 1f, EasingType.QuadOut )
						: 1f;
				}
			}
			else
			{
				Scene.TimeScale = 1f;
			}
		}

		HandleEnemySpawn();

		if ( !HasSpawnedBoss && !IsGameOver && ElapsedTime > Player.Stats[PlayerStat.BossArrivalTime] )
		{
			SpawnBoss( new Vector2( 0f, 0f ) );
			HasSpawnedBoss = true;
		}
	}

	public void OnActive( Connection channel )
	{
		Log.Info( $"Player '{channel.DisplayName}' is becoming active (local = {channel == Connection.Local}) (host = {channel.IsHost})" );

		CreatePlayer( channel );
	}

	void CreatePlayer( Connection channel )
	{
		var playerObj = PlayerPrefab.Clone( new Vector3( Game.Random.Float( -3f, 3f ), Game.Random.Float( -3f, 3f ), Globals.GetZPos( 0f ) ) );
		var player = playerObj.Components.Get<Player>();

		Player = player;

		playerObj.NetworkSpawn( channel );
	}

	void HandleEnemySpawn()
	{
		var t = ElapsedTime;

		var spawnTime = Utils.Map( EnemyCount, 0, MAX_ENEMY_COUNT, 0.05f, 0.3f, EasingType.QuadOut )
			* Utils.Map( t, 0f, 80f, 1.5f, 1f )
			* Utils.Map( t, 0f, 250f, 3f, 1f )
			* Utils.Map( t, 0f, 800f, 1.2f, 1f );

		if ( HasSpawnedBoss )
		{
			if ( Difficulty == -1 )
				spawnTime *= 4f;
			else if ( Difficulty == 0 )
				spawnTime *= 3.5f;
			else if ( Difficulty == 1 )
				spawnTime *= 2.5f;
			else if ( Difficulty == 2 )
				spawnTime *= 2f;
			else if ( Difficulty == 3 )
				spawnTime *= 1.5f;
			else if ( Difficulty == 4 )
				spawnTime *= 1.25f;
		}

		//if ( Difficulty < 0 )
		//	spawnTime *= 1.1f;

		if ( _timeSinceEnemySpawn > spawnTime )
		{
			SpawnRandomEnemy();

			//if ( Difficulty == 0 )
			//	SpawnRandomEnemy();
			//else
			//	SpawnRandomEnemyNew( ElapsedTime );

			_timeSinceEnemySpawn = 0f;
		}
	}

	void SpawnRandomEnemy()
	{
		if ( EnemyCount >= MAX_ENEMY_COUNT )
			return;

		var pos = new Vector2( Game.Random.Float( BOUNDS_MIN_SPAWN.x, BOUNDS_MAX_SPAWN.x ), Game.Random.Float( BOUNDS_MIN_SPAWN.y, BOUNDS_MAX_SPAWN.y ) );

		//// ZOMBIE (DEFAULT)
		TypeDescription type = TypeLibrary.GetType( typeof( Zombie ) );

		var t = ElapsedTime;

		if ( Difficulty < 0 )
			t *= 0.9f;
		else if ( Difficulty == 1 )
			t *= 1.15f;
		else if ( Difficulty == 2 )
			t *= 1.25f;
		else if ( Difficulty == 3 )
			t *= 1.35f;
		else if ( Difficulty == 4 )
			t *= 1.45f;
		else if ( Difficulty == 5 )
			t *= 1.55f;

		// CRATE
		if ( CrateCount < MAX_CRATE_COUNT )
		{
			float crateChance = t < 20f ? 0f : Utils.Map( t, 20f, 200f, 0.005f, 0.01f );

			if ( Difficulty == -1 )
				crateChance *= 1.2f;

			//float additionalCrateChance = 0f;
			//foreach ( Player player in GetPlayers() )
			//{
			//	if ( player.Stats[PlayerStat.CrateChanceAdditional] > 0f )
			//		additionalCrateChance += player.Stats[PlayerStat.CrateChanceAdditional];
			//}
			//crateChance *= (1f + additionalCrateChance);

			if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < crateChance )
				type = TypeLibrary.GetType( typeof( Crate ) );
		}

		// EXPLODER
		float exploderChance = t < 35f ? 0f : Utils.Map( t, 35f, 1000f, 0.022f, 0.06f );

		if ( Difficulty == -1 )
			exploderChance *= 0.9f;

		if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < exploderChance )
		{
			float eliteChance = t < 510f ? 0f : Utils.Map( t, 510f, 1600f, 0.02f, 0.95f, EasingType.SineIn );

			//if ( Difficulty == -1 )
			//	eliteChance *= 0.5f;
			//else if ( Difficulty > 0 )
			//	eliteChance *= 1.2f;

			type = Game.Random.Float( 0f, 1f ) < eliteChance ? TypeLibrary.GetType( typeof( ExploderElite ) ) : TypeLibrary.GetType( typeof( Exploder ) );
		}

		if ( Difficulty >= 2 && type == TypeLibrary.GetType( typeof( Zombie ) ) && t > 30f && Game.Random.Float( 0f, 1f ) < Utils.Map( t, 30f, 1700f, 0.0008f, 0.07f ) * Utils.Map( Difficulty, 2, 5, 0.95f, 1.25f, EasingType.Linear ) )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( Scene.GetAll<ExploderSpecial>().Count(), 0, 2 + Math.Min( Manager.Instance.Difficulty, 5 ), 1f, 0f, EasingType.ExpoOut ) )
				type = TypeLibrary.GetType( typeof( ExploderSpecial ) );
		}

		// SPITTER
		float spitterChance = t < 100f ? 0f : Utils.Map( t, 100f, 1100f, 0.013f, 0.1f );

		if ( Difficulty == -1 )
			spitterChance *= 0.7f;

		if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < spitterChance )
		{
			float eliteChance = t < 540f ? 0f : Utils.Map( t, 540f, 1350f, 0.025f, 1f, EasingType.QuadIn );

			//if ( Difficulty == -1 )
			//	eliteChance *= 0.5f;
			//else if ( Difficulty > 0 )
			//	eliteChance *= 1.1f;

			type = Game.Random.Float( 0f, 1f ) < eliteChance ? TypeLibrary.GetType( typeof( SpitterElite ) ) : TypeLibrary.GetType( typeof( Spitter ) );
		}

		if ( Difficulty >= 2 && type == TypeLibrary.GetType( typeof( Zombie ) ) && t > 120f && Game.Random.Float( 0f, 1f ) < Utils.Map( t, 120f, 2600f, 0.000085f, 0.06f ) * Utils.Map( Difficulty, 2, 5, 0.9f, 1.25f, EasingType.Linear ) )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( Scene.GetAll<SpitterSpecial>().Count(), 0, Math.Min( Manager.Instance.Difficulty, 5 ) - 1, 1f, 0f, EasingType.ExpoOut ) )
				type = TypeLibrary.GetType( typeof( SpitterSpecial ) );
		}

		if ( Difficulty >= 2 && type == TypeLibrary.GetType( typeof( Zombie ) ) && t > 150f && Game.Random.Float( 0f, 1f ) < Utils.Map( t, 150f, 2700f, 0.000125f, 0.06f ) * Utils.Map( Difficulty, 2, 5, 0.9f, 1.25f, EasingType.Linear ) )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( Scene.GetAll<SpitterEliteSpecial>().Count(), 0, Math.Min( Manager.Instance.Difficulty, 5 ) - 1, 1f, 0f, EasingType.ExpoOut ) )
				type = TypeLibrary.GetType( typeof( SpitterEliteSpecial ) );
		}

		// SPIKER
		float spikerChance = t < 320f ? 0f : Utils.Map( t, 320f, 120f, 0.018f, 0.07f, EasingType.SineIn );

		if ( Player.TimeSinceInputMove > 20f )
			spikerChance = Math.Max( spikerChance, Utils.Map( Player.TimeSinceInputMove, 20f, 400f, 0.02f, 0.25f ) ); // todo

		//if ( Difficulty == -1 )
		//	spikerChance *= 0.65f;

		if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < spikerChance )
		{
			float eliteChance = t < 580f ? 0f : Utils.Map( t, 580f, 1500f, 0.008f, 1f, EasingType.SineIn );

			if ( Player.TimeSinceInputMove > 30f )
				eliteChance = Math.Max( eliteChance, Utils.Map( Player.TimeSinceInputMove, 30f, 500f, 0.02f, 0.25f ) ); // todo

			//if ( Difficulty == -1 )
			//	eliteChance *= 0.5f;
			//else if ( Difficulty > 0 )
			//	eliteChance *= 1.2f;

			type = Game.Random.Float( 0f, 1f ) < eliteChance ? TypeLibrary.GetType( typeof( SpikerElite ) ) : TypeLibrary.GetType( typeof( Spiker ) );
		}

		if ( Difficulty >= 2 && type == TypeLibrary.GetType( typeof( Zombie ) ) && t > 150f && Game.Random.Float( 0f, 1f ) < Utils.Map( t, 150f, 2500f, 0.000325f, 0.045f ) * Utils.Map( Difficulty, 2, 5, 0.9f, 1.25f, EasingType.Linear ) )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( Scene.GetAll<SpikerSpecial>().Count(), 0, Math.Min( Manager.Instance.Difficulty, 5 ) - 1, 1f, 0f, EasingType.ExpoOut ) )
				type = TypeLibrary.GetType( typeof( SpikerSpecial ) );
		}

		// CHARGER
		float chargerChance = t < 420f ? 0f : Utils.Map( t, 420f, 1200f, 0.022f, 0.075f );
		//if ( Difficulty == -1 )
		//	chargerChance *= 0.6f;

		if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < chargerChance )
		{
			float eliteChance = t < 660f ? 0f : Utils.Map( t, 660f, 1700f, 0.008f, 1f, EasingType.SineIn );

			//if ( Difficulty == -1 )
			//	eliteChance *= 0.5f;
			//else if ( Difficulty > 0 )
			//	eliteChance *= 1.2f;

			type = Game.Random.Float( 0f, 1f ) < eliteChance ? TypeLibrary.GetType( typeof( ChargerElite ) ) : TypeLibrary.GetType( typeof( Charger ) );
		}

		if ( Difficulty >= 2 && type == TypeLibrary.GetType( typeof( Zombie ) ) && t > 220f && Game.Random.Float( 0f, 1f ) < Utils.Map( t, 220f, 3300f, 0.0005f, 0.04f ) * Utils.Map( Difficulty, 2, 5, 0.9f, 1.25f, EasingType.Linear ) )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( Scene.GetAll<ChargerSpecial>().Count(), 0, Math.Min( Manager.Instance.Difficulty, 5 ) - 1, 1f, 0f, EasingType.ExpoOut ) )
				type = TypeLibrary.GetType( typeof( ChargerSpecial ) );
		}

		// RUNNER
		float runnerChance = t < 500f ? 0f : Utils.Map( t, 500f, 1200f, 0.035f, 0.15f, EasingType.QuadIn );

		//if ( Difficulty == -1 )
		//	runnerChance *= 0.65f;

		if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < runnerChance )
		{
			float eliteChance = t < 720f ? 0f : Utils.Map( t, 720f, 1700f, 0.01f, 1f, EasingType.QuadIn );

			//if ( Difficulty == -1 )
			//	eliteChance *= 0.5f;
			//else if ( Difficulty > 0 )
			//	eliteChance *= 1.1f;

			type = Game.Random.Float( 0f, 1f ) < eliteChance ? TypeLibrary.GetType( typeof( RunnerElite ) ) : TypeLibrary.GetType( typeof( Runner ) );
		}

		if ( Difficulty >= 2 && type == TypeLibrary.GetType( typeof( Zombie ) ) && t > 220f && Game.Random.Float( 0f, 1f ) < Utils.Map( t, 220f, 2800f, 0.0005f, 0.05f ) * Utils.Map( Difficulty, 2, 5, 0.9f, 1.25f, EasingType.Linear ) )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( Scene.GetAll<RunnerEliteSpecial>().Count(), 0, Math.Min( Manager.Instance.Difficulty, 5 ) - 1, 1f, 0f, EasingType.ExpoOut ) )
				type = TypeLibrary.GetType( typeof( RunnerEliteSpecial ) );
		}

		// ZOMBIE ELITE
		var zombieEliteChance = t < 400f ? 0f : Utils.Map( t, 400f, 1600f, 0.0175f, (Difficulty >= 4 ? 0.33f : 1f), EasingType.SineIn );

		if ( type == TypeLibrary.GetType( typeof( Zombie ) ) && Game.Random.Float( 0f, 1f ) < zombieEliteChance )
		{
			type = TypeLibrary.GetType( typeof( ZombieElite ) );
		}

		SpawnEnemy( type, pos );
	}

	//void SpawnRandomEnemyNew(float t)
	//{
	//	if ( EnemyCount >= MAX_ENEMY_COUNT )
	//		return;

	//	List<(TypeDescription Type, float Weight)> chances = new();

	//	if ( CrateCount < MAX_CRATE_COUNT )
	//		chances.Add( (TypeLibrary.GetType( typeof( Crate ) ), t < 20f ? 0f : Utils.Map( t, 20f, 200f, 0.005f, 0.01f ) ) );

	//	float zombieChance = 0f;
	//	float zombieEliteChance = 0f;
	//	float exploderChance = 0f;
	//	float exploderEliteChance = 0f;
	//	float spitterChance = 0f;
	//	float spitterEliteChance = 0f;
	//	float spikerChance = 0f;
	//	float spikerEliteChance = 0f;
	//	float chargerChance = 0f;
	//	float chargerEliteChance = 0f;
	//	float runnerChance = 0f;
	//	float runnerEliteChance = 0f;

	//	switch(Difficulty)
	//	{
	//		case -1:
	//			if ( t < 15 * 60f )
	//			{
	//				zombieChance = 1f;
	//				zombieEliteChance = t < 400f ? 0f : Utils.Map( t, 400f, 1200f, 0.0175f, 1f, EasingType.SineIn );
	//				exploderChance = t < 35f ? 0f : Utils.Map( t, 35f, 700f, 0.022f, 0.08f );
	//				exploderEliteChance = t < 480f ? 0f : Utils.Map( t, 480f, 1200f, 0.025f, 1f, EasingType.SineIn );
	//				spitterChance = t < 100f ? 0f : Utils.Map( t, 100f, 800f, 0.015f, 0.1f );
	//				spitterEliteChance = t < 540f ? 0f : Utils.Map( t, 540f, 1200f, 0.025f, 1f, EasingType.QuadIn );
	//				spikerChance = t < 320f ? 0f : Utils.Map( t, 320f, 800f, 0.018f, 0.1f, EasingType.SineIn );
	//				spikerEliteChance = t < 580f ? 0f : Utils.Map( t, 580f, 1300f, 0.008f, 1f, EasingType.SineIn );
	//				chargerChance = t < 420f ? 0f : Utils.Map( t, 420f, 800f, 0.022f, 0.075f );
	//				chargerEliteChance = t < 660f ? 0f : Utils.Map( t, 660f, 1400f, 0.008f, 1f, EasingType.SineIn );
	//				runnerChance = t < 500f ? 0f : Utils.Map( t, 500f, 900f, 0.035f, 0.15f, EasingType.QuadIn );
	//				runnerEliteChance = t < 720f ? 0f : Utils.Map( t, 720f, 1500f, 0.01f, 1f, EasingType.QuadIn );
	//			}
	//			else
	//			{
	//				zombieChance = Utils.Map( t, 15 * 60f, 20 * 60f, 1f, 0f );
	//				zombieEliteChance = t < 400f ? 0f : Utils.Map( t, 400f, 1200f, 0.0175f, 1f, EasingType.SineIn );
	//				exploderChance = t < 35f ? 0f : Utils.Map( t, 35f, 700f, 0.022f, 0.08f );
	//				exploderEliteChance = t < 480f ? 0f : Utils.Map( t, 480f, 1200f, 0.025f, 1f, EasingType.SineIn );
	//				spitterChance = t < 100f ? 0f : Utils.Map( t, 100f, 800f, 0.015f, 0.1f );
	//				spitterEliteChance = t < 540f ? 0f : Utils.Map( t, 540f, 1200f, 0.025f, 1f, EasingType.QuadIn );
	//				spikerChance = t < 320f ? 0f : Utils.Map( t, 320f, 800f, 0.018f, 0.1f, EasingType.SineIn );
	//				spikerEliteChance = t < 580f ? 0f : Utils.Map( t, 580f, 1300f, 0.008f, 1f, EasingType.SineIn );
	//				chargerChance = t < 420f ? 0f : Utils.Map( t, 420f, 800f, 0.022f, 0.075f );
	//				chargerEliteChance = t < 660f ? 0f : Utils.Map( t, 660f, 1400f, 0.008f, 1f, EasingType.SineIn );
	//				runnerChance = t < 500f ? 0f : Utils.Map( t, 500f, 900f, 0.035f, 0.15f, EasingType.QuadIn );
	//				runnerEliteChance = t < 720f ? 0f : Utils.Map( t, 720f, 1500f, 0.01f, 1f, EasingType.QuadIn );
	//			}

	//			break;

	//		case 0: // NORMAL
	//			if ( t < 15 * 60f )
	//			{
	//				//zombieChance = 1f;
	//				//zombieEliteChance = t < 400f ? 0f : Utils.Map( t, 400f, 1200f, 0.0175f, 1f, EasingType.SineIn );
	//				//exploderChance = t < 35f ? 0f : Utils.Map( t, 35f, 700f, 0.022f, 0.08f );
	//				//exploderEliteChance = t < 480f ? 0f : Utils.Map( t, 480f, 1300f, 0.022f, 1f, EasingType.SineIn );
	//				//spitterChance = t < 100f ? 0f : Utils.Map( t, 100f, 800f, 0.015f, 0.1f );
	//				//spitterEliteChance = t < 540f ? 0f : Utils.Map( t, 540f, 1200f, 0.025f, 1f, EasingType.QuadIn );
	//				//spikerChance = t < 320f ? 0f : Utils.Map( t, 320f, 800f, 0.018f, 0.1f, EasingType.SineIn );
	//				//spikerEliteChance = t < 580f ? 0f : Utils.Map( t, 580f, 1300f, 0.008f, 1f, EasingType.SineIn );
	//				//chargerChance = t < 420f ? 0f : Utils.Map( t, 420f, 800f, 0.022f, 0.075f );
	//				//chargerEliteChance = t < 660f ? 0f : Utils.Map( t, 660f, 1400f, 0.008f, 1f, EasingType.SineIn );
	//				//runnerChance = t < 500f ? 0f : Utils.Map( t, 500f, 900f, 0.035f, 0.15f, EasingType.QuadIn );
	//				//runnerEliteChance = t < 720f ? 0f : Utils.Map( t, 720f, 1500f, 0.01f, 1f, EasingType.QuadIn );
	//			}
	//			else
	//			{
	//				zombieChance = Utils.Map( t, 20 * 60f, 30 * 60f, 5f, 0f );
	//				zombieEliteChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.15f, 0f );
	//				exploderChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.08f, 0f );
	//				exploderEliteChance = Utils.Map( t, 20 * 60f, 40 * 60f, 0.8f, 1f );
	//				spitterChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.1f, 0f );
	//				spitterEliteChance = Utils.Map( t, 20 * 60f, 30 * 60f, 0.08f, 0.25f );
	//				spikerChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.1f, 0f );
	//				spikerEliteChance = Utils.Map( t, 20 * 60f, 40 * 60f, 0.7f, 5f );
	//				chargerChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.15f, 0.02f );
	//				chargerEliteChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.6f, 4f );
	//				runnerChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.15f, 0.01f );
	//				runnerEliteChance = Utils.Map( t, 20 * 60f, 35 * 60f, 0.6f, 0.8f );
	//			}

	//			break;

	//		case 1:
	//			if ( t < 15f * 60f )
	//			{
	//				zombieChance = 1f;
	//				zombieEliteChance = t < (6.6f * 60) ? 0f : Utils.Map( t, (6.6f * 60), (20f * 60), 0.0175f, 1f, EasingType.SineIn );
	//				exploderChance = t < (0.5f * 60) ? 0f : Utils.Map( t, (0.5f * 60), (11.5f * 60), 0.02f, 0.08f );
	//				exploderEliteChance = t < (8.5f * 60) ? 0f : Utils.Map( t, (8.5f * 60), (20f * 60), 0.02f, 1f, EasingType.SineIn );
	//				spitterChance = t < (1.4f * 60) ? 0f : Utils.Map( t, (1.4f * 60), (12f * 60), 0.015f, 0.1f );
	//				spitterEliteChance = t < (8f * 60) ? 0f : Utils.Map( t, (8f * 60), (18f * 60), 0.021f, 1f, EasingType.QuadIn );
	//				spikerChance = t < (4.5f * 60) ? 0f : Utils.Map( t, (4.5f * 60), (13f * 60), 0.016f, 0.1f, EasingType.SineIn );
	//				spikerEliteChance = t < (9f * 60) ? 0f : Utils.Map( t, (9f * 60), (19f * 60), 0.008f, 1f, EasingType.SineIn );
	//				chargerChance = t < (6f * 60) ? 0f : Utils.Map( t, (6f * 60), (14f * 60), 0.02f, 0.075f );
	//				chargerEliteChance = t < (10f * 60) ? 0f : Utils.Map( t, (10f * 60), (18f * 60), 0.007f, 1f, EasingType.SineIn );
	//				runnerChance = t < (7.5f * 60) ? 0f : Utils.Map( t, (7.5f * 60), (14f * 60), 0.035f, 0.14f, EasingType.QuadIn );
	//				runnerEliteChance = t < (10.5f * 60) ? 0f : Utils.Map( t, (10.5f * 60), (22f * 60), 0.01f, 1f, EasingType.QuadIn );
	//			}
	//			else
	//			{
	//				zombieChance = Utils.Map( t, (15f * 60), (25f * 60), 1f, 0f, EasingType.Linear );
	//				zombieEliteChance = Utils.Map( t, (15f * 60), (35f * 60), 0.8f, 0f, EasingType.Linear );
	//				exploderChance = Utils.Map( t, (15f * 60), (35f * 60), 0.08f, 0f, EasingType.Linear );
	//				exploderEliteChance = Utils.Map( t, (15f * 60), (33f * 60), 0.8f, 8f, EasingType.Linear );
	//				spitterChance = Utils.Map( t, (15f * 60), (20f * 60), 0.1f, 0f, EasingType.Linear );
	//				spitterEliteChance = 1f;
	//				spikerChance = Utils.Map( t, (15f * 60), (19f * 60), 0.1f, 0f, EasingType.Linear );
	//				spikerEliteChance = Utils.Map( t, (15f * 60), (40f * 60), 0.7f, 10f, EasingType.Linear );
	//				chargerChance = Utils.Map( t, (15f * 60), (25f * 60), 0.075f, 0f, EasingType.Linear );
	//				chargerEliteChance = Utils.Map( t, (15f * 60), (37f * 60), 0.8f, 6f, EasingType.Linear );
	//				runnerChance = Utils.Map( t, (15f * 60), (23f * 60), 0.15f, 0f, EasingType.Linear );
	//				runnerEliteChance = Utils.Map( t, (15f * 60), (33f * 60), 0.6f, 0.05f, EasingType.Linear );
	//			}

	//			break;
	//	}

	//	chances.Add( (TypeLibrary.GetType( typeof( Zombie ) ), zombieChance ) );
	//	chances.Add( (TypeLibrary.GetType( typeof( ZombieElite ) ), zombieEliteChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( Exploder ) ), exploderChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( ExploderElite ) ), exploderEliteChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( Spitter ) ), spitterChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( SpitterElite ) ), spitterEliteChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( Spiker ) ), spikerChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( SpikerElite ) ), spikerEliteChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( Charger ) ), chargerChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( ChargerElite ) ), chargerEliteChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( Runner ) ), runnerChance) );
	//	chances.Add( (TypeLibrary.GetType( typeof( RunnerElite ) ), runnerEliteChance) );

	//	TypeDescription chosenEnemyType = null;

	//	float totalWeight = chances.Sum( x => x.Weight );
	//	var rand = Game.Random.Float( 0f, totalWeight );

	//	for ( int i = chances.Count - 1; i >= 0; i-- )
	//	{
	//		var (type, weight) = chances[i];
	//		rand -= weight;

	//		if ( rand < 0f )
	//		{
	//			chosenEnemyType = type;
	//			break;
	//		}
	//	}

	//	if(chosenEnemyType != null)
	//	{
	//		var pos = new Vector2( Game.Random.Float( BOUNDS_MIN_SPAWN.x, BOUNDS_MAX_SPAWN.x ), Game.Random.Float( BOUNDS_MIN_SPAWN.y, BOUNDS_MAX_SPAWN.y ) );
	//		SpawnEnemy( chosenEnemyType, pos );
	//	}
	//}

	[ConCmd( "spawn_enemy" )]
	public static void GiveStatus( string name )
	{
		// Cheat only works in the editor
		if ( !Game.IsEditor )
			return;

		var type = TypeLibrary.GetType( name );
		if ( type == null )
		{
			Log.Info( $"No enemy with name '{name}' found!" );
			return;
		}

		Manager.Instance?.SpawnEnemy( type, Utils.GetRandomVector() * 1f );
	}

	public Enemy SpawnEnemy( TypeDescription type, Vector2 pos, bool forceSpawn = false )
	{
		if ( EnemyCount >= MAX_ENEMY_COUNT && !forceSpawn )
			return null;

		GameObject enemyObj;
		Enemy enemy;
		var pos3 = new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) );
		if ( type == TypeLibrary.GetType( typeof( Crate ) ) )
		{
			enemyObj = CratePrefab.Clone( pos3 );
			CrateCount++;
		}
		else if ( type == TypeLibrary.GetType( typeof( Zombie ) ) ) { enemyObj = ZombiePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( ZombieElite ) ) ) { enemyObj = ZombieElitePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( Exploder ) ) ) { enemyObj = ExploderPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( ExploderElite ) ) ) { enemyObj = ExploderElitePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( ExploderSpecial ) ) ) { enemyObj = ExploderSpecialPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( Spitter ) ) ) { enemyObj = SpitterPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( SpitterElite ) ) ) { enemyObj = SpitterElitePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( SpitterSpecial ) ) ) { enemyObj = SpitterSpecialPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( SpitterEliteSpecial ) ) ) { enemyObj = SpitterEliteSpecialPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( Spiker ) ) ) { enemyObj = SpikerPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( SpikerSpecial ) ) ) { enemyObj = SpikerSpecialPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( SpikerElite ) ) ) { enemyObj = SpikerElitePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( Charger ) ) ) { enemyObj = ChargerPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( ChargerElite ) ) ) { enemyObj = ChargerElitePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( ChargerSpecial ) ) ) { enemyObj = ChargerSpecialPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( Runner ) ) ) { enemyObj = RunnerPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( RunnerElite ) ) ) { enemyObj = RunnerElitePrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( RunnerEliteSpecial ) ) ) { enemyObj = RunnerEliteSpecialPrefab.Clone( pos3 ); }
		else if ( type == TypeLibrary.GetType( typeof( Boss ) ) ) { enemyObj = BossPrefab.Clone( pos3 ); }
		else
		{
			Log.Info( $"Enemy {type} not implemented yet!" );
			return null;
		}

		enemy = enemyObj.Components.Get<Enemy>();

		var closestPlayer = GetClosestPlayer( pos );
		if ( closestPlayer?.Position2D.x > pos.x && type != TypeLibrary.GetType( typeof( Crate ) ) )
		{
			enemy.FlipX = true;
			enemy.Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;
		}

		enemyObj.Name = type.ToString();
		//enemyObj.NetworkSpawn( null );

		enemy.Target = Player;

		AddThing( enemy );
		EnemyCount++;

		PlaySfxNearby( "zombie.dirt", pos, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f, maxDist: 7.5f );

		enemy.EnemyIdNum = _currEnemyIdNum++;

		//if ( Difficulty >= 7 && type != TypeLibrary.GetType( typeof( Boss ) ) && type != TypeLibrary.GetType( typeof( Crate ) ) )
		//{
		//	Log.Info( $"enemy.Health: {enemy.Health} Globals.DIFFICULTY_4_HP_BOOST: {Globals.DIFFICULTY_ENEMY_HP_BOOST}" );
		//	enemy.Health = enemy.Health * Globals.DIFFICULTY_ENEMY_HP_BOOST;
		//	enemy.MaxHealth = enemy.Health;
		//}

		return enemy;
	}

	public void SpawnCoin( Vector2 pos, Vector2 vel, int value = 1, bool force = false )
	{
		if ( CoinCount > 60 && !force )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( CoinCount, 60, 200, 0f, 1f, EasingType.QuadOut ) )
			{
				_coinDebt += value;
				return;
			}
		}

		//if ( CoinCount >= MAX_COIN_COUNT )
		//	return;

		var coinObj = CoinPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var coin = coinObj.Components.Get<Coin>();
		coin.Velocity = vel;
		coin.SetValue( value + _coinDebt );
		_coinDebt = 0;

		//coinObj.NetworkSpawn( null );

		AddThing( coin );
		CoinCount++;

		return;
	}

	public Magnet SpawnMagnet( Vector2 pos, Vector2 vel )
	{
		var magnetObj = MagnetPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var magnet = magnetObj.Components.Get<Magnet>();
		magnet.Velocity = vel;
		//magnetObj.NetworkSpawn( null );

		TimeSinceMagnet = 0f;

		AddThing( magnet );

		return magnet;
	}

	public void SpawnReviveSoul( Vector2 pos, Vector2 vel )
	{
		var reviveObj = ReviveSoulPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var revive = reviveObj.Components.Get<ReviveSoul>();
		revive.Velocity = vel;

		//reviveObj.NetworkSpawn( null );
		AddThing( revive );
	}

	public void SpawnHealthPack( Vector2 pos, Vector2 vel )
	{
		var healthPackObj = HealthPackPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var healthPack = healthPackObj.Components.Get<HealthPack>();
		healthPack.Velocity = vel;

		//healthPackObj.NetworkSpawn( null );
		AddThing( healthPack );
	}

	public void SpawnRerollPickup( Vector2 pos, Vector2 vel )
	{
		var rerollPickupObj = RerollPickupPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var rerollPickup = rerollPickupObj.Components.Get<RerollPickup>();
		rerollPickup.Velocity = vel;

		AddThing( rerollPickup );
	}

	public EnemyBullet SpawnEnemyBullet( Vector2 pos, Vector2 dir, float speed )
	{
		var enemyBulletObj = EnemyBulletPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var enemyBullet = enemyBulletObj.Components.Get<EnemyBullet>();
		enemyBullet.Direction = dir;

		enemyBullet.Speed = speed;

		if ( dir.x < 0f )
			enemyBullet.Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;

		//enemyBulletObj.NetworkSpawn( null );
		AddThing( enemyBullet );

		return enemyBullet;
	}

	public EnemySpike SpawnEnemySpike( Vector2 pos, bool special = false )
	{
		var spikeObj = (special ? EnemySpikeSpecialPrefab : EnemySpikePrefab).Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var spike = spikeObj.Components.Get<EnemySpike>();
		//spikeObj.NetworkSpawn( null );
		AddThing( spike );
		return spike;
	}

	public EnemySpikeElite SpawnEnemySpikeElite( Vector2 pos, Thing target = null )
	{
		var spikeObj = EnemySpikeElitePrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var spike = spikeObj.Components.Get<EnemySpikeElite>();
		spike.Target = target;
		//spikeObj.NetworkSpawn( null );
		AddThing( spike );
		return spike;
	}

	public void SpawnFire( Vector2 pos, Guid playerId )
	{
		var playerObj = Scene.Directory.FindByGuid( playerId );
		Player player = playerObj?.Components.Get<Player>() ?? null;
		if ( player == null )
			return;

		var fireObj = FirePrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var fire = fireObj.Components.Get<Fire>();
		fire.Shooter = player;
		fire.Lifetime = player.Stats[PlayerStat.FireLifetime];

		//fireObj.NetworkSpawn( null );
		AddThing( fire );
	}

	public void SpawnBoss( Vector2 pos )
	{
		Boss = SpawnEnemy( TypeLibrary.GetType( typeof( Boss ) ), pos, forceSpawn: true ) as Boss;
		Boss.BossNum = 0;

		if ( Difficulty >= 5 )
		{
			OtherBoss = SpawnEnemy( TypeLibrary.GetType( typeof( Boss ) ), new Vector2( Game.Random.Float( -2f, 2f ), Game.Random.Float( -2f, 2f ) ), forceSpawn: true ) as Boss;
			OtherBoss.BossNum = 1;
			OtherBoss.Sprite.Sprite = OtherBoss.OtherBossSprite;
		}

		PlaySfxNearby( "boss.fanfare", pos, pitch: 1.0f, volume: 1.3f, maxDist: 30f );

		Player.ShakeCam( 0.025f, 2f, EasingType.SineIn );
	}

	public Crown SpawnCrown( Vector2 pos )
	{
		var crownObj = CrownPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ) );
		var crown = crownObj.Components.Get<Crown>();

		AddThing( crown );

		return crown;
	}

	//private T GetClosest<T>( IEnumerable<T> enumerable, Vector3 pos, float maxRange, bool ignoreZ, T except )
	//	where T : Thing
	//{
	//	var dists = ignoreZ
	//		? enumerable.Select( x => (Thing: x, DistSq: (x.WorldPosition - pos).WithZ( 0f ).LengthSquared) )
	//		: enumerable.Select( x => (Thing: x, DistSq: (x.WorldPosition - pos).LengthSquared) );

	//	return dists.OrderBy( x => x.DistSq )
	//		//.FirstOrDefault( x => x.DistSq <= maxRange * maxRange && x.Thing != except && (!ignoreZ || x.Thing.Parent == null) )
	//		.FirstOrDefault( x => x.DistSq <= maxRange * maxRange && x.Thing != except )
	//		.Thing;
	//}

	public GridSquare GetGridSquareForPos( Vector2 pos )
	{
		return new GridSquare( (int)MathF.Floor( pos.x ), (int)MathF.Floor( pos.y ) );
	}

	public List<Thing> GetThingsInGridSquare( GridSquare gridSquare )
	{
		if ( ThingGridPositions.ContainsKey( gridSquare ) )
		{
			return ThingGridPositions[gridSquare];
		}

		return null;
	}

	public bool IsGridSquareInArena( GridSquare gridSquare )
	{
		return ThingGridPositions.ContainsKey( gridSquare );
	}

	public void RegisterThingGridSquare( Thing thing, GridSquare gridSquare )
	{
		if ( IsGridSquareInArena( gridSquare ) )
			ThingGridPositions[gridSquare].Add( thing );
	}

	public void DeregisterThingGridSquare( Thing thing, GridSquare gridSquare )
	{
		if ( ThingGridPositions.ContainsKey( gridSquare ) && ThingGridPositions[gridSquare].Contains( thing ) )
		{
			ThingGridPositions[gridSquare].Remove( thing );
		}
	}

	public void AddThing( Thing thing )
	{
		//_things.Add( thing );
		thing.GridPos = GetGridSquareForPos( thing.Position2D );
		RegisterThingGridSquare( thing, thing.GridPos );
	}

	public void RemoveThing( Thing thing )
	{
		if ( ThingGridPositions.ContainsKey( thing.GridPos ) )
			ThingGridPositions[thing.GridPos].Remove( thing );

		if ( thing is Enemy enemy ) // counts Crate too
		{
			EnemyCount--;

			//if ( enemy.IsCharmed )
			//	CharmedEnemyCount--;

			if ( thing is Crate )
				CrateCount--;
		}
		else if ( thing is Coin )
		{
			CoinCount--;
		}
	}

	public void HandleThingCollisionForGridSquare( Thing thing, GridSquare gridSquare, float dt )
	{
		if ( thing.IsProxy )
			return;

		if ( !ThingGridPositions.ContainsKey( gridSquare ) )
			return;

		var things = ThingGridPositions[gridSquare];
		if ( things.Count == 0 )
			return;

		for ( int i = things.Count - 1; i >= 0; i-- )
		{
			if ( i >= things.Count )
				continue;

			if ( thing == null || !thing.IsValid() || thing.IsRemoved )
				return;

			var other = things[i];
			if ( other == thing || other.IsRemoved || !other.IsValid() )
				continue;

			bool isValidType = false;
			foreach ( var t in thing.CollideWith )
			{
				if ( t.IsAssignableFrom( other.GetType() ) )
				{
					isValidType = true;
					break;
				}
			}

			if ( !isValidType )
				continue;

			if ( other is Enemy enemy && (enemy.IsDying || enemy.IgnoreCollision) )
				continue;

			var dist_sqr = (thing.Position2D - other.Position2D).LengthSquared;
			var total_radius_sqr = MathF.Pow( thing.Radius + other.Radius, 2f );
			if ( dist_sqr < total_radius_sqr )
			{
				float percent = Utils.Map( dist_sqr, total_radius_sqr, 0f, 0f, 1f );
				thing.Colliding( other, percent, dt * thing.TimeScale );
			}
		}
	}

	public void AddThingsInGridSquare( GridSquare gridSquare, List<Thing> things )
	{
		if ( !ThingGridPositions.ContainsKey( gridSquare ) )
			return;

		things.AddRange( ThingGridPositions[gridSquare] );
	}

	public void PlayerDied( Player player )
	{
		//int numPlayersAlive = GetPlayers(alive: true).Count();
		//if ( numPlayersAlive == 0 )
		GameOver();
	}

	public void GameOver()
	{
		if ( IsGameOver )
			return;

		IsGameOver = true;
		IsVictory = false;
		IsWaitingForFinalPanel = true;
		_realTimeSinceFinalPanelWait = 0f;
		ShowFinalPanel = false;
		FinalRunTime = TimeSinceRunStart.Relative;

		BroadcastLoss();

		GameOverAsync();
	}

	async void GameOverAsync()
	{
		var enemies = Scene.GetAll<Enemy>().ToList();
		enemies.Shuffle();

		var currNum = 0;
		while ( currNum < 20 && currNum < enemies.Count )
		{
			var enemy = enemies[currNum];
			if ( IsGameOver && enemy.IsValid() && !enemy.IsDying )
			{
				PlaySfxNearby( "enemy_cheer", enemy.Position2D, Game.Random.Float( 0.7f, 0.8f ), 1f, 6f );
			}

			currNum++;

			await Task.Delay( Game.Random.Int( 30, 110 ) );
		}
	}


	void BroadcastLoss()
	{
		//Sandbox.Services.Stats.SetValue( GetStatName( Difficulty, survival: true ), ElapsedTime );
	}

	public void Victory()
	{
		if ( IsGameOver )
			return;

		IsGameOver = true;
		IsVictory = true;
		IsWaitingForFinalPanel = true;
		_realTimeSinceFinalPanelWait = 0f;
		ShowFinalPanel = false;
		FinalRunTime = TimeSinceRunStart.Relative;

		BroadcastVictory();
	}


	void BroadcastVictory()
	{
		Sandbox.Services.Stats.SetValue( GetStatName( Difficulty ), TimeSinceRunStart );
		Log.Info( $"submitting_score: {GetStatName( Difficulty )}" );

		//Sandbox.Services.Stats.SetValue( GetStatName( Difficulty, survival: true ), ElapsedTime );

		if ( Difficulty >= 0 )
		{
			Sandbox.Services.Achievements.Unlock( "winner" );

			bool isJackOfAllTradesValid = true;
			foreach ( KeyValuePair<int, Status> pair in Player.Statuses )
			{
				Status status = pair.Value;
				if ( status.Level > 1 )
				{
					isJackOfAllTradesValid = false;
					break;
				}
			}

			if ( isJackOfAllTradesValid )
				Sandbox.Services.Achievements.Unlock( "jack_trades" );
		}

		//Sandbox.Services.Stats.SetValue( "victory_time_2", TimeSinceRunStart );

		//Sandbox.Services.Stats.SetValue( "victory_time_add", TimeSinceRunStart );

		//Sandbox.Services.Stats.Increment( "v_int_test", Game.Random.Int(1, 99) );
	}

	public BloodSplatter SpawnBloodSplatter( Vector2 pos )
	{
		if ( _bloodSplatters.Count > 30 )
		{
			if ( Game.Random.Float( 0f, 1f ) < Utils.Map( _bloodSplatters.Count, 30, 80, 0f, 1f ) )
				return null;
		}

		var bloodObj = BloodSplatterPrefab.Clone( new Vector3( pos.x, pos.y, Globals.BLOOD_DEPTH ) );
		var bloodSplatter = bloodObj.Components.Get<BloodSplatter>();
		bloodSplatter.Lifetime = Utils.Map( _lavaPuddles.Count, 0, 100, 10f, 1f ) * Game.Random.Float( 0.8f, 1.2f );

		_bloodSplatters.Add( bloodSplatter );
		return bloodSplatter;
	}

	public void RemoveBloodSplatter( BloodSplatter blood )
	{
		if ( _bloodSplatters.Contains( blood ) )
			_bloodSplatters.Remove( blood );
	}

	public bool GetLavaBlobEndPos( Vector2 startPos, out Vector2 endPos )
	{
		endPos = ClampToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 0f, 5f ), buffer: 1f );
		int NUM_TRIES = 40;
		int count = 0;
		while ( IsInLava( endPos ) && count < NUM_TRIES )
		{
			endPos = ClampToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 0f, 5f ), buffer: 1f );
			count++;
		}

		count = 0;
		while ( IsInLava( endPos ) && count < NUM_TRIES )
		{
			endPos = ClampToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 5f, 6.5f ), buffer: 1f );
			count++;
		}

		count = 0;
		while ( IsInLava( endPos ) && count < NUM_TRIES )
		{
			endPos = ClampToBounds( startPos + Utils.GetRandomVector() * Game.Random.Float( 6.5f, 8f ), buffer: 1f );
			count++;
		}

		return !IsInLava( endPos );
	}

	public LavaBlob SpawnLavaBlob( Vector2 startPos, Vector2 endPos )
	{
		var lavaBlobObj = LavaBlobPrefab.Clone( new Vector3( startPos.x, startPos.y, 0f ) );
		var lavaBlob = lavaBlobObj.Components.Get<LavaBlob>();

		lavaBlob.Init( startPos, endPos );

		_lavaBlobs.Add( lavaBlob );
		return lavaBlob;
	}

	public void RemoveLavaBlob( LavaBlob lavaBlob )
	{
		if ( _lavaBlobs.Contains( lavaBlob ) )
			_lavaBlobs.Remove( lavaBlob );
	}

	bool IsInLava( Vector2 pos )
	{
		foreach ( var lavaPuddle in Scene.GetAllComponents<LavaPuddle>() )
		{
			if ( lavaPuddle.TimeSinceSpawn > lavaPuddle.Lifetime - 0.7f )
				continue;

			if ( (lavaPuddle.Position2D - pos).LengthSquared < MathF.Pow( lavaPuddle.Radius * 1.2f, 2f ) )
				return true;
		}

		foreach ( var lavaBlob in Scene.GetAllComponents<LavaBlob>() )
		{
			if ( (lavaBlob.EndPos - pos).LengthSquared < MathF.Pow( 2f * 1.2f, 2f ) )
				return true;
		}

		return false;
	}

	public LavaPuddle SpawnLavaPuddle( Vector2 pos, float lifetime, Color colorA, Color colorB, float damage, float radius )
	{
		var lavaPuddleObj = LavaPuddlePrefab.Clone( new Vector3( pos.x, pos.y, Globals.LAVA_PUDDLE_DEPTH ) );
		var lavaPuddle = lavaPuddleObj.Components.Get<LavaPuddle>();
		lavaPuddle.Lifetime = lifetime;
		lavaPuddle.ColorA = colorA;
		lavaPuddle.ColorB = colorB;
		lavaPuddle.DamageToPlayer = damage;
		lavaPuddle.FullRadius = radius;

		_lavaPuddles.Add( lavaPuddle );
		return lavaPuddle;
	}

	public void RemoveLavaPuddle( LavaPuddle lavaPuddle )
	{
		if ( _lavaPuddles.Contains( lavaPuddle ) )
			_lavaPuddles.Remove( lavaPuddle );
	}

	public Cloud SpawnCloud( Vector2 pos )
	{
		var cloudObj = CloudPrefab.Clone( new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) ), new Angles( 0f, -90f, 0f ) );
		var cloud = cloudObj.Components.Get<Cloud>();
		cloud.Lifetime = 0.7f * Game.Random.Float( 0.8f, 1.2f );

		_clouds.Add( cloud );
		return cloud;
	}

	public void RemoveCloud( Cloud cloud )
	{
		if ( _clouds.Contains( cloud ) )
			_clouds.Remove( cloud );
	}

	public ExplosionEffect SpawnExplosionEffect( Vector2 pos, Color colorA, Color colorB, float opacity = 0.8f, float scaleModifier = 1f )
	{
		var explosionObj = ExplosionEffectPrefab.Clone( new Vector3( pos.x, pos.y, 100f ) );
		var explosion = explosionObj.Components.Get<ExplosionEffect>();
		explosion.Lifetime = 0.5f;
		explosion.LocalScale *= scaleModifier;
		explosion.Init( colorA, colorB, opacity );

		_explosions.Add( explosion );
		return explosion;
	}

	public void RemoveExplosionEffect( ExplosionEffect explosion )
	{
		if ( _explosions.Contains( explosion ) )
			_explosions.Remove( explosion );
	}

	public RingEffect SpawnRingEffect( Vector2 pos, Color colorA, Color colorB, float damage = 0f, float opacity = 0.8f, float scaleMin = 1f, float scaleMax = 1f, float depth = 100f )
	{
		var ringObj = RingEffectPrefab.Clone( new Vector3( pos.x, pos.y, depth ) );
		var ring = ringObj.Components.Get<RingEffect>();
		ring.Lifetime = 0.5f;
		ring.ScaleMin = scaleMin;
		ring.ScaleMax = scaleMax;
		ring.Init( colorA, colorB, opacity );
		ring.Position2D = pos;
		ring.Damage = damage;

		_ringEffects.Add( ring );
		return ring;
	}

	public void RemoveRingEffect( RingEffect ring )
	{
		if ( _ringEffects.Contains( ring ) )
			_ringEffects.Remove( ring );
	}

	public Warning SpawnWarning( Vector2 pos, Color colorA, Color colorB )
	{
		var warningObj = WarningPrefab.Clone( new Vector3( pos.x, pos.y, Globals.WARNING_DEPTH ), new Angles( 0f, -90f, 0f ) );
		var warning = warningObj.Components.Get<Warning>();
		warning.ColorA = colorA;
		warning.ColorB = colorB;

		_warnings.Add( warning );
		return warning;
	}

	public void RemoveWarning( Warning warning )
	{
		if ( _warnings.Contains( warning ) )
			_warnings.Remove( warning );
	}

	public SatelliteLaser SpawnSatelliteLaser( Vector2 pos )
	{
		var laserObj = SatelliteLaserPrefab.Clone( new Vector3( pos.x, pos.y, Globals.WARNING_DEPTH ) );
		var laser = laserObj.Components.Get<SatelliteLaser>();
		laser.Init();

		_satelliteLasers.Add( laser );
		return laser;
	}

	public void RemoveSatelliteLaser( SatelliteLaser laser )
	{
		if ( _satelliteLasers.Contains( laser ) )
			_satelliteLasers.Remove( laser );
	}

	public void Restart()
	{
		MusicManager.Restart();

		foreach ( var blood in _bloodSplatters )
			blood.GameObject.Destroy();
		_bloodSplatters.Clear();

		foreach ( var lavaPuddle in _lavaPuddles )
			lavaPuddle.GameObject.Destroy();
		_lavaPuddles.Clear();

		foreach ( var lavaBlob in _lavaBlobs )
			lavaBlob.GameObject.Destroy();
		_lavaBlobs.Clear();

		foreach ( var cloud in _clouds )
			cloud.GameObject.Destroy();
		_clouds.Clear();

		foreach ( var explosion in _explosions )
			explosion.GameObject.Destroy();
		_explosions.Clear();

		foreach ( var ring in _ringEffects )
			ring.GameObject.Destroy();
		_ringEffects.Clear();

		foreach ( var warning in _warnings )
			warning.GameObject.Destroy();
		_warnings.Clear();

		foreach ( var laser in _satelliteLasers )
			laser.GameObject.Destroy();
		_satelliteLasers.Clear();

		foreach ( KeyValuePair<GridSquare, List<Thing>> pair in ThingGridPositions )
			pair.Value.Clear();

		EnemyCount = 0;
		CharmedEnemyCount = 0;
		CrateCount = 0;
		CoinCount = 0;
		_coinDebt = 0;
		_timeSinceEnemySpawn = 0f;
		ElapsedTime = 0f;
		TimeSinceRunStart = 0f;
		IsGameOver = false;
		IsWaitingForFinalPanel = false;
		HasSpawnedBoss = false;
		IsBossDead = false;
		NumBossesKilled = 0;
		IsBoss0Dead = false;
		IsBoss1Dead = false;
		Boss = null;
		TimeSinceMagnet = 0f;
		Hud.Instance?.FadeIn();
		Camera.OrthographicHeight = 10f;

		//Components.Get<PauseMenu>().IsOpen = false;
		IsPauseMenuOpen = false;

		if ( IsProxy )
			return;

		foreach ( Thing thing in Scene.GetAllComponents<Thing>() )
		{
			if ( thing is Player player )
				player.Restart();
			else
				thing.GameObject.Destroy();
		}

		//foreach ( var number in Scene.GetAllComponents<LegacyParticleSystem>() )
		//	number.GameObject.Destroy();

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

		_currEnemyIdNum = 0;

		SpawnStartingThings();
	}

	public void PlayEnemyDeathSfxLocal( Vector3 worldPos )
	{
		if ( _numEnemyDeathSfxs >= 3 )
			return;

		PlaySfxNearby( "enemy.die", worldPos, pitch: Game.Random.Float( 0.85f, 1.15f ), volume: 1f, maxDist: 5.5f );

		_numEnemyDeathSfxs++;
	}

	public void PlaySfxNearby( string name, Vector2 worldPos, float pitch, float volume, float maxDist )
	{
		maxDist *= Globals.SFX_DIST_MODIFIER;

		var player = GetLocalPlayer();
		if ( player == null )
			return;

		var playerPos = player.Position2D;

		var distSqr = (player.Position2D - worldPos).LengthSquared;
		if ( distSqr < maxDist * maxDist )
		{
			var dist = (player.Position2D - worldPos).Length;
			var falloff = Utils.Map( dist, 0f, maxDist, 1f, 0f, EasingType.SineIn );
			var pos = playerPos + (worldPos - playerPos) * 0.1f;

			player.PlaySfx( name, pos, pitch * Globals.SFX_PITCH_MODIFIER, volume * falloff );
		}
	}

	public Player GetLocalPlayer()
	{
		foreach ( var player in Players )
		{
			if ( player.Network.IsOwner )
				return player;
		}

		return null;
	}

	public void AddPlayer( Player player )
	{
		Players.Add( player );
	}

	public void RemovePlayer( Player player )
	{
		Players.Remove( player );
	}

	private List<Player> _players = new();
	public List<Player> GetPlayers( bool alive = true )
	{
		_players.Clear();

		foreach ( var player in Players )
		{
			if ( alive != player.IsDead )
				_players.Add( player );
		}

		return _players;
	}

	public Player GetClosestPlayer( Vector2 pos, bool alive = true )
	{
		Player closestPlayer = null;
		float closestDistSqr = float.MaxValue;

		foreach ( var player in Players )
		{
			if ( alive != player.IsDead )
			{
				var distSqr = (player.Position2D - pos).LengthSquared;

				if ( distSqr < closestDistSqr )
				{
					closestDistSqr = distSqr;
					closestPlayer = player;
				}
			}
		}

		return closestPlayer;
	}

	public Vector2 ClampToBounds( Vector2 pos, float buffer = 0.3f )
	{
		return new Vector2( MathX.Clamp( pos.x, Manager.Instance.BOUNDS_MIN.x + buffer, Manager.Instance.BOUNDS_MAX.x - buffer ), MathX.Clamp( pos.y, Manager.Instance.BOUNDS_MIN.y + buffer, Manager.Instance.BOUNDS_MAX.y - buffer ) );
	}

	public void ShakePlayerCam( Vector2 pos, float radius, float maxStrength, float time )
	{
		if ( (Player.Position2D - pos).LengthSquared < MathF.Pow( radius, 2f ) )
		{
			Player.ShakeCam( Utils.Map( (Player.Position2D - pos).Length, 0f, radius, maxStrength, 0f, EasingType.Linear ), time, EasingType.Linear );
		}
	}

	public static string GetStatName( int difficulty, bool survival = false )
	{
		//if ( survival )
		//{
		//	switch ( difficulty )
		//	{
		//		case -1: return "end_time_easy";
		//		case 0: return "end_time";
		//		default: return $"end_time_diff_{difficulty}";
		//	}
		//}
		//else
		//{
		switch ( difficulty )
		{
			case -1: return "victory_elapsed_time_easy_2";
			case 0: return "victory_elapsed_time";
			case 5: return "victory_elapsed_time_diff_5_c";
			case 6: return "victory_elapsed_time_diff_6_b";
			default: return $"victory_elapsed_time_diff_{difficulty}";
		}
		//}
	}

	public static string GetNameForDifficulty( int difficulty )
	{
		switch ( difficulty )
		{
			case -1: return "Easy Difficulty";
			case 0: return "Normal Difficulty";
			default: return $"Difficulty +{difficulty}";
		}
	}

	public static string GetDescriptionForDifficulty( int difficulty )
	{
		switch ( difficulty )
		{
			case -1: return "achievements disabled";
			case 0: default: return "";
			case 1: return "new enemy behavior";
			case 2: return "special enemies appear";
			case 3: return "less resources";
			case 4: return "basic zombies are more dangerous";
			case 5: return "twin bosses";
			case 6: return "cursed every 10 levels";
			case 7: return "cursed every 9 levels";
			case 8: return "cursed every 8 levels";
			case 9: return "cursed every 7 levels";
			case 10: return "cursed every 6 levels";
			case 11: return "cursed every 5 levels";
			case 12: return "cursed every 4 levels";
			case 13: return "cursed every 3 levels";
			case 14: return "cursed every other level";
			case 15: return "cursed 2/3 levels";
		}
	}

	public static Color GetDifficultyLabelColor( int difficulty )
	{
		switch ( difficulty )
		{
			case -1: return new Color( 1f, 0.5f, 1f, 0.6f );
			case 0: return new Color( 1f, 1f, 1f, 0.5f );
			default: return Color.Lerp( new Color( 1f, 0.5f, 0.5f, 0.5f ), new Color( 1f, 0f, 0f, 0.7f ), Utils.Map( difficulty, 0, Manager.MaxDifficulty, 0f, 1f, EasingType.Linear ) );
		}
	}

	public void SpawnDamageNumber( Vector3 pos, float damage, Color color, float size = 1f, FloaterType floaterType = FloaterType.Damage )
	{
		int amount = GetFloaterNumber( damage );
		SpawnFloaterParticle( pos, amount.ToString(), color, size, floaterType );
	}

	public int GetFloaterNumber( float amount )
	{
		if ( amount < 1f )
		{
			amount = MathF.Ceiling( amount );
		}
		else
		{
			float fractional = amount - MathF.Floor( amount );
			if ( fractional > 0f && Game.Random.Float( 0f, 1f ) > fractional ) // the higher the fractional, the lower the chance of rolling higher than it, so Floor when doing so
				amount = MathF.Floor( amount );
			else
				amount = MathF.Ceiling( amount );
		}

		return (int)amount;
	}

	public void SpawnFloaterParticle( Vector3 pos, string message, Color color, float size = 1f, FloaterType floaterType = FloaterType.Damage )
	{
		if ( color == default )
			color = Color.White;

		pos = pos.WithZ( 200f );

		size *= 8f;

		var particle = FloaterParticleTextPrefab.Clone( new CloneConfig()
		{
			Transform = new Transform( pos, new Angles( 0, 90, 0 ) ),
			StartEnabled = true
		} );

		var particleEffect = particle.Components.Get<ParticleEffect>();

		if ( floaterType == FloaterType.Xp )
			message += " XP";

		var textRenderer = particle.Components.Get<ParticleTextRenderer>();
		if ( textRenderer != null )
		{
			var text = new TextRendering.Scope( message, color, 44, weight: 800 );
			text.FontName = "Mini Pixel-7";
			text.Outline.Enabled = true;
			text.Outline.Color = Color.Black;
			text.Outline.Size = 8;
			//textRenderer.Scale = 1.5f;
			//text.FontItalic = true;

			textRenderer.Text = text;
			textRenderer.Scale = size;
		}

		if ( floaterType == FloaterType.Heal )
		{
			particleEffect.StartVelocity = Game.Random.Float( 0f, 0.1f );
			particleEffect.ForceDirection = new Vector3( 0f, Game.Random.Float( 10f, 20f ), 0f );
			particleEffect.Damping = 0f;
			particleEffect.Stretch = 0f;
		}
		else if ( floaterType == FloaterType.Xp )
		{
			particleEffect.StartVelocity = Game.Random.Float( 0.3f, 0.4f );
			particleEffect.ForceDirection = new Vector3( 0f, Game.Random.Float( 40f, 50f ), 0f );
			particleEffect.Damping = 0.03f;
			particleEffect.Stretch = 0f;
			particleEffect.Lifetime = 0.5f;
		}
	}

	public void BossDied( int bossNum )
	{
		if ( Difficulty >= 5 )
		{
			if ( bossNum == 0 )
				IsBoss0Dead = true;
			else
				IsBoss1Dead = true;

			NumBossesKilled++;
			if ( NumBossesKilled >= 2 )
				IsBossDead = true;
		}
		else
		{
			IsBossDead = true;
		}
	}
}