Shop.cs
namespace NoChillquarium;
/// <summary>
/// Shop spends: fish, capacity, habitats, salt tank, meth, M80.
/// </summary>
public static class Shop
{
public const int CapacityStep = 2;
public const double BaseCapacityCost = 28;
/// <summary>Ð added per slot already past the habitat floor.</summary>
public const double CapacityCostPerExtra = 18;
public const double MethCost = 45;
public const double M80Cost = 28;
public const double SaltTankCost = TankSim.SaltTankCost;
static int _version;
static int _methBags;
static int _m80Bags;
public static int Version => _version;
public static int MethBags => _methBags;
public static int M80Bags => _m80Bags;
/// <summary>MainShell sets this while the laptop shop is open. Gifts wait.</summary>
public static bool UiOpen { get; set; }
public static string LastMessage { get; private set; } = "Pick a pack.";
public static void SetLastMessage( string msg )
{
LastMessage = msg ?? "";
_version++;
}
public static double CapacityCost =>
BaseCapacityCost + Math.Max( 0, TankSim.Capacity - TankSim.DefaultCapacity ) * CapacityCostPerExtra;
public static bool CanBuyFish( FishSpecies species ) =>
species is not null
&& TankSim.IsActive
&& FishSpecies.IsShopUnlocked( species )
&& Economy.Balance + 0.001 >= FishPurchaseTotal( species );
/// <summary>Fish price, plus a capacity bump if this tank is packed.</summary>
public static double FishPurchaseTotal( FishSpecies species )
{
if ( species is null )
return 0;
var total = species.BuyCost;
if ( TankSim.IsActive && TankSim.FishCount >= TankSim.Capacity )
total += CapacityCost;
return total;
}
public static string FishActLabel( FishSpecies species )
{
if ( species is null || !TankSim.IsActive )
return "—";
if ( !FishSpecies.IsShopUnlocked( species ) )
return "Soon";
if ( CanBuyFish( species ) )
return TankSim.FishCount >= TankSim.Capacity ? "Expand" : "Buy";
if ( TankSim.FishCount >= TankSim.Capacity )
return "Full";
return "Need Ð";
}
public static string FishBuyBlockedReason( FishSpecies species )
{
if ( species is null )
return "Invalid";
if ( !TankSim.IsActive )
return "No tank";
if ( !FishSpecies.IsShopUnlocked( species ) )
return FishSpecies.ShopUnlockHint( species );
if ( TankSim.FishCount >= TankSim.Capacity )
{
var need = CapacityCost + species.BuyCost;
if ( Economy.Balance + 0.001 < need )
return "Tank full · +2 slots Ð" + Economy.FormatDoge( CapacityCost );
return "";
}
if ( Economy.Balance < species.BuyCost )
return $"Need Ð{Economy.FormatDoge( species.BuyCost )}";
return "";
}
public static bool CanBuyCapacity() =>
TankSim.IsActive && Economy.Balance >= CapacityCost;
public static bool CanBuySaltTank() =>
TankSim.IsActive && !TankSim.SaltUnlocked && Economy.Balance >= SaltTankCost;
public static HabitatDef NextHabitatOffer => TankSim.NextHabitat;
public static string HabitatUnlockText( HabitatDef def )
{
if ( def is null )
return "";
if ( Economy.PlaytimeSeconds >= def.UnlockPlaytime )
return def.Note;
var left = def.UnlockPlaytime - Economy.PlaytimeSeconds;
var totalMin = (int)(left / 60f);
if ( totalMin >= 60 )
{
var h = totalMin / 60;
var m = totalMin % 60;
return m > 0 ? $"Unlocks in {h}h {m}m" : $"Unlocks in {h}h";
}
var s = (int)(left % 60f);
return $"Unlocks in {totalMin}:{s:00}";
}
public static bool CanBuyHabitat( HabitatDef def ) =>
def is not null
&& TankSim.CanUpgradeTo( def )
&& Economy.Balance >= def.Cost;
public static bool CanBuyMeth() =>
TankSim.IsActive && Economy.Balance >= MethCost;
public static bool CanBuyM80() =>
TankSim.IsActive && Economy.Balance >= M80Cost;
public static bool CanFeedMeth() =>
TankSim.IsActive && _methBags > 0;
public static bool CanArmM80() =>
TankSim.IsActive && _m80Bags > 0;
public static void ResetInventory()
{
_methBags = 0;
_m80Bags = 0;
LastMessage = "Pick a pack.";
DarkWeb.Clear();
_version++;
}
public static void LoadInventory( int methBags, int m80Bags = 0 )
{
_methBags = Math.Max( 0, methBags );
_m80Bags = Math.Max( 0, m80Bags );
_version++;
}
public static void WriteInventory( SaveData data )
{
if ( data is not null )
{
data.MethBags = _methBags;
data.M80Bags = _m80Bags;
}
}
public static bool TryBuyFish( FishSpecies species )
{
if ( species is null || !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
if ( !FishSpecies.IsShopUnlocked( species ) )
{
Fail( FishSpecies.ShopUnlockHint( species ) );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
var expanded = false;
if ( TankSim.FishCount >= TankSim.Capacity )
{
var cap = CapacityCost;
if ( Economy.Balance + 0.001 < cap + species.BuyCost )
{
Fail( "Tank full. +2 slots Ð" + Economy.FormatDoge( cap )
+ " · then Ð" + Economy.FormatDoge( species.BuyCost ) + " for the fish." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Economy.TrySpend( cap ) )
{
Fail( "Tank full. Need Ð" + Economy.FormatDoge( cap ) + " to add slots." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
TankSim.IncreaseCapacity( CapacityStep );
expanded = true;
}
if ( !Economy.TrySpend( species.BuyCost ) )
{
Fail( expanded
? $"+{CapacityStep} slots bought. Need Ð{Economy.FormatDoge( species.BuyCost )} for the fish."
: $"Need Ð{Economy.FormatDoge( species.BuyCost )}." );
GameAudio.PlayUi( GameAudio.Error );
_version++;
return false;
}
if ( !TankSim.Spawn( species ) )
{
Economy.Add( species.BuyCost );
Fail( "Could not spawn fish." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
var rarity = TankSim.LastSpawnRarity;
var warn = species.Water != TankSim.Water && species.Water != WaterType.Any
? " · WRONG WATER ⚠"
: "";
LastMessage = expanded
? $"+{CapacityStep} slots · unfolding {species.Name} · Ð{Economy.FormatDoge( CapacityCost + species.BuyCost )}{warn}"
: $"Unfolding {species.Name}… Ð{Economy.FormatDoge( species.BuyCost )}{warn}";
// Juice + rarity stamp live on the card — don't spoil the flip here.
BuyReveal.Begin( species, rarity );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryBuyCapacity()
{
if ( !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
var cost = CapacityCost;
if ( !Economy.TrySpend( cost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( cost )} for +{CapacityStep} slots." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
TankSim.IncreaseCapacity( CapacityStep );
LastMessage = $"+{CapacityStep} capacity on {TankSim.TankName} (now {TankSim.Capacity}).";
GameAudio.PlayUi( GameAudio.Upgrade );
BoomFeel.SoftPunch( 0.3f );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryBuySaltTank()
{
if ( !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
if ( TankSim.SaltUnlocked )
{
Fail( "You already own a salt tank." );
return false;
}
if ( !Economy.TrySpend( SaltTankCost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( SaltTankCost )} for a salt tank." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
TankSim.TryUnlockSaltTank( announce: false );
LastMessage = $"Salt tank + clownfish · Ð{Economy.FormatDoge( SaltTankCost )}";
AchievementSystem.Unlock( "salt" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryBuyHabitat( HabitatDef def )
{
if ( def is null || !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
if ( !TankSim.CanUpgradeTo( def ) )
{
if ( Economy.PlaytimeSeconds < def.UnlockPlaytime )
Fail( HabitatUnlockText( def ) );
else if ( def.Rank != TankSim.HabitatRank + 1 )
Fail( "Buy the next habitat in order." );
else
Fail( "Can't move into that habitat yet." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Economy.TrySpend( def.Cost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !TankSim.TryUpgradeHabitat( def, announce: false ) )
{
Economy.Add( def.Cost );
Fail( "Habitat move failed." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
// Housewarming tip scales with rank so upgrades feel like progression.
var houseTip = Math.Round( 10 + def.Rank * 8.0, 1 );
Economy.AddForced( houseTip );
BoomFeel.SoftPunch( 0.5f + def.Rank * 0.08f );
LastMessage = $"Moved into {def.Name} · cap {TankSim.Capacity} · +Ð{Economy.FormatDoge( houseTip )}";
TankSim.ShowBanner( $"{def.Name}! Cap {TankSim.Capacity}. Housewarming +Ð{Economy.FormatDoge( houseTip )}" );
GameAudio.PlayBigCash();
if ( def.Rank == 1 )
GrandmaReact.OnFirstHabitat();
AchievementSystem.Unlock( "habitat" );
Chaos.Add( 2f + def.Rank * 0.5f );
Karma.Add( 3f + def.Rank * 0.5f ); // better homes = good-boy-ish
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryBuyMeth()
{
if ( !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
if ( !Economy.TrySpend( MethCost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( MethCost )} for meth." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
_methBags++;
LastMessage = $"Bought meth ×{_methBags} · Ð{Economy.FormatDoge( MethCost )}";
GameAudio.PlayUi( GameAudio.Trade );
Chaos.Add( 5f );
JanitorPeep.On( "meth" );
AchievementSystem.Unlock( "shady" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
public static bool TryBuyM80()
{
if ( !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
if ( !Economy.TrySpend( M80Cost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( M80Cost )} for an M80." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
_m80Bags++;
// First stick is a two-pack so shady unlock feels generous.
if ( _m80Bags == 1 )
_m80Bags = 2;
LastMessage = $"Bought M80 ×{_m80Bags} · Ð{Economy.FormatDoge( M80Cost )}";
GameAudio.PlayUi( GameAudio.Trade );
Chaos.Add( 3f );
AchievementSystem.Unlock( "shady" );
StorySystem.Evaluate();
SaveGame.TrySave();
_version++;
return true;
}
/// <summary>Spend M80 stock when detonating (1 per taped fish).</summary>
public static bool TryConsumeM80( int count )
{
if ( count <= 0 )
return true;
if ( _m80Bags < count )
return false;
_m80Bags -= count;
_version++;
return true;
}
/// <summary>Fight loot / hush money / drawer swipe — no shop banner.</summary>
public static void GrantM80( int count )
{
if ( count <= 0 )
return;
_m80Bags += count;
_version++;
}
public static void GrantMeth( int count )
{
if ( count <= 0 )
return;
_methBags += count;
_version++;
}
public static bool TryTakeMeth( int count = 1 )
{
if ( count <= 0 )
return true;
if ( _methBags < count )
return false;
_methBags -= count;
_version++;
return true;
}
public static bool TryFeedMeth()
{
if ( !CanFeedMeth() )
{
Fail( "No meth bags. Buy some in Shady." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
_methBags--;
var n = TankSim.ApplyMethToActiveTank();
GameAudio.PlayUi( n > 0 ? GameAudio.Confirm : GameAudio.Error );
if ( n > 0 )
{
Karma.Add( -6f - n * 0.5f );
var saved = 0;
foreach ( var f in TankSim.Fish )
{
if ( f.OnMeth && f.IsWrongWater( TankSim.Water ) )
saved++;
}
// Contain the thrash or the glass pays for it.
MethRush.TryStart();
AchievementSystem.Unlock( "meth" );
}
SaveGame.TrySave();
_version++;
return true;
}
public static bool CanBuyDecor( DecorDef def )
{
if ( def is null || !TankSim.IsActive )
return false;
if ( def.IsBackdrop )
{
var hung = TankSim.ActiveBackdropId;
if ( def.Id == DecorDef.BackdropNone.Id )
return !string.IsNullOrEmpty( hung );
if ( string.Equals( hung, def.Id, StringComparison.Ordinal ) )
return false;
return Economy.Balance >= def.Cost;
}
if ( def.IsSubstrate )
{
var laid = TankSim.ActiveGravelId;
if ( def.Id == DecorDef.GravelNone.Id )
return !string.IsNullOrEmpty( laid );
if ( string.Equals( laid, def.Id, StringComparison.Ordinal ) )
return false;
return Economy.Balance >= def.Cost;
}
return TankSim.DecorCount < TankSim.MaxDecor && Economy.Balance >= def.Cost;
}
public static bool TryBuyDecor( DecorDef def )
{
if ( def is null || !TankSim.IsActive )
{
Fail( "No tank open." );
return false;
}
if ( def.IsBackdrop )
{
var hung = TankSim.ActiveBackdropId;
if ( def.Id == DecorDef.BackdropNone.Id )
{
if ( string.IsNullOrEmpty( hung ) )
{
Fail( "Already plain glass." );
return false;
}
TankSim.TrySetBackdrop( DecorDef.BackdropNone, announce: false );
LastMessage = "Peeled the poster. Plain glass.";
GameAudio.PlayUi( GameAudio.Upgrade );
SaveGame.TrySave();
_version++;
return true;
}
if ( string.Equals( hung, def.Id, StringComparison.Ordinal ) )
{
Fail( $"{def.Name} is already hung." );
return false;
}
if ( !Economy.TrySpend( def.Cost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !TankSim.TrySetBackdrop( def, announce: false ) )
{
Economy.Add( def.Cost );
Fail( "Could not hang backdrop." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
LastMessage = $"Hung {def.Name} · Ð{Economy.FormatDoge( def.Cost )}";
GameAudio.PlayUi( GameAudio.Upgrade );
Karma.Add( def.ChaosOnPlace > 0f ? -1f : 1.2f );
SaveGame.TrySave();
_version++;
return true;
}
if ( def.IsSubstrate )
{
var laid = TankSim.ActiveGravelId;
if ( def.Id == DecorDef.GravelNone.Id )
{
if ( string.IsNullOrEmpty( laid ) )
{
Fail( "Already lot dirt." );
return false;
}
TankSim.TrySetGravel( DecorDef.GravelNone, announce: false );
LastMessage = "Scooped back to lot dirt.";
GameAudio.PlayUi( GameAudio.Upgrade );
SaveGame.TrySave();
_version++;
return true;
}
if ( string.Equals( laid, def.Id, StringComparison.Ordinal ) )
{
Fail( $"{def.Name} is already down." );
return false;
}
if ( !Economy.TrySpend( def.Cost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !TankSim.TrySetGravel( def, announce: false ) )
{
Economy.Add( def.Cost );
Fail( "Could not dump gravel." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
LastMessage = $"Dumped {def.Name} · Ð{Economy.FormatDoge( def.Cost )}";
GameAudio.PlayUi( GameAudio.Upgrade );
Karma.Add( def.ChaosOnPlace > 0f ? -0.4f : 0.6f );
SaveGame.TrySave();
_version++;
return true;
}
if ( TankSim.DecorCount >= TankSim.MaxDecor )
{
Fail( $"Decor full ({TankSim.MaxDecor} max on this tank)." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
if ( !Economy.TrySpend( def.Cost ) )
{
Fail( $"Need Ð{Economy.FormatDoge( def.Cost )} for {def.Name}." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
// Shop owns the status line — no tank banner (avoids double toast + layout thrash).
if ( !TankSim.TryPlaceDecor( def, announce: false ) )
{
Economy.Add( def.Cost );
Fail( "Could not place decor." );
GameAudio.PlayUi( GameAudio.Error );
return false;
}
LastMessage = $"Placed {def.Name} · Ð{Economy.FormatDoge( def.Cost )}";
GameAudio.PlayUi( GameAudio.Upgrade );
Karma.Add( def.ChaosOnPlace > 0f ? -1f : 1.5f );
SaveGame.TrySave();
_version++;
return true;
}
static void Fail( string msg )
{
LastMessage = msg;
_version++;
}
/// <summary>Set shop banner from other systems (WMD etc.).</summary>
public static void FailPublic( string msg ) => Fail( msg );
public static void OkPublic( string msg )
{
LastMessage = msg ?? "";
_version++;
}
}