things/enemies/Enemy.cs
using Sandbox.Diagnostics;
using static Manager;

public abstract class Enemy : Thing
{
	public float Health { get; set; }
	public bool FlipX { get; set; }
	public float MoveTimeOffset { get; set; }

	private float _flashTimer;
	public bool IsFlashing { get; private set; }

	public float MaxHealth { get; set; }

	public bool IsSpawning { get; protected set; }
	public TimeSince TimeSinceSpawn { get; protected set; }
	public bool IsDying { get; private set; }
	public float DeathTimeElapsed { get; private set; }
	public float DeathTime { get; protected set; }
	public float DeathProgress { get; private set; }
	private Vector3 _deathScale;

	[Sync] public bool IsAttacking { get; protected set; }
	private float _aggroTimer;
	public bool CanAttack { get; set; }
	public bool CanAttackAnim { get; set; }
	public bool CanTurn { get; set; }
	public bool DontChangeAnimSpeed { get; set; }
	public virtual bool CanBleed => true;
	public float AggroRange { get; protected set; }
	protected const float AGGRO_START_TIME = 0.2f;
	protected const float AGGRO_LOSE_TIME = 0.4f;

	public float DamageToPlayer { get; protected set; }

	public float ScaleFactor { get; protected set; }
	public float PushStrength { get; protected set; }

	public float SpawnTime { get; protected set; }
	public float ShadowFullOpacity { get; protected set; }
	public virtual float FullOpacity => 1f;

	public string AnimSpawnPath { get; protected set; }
	public string AnimIdlePath { get; protected set; }
	public string AnimAttackPath { get; protected set; }
	public string AnimDiePath { get; protected set; }

	public float Deceleration { get; protected set; }
	public float DecelerationAttacking { get; protected set; }

	protected TimeSince _spawnCloudTime;

	public Dictionary<TypeDescription, EnemyStatus> EnemyStatuses = new Dictionary<TypeDescription, EnemyStatus>();

	public BurningVfx BurningVfx { get; protected set; }
	private FrozenVfx _frozenVfx;
	private FearVfx _fearVfx;
	private CharmVfx _charmVfx;

	public float VfxOpacity { get; set; }

	public bool IsFrozen { get; set; }
	public bool IsFeared { get; set; }

	private float _animSpeed;
	public float AnimSpeed { get { return _animSpeed; } set { _animSpeed = value; Sprite.PlaybackSpeed = _animSpeed * _animSpeedModifier; } }
	private float _animSpeedModifier;
	public float AnimSpeedModifier { get { return _animSpeedModifier; } set { _animSpeedModifier = value; Sprite.PlaybackSpeed = _animSpeed * _animSpeedModifier; } }

	public int CoinValueMin { get; protected set; }
	public int CoinValueMax { get; protected set; }
	public float CoinChance { get; protected set; }

	public virtual float HeightVariance => 0f;
	public virtual float WidthVariance => 0f;


	public Thing Target { get; set; }

	public Color TintFullHp { get; set; }
	public Color TintZeroHp { get; set; }
	public Color TintDeath { get; set; }

	public bool IgnoreCollision { get; set; }

	public bool IsCharmed { get; set; }
	private TimeSince _timeSinceLookForEnemyTarget;
	private float _nextEnemyTargetDelay;
	private const float ENEMY_TARGET_DELAY_MIN = 0.3f;
	private const float ENEMY_TARGET_DELAY_MAX = 0.8f;
	public float CharmDamageTakenMultiplier { get; set; }
	public float CharmDamageDealtMultiplier { get; set; }

	public bool ShouldUpdateAfterGameOver { get; set; }
	public TimeSince TimeSinceTakeInfightingDamage { get; set; }

	public int EnemyIdNum { get; set; }

	public TimeSince TimeSinceBurn { get; set; }

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

		Sprite.LocalScale = new Vector3( Scale * Game.Random.Float( 1f - HeightVariance, 1f + HeightVariance ), Scale * Game.Random.Float( 1f - WidthVariance, 1f + WidthVariance ), 1f ) * Globals.SPRITE_SCALE;

		AnimSpawnPath = "spawn";
		AnimIdlePath = "walk";
		AnimAttackPath = "attack";
		AnimDiePath = "die";

		_animSpeed = 1f;
		_animSpeedModifier = 1f;

		SpawnShadow( ShadowScale, ShadowOpacity );

		IsSpawning = true;
		TimeSinceSpawn = 0f;
		SpawnTime = 1.75f;

		MoveTimeOffset = Game.Random.Float( 0f, 5f );
		Deceleration = 1.47f;
		DecelerationAttacking = 1.33f;
		DeathTime = 0.3f;
		AggroRange = 1.4f;
		CanAttack = true;
		CanAttackAnim = true;
		CanTurn = true;

		CoinValueMin = 1;
		CoinValueMax = 1;
		CoinChance = 0.55f;

		TintFullHp = Color.White;
		TintZeroHp = new Color( 0.3f, 0.3f, 0.3f );
		TintDeath = new Color( 0.5f, 0.5f, 0.5f );

		VfxOpacity = 0f;

		Sprite.PlayAnimation( AnimSpawnPath );
	}

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

	//	Sprite.Tint = TintFullHp;
	//}

	protected override void OnUpdate()
	{
		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"Anim: {Sprite.CurrentAnimation.Name}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );

		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.05f );
		//Gizmo.Draw.LineSphere( (Vector3)Position2D, Radius );

		//Gizmo.Draw.Color = Color.Black.WithAlpha( 0.2f );
		//Gizmo.Draw.Text( $"{Health}/{MaxHealth}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.2f, 0f ) ) );

		float dt = Time.Delta;

		//HandleFlashing( dt );
		HandleFlashing( RealTime.Delta );

		if ( !Manager.Instance.ShouldUpdateThings && !ShouldUpdateAfterGameOver )
			return;

		base.OnUpdate();

		if ( !Target.IsValid() )
		{
			if ( !IsCharmed )
				Target = Manager.Instance.Player;
		}

		UpdateSprite( Target );

		if ( IsSpawning )
		{
			HandleSpawning();
			return;
		}

		if ( IsDying )
		{
			HandleDying( dt );
			return;
		}

		Sprite.SpriteFlags = FlipX ? SpriteFlags.HorizontalFlip : SpriteFlags.None;

		if ( IsProxy )
			return;

		HandleStatuses( dt );

		float timeScale = this is Boss ? Utils.Map( TimeScale, 1f, 0f, 1f, 0.3f ) : TimeScale;
		UpdatePosition( dt * timeScale );
		WorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );

		ClampToBounds();
		//Depth = -Position.y * 10f;

		if ( !IgnoreCollision )
		{
			CheckCollisions( dt );
			HandleAttacking( Target, dt * timeScale );
		}

		HandleDeceleration( dt );

		TempWeight *= (1f - dt * 4.7f);

		if ( Target.IsValid() && Target is Enemy enemy )
		{
			if ( enemy.IsDying || enemy.IsCharmed == IsCharmed )
			{
				Target = IsCharmed ? null : Manager.Instance.Player;
			}
		}

		if ( Manager.Instance.CharmedEnemyCount > 0 )
		{
			//if ( Target.IsValid() && !IsCharmed && Target is Enemy )
			//{
			//	Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.3f );
			//	Gizmo.Draw.LineThickness = 5f;
			//	Gizmo.Draw.Line( WorldPosition, Target.WorldPosition + new Vector3( 0f, 0.05f, 0f ) );
			//}

			//if ( Target.IsValid() && IsCharmed )
			//{
			//	Gizmo.Draw.Color = Color.Red;
			//	Gizmo.Draw.LineThickness = 1f;
			//	Gizmo.Draw.Line( WorldPosition, Target.WorldPosition );
			//}

			if ( _timeSinceLookForEnemyTarget > _nextEnemyTargetDelay )
				LookForEnemyTarget();
		}
	}

	//protected override void OnFixedUpdate()
	//{
	//	base.OnFixedUpdate();

	//	if ( !IgnoreCollision )
	//	{
	//		CheckCollisions( Time.Delta );
	//	}
	//}

	protected virtual void HandleStatuses( float dt )
	{
		for ( int i = EnemyStatuses.Count - 1; i >= 0; i-- )
		{
			var status = EnemyStatuses.Values.ElementAt( i );
			if ( status.ShouldUpdate )
				status.Update( dt );
		}
	}

	protected virtual void HandleDeceleration( float dt )
	{
		//var oldVel = Velocity;
		Velocity *= Math.Max( 1f - dt * (IsAttacking ? DecelerationAttacking : Deceleration), 0f );

		//if(this is Zombie)
		//	Log.Info( $"decel - dt: {dt}, oldVel: {oldVel.Length} newVel: {Velocity.Length} now: {Time.Now} {(dt > 0.1f ? "----------------------" : "")}" );
	}

	protected virtual void HandleAttacking( Thing target, float dt )
	{
		if ( !target.IsValid() )
		{
			IsAttacking = false;
			return;
		}

		float dist_sqr = (target.Position2D - Position2D).LengthSquared;
		float attack_dist_sqr = MathF.Pow( AggroRange, 2f );

		if ( !IsAttacking )
		{
			if ( CanAttack )
			{
				if ( dist_sqr < attack_dist_sqr )
				{
					_aggroTimer += dt;
					if ( _aggroTimer > AGGRO_START_TIME )
					{
						StartAttacking();
						_aggroTimer = 0f;
					}
				}
				else
				{
					_aggroTimer = 0f;
				}
			}
		}
		else
		{
			if ( dist_sqr > attack_dist_sqr )
			{
				_aggroTimer += dt;
				if ( _aggroTimer > AGGRO_LOSE_TIME )
				{
					IsAttacking = false;
				}
			}
			else
			{
				if ( !DontChangeAnimSpeed )
					AnimSpeed = Utils.Map( dist_sqr, attack_dist_sqr, 0f, 1f, 4f, EasingType.Linear );

				_aggroTimer = 0f;
			}
		}
	}

	public virtual void StartAttacking()
	{
		IsAttacking = true;
	}

	protected virtual void UpdateSprite( Thing target )
	{
		if ( IsDying )
			return;

		if ( !IsAttacking || !target.IsValid() || IsSpawning )
		{
			if ( IsSpawning )
				Sprite.PlayAnimation( AnimSpawnPath );
			else
				Sprite.PlayAnimation( AnimIdlePath );

			if ( !DontChangeAnimSpeed )
				AnimSpeed = Utils.Map( Utils.FastSin( MoveTimeOffset + Time.Now * 7.5f ), -1f, 1f, 0.75f, 3f, EasingType.ExpoIn );

			if ( CanTurn && !IsFrozen )
			{
				if ( MathF.Abs( Velocity.x ) > 0.175f )
					FlipX = Velocity.x > 0f;
			}
		}
		else
		{
			Sprite.PlayAnimation( AnimAttackPath );
			if ( !DontChangeAnimSpeed )
			{
				float dist_sqr = (target.Position2D - Position2D).LengthSquared;
				float attack_dist_sqr = MathF.Pow( AggroRange, 2f );
				AnimSpeed = Utils.Map( dist_sqr, attack_dist_sqr, 0f, 1f, 4f, EasingType.Linear );
			}

			if ( !IsProxy && CanTurn && !IsFrozen )
			{
				if ( IsFeared )
					FlipX = target.Position2D.x < Position2D.x;
				else
					FlipX = target.Position2D.x > Position2D.x;
			}
		}
	}

	void HandleFlashing( float dt )
	{
		if ( IsFlashing )
		{
			_flashTimer -= dt;
			if ( _flashTimer < 0f )
			{
				IsFlashing = false;
				Sprite.FlashTint = IsCharmed ? Color.Magenta.WithAlpha( 0.4f ) : Color.White.WithAlpha( 0f );
				Sprite.Tint = Color.Lerp( TintFullHp, TintZeroHp, Utils.Map( Health, MaxHealth, 0f, 0f, 1f ) ).WithAlpha( FullOpacity );

				//if ( IsCharmed )
				//	Sprite.Tint = Color.Lerp( Sprite.Tint, Color.Magenta, 0.9f );
			}
		}
	}

	void HandleDying( float dt )
	{
		DeathTimeElapsed += dt;

		Sprite.LocalScale = _deathScale * Utils.Map( DeathTimeElapsed, 0f, DeathTime, 1f, 1.2f );

		if ( DeathTimeElapsed > DeathTime )
		{
			DeathProgress = 1f;
			FinishDying();
		}
		else
		{
			DeathProgress = Utils.Map( DeathTimeElapsed, 0f, DeathTime, 0f, 1f );
			ShadowOpacity = Utils.Map( DeathProgress, 0f, 1f, ShadowFullOpacity, 0f, EasingType.QuadIn );
			ShadowSpriteDirty = true;
		}
	}

	void HandleSpawning()
	{
		if ( TimeSinceSpawn > SpawnTime )
		{
			FinishSpawning();
		}
		else
		{
			//Sprite.PlayAnimation( AnimSpawnPath );

			if ( _spawnCloudTime > (0.3f / TimeScale) )
			{
				var cloud = Manager.Instance.SpawnCloud( Position2D + new Vector2( 0f, 0.05f ) );
				cloud.Velocity = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ).Normal * Game.Random.Float( 0.2f, 0.6f );
				_spawnCloudTime = Game.Random.Float( 0f, 0.15f );
			}

			ShadowOpacity = Utils.Map( TimeSinceSpawn, 0f, SpawnTime, 0f, ShadowFullOpacity );
			VfxOpacity = Utils.Map( TimeSinceSpawn, 0f, SpawnTime, 0f, 1f );
		}

		ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );
	}

	protected virtual void FinishSpawning()
	{
		IsSpawning = false;
		ShadowOpacity = ShadowFullOpacity;
		ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );
		VfxOpacity = 1f;
	}

	void ClampToBounds()
	{
		var x_min = Manager.Instance.BOUNDS_MIN.x + Radius;
		var x_max = Manager.Instance.BOUNDS_MAX.x - Radius;
		var y_min = Manager.Instance.BOUNDS_MIN.y;
		var y_max = Manager.Instance.BOUNDS_MAX.y - Radius;
		Position2D = new Vector2( MathX.Clamp( Position2D.x, x_min, x_max ), MathX.Clamp( Position2D.y, y_min, y_max ) );
	}

	protected virtual void UpdatePosition( float dt )
	{

	}

	public virtual void Damage( float damage, Player player, Vector2 addVel, float addTempWeight, bool isCrit = false, DamageType damageType = DamageType.PlayerBullet )
	{
		if ( IsDying )
			return;

		if ( player != null )
		{
			if ( IsFeared )
			{
				damage *= player.Stats[PlayerStat.FearDamageMultiplier];
			}
		}

		Flash( 0.12f );

		if ( Manager.Instance.Difficulty >= 0 && !Manager.Instance.UnlockedBulletDmgAchievement && damage > 500f )
		{
			Sandbox.Services.Achievements.Unlock( "big_bullet" );
			Manager.Instance.UnlockedBulletDmgAchievement = true;
		}

		if ( IsCharmed )
			damage *= CharmDamageTakenMultiplier;

		//DamageNumbers.Add( (int)damage, Position2D + Vector2.Up * Radius * 3f + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.2f, color: isCrit ? Color.Yellow : Color.White );
		var color = IsCharmed ? new Color( 1f, 0.05f, 1f ) : (isCrit ? new Color( 1f, 1f, 0.02f ) : Color.White);
		//DamageNumbersLegacy.Create( damage, Position2D + new Vector2( 0.4f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color );

		var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );

		float size;
		if ( damage < 5f ) size = Utils.Map( damage, 1f, 5f, 1.05f, 1.3f, EasingType.QuadOut );
		else if ( damage < 20f ) size = Utils.Map( damage, 5f, 20f, 1.3f, 1.6f, EasingType.Linear );
		else size = Utils.Map( damage, 20f, 100f, 1.6f, 1.9f, EasingType.Linear );

		Manager.Instance.SpawnDamageNumber( pos, damage, color, size );

		if ( IsProxy )
			return;

		if ( player != null )
		{
			if ( IsFeared )
			{
				if ( player.Stats[PlayerStat.FearDrainPercent] > 0f )
				{
					if ( player.Health < player.Stats[PlayerStat.MaxHp] )
						player.TimeSinceChangeHP = 0f;

					//Log.Info( $"damage: {damage}, Health: {Health}" );

					float damageToDrainFrom = damage * (damageType == DamageType.FearPain ? 0.2f : 1f);
					player.RegenHealth( Math.Min( damageToDrainFrom, Math.Max( Health, 0f ) ) * player.Stats[PlayerStat.FearDrainPercent] );
				}
			}
		}

		Velocity += addVel;
		TempWeight += addTempWeight;

		Health -= damage;

		if ( Health <= 0f )
			StartDying( player );
	}

	public virtual void DamageFire( float damage, Player player )
	{
		damage *= player.GetDamageMultiplier();

		if ( IsFrozen )
			damage *= player?.Stats[PlayerStat.FreezeFireDamageMultiplier] ?? 1f;

		Damage( damage, player, addVel: Vector2.Zero, addTempWeight: 0f );
	}

	public virtual void StartDying( Player player )
	{
		IsDying = true;
		DeathProgress = 0f;
		DeathTimeElapsed = 0f;
		Sprite.PlayAnimation( AnimDiePath );
		AnimSpeed = Game.Random.Float( 6.5f, 9f );

		IsFlashing = false;

		Sprite.Tint = TintDeath;
		Sprite.FlashTint = Color.White.WithAlpha( 0f );

		_deathScale = Sprite.LocalScale;

		if ( CanBleed )
			Manager.Instance.SpawnBloodSplatter( Position2D );

		Manager.Instance.PlayEnemyDeathSfxLocal( Position2D );

		if ( IsProxy )
			return;

		if ( Manager.Instance.Difficulty >= 0 )
			Sandbox.Services.Stats.Increment( "zombies_killed", 1 );

		if ( player is not null )
		{
			player.ForEachStatus( status => status.OnKill( this ) );

			//if ( this is not Crate )
			//{
			//	Sandbox.Services.Stats.Increment( player.Client, "kills", 1, $"{GetType().Name.ToLowerInvariant()}" );
			//}
			//else
			//{
			//	Sandbox.Services.Stats.Increment( player.Client, "crates", 1 );
			//}
		}

		DropLoot( player );

		for ( int i = EnemyStatuses.Count - 1; i >= 0; i-- )
			EnemyStatuses.Values.ElementAt( i ).StartDying();
	}

	public virtual void DropLoot( Player player )
	{
		//var coin_chance = player != null ? Utils.Map( player.Stats[PlayerStat.Luck], 0f, 10f, 0.5f, 1f ) : 0.5f;
		var coin_chance = player == null || Manager.Instance.ElapsedTime > 60f
			? CoinChance
			: Utils.Map( player.Level, 0, 3, MathX.Lerp( CoinChance, 1f, 0.55f ), CoinChance );

		if ( Manager.Instance.Difficulty < 0 )
			coin_chance *= 1.25f;

		//Log.Info( $"{this.GetType()} coin_chance: {coin_chance} CoinValueMin: {CoinValueMin}" );

		if ( Game.Random.Float( 0f, 1f ) < coin_chance )
		{
			Manager.Instance.SpawnCoin( Position2D, vel: Vector2.Zero, value: Game.Random.Int( CoinValueMin, CoinValueMax ), force: coin_chance >= 0.95f );
		}
		else
		{
			var lowest_hp_percent = 1f;
			foreach ( Player p in Manager.Instance.GetPlayers() )
				lowest_hp_percent = MathF.Min( lowest_hp_percent, p.Health / p.Stats[PlayerStat.MaxHp] );

			var healthPackCount = Scene.GetAllComponents<HealthPack>().Count();
			var health_pack_chance = Utils.Map( lowest_hp_percent, 1f, 0f, 0f, 0.1f ) * Utils.Map( healthPackCount, 0, 5, 1f, 0f, EasingType.SineOut );

			if ( Manager.Instance.Difficulty < 0 )
				health_pack_chance *= 1.5f;
			else if ( Manager.Instance.Difficulty >= 3 )
				health_pack_chance *= 0.2f;

			if ( Game.Random.Float( 0f, 1f ) < health_pack_chance )
			{
				Manager.Instance.SpawnHealthPack( Position2D, vel: Vector2.Zero );
			}
		}
	}

	public virtual void FinishDying()
	{
		Remove();
	}

	public override void Remove()
	{
		if ( !IsProxy )
		{
			for ( int i = EnemyStatuses.Count - 1; i >= 0; i-- )
				EnemyStatuses.Values.ElementAt( i ).Remove();

			EnemyStatuses.Clear();
		}

		base.Remove();
	}

	public void Flash( float time )
	{
		if ( IsFlashing )
			return;

		Sprite.Tint = Color.White.WithAlpha( 1f );
		Sprite.FlashTint = Color.White.WithAlpha( 1f );
		IsFlashing = true;
		_flashTimer = time;
	}

	public void Flash( float time, Color color )
	{
		if ( IsFlashing )
			return;

		Sprite.Tint = color;
		Sprite.FlashTint = color;
		IsFlashing = true;
		_flashTimer = time;
	}

	void CheckCollisions( float dt )
	{
		for ( int dx = -1; dx <= 1; dx++ )
		{
			for ( int dy = -1; dy <= 1; dy++ )
			{
				Manager.Instance.HandleThingCollisionForGridSquare( this, new GridSquare( GridPos.x + dx, GridPos.y + dy ), dt );
			}
		}
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		for ( int i = EnemyStatuses.Count - 1; i >= 0; i-- )
			EnemyStatuses.Values.ElementAt( i ).Colliding( other, percent, dt );
	}

	public TStatus AddEnemyStatus<TStatus>()
		where TStatus : EnemyStatus
	{
		var type = TypeLibrary.GetType<TStatus>();

		if ( EnemyStatuses.TryGetValue( type, out var status ) )
		{
			status.Refresh();
			return (TStatus)status;
		}
		else
		{
			status = type.Create<EnemyStatus>();
			EnemyStatuses.Add( type, status );
			status.Init( this );
			return (TStatus)status;
		}
	}

	public void RemoveEnemyStatus<TStatus>( TStatus status )
		where TStatus : EnemyStatus
	{
		if ( EnemyStatuses.Remove( TypeLibrary.GetType<TStatus>(), out var existing ) )
		{
			Assert.AreEqual( existing, status );
			status.Remove();
		}
	}

	private void RemoveEnemyStatus<TStatus>()
		where TStatus : EnemyStatus
	{
		if ( EnemyStatuses.Remove( TypeLibrary.GetType<TStatus>(), out var status ) )
		{
			status.Remove();
		}
	}

	public TStatus GetEnemyStatus<TStatus>()
		where TStatus : EnemyStatus
	{
		return EnemyStatuses.TryGetValue( TypeLibrary.GetType<TStatus>(), out var status )
			? (TStatus)status
			: null;
	}

	public bool HasEnemyStatus<TStatus>( TStatus status )
		where TStatus : EnemyStatus
	{
		return EnemyStatuses.TryGetValue( TypeLibrary.GetType<TStatus>(), out var existing ) && existing == status;
	}

	public bool HasEnemyStatus<TStatus>()
		where TStatus : EnemyStatus
	{
		return EnemyStatuses.ContainsKey( TypeLibrary.GetType<TStatus>() );
	}

	public void CreateBurningVfx()
	{
		var obj = Manager.Instance.BurningVfxPrefab.Clone( WorldPosition );
		obj.Parent = GameObject;
		obj.LocalPosition = new Vector3( 0f, 0f, 1f );

		BurningVfx = obj.Components.Get<BurningVfx>();
		BurningVfx.Enemy = this;
	}

	public void RemoveBurningVfx()
	{
		if ( BurningVfx != null )
		{
			BurningVfx.GameObject.Destroy();
			BurningVfx = null;
		}
	}

	public void CreateFrozenVfx()
	{
		var obj = Manager.Instance.FrozenVfxPrefab.Clone( WorldPosition );
		obj.Parent = GameObject;
		obj.LocalPosition = new Vector3( 0f, 0f, 2f );

		_frozenVfx = obj.Components.Get<FrozenVfx>();
		_frozenVfx.Enemy = this;
	}

	public void RemoveFrozenVfx()
	{
		if ( _frozenVfx != null )
		{
			_frozenVfx.GameObject.Destroy();
			_frozenVfx = null;
		}
	}

	public void CreateFearVfx()
	{
		var obj = Manager.Instance.FearVfxPrefab.Clone( WorldPosition );
		obj.Parent = GameObject;
		obj.LocalPosition = new Vector3( 0f, 0f, 3f );

		_fearVfx = obj.Components.Get<FearVfx>();
		_fearVfx.Enemy = this;
	}

	public void RemoveFearVfx()
	{
		if ( _fearVfx != null )
		{
			_fearVfx.GameObject.Destroy();
			_fearVfx = null;
		}
	}

	public void CreateCharmVfx()
	{
		var obj = Manager.Instance.CharmVfxPrefab.Clone( WorldPosition );
		obj.Parent = GameObject;
		obj.LocalPosition = new Vector3( 0f, 0f, 2f );

		_charmVfx = obj.Components.Get<CharmVfx>();
		_charmVfx.Enemy = this;
	}

	public void RemoveCharmVfx()
	{
		if ( _charmVfx != null )
		{
			_charmVfx.GameObject.Destroy();
			_charmVfx = null;
		}
	}

	public void Burn( Player player, float damage, float lifetime, float spreadChance )
	{
		var burning = AddEnemyStatus<BurningEnemyStatus>();
		burning.Player = player;
		burning.Damage = damage;
		burning.Lifetime = lifetime;
		burning.SpreadChance = spreadChance;

		//if ( player != null )
		//	player.ForEachStatus( status => status.OnBurn( this ) );
	}

	public void Freeze( Player player )
	{
		if ( IsDying )
			return;

		var frozen = AddEnemyStatus<FrozenEnemyStatus>();
		frozen.Player = player;
		frozen.SetLifetime( player.Stats[PlayerStat.FreezeLifetime] );
		frozen.SetTimeScale( player.Stats[PlayerStat.FreezeTimeScale] );

		//if ( player != null )
		//	player.ForEachStatus( status => status.OnFreeze( this ) );
	}

	public void Fear( Player player )
	{
		if ( IsDying || this is Crate )
			return;

		var fear = AddEnemyStatus<FearEnemyStatus>();
		fear.Player = player;
		fear.SetLifetime( player?.Stats[PlayerStat.FearLifetime] ?? 4f );

		if ( player != null )
		{
			Log.Info( $"player.Stats[PlayerStat.FearPainPercent]: {player.Stats[PlayerStat.FearPainPercent]}" );

			if ( player.Stats[PlayerStat.FearPainPercent] > 0f )
			{
				if ( this is Boss )
					fear.PainPercent = player.Stats[PlayerStat.FearPainPercent] * 0.1f;
				else
					fear.PainPercent = player.Stats[PlayerStat.FearPainPercent];
			}

			//player.ForEachStatus( status => status.OnFear( this ) );
		}
	}

	protected virtual void OnDamagePlayer( Player player, float damage )
	{
		if ( player.Stats[PlayerStat.ThornsPercent] > 0f )
			Damage( damage * player.Stats[PlayerStat.ThornsPercent] * player.GetDamageMultiplier(), player, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false );

		if ( Game.Random.Float( 0f, 1f ) < player.Stats[PlayerStat.FreezeOnMeleeChance] )
		{
			if ( !HasEnemyStatus<FrozenEnemyStatus>() )
				Manager.Instance.PlaySfxNearby( "frozen", Position2D, pitch: Game.Random.Float( 1.1f, 1.2f ), volume: 1.3f, maxDist: 5f );

			Freeze( player );
		}

		if ( Game.Random.Float( 0f, 1f ) < player.Stats[PlayerStat.FearOnMeleeChance] )
		{
			if ( !HasEnemyStatus<FearEnemyStatus>() )
				Manager.Instance.PlaySfxNearby( "fear", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 0.6f, maxDist: 5f );

			Fear( player );
		}
	}

	public void Charm()
	{
		if ( IsCharmed || this is Crate || this is Boss )
			return;

		var charmed = AddEnemyStatus<CharmedEnemyStatus>();
		charmed.Lifetime = 999f;

		CharmDamageTakenMultiplier = Manager.Instance.Player.Stats[PlayerStat.CharmedEnemyDmgTakenMultiplier];
		CharmDamageDealtMultiplier = Manager.Instance.Player.Stats[PlayerStat.CharmedEnemyDmgDealtMultiplier];
	}

	public void LookForEnemyTarget()
	{
		if ( Manager.Instance.CharmedEnemyCount == 0 )
			return;

		if ( !IsValid || IsRemoved || IsDying || this is Crate )
			return;

		Enemy closestTarget = null;
		var closestDistSqr = float.MaxValue;

		for ( int dx = -2; dx <= 2; dx++ )
		{
			for ( int dy = -2; dy <= 2; dy++ )
			{
				var gridSquare = new GridSquare( GridPos.x + dx, GridPos.y + dy );

				if ( !Manager.Instance.ThingGridPositions.ContainsKey( gridSquare ) )
					continue;

				var things = Manager.Instance.ThingGridPositions[gridSquare];
				if ( things.Count == 0 )
					continue;

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

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

					if ( other is not Enemy enemy )
						continue;

					if ( IsCharmed == enemy.IsCharmed )
						continue;

					if ( enemy is Crate || enemy.IsDying || enemy.IgnoreCollision )
						continue;

					var dist_sqr = (Position2D - other.Position2D).LengthSquared;
					if ( dist_sqr < closestDistSqr )
					{
						closestDistSqr = dist_sqr;
						closestTarget = enemy;
					}
				}
			}
		}

		Target = closestTarget;

		if ( !IsCharmed )
		{
			float player_dist_sqr = (Manager.Instance.Player.Position2D - Position2D).LengthSquared;
			if ( player_dist_sqr < closestDistSqr )
			{
				Target = Manager.Instance.Player;
			}
		}

		_nextEnemyTargetDelay = Game.Random.Float( ENEMY_TARGET_DELAY_MIN, ENEMY_TARGET_DELAY_MAX );
		_timeSinceLookForEnemyTarget = 0f;
	}

	public virtual void Celebrate()
	{
		if ( IsSpawning )
			FinishSpawning();
	}
}