ProgressionCoach.cs

A static helper that suggests the next player progression hint. It inspects many game systems (GrandmaGifts, TankSim, Shop, Economy, StorySystem, BattleSystem, etc.) and returns a Step with a kicker, text and kind describing what the player should do next.

Reflection
namespace NoChillquarium;

/// <summary>
/// One quest pill: “what now?” Prefer free play over rushing the next gift.
/// Ladder: care → combine → fight → adventure.
/// </summary>
public static class ProgressionCoach
{
	public sealed class Step
	{
		public string Kicker { get; init; }
		public string Text { get; init; }
		/// <summary>story · gift · action · expand · free</summary>
		public string Kind { get; init; }
	}

	public static Step Current
	{
		get
		{
			// Prologue only — then get out of the way.
			if ( !GrandmaGifts.HasReceived( "lot_establishing" ) )
				return StepOf( "Lot", "Walk home · form in hand", "gift" );
			if ( !GrandmaGifts.HasTank )
				return StepOf( "Lot", "Show Grandma the form", "gift" );

			// Early care — flakes come with the tank; first verb is Feed.
			if ( !FeedSystem.HasAnyFood )
				return StepOf( "Shop", "Shop → Food · Flakes", "action" );
			if ( TankSim.TotalFishCount == 0 )
				return StepOf( "Shop", "Shop → Fish · something wet", "action" );
			if ( !StorySystem.HasFedOnce )
				return StepOf( "Feed", "Tools → Feed · grow + Ð", "action" );

			// Soft gift readiness — never yell "incoming" while the gap is cooling.
			var gift = NextGiftStep();
			if ( gift is not null )
				return gift;

			// Capacity — only when blocked.
			if ( TankSim.FishCount >= TankSim.Capacity )
			{
				if ( Shop.CanBuyCapacity() )
					return StepOf( "Tank", "Full · Shop → +capacity", "expand" );
				return StepOf( "Tank", "Full · sell or earn for +capacity", "expand" );
			}

			// Explore before power ladder — first guppy is the dopamine hit.
			if ( TankSim.TotalFishCount < 3 && Economy.Balance + 0.001 >= FishSpecies.Guppy.BuyCost )
				return StepOf( "Grow", "Shop → Fish · guppy time", "free" );

			// Rare chase — pity getting hot or no rares yet.
			if ( StorySystem.HasFedOnce
				&& FishRarityInfo.CommonStreak >= 5
				&& Economy.Balance + 0.001 >= FishSpecies.Guppy.BuyCost
				&& TankSim.FishCount < TankSim.Capacity )
				return StepOf( "Rare", "Shop → Fish · pity warming · chase spice", "free" );

			// Breeding when adults sit fat and room exists.
			if ( TankSim.Water == WaterType.Fresh
				&& BreedingSystem.EligiblePairCount >= 1
				&& TankSim.FishCount < TankSim.Capacity
				&& BreedingSystem.Eggs.Count == 0
				&& Economy.PlaytimeSeconds >= 4f * 60f )
				return StepOf( "Breed", "Feed adults fat · eggs hatch guppies", "free" );

			// Train early — free skill that unlocks fight money.
			if ( Progression.TrainUnlocked && !StorySystem.HasTrainedOnce
				&& Economy.PlaytimeSeconds >= 35f )
				return StepOf( "Train", "Fish menu → Train · free stats", "action" );

			// Combine after janitor gift (or if they already combined).
			if ( GrandmaGifts.HasReceived( "janitor_fusion" )
				&& Progression.CombineUnlocked
				&& TankSim.TotalFishCount >= 2
				&& !StorySystem.HasCombinedOnce )
				return StepOf( "Combine", "Mash two fish · birth pays Ð", "action" );

			// Fight after Brad intro.
			if ( GrandmaGifts.HasReceived( "lot_bully_fight" )
				&& BattleSystem.Wins + BattleSystem.Losses == 0 )
			{
				if ( !Progression.FightUnlocked )
				{
					var fee = Progression.CheapestOpenEntryFee();
					if ( fee > 0.01 && Economy.Balance + 0.001 < fee )
						return StepOf( "Fight", "Earn Ð" + Economy.FormatDoge( fee ) + " entry", "action" );
					if ( !StorySystem.HasTrainedOnce )
						return StepOf( "Train", "Train once · then garage", "action" );
					return StepOf( "Fight", "Train · then pay entry", "action" );
				}
				return StepOf( "Fight", "Fish menu → Fight · ~2× purse", "action" );
			}

			// Adventure last.
			if ( LotRunSystem.Unlocked && !LotRunSystem.IsOpen )
			{
				if ( !LotRunSystem.HasFlushed )
					return StepOf( "Adventure", "Flush a trained fish · risk/reward", "action" );
				if ( !LotRunSystem.IsCleared( "toilet_pipes" ) )
					return StepOf( "Adventure", "Tools → Adventure · keep mapping", "action" );
			}

			// Soft mid-game loops with risk/reward flavor.
			if ( Progression.DecorShopUnlocked && TankSim.DecorCount == 0 && Economy.Balance >= 12 )
				return StepOf( "Decor", "Shop → Decor · porch glow-up", "free" );

			// Salt is the mid-session "new aquarium" hit after porch basics.
			if ( !TankSim.SaltUnlocked
				&& StorySystem.HasFedOnce
				&& (StorySystem.HasCombinedOnce || BattleSystem.Wins > 0 || Economy.PlaytimeSeconds >= 8f * 60f)
				&& Economy.Balance + 0.001 >= Shop.SaltTankCost )
				return StepOf( "Salt", "Shop → Tank · salt (+ free clown)", "expand" );

			// Next habitat when offered and affordable — housewarming pays Ð.
			var nextHab = TankSim.NextHabitat;
			if ( nextHab is not null
				&& !nextHab.FightPrizeOnly
				&& TankSim.CanUpgradeTo( nextHab )
				&& Economy.Balance + 0.001 >= nextHab.Cost )
				return StepOf( "Habitat", "Shop → Tank · " + nextHab.Name, "expand" );

			if ( Progression.ShadyUnlocked && Shop.M80Bags <= 0
				&& StorySystem.HasCombinedOnce
				&& Economy.Balance >= Shop.M80Cost )
				return StepOf( "Shady", "M80 · scrap failures for Ð", "free" );

			if ( Progression.ShadyUnlocked && Shop.MethBags <= 0
				&& StorySystem.HasCombinedOnce
				&& Economy.Balance >= Shop.MethCost
				&& Economy.PlaytimeSeconds >= 10f * 60f )
				return StepOf( "Shady", "Meth · wrong-water chaos + income", "free" );

			if ( BattleSystem.Wins >= 3 && BattleSystem.ChampionshipTitles == 0 )
			{
				var cup = BattleSystem.Find( "championship_cup" );
				if ( cup is not null && BattleSystem.IsUnlocked( cup ) )
					return StepOf( "Cup", "Abom Championship · pride path", "action" );
			}

			// Injury tax — nudge recovery instead of dead-end.
			if ( CountInjured() >= 2 && BattleSystem.Wins + BattleSystem.Losses > 0 )
				return StepOf( "Heal", "Injured fish cooling · train the rest", "free" );

			if ( EndingSystem.CanGoodBoy )
				return StepOf( "Ending", "Dock · Grandma's Pride", "story" );
			if ( EndingSystem.CanBadBoy )
				return StepOf( "Ending", "Dock · Your Fault", "story" );
			if ( EndingSystem.CanMixed )
				return StepOf( "Ending", "Dock · Wuss", "story" );

			// Almost-there endings — keep the arc visible so late game has a finish line.
			if ( EndingSystem.NearestEndingProgress >= 0.55f )
			{
				if ( BattleSystem.ChampionshipTitles < 1
					&& (TankSim.AbominationsCreated > 0 || TankSim.HasAnyMonster)
					&& Economy.PlaytimeSeconds >= 12f * 60f )
					return StepOf( "Pride", "Win Abom Championship · dock pride", "story" );
				if ( TankSim.FishDeaths >= 2 && Karma.Value <= 50f )
					return StepOf( "Blame", EndingSystem.BadHint, "story" );
				if ( BattleSystem.Wins + BattleSystem.Losses < 3
					&& Economy.LifetimeEarned >= 80
					&& Karma.Value <= 45f )
					return StepOf( "Wuss", EndingSystem.MixedHint, "story" );
			}

			// Ocean setpiece once you own it.
			if ( SharkSystem.IsOceanActive )
			{
				if ( SharkSystem.HasSharks )
					return StepOf( "Ocean", "Fend sharks · loot · or Scare", "action" );
				if ( SharkSystem.Kills == 0 && Economy.PlaytimeSeconds >= 30f )
					return StepOf( "Ocean", "Sharks inbound · train the pack", "free" );
			}

			if ( !string.IsNullOrEmpty( StorySystem.QuestHint ) && !StorySystem.IsDone )
				return StepOf( "Hint", StorySystem.QuestHint, "story" );

			return ExploreStep();
		}
	}

	/// <summary>Only surface gifts when they're actually ready (not while cooling down).</summary>
	static Step NextGiftStep()
	{
		if ( GrandmaGifts.IsBusy )
			return null;

		if ( !GrandmaGifts.HasReceived( "cold_wallet" ) )
		{
			if ( !StorySystem.HasFedOnce )
				return null;
			if ( IsGiftOnGap( "cold_wallet" ) || !IsGiftFullyReady( "cold_wallet" ) )
				return StepOf( "Porch", "Feed · grow · earn · explore", "free" );
			return StepOf( "Gift", "Grandma's got something…", "gift" );
		}

		if ( !GrandmaGifts.HasReceived( "janitor_fusion" ) && !StorySystem.HasCombinedOnce )
		{
			if ( !Progression.CombineUnlocked || TankSim.TotalFishCount < 2 )
				return null;
			if ( IsGiftOnGap( "janitor_fusion" ) || !IsGiftFullyReady( "janitor_fusion" ) )
				return ExploreStep();
			return StepOf( "Lot", "Someone at the window…", "gift" );
		}

		if ( !GrandmaGifts.HasReceived( "lot_bully_fight" )
			&& BattleSystem.Wins + BattleSystem.Losses == 0 )
		{
			if ( !( StorySystem.HasCombinedOnce || TankSim.AbominationsCreated > 0 ) )
				return null;
			if ( !StorySystem.HasTrainedOnce )
				return StepOf( "Train", "Train someone · Brad later", "action" );
			if ( IsGiftOnGap( "lot_bully_fight" ) || !IsGiftFullyReady( "lot_bully_fight" ) )
				return ExploreStep();
			return StepOf( "Brad", "4B wants a word…", "gift" );
		}

		if ( !GrandmaGifts.HasAdventureIntro && !LotRunSystem.HasFlushed )
		{
			if ( !StorySystem.HasCombinedOnce && TankSim.AbominationsCreated <= 0 )
				return null;
			if ( BattleSystem.Wins + BattleSystem.Losses < 1 )
				return null;
			if ( IsGiftOnGap( "adventure_intro" ) || !IsGiftFullyReady( "adventure_intro" ) )
				return ExploreStep();
			return StepOf( "Adventure", "Toilet's been staring…", "gift" );
		}

		return null;
	}

	static Step ExploreStep()
	{
		// Rotate soft free-play flavor — always a next click, never "just wait".
		var t = (int)(Economy.PlaytimeSeconds / 40f) % 10;
		return t switch
		{
			0 => StepOf( "Feed", "Tools → Feed · growth is money", "free" ),
			1 => StepOf( "Shop", "Buy guppies · rare chase " + FishRarityInfo.ShopOddsLine( TankSim.RarityLuck ), "free" ),
			2 => StepOf( "Train", "Fish menu → Train · free power", "free" ),
			3 => StepOf( "Sell", "Click fish → Sell grown commons", "free" ),
			4 => StepOf( "Combine", "Mash two fish · birth pays Ð", "free" ),
			5 => StepOf( "Fight", "Garage circuits · Rematch for grind", "free" ),
			6 => StepOf( "Breed", "Fat adults court · eggs → free guppies", "free" ),
			7 => StepOf( "Salt", TankSim.SaltUnlocked ? "Salt fish earn · match water" : "Save for salt tank", "free" ),
			8 => StepOf( "Habitat", "Bigger glass · housewarming Ð", "free" ),
			_ => StepOf( "Shady", "M80 scrap · meth chaos", "free" ),
		};
	}

	static int CountInjured()
	{
		var n = 0;
		foreach ( var f in TankSim.AllFish() )
		{
			if ( f is not null && f.IsFightInjured )
				n++;
		}
		return n;
	}

	static bool IsGiftOnGap( string giftId )
	{
		for ( var i = 0; i < GrandmaGifts.Catalog.Count; i++ )
		{
			var g = GrandmaGifts.Catalog[i];
			if ( g is not null && string.Equals( g.Id, giftId, StringComparison.OrdinalIgnoreCase ) )
				return GrandmaGifts.IsWaitingOnGiftGap( g );
		}
		return false;
	}

	static bool IsGiftFullyReady( string giftId )
	{
		for ( var i = 0; i < GrandmaGifts.Catalog.Count; i++ )
		{
			var g = GrandmaGifts.Catalog[i];
			if ( g is not null && string.Equals( g.Id, giftId, StringComparison.OrdinalIgnoreCase ) )
				return GrandmaGifts.IsReady( g );
		}
		return false;
	}

	static Step StepOf( string kicker, string text, string kind ) =>
		new()
		{
			Kicker = kicker ?? "",
			Text = text ?? "",
			Kind = kind ?? "free"
		};
}