Game/DesertPumpGame.Extras.cs
namespace DesertPump;

/// <summary>
/// The parts that exist to bring you back tomorrow: backgrounds to spend on, a daily
/// bonus that rewards a streak, and the gusher - a rare click worth a lot of coins.
/// </summary>
public sealed partial class DesertPumpGame
{
	// ---- backgrounds ------------------------------------------------------

	readonly HashSet<string> ownedBackgrounds = new() { BackgroundStyle.DefaultId };

	public string SelectedBackground { get; private set; } = BackgroundStyle.DefaultId;

	public bool OwnsBackground( string id ) => ownedBackgrounds.Contains( id );

	public bool CanBuyBackground( BackgroundStyle style ) =>
		style is not null && !OwnsBackground( style.Id ) && Coins >= style.Cost;

	public bool BuyBackground( BackgroundStyle style )
	{
		if ( style is null || OwnsBackground( style.Id ) )
			return false;

		if ( Coins < style.Cost )
		{
			Refuse( $"Need {Numbers.Short( style.Cost - Coins )} more coins" );
			return false;
		}

		Coins -= style.Cost;
		ownedBackgrounds.Add( style.Id );
		SelectedBackground = style.Id;

		Play( UpgradeSound );
		Save();

		return true;
	}

	public void SelectBackground( string id )
	{
		if ( !OwnsBackground( id ) )
			return;

		SelectedBackground = id;
		Save();
	}

	// ---- daily bonus ------------------------------------------------------

	/// <summary>Unix day number of the last claim.</summary>
	long lastDailyDay;

	public int DailyStreak { get; private set; }

	static long Today => DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 86400;

	public bool CanClaimDaily => Today > lastDailyDay;

	/// <summary>
	/// The streak this claim would land on. Miss a day and it's back to 1, so the
	/// banner has to work that out rather than just adding one to the old streak.
	/// </summary>
	public int ProjectedDailyStreak => Today == lastDailyDay + 1 ? DailyStreak + 1 : 1;

	/// <summary>
	/// Scales with where you are, so it stays worth coming back for at tier 12 - about
	/// a minute of your current income, plus a bit more for each day of the streak.
	/// </summary>
	public double DailyReward
	{
		get
		{
			var perSecond = (CurrentPump.PerSecond + LitresPerClick) * PricePerLitre;
			var streakBonus = 1d + Math.Min( DailyStreak, 6 ) * 0.25d;

			return Math.Max( 100d, perSecond * 60d * streakBonus );
		}
	}

	/// <summary>Returns the coins granted, or zero if it wasn't available.</summary>
	public double ClaimDaily()
	{
		if ( !CanClaimDaily )
			return 0;

		// Consecutive days keep the streak, a gap resets it.
		DailyStreak = Today == lastDailyDay + 1 ? DailyStreak + 1 : 1;
		lastDailyDay = Today;

		var reward = DailyReward;
		AddCoins( reward );

		Play( UpgradeSound );
		Save();

		return reward;
	}

	// ---- the gusher -------------------------------------------------------

	/// <summary>Seconds between gusher appearances, randomised inside this band.</summary>
	public const float GusherMinDelay = 75f;
	public const float GusherMaxDelay = 200f;

	/// <summary>How long it hangs around waiting to be clicked.</summary>
	public const float GusherLifetime = 11f;

	TimeSince timeSinceGusher;
	float nextGusherAt = GusherMinDelay;

	/// <summary>True while a gusher is on screen and clickable.</summary>
	public bool GusherActive { get; private set; }

	/// <summary>0-1 across the screen, so it doesn't appear in the same place twice.</summary>
	public float GusherX { get; private set; }
	public float GusherY { get; private set; }

	/// <summary>Fired when a gusher is cashed in, with the coins won.</summary>
	public Action<double> GusherClaimed { get; set; }

	/// <summary>Worth a couple of minutes of income - enough to feel like a find.</summary>
	public double GusherReward =>
		Math.Max( 50d, (CurrentPump.PerSecond + LitresPerClick * 2d) * PricePerLitre * 120d );

	void TickGusher()
	{
		if ( GusherActive )
		{
			if ( timeSinceGusher > GusherLifetime )
			{
				GusherActive = false;
				ScheduleGusher();
			}

			return;
		}

		if ( timeSinceGusher < nextGusherAt )
			return;

		GusherActive = true;
		GusherX = System.Random.Shared.NextSingle() * 0.7f + 0.15f;
		GusherY = System.Random.Shared.NextSingle() * 0.4f + 0.15f;
		timeSinceGusher = 0;
	}

	void ScheduleGusher()
	{
		timeSinceGusher = 0;
		nextGusherAt = GusherMinDelay + System.Random.Shared.NextSingle() * (GusherMaxDelay - GusherMinDelay);
	}

	/// <summary>Cash in the gusher. Returns the coins won, or zero if there wasn't one.</summary>
	public double ClaimGusher()
	{
		if ( !GusherActive )
			return 0;

		GusherActive = false;
		ScheduleGusher();

		var reward = GusherReward;
		AddCoins( reward );

		Play( SellSound );
		GusherClaimed?.Invoke( reward );

		// Saved like every other windfall. A gusher is worth two minutes of income and
		// shows up every couple of minutes, so leaving it to the autosave meant quitting
		// on the wrong second threw it away.
		Save();

		return reward;
	}

	/// <summary>Drop straight onto a pump. For balance testing, not for players.</summary>
	public void JumpToPump( int index )
	{
		PumpLevel = Math.Clamp( index, 0, PumpModel.MaxIndex );
		Water = Math.Min( Water, TankCapacity );
		Save();
	}

	// ---- tier progress ----------------------------------------------------

	/// <summary>
	/// How far through the current tier's nine pumps, 0-1. Owning rank 9 is a finished
	/// tier, so that reads full - counting from rank-1 left the bar at 89% next to a
	/// label saying nothing was left to buy.
	/// </summary>
	public float TierProgress => CurrentPump.Rank / (float)PumpTier.PumpsPerTier;

	/// <summary>Pumps left before the next tier opens up.</summary>
	public int PumpsLeftInTier => PumpTier.PumpsPerTier - CurrentPump.Rank;
}