LotRunDef.cs

Definitions for lot-run content: enums and data classes that describe room kinds, individual rooms (LotRoomDef), and locations (LotLocationDef) with a static Catalog of predefined locations and a Find(id) lookup.

File Access
namespace NoChillquarium;

/// <summary>What happens in one dungeon room.</summary>
public enum LotRoomKind
{
	/// <summary>Flavor only — click Continue.</summary>
	Look,
	/// <summary>Auto-resolve skirmish with local AI fish.</summary>
	Fight,
	/// <summary>Instant Ð (and maybe chaos).</summary>
	Loot,
	/// <summary>Risk: injury / chaos / small Ð.</summary>
	Hazard,
	/// <summary>Two buttons with different outcomes.</summary>
	Choice,
	/// <summary>Harder fight; clearing ends the run.</summary>
	Boss
}

/// <summary>One room on a lot-run map.</summary>
public sealed class LotRoomDef
{
	public string Title { get; init; }
	public string Body { get; init; }
	public LotRoomKind Kind { get; init; } = LotRoomKind.Look;
	public string Button { get; init; } = "Continue";
	// Fight / boss
	public float AiStatMin { get; init; } = 2f;
	public float AiStatMax { get; init; } = 4f;
	public string[] EnemyNames { get; init; }
	public string EnemyColor { get; init; } = "#8090a0";
	public double FightRewardMin { get; init; }
	public double FightRewardMax { get; init; }
	// Loot
	public double LootMin { get; init; }
	public double LootMax { get; init; }
	// Hazard
	public float InjuryChance { get; init; } = 0.35f;
	public float ChaosOnHazard { get; init; } = 2f;
	// Choice
	public string ChoiceA { get; init; }
	public string ChoiceB { get; init; }
	public string ChoiceAResult { get; init; }
	public string ChoiceBResult { get; init; }
	public double ChoiceALoot { get; init; }
	public double ChoiceBLoot { get; init; }
	public float ChoiceAChaos { get; init; }
	public float ChoiceBChaos { get; init; }
	public float ChoiceBInjuryChance { get; init; }
}

/// <summary>A crawlable location off Grandma's lot (jr high, alley, pool, …).</summary>
public sealed class LotLocationDef
{
	public string Id { get; init; }
	public string Name { get; init; }
	public string Note { get; init; }
	public string Blurb { get; init; }
	public string ThemeClass { get; init; } = "";
	public double EntryFee { get; init; }
	public int TeamSize { get; init; } = 1;
	/// <summary>Min playtime seconds before the map shows as unlocked.</summary>
	public float UnlockPlaytime { get; init; }
	/// <summary>Need this many fight wins (or equal losses+wins) to enter.</summary>
	public int UnlockFights { get; init; }
	/// <summary>Must have trained once.</summary>
	public bool RequireTrained { get; init; }
	public double ClearBonus { get; init; }
	public string ClearBanner { get; init; }
	/// <summary>First adventure: only entered by flushing a fish (then re-openable from map).</summary>
	public bool FlushOrigin { get; init; }
	public IReadOnlyList<LotRoomDef> Rooms { get; init; }

	public static IReadOnlyList<LotLocationDef> Catalog { get; } = new[]
	{
		// 0 — first crawl (flush entry)
		new LotLocationDef
		{
			Id = "toilet_pipes",
			Name = "Toilet Pipes",
			Note = "under the trailer",
			Blurb = "Municipal pipe. U-bends. Your fish is ahead of you.",
			ThemeClass = "run-toilet",
			EntryFee = 0,
			TeamSize = 1,
			UnlockPlaytime = 0f,
			RequireTrained = false,
			UnlockFights = 0,
			FlushOrigin = true,
			ClearBonus = 40,
			ClearBanner = "Pipes cleared. Other maps unlocked on the board.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Into the trap",
					Body = "Tile walls. Cold water. They're already past the U-bend.",
					Button = "Follow"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Hazard,
					Title = "U-Bend",
					Body = "Narrow curve. Rough scale. Easy to scrape if you rush.",
					Button = "Squeeze through",
					InjuryChance = 0.25f,
					ChaosOnHazard = 1.5f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Pipe fauna",
					Body = "Small, mean things that live in standing water.",
					Button = "Fight",
					AiStatMin = 1.4f,
					AiStatMax = 2.8f,
					EnemyNames = new[] { "Pipe Mite", "Silt Ray", "Trap Snail", "Scale Flake" },
					EnemyColor = "#607080",
					FightRewardMin = 12,
					FightRewardMax = 22
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Loot,
					Title = "Silt pocket",
					Body = "A ring, some coins, grit. Worth pocketing.",
					Button = "Take it",
					LootMin = 16,
					LootMax = 34
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "Main line",
					Body = "Where house lines join the lot trunk. Something large blocks the flow.",
					Button = "Fight",
					AiStatMin = 2.4f,
					AiStatMax = 4.2f,
					EnemyNames = new[] { "Mainline Carp", "Trunk Bass", "Joint Eel", "Sewer Perch" },
					EnemyColor = "#405060",
					FightRewardMin = 28,
					FightRewardMax = 48
				}
			}
		},
		// 1 — after first flush adventure
		new LotLocationDef
		{
			Id = "jr_high_night",
			Name = "Jr High After Hours",
			Note = "after dark",
			Blurb = "Fence, halls, principal's office. Bring one trained fish.",
			ThemeClass = "run-school",
			EntryFee = 12,
			TeamSize = 1,
			UnlockPlaytime = 6f * 60f,
			RequireTrained = true,
			UnlockFights = 0,
			ClearBonus = 35,
			ClearBanner = "School run done. Stay off the cameras.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Chain-link",
					Body = "Wax and metal. Hall lights on a timer.",
					Button = "Climb in"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Hall patrol",
					Body = "Someone's fish in a plastic carrier. They take the job seriously.",
					Button = "Fight",
					AiStatMin = 1.6f,
					AiStatMax = 3.2f,
					EnemyNames = new[] { "Carrier Guppy", "Badge Molly", "Tardy Tetra", "Whistle Ray" },
					EnemyColor = "#708090",
					FightRewardMin = 10,
					FightRewardMax = 18
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Loot,
					Title = "Vending machine",
					Body = "Kick once. Coins rattle out.",
					Button = "Kick",
					LootMin = 8,
					LootMax = 22
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Choice,
					Title = "Split hall",
					Body = "Detention wing is quiet. Gym still smells like bleach.",
					ChoiceA = "Detention",
					ChoiceB = "Gym",
					ChoiceAResult = "Teacher's coffee can — cash and receipts.",
					ChoiceBResult = "Hard floor. Your fish takes a knock.",
					ChoiceALoot = 14,
					ChoiceBLoot = 6,
					ChoiceBChaos = 1.5f,
					ChoiceBInjuryChance = 0.25f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "Principal's office",
					Body = "Nameplate. Plastic plant. A prize guppy in a desk tank.",
					Button = "Fight",
					AiStatMin = 2.8f,
					AiStatMax = 4.6f,
					EnemyNames = new[] { "Desk Guppy", "Office Betta", "Policy Pike", "Badge Bass" },
					EnemyColor = "#405070",
					FightRewardMin = 28,
					FightRewardMax = 48
				}
			}
		},
		// 2
		new LotLocationDef
		{
			Id = "dark_alley",
			Name = "Dark Alley",
			Note = "behind 4B",
			Blurb = "Dumpsters and standing water. Watch for glass.",
			ThemeClass = "run-alley",
			EntryFee = 22,
			TeamSize = 1,
			UnlockPlaytime = 10f * 60f,
			RequireTrained = true,
			UnlockFights = 1,
			ClearBonus = 55,
			ClearBanner = "Alley cleared.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Dumpster line",
					Body = "Gas-station neon. Grease film on the water.",
					Button = "Enter"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Hazard,
					Title = "Broken glass",
					Body = "Shards in the runoff. Easy cut if you're careless.",
					Button = "Push through",
					InjuryChance = 0.4f,
					ChaosOnHazard = 2.5f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Alley stock",
					Body = "Half-feral fish living off trash water.",
					Button = "Fight",
					AiStatMin = 2.4f,
					AiStatMax = 4.2f,
					EnemyNames = new[] { "Grease Molly", "Moth Betta", "Neon Trash", "Curb Guppy" },
					EnemyColor = "#504060",
					FightRewardMin = 16,
					FightRewardMax = 30
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Loot,
					Title = "Dropped wallet",
					Body = "Not yours. Still money.",
					Button = "Take Ð",
					LootMin = 18,
					LootMax = 40
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "4B cousin",
					Body = "Related to Brad. Runs this strip like he owns it.",
					Button = "Fight",
					AiStatMin = 3.5f,
					AiStatMax = 5.5f,
					EnemyNames = new[] { "4B Cousin", "Alley Lead", "Strip Boss", "Lot Ray" },
					EnemyColor = "#803040",
					FightRewardMin = 40,
					FightRewardMax = 70
				}
			}
		},
		// 3
		new LotLocationDef
		{
			Id = "neighbor_pool_night",
			Name = "Neighbor's Pool",
			Note = "after midnight",
			Blurb = "Chlorine, cameras, deed-rule koi. Team of two.",
			ThemeClass = "run-pool",
			EntryFee = 35,
			TeamSize = 2,
			UnlockPlaytime = 14f * 60f,
			RequireTrained = true,
			UnlockFights = 2,
			ClearBonus = 80,
			ClearBanner = "Pool cleared. Don't leave wet footprints.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Back gate",
					Body = "Motion light. Timer. You have a window.",
					Button = "Slip in"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Skimmer line",
					Body = "Pool fish used to clean water. Territorial.",
					Button = "Fight",
					AiStatMin = 3f,
					AiStatMax = 5f,
					EnemyNames = new[] { "Skimmer Ray", "Float Bass", "Filter Guppy", "Deep Molly" },
					EnemyColor = "#2080a0",
					FightRewardMin = 22,
					FightRewardMax = 40
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Hazard,
					Title = "Chemical shock",
					Body = "High chlorine. Hard on gills if you linger.",
					Button = "Push through",
					InjuryChance = 0.45f,
					ChaosOnHazard = 3f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Choice,
					Title = "Cabana / board",
					Body = "Quiet loot at the cabana, or risk noise off the board.",
					ChoiceA = "Cabana",
					ChoiceB = "Board",
					ChoiceAResult = "Cash in a towel bin.",
					ChoiceBResult = "Splash carries. Injury risk.",
					ChoiceALoot = 28,
					ChoiceBLoot = 12,
					ChoiceBChaos = 4f,
					ChoiceBInjuryChance = 0.35f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "Deed koi",
					Body = "HOA champion stock. Big, trained, expensive.",
					Button = "Fight",
					AiStatMin = 4.2f,
					AiStatMax = 6.5f,
					EnemyNames = new[] { "Covenant Koi", "Deed Bass", "Laminate Ray", "Rule Carp" },
					EnemyColor = "#30a060",
					FightRewardMin = 55,
					FightRewardMax = 95
				}
			}
		},
		// 4
		new LotLocationDef
		{
			Id = "gas_station",
			Name = "Gas Station Dumpsters",
			Note = "behind pumps",
			Blurb = "Grease trap water. Janitor pretends not to see.",
			ThemeClass = "run-gas",
			EntryFee = 28,
			TeamSize = 1,
			UnlockPlaytime = 12f * 60f,
			RequireTrained = true,
			UnlockFights = 1,
			ClearBonus = 60,
			ClearBanner = "Dumpster line cleared.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Pump row",
					Body = "Janitor mops past. \"Not my dumpsters.\"",
					Button = "Around back"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Loot,
					Title = "Change tray",
					Body = "Uncounted change under the register shelf.",
					Button = "Take it",
					LootMin = 12,
					LootMax = 28
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Grease trap",
					Body = "Something lives in the trap water. Hungry.",
					Button = "Fight",
					AiStatMin = 2.8f,
					AiStatMax = 4.8f,
					EnemyNames = new[] { "Trap Carp", "Oil Ray", "Sludge Molly", "Pump Perch" },
					EnemyColor = "#806020",
					FightRewardMin = 18,
					FightRewardMax = 34
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Hazard,
					Title = "Oil film",
					Body = "Slick surface. Hard to get purchase. Easy bruise.",
					Button = "Cross",
					InjuryChance = 0.3f,
					ChaosOnHazard = 2f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "Under-counter tank",
					Body = "Illegal display stock. Big, fed on scraps, mean.",
					Button = "Fight",
					AiStatMin = 3.8f,
					AiStatMax = 5.8f,
					EnemyNames = new[] { "Counter Bass", "Shelf Shark", "Lottery Carp", "Shift Lead" },
					EnemyColor = "#a05020",
					FightRewardMin = 45,
					FightRewardMax = 75
				}
			}
		},
		// 5
		new LotLocationDef
		{
			Id = "storm_drain",
			Name = "Storm Drain",
			Note = "lot runoff",
			Blurb = "Concrete under the street. Team of two. Watch the drop.",
			ThemeClass = "run-drain",
			EntryFee = 40,
			TeamSize = 2,
			UnlockPlaytime = 18f * 60f,
			RequireTrained = true,
			UnlockFights = 3,
			ClearBonus = 90,
			ClearBanner = "Drain cleared.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Grate",
					Body = "Lift with a stick. Cold air, wet concrete.",
					Button = "Drop in"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Runoff school",
					Body = "Fish living on street wash. Tough and dirty.",
					Button = "Fight",
					AiStatMin = 3.2f,
					AiStatMax = 5.2f,
					EnemyNames = new[] { "Leaf Carp", "Curb Ray", "Grate Guppy", "Gutter Bass" },
					EnemyColor = "#405848",
					FightRewardMin = 24,
					FightRewardMax = 42
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Loot,
					Title = "Coin silt",
					Body = "Fountain and parking runoff. Small metal, real Ð.",
					Button = "Scoop",
					LootMin = 20,
					LootMax = 50
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Choice,
					Title = "Forked pipe",
					Body = "Left is quieter. Right is deeper and richer.",
					ChoiceA = "Quiet pipe",
					ChoiceB = "Deep pipe",
					ChoiceAResult = "Cleaner water. Small take.",
					ChoiceBResult = "Bigger take. Something bit.",
					ChoiceALoot = 15,
					ChoiceBLoot = 45,
					ChoiceBChaos = 3f,
					ChoiceBInjuryChance = 0.4f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "Trunk blockage",
					Body = "Large fish lodged in the main trunk. Clear it or turn back.",
					Button = "Fight",
					AiStatMin = 4.5f,
					AiStatMax = 7f,
					EnemyNames = new[] { "Trunk Carp", "Block Bass", "Cap Ray", "Mainline Eel" },
					EnemyColor = "#304838",
					FightRewardMin = 70,
					FightRewardMax = 110
				}
			}
		},
		// 6
		new LotLocationDef
		{
			Id = "bingo_hall",
			Name = "Bingo Hall Basement",
			Note = "downstairs",
			Blurb = "Under the hall Grandma plays. She pretends not to know the door code.",
			ThemeClass = "run-bingo",
			EntryFee = 50,
			TeamSize = 2,
			UnlockPlaytime = 22f * 60f,
			RequireTrained = true,
			UnlockFights = 4,
			ClearBonus = 120,
			ClearBanner = "Basement cleared. Don't tell bingo night.",
			Rooms = new[]
			{
				new LotRoomDef
				{
					Kind = LotRoomKind.Look,
					Title = "Side door",
					Body = "Coffee, ash, floor wax. Her crowd.",
					Button = "Enter"
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Fight,
					Title = "Basement stock",
					Body = "Fish kept for side bets. Ink-stained carriers.",
					Button = "Fight",
					AiStatMin = 3.6f,
					AiStatMax = 5.8f,
					EnemyNames = new[] { "Daub Molly", "Card Ray", "Caller Carp", "Pot Bass" },
					EnemyColor = "#704080",
					FightRewardMin = 28,
					FightRewardMax = 48
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Loot,
					Title = "Tip jar",
					Body = "She said not to. You take a little.",
					Button = "Take Ð",
					LootMin = 30,
					LootMax = 70
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Hazard,
					Title = "Smoke break",
					Body = "Stale air. Hard on gills if you hang around.",
					Button = "Push through",
					InjuryChance = 0.35f,
					ChaosOnHazard = 2f
				},
				new LotRoomDef
				{
					Kind = LotRoomKind.Boss,
					Title = "Hall champion",
					Body = "Not Grandma. Someone who wins too often to be lucky.",
					Button = "Fight",
					AiStatMin = 5f,
					AiStatMax = 7.5f,
					EnemyNames = new[] { "Hall Champ", "Blackout Bass", "Hard-Way Ray", "House Carp" },
					EnemyColor = "#903070",
					FightRewardMin = 90,
					FightRewardMax = 140
				}
			}
		},
	};

	public static LotLocationDef Find( string id )
	{
		if ( string.IsNullOrWhiteSpace( id ) )
			return null;
		foreach ( var loc in Catalog )
		{
			if ( string.Equals( loc.Id, id, StringComparison.OrdinalIgnoreCase ) )
				return loc;
		}
		return null;
	}
}