Game/Idle/IdleSave.cs
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Persistent state for Reactor (Idle) mode.
/// Serialized as a nested object inside PlayerSave — see PlayerSave.Idle.
/// Everything here survives a Meltdown except what ResetForMeltdown() clears.
/// </summary>
public class IdleSave
{
	// ── Unlock / onboarding ───────────────────────────────────────
	public bool Unlocked      { get; set; } = false;  // granted after the first finished run
	public bool IntroShown    { get; set; } = false;  // first-entry explainer
	public bool MeltdownSeen  { get; set; } = false;  // has the prestige panel ever been opened

	/// <summary>
	/// The guided walkthrough has run to the end, or been skipped. Separate from
	/// IntroShown so an existing save — which already has IntroShown set and has
	/// therefore never seen the walkthrough — still gets offered it once.
	/// </summary>
	public bool TutorialDone { get; set; } = false;

	/// <summary>The walkthrough has been offered and declined; do not re-prompt.</summary>
	public bool TutorialOffered { get; set; } = false;

	// ── Currencies ────────────────────────────────────────────────
	public double Cores         { get; set; } = 0;   // spendable, wiped by Meltdown
	public double CycleCores    { get; set; } = 0;   // earned since last Meltdown — drives Flux payout
	public double AllTimeCores  { get; set; } = 0;   // never resets
	public int    Flux          { get; set; } = 0;   // unspent prestige currency
	public int    FluxEarned    { get; set; } = 0;   // lifetime, for milestones
	public int    Meltdowns     { get; set; } = 0;
	public int    LastReactorRank { get; set; } = 0;   // 0 == never ranked; drives the "up N places" note

	// ── Singularity (second prestige) ─────────────────────────────
	public int  Shards          { get; set; } = 0;   // unspent, buys Powers
	public int  ShardsEarned    { get; set; } = 0;   // lifetime — drives the passive multiplier
	public int  Singularities   { get; set; } = 0;
	public bool SingularitySeen { get; set; } = false;  // has the collapse panel ever been opened

	// ── Upgrade levels ────────────────────────────────────────────
	// Keys are IdleUpgradeId.ToString(); missing key == level 0.
	public Dictionary<string, int> Upgrades { get; set; } = new();
	public Dictionary<string, int> Perks    { get; set; } = new();
	public Dictionary<string, int> Powers   { get; set; } = new();

	// ── Offline bookkeeping ───────────────────────────────────────
	public long   LastSeenUnix   { get; set; } = 0;
	public double MeasuredCps    { get; set; } = 0;   // smoothed cores/sec, used for offline payout
	public double BestCps        { get; set; } = 0;

	// Pending offline haul, staged so the welcome-back panel can show it
	public double PendingOfflineCores   { get; set; } = 0;
	public double PendingOfflineSeconds { get; set; } = 0;
	public int    PendingOfflineFrags   { get; set; } = 0;

	// ── Daily loop ────────────────────────────────────────────────
	public string LastDailyDate { get; set; } = "";
	public int    DailyStreak   { get; set; } = 0;
	public bool   DailyClaimed  { get; set; } = false;

	// ── Fragment drip (own bucket — does not touch the pet farm cap) ──
	public int    TodayReactorFragments { get; set; } = 0;
	public string LastFragDate          { get; set; } = "";
	public double FragProgress          { get; set; } = 0; // cores banked toward the next fragment

	// ── Records / milestones ──────────────────────────────────────
	public int    TotalDetonations { get; set; } = 0;
	public long   TotalChestsBlown { get; set; } = 0;
	public int    BestChain        { get; set; } = 0;
	public double BestSingleBlast  { get; set; } = 0;
	public int    GoldenGrabbed    { get; set; } = 0;
	public double SecondsRun       { get; set; } = 0;
	public List<string> Achievements { get; set; } = new();

	// Counters that exist purely so Daily Contracts have something to watch.
	// They are lifetime totals; a contract stores its own baseline at issue.
	public int TotalOverloads      { get; set; } = 0;
	public int TotalSurges         { get; set; } = 0;
	public int TotalUpgradesBought { get; set; } = 0;

	/// <summary>Longest chain since the contract set was last issued. Reset with it.</summary>
	public int BestChainToday { get; set; } = 0;

	// ── Daily contracts ───────────────────────────────────────────
	// Parallel lists rather than a list of objects: the save is plain JSON and
	// a schema change to a nested type would break every existing file.
	public string       ContractDate     { get; set; } = "";
	public List<string> ContractIds      { get; set; } = new();
	public List<double> ContractTargets  { get; set; } = new();
	public List<double> ContractBaseline { get; set; } = new();
	public List<bool>   ContractClaimed  { get; set; } = new();

	/// <summary>Consecutive days on which all three contracts were cleared.</summary>
	public int    ContractStreak      { get; set; } = 0;
	public string ContractStreakDate  { get; set; } = "";

	// ── Upgrade helpers ───────────────────────────────────────────
	public int Level( IdleUpgradeId id )
		=> Upgrades.TryGetValue( id.ToString(), out int v ) ? v : 0;

	public void SetLevel( IdleUpgradeId id, int level )
		=> Upgrades[id.ToString()] = Math.Max( 0, level );

	public int PerkLevel( IdlePerkId id )
		=> Perks.TryGetValue( id.ToString(), out int v ) ? v : 0;

	public void SetPerkLevel( IdlePerkId id, int level )
		=> Perks[id.ToString()] = Math.Max( 0, level );

	public int PowerLevel( IdlePowerId id )
		=> Powers != null && Powers.TryGetValue( id.ToString(), out int v ) ? v : 0;

	public void SetPowerLevel( IdlePowerId id, int level )
	{
		Powers ??= new();
		Powers[id.ToString()] = Math.Max( 0, level );
	}

	public bool HasAchievement( string id ) => Achievements.Contains( id );

	// ── Meltdown (prestige) ───────────────────────────────────────
	/// <summary>Wipes the run-scoped economy. Flux, perks, records and achievements stay.</summary>
	public void ResetForMeltdown()
	{
		Cores      = 0;
		CycleCores = 0;
		Upgrades.Clear();
		Meltdowns++;

		// Kickstart perk hands back a head start so the early grind shrinks each time
		int kick = PerkLevel( IdlePerkId.Kickstart );
		if ( kick > 0 )
		{
			SetLevel( IdleUpgradeId.ChestValue, kick * 6 );
			SetLevel( IdleUpgradeId.SpawnRate,  kick * 4 );
			SetLevel( IdleUpgradeId.CycleSpeed, kick * 4 );
			SetLevel( IdleUpgradeId.DroneCount, Math.Min( kick, 3 ) );
		}
	}

	// ── Singularity (second prestige) ─────────────────────────────
	/// <summary>
	/// Collapses everything a Meltdown normally protects — Flux, lifetime Flux
	/// and the whole perk tree. Shards, Powers, records, achievements, Fragments
	/// and the pet survive; those are what carry the run forward.
	///
	/// Meltdowns is deliberately kept: the Reactor Log counts it, and wiping a
	/// lifetime tally the player earned honestly reads as a bug, not a cost.
	/// </summary>
	public void ResetForSingularity()
	{
		Flux       = 0;
		FluxEarned = 0;
		Perks.Clear();

		// Kickstart is gone with the perks, so ResetForMeltdown lands us at a
		// genuine level-zero board — which is the point of the layer.
		ResetForMeltdown();
		Meltdowns--;   // ResetForMeltdown counts a Meltdown that did not happen

		Singularities++;
	}

	// ── Day rollover for the fragment bucket ──────────────────────
	public void EnsureFragDay()
	{
		string today = DateTime.UtcNow.ToString( "yyyy-MM-dd" );
		if ( LastFragDate == today ) return;
		LastFragDate = today;
		TodayReactorFragments = 0;
	}

	public int FragmentsToday
	{
		get { EnsureFragDay(); return TodayReactorFragments; }
	}
}