things/enemies/Zombie.cs
using Sandbox;
using Sandbox.Diagnostics;
using SpriteTools;
using System.Reflection;

public class Zombie : Enemy
{
	private TimeSince _damageTime;
	private const float DAMAGE_TIME = 0.5f;

	public bool IsWandering { get; set; }
	private Vector2 _wanderPos;

	public override float HeightVariance => 0.065f;
	public override float WidthVariance => 0.035f;

	[Property] public Sprite EvolvedSprite { get; set; }
	[Property] public Sprite EvolvedBloodySprite { get; set; }

	private bool _hasHitPlayer;

	private float _movespeed;
	private float _movespeedAttacking;
	private int _behaviourNum;
	private TimeSince _timeSinceHurt;
	private float _behaviourRand0;
	private float _behaviourRand1;
	private float _behaviourRandSpeed0;
	private float _behaviourRandSpeed1;

	protected override void OnAwake()
	{
		ShadowScale = 0.95f;
		ShadowFullOpacity = 0.8f;
		ShadowOpacity = 0f;

		Scale = 0.85f;

		base.OnAwake();

		Sprite.LocalScale = Sprite.LocalScale.WithX( Sprite.LocalScale.x * Game.Random.Float( 0.9f, 1.1f ) );

		PushStrength = 10f;

		Radius = 0.25f;

		Health = 30f;

		if ( Manager.Instance.Difficulty < 0 )
			Health = 25f;

		MaxHealth = Health;
		DamageToPlayer = 7f;

		Sprite.PlayAnimation( AnimSpawnPath );

		TintDeath = new Color( 0.7f, 0.7f, 0.7f );

		if ( IsProxy )
			return;
		
		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );

		_damageTime = DAMAGE_TIME;

		IsWandering = Game.Random.Float(0f, 1f) < Utils.Map( Manager.Instance.ElapsedTime, 0f, 5f * 60f, 1f, 0f, EasingType.SineIn);

		_wanderPos = new Vector2( Game.Random.Float( Manager.Instance.BOUNDS_MIN.x + 1f, Manager.Instance.BOUNDS_MAX.x - 1f ), Game.Random.Float( Manager.Instance.BOUNDS_MIN.y + 1f, Manager.Instance.BOUNDS_MAX.y - 2f ) );

		_movespeed = 0.7f;
		_movespeedAttacking = 1.3f;

		if ( Manager.Instance.Difficulty >= 4 )
		{
			DamageToPlayer = 18f;

			//Health = 32f;
			//MaxHealth = Health;

			Sprite.Sprite = EvolvedSprite;

			//TintFullHp = new Color( 0f, 0.3f, 0f );
			//TintZeroHp = new Color( 0.1f, 0.3f, 0.3f );
			//TintDeath = new Color( 0.2f, 0.5f, 0.5f );

			_movespeed = 1f;
			_movespeedAttacking = 1.32f;

			_behaviourRand0 = Game.Random.Float( 0f, 99f );
			_behaviourRand1 = Game.Random.Float( 0f, 99f );
			_behaviourRandSpeed0 = Game.Random.Float( 0.7f, 1.3f );
			_behaviourRandSpeed1 = Game.Random.Float( 0.7f, 1.3f );

			float behaviourRand = Game.Random.Float( 0f, 1f );
			if ( behaviourRand < 0.65f )
				_behaviourNum = 0;
			else if (behaviourRand < 0.7f)
				_behaviourNum = 1;
			else if ( behaviourRand < 0.85f )
				_behaviourNum = 2;
			else if ( behaviourRand < 0.9f )
				_behaviourNum = 3;
			else if ( behaviourRand < 0.95f )
				_behaviourNum = 4;
			else
				_behaviourNum = 5;
		}
	}

	protected override void UpdatePosition( float dt )
	{
		base.UpdatePosition( dt );

		//Gizmo.Draw.Text( $"{_behaviourNum}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.2f, 0f ) ) );

		var manager = Manager.Instance;

		var targetPos = Position2D;

		if( Target.IsValid() )
		{
			targetPos = Target.Position2D;

			//if( Target is Player player && EnemyIdNum % 2 == 0 )
			//{
			//	var distSqr = (Target.Position2D - Position2D).LengthSquared;

			//	Vector2 anchor = player.Position2D + player.AverageVelocity * Utils.Map( distSqr, 0f, 10f * 10f, 0f, 1f);
			//	Vector2 perp = Utils.GetPerpendicularVector( player.AverageVelocity ).Normal;
			//	var perpA = anchor - perp * 10f;
			//	var perpB = anchor + perp * 10f;
			//	var isBehindPlayer = Utils.CheckPointSideOfLine( perpA, perpB, Position2D );

			//	if ( distSqr < 7f )
			//	{
			//		targetPos = Target.Position2D;

			//		//Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.4f );
			//		//Gizmo.Draw.Line( Position2D, targetPos );
			//	}
			//	else if( !isBehindPlayer || player.AverageVelocity.LengthSquared < 1f )
			//	{
			//		targetPos = player.Position2D + player.AverageVelocity * Utils.Map( distSqr, 7f, 12f, 0f, 1.2f );

			//		//Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.4f );
			//		//Gizmo.Draw.Line( Position2D, targetPos );
			//	}
			//	else
			//	{
			//		//targetPos = player.Position2D + player.AverageVelocity * Utils.Map( distSqr, 7f, 12f, 0f, 1.2f );

			//		targetPos = Position2D + player.AverageVelocity;

			//		//Gizmo.Draw.Color = Color.Blue.WithAlpha( 0.4f );
			//		//Gizmo.Draw.Line( Position2D, targetPos );
			//	}
			//}
			//else
			//{
			//	targetPos = Target.Position2D;
			//}
		}
		else if(IsCharmed)
		{
			targetPos = manager.Player.Position2D;
		}

		if ( !IsWandering )
		{
			if( Manager.Instance.Difficulty >= 4 )
			{
				switch(_behaviourNum)
				{
					case 0: default:
						Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);
						break;
					case 1:
						if(_timeSinceHurt > 0.5f && Utils.FastSin( (TimeSinceSpawn + _timeSinceHurt) * 0.5f ) > 0.85f )
							Velocity += Utils.GetPerpendicularVector((targetPos - Position2D).Normal) * Utils.FastSin(TimeSinceSpawn * 0.5f) * 2f * dt * (IsFeared ? -1f : 1f);
						else
							Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

						break;
					case 2:
						if ( Target.IsValid() )
							targetPos = Target.Position2D + Target.Velocity * 3f * (0.8f + Utils.FastSin( _behaviourRand0 + TimeSinceSpawn * _behaviourRandSpeed0 ) * 0.5f);

						Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

						break;
					case 3:
						if(!IsAttacking)
							targetPos = Position2D + new Vector2( Utils.FastSin( _behaviourRand0 + TimeSinceSpawn * _behaviourRandSpeed0 ), Utils.FastSin( _behaviourRand1 + TimeSinceSpawn * _behaviourRandSpeed1 ) );

						Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

						break;
					case 4:
						if ( Target.IsValid() && Target.Velocity.LengthSquared > 0f )
							targetPos = Position2D + Utils.GetPerpendicularVector( Target.Velocity ) * (Target.Position2D.x < Position2D.x ? -1f : 1f);

						Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

						break;
					case 5:
						if ( Target.IsValid() && Target is Player player )
							targetPos = player.Position2D - player.AimDir * 1f + Utils.FastSin( _behaviourRand0 + TimeSinceSpawn * _behaviourRandSpeed0 ) * 1f;

						Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

						break;
				}

				//return Position2D + Utils.GetPerpendicularVector( Target.Velocity ) * (Target.Position2D.x < Position2D.x ? -1f : 1f);
			}
			else
			{
				Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);
			}
		}
		else
		{
			var wander_dist_sqr = (_wanderPos - Position2D).LengthSquared;
			if ( wander_dist_sqr < 0.25f )
			{
				_wanderPos = new Vector2( MathX.Clamp( targetPos.x + Game.Random.Float( -5f, 5f ), manager.BOUNDS_MIN.x + 1f, manager.BOUNDS_MAX.x - 1f ), MathX.Clamp( targetPos.y + Game.Random.Float( -5f, 5f ), manager.BOUNDS_MIN.y + 1f, manager.BOUNDS_MAX.y - 2f ) );
			}

			Velocity += (_wanderPos - Position2D).Normal * dt;

			var timeSinceInputMove = Target is Player player ? player.TimeSinceInputMove.Relative : 0f;
			float detect_dist = timeSinceInputMove < 20f ? 3.5f : Utils.Map( timeSinceInputMove, 20f, 400f, 9f, 30f, EasingType.Linear );

			var player_dist_sqr = (targetPos - Position2D).LengthSquared;
			if ( player_dist_sqr < detect_dist * detect_dist )
			{
				IsWandering = false;

				if(detect_dist < 4f)
					Manager.Instance.PlaySfxNearby( "zombie.spawn0", Position2D, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 1f, maxDist: 4f );
			}

			if ( TimeSinceSpawn > 60f )
				IsWandering = false;
		}

		float speed = (IsAttacking ? _movespeedAttacking : _movespeed) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);

		if ( manager.Difficulty < 0 )
			speed *= 0.85f;
		//else if ( manager.Difficulty >= 4 )
		//	speed *= 1.5f;

		WorldPosition += (Vector3)Velocity * speed * dt;
	}

	public override void Colliding( Thing other, float percent, float dt )
	{
		base.Colliding( other, percent, dt );

		if ( other is Enemy enemy && !enemy.IsDying )
		{
			var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
			Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;

			if ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
			{
				var dmg = DamageToPlayer;
				if ( IsCharmed )
					dmg *= CharmDamageDealtMultiplier;

				enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
				enemy.Target = this;
				_damageTime = 0f;

				if(enemy.TimeSinceTakeInfightingDamage > 0.25f)
					Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );

				enemy.TimeSinceTakeInfightingDamage = 0f;
			}
		}
		// todo: move collision check to player instead to prevent laggy hits?
		else if ( other is Player player )
		{
			if ( !player.IsDead )
			{
				//var oldVel = Velocity;
				Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;

				//Log.Info( $"Colliding - dt: {dt} oldVel: {oldVel.Length} newVel: {Velocity.Length} now: {Time.Now} {(dt > 0.1f ? "----------------------" : "")}" );

				if ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
				{
					float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );

					if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
					{
						Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );

						player.Damage( dmg );

						if( dmg > 0f )
						{
							OnDamagePlayer( player, dmg );

							if(!_hasHitPlayer && Manager.Instance.Difficulty >= 4 && !IsDying)
							{
								Sprite.Sprite = EvolvedBloodySprite;
								_hasHitPlayer = true;
							}
						}
					}

					_damageTime = 0f;
				}
			}
		}
	}

	public override void Damage( float damage, Player player, Vector2 addVel, float addTempWeight, bool isCrit = false, DamageType damageType = DamageType.PlayerBullet )
	{
		base.Damage( damage, player, addVel, addTempWeight, isCrit, damageType );

		IsWandering = false;
		_timeSinceHurt = 0f;
	}

	public override void Celebrate()
	{
		base.Celebrate();

		CelebrateAsync();
	}

	async void CelebrateAsync()
	{
		await Task.Delay( Game.Random.Int( 0, 500 ) );

		Sprite.PlaybackSpeed = Game.Random.Float( 1.5f, 3f );

		Sprite.PlayAnimation( "cheer_start" );

		await Task.Delay( Game.Random.Int( 200, 400 ) );

		Sprite.PlayAnimation( "cheer" );
	}
}