Game/Idle/IdleBalance.cs
using System;
using System.Collections.Generic;

/// <summary>
/// Every tuning number for Reactor (Idle) mode lives here.
/// Pure functions only — the simulation, the UI and the offline estimator all
/// read from this file so a balance change never needs touching three places.
/// </summary>
public static class IdleBalance
{
	// ══════════════════════════════════════════════════════════════
	// PRODUCTION — how fast chests arrive on the board
	// ══════════════════════════════════════════════════════════════

	public const float SpawnIntervalBase  = 0.9f;
	public const float SpawnIntervalFloor = 0.30f;

	/// <summary>Ambient drip — one chest lands every this many seconds.</summary>
	public static float SpawnInterval( int spawnRateLevel )
		=> MathF.Max( SpawnIntervalFloor, SpawnIntervalBase / (1f + 0.105f * spawnRateLevel) );

	/// <summary>
	/// Hopper batch dumped onto the board immediately before every detonation.
	///
	/// This exists so chain length is driven by the hopper rather than by how
	/// long chests were left to pile up. Without it, a slower cycle would mean
	/// longer chains and — because payout scales super-linearly with chain
	/// length — buying Fuse Timing could actually *lower* income. Topping the
	/// board up per cycle keeps every upgrade in the mode strictly positive.
	/// </summary>
	public static int SpawnCount( int spawnCountLevel ) => 3 + spawnCountLevel;

	/// <summary>Chance a spawned chest rolls Gem instead of Normal.</summary>
	public static float GemChance( int level ) => MathF.Min( 0.60f, 0.02f * level );

	/// <summary>Chance a spawned chest rolls BombChest — the chain fuel.</summary>
	public static float BombChestChance( int level ) => MathF.Min( 0.45f, 0.015f * level );

	// ══════════════════════════════════════════════════════════════
	// BOARD
	// ══════════════════════════════════════════════════════════════

	public static int GridSize( int frameLevel ) => 5 + Math.Clamp( frameLevel, 0, 4 );

	// ══════════════════════════════════════════════════════════════
	// DETONATION
	// ══════════════════════════════════════════════════════════════

	public const float CycleIntervalBase  = 3.0f;
	public const float CycleIntervalFloor = 0.45f;

	public static float CycleInterval( int cycleSpeedLevel )
		=> MathF.Max( CycleIntervalFloor, CycleIntervalBase * MathF.Pow( 0.958f, cycleSpeedLevel ) );

	public static int DroneCount( int level ) => 1 + Math.Clamp( level, 0, 9 );

	public static string BlastName( int level ) => level switch
	{
		0 => "Cross ✛",
		1 => "Diagonal ✕",
		2 => "Square 3×3",
		3 => "Heavy 3×3 +arms",
		_ => "Nova 5×5",
	};

	/// <summary>Blast offsets for the drone payload at a given Warhead Class level.</summary>
	public static IEnumerable<(int dr, int dc)> BlastOffsets( int blastLevel )
	{
		switch ( Math.Clamp( blastLevel, 0, 4 ) )
		{
			case 0: // Cross
				yield return (0, 0);
				yield return (-1, 0); yield return (1, 0);
				yield return (0, -1); yield return (0, 1);
				break;

			case 1: // Diagonal X, 2 deep — mirrors the Diagonal bomb from the main game
				yield return (0, 0);
				yield return (-1, -1); yield return (-1, 1); yield return (1, -1); yield return (1, 1);
				yield return (-2, -2); yield return (-2, 2); yield return (2, -2); yield return (2, 2);
				break;

			case 2: // Full 3x3
				for ( int dr = -1; dr <= 1; dr++ )
					for ( int dc = -1; dc <= 1; dc++ )
						yield return (dr, dc);
				break;

			case 3: // 3x3 with cross arms poking out to range 2
				for ( int dr = -1; dr <= 1; dr++ )
					for ( int dc = -1; dc <= 1; dc++ )
						yield return (dr, dc);
				yield return (-2, 0); yield return (2, 0);
				yield return (0, -2); yield return (0, 2);
				break;

			default: // Nova — full 5x5
				for ( int dr = -2; dr <= 2; dr++ )
					for ( int dc = -2; dc <= 2; dc++ )
						yield return (dr, dc);
				break;
		}
	}

	/// <summary>Blast a BombChest fires when it dies — always a cross, that is its identity.</summary>
	public static IEnumerable<(int dr, int dc)> BombChestOffsets()
	{
		yield return (0, 0);
		yield return (-1, 0); yield return (1, 0);
		yield return (0, -1); yield return (0, 1);
	}

	// ══════════════════════════════════════════════════════════════
	// VALUE
	// ══════════════════════════════════════════════════════════════

	/// <summary>Raw Core worth of one chest before any multiplier.</summary>
	public static double ChestBaseValue( GridManager.ChestType type ) => type switch
	{
		GridManager.ChestType.Normal    => 1.0,
		GridManager.ChestType.Gem       => 4.0,
		GridManager.ChestType.BombChest => 2.0,
		GridManager.ChestType.Locked    => 14.0,
		GridManager.ChestType.Cursed    => 32.0,
		GridManager.ChestType.Void      => 55.0,
		GridManager.ChestType.Boss      => 400.0,
		GridManager.ChestType.Secret    => 25.0,
		_                               => 1.0,
	};

	/// <summary>Hits a chest takes before it pops.</summary>
	public static int ChestHits( GridManager.ChestType type ) => type switch
	{
		GridManager.ChestType.Gem    => 2,
		GridManager.ChestType.Locked => 3,
		GridManager.ChestType.Cursed => 4,
		GridManager.ChestType.Void   => 3,
		GridManager.ChestType.Boss   => 12,
		_                            => 1,
	};

	public static double ChestValueMult( int denseLootLevel ) => 1.0 + 0.16 * denseLootLevel;

	/// <summary>
	/// The heart of the mode. A detonation that clears N chests multiplies the
	/// whole payout by N^exponent — so doubling chain length more than doubles income.
	/// </summary>
	public static double ChainExponent( int chainPowerLevel, int resonancePerkLevel )
		=> 0.80 + 0.026 * chainPowerLevel + 0.02 * resonancePerkLevel;

	public static double ChainMultiplier( int cleared, double exponent )
	{
		if ( cleared <= 1 ) return 1.0;
		return Math.Pow( cleared, exponent );
	}

	public static float  CritChance( int level ) => MathF.Min( 0.60f, 0.020f * level );
	public static double CritPower(  int level ) => 3.0 + 0.9 * level;

	// ══════════════════════════════════════════════════════════════
	// MANUAL PLAY
	// ══════════════════════════════════════════════════════════════

	public const double OverloadMultiplier = 3.5;

	public static float OverloadCooldown( int level )
		=> MathF.Max( 0.8f, 6f * MathF.Pow( 0.90f, level ) );

	/// <summary>Surge meter filled per manual overload, 0..1.</summary>
	public static float ChargeGain( int capacitorLevel, int momentumPerkLevel )
		=> MathF.Min( 0.5f, 0.07f * (1f + 0.09f * capacitorLevel) * (1f + 0.15f * momentumPerkLevel) );

	public static float SurgeDuration( int momentumPerkLevel ) => 15f + 4f * momentumPerkLevel;
	public const double SurgeMultiplier = 10.0;

	public static float GoldenInterval( int level )
		=> MathF.Max( 25f, 110f * MathF.Pow( 0.90f, level ) );

	public const float GoldenLifetime = 11f;

	/// <summary>Golden Chest pays this many seconds of current output.</summary>
	public const double GoldenSecondsOfOutput = 150.0;

	public static float AutoOverloadInterval( int autopilotLevel ) => autopilotLevel switch
	{
		<= 0 => 0f,
		1    => 14f,
		2    => 9f,
		_    => 5f,
	};

	// ══════════════════════════════════════════════════════════════
	// OFFLINE
	// ══════════════════════════════════════════════════════════════

	public static float OfflineEfficiency( int nightCrewLevel, int nightShiftPerkLevel )
		=> MathF.Min( 1.6f, 0.25f + 0.05f * nightCrewLevel + 0.08f * nightShiftPerkLevel );

	public static float OfflineCapHours( int storageLevel, int deepStoragePerkLevel )
		=> 2f + 2f * storageLevel + 2f * deepStoragePerkLevel;

	/// <summary>Offline never pays for stretches under this — stops menu bounces printing money.</summary>
	public const double OfflineMinSeconds = 20.0;

	// ══════════════════════════════════════════════════════════════
	// FRAGMENTS — the bridge into the main game economy
	// ══════════════════════════════════════════════════════════════

	/// <summary>
	/// Cores needed to condense the next Fragment today.
	///
	/// The price climbs 11% per Fragment already condensed today, which is what
	/// actually limits the drip — a flat price would let a late-game reactor
	/// bottom out the daily allowance in seconds and make the whole condenser
	/// branch pointless. Condenser Coil and Condenser Array shift the curve
	/// down, so each level is worth a real handful of extra Fragments per day.
	/// </summary>
	public static double FragmentCost( int coilLevel, int condenserPerkLevel, int fragmentsToday )
	{
		int n = Math.Clamp( fragmentsToday, 0, 400 );
		return 1_200.0
			 * Math.Pow( 1.11, n )
			 * Math.Pow( 0.62, coilLevel )
			 * Math.Pow( 0.85, condenserPerkLevel );
	}

	/// <summary>Hard safety ceiling on top of the escalating price.</summary>
	public static int DailyFragmentCap( int condenserPerkLevel, int petLevel )
		=> 200 + 60 * condenserPerkLevel + 15 * Math.Max( 0, petLevel - 1 );

	// ══════════════════════════════════════════════════════════════
	// PET LINK — cross-mode payoff for levelling the pet in Wave/Survival
	// ══════════════════════════════════════════════════════════════

	public static double PetBonus( int petLevel, bool petUnlocked, int petLinkPerk )
	{
		if ( !petUnlocked ) return 1.0;
		double bonus = IdlePet.OutputBonus( petLevel );   // Lv6 pet = +75%
		if ( petLinkPerk > 0 ) bonus *= 2.0;              // Pet Link doubles it
		return 1.0 + bonus;
	}

	// ══════════════════════════════════════════════════════════════
	// MELTDOWN (prestige)
	// ══════════════════════════════════════════════════════════════

	public const double MeltdownRequirement = 1_000_000.0;

	public static int FluxFor( double cycleCores )
	{
		if ( cycleCores < MeltdownRequirement ) return 0;
		return (int)Math.Floor( 8.0 * Math.Pow( cycleCores / MeltdownRequirement, 0.40 ) );
	}

	/// <summary>Global income multiplier granted by unspent + spent Flux alike.</summary>
	public static double FluxMultiplier( int fluxEarned ) => 1.0 + 0.01 * fluxEarned;

	// ══════════════════════════════════════════════════════════════
	// CHEST TIER UNLOCKS — paced so new material keeps appearing for hours
	// ══════════════════════════════════════════════════════════════

	public class ChestTier
	{
		public GridManager.ChestType Type;
		public double  Requires;     // cycle cores
		public string  Name;
		public string  Icon;
		public string  Blurb;
		public float   Weight;       // relative spawn weight once unlocked
	}

	public static readonly List<ChestTier> Tiers = new()
	{
		new ChestTier { Type = GridManager.ChestType.Locked, Requires = 150_000,
			Name = "Locked Chests", Icon = "🔒", Weight = 0.10f,
			Blurb = "3 hits, 14× the Cores of a normal chest." },

		new ChestTier { Type = GridManager.ChestType.Cursed, Requires = 3_000_000,
			Name = "Cursed Chests", Icon = "💀", Weight = 0.07f,
			Blurb = "4 hits, 32× value. Stubborn but worth it." },

		new ChestTier { Type = GridManager.ChestType.Void, Requires = 25_000_000,
			Name = "Void Chests", Icon = "🌑", Weight = 0.05f,
			Blurb = "Detonates a 3×3 when it dies — a free chain amplifier." },

		new ChestTier { Type = GridManager.ChestType.Boss, Requires = 300_000_000,
			Name = "Reactor Cores", Icon = "👑", Weight = 0.015f,
			Blurb = "12 hits, 400× value. The big one." },
	};

	// ══════════════════════════════════════════════════════════════
	// DAILY LOOP
	// ══════════════════════════════════════════════════════════════

	/// <summary>Day 1..7 of the login streak — boost multiplier and duration.</summary>
	public static (double mult, float seconds, int frags) DailyReward( int streakDay )
	{
		int d = Math.Clamp( streakDay, 1, 7 );
		return d switch
		{
			1 => (2.0,  180f,  5),
			2 => (2.5,  240f,  8),
			3 => (3.0,  300f, 12),
			4 => (4.0,  360f, 18),
			5 => (5.0,  420f, 25),
			6 => (6.0,  480f, 35),
			_ => (8.0,  600f, 50),
		};
	}
}