things/enemies/MinibossSniper.cs

Enemy AI class MinibossSniper, derived from Spitter. It configures miniboss stats, movement, shooting and blink teleport behavior, handles damage and death effects, and spawns projectile attacks.

Networking
using System;
using Sandbox;

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

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

	public override bool ShowHealthbar => true;
	public override float HealthbarOffset => 95f;
	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 int _numTimesShot;
	protected int _numShotsTotal;
	//private float _currShootDelay;

	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 = 3000f;
		LoseTargetRange = 1400f;
		LoseTargetTime = 6f;
		MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 11f, 12f, 13f );
		DamageTargetDelay = 0.6f;
		_personalTurnSpeed = Game.Random.Float( 12f, 12f );
		Acceleration = 210f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
		AccelerationAttacking = 225f * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
		Deceleration = 2f;
		DecelerationAttacking = 1.8f;

		_shootDelayMin = Utils.Select( Manager.Instance.Difficulty, 6.5f, 4f, 3.5f );
		_shootDelayMax = Utils.Select( Manager.Instance.Difficulty, 14f, 12.5f, 11f );
		_shootRange = 3000f;
		_shootDelayTimer = 1f;
		_prepareShootTime = 0.7f;
		_shootTime = 0.3f;

		_blinkDelayMin = 5f;
		_blinkDelayMax = 13f;
		_blinkPrepareDelay = 0.45f;
		_blinkRange = 950f;
		_blinkDelayTimer = Game.Random.Float( _blinkDelayMin, _blinkDelayMax );

		_personalRetreatRange = Game.Random.Float( 640f, 660f );
		_personalStopRetreatRange = Game.Random.Float( 870f, 890f );
	}

	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 PlayShootAnim()
	//{
	//	SetAnim( Game.Random.Float( 0f, 1f ) < 0.5f ? "HoldItem_RH_Throw_Strong" : "HoldItem_LH_Throw_Strong" );
	//	SetPlaybackRate( 0.5f );
	//}

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

        _numTimesShot = 0;
        _numShotsTotal = 3;
    }

	protected override void SetStateShoot()
	{
		ShootRpc();

		_numTimesShot++;
		//_currShootDelay = Game.Random.Float( 0.1f, 0.5f );
	}

	protected override void HandleStateShoot()
	{
		if ( _numTimesShot >= _numShotsTotal )
		{
			if ( _timeSinceChangeState > 0.6f )
				SetState( SpitterState.ShootFinish );
		}
		else
		{
			if ( _timeSinceChangeState > 0.4f )
				SetState( SpitterState.ShootPrepare );
		}
	}

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

		SetPlaybackRate( 1.35f );
    }
    
    protected override void Shoot()
	{
		Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 1.4f, 1.5f ), volume: 0.65f, maxDist: 850f );

		if ( IsProxy )
			return;

		var dir = (Vector2)WorldRotation.Forward;

		var minutes = Manager.Instance.ElapsedTime / 60f;
		var projectileType = Manager.Instance.GetRandomEnemyProjectileType( minutes );

		var currDir = Utils.RotateVector( dir, Game.Random.Float( -7f, 7f ) );
		var pos = Position2D + currDir * 45f;
		var speed = 290f * Game.Random.Float( 0.95f, 1.05f ) * Utils.Map( HpPercent, 1f, 0f, 1f, 1.5f, EasingType.SineIn );
		Manager.Instance.SpawnEnemyProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: 300f, projectileType, lifetimeModifier: 3f );
	}

	protected override Vector2 GetBlinkTargetPos()
	{
		// todo: blink to a corner far away from target player

		Vector2 blinkPos = GetRandomBlinkPos();

		int numTries = 0;
		while ( numTries < 10 )
		{
			if ( Manager.Instance.IsInBounds( blinkPos ) )
				break;

			blinkPos = GetRandomBlinkPos();
			numTries++;
		}

		return Manager.Instance.ClampPosToBounds( blinkPos );
	}

	Vector2 GetRandomBlinkPos()
	{
		var targetPos = TargetUnit.Position2D + Utils.GetRandomVector() * Game.Random.Float( 500f, 1000f );

		int numTries = 0;
		while ( numTries < 20 )
		{
			if ( (TargetUnit.Position2D - targetPos).LengthSquared > MathF.Pow( Utils.Map( numTries, 0, 20, 800f, 400f ), 2f ) )
				break;

			targetPos = TargetUnit.Position2D + Utils.GetRandomVector() * Game.Random.Float( 500f, 1000f );

			numTries++;
		}

		return targetPos;
	}

	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 );
		Manager.Instance.SpawnAcidPuddle( Position2D, lifetime: Game.Random.Float( 24f, 30f ), damage: 5f, scale: Game.Random.Float( 1.35f, 1.8f ), new Color( 0.5f, 0.3f, 0.6f ), new Color( 0.5f, 0.5f, 0.7f ), playerSource: null, enemySource: this, enemyType: this.EnemyType );
	}

}