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.
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;
}
}