Static Economy manager for an idle game. Tracks real and shown Dogecoin balance, lifetime earnings, playtime, idle income accrual, milestone thresholds and autosave timing, and exposes formatted balance/income strings and spend/add methods.
namespace NoChillquarium;
/// <summary>
/// Dogecoin wallet + idle income from living fish.
/// Real balance is always accurate; the HUD number settles calmly every couple seconds.
/// </summary>
public static class Economy
{
static readonly double[] MilestoneThresholds =
{
10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000
};
/// <summary>How often idle income refreshes the visible wallet (seconds).</summary>
const float DisplaySettleInterval = 3f;
static double _balance;
static double _shownBalance;
static double _lifetimeEarned;
static double _nextMilestone = 10;
static float _playtimeSeconds;
static float _autosaveTimer;
static float _displayTimer;
static float _pendingShowDelay;
static float _walletPulse;
static int _displayVersion;
public static double Balance => _balance;
/// <summary>Calm on-screen balance (settles every few seconds).</summary>
public static double ShownBalance => _shownBalance;
public static double LifetimeEarned => _lifetimeEarned;
public static float PlaytimeSeconds => _playtimeSeconds;
public static int DisplayVersion => _displayVersion;
public static double IncomePerSecond => TankSim.IncomePerSecond * FeedSystem.IncomeMultiplier;
/// <summary>True while the wallet chip is doing a soft settle pulse.</summary>
public static bool WalletPulse => _walletPulse > 0.04f;
/// <summary>Settled HUD balance (not the frantic live accrual).</summary>
public static string BalanceText => FormatDoge( _shownBalance );
public static string IncomeText
{
get
{
var rate = IncomePerSecond;
// Round rate for a calm readout — not three decimals twitching.
var shownRate = rate >= 10 ? Math.Round( rate, 1 ) : Math.Round( rate, 2 );
var text = $"+{FormatDoge( shownRate )}/s";
if ( FeedSystem.IsBoosted )
text += " FED";
if ( TankSim.SaltUnlocked )
text += " ALL TANKS";
return text;
}
}
public static void ResetForNewGame()
{
// Small starter wallet so the shop is reachable without a long AFK.
_balance = 25;
_shownBalance = 25;
_lifetimeEarned = 0;
_playtimeSeconds = 0;
_autosaveTimer = 0;
_displayTimer = 0;
_pendingShowDelay = 0;
_walletPulse = 0;
_nextMilestone = MilestoneThresholds[0];
FeedSystem.Clear();
ExplodeSystem.Clear();
DogeFloat.Clear();
BumpDisplay();
}
public static void LoadFromSave( SaveData data )
{
if ( data is null )
{
ResetForNewGame();
return;
}
_balance = Math.Max( 0, data.Dogecoin );
_shownBalance = _balance;
_lifetimeEarned = Math.Max( 0, data.LifetimeEarned );
_playtimeSeconds = Math.Max( 0, data.PlaytimeSeconds );
_autosaveTimer = 0;
_displayTimer = 0;
_pendingShowDelay = 0;
_walletPulse = 0;
RecalcNextMilestone();
BumpDisplay();
}
public static void WriteToSave( SaveData data )
{
if ( data is null )
return;
data.Dogecoin = _balance;
data.LifetimeEarned = _lifetimeEarned;
data.PlaytimeSeconds = _playtimeSeconds;
}
public static void Tick( float dt )
{
if ( !TankSim.IsActive || dt <= 0f )
return;
if ( dt > 0.05f )
dt = 0.05f;
_playtimeSeconds += dt;
var rate = IncomePerSecond;
if ( rate > 0 )
{
var gained = rate * dt;
_balance += gained;
_lifetimeEarned += gained;
// Idle drip — advance milestones silently (no bark spam).
CheckMilestones( playSound: false );
}
_autosaveTimer += dt;
if ( _autosaveTimer >= 15f )
{
_autosaveTimer = 0f;
SaveGame.TrySave( quiet: true );
}
if ( _walletPulse > 0f )
_walletPulse = MathF.Max( 0f, _walletPulse - dt * 1.35f );
// Explicit earn: short pause so the float can appear, then settle the number.
if ( _pendingShowDelay > 0f )
{
_pendingShowDelay -= dt;
if ( _pendingShowDelay <= 0f )
{
_pendingShowDelay = 0f;
SettleShown( soft: true );
_displayTimer = 0f;
}
}
else
{
// Idle: only refresh the chip every couple seconds — calm ticker, not a slot machine.
_displayTimer += dt;
if ( _displayTimer >= DisplaySettleInterval )
{
_displayTimer = 0f;
SettleShown( soft: true );
}
}
}
public static bool TrySpend( double amount )
{
if ( amount <= 0 )
return true;
if ( _balance < amount )
return false;
_balance -= amount;
DogeFloat.NotifySpend( amount );
// Spends snap now — you always trust the number after a purchase.
_pendingShowDelay = 0f;
_displayTimer = 0f;
SettleShown( soft: false );
return true;
}
public static void Add( double amount )
{
if ( amount <= 0 )
return;
_balance += amount;
_lifetimeEarned += amount;
// Explicit payouts only (not idle drip) — pleasant/meme floats + milestone SFX.
DogeFloat.NotifyEarn( amount );
CheckMilestones( playSound: true );
// Let the float lead, then the wallet ticks up with a soft pulse.
_pendingShowDelay = 0.45f;
BumpDisplay();
}
/// <summary>
/// Push the visible balance toward the real one.
/// Soft = only if it changed enough to matter; hard = always (spends).
/// </summary>
static void SettleShown( bool soft )
{
var delta = Math.Abs( _balance - _shownBalance );
if ( soft && delta < 0.05 )
return;
_shownBalance = _balance;
_walletPulse = soft ? 0.85f : 1f;
BumpDisplay();
}
static void CheckMilestones( bool playSound )
{
while ( _balance >= _nextMilestone )
{
var hit = _nextMilestone;
if ( playSound )
{
// Bark only for chunky wallet thresholds; small ones soft coin.
if ( hit >= 100 )
GameAudio.PlayDogeBark();
else
GameAudio.PlayCoin();
}
AdvanceMilestone();
}
}
static void AdvanceMilestone()
{
for ( var i = 0; i < MilestoneThresholds.Length; i++ )
{
if ( MilestoneThresholds[i] > _nextMilestone + 0.001 )
{
_nextMilestone = MilestoneThresholds[i];
return;
}
}
// Beyond table: keep doubling.
_nextMilestone = Math.Max( _nextMilestone * 2.0, _nextMilestone + 10000 );
}
static void RecalcNextMilestone()
{
_nextMilestone = MilestoneThresholds[0];
foreach ( var t in MilestoneThresholds )
{
if ( _balance < t )
{
_nextMilestone = t;
return;
}
}
_nextMilestone = Math.Max( _balance * 2.0, MilestoneThresholds[^1] * 2.0 );
}
static void BumpDisplay() => _displayVersion++;
public static string FormatDoge( double value )
{
if ( value >= 1_000_000_000 )
return $"{value / 1_000_000_000:0.##}B";
if ( value >= 1_000_000 )
return $"{value / 1_000_000:0.##}M";
if ( value >= 10_000 )
return $"{value / 1000:0.#}K";
if ( value >= 100 )
return $"{value:0}";
if ( value >= 10 )
return $"{value:0.#}";
return $"{value:0.##}";
}
/// <summary>
/// Extra-compact form for the fixed LCD (keeps glyph count low so text stays large).
/// Prefer abbreviations earlier than general HUD formatting.
/// </summary>
public static string FormatDogeLcd( double value )
{
value = Math.Abs( value );
if ( value >= 1_000_000_000 )
return $"{value / 1_000_000_000:0.#}B";
if ( value >= 1_000_000 )
return $"{value / 1_000_000:0.#}M";
// LCD switches to K earlier so we don't show 5–6 raw digits
if ( value >= 1_000 )
return $"{value / 1000:0.#}K";
if ( value >= 100 )
return $"{value:0}";
if ( value >= 10 )
return $"{value:0.#}";
return $"{value:0.##}";
}
}