Part of the DesertPumpGame class that implements a persistent, timed tree growth mechanic. Tracks current tree height, water poured, required water per stage, growth timer, UI helpers (fraction, time left, art stage), and methods to pour water, advance growth on tick, and fast-forward the timer for testing.
namespace DesertPump;
/// <summary>
/// The tree. Pour water into it, wait, and it grows a stage taller. Height is the
/// number that goes on the leaderboard, and because growth runs on wall-clock time it
/// keeps ticking while the game is shut - there's always a reason to look in later.
/// </summary>
public sealed partial class DesertPumpGame
{
/// <summary>Litres the first stage needs. Every stage after costs more.</summary>
public const double TreeFirstStageWater = 1000;
/// <summary>How much more water each stage wants than the last.</summary>
public const double TreeWaterGrowth = 2.2;
/// <summary>Real seconds a stage takes once it's been watered enough.</summary>
public const double TreeGrowSeconds = 15 * 60;
/// <summary>Stages grown. This is the leaderboard score.</summary>
public int TreeHeight { get; private set; }
/// <summary>Litres poured into the current stage so far.</summary>
public double TreeWater { get; private set; }
/// <summary>Unix seconds when the current stage finishes, or 0 if it isn't growing.</summary>
long treeGrowsAt;
/// <summary>Fired when a stage completes, with the new height.</summary>
public Action<int> TreeGrew { get; set; }
/// <summary>Litres the current stage needs in total.</summary>
public double TreeWaterNeeded => TreeFirstStageWater * Math.Pow( TreeWaterGrowth, TreeHeight );
/// <summary>Litres still to pour before it starts growing.</summary>
public double TreeWaterRemaining => Math.Max( 0, TreeWaterNeeded - TreeWater );
/// <summary>0-1 toward the next stage.</summary>
public float TreeFraction => TreeWaterNeeded <= 0
? 0f
: (float)Math.Clamp( TreeWater / TreeWaterNeeded, 0d, 1d );
public bool TreeIsGrowing => treeGrowsAt > 0;
/// <summary>Seconds left on the current stage, zero when it isn't growing.</summary>
public double TreeSecondsLeft => TreeIsGrowing
? Math.Max( 0, treeGrowsAt - DateTimeOffset.UtcNow.ToUnixTimeSeconds() )
: 0;
/// <summary>You can always pour something in, as long as there's water and it isn't busy.</summary>
public bool CanWaterTree => !TreeIsGrowing && Water > 0.0001;
/// <summary>"14:02" - what's left on the timer.</summary>
public string TreeTimeLeft
{
get
{
var seconds = (int)Math.Ceiling( TreeSecondsLeft );
return $"{seconds / 60}:{seconds % 60:00}";
}
}
/// <summary>
/// Pour the tank into the tree. Takes only what this stage still needs, so you never
/// waste the overflow, and kicks off the timer once it's had enough.
/// </summary>
public double WaterTree()
{
if ( TreeIsGrowing )
{
Refuse( $"Already growing - {TreeTimeLeft} left" );
return 0;
}
if ( Water <= 0.0001 )
{
Refuse( "No water to pour" );
return 0;
}
var poured = Math.Min( Water, TreeWaterRemaining );
Water -= poured;
TreeWater += poured;
if ( TreeWater >= TreeWaterNeeded - 0.0001 )
{
treeGrowsAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + (long)TreeGrowSeconds;
Play( UpgradeSound );
}
else
{
Play( SellSound );
}
Save();
return poured;
}
/// <summary>Finish the stage if its timer has run out. Works across a restart.</summary>
void TickTree()
{
if ( !TreeIsGrowing || TreeSecondsLeft > 0 )
return;
treeGrowsAt = 0;
TreeWater = 0;
TreeHeight++;
Play( UpgradeSound );
TreeGrew?.Invoke( TreeHeight );
Leaderboard.Submit( TreeHeight );
Save();
}
/// <summary>Tree art has a handful of drawn stages - this picks which one to show.</summary>
public int TreeArtStage => Math.Clamp( TreeHeight, 0, 6 );
/// <summary>
/// Wind the growth timer forward. For testing a fifteen minute mechanic without
/// sitting through fifteen minutes - not reachable from the UI.
/// </summary>
public void SkipTreeTime( double seconds )
{
if ( !TreeIsGrowing )
return;
treeGrowsAt -= (long)seconds;
TickTree();
}
}