things/enemies/ExploderElite.cs

An enemy AI class ExploderElite that extends Enemy. Controls movement, explosion charging and detonation, collision behavior with players and other enemies, spawning effects on death, and celebratory animations.

NetworkingFile Access
using Sandbox;
using static Manager;

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

	private const float EXPLOSION_RADIUS = 1.45f;
	private const float EXPLOSION_DAMAGE = 40f;
	private const float EXPLODE_TIME = 3.5f;

	[Sync] public bool IsExploding { get; set; }
	private TimeSince _explodeStartTime;
	private bool _hasExploded;
	//private bool _hasStartedLooping;

	private Player _playerWhoKilledUsId;

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

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

		Scale = 1.2f;

		base.OnAwake();

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

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

		PushStrength = 12f;
		Deceleration = 1.87f;
		DecelerationAttacking = 1.53f;

		Radius = 0.275f;

		Health = 75f;

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

		MaxHealth = Health;
		DamageToPlayer = 13f;

		DeathTime = 0.2f;

		CoinValueMin = 2;
		CoinValueMax = 6;
		CoinChance = 0.65f;

		if ( IsProxy )
			return;

		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );

		_damageTime = DAMAGE_TIME;
	}

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

		if ( !Manager.Instance.ShouldUpdateThings )
			return;

		if ( IsExploding )
			Sprite.FlashTint = Color.Yellow.WithAlpha( (0.5f + Utils.FastSin( Time.Now * 32f ) * 0.5f) * Utils.Map( _explodeStartTime, 0.5f, EXPLODE_TIME, 0f, 0.75f, EasingType.QuadIn ) );

		if ( IsExploding )
		{
			if ( !IsProxy && !_hasExploded && _explodeStartTime > EXPLODE_TIME )
				Explode();
		}
	}

	protected override void UpdateSprite( Thing target )
	{
		if ( IsExploding )
		{
			if ( _explodeStartTime > 0.5f )
			{
				Sprite.PlayAnimation( "explode_loop" );
			}
			else
			{
				Sprite.PlayAnimation( "explode_start" );
			}
		}
		else
		{
			base.UpdateSprite( target );
		}
	}

	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 * 1.0f * dt * (IsFeared ? -1f : 1f);

		if ( IsExploding )
		{
			var speed = Manager.Instance.Difficulty < 0 ? 2.5f : (Manager.Instance.Difficulty == 0 ? 4f : 4.5f);

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

			WorldPosition += (Vector3)Velocity * speed * dt;
		}
		else
		{
			float speed = IsAttacking ? 2f : 1.3f;

			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;
				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 );
			}
		}
		// 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 StartDying( Player player )
	{
		if ( !IsExploding )
		{
			StartExploding();
			_playerWhoKilledUsId = player;
		}
	}

	public void StartExploding()
	{
		IsExploding = true;
		_explodeStartTime = 0f;
		Sprite.PlayAnimation( "explode_start" );
		CanAttack = false;
		CanAttackAnim = false;
		CanTurn = false;
	}

	public void Explode()
	{
		Manager.Instance.SpawnExplosionEffect( Position2D, Color.Yellow, Color.Orange );
		Manager.Instance.PlaySfxNearby( "explode", Position2D, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 0.9f, maxDist: 6f );

		_hasExploded = true;
		IsExploding = false;

		if ( IsProxy )
			return;

		base.StartDying( _playerWhoKilledUsId );
	}

	public override void FinishDying()
	{
		if ( Manager.Instance.Difficulty >= 1 && Game.Random.Float( 0f, 1f ) < 0.5f )
		{
			int numBlobs = (int)MathF.Floor( Game.Random.Float( 1f, (float)Math.Min( Manager.Instance.Difficulty, 5 ) ) );
			
			bool spawnedBlob = false;

			for ( int i = 0; i < numBlobs; i++ )
			{
				if ( Manager.Instance.GetLavaBlobEndPos( Position2D, out Vector2 endPos ) )
				{
					Manager.Instance.SpawnLavaBlob( Position2D, endPos );
					spawnedBlob = true;
				}
			}

			if ( spawnedBlob )
			{
				Manager.Instance.PlaySfxNearby( "lava_blob_01", Position2D, pitch: Game.Random.Float( 0.95f, 1.15f ), volume: 1.4f, maxDist: 6f );
			}
		}

		List<Thing> nearbyThings = new List<Thing>();

		for ( int dx = -2; dx <= 2; dx++ )
			for ( int dy = -2; dy <= 2; dy++ )
				Manager.Instance.AddThingsInGridSquare( new GridSquare( GridPos.x + dx, GridPos.y + dy ), nearbyThings );

		var dmgToDeal = EXPLOSION_DAMAGE;
		if ( IsCharmed )
			dmgToDeal *= CharmDamageDealtMultiplier;

		foreach ( Thing thing in nearbyThings )
		{
			if ( !thing.IsValid() || thing == this )
				continue;

			if ( thing is Enemy enemy ) 
			{
				if ( !enemy.IsDying && (!enemy.IsSpawning || enemy.TimeSinceSpawn > 0.75f) && !enemy.IgnoreCollision )
				{
					var dist_sqr = (thing.Position2D - Position2D).LengthSquared;
					if ( dist_sqr < MathF.Pow( EXPLOSION_RADIUS, 2f ) )
					{
						var force = EXPLOSION_DAMAGE * Utils.Map( dist_sqr, 0f, MathF.Pow( EXPLOSION_RADIUS, 2f ), 0.2f, 0f, EasingType.QuadIn );
						if ( enemy is ExploderElite )
							force *= 0.2f;

						var addVel = (thing.Position2D - Position2D).Normal * force;
						var addTempWeight = 0f;

						enemy.Damage( dmgToDeal, null, addVel, addTempWeight, false );
					}
				}
			}
			else if ( thing is Player player )
			{
				if ( !player.IsDead && !player.IsInvulnerable && !player.IsTimePausedForChoosing )
				{
					var dist_sqr = (thing.Position2D - Position2D).LengthSquared;
					if ( dist_sqr < MathF.Pow( EXPLOSION_RADIUS, 2f ) * 0.95f )
					{
						var dmg = player.CheckDamageAmount( dmgToDeal, DamageType.Explosion );
						player.Damage( dmg );
						player.AddVelocity( (thing.Position2D - Position2D).Normal * dmg * Utils.Map( dist_sqr, 0f, MathF.Pow( EXPLOSION_RADIUS, 2f ) * 0.95f, 0.2f, 0f, EasingType.QuadIn ) );
					}
				}
			}
			else if ( thing is Coin || thing is Magnet || thing is HealthPack || thing is RerollPickup )
			{
				var dist_sqr = (thing.Position2D - Position2D).LengthSquared;
				if ( dist_sqr < MathF.Pow( EXPLOSION_RADIUS, 2f ) * 0.95f )
				{
					thing.Velocity += (thing.Position2D - Position2D).Normal * 3f * Utils.Map( dist_sqr, 0f, MathF.Pow( EXPLOSION_RADIUS, 2f ) * 0.95f, 1f, 0f, EasingType.QuadIn );
				}
			}
		}

		base.FinishDying();
	}

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

		CelebrateAsync();
	}

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

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

		Sprite.PlayAnimation( "cheer_start" );

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

		Sprite.PlayAnimation( "cheer" );
	}
}