HabitatDef.cs

Definition of habitat upgrade data for a game. It declares the HabitatDef record-like class with properties for id, name, rank, water type, capacities, cost, unlock time, income multiplier and a set of static predefined habitat instances plus helper lookup methods (Catalog, Find, StarterFor, NextFor, MatchesTank).

File Access
namespace NoChillquarium;

/// <summary>
/// Habitat / container upgrade. Water type of the tank slot is unchanged —
/// habitats are bigger (or funnier) homes with more slots and flavor.
/// </summary>
public sealed class HabitatDef
{
	public string Id { get; init; }
	public string Name { get; init; }
	public string Note { get; init; }
	/// <summary>Upgrade rank within a chain (0 = starter).</summary>
	public int Rank { get; init; }
	/// <summary>Which tank slot may buy this (Any = both).</summary>
	public WaterType Water { get; init; }
	public int BaseCapacity { get; init; }
	public int MaxDecor { get; init; }
	public double Cost { get; init; }
	/// <summary>Playtime seconds before shop offers this upgrade.</summary>
	public float UnlockPlaytime { get; init; }
	/// <summary>Multiplies fish + decor income on this tank.</summary>
	public float IncomeMult { get; init; }
	/// <summary>CSS class on tank-glass (e.g. hab-bathtub).</summary>
	public string CssClass { get; init; }
	public string Banner { get; init; }
	/// <summary>Only awarded by winning fights — hidden from shop upgrade chain.</summary>
	public bool FightPrizeOnly { get; init; }

	// --- Fresh chain ---
	// Caps stay modest early so +slots and fish buys stay meaningful.
	public static readonly HabitatDef StarterFresh = new()
	{
		Id = "tank_fresh_1",
		Name = "Trailer Lot Tank",
		Note = "porch glass",
		Rank = 0,
		Water = WaterType.Fresh,
		BaseCapacity = 4,
		MaxDecor = 6,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1f,
		CssClass = "hab-glass",
		Banner = "Trailer lot tank online."
	};

	public static readonly HabitatDef DeluxeFresh = new()
	{
		Id = "tank_fresh_2",
		Name = "Deluxe Fresh",
		Note = "roomier glass",
		Rank = 1,
		Water = WaterType.Fresh,
		BaseCapacity = 10,
		MaxDecor = 8,
		Cost = 220,
		UnlockPlaytime = 10f * 60f, // ~10 min — mid-session goal, not a wall
		IncomeMult = 1.15f,
		CssClass = "hab-deluxe",
		Banner = "Deluxe glass."
	};

	// --- Salt chain start ---
	public static readonly HabitatDef StarterSalt = new()
	{
		Id = "tank_salt_1",
		Name = "Saltwater Tank",
		Note = "starter reef",
		Rank = 0,
		Water = WaterType.Salt,
		BaseCapacity = 4,
		MaxDecor = 6,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1f,
		CssClass = "hab-glass",
		Banner = "Salt glass online."
	};

	public static readonly HabitatDef DeluxeSalt = new()
	{
		Id = "tank_salt_2",
		Name = "Deluxe Reef",
		Note = "bigger salt box",
		Rank = 1,
		Water = WaterType.Salt,
		BaseCapacity = 10,
		MaxDecor = 8,
		Cost = 240,
		UnlockPlaytime = 10f * 60f,
		IncomeMult = 1.15f,
		CssClass = "hab-deluxe",
		Banner = "Deluxe reef."
	};

	// --- Shared comedy / scale habitats (rank 2+) ---
	public static readonly HabitatDef Bathtub = new()
	{
		Id = "bathtub",
		Name = "Bathtub",
		Note = "porcelain comedy",
		Rank = 2,
		Water = WaterType.Any,
		BaseCapacity = 16,
		MaxDecor = 10,
		Cost = 580,
		UnlockPlaytime = 22f * 60f, // ~22 min
		IncomeMult = 1.25f,
		CssClass = "hab-bathtub",
		Banner = "Bathtub habitat."
	};

	public static readonly HabitatDef KiddiePool = new()
	{
		Id = "kiddie_pool",
		Name = "Kiddie Pool",
		Note = "backyard upgrade",
		Rank = 3,
		Water = WaterType.Any,
		BaseCapacity = 24,
		MaxDecor = 12,
		Cost = 900,
		UnlockPlaytime = 28f * 60f, // ~28 min — same-session mid goal
		IncomeMult = 1.38f,
		CssClass = "hab-kiddie",
		Banner = "Kiddie pool."
	};

	public static readonly HabitatDef NeighborPool = new()
	{
		Id = "neighbor_pool",
		Name = "Neighbor's Pool",
		Note = "borrowed permanently",
		Rank = 4,
		Water = WaterType.Any,
		BaseCapacity = 45,
		MaxDecor = 14,
		Cost = 2200,
		UnlockPlaytime = 45f * 60f, // ~45 min
		IncomeMult = 1.55f,
		CssClass = "hab-neighbor",
		Banner = "Neighbor's pool."
	};

	public static readonly HabitatDef Lake = new()
	{
		Id = "lake",
		Name = "Lake",
		Note = "real water now",
		Rank = 5,
		Water = WaterType.Any,
		BaseCapacity = 80,
		MaxDecor = 16,
		Cost = 4800,
		UnlockPlaytime = 55f * 60f, // ~55 min
		IncomeMult = 1.8f,
		CssClass = "hab-lake",
		Banner = "Lake habitat."
	};

	public static readonly HabitatDef Gulf = new()
	{
		Id = "gulf",
		Name = "Gulf of America",
		Note = "branded brine",
		Rank = 6,
		Water = WaterType.Any,
		BaseCapacity = 140,
		MaxDecor = 18,
		Cost = 11000,
		UnlockPlaytime = 90f * 60f, // ~1h30
		IncomeMult = 2.15f,
		CssClass = "hab-gulf",
		Banner = "Gulf of America."
	};

	public static readonly HabitatDef Ocean = new()
	{
		Id = "ocean",
		Name = "Ocean",
		Note = "cocaine sharks",
		Rank = 7,
		Water = WaterType.Any,
		BaseCapacity = 280,
		MaxDecor = 20,
		Cost = 18000,
		UnlockPlaytime = 2.1f * 3600f, // ~2h10 — late setpiece, not a weekend wall
		IncomeMult = 2.7f,
		CssClass = "hab-ocean",
		Banner = "Ocean. Sharks inbound."
	};

	// --- Fight prizes (not sold — win them in AI circuits) ---
	public static readonly HabitatDef SewageDuct = new()
	{
		Id = "sewage_duct",
		Name = "Sewage Duct",
		Note = "fight prize · fragrant",
		Rank = 3,
		Water = WaterType.Any,
		BaseCapacity = 20,
		MaxDecor = 11,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1.28f,
		CssClass = "hab-sewage",
		Banner = "Sewage duct claimed.",
		FightPrizeOnly = true
	};

	public static readonly HabitatDef StormDrain = new()
	{
		Id = "storm_drain",
		Name = "Storm Drain",
		Note = "fight prize · runoff",
		Rank = 3,
		Water = WaterType.Any,
		BaseCapacity = 22,
		MaxDecor = 12,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1.3f,
		CssClass = "hab-storm",
		Banner = "Storm drain secured.",
		FightPrizeOnly = true
	};

	public static readonly HabitatDef AbandonedSpa = new()
	{
		Id = "abandoned_spa",
		Name = "Abandoned Spa",
		Note = "fight prize · chlorine ghost",
		Rank = 4,
		Water = WaterType.Any,
		BaseCapacity = 40,
		MaxDecor = 13,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1.45f,
		CssClass = "hab-spa",
		Banner = "Abandoned spa.",
		FightPrizeOnly = true
	};

	public static readonly HabitatDef CommunityFountain = new()
	{
		Id = "community_fountain",
		Name = "Community Fountain",
		Note = "fight prize · coins optional",
		Rank = 4,
		Water = WaterType.Any,
		BaseCapacity = 38,
		MaxDecor = 12,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1.42f,
		CssClass = "hab-fountain",
		Banner = "Community fountain.",
		FightPrizeOnly = true
	};

	public static readonly HabitatDef HotTubHeist = new()
	{
		Id = "hot_tub_heist",
		Name = "Stolen Hot Tub",
		Note = "fight prize · jets of shame",
		Rank = 5,
		Water = WaterType.Any,
		BaseCapacity = 55,
		MaxDecor = 14,
		Cost = 0,
		UnlockPlaytime = 0f,
		IncomeMult = 1.55f,
		CssClass = "hab-hottub",
		Banner = "Stolen hot tub.",
		FightPrizeOnly = true
	};

	/// <summary>All habitats (including starters + fight prizes).</summary>
	public static IReadOnlyList<HabitatDef> Catalog { get; } = new[]
	{
		StarterFresh, DeluxeFresh,
		StarterSalt, DeluxeSalt,
		Bathtub, KiddiePool, NeighborPool, Lake, Gulf, Ocean,
		SewageDuct, StormDrain, AbandonedSpa, CommunityFountain, HotTubHeist
	};

	public static IEnumerable<HabitatDef> FightPrizes =>
		Catalog.Where( h => h.FightPrizeOnly );

	public static HabitatDef Find( string id )
	{
		if ( string.IsNullOrWhiteSpace( id ) )
			return null;
		foreach ( var h in Catalog )
		{
			if ( h.Id == id )
				return h;
		}
		return null;
	}

	public static HabitatDef StarterFor( WaterType water ) =>
		water == WaterType.Salt ? StarterSalt : StarterFresh;

	/// <summary>Next buyable habitat for a tank at the given rank + water (shop chain only).</summary>
	public static HabitatDef NextFor( WaterType tankWater, int currentRank )
	{
		var want = currentRank + 1;
		HabitatDef best = null;
		foreach ( var h in Catalog )
		{
			if ( h.FightPrizeOnly )
				continue;
			if ( h.Rank != want )
				continue;
			if ( h.Water != WaterType.Any && h.Water != tankWater )
				continue;
			// Prefer water-specific over Any at same rank.
			if ( best is null || (h.Water == tankWater && best.Water == WaterType.Any) )
				best = h;
		}
		return best;
	}

	public static bool MatchesTank( HabitatDef def, WaterType tankWater )
	{
		if ( def is null )
			return false;
		return def.Water == WaterType.Any || def.Water == tankWater;
	}
}