Entities/Blocks/BlockWind.cs

A LaneBlock-derived wind block. It controls mouth animation based on EyesOpenAmount and Phase, supplies outward force direction, particle behavior, overlay tint, and forwards player impulse to AddWindImpulse.

namespace BlockParty;

/// <summary>
/// Wind block — while its eyes are open (phase 1+) it blows a block-width lane of gusting wind in the
/// direction it faces, pushing the player and projectiles (fireballs, teardrops) OUTWARD but NOT blocks
/// or lasers. Phase 1 blows one lane in <see cref="Block.MoveDirection"/>; phase 2 blows four lanes
/// outward in every cardinal direction at once.
///
/// All of the lane geometry, per-column blocking, force application, particle streaming and overlay
/// rendering live in <see cref="LaneBlock"/>; wind just supplies the direction (outward), the player's
/// wind channel, dust particles that fly straight out and die at the blocker, and a faint white tint.
/// </summary>
public sealed class BlockWind : LaneBlock
{
	// Alpha at full strength (eyes fully open).
	const float OVERLAY_ALPHA = 0.2f;

	// Tracks the mouth animation currently playing so we only re-issue PlayAnimation on a change.
	string _mouthAnim;

	public override void Tick( float dt )
	{
		base.Tick( dt );
		UpdateMouth();
	}

	/// <summary>Blow the mouth open while the gust is active and close it as the block slams to a stop /
	/// while parked, so a closed mouth reads as "no wind". The open-mouth frame follows
	/// <see cref="Block.EyesOpenAmount"/> (the same ramp that drives the gust strength), so the mouth
	/// closes then re-opens across each slam→resume. Never opens in phase 0 (no gust there).</summary>
	void UpdateMouth()
	{
		if ( IsDead ) return; // the death sequence owns the mouth (mouth_dead)

		int level = Phase >= 1 ? Math.Clamp( (int)MathF.Round( EyesOpenAmount * 3f ), 0, 3 ) : 0;
		string want = level > 0 ? $"mouth_open_{Phase}_{level}" : $"mouth_{Phase}";
		if ( want == _mouthAnim ) return;

		_mouth.PlayAnimation( want );
		_mouthAnim = want;
	}

	protected override Direction GetPhase1Direction() => MoveDirection;

	protected override Vector2 GetForceDirection( in Lane lane ) => lane.D; // blow outward

	protected override void ApplyPlayerForce( Player player, Vector2 impulse ) => player.AddWindImpulse( impulse );

	protected override ParticleKind ParticleKind => ParticleKind.Dust;

	// Wind particles fly OUTWARD, so they travel the remaining reach past their spawn point.
	protected override float GetParticleTravel( float reach, float along ) => reach - along;

	protected override Color GetOverlayColor( float strength ) => new Color( 1f, 1f, 1f, OVERLAY_ALPHA * strength );
}