Game/PumpModel.cs

Data model for a pump in the game. It defines constants for progression, builds a static array of all 108 pumps with computed cost, click value, idle income, tank size and price per litre, and exposes helper properties like Name, Tier, Rank and lookup methods.

namespace DesertPump;

/// <summary>
/// One pump - one rung of the 108 step ladder. Pure data, built once from the curve
/// below rather than written out by hand.
/// </summary>
/// <remarks>
/// The constants come out of tools/balance.py, which simulates a playthrough. Costs
/// grow 2.88x per pump against roughly 2.43x income growth from the pump itself, and
/// the shop adds the rest on top - so each pump takes slightly longer than the last
/// and each tier lands between one and seven hours. About 55 hours of steady clicking
/// for all twelve tiers, a few hundred at a casual pace.
///
/// Change a constant, re-run the simulation, and look at the shape before shipping it.
/// Two earlier passes at this curve finished the whole game in two minutes.
/// </remarks>
public sealed class PumpModel
{
	// ---- the curve ----------------------------------------------------------
	const double CostFirst = 500.0;      // what pump #2 costs
	const double CostGrowth = 2.88;
	const float ClickFirst = 1f;
	const float ClickGrowth = 1.75f;
	const float IdleFirst = 0.5f;        // litres/sec at pump #2
	const float IdleGrowth = 1.90f;
	const float TankFirst = 30f;
	const float TankGrowth = 1.82f;
	const float PriceFirst = 1f;
	const float PriceGrowth = 1.28f;

	/// <summary>0-based position across all 108 pumps.</summary>
	public int Index { get; init; }

	/// <summary>The tier this belongs to.</summary>
	public PumpTier Tier { get; init; }

	/// <summary>1-9 within its tier.</summary>
	public int Rank { get; init; }

	public string Name { get; init; }

	/// <summary>Coins needed to unlock. The very first pump is free.</summary>
	public double Cost { get; init; }

	/// <summary>Base litres per manual pump, before the Pump Power upgrade.</summary>
	public double PerClick { get; init; }

	/// <summary>Litres per second with no input at all.</summary>
	public double PerSecond { get; init; }

	/// <summary>Base tank size, before the Water Tank upgrade.</summary>
	public double Tank { get; init; }

	/// <summary>Base coins per litre, before the Market Contacts upgrade.</summary>
	public double PricePerLitre { get; init; }

	public Color Tint => Tier.Tint;

	/// <summary>"Tier 3 - 4 of 9"</summary>
	public string Position => $"TIER {Tier.Number} - {Rank} OF {PumpTier.PumpsPerTier}";

	/// <summary>Re-read on hotload so balance edits apply without restarting the editor.</summary>
	[SkipHotload]
	public static readonly PumpModel[] All = Build();

	static PumpModel[] Build()
	{
		var total = PumpTier.TotalPumps;
		var list = new PumpModel[total];

		for ( var i = 0; i < total; i++ )
		{
			var tier = PumpTier.ForPump( i );
			var rank = i % PumpTier.PumpsPerTier;

			list[i] = new PumpModel
			{
				Index = i,
				Tier = tier,
				Rank = rank + 1,
				Name = tier.PumpNames[rank],
				Cost = i == 0 ? 0 : CostFirst * Math.Pow( CostGrowth, i - 1 ),
				PerClick = ClickFirst * Math.Pow( ClickGrowth, i ),
				PerSecond = i == 0 ? 0 : IdleFirst * Math.Pow( IdleGrowth, i - 1 ),
				Tank = TankFirst * Math.Pow( TankGrowth, i ),
				PricePerLitre = PriceFirst * Math.Pow( PriceGrowth, i )
			};
		}

		return list;
	}

	public static int MaxIndex => All.Length - 1;

	public static PumpModel Get( int index ) => All[Math.Clamp( index, 0, MaxIndex )];
}