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.
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"
};
}