things/enemies/MinibossExploder.cs

An enemy subclass for a miniboss type that extends Exploder. It configures stats, state machine for shooting fireball projectiles, spawning warnings and fireballs, explosion behavior on death, animations and RPCs for networked events.

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