things/enemies/Exploder.cs

An enemy subclass 'Exploder' for a game, representing a suicide/exploding enemy. It sets stats (health, speed, explosion radius/damage), controls animation playback rates and movement factors, starts explosion on death, and spawns lava blob projectiles when exploding.

NetworkingFile Access
using System;
using Sandbox;

public class Exploder : Enemy
{
	public override EnemyType EnemyType => EnemyType.Exploder;
	public override float GetMaxHealth()
	{
		return 40f;
	}

	public override Vector3 SpawnScale => new Vector3( 1.1f );

	protected override bool ShouldSpawnBloodDecal => false;
	public override bool CanTurn => !IsDying && !IsStunned;

	protected float _personalExplosionRadius;
	protected float _personalExplosionDamage;
	protected float _personalExplodeTime;

	public override bool CanCombust => false;

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

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

		PushStrength = 7000f;
		Weight = 1.5f;

		_personalSpeedScale = Game.Random.Float( 0.9f, 1.1f );
		_personalSpeedFreq = Game.Random.Float( 4f, 5f );

		_personalExplosionRadius = 110f;
		_personalExplosionDamage = 40f;
		_personalExplodeTime = 1.75f;

		_explodeAnim = "Explode";

		if ( IsProxy )
			return;

		AggroRange = 85f;
		DetectTargetRange = 250f;
		LoseTargetRange = 350f;
		LoseTargetTime = 3.5f;
		MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 9f, 11f, 12f );
		DamageTargetDelay = 0.75f;
		_personalTurnSpeed = Game.Random.Float( 0.5f, 2f );
		Acceleration = 100f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
		AccelerationAttacking = 235f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
		Deceleration = 2.5f;
		DecelerationAttacking = 2.2f;
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{ModelRenderer.PlaybackRate.ToString( "N2" )}", new global::Transform( WorldPosition ) );
		//Gizmo.Draw.Text( $"{ModelRenderer.SceneModel.CurrentSequence.TimeNormalized.ToString("N2")}", new global::Transform( WorldPosition ) );

		if ( IsProxy )
			return;
	}

	protected override void HandleExplodingPlaybackRate( float explodeTime )
	{
		SetPlaybackRate( Utils.Map( _timeSinceExplodeStart, 0f, explodeTime, 1f, 6f, EasingType.QuadIn ) * AnimSpeedModifier );
	}

	protected override float GetMoveSpeedFactor()
	{
		var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
		return Exploder.GetWalkAnimSpeed( progress, IsAttacking, EasingType.QuadInOut );
	}

	public static float GetWalkAnimSpeed( float progress, bool isAttacking, EasingType easingType )
	{
		if ( isAttacking )
		{
			var move0Start = 0.1f;
			var move0End = 0.40f;
			var move1Start = 0.5f;
			var move1End = 0.65f;
			var move2Start = 0.8f;
			var move2End = 0.975f;

			if ( progress > move0Start && progress < move0End )
				return Utils.Map( progress, move0Start, move0End, 0f, 1f, easingType );
			else if ( progress > move1Start && progress < move1End )
				return Utils.Map( progress, move1Start, move1End, 0f, 1f, easingType );
			else if ( progress > move2Start && progress < move2End )
				return Utils.Map( progress, move2Start, move2End, 0f, 1f, easingType );
		}
		else
		{
			var leftFootStart = 0.4f;
			var leftFootEnd = 0.90f;
			var rightFootStart = 0.0f;
			var rightFootEnd = 0.36f;

			if ( progress > leftFootStart && progress < leftFootEnd )
				return Utils.Map( progress, leftFootStart, leftFootEnd, 0f, 1f, easingType );
			else if ( progress > rightFootStart && progress < rightFootEnd )
				return Utils.Map( progress, rightFootStart, rightFootEnd, 0f, 1f, easingType );
		}

		return 0f;
	}

	protected override float GetAnimSpeedFactor()
	{
		if ( HasTarget && TargetUnit.IsValid() )
		{
			float distSqr = (TargetUnit.Position2D - Position2D).LengthSquared;
			float attackDistSqr = MathF.Pow( AggroRange, 2f );

			return _personalSpeedScale * Utils.Map( distSqr, attackDistSqr, 0f, 1f, 1.5f, EasingType.Linear );
		}

		return _personalSpeedScale;
	}

	protected override void StartDying( Vector2 dir, float force, Player player, DamageType damageType )
	{
		IsDying = true;
		IsSpawning = false;

		if ( IsMiniboss )
			StartMinibossDeathWatchdog( "exploder", player, damageType );

		Velocity *= 0.5f;

		//if ( player.IsValid() && player.CombustionActive )
		//	Combust( player );
		//else
			StartExplodingRpc( _personalExplodeTime, _personalExplosionRadius, _personalExplosionDamage, player );
	}

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

		int numLavaBlobs = GetNumLavaBlobs();
		for(int i = 0; i < numLavaBlobs; i++ )
		{
			if ( Manager.Instance.GetLavaBlobEndPos( Position2D, out Vector2 endPos ) )
				Manager.Instance.SpawnLavaBlob( Position2D, endPos, puddleRadius: Game.Random.Float( 170f, 250f ), enemySource: this, enemyType: EnemyType );
		}
	}

	public virtual int GetNumLavaBlobs()
	{
		var numBlobs = Utils.Select( Manager.Instance.Difficulty, 0, 1, Game.Random.Int( 1, 2 ) );
		numBlobs += GetNumExtraLavaBlobs();

		return numBlobs;
	}

	protected virtual int GetNumExtraLavaBlobs()
	{
		var numExtraBlobs = 0;

		if ( Manager.Instance.ElapsedTime > 17f * 60f ) numExtraBlobs += Game.Random.Int( 0, 1 );
		if ( Manager.Instance.ElapsedTime > 24f * 60f ) numExtraBlobs += Game.Random.Int( 0, 1 );
		if ( Manager.Instance.ElapsedTime > 32f * 60f ) numExtraBlobs += Game.Random.Int( 0, 1 );
		if ( Manager.Instance.ElapsedTime > 42f * 60f ) numExtraBlobs += 1;
		if ( Manager.Instance.ElapsedTime > 55f * 60f ) numExtraBlobs += 1;
		if ( Manager.Instance.ElapsedTime > 60f * 60f ) numExtraBlobs += 1;
		if ( Manager.Instance.ElapsedTime > 70f * 60f ) numExtraBlobs += 1;
		if ( Manager.Instance.ElapsedTime > 80f * 60f ) numExtraBlobs += 1;

		return numExtraBlobs;
	}
}