DeathFeel.cs

Utility static class that computes visual/audio 'death' effects for fish in a tank. It tracks peak sickness progress among visible fish, produces shake/flash values, stage labels, and triggers UI banners and audio callbacks when stages change.

NetworkingFile Access
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 — Grandma already set a plate",
				2 => "Gasping… she loves you / she's hungry",
				3 => "Critical — meth… or Grandma's skillet",
				_ => "Last gasp — still hugs you after supper"
			};
			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;
	}
}