things/enemies/MinibossSpitter.cs

Enemy subclass for a spitter miniboss. It configures stats, behaviors, projectiles, spawning visuals, shooting patterns, blinking teleport, and death effects specific to the miniboss variant of Spitter.

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

public class MinibossSpitter : Spitter
{
	public override EnemyType EnemyType => EnemyType.MinibossSpitter;
	public override string GibFolder => "miniboss_spitter";
	public override float OverrideGibChance => 1f;
	public override int ExtraDeathBloodSprayAmount => 25;
	protected override float MinibossHealthScale => 1.1f;
	public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;

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

	public override bool ShowHealthbar => true;
	public override float HealthbarOffset => 90f;
	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;

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

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

		PushStrength = 6000f;
		Weight = 1.4f;

		_personalSpeedScale = 1f;
		_personalSpeedFreq = Game.Random.Float( 9f, 11f );

		if ( IsProxy )
			return;

		DetectTargetRange = 900f;
		LoseTargetRange = 1400f;
		LoseTargetTime = 6f;
		MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 9f, 12f, 13f );
		DamageTargetDelay = 0.6f;
		_personalTurnSpeed = Game.Random.Float( 5f, 7f );
		Acceleration = 250f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
		AccelerationAttacking = 285f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
		Deceleration = 2f;
		DecelerationAttacking = 1.8f;

		_shootDelayMin = 3f;
		_shootDelayMax = 7f;
		_shootRange = 500f;
		_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax );
		_prepareShootTime = 0.9f;
		_shootTime = 0.6f;

		_blinkDelayMin = 4f;
		_blinkDelayMax = 14f;
		_blinkPrepareDelay = 0.45f;
		_blinkRange = 1200f;
		_blinkDelayTimer = Game.Random.Float( _blinkDelayMin, _blinkDelayMax );

		_personalRetreatRange = Game.Random.Float( 185f, 250f );
		_personalStopRetreatRange = Game.Random.Float( 300f, 370f );
	}

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


	}

	protected override Vector2 GetTargetOffset()
	{
		var dist = (TargetUnit.Position2D - Position2D).Length;

		var offset = TargetUnit.Velocity * (0.5f + Utils.FastSin( TimeSinceSpawn * 2.6f ) * 0.5f) * dist * 0.012f;

		// don't lead shots if target is very close and heading toward us		
		if ( dist < 110f )
		{
			var dot = Vector2.Dot( (Position2D - TargetUnit.Position2D).Normal, TargetUnit.Velocity.Normal );
			if ( dot > 0.65f )
				offset = Vector2.Zero;
		}

		if ( State == SpitterState.Default && !_isRetreating )
			offset += new Vector2( Utils.FastSin( TimeSinceSpawn * 0.73f ), Utils.FastSin( TimeSinceSpawn * 0.64f ) ) * 85f;

		return offset;
	}
	protected override void StartShooting()
	{
		base.StartShooting();

		//SetPlaybackRate( 1.43f );
		SetPlaybackRate( 1f );
	}

	protected override void Shoot()
	{
		Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 1.4f, 1.5f ), volume: 0.85f, maxDist: 450f );

		if ( IsProxy )
			return;

		var dir = (Vector2)WorldRotation.Forward;

		var numBullets = MathX.FloorToInt( Utils.Map( HpPercent, 1f, 0f, 1f, Utils.Select( Manager.Instance.Difficulty, 2f, 3f, 4f ), EasingType.SineIn ) ) + Game.Random.Int( 0, Utils.Select( Manager.Instance.Difficulty, 1, 2, 3 ) );
		var spread = Game.Random.Float( 30f, Utils.Map( numBullets, 1, 7, 70f, 140f) );

		float currAngleOffset = -spread * 0.5f;
		float increment = numBullets > 1 ? (spread / (float)(numBullets - 1)) : 0f;

		var minutes = Manager.Instance.ElapsedTime / 60f;
		var projectileType = Manager.Instance.GetRandomEnemyProjectileType( minutes * Utils.Select( Manager.Instance.Difficulty, 0.8f, 1f, 1.25f ) );

		// todo: less numProjectiles when Cursed type

		for ( int i = 0; i < numBullets; i++ )
		{
			var currDir = Utils.RotateVector( dir, currAngleOffset + increment * i );
			bool isHoming = Game.Random.Float( 0f, 1f ) < 0.35f;

			if ( isHoming )
			{
				var pos = Position2D + dir * 40f + currDir * 10f + Utils.GetRandomVector() * Game.Random.Float( 0f, 4f );
				Manager.Instance.SpawnEnemyHomingProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: Game.Random.Float( 50f, 70f ), projectileType, lifetimeModifier: Game.Random.Float(1f, 1.4f) );
			}
			else
			{
				var pos = Position2D + dir * 40f + currDir * 10f;
				Manager.Instance.SpawnEnemyProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: 150f, projectileType, lifetimeModifier: Game.Random.Float( 1f, 1.3f ) );
			}
		}
	}

	protected override void Damage( float damage, Player player, DamageType damageType, Vector3 hitPos, Vector2 force, bool isCrit = false, bool shouldFlinch = true, DamageResultFlags damageFlags = DamageResultFlags.None )
	{
		base.Damage( damage, player, damageType, hitPos, force, isCrit, shouldFlinch, damageFlags );

		if ( IsProxy || IsDying )
			return;

		if ( State == SpitterState.Default && _timeSinceBlinking > Game.Random.Float( 4f, 8f ) && Game.Random.Float( 0f, 1f ) < Utils.Map( HpPercent, 1f, 0f, 0f, 1f ) )
			SetState( SpitterState.BlinkPrepare );
	}

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

		Manager.Instance.PlaySfxNearbyRpc( "puddle_splat", Position2D, pitch: Game.Random.Float( 1.05f, 1.1f ), volume: 1.2f, maxDist: 350f );

		var damage = Utils.Select( Manager.Instance.Difficulty, 3f, 5f, 7f );
		Manager.Instance.SpawnAcidPuddle( Position2D, lifetime: Game.Random.Float( 24f, 30f ), damage, scale: Game.Random.Float( 1.35f, 1.8f ), new Color( 0.5f, 0.2f, 0.3f ), new Color( 0.5f, 0.3f, 0.4f ), playerSource: null, enemySource: this, enemyType: this.EnemyType );
	}

	protected override Vector2 GetBlinkTargetPos()
	{
		Vector2 blinkPos = TargetUnit.IsValid()
			? TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 1.5f ) + Utils.GetRandomVector() * Game.Random.Float( 200f, 440f )
			: Position2D + Utils.GetRandomVector() * Game.Random.Float( 200f, 500f );

		return Manager.Instance.ClampPosToBounds( blinkPos );
	}

	protected override void Jump( Vector2 targetPos, float height, float lifetime )
	{
		SetState( SpitterState.Default );

		base.Jump( targetPos, height, lifetime );
	}
}