Spending/Research/Improvements/BaseImprovements.cs
using System;
using System.Numerics;
using System.Text.Json.Nodes;

namespace PlanetMeat;

public class BaseImprovement : IPurchaseableUpgrade, ISaveTarget
{
	public string Ident { get; private set; }
	public string CategoryIdent { get; private set; }
	public string UpgradeNameIdent { get; set; } = null;
	public int Rank { get; set; }

	public virtual double NextCost => CostOffset + Progressions.StairSlow( Rank + CostStart );
	public int CostStart { get; set; } = 1;
	public int CostOffset { get; set; } = 0;
	public string RequiredTech { get => field == "" && Research.Current.IsTech( CategoryIdent ) ? CategoryIdent : field; set; } = "";

	public virtual bool Visible => Research.Current.HasTech( RequiredTech );
	public virtual bool Purchaseable => true;

	public string DisplayedCurrentEffect => GetDisplayedEffectAt( Rank );
	public string DisplayedNextEffect => GetDisplayedEffectAt( Rank + 1 );
	public string AdvancementText => DisplayedCurrentEffect + " -> " + DisplayedNextEffect;
	public string DisplayName => UpgradeNameIdent is null ? ("improvement." + Ident + ".title") : ("upgrade." + UpgradeNameIdent + ".title");

	public JsonNode NextSave => new JsonObject { { nameof( Rank ), Rank } };

	public BaseImprovement( string ident, string category )
	{
		Ident = ident;
		CategoryIdent = category;
	}

	public bool TryPurchase()
	{
		var cost = NextCost;
		if ( Purchaseable && Bank.Current.GetAccount( "meat" ).TrySpend( cost ) )
		{
			Career.Current.IncrementStat( "meat_spent", cost );
			Career.Current.IncrementStat( "improvements" );
			Rank++;
			return true;
		}
		return false;
	}

	public virtual void BakeEffects() { }

	public virtual string GetDisplayedEffectAt( int rank ) => rank.ToString();

	public void LoadFromSave( JsonNode save )
	{
		if ( save.AsObject() is JsonObject obj && obj.TryGetTypedProperty( nameof( Rank ), out int r ) )
			Rank = r;
	}

	public void ClearSave()
	{
		Rank = 0;
	}
}

public abstract class BaseEffectImprovement<T>( string ident, string category, T effectBase, T effectPerRank, T effectMax ) : BaseImprovement( ident, category ) where T : IAdditionOperators<T, T, T>, IComparable<T>
{
	public T CurrentEffect => GetEffectAt( Rank );

	public abstract T GetEffectAt( int rank );
	public abstract bool HasCap { get; }
	public T EffectBase { get; } = effectBase;
	public T EffectMax { get; } = effectMax;
	public T EffectPerRank { get; } = effectPerRank;

	public bool IsIncrease => GetEffectAt( Rank ).CompareTo( GetEffectAt( Rank + 1 ) ) < 0;
	protected virtual bool IsUnderCap => CurrentEffect.CompareTo( EffectMax ) < 0;
	public override bool Purchaseable => base.Purchaseable && IsIncrease && (!HasCap || IsUnderCap);

	public override string GetDisplayedEffectAt( int rank )
	{
		var effect = GetEffectAt( rank );
		if ( effect is double d )
			return LargeNumberString.ToCompact( d );
		if ( effect is float f )
			return f.ToString( "N" );
		return effect.ToString();
	}
}