Entities/Blocks/BlockSad.cs

A Block entity called BlockSad that periodically spawns teardrop projectiles from one of its eyes when in phase 1 or 2. It tracks a timer, alternates which eye emits the next tear, computes spawn position and velocity, adds a teardrop to the Stage and plays a sound. It also respects an enclosed-eye loop that can pause crying.

NetworkingFile Access
namespace BlockParty;

/// <summary>
/// Sad block — at phase 1+ it periodically "cries" a teardrop from one of its eyes (every
/// 1.0s at phase 1, 0.33s at phase 2). Teardrops fall and kill the player. Port of the
/// original BlockSad. (Impact uses the default block behaviour.)
/// </summary>
public class BlockSad : Block
{
	static readonly Vector2 EYE_LEFT = new Vector2( -8f, -2f );
	static readonly Vector2 EYE_RIGHT = new Vector2( 8f, -2f );
	const float CRY_TIME_PHASE_1 = 1.0f;
	const float CRY_TIME_PHASE_2 = 0.33f;

	float _cryTimer;
	bool? _cryFromLeft;
	protected virtual TeardropVariant CryVariant => TeardropVariant.Standard;

	/// <summary>While boxed in at phase 1+, the authored ability timings loop the crying on/off:
	/// tears at the native cadence while the eyes stay open (Interval), silence while they're shut
	/// (ClosedTime). ClosedTime 0 = permanently crying. Non-enclosed sads are unchanged (the gate
	/// below requires enclosure, so ordinary impact blinks never pause the tears).</summary>
	protected override bool HasEnclosedEyeLoop => true;

	public override void Tick( float dt )
	{
		base.Tick( dt );
		if ( IsDead ) return;

		// The enclosed loop's off-leg: shut (or shutting) eyes can't cry. The countdown pauses
		// rather than running dry, so the first tear after a re-open keeps the cadence.
		if ( EnclosedEyeLoopRunning && EyesOpenAmount < 1f ) return;

		if ( Phase == 1 || Phase == 2 )
		{
			_cryTimer -= dt;
			if ( _cryTimer <= 0f )
			{
				Cry();
				_cryTimer = Phase == 1 ? CRY_TIME_PHASE_1 : CRY_TIME_PHASE_2;
			}
		}
	}

	void Cry()
	{
		_cryFromLeft ??= Rng.Int( 0, 2 ) == 0;
		Vector2 pos = Position + (_cryFromLeft.Value ? EYE_LEFT : EYE_RIGHT);
		_cryFromLeft = !_cryFromLeft.Value;
		Vector2 vel = new Vector2( VelX * Rng.Float( 0f, 0.33f ), (VelY < 0) ? VelY * 0.6f : Rng.Float( 0f, 0.2f ) );
		Stage.AddTeardrop( pos, vel, this, CryVariant );
		Audio.PlaySfx( SfxType.TearForm, Position, Rng.Float( 0.40f, 0.48f ) );
	}

	protected override void NextPhase()
	{
		base.NextPhase();
		_cryTimer = Phase == 1 ? CRY_TIME_PHASE_1 : CRY_TIME_PHASE_2;
	}
}