Game UI/feedback helper that ramps visual and audio tension when fish are dying from salinity. It tracks peak sickness progress among active tank fish, computes shake, flash, stage labels, announces stage changes via TankSim and GameAudio, and exposes read-only properties plus a version counter.
namespace NoChillquarium;
/// <summary>
/// Ramps tension while any visible fish is dying of salinity:
/// shake, flash, and stage labels so you feel the clock.
/// </summary>
public static class DeathFeel
{
static float _time;
static float _shakeX;
static float _shakeY;
static float _flash;
static int _lastStageAnnounced = -1;
static int _version;
public static int Version => _version;
/// <summary>0–1 worst sickness among fish in the active tank (meth-safe fish ignored).</summary>
public static float PeakProgress { get; private set; }
public static int PeakStage => ProgressToStage( PeakProgress );
public static float ShakeX => _shakeX;
public static float ShakeY => _shakeY;
public static float Flash => _flash;
public static string StageLabel => PeakStage switch
{
0 => "",
1 => "SOMETHING'S OFF…",
2 => "STRUGGLING",
3 => "CRITICAL",
_ => "LAST GASP"
};
public static void Clear()
{
PeakProgress = 0f;
_shakeX = 0f;
_shakeY = 0f;
_flash = 0f;
_lastStageAnnounced = -1;
_time = 0f;
_version++;
}
public static void Tick( float dt )
{
if ( dt <= 0f )
return;
if ( dt > 0.05f )
dt = 0.05f;
_time += dt;
// Peak dying fish in the tank you're watching.
var peak = 0f;
if ( TankSim.IsActive )
{
foreach ( var fish in TankSim.Fish )
{
if ( !fish.IsSick )
continue;
peak = MathF.Max( peak, fish.SickProgress );
}
}
var prevStage = ProgressToStage( PeakProgress );
PeakProgress = peak;
var stage = ProgressToStage( peak );
if ( peak <= 0.001f )
{
_shakeX *= MathF.Max( 0f, 1f - dt * 8f );
_shakeY *= MathF.Max( 0f, 1f - dt * 8f );
_flash *= MathF.Max( 0f, 1f - dt * 6f );
if ( _lastStageAnnounced != 0 || prevStage != 0 )
{
_lastStageAnnounced = 0;
_version++;
// Hand music back to chill / chaos once the crisis ends.
GameAudio.OnDeathStageChanged( 0 );
}
return;
}
// Stage banners + music gear shifts once when crossing thresholds.
if ( stage > _lastStageAnnounced )
{
_lastStageAnnounced = stage;
var msg = stage switch
{
1 => "Wrong water — fish struggling",
2 => "Gasping…",
3 => "Critical — meth or lose them",
_ => "Last gasp — meth or mourn"
};
TankSim.ShowBanner( msg );
if ( stage >= 3 )
GameAudio.PlayUi( GameAudio.Error );
GameAudio.OnDeathStageChanged( stage );
_version++;
}
else if ( stage != prevStage )
{
// Stage dropped (meth save, etc.) — step music back down.
_lastStageAnnounced = stage;
GameAudio.OnDeathStageChanged( stage );
_version++;
}
// Shake intensity ramps hard near death (softened by Reduce Strobe).
var shakeAmp = peak * peak * 5.5f * GameSettings.ShakeScale;
var shakeHz = GameSettings.ReduceStrobe
? 1.2f + peak * 3f
: 3f + peak * 14f;
_shakeX = MathF.Sin( _time * shakeHz * MathF.PI * 2f ) * shakeAmp;
_shakeY = MathF.Cos( _time * shakeHz * 1.7f * MathF.PI * 2f ) * shakeAmp * 0.65f;
// Flash: slow pulse early, frantic red blink late — or steady dim with Reduce Strobe.
if ( GameSettings.ReduceStrobe )
{
_flash = Math.Clamp( peak * peak * 0.18f, 0f, 0.2f );
}
else
{
var flashHz = 0.6f + peak * peak * 8f;
var pulse = 0.5f + 0.5f * MathF.Sin( _time * flashHz * MathF.PI * 2f );
_flash = peak * peak * (0.15f + 0.85f * pulse);
}
_version++;
}
public static int ProgressToStage( float progress )
{
if ( progress < 0.08f ) return 0;
if ( progress < 0.30f ) return 1;
if ( progress < 0.55f ) return 2;
if ( progress < 0.78f ) return 3;
return 4;
}
}