Game/Numbers.cs
namespace DesertPump;
/// <summary>
/// Clicker numbers get big fast, so everything the player sees goes through here.
/// </summary>
public static class Numbers
{
/// <summary>
/// Short scale, far enough to cover the last pump in tier 12 - which costs about
/// 2.5e51. Anything past the end of this falls back to scientific notation.
/// </summary>
static readonly string[] Suffixes =
{
"", "K", "M", "B", "T",
"Qa", "Qi", "Sx", "Sp", "Oc", "No",
"Dc", "UDc", "DDc", "TDc", "QaDc", "QiDc", "SxDc", "SpDc", "OcDc", "NoDc",
"Vg", "UVg", "DVg", "TVg", "QaVg", "QiVg", "SxVg", "SpVg", "OcVg", "NoVg",
"Tg"
};
/// <summary>
/// 950 -> "950", 1250 -> "1.25K", 3_400_000 -> "3.40M".
/// </summary>
public static string Short( double value )
{
if ( double.IsNaN( value ) || double.IsInfinity( value ) ) return "0";
var negative = value < 0;
value = Math.Abs( value );
if ( value < 1000 )
{
var whole = Math.Floor( value );
var text = value < 10 && whole != value
? value.ToString( "0.#" )
: whole.ToString( "0" );
return negative ? $"-{text}" : text;
}
var tier = 0;
while ( value >= 1000 && tier < Suffixes.Length - 1 )
{
value /= 1000;
tier++;
}
// Ran off the end of the names - past here nobody minds an exponent.
if ( value >= 1000 )
{
var raw = negative ? -value : value;
return (raw * Math.Pow( 1000, tier )).ToString( "0.00e+0" );
}
var format = value < 10 ? "0.00" : value < 100 ? "0.0" : "0";
var result = value.ToString( format ) + Suffixes[tier];
return negative ? $"-{result}" : result;
}
/// <summary>
/// A hash bucket for a number the player is looking at, one bucket per visible
/// change - three significant figures, which is exactly what <see cref="Short"/> shows.
/// </summary>
/// <remarks>
/// Panels used to hash their numbers as <c>(long)Coins</c>. That works until tier 9,
/// where coins pass long.MaxValue and every out-of-range cast lands on the same
/// value - so the hash stops moving, the tree stops rebuilding, and the wallet
/// freezes on screen while the real number keeps climbing. Water and tree water go
/// the same way, sooner, because they were cast to int.
///
/// Bucketing by mantissa and exponent has no ceiling at all, so it holds for the
/// whole ladder. The mantissa is rounded rather than truncated so a bucket turns
/// over on exactly the value <see cref="Short"/> starts printing differently -
/// truncating left the last digit of the readout a fraction of a step stale.
/// </remarks>
public static int Bucket( double value )
{
if ( double.IsNaN( value ) ) return 0;
if ( double.IsInfinity( value ) ) return int.MaxValue;
var negative = value < 0;
value = Math.Abs( value );
if ( value < 1e-6 ) return 0;
var exponent = (int)Math.Floor( Math.Log10( value ) );
// 100-1000, three significant figures. 1000 only comes up when 999.5 rounds up,
// and it can't collide with the next decade because a mantissa is never zero.
var mantissa = (int)Math.Round( value / Math.Pow( 10, exponent - 2 ) );
var bucket = exponent * 1000 + mantissa;
return negative ? -bucket : bucket;
}
/// <summary>Litres per second, trimmed so it doesn't jitter in the HUD.</summary>
public static string Rate( double value )
{
if ( value <= 0 ) return "0";
if ( value < 10 ) return value.ToString( "0.#" );
return Short( value );
}
/// <summary>"1 coin", "2 coins" - only exactly one is singular.</summary>
public static string Plural( double value, string singular, string plural )
{
return value == 1d ? singular : plural;
}
}