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