Entities/Blocks/BlockDragon.cs

BlockDragon entity that handles a dragon block's shooting behavior. It starts a windup on high-speed impacts, spawns a fireball aimed at the closest player (bouncing variant in phase 2), plays face/mouth animations and a recover period.

NetworkingFile Access
namespace BlockParty;

/// <summary>
/// Dragon block — on a hard impact (speed > 40) at phase 1 it spits a straight fireball at
/// the player; at phase 2 a faster bouncing one. Brief "shooting" face during the shot.
/// Port of the original <c>BlockDragon</c> (coroutine → sim-timer windup/recover).
/// </summary>
public sealed class BlockDragon : Block
{
	enum ShootState { Idle, Windup, Recover }
	ShootState _shootState = ShootState.Idle;
	float _shootTimer;
	bool _shootBouncing;
	protected override bool CanActivateTimedAbility => _shootState == ShootState.Idle;

	const float SHOOT_WINDUP = 0.35f;
	const float SHOOT_RECOVER = 0.65f;

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

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

	void StartShoot( bool bouncing )
	{
		_shootBouncing = bouncing;
		_shootState = ShootState.Windup;
		_shootTimer = SHOOT_WINDUP;
		FaceBlinkAnimOverride = $"face_shooting_{Phase}"; // the palette blink keeps flashing the shooting face
		_face.PlayAnimation( $"face_shooting_{Phase}" );
		_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 ( !TrySpawnFireball( _shootBouncing ) )
			{
				FinishShoot();
				return;
			}

			Audio.PlaySfx( SfxType.FireballShoot, Position );
			_shootState = ShootState.Recover;
			_shootTimer = SHOOT_RECOVER;
		}
		else // Recover
		{
			FinishShoot();
		}
	}

	void FinishShoot()
	{
		FaceBlinkAnimOverride = null; // hand the blink back to the regular face
		RestoreFaceAndMouth();
		_shootState = ShootState.Idle;
	}

	bool TrySpawnFireball( bool bouncing )
	{
		var player = Stage.ClosestTargetablePlayer( Position );
		if ( player is null ) return false;

		float speed = bouncing ? 33.0f : 15.0f;
		Vector2 vel = Utils.Normalized( player.Position - Position ) * speed;
		Stage.AddFireball( Position + new Vector2( 0f, -8f ), vel, bouncing );
		return true;
	}
}