Shop.cs

Static Shop class for an in-game store. It exposes purchase checks and buy actions for fish, capacity, habitats, salt tank, meth and M80 items, tracks small player-side inventory (meth/M80 bags) and last UI message, and triggers economy, tank and game-state side effects and saves.

File Access
namespace NoChillquarium;

/// <summary>
/// Shop spends: fish, capacity, habitats, salt tank, meth, M80.
/// </summary>
public static class Shop
{
	public const int CapacityStep = 5;
	public const double BaseCapacityCost = 50;
	public const double MethCost = 28;
	public const double M80Cost = 18;
	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;
	public static string LastMessage { get; private set; } = "Click a row.";

	public static void SetLastMessage( string msg )
	{
		LastMessage = msg ?? "";
		_version++;
	}

	public static double CapacityCost =>
		BaseCapacityCost + Math.Max( 0, TankSim.Capacity - TankSim.DefaultCapacity ) * 15.0;

	public static bool CanBuyFish( FishSpecies species ) =>
		species is not null
		&& TankSim.IsActive
		&& TankSim.FishCount < TankSim.Capacity
		&& Economy.Balance >= species.BuyCost;

	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 = "Click a row.";
		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 ( TankSim.FishCount >= TankSim.Capacity )
		{
			Fail( "Tank full. Buy capacity." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		if ( !Economy.TrySpend( species.BuyCost ) )
		{
			Fail( $"Need Ð{Economy.FormatDoge( species.BuyCost )}." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		if ( !TankSim.Spawn( species ) )
		{
			Economy.Add( species.BuyCost );
			Fail( "Could not spawn fish." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		var warn = species.Water != TankSim.Water && species.Water != WaterType.Any
			? " · WRONG WATER ⚠"
			: "";
		LastMessage = $"Bought {species.Name} · Ð{Economy.FormatDoge( species.BuyCost )}{warn}";
		GameAudio.PlayUi( GameAudio.Trade );
		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 );
		Achievements.OnCapacityUpgraded();
		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();
		LastMessage = $"Saltwater tank purchased for Ð{Economy.FormatDoge( SaltTankCost )}.";
		GameAudio.PlayUi( GameAudio.Upgrade );
		Achievements.OnSaltUnlocked();
		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 ) )
		{
			Economy.Add( def.Cost );
			Fail( "Habitat move failed." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		LastMessage = $"Moved into {def.Name} · cap {TankSim.Capacity} · ×{def.IncomeMult:0.##} income.";
		GameAudio.PlayUi( GameAudio.Upgrade );
		Chaos.Add( 2f + def.Rank * 0.5f );
		Karma.Add( 3f + def.Rank * 0.5f ); // better homes = good-boy-ish
		Achievements.OnHabitatUpgraded( def );
		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 );
		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++;
		LastMessage = $"Bought M80 ×{_m80Bags} · Ð{Economy.FormatDoge( M80Cost )}";
		GameAudio.PlayUi( GameAudio.Trade );
		Chaos.Add( 3f );
		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;
	}

	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++;
			}
			Achievements.OnMethFed( saved );
			// Contain the thrash or the glass pays for it.
			MethRush.TryStart();
		}
		SaveGame.TrySave();
		_version++;
		return true;
	}

	public static bool CanBuyDecor( DecorDef def ) =>
		def is not null
		&& TankSim.IsActive
		&& 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 ( 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;
		}

		if ( !TankSim.TryPlaceDecor( def ) )
		{
			Economy.Add( def.Cost );
			Fail( "Could not place decor." );
			GameAudio.PlayUi( GameAudio.Error );
			return false;
		}

		LastMessage = $"Placed {def.Name} for Ð{Economy.FormatDoge( def.Cost )}.";
		GameAudio.PlayUi( GameAudio.Upgrade );
		Karma.Add( def.ChaosOnPlace > 0f ? -1f : 1.5f );
		Achievements.OnDecorPlaced();
		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++;
	}
}