Achievement system for the game. Defines AchievementDef and a static AchievementSystem that holds the catalog of achievements, tracks unlocked ones, queues and displays toast notifications, saves/loads unlocked state, evaluates achievements from world systems, and exposes UI state and counters.
namespace NoChillquarium;
/// <summary>One porch trophy. Hint is what to do; punchline is the unlock toast.</summary>
public sealed class AchievementDef
{
public string Id { get; init; }
public string Name { get; init; }
/// <summary>Shown while locked — points at the next verb.</summary>
public string Hint { get; init; }
/// <summary>Toast line when it pops.</summary>
public string Punch { get; init; }
}
/// <summary>
/// Twenty gameplay trophies. They teach the ladder and celebrate the felonies.
/// </summary>
public static class AchievementSystem
{
public static IReadOnlyList<AchievementDef> Catalog { get; } = new[]
{
new AchievementDef
{
Id = "feed",
Name = "Papa Flakes",
Hint = "Tools → Feed. They have to eat.",
Punch = "They're eating. Don't get cocky."
},
new AchievementDef
{
Id = "train",
Name = "Swim Lessons",
Hint = "Click a fish → Train. Sweat first.",
Punch = "Nice sweat. Brad's garage is listening."
},
new AchievementDef
{
Id = "combine",
Name = "Two Become Wet",
Hint = "Click a fish → Combine. Mash two.",
Punch = "Ugly little angel. The janitor took notes."
},
new AchievementDef
{
Id = "fight",
Name = "Garage Receipt",
Hint = "Fish menu → Fight. Pay Brad's entry.",
Punch = "You showed up. He laminated it either way."
},
new AchievementDef
{
Id = "win",
Name = "Brad Cried",
Hint = "Win a fight in the garage.",
Punch = "He cried into a receipt. Fridge magnet pending."
},
new AchievementDef
{
Id = "flush",
Name = "Municipal Directions",
Hint = "Click a fish → Flush. Adventure starts dirty.",
Punch = "Down. Down. Down. The pipes said thank you."
},
new AchievementDef
{
Id = "pipes",
Name = "Adventure Mode",
Hint = "Clear a crawl. Die = faucet home.",
Punch = "You mapped a hole. Grandma is pretending not to know."
},
new AchievementDef
{
Id = "sell",
Name = "Cash for Wet",
Hint = "Fish menu → Sell. Flip a common.",
Punch = "Yard sale energy. Very American."
},
new AchievementDef
{
Id = "decor",
Name = "Tasteful Junk",
Hint = "Shop → Decor. Put something in the glass.",
Punch = "Interior design. The fish did not consent."
},
new AchievementDef
{
Id = "habitat",
Name = "Bigger Bowl",
Hint = "Shop → Tank. Upgrade the habitat.",
Punch = "Bigger glass. Still her porch."
},
new AchievementDef
{
Id = "salt",
Name = "Second Fridge",
Hint = "Shop → Tank → Salt. Clownfish come free.",
Punch = "Two tanks. Wrong water still means supper."
},
new AchievementDef
{
Id = "shady",
Name = "Science Illegal",
Hint = "Shop → Shady. Buy an M80 or a bag.",
Punch = "The laptop made a noise. The noise has a mop."
},
new AchievementDef
{
Id = "tape",
Name = "Duct Tape Solutions",
Hint = "Tools → M80 → click a fish.",
Punch = "You taped an M80 to a coworker. HR is the janitor."
},
new AchievementDef
{
Id = "boom",
Name = "Indoor Fireworks",
Hint = "Detonate a taped M80.",
Punch = "Pop. Quality control. Grandma: \"That one had potential.\""
},
new AchievementDef
{
Id = "furniture",
Name = "Fish + Furniture",
Hint = "Combine a fish with a tank prop.",
Punch = "That's a flamingo with gills. That's a sentence."
},
new AchievementDef
{
Id = "meth",
Name = "Breaking Bass",
Hint = "Tools → Meth. Dose the glass.",
Punch = "Wrong water, right snack. Contain the thrash."
},
new AchievementDef
{
Id = "janitor",
Name = "Folgers Has a Lawyer",
Hint = "BAD or click the janitor. Offer meth.",
Punch = "He refused. Caffeine is his manager."
},
new AchievementDef
{
Id = "kick",
Name = "Assault Furniture",
Hint = "Tools → Kick. The glass remembers.",
Punch = "You kicked a living room. The gravel filed a complaint."
},
new AchievementDef
{
Id = "tin",
Name = "Funeral Funds",
Hint = "BAD → Rob the funeral tin.",
Punch = "Grandpa's leftover hash. I mean cash. She can still count."
},
new AchievementDef
{
Id = "dead",
Name = "Still Your Fault",
Hint = "Let one die. Wrong water works.",
Punch = "Supper's on. Love you. Wipe the glass."
},
};
const float ToastSeconds = 4.2f;
static readonly HashSet<string> _unlocked = new( StringComparer.OrdinalIgnoreCase );
static readonly Queue<string> _toastQ = new();
static string _toastId = "";
static float _toastLeft;
static bool _boardOpen;
static int _version;
public static int Version => _version;
public static int UnlockedCount => _unlocked.Count;
public static int Total => Catalog.Count;
public static string ProgressLabel => UnlockedCount + "/" + Total;
public static bool BoardOpen => _boardOpen;
public static bool ToastVisible => _toastLeft > 0f && !string.IsNullOrEmpty( _toastId );
public static AchievementDef ToastDef => Find( _toastId );
public static bool IsUnlocked( string id ) =>
!string.IsNullOrEmpty( id ) && _unlocked.Contains( id );
public static AchievementDef Find( string id )
{
if ( string.IsNullOrEmpty( id ) )
return null;
foreach ( var d in Catalog )
{
if ( string.Equals( d.Id, id, StringComparison.OrdinalIgnoreCase ) )
return d;
}
return null;
}
public static void ResetForNewGame()
{
_unlocked.Clear();
_toastQ.Clear();
_toastId = "";
_toastLeft = 0f;
_boardOpen = false;
_version++;
}
public static void Clear()
{
_toastQ.Clear();
_toastId = "";
_toastLeft = 0f;
_boardOpen = false;
_version++;
}
public static void Load( SaveData data )
{
_unlocked.Clear();
_toastQ.Clear();
_toastId = "";
_toastLeft = 0f;
_boardOpen = false;
if ( data?.AchievementsUnlocked is not null )
{
foreach ( var id in data.AchievementsUnlocked )
{
if ( !string.IsNullOrWhiteSpace( id ) && Find( id.Trim() ) is not null )
_unlocked.Add( id.Trim() );
}
}
_version++;
Evaluate( silent: true );
}
public static void WriteToSave( SaveData data )
{
if ( data is null )
return;
data.AchievementsUnlocked = _unlocked.ToList();
}
public static void ToggleBoard()
{
if ( !GrandmaGifts.HasTank )
return;
_boardOpen = !_boardOpen;
GameAudio.PlayUi( _boardOpen ? GameAudio.Notice : GameAudio.Close );
_version++;
}
public static void CloseBoard()
{
if ( !_boardOpen )
return;
_boardOpen = false;
_version++;
}
public static void Tick( float dt )
{
if ( dt <= 0f )
return;
if ( _toastLeft > 0f )
{
_toastLeft -= dt;
if ( _toastLeft <= 0f )
{
_toastLeft = 0f;
_toastId = "";
_version++;
TryShowNextToast();
}
}
// Cheap poll so we don't miss a flag that fired without a hook.
if ( TankSim.IsActive && GrandmaGifts.HasTank )
Evaluate( silent: false );
}
public static bool Unlock( string id, bool silent = false )
{
if ( string.IsNullOrEmpty( id ) )
return false;
if ( Find( id ) is null )
return false;
if ( !_unlocked.Add( id ) )
return false;
if ( !silent )
{
_toastQ.Enqueue( id );
if ( !ToastVisible )
TryShowNextToast();
SaveGame.TrySave( quiet: true );
}
_version++;
return true;
}
/// <summary>Backfill from world state. Silent on load so we don't spam a mid-empire save.</summary>
public static void Evaluate( bool silent )
{
if ( !TankSim.IsActive || !GrandmaGifts.HasTank )
return;
if ( StorySystem.HasFedOnce )
Unlock( "feed", silent );
if ( StorySystem.HasTrainedOnce )
Unlock( "train", silent );
if ( StorySystem.HasCombinedOnce || TankSim.AbominationsCreated > 0 )
Unlock( "combine", silent );
if ( BattleSystem.Wins + BattleSystem.Losses > 0 )
Unlock( "fight", silent );
if ( BattleSystem.Wins > 0 )
Unlock( "win", silent );
if ( LotRunSystem.HasFlushed )
Unlock( "flush", silent );
if ( AnyMapCleared() )
Unlock( "pipes", silent );
if ( StorySystem.HasSoldOnce )
Unlock( "sell", silent );
if ( TankSim.DecorCount > 0 || !string.IsNullOrEmpty( TankSim.ActiveBackdropId ) )
Unlock( "decor", silent );
if ( TankSim.HabitatRank > 0 )
Unlock( "habitat", silent );
if ( TankSim.SaltUnlocked )
Unlock( "salt", silent );
if ( Shop.M80Bags > 0 || Shop.MethBags > 0 || StorySystem.HasBoomOnce )
Unlock( "shady", silent );
if ( StorySystem.HasBoomOnce )
Unlock( "boom", silent );
if ( CrimeSystem.JanitorOffers > 0 )
Unlock( "janitor", silent );
if ( CrimeSystem.Kicks > 0 )
Unlock( "kick", silent );
if ( CrimeSystem.RobbedTin )
Unlock( "tin", silent );
if ( TankSim.FishDeaths > 0 )
Unlock( "dead", silent );
}
static bool AnyMapCleared()
{
foreach ( var loc in LotLocationDef.Catalog )
{
if ( LotRunSystem.IsCleared( loc.Id ) )
return true;
}
return false;
}
static void TryShowNextToast()
{
if ( ToastVisible || _toastQ.Count == 0 )
return;
_toastId = _toastQ.Dequeue();
_toastLeft = ToastSeconds;
GameAudio.PlayUi( GameAudio.Achievement );
_version++;
}
}