things/enemies/Exploder.cs

Enemy subclass 'Exploder' that moves, starts an exploding animation when dying, and deals an area explosion on death. Handles movement, collisions with players and other enemies, starts explosion timing, spawns effects/sfx, and applies damage/forces to nearby Things when it finishes dying.

NetworkingFile Access
using Sandbox;
using static Manager;

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

	private const float EXPLOSION_RADIUS = 1.45f;
	public float ExplosionDamage { get; set; }
	private const float EXPLODE_TIME = 1.5f;

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

	private Player _playerWhoKilledUsId;

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

	public float MoveSpeed { get; set; }
	public float MoveSpeedAttacking { get; set; }

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

		Scale = 1.1f;

		base.OnAwake();

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

		Radius = 0.24f;

		Health = 40f;

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

		MaxHealth = Health;
		DamageToPlayer = 12f;

		DeathTime = 0.2f;

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

		if ( IsProxy )
			return;

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

		_damageTime = DAMAGE_TIME;

		MoveSpeed = 0.7f;
		MoveSpeedAttacking = 1.3f;

		ExplosionDamage = 40f;
	}

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

		if ( !IsExploding )
		{
			var targetPos = GetTargetPos();
			Velocity += (targetPos - Position2D).Normal * 1.0f * dt * (IsFeared ? -1f : 1f);

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

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

			WorldPosition += (Vector3)Velocity * speed * dt;
		}
		else
		{
			WorldPosition += (Vector3)Velocity * 0.2f * dt;
		}
	}

	protected virtual Vector2 GetTargetPos()
	{
		return Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
	}

	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 virtual void Explode()
	{
		Manager.Instance.SpawnExplosionEffect( Position2D, Color.Yellow, Color.Red );
		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)) );

			if ( numBlobs > 3 && Game.Random.Float( 0f, 1f ) < 0.5f )
				numBlobs--;

			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.9f, 1.1f ), 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 = ExplosionDamage;
		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 ) )
					{
						float force = ExplosionDamage * 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 * 2.5f * 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( 1.5f, 3f );

		Sprite.PlayAnimation( "cheer_start" );

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

		Sprite.PlayAnimation( "cheer" );
	}
}