Entities/Blocks/BlockBullet.cs

A Block subclass that implements a marksman-style bullet attack. It starts a windup when the block is moving fast and in phase 1 or 2, spawns a led projectile at windup completion, plays an animation and sound, then goes into a recovery period.

namespace BlockParty;

/// <summary>
/// Dragon-derived marksman block. Hard impacts wind up aimed shots which pass through authored
/// obstacles and arena walls; phase 1 is slower and phase 2 uses the original fast bullet.
/// </summary>
public class BlockBullet : Block
{
	enum ShootState { Idle, Windup, Recover }

	private const float SHOOT_WINDUP = 0.35f;
	private const float SHOOT_RECOVER = 0.65f;
	private ShootState _shootState;
	private float _shootTimer;
	private BlockBulletVariant _shootVariant;
	protected override bool CanActivateTimedAbility => _shootState == ShootState.Idle;
	protected virtual BlockBulletVariant ProjectileVariantForPhase( int phase )
		=> phase == 1 ? BlockBulletVariant.BulletPhase1 : BlockBulletVariant.BulletPhase2;

	public override void Impact( Block otherBlock )
	{
		base.Impact( otherBlock );
		if ( IsDead ) return;

		if ( _moveSpeed > 40f && (Phase == 1 || Phase == 2) && _shootState == ShootState.Idle )
			StartShoot();
	}

	private void StartShoot()
	{
		_shootVariant = ProjectileVariantForPhase( Phase );
		_shootState = ShootState.Windup;
		_shootTimer = SHOOT_WINDUP;
		_mouth.PlayAnimation( $"mouth_shooting_{Phase}" );
	}

	public override void Tick( float dt )
	{
		base.Tick( dt );
		if ( IsDead || _shootState == ShootState.Idle ) return;

		_shootTimer -= dt;
		if ( _shootTimer > 0f ) return;

		if ( _shootState == ShootState.Windup )
		{
			if ( !TrySpawnBullet() )
			{
				FinishShoot();
				return;
			}

			Audio.PlaySfx( SfxType.FireballShoot, Position, 0.9f, 1.15f );
			_shootState = ShootState.Recover;
			_shootTimer = SHOOT_RECOVER;
			return;
		}

		FinishShoot();
	}

	private void FinishShoot()
	{
		RestoreFaceAndMouth();
		_shootState = ShootState.Idle;
	}

	private bool TrySpawnBullet()
	{
		Player player = Stage.ClosestTargetablePlayer( Position );
		if ( player is null ) return false;

		Vector2 spawnPosition = Position + new Vector2( 0f, -Height * 0.2f );
		Vector2 direction = BlockBulletProjectile.LeadDirection(
			spawnPosition, player, BlockBulletProjectile.FullSpeed( _shootVariant ) );
		Stage.AddBlockBullet( spawnPosition, direction, _shootVariant );
		return true;
	}
}