UI helper for floating Dogecoin popup messages. Defines DogeFloatItem data and DogeFloat static manager that creates, updates, rate-limits, and formats spend/earn/meme popups with lifetimes, drift and labels.
namespace NoChillquarium;
public sealed class DogeFloatItem
{
public string Key { get; set; }
public string Label { get; set; }
public bool IsSpend { get; set; }
public bool IsFunny { get; set; }
public float Life { get; set; }
public float MaxLife { get; set; }
public float OffsetY { get; set; }
public float DriftX { get; set; }
}
/// <summary>
/// Quick Dogecoin popups near the wallet.
/// Spends: always simple. Earns: only pleasant / meme amounts (no drip spam).
/// </summary>
public static class DogeFloat
{
const int MaxVisible = 6;
const float SpendLife = 1.35f;
const float EarnLife = 1.75f;
const float FunnyLife = 2.4f;
static readonly List<DogeFloatItem> _items = new();
static readonly Random _rng = new();
static int _version;
static float _earnCooldown; // soft rate-limit pleasant earns
/// <summary>Meme / joke whole-coin payouts worth a float + sometimes achievements.</summary>
public static readonly int[] FunnyAmounts =
{
11, 22, 33, 42, 69, 88, 99, 123, 180, 360, 404, 420, 666, 777, 800, 1337, 8008, 9001
};
static readonly HashSet<int> FunnySet = new( FunnyAmounts );
public static int Version => _version;
public static IReadOnlyList<DogeFloatItem> Items => _items;
public static void Clear()
{
_items.Clear();
_earnCooldown = 0f;
_version++;
}
public static void Tick( float dt )
{
if ( dt <= 0f )
return;
if ( dt > 0.05f )
dt = 0.05f;
if ( _earnCooldown > 0f )
_earnCooldown = MathF.Max( 0f, _earnCooldown - dt );
var dirty = false;
for ( var i = _items.Count - 1; i >= 0; i-- )
{
var it = _items[i];
it.Life -= dt;
it.OffsetY -= 28f * dt;
if ( it.Life <= 0f )
{
_items.RemoveAt( i );
dirty = true;
}
else
{
dirty = true; // animate every frame while visible
}
}
if ( dirty )
_version++;
}
/// <summary>Always show a simple spend pop (e.g. −Ð15).</summary>
public static void NotifySpend( double amount )
{
if ( amount < 0.5 )
return;
var whole = Math.Max( 1, (int)Math.Round( amount ) );
Push( new DogeFloatItem
{
Key = Guid.NewGuid().ToString( "N" )[..6],
Label = $"−Ð{Economy.FormatDoge( whole )}",
IsSpend = true,
IsFunny = false,
Life = SpendLife,
MaxLife = SpendLife,
OffsetY = 0f,
DriftX = (float)(_rng.NextDouble() * 24.0 - 12.0)
} );
}
/// <summary>
/// Earn pop only for pleasant / meme whole amounts — never idle drip crumbs.
/// </summary>
public static void NotifyEarn( double amount )
{
if ( amount < 0.5 )
return;
var whole = (int)Math.Round( amount );
if ( whole < 1 )
return;
var funny = FunnySet.Contains( whole );
if ( !funny && !IsPleasantEarn( whole ) )
return;
// Soft cooldown so a burst of rewards doesn't bury the HUD.
if ( !funny && _earnCooldown > 0f )
return;
var life = funny ? FunnyLife : EarnLife;
var label = funny
? FunnyLabel( whole )
: $"+Ð{Economy.FormatDoge( whole )}";
Push( new DogeFloatItem
{
Key = Guid.NewGuid().ToString( "N" )[..6],
Label = label,
IsSpend = false,
IsFunny = funny,
Life = life,
MaxLife = life,
OffsetY = 0f,
DriftX = (float)(_rng.NextDouble() * 28.0 - 14.0)
} );
if ( !funny )
_earnCooldown = 0.55f;
if ( funny )
GameAudio.PlayDogeBark();
else if ( whole >= 25 )
GameAudio.PlayCoin();
Achievements.OnPleasantDogeEarn( whole, funny );
}
static bool IsPleasantEarn( int whole )
{
// Skip tiny crumbs — idle never hits NotifyEarn, but small payouts still can.
if ( whole < 5 )
return false;
// Nice round / "good even" feeling numbers.
if ( whole % 100 == 0 )
return true;
if ( whole % 50 == 0 && whole >= 50 )
return true;
if ( whole % 25 == 0 && whole >= 25 )
return true;
if ( whole % 10 == 0 && whole >= 10 )
return true;
if ( whole is 5 or 15 or 20 )
return true;
return false;
}
static string FunnyLabel( int whole ) => whole switch
{
69 => "+Ð69 nice",
420 => "+Ð420 blaze it",
1337 => "+Ð1337 leet",
666 => "+Ð666 oh no",
777 => "+Ð777 jackpot",
42 => "+Ð42 answer",
88 => "+Ð88 lucky",
404 => "+Ð404 not found",
8008 => "+Ð8008 boobies",
9001 => "+Ð9001 over 9000",
11 => "+Ð11 pairwise",
22 => "+Ð22 double",
33 => "+Ð33 thrice",
99 => "+Ð99 almost",
123 => "+Ð123 easy",
180 => "+Ð180 full turn",
360 => "+Ð360 no scope",
800 => "+Ð800 solid",
_ => $"+Ð{whole} much wow"
};
static void Push( DogeFloatItem item )
{
_items.Add( item );
while ( _items.Count > MaxVisible )
_items.RemoveAt( 0 );
_version++;
}
/// <summary>0–1 remaining life for CSS opacity/rise.</summary>
public static float LifeNorm( DogeFloatItem item )
{
if ( item is null || item.MaxLife <= 0f )
return 0f;
return Math.Clamp( item.Life / item.MaxLife, 0f, 1f );
}
}