GrandmaReact.cs
namespace NoChillquarium;
/// <summary>
/// Mid-game porch reactions — one-shot dry-love Grandma lines on big firsts.
/// Queues if a gift/story card is open so nothing gets buried.
/// </summary>
public static class GrandmaReact
{
static readonly HashSet<string> _said = new( StringComparer.OrdinalIgnoreCase );
static string _pendingId = "";
static string _pendingLine = "";
/// <summary>Let system banners show first (train unlock, shady tab, fight payout).</summary>
static float _holdTimer;
static int _version;
public static int Version => _version;
// ---- Milestone ids (save as strings) ----
public const string FirstFeed = "first_feed";
public const string FirstTrain = "first_train";
public const string FirstAbom = "first_abom";
public const string FirstFightWin = "first_fight_win";
public const string FirstFightLoss = "first_fight_loss";
public const string FirstChamp = "first_champ";
public const string FirstDeath = "first_death";
public const string FirstSharkKill = "first_shark_kill";
public const string FirstSalt = "first_salt";
public const string FirstHabitat = "first_habitat";
public const string FirstTankKick = "first_tank_kick";
public const string FirstJanitorMeth = "first_janitor_meth";
public const string RobbedTin = "robbed_tin";
public const string DosedPipes = "dosed_pipes";
public const string GiftedMeth = "gifted_meth";
public const string FirstRansack = "first_ransack";
public const string FirstSell = "first_sell";
public const string FirstPainted = "first_painted";
public const string FirstGolden = "first_golden";
public const string FirstRainbow = "first_rainbow";
public const string FirstFlush = "first_flush";
public static void ResetForNewGame()
{
_said.Clear();
_pendingId = "";
_pendingLine = "";
_holdTimer = 0f;
_version++;
}
public static void Clear()
{
_pendingId = "";
_pendingLine = "";
_holdTimer = 0f;
_version++;
}
public static void Load( SaveData data )
{
_said.Clear();
_pendingId = "";
_pendingLine = "";
_holdTimer = 0f;
if ( data?.GrandmaReactSaid is null )
{
// Old saves: treat past progress as already "heard" so we don't spam.
SeedFromProgress( data );
_version++;
return;
}
foreach ( var id in data.GrandmaReactSaid )
{
if ( !string.IsNullOrWhiteSpace( id ) )
_said.Add( id.Trim() );
}
_version++;
}
public static void WriteToSave( SaveData data )
{
if ( data is null )
return;
data.GrandmaReactSaid = _said.ToList();
}
/// <summary>Call from play tick — flush queued quip when UI is quiet.</summary>
public static void Tick( float dt )
{
if ( string.IsNullOrEmpty( _pendingId ) )
return;
if ( _holdTimer > 0f )
{
_holdTimer -= MathF.Max( 0f, dt );
return;
}
if ( IsUiBlocking() )
return;
var id = _pendingId;
var line = _pendingLine;
_pendingId = "";
_pendingLine = "";
SpeakNow( id, line );
}
/// <summary>One-shot Grandma banner. Always queues briefly so system tips show first.</summary>
public static void Try( string id, string line )
{
if ( string.IsNullOrEmpty( id ) || string.IsNullOrEmpty( line ) )
return;
if ( _said.Contains( id ) )
return;
// Don't stack over an already-queued different quip — first wins.
if ( !string.IsNullOrEmpty( _pendingId ) )
return;
_pendingId = id;
_pendingLine = line;
// Let the player sit with the system tip + porch beat before dry love.
_holdTimer = IsUiBlocking() ? 0.8f : 4.5f;
_version++;
}
// ---- Named firsts (call sites stay thin) ----
public static void OnFirstFeed() =>
Try( FirstFeed, "Grandma: \"They're eating. Cute. Don't get a big head. Cookies after you buy another one. That's called providing.\"" );
public static void OnFirstTrain() =>
Try( FirstTrain, "Grandma: \"Nice sweat. Now go make Brad regret opening that garage. Love you. Wipe your face.\"" );
public static void OnFirstAbom() =>
Try( FirstAbom, "Grandma: \"Ugly little angel. Keep making those. I want a whole ugly choir. Bingo's gonna hate us.\"" );
public static void OnFirstFightWin() =>
Try( FirstFightWin, "Grandma: \"I told Brad my punk hits. You just proved it. Sit. Drink water. Don't gloat at bingo.\"" );
public static void OnFirstFightLoss() =>
Try( FirstFightLoss, "Grandma: \"Cute try. Ice them, train again, bite back. I'm making soup either way. Soup doesn't care about receipts.\"" );
public static void OnFirstChamp() =>
Try( FirstChamp, "Grandma: \"Belt's crooked. Fix it. Then try not to look so proud at bingo. I will look proud. You don't get to.\"" );
public static void OnFirstDeath() =>
Try( FirstDeath, "Grandma: \"Supper's on. Love you. Still your fault. Wipe the glass. We do not skip wipe the glass.\"" );
public static void OnFirstSharkKill() =>
Try( FirstSharkKill, "Grandma: \"You punched a cocaine shark. Bingo will hear about this. I'm so proud. And concerned. Mostly concerned. Sit.\"" );
public static void OnFirstSalt() =>
Try( FirstSalt, "Grandma: \"Two tanks. Fancy. Wrong water still means supper. I will say it at church. I will say it twice.\"" );
public static void OnFirstHabitat() =>
Try( FirstHabitat, "Grandma: \"Bigger glass. Still my porch. Don't flood the steps. Love you. Shoo. That's the whole speech.\"" );
public static void OnFirstTankKick() =>
Try( FirstTankKick, "Grandma: \"That's a living room. You just assaulted furniture. I'm making soup. The soup is disappointed. The soup has a lawyer.\"" );
public static void OnFirstJanitorMeth() =>
Try( FirstJanitorMeth, "Grandma: \"You tried to drug the help. He drinks gas-station coffee. I raised a punk, not a pharmacist. Sit. Think about that.\"" );
public static void OnRobbedTin() =>
Try( RobbedTin, "Grandma: \"That tin was for bingo and a funeral. I can still count. Put it back in spirit. Love you. Thief. Sit anyway.\"" );
public static void OnDosedPipes() =>
Try( DosedPipes, "Grandma: \"The toilet is not a laboratory. If the city calls I'm telling them your full government name. I remember it.\"" );
public static void OnGiftedMeth() =>
Try( GiftedMeth, "Grandma: \"I put it in the soup. The soup said no. The soup is grounded. You're still my idiot. Sit. Don't season anything.\"" );
public static void OnFirstRansack() =>
Try( FirstRansack, "Grandma: \"You robbed a garage. Brad will laminate it. I'm proud in a way that requires a lawyer. Drink water.\"" );
public static void OnFirstSell() =>
Try( FirstSell, "Grandma: \"That's budgeting. Keep the shiny ones. The tin likes both kinds of love. Don't make it a TED talk.\"" );
public static void OnFirstPainted() =>
Try( FirstPainted, "Grandma: \"Painted. Like a crime. Like a compliment. Name it. Don't sell it. I will know.\"" );
public static void OnFirstGolden() =>
Try( FirstGolden, "Grandma: \"Gold in my glass. Don't sell that one. I will know. Love you. That's not a request.\"" );
public static void OnFirstRainbow() =>
Try( FirstRainbow, "Grandma: \"That one looks like a fever. Keep it. Bingo would riot. I would let them.\"" );
public static void OnFirstFlush() =>
Try( FirstFlush, "Grandma: \"I heard the splash. I didn't look. The faucet's going to gossip. Love you. Menace. Shoo.\"" );
/// <summary>Keeper chase — one-shot per tier so the tin and the glass stay the same story.</summary>
public static void OnKeeper( FishRarity rarity )
{
if ( rarity >= FishRarity.Mythical )
OnFirstRainbow();
else if ( rarity >= FishRarity.Legendary )
OnFirstGolden();
else if ( rarity >= FishRarity.PaintedRare )
OnFirstPainted();
}
// ---- Internals ----
static bool IsUiBlocking() =>
GrandmaGifts.IsBusy
|| StorySystem.CardOpen
|| BattleSystem.Phase == BattlePhase.Result
|| BattleSystem.Phase == BattlePhase.Fighting;
static void SpeakNow( string id, string line )
{
if ( _said.Contains( id ) )
return;
_said.Add( id );
TankSim.ShowBanner( line );
_version++;
SaveGame.TrySave( quiet: true );
}
/// <summary>Continuing mid-empire: don't re-fire early tutorial quips.</summary>
static void SeedFromProgress( SaveData data )
{
if ( data is null )
return;
if ( data.StoryFedOnce || data.PlaytimeSeconds > 120f )
_said.Add( FirstFeed );
if ( data.StoryTrainedOnce )
_said.Add( FirstTrain );
if ( data.StoryCombinedOnce || data.AbominationsCreated > 0 )
_said.Add( FirstAbom );
if ( data.BattleWins > 0 )
_said.Add( FirstFightWin );
if ( data.BattleLosses > 0 )
_said.Add( FirstFightLoss );
if ( data.ChampionshipTitles > 0 )
_said.Add( FirstChamp );
if ( data.FishDeaths > 0 )
_said.Add( FirstDeath );
if ( data.SharkKills > 0 )
_said.Add( FirstSharkKill );
if ( data.SaltUnlocked )
_said.Add( FirstSalt );
if ( data.StorySoldOnce )
_said.Add( FirstSell );
if ( data.AdventureFlushed )
_said.Add( FirstFlush );
// Any non-starter habitat id means they already upgraded once.
if ( !string.IsNullOrEmpty( data.FreshHabitatId )
&& !string.Equals( data.FreshHabitatId, "tank_fresh_1", StringComparison.OrdinalIgnoreCase ) )
_said.Add( FirstHabitat );
else if ( !string.IsNullOrEmpty( data.SaltHabitatId )
&& !string.Equals( data.SaltHabitatId, "tank_salt_1", StringComparison.OrdinalIgnoreCase ) )
_said.Add( FirstHabitat );
}
}