things/enemies/SpitterElite.cs
using Sandbox;
using System;
using System.Runtime;

public class SpitterElite : Enemy
{
	private TimeSince _damageTime;
	private const float DAMAGE_TIME = 0.75f;

	private float _shootDelayTimer;
	protected float SHOOT_DELAY_MIN = 2f;
	protected float SHOOT_DELAY_MAX = 5f;

	public bool IsShooting { get; private set; }

	protected TimeSince _prepareShootTime;
	protected int _numVolleysShot;
	protected float _currShootDelay;

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

	private bool _isTeleporting;
	protected TimeSince _timeSinceTeleported;
	protected float _nextTeleportDelay;
	protected float TELEPORT_DELAY_MIN = 4f;
	protected float TELEPORT_DELAY_MAX = 20f;
	private TimeSince _timeSincePrepareTeleport;
	private const float TELEPORT_PREPARE_TIME = 0.6f;
	private Vector2 _teleportTargetPos;
	private bool _hasTeleported;

	public float ShootRange { get; protected set; }
	public float MoveSpeedModifier { get; protected set; }
	public float MoveSpeedAttacking { get; protected set; }
	public float TeleportRequiredDist { get; protected set; }

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

		Scale = 1.05f;

		base.OnAwake();

		//Sprite.Texture = Texture.Load("textures/sprites/spitter_elite.vtex");

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

		PushStrength = 8f;

		Radius = 0.26f;

		Health = 100f;

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

		MaxHealth = Health;
		DamageToPlayer = 11f;

		CoinValueMin = 2;
		CoinValueMax = 3;
		CoinChance = 0.7f;

		Sprite.PlayAnimation( AnimSpawnPath );

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

		_damageTime = DAMAGE_TIME;
		_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );
		_nextTeleportDelay = Game.Random.Float( TELEPORT_DELAY_MIN, TELEPORT_DELAY_MAX );
		_timeSinceTeleported = 0f;

		ShootRange = 9f;
		MoveSpeedModifier = 0.6f;
		MoveSpeedAttacking = 1.3f;
		TeleportRequiredDist = 15f;
	}

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

		base.UpdatePosition( dt );

		var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);

		if ( IsShooting )
		{
			if ( _numVolleysShot < 3 && _prepareShootTime > (_numVolleysShot == 0 ? 1.0f : _currShootDelay) )
				Shoot();
			else if ( _numVolleysShot >= 3 && _prepareShootTime > 0.6f )
				FinishShooting();

			return;
		}
		else if (_isTeleporting)
		{
			//Scene.TimeScale = 0.1f;
			if (!_hasTeleported)
			{
				float shadowOpacity = Utils.Map( _timeSincePrepareTeleport, 0f, TELEPORT_PREPARE_TIME, ShadowFullOpacity, 0f, EasingType.SineOut );
				ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );

				VfxOpacity = Utils.Map( _timeSincePrepareTeleport, 0f, TELEPORT_PREPARE_TIME, 1f, 0f, EasingType.QuadOut );

				if ( _timeSincePrepareTeleport > 0.2f )
					IgnoreCollision = true;

				if ( _timeSincePrepareTeleport > TELEPORT_PREPARE_TIME )
					ApplyTeleport();
			}
			else
			{
				float shadowOpacity = Utils.Map( _timeSincePrepareTeleport, 0f, 0.3f, 0f, ShadowFullOpacity, EasingType.SineOut );
				ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );

				VfxOpacity = Utils.Map( _timeSincePrepareTeleport, 0f, 0.3f, 0f, 1f );

				if ( _timeSincePrepareTeleport > 0.3f )
					FinishTeleport();
			}

			return;
		}
		else
		{
			UpdateVelocity(targetPos, dt);
		}

		float speed = MoveSpeedModifier * (IsAttacking ? MoveSpeedAttacking : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.5f : 0.2f);

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

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

		if( !IsShooting && !_isTeleporting && (Target.IsValid() || IsCharmed) && !Manager.Instance.IsGameOver )
		{
			var target_dist_sqr = (targetPos - Position2D).LengthSquared;
			var rangeSqr = ShootRange * ShootRange;

			if ( Manager.Instance.Difficulty < 0 )
				rangeSqr *= 0.7f;

			if ( target_dist_sqr < rangeSqr )
			{
				_shootDelayTimer -= dt;
				if ( _shootDelayTimer < 0f )
				{
					PrepareToShoot();
				}
			}
			
			if ( !IsShooting && target_dist_sqr < TeleportRequiredDist * TeleportRequiredDist && Manager.Instance.Difficulty > 0 )
			{
				if(_timeSinceTeleported > _nextTeleportDelay)
				{
					var teleportPos = GetTeleportPos();
					StartTeleporting( teleportPos );
				}
			}
		}
	}

	protected virtual Vector2 GetTeleportPos()
	{
		return Target != null
			? Target.Position2D + Target.Velocity * Game.Random.Float( 0f, 2.5f ) + Utils.GetRandomVector() * Game.Random.Float( 1f, 4f )
			: Position2D + Utils.GetRandomVector() * Game.Random.Float( 2f, 5f );
	}

	protected virtual void UpdateVelocity(Vector2 targetPos, float dt)
	{
		Velocity += (targetPos - Position2D).Normal * 1.0f * dt * (IsFeared ? -1f : 1f);
	}

	protected override void UpdateSprite( Thing target )
	{
		if ( Sprite.CurrentAnimation.Name.Contains( "shoot" ) || _isTeleporting ) return;

		base.UpdateSprite( target );
	}

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

		if(!_isTeleporting && !IsShooting && _timeSinceTeleported > _nextTeleportDelay * Game.Random.Float(0.75f, 0.95f) && Manager.Instance.Difficulty > 0)
		{
			var targetPos = GetTeleportPos();
			StartTeleporting( targetPos );
		}
	}

	public void PrepareToShoot()
	{
		if ( Manager.Instance.IsGameOver )
			return;

		_prepareShootTime = 0f;
		IsShooting = true;
		Sprite.PlayAnimation( "shoot" );
		DontChangeAnimSpeed = true;
		AnimSpeed = 1f;
		Manager.Instance.PlaySfxNearby( "spitter.prepare", Position2D, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.6f, maxDist: 2.75f );
		CanAttack = false;
		CanAttackAnim = false;
		_numVolleysShot = 0;
		_currShootDelay = Game.Random.Float( 0.1f, 0.5f );
	}

	public virtual void Shoot()
	{
		var target_pos = Target.IsValid() ? Target.Position2D + Target.Velocity * Game.Random.Float( 0.5f, 1.85f ) : Position2D + Utils.GetRandomVector() * 5f;
		var dir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -14f, 14f ) );
		//var enemyBullet = Manager.Instance.SpawnEnemyBullet( Position2D + new Vector2(0f, 0.55f) + dir * 0.03f, dir, speed: 2.15f );
		var enemyBullet = Manager.Instance.SpawnEnemyBullet( Position2D + dir * 0.03f, dir, speed: 2.1f );
		enemyBullet.SetColor( new Color( 1f, 0.2f, 0f ) );

		var lifetime = 6f;

		if ( Manager.Instance.Difficulty < 0 )
			lifetime *= 0.6f;
		else if ( Manager.Instance.Difficulty > 0 )
			lifetime += Manager.Instance.Difficulty * 1.2f;

		enemyBullet.Lifetime = lifetime;

		if ( IsCharmed )
		{
			enemyBullet.Creator = this;
			enemyBullet.BecomeCharmed(elite: true);
		}

		Velocity *= 0.25f;
		_numVolleysShot++;
		_prepareShootTime = 0f;
		_currShootDelay = Game.Random.Float( 0.1f, 0.5f );

		Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 1.0f, 1.1f ), volume: 0.9f, maxDist: 5f );

		if ( _numVolleysShot >= 3 )
		{
			Sprite.PlaybackSpeed = Game.Random.Float( 2f, 3f );
			Sprite.PlayAnimation( "shoot_reverse" );
		}
		else
		{
			Sprite.PlaybackSpeed = Game.Random.Float(4f, 6f);
			Sprite.PlayAnimation( "walk" );
			Sprite.PlayAnimation( "shoot" );
		}
	}

	public void FinishShooting()
	{
		Sprite.PlayAnimation( AnimIdlePath );
		CanAttack = true;
		CanAttackAnim = true;
		_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 1.5f : 1f);
		IsShooting = false;
		DontChangeAnimSpeed = false;
	}

	public void StartTeleporting(Vector2 pos)
	{
		pos = Manager.Instance.ClampToBounds( pos );

		_isTeleporting = true;
		_hasTeleported = false;
		_timeSincePrepareTeleport = 0f;
		CanAttack = false;
		CanAttackAnim = false;
		CanTurn = false;
		Velocity *= 0.5f;
		Sprite.PlayAnimation( "teleport" );
		_teleportTargetPos = pos;
		DontChangeAnimSpeed = true;
		Sprite.PlaybackSpeed = 2f;

		ShouldUpdateAfterGameOver = true;

		// todo: sfx
	}

	public void ApplyTeleport()
	{
		Position2D = _teleportTargetPos;
		_hasTeleported = true;
		Sprite.PlayAnimation( "teleport_reverse" );
		_timeSincePrepareTeleport = 0f;
		ShadowSprite.Tint = Color.Black.WithAlpha( 0f );
		Transform.ClearInterpolation();

		VfxOpacity = 0f;
		 
		// todo: sfx
	}

	public void FinishTeleport()
	{
		_isTeleporting = false;
		IgnoreCollision = false;
		CanAttack = true;
		CanAttackAnim = true;
		CanTurn = true;
		_timeSinceTeleported = 0f;
		_nextTeleportDelay = Game.Random.Float( TELEPORT_DELAY_MIN, TELEPORT_DELAY_MAX );
		ShadowSprite.Tint = Color.Black.WithAlpha( ShadowFullOpacity );
		DontChangeAnimSpeed = false;

		VfxOpacity = 1f;

		ShouldUpdateAfterGameOver = false;
		if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
			Celebrate();
	}

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

		if ( _isTeleporting )
			return;

		CelebrateAsync();
	}

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

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

		Sprite.PlayAnimation( "cheer_start" );

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

		Sprite.PlayAnimation( "cheer" );
	}
}