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).
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;
}
}