MethRush.cs

Minigame controller for a "Meth Rush" timing mini-game. Manages state (rounds, marker, zone, hits/misses, cracks), starts/stops the minigame, advances marker each Tick, handles player hits/misses, applies effects to TankSim fish, updates audio/banners, persists crack count, and allows repairing cracks.

File Access
namespace NoChillquarium;

/// <summary>
/// Minigame when fish hit meth: calm the thrash or the tank cracks.
/// Timing bar — hit CALM! in the green zone. Misses crack the glass.
/// </summary>
public static class MethRush
{
	public const int Rounds = 4;
	public const int CrackCap = 5;
	public const float ZoneMin = 0.38f;
	public const float ZoneMax = 0.58f;

	static readonly Random _rng = new();
	static int _version;
	static bool _open;
	static float _marker;
	static float _markerDir = 1f;
	static float _markerSpeed = 1.35f;
	static int _round;
	static int _hits;
	static int _misses;
	static int _cracks; // 0..CrackCap, persists (saved)
	static string _feedback = "";
	static float _feedbackTimer;
	static float _zoneMin = ZoneMin;
	static float _zoneMax = ZoneMax;

	public static int Version => _version;
	public static bool Open => _open;
	public static float Marker => _marker;
	public static int Round => _round;
	public static int Hits => _hits;
	public static int Misses => _misses;
	public static int Cracks => _cracks;
	public static string Feedback => _feedbackTimer > 0f ? _feedback : "";
	public static float ZoneLeftPct => _zoneMin * 100f;
	public static float ZoneWidthPct => MathF.Max( 4f, ( _zoneMax - _zoneMin ) * 100f );
	public static string RoundLabel => _open ? $"Calm {_round + 1} / {Rounds}" : "Meth Rush";
	public static string CrackLabel =>
		_cracks <= 0 ? "Glass intact" : $"Cracks ×{_cracks}";

	public static void Clear()
	{
		_open = false;
		_marker = 0f;
		_round = 0;
		_hits = 0;
		_misses = 0;
		_feedback = "";
		_feedbackTimer = 0f;
		_version++;
	}

	public static void Load( int cracks )
	{
		_cracks = Math.Clamp( cracks, 0, CrackCap );
		_open = false;
		_version++;
	}

	public static void WriteToSave( SaveData data )
	{
		if ( data is not null )
			data.TankCracks = _cracks;
	}

	/// <summary>Start after a successful meth dose. Blocks other minigames.</summary>
	public static bool TryStart()
	{
		if ( !TankSim.IsActive || TankSim.FishCount == 0 )
			return false;
		if ( _open )
			return true;
		if ( TrainingSystem.TrainOpen || TrainingSystem.InjectOpen )
		{
			// Priority: meth chaos interrupts gym? Force cancel train.
			if ( TrainingSystem.TrainOpen )
				TrainingSystem.CancelTrain();
			if ( TrainingSystem.InjectOpen )
				TrainingSystem.CancelInject();
		}
		if ( BattleSystem.IsOpen && BattleSystem.Phase != BattlePhase.Fighting )
			BattleSystem.Close();

		_open = true;
		_round = 0;
		_hits = 0;
		_misses = 0;
		_feedback = "They're thrashing — hit CALM! in the green!";
		_feedbackTimer = 1.6f;
		SetupRound();
		GameAudio.PlayUi( GameAudio.AlertWarning );
		TankSim.ShowBanner( "METH RUSH — calm the tank or the glass cracks!" );
		_version++;
		return true;
	}

	public static void Tick( float dt )
	{
		if ( dt <= 0f )
			return;
		if ( dt > 0.05f )
			dt = 0.05f;

		if ( _feedbackTimer > 0f )
		{
			_feedbackTimer -= dt;
			_version++;
		}

		if ( !_open )
			return;

		// Faster each round + more fish on meth
		var methCount = 0;
		foreach ( var f in TankSim.Fish )
		{
			if ( f.OnMeth )
				methCount++;
		}
		var baseSpeed = 1.25f + _round * 0.28f + methCount * 0.08f;
		_marker += _markerDir * _markerSpeed * dt;
		if ( _marker >= 1f )
		{
			_marker = 1f;
			_markerDir = -1f;
		}
		else if ( _marker <= 0f )
		{
			_marker = 0f;
			_markerDir = 1f;
		}
		_markerSpeed = baseSpeed + MathF.Sin( Time.Now * 3.1f ) * 0.15f;
		_version++;
	}

	public static bool TryHit()
	{
		if ( !_open )
			return false;

		var inZone = _marker >= _zoneMin && _marker <= _zoneMax;
		if ( inZone )
		{
			_hits++;
			_feedback = _hits >= Rounds ? "LOCKED DOWN!" : "Got 'em!";
			_feedbackTimer = 0.75f;
			GameAudio.PlayUi( GameAudio.Confirm );
			// Slightly calm thrash
			foreach ( var f in TankSim.Fish )
			{
				if ( !f.OnMeth )
					continue;
				f.Vx *= 0.72f;
				f.Vy *= 0.72f;
			}
		}
		else
		{
			_misses++;
			_feedback = "Miss — fins hit the glass!";
			_feedbackTimer = 0.85f;
			GameAudio.PlayUi( GameAudio.Error );
			AddCrack( 1 );
			// Panic burst
			foreach ( var f in TankSim.Fish )
			{
				if ( !f.OnMeth )
					continue;
				var ang = (float)(_rng.NextDouble() * Math.PI * 2);
				f.Vx = MathF.Cos( ang ) * f.Species.Speed * 2.8f;
				f.Vy = MathF.Sin( ang ) * f.Species.Speed * 1.2f;
			}
			Chaos.Add( 2.5f );
		}

		_round++;
		if ( _round >= Rounds )
		{
			Finish();
			return inZone;
		}

		SetupRound();
		_version++;
		return inZone;
	}

	/// <summary>Bail / Esc — counts as a bad contain, cracks glass.</summary>
	public static void Bail()
	{
		if ( !_open )
			return;
		AddCrack( 1 );
		Chaos.Add( 3f );
		_open = false;
		TankSim.ShowBanner( "You let them thrash. " + CrackLabel + "." );
		GameAudio.PlayUi( GameAudio.Error );
		SaveGame.TrySave();
		_version++;
	}

	static void Finish()
	{
		_open = false;
		if ( _misses == 0 )
		{
			TankSim.ShowBanner( "Perfect contain. Glass holds. Barely." );
			Karma.Add( 1.5f );
			GameAudio.PlayUi( GameAudio.Upgrade );
		}
		else if ( _hits >= _misses )
		{
			TankSim.ShowBanner( $"Contained. Hits {_hits} · misses {_misses}. {CrackLabel}." );
			GameAudio.PlayUi( GameAudio.Notice );
		}
		else
		{
			TankSim.ShowBanner( $"Chaos. Hits {_hits} · misses {_misses}. {CrackLabel}." );
			Chaos.Add( 4f );
			GameAudio.PlayUi( GameAudio.Error );
		}
		SaveGame.TrySave();
		_version++;
	}

	static void SetupRound()
	{
		var width = 0.22f - _round * 0.03f;
		width = Math.Clamp( width, 0.1f, 0.24f );
		var center = 0.25f + (float)_rng.NextDouble() * 0.5f;
		_zoneMin = Math.Clamp( center - width * 0.5f, 0.06f, 0.88f );
		_zoneMax = Math.Clamp( _zoneMin + width, 0.12f, 0.94f );
		_marker = _rng.NextDouble() < 0.5 ? 0.05f : 0.95f;
		_markerDir = _marker < 0.5f ? 1f : -1f;
		_markerSpeed = 1.2f + _round * 0.3f;
	}

	static void AddCrack( int n )
	{
		var before = _cracks;
		_cracks = Math.Clamp( _cracks + n, 0, CrackCap );
		if ( _cracks > before )
		{
			TankSim.ShowBanner( _cracks >= CrackCap
				? "Tank is spiderwebbed. One more bad day…"
				: $"Crack! Glass stress ×{_cracks}." );
			BoomFeel.Trigger( 1, new[] { (TankSim.Width * 0.5f, TankSim.Height * 0.35f) } );
		}
		_version++;
	}

	/// <summary>Optional repair from shop later — free debug/karma path.</summary>
	public static bool TryRepair( double cost )
	{
		if ( _cracks <= 0 )
			return false;
		if ( !Economy.TrySpend( cost ) )
			return false;
		_cracks = Math.Max( 0, _cracks - 1 );
		TankSim.ShowBanner( _cracks == 0 ? "Glass resealed. Mostly." : $"Patched. {CrackLabel}." );
		GameAudio.PlayUi( GameAudio.Upgrade );
		SaveGame.TrySave();
		_version++;
		return true;
	}
}