things/enemies/ZombieElite.cs
using Sandbox;

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

	public override float HeightVariance => 0.04f;
	public override float WidthVariance => 0.02f;

	protected override void OnAwake()
	{
		//OffsetY = -0.45f;
		ShadowScale = 1f;
		ShadowFullOpacity = 0.8f;
		ShadowOpacity = 0f;

		Scale = 0.95f;

		base.OnAwake();

		//AnimSpeed = 2f;
		//Sprite.Texture = Texture.Load("textures/sprites/zombie_elite.vtex");

		//ScaleFactor = 0.85f;
		//Sprite.Size = new Vector2( 1f, 1f ) * Scale;

		PushStrength = 12f;

		Radius = 0.27f;

		Health = 55f;

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

		MaxHealth = Health;
		DamageToPlayer = 9f;

		Sprite.PlayAnimation( AnimSpawnPath );

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

		//ShadowScale = 0.95f;
		_damageTime = DAMAGE_TIME;

		if(Manager.Instance.Difficulty > 0)
		{
			Deceleration = 1.8f;
			DecelerationAttacking = 0.6f;
			AggroRange = 1.25f;
		}
	}

	protected override void UpdatePosition( float dt )
	{
		base.UpdatePosition( dt );
		
		var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
		Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);

		var baseSpeed = Manager.Instance.Difficulty > 0
			? (IsAttacking ? 2.6f : 0.5f)
			: (IsAttacking ? 1.9f : 1f);

		var adjustSpeed = Manager.Instance.Difficulty > 0
			? (IsAttacking ? 24f : 14f)
			: (IsAttacking ? 16f : 9.5f);

		float speed = baseSpeed + Utils.FastSin( MoveTimeOffset + Time.Now * adjustSpeed ) * baseSpeed;

		if ( Manager.Instance.Difficulty < 0 )
			speed *= 0.85f;

		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 )
			{
				Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;

				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 );
					}

					_damageTime = 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" );
	}
}