things/enemies/Boss.Shooting.cs

Partial Boss class that implements the boss enemy shooting behavior. It initializes shooting timers and range, checks distance to target and readiness to start a shoot prepare state, and contains RPCs that play animations, sound effects and spawn either homing or straight projectiles with spread based on boss HP and difficulty.

Networking
using System;
using Sandbox;
using Sandbox.Citizen;

public partial class Boss
{
	protected float _shootDelayTimer;
	protected float _shootDelayMin;
	protected float _shootDelayMax;

	protected float _shootRange;

	void InitShooting()
	{
		if ( IsProxy )
			return;

		_shootDelayMin = 3.5f;
		_shootDelayMax = 8.5f;
		_shootRange = 500f;

		_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax ) * Utils.Select( Manager.Instance.Difficulty, 1.3f, 1f, 0.925f );
	}

	void HandleShooting()
	{
		if ( !IsReadyForAction || !TargetUnit.IsValid() )
			return;

		var targetDistSqr = (TargetUnit.Position2D - Position2D).LengthSquared;

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

	[Rpc.Broadcast]
	protected void StartShootingRpc()
	{
		SetAnim( "ShootPrepare" );
		//AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.Pistol;

		Manager.Instance.PlaySfxNearby( "boss.prepare", Position2D, pitch: Game.Random.Float( 0.75f, 0.85f ), volume: 1.4f, maxDist: 500f );
	}

	[Rpc.Broadcast]
	protected void ShootRpc()
	{
		SetAnim( "Shoot" );
		//AnimationHelper.Target.Set( "b_attack", true );

		if ( IsProxy )
			return;

		var dir = (Vector2)WorldRotation.Forward;

		var homingChance = Utils.Map( HpPercent, 1f, 0f, 0f, 0.6f, EasingType.Linear );
		if ( Game.Random.Float( 0f, 1f ) < homingChance )
		{
			var numBullets = MathX.FloorToInt( Utils.Map( HpPercent, 0.85f, 0f, 1f, 6f ) );
			var spread = 360f;

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

			for ( int i = 0; i < numBullets; i++ )
			{
				var currDir = Utils.RotateVector( dir, currAngleOffset + increment * i );
				var pos = Position2D + currDir * 20f;

				// todo: different types on harder difficulties
				EnemyProjectileType projectileType = EnemyProjectileType.Normal;
				Manager.Instance.SpawnEnemyHomingProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: Game.Random.Float( 50f, 90f ) * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.25f ), projectileType );
			}

			//Manager.Instance.PlaySfxNearbyRpc( "boss.shoot", Position2D, pitch: Game.Random.Float( 0.35f, 0.4f ), volume: 1.3f, maxDist: 600f );
			Manager.Instance.PlaySfxNearbyRpc( "squelch", Position2D, pitch: Game.Random.Float( 1f, 1.1f ), volume: 2.4f, maxDist: 600f );
		}
		else
		{
			var numBullets = MathX.FloorToInt( Utils.Map( HpPercent, 1f, 0f, 3f, 8f, EasingType.SineIn ) ) + Game.Random.Int( 0, 1 );
			var spread = Game.Random.Float( 30f, 80f );

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

			for ( int i = 0; i < numBullets; i++ )
			{
				var currDir = Utils.RotateVector( dir, currAngleOffset + increment * i );

				var pos = Position2D + currDir * 20f;
				var vel = Utils.Select( Manager.Instance.Difficulty, 200f, 220f, 260f );

				// todo: different types on harder difficulties
				EnemyProjectileType projectileType = EnemyProjectileType.Normal;
				Manager.Instance.SpawnEnemyProjectile( pos, currDir, shooter: this, enemyType: this.EnemyType, startVel: vel, projectileType );
			}

			Manager.Instance.PlaySfxNearbyRpc( "squelch", Position2D, pitch: Game.Random.Float( 1.2f, 1.3f ), volume: 2.4f, maxDist: 600f );
		}
	}
}