things/enemies/MinibossExploder.cs

Enemy subclass for a miniboss that throws fireballs and explodes, it configures stats, handles state machine for preparing/shooting fireballs, spawns projectile prefabs, and creates an explosion and ground fire on death.

NetworkingFile Access
using System;
using System.Collections.Generic;
using Sandbox;

public class MinibossExploder : Exploder
{
	public override EnemyType EnemyType => EnemyType.MinibossExploder;
	public override string GibFolder => "miniboss_exploder";
	public override float OverrideGibChance => 1f;
	public override int ExtraDeathBloodSprayAmount => 25;
	protected override float MinibossHealthScale => 1.25f;
	public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;

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

	public override bool ShowHealthbar => true;
	public override float HealthbarOffset => 105f;
	public override float HealthbarOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
	public override float HealthbarArmorOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
	public override bool IsBoss => true;
	public override bool IsMiniboss => true;

	public override bool CanAttack => base.CanAttack && State == MinibossExploderState.Default;
	public override bool CanAccelerate => State == MinibossExploderState.Default;

	protected float _shootDelayTimer;
	protected float _shootDelayMin;
	protected float _shootDelayMax;

	protected float _shootRange;

	public override float ParticleYPosOverride => 0.7f;
	public override float StunParticleYPosOverride => 1.1f;

	protected enum MinibossExploderState
	{
		Default,
		ShootPrepare,
		Shoot,
		ShootFinish,
	}

	protected MinibossExploderState State { get; private set; } = MinibossExploderState.Default;

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

		CoinValueMin = 10;
		CoinValueMax = 18;
		CoinChance = 1f;

		PushStrength = 9000f;
		Weight = 1.8f;

		_personalSpeedScale = 1f;
		_personalSpeedFreq = Game.Random.Float( 6f, 7f );

		_personalExplosionRadius = 150f;
		_personalExplodeTime = 2f;

		if ( IsProxy )
			return;

		DetectTargetRange = 900f;
		LoseTargetRange = 1200f;
		LoseTargetTime = 6f;
		MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 12f, 14f, 15f );
		DamageTargetDelay = 0.7f;
		_personalTurnSpeed = 4f;
		Acceleration = 160f;
		AccelerationAttacking = 170f;
		Deceleration = 1.25f;
		DecelerationAttacking = 0.8f;

		_shootDelayMin = 2f;
		_shootDelayMax = 9f;
		_shootRange = 650f;
		_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax );
	}

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

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"{State}\nVelocity: {Velocity}\nGetMoveSpeedFactor(): {GetMoveSpeedFactor()}", new global::Transform( WorldPosition ) );

		if ( IsProxy )
			return;

		if ( IsExploding || Manager.Instance.IsGameOver )
			return;

		if ( !IsStunned && !IsDying )
			HandleState();
	}

	protected override float GetMoveSpeedFactor()
	{
		return 1f;
	}

	protected void HandleState()
	{
		switch ( State )
		{
			case MinibossExploderState.Default:
				if ( TargetUnit.IsValid() && !IsInTheAir )
				{
					var targetDistSqr = (TargetUnit.Position2D - Position2D).LengthSquared;

					if ( targetDistSqr < MathF.Pow( _shootRange, 2f ) )
					{
						_shootDelayTimer -= Time.Delta * TimeScale;
						if ( _shootDelayTimer < 0f && targetDistSqr < MathF.Pow( _shootRange * 0.95f, 2f ) )
							SetState( MinibossExploderState.ShootPrepare );
					}
				}

				break;
			case MinibossExploderState.ShootPrepare:
				if ( _timeSinceChangeState > 0.7f )
					SetState( MinibossExploderState.Shoot );

				break;
			case MinibossExploderState.Shoot:
				if ( _timeSinceChangeState > 0.7f )
					SetState( MinibossExploderState.ShootFinish );
				break;
		}
	}

	protected void SetState( MinibossExploderState state )
	{
		State = state;
		_timeSinceChangeState = 0f;

		switch ( state )
		{
			case MinibossExploderState.Default:
				EnterDefaultStateRpc();

				break;
			case MinibossExploderState.ShootPrepare:
				StartShootingRpc();

				_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax ) * Utils.Map( HpPercent, 1f, 0f, 1f, 0.6f, EasingType.SineIn );

				break;
			case MinibossExploderState.Shoot:
				ShootRpc();

				var dir = (Vector2)WorldRotation.Forward;

				var pos = Position2D + dir * 20f;
				var targetPos = TargetUnit.IsValid()
					? (Position2D + (TargetUnit.Position2D - Position2D).Normal * MathF.Min( (TargetUnit.Position2D - Position2D).Length, 350f ) + TargetUnit.Velocity * Game.Random.Float( 0f, 2.2f ) + Utils.GetRandomVector() * Game.Random.Float( 30f, 60f ))
					: Position2D + Utils.GetRandomVector() * Game.Random.Float( 100f, 300f );

				targetPos = Manager.Instance.ClampPosToBounds( targetPos );

				SpawnFireball( pos, targetPos );

				break;
			case MinibossExploderState.ShootFinish:
				//Velocity = Vector2.Zero;

				SetState( MinibossExploderState.Default );

				break;
		}
	}

	[Rpc.Broadcast]
	protected void StartShootingRpc()
	{
		CanAnimate = false;

		PlayShootAnim();

		// todo: new sfx
		Manager.Instance.PlaySfxNearby( "spitter.prepare", Position2D, pitch: Game.Random.Float( 0.9f, 0.95f ), volume: 0.6f, maxDist: 400f );
	}

	protected virtual void PlayShootAnim()
	{
		SetAnim( "Attack" );
		SetPlaybackRate( 0.3f );
		CanAnimate = false;
	}

	[Rpc.Broadcast]
	protected void ShootRpc()
	{
		// todo: new sfx
		Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 0.9f, 0.95f ), volume: 0.85f, maxDist: 450f );
	}

	void SpawnFireball( Vector2 pos, Vector2 targetPos )
	{
		SpawnFireballWarning( targetPos );

		var zPos = 80f;
		var fireballGo = GameObject.Clone( "prefabs/enemyProjectiles/tossed_fireball.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ) ) } );
		var fireball = fireballGo.Components.Get<TossedFireball>( true );

		fireball.Shooter = this;
		fireball.Damage = Utils.Select( Manager.Instance.Difficulty, 9f, 10f, 13f );
		fireball.Lifetime = Game.Random.Float( 1.7f, 2.5f );
		fireball.StartPos2D = pos;
		fireball.TargetPos2D = Manager.Instance.ClampPosToBounds( targetPos );

		fireballGo.NetworkSpawn();
	}

	[Rpc.Broadcast]
	public void SpawnFireballWarning( Vector2 targetPos )
	{
		GameObject.Clone( "prefabs/effects/warning_fireball.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( targetPos.x, targetPos.y, 0f ) ) } );
	}

	public override void Explode()
	{
		var repelRadius = _explosionRadius * 1.35f;
		var force = 900f;
		Manager.Instance.CreateExplosionRpc( Position2D, _explosionRadius, _explosionDamage, repelRadius, force, playerSource: _playerWhoKilledUs, enemySource: this, enemyType: this.EnemyType, new Color(1f, 0f, 0.75f), options: RepelOptions.RepelPlayers | RepelOptions.DamagePlayers | RepelOptions.RepelEnemies | RepelOptions.RepelItems );

		for ( int i = 0; i < Game.Random.Int( 2, 4 ); i++ )
		{
			var dir = Utils.GetRandomVector();
			var pos = Position2D + dir * Game.Random.Float( 10f, 20f );
			var targetPos = pos + dir * Game.Random.Float( 100f, 500f );
			SpawnFireball( pos, targetPos );
		}

		DieRpc( dir: Vector2.Zero, force: 4f, player: _playerWhoKilledUs, damageType: DamageType.Explosion );
	}

	public override void Die( Vector2 dir, float force, Player player, DamageType damageType )
	{
		base.Die( dir, force, player, damageType );

		Manager.Instance.SpawnFireGroundRpc( Position2D, player: null, enemySource: null, enemyType: EnemyType.MinibossExploder, damage: 8f, lifetime: Game.Random.Float( 12f, 15f ), spreadChance: 0.5f, canStack: false, scale: 2.5f, colorA: Color.Magenta, colorB: Color.Red, hurtPlayers: true, hurtEnemies: false );
	}

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

		PlayFlinchAnim();

		SetState( MinibossExploderState.Default );
	}

	[Rpc.Broadcast]
	public void EnterDefaultStateRpc()
	{
		CanAnimate = true;
		PlayWalkAnim();
	}
}