Static helper that queues and shows one-shot "Grandma" banner lines for first-time milestones. It tracks which lines have been said, queues a pending line until UI is quiet, persists said IDs to SaveData, and seeds state from older saves or progress.
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 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. Good. Don't get cocky.\"" );
public static void OnFirstTrain() =>
Try( FirstTrain, "Grandma: \"Nice sweat. Now go make Brad regret opening that garage.\"" );
public static void OnFirstAbom() =>
Try( FirstAbom, "Grandma: \"Ugly little angel. Keep making those — I want more.\"" );
public static void OnFirstFightWin() =>
Try( FirstFightWin, "Grandma: \"I told Brad my punk hits. You just proved it.\"" );
public static void OnFirstFightLoss() =>
Try( FirstFightLoss, "Grandma: \"Cute try. Ice them, train again, and bite back.\"" );
public static void OnFirstChamp() =>
Try( FirstChamp, "Grandma: \"Belt's crooked. Fix it — and try not to look so proud.\"" );
public static void OnFirstDeath() =>
Try( FirstDeath, "Grandma: \"Supper's on. Love you. Still your fault.\"" );
public static void OnFirstSharkKill() =>
Try( FirstSharkKill, "Grandma: \"You punched a cocaine shark. Bingo will hear about this.\"" );
public static void OnFirstSalt() =>
Try( FirstSalt, "Grandma: \"Two tanks. Fancy. Wrong water still means supper.\"" );
public static void OnFirstHabitat() =>
Try( FirstHabitat, "Grandma: \"Bigger glass. Still my porch. Don't flood the steps.\"" );
// ---- 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 );
// 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 );
}
}