Game/CoreTree.cs
namespace Monolith;

public enum CoreNodeKind
{
	Deepening,
	Cadence,
	Avarice,
	Anchored,
	Foresight,
	Sympathy,
}

public sealed class CoreNodeDef
{
	public CoreNodeKind Kind;
	public string Name;
	public string Description;

	/// <summary>Cores for the first level. Later levels scale by <see cref="CoreTree.Growth"/>.</summary>
	public int BaseCost;

	public Func<int, string> Format;

	/// <summary>What the next level actually changes, in numbers. See UpgradeDef.Gain.</summary>
	public Func<int, string> Gain;
}

/// <summary>
/// Cores are SPENT here rather than being a flat multiplier.
///
/// A flat "+10% per Core" is the least interesting prestige a game can have: it changes the
/// speed of a run and nothing else, so every run after the first is the same run slightly
/// faster. Every node below has to change how a run is actually played, which is the standard
/// Cookie Clicker's heavenly upgrades set and the reason its ascensions stay interesting
/// hundreds of hours in.
/// </summary>
public static class CoreTree
{
	/// <summary>Cost multiplier per level of the same node.</summary>
	public const double Growth = 1.75;

	public static readonly CoreNodeDef[] All =
	{
		new()
		{
			Kind = CoreNodeKind.Deepening,
			Name = "Deepening",
			Description = "Every shot bites deeper. Blast radius scales harder, forever.",
			BaseCost = 1,
			Format = lvl => $"+{lvl * Tuning.CoreDeepeningPerLevel * 100f:0}% blast radius",
			Gain = lvl => $"blast radius +{lvl * Tuning.CoreDeepeningPerLevel * 100f:0}% to "
				+ $"+{(lvl + 1) * Tuning.CoreDeepeningPerLevel * 100f:0}%, permanently. "
				+ "Radius is cubed, so this is roughly "
				+ $"+{(MathF.Pow( 1f + (lvl + 1) * Tuning.CoreDeepeningPerLevel, 3f ) / MathF.Pow( 1f + lvl * Tuning.CoreDeepeningPerLevel, 3f ) - 1f) * 100f:0}% cubes per shot",
		},
		new()
		{
			Kind = CoreNodeKind.Cadence,
			Name = "Cadence",
			Description = "You fire faster and your charges come back sooner.",
			BaseCost = 1,
			Format = lvl => $"+{lvl * Tuning.CoreCadencePerLevel * 100f:0}% speed, " +
				$"-{lvl * Tuning.CoreCadenceCooldownPerLevel:0.0}s charge cooldown",
			Gain = lvl => $"fire rate +{lvl * Tuning.CoreCadencePerLevel * 100f:0}% to "
				+ $"+{(lvl + 1) * Tuning.CoreCadencePerLevel * 100f:0}%, and charge cooldown "
				+ $"{MathF.Max( Tuning.DemolitionCooldownFloor, Tuning.DemolitionCooldown - lvl * Tuning.CoreCadenceCooldownPerLevel ):0.0}s to "
				+ $"{MathF.Max( Tuning.DemolitionCooldownFloor, Tuning.DemolitionCooldown - (lvl + 1) * Tuning.CoreCadenceCooldownPerLevel ):0.0}s "
				+ $"(floor {Tuning.DemolitionCooldownFloor:0.0}s)",
		},
		new()
		{
			Kind = CoreNodeKind.Avarice,
			Name = "Avarice",
			Description = "More dust per cube, so the next run arms itself faster.",
			BaseCost = 1,
			Format = lvl => $"+{lvl * Tuning.CoreAvaricePerLevel * 100f:0}% dust",
			Gain = lvl => $"dust per cube +{lvl * Tuning.CoreAvaricePerLevel * 100f:0}% to "
				+ $"+{(lvl + 1) * Tuning.CoreAvaricePerLevel * 100f:0}%, on every run from now on",
		},
		new()
		{
			Kind = CoreNodeKind.Anchored,
			Name = "Anchored Upgrades",
			Description = "Keep some upgrade levels through a Collapse instead of losing all of them.",
			BaseCost = 3,
			Format = lvl => $"keep {lvl * Tuning.CoreAnchoredPerLevel} levels of each upgrade",
			Gain = lvl => $"keep {lvl * Tuning.CoreAnchoredPerLevel} to "
				+ $"{(lvl + 1) * Tuning.CoreAnchoredPerLevel} levels of EACH of the "
				+ $"{Upgrades.All.Length} upgrades through a Collapse",
		},
		new()
		{
			Kind = CoreNodeKind.Foresight,
			Name = "Foresight",
			Description = "Skip the opening stages entirely. Start each run further up the ladder.",
			BaseCost = 2,
			Format = lvl => $"start at stage {1 + lvl * Tuning.CoreForesightPerLevel}",
			Gain = lvl => $"every run starts at stage {1 + lvl * Tuning.CoreForesightPerLevel}, "
				+ $"now stage {1 + (lvl + 1) * Tuning.CoreForesightPerLevel}: "
				+ $"{Tuning.CoreForesightPerLevel} fewer stages to climb of the "
				+ $"{Tuning.PrestigeStageRequirement}",
		},
		new()
		{
			Kind = CoreNodeKind.Sympathy,
			Name = "Sympathy",
			Description = "Charges become part of your baseline rather than something you buy.",
			BaseCost = 2,
			Format = lvl => $"+{lvl * Tuning.CoreSympathyPerLevel * 100f:0.0}% charge chance",
			Gain = lvl => $"volatile blocks +{lvl * Tuning.CoreSympathyPerLevel * 100f:0.0}% to "
				+ $"+{(lvl + 1) * Tuning.CoreSympathyPerLevel * 100f:0.0}% before you buy any "
				+ "Instability at all",
		},
	};

	public static CoreNodeDef Get( CoreNodeKind kind ) => All.First( x => x.Kind == kind );

	public static int CostAt( CoreNodeDef def, int level )
		=> Math.Max( 1, (int)Math.Round( def.BaseCost * Math.Pow( Growth, level ) ) );
}