FoodDef.cs

Defines FoodDef, a plain data holder for different food types in the game. It declares properties for display names, costs, boost values, visuals, and several predefined static FoodDef instances collected in a Catalog with helper find methods.

Reflection
namespace NoChillquarium;

/// <summary>
/// Escalating food ladder. Nothing free — buy Fish Flakes first, then weirder food.
/// </summary>
public sealed class FoodDef
{
	public string Id { get; init; }
	public string Name { get; init; }
	/// <summary>Optional short HUD label (inventory). Falls back to <see cref="Name"/>.</summary>
	public string InvName { get; init; }
	public string Note { get; init; }
	/// <summary>Name for inventory / compact UI.</summary>
	public string DisplayInv => string.IsNullOrWhiteSpace( InvName ) ? Name : InvName;
	public double UnlockCost { get; init; }
	public float BoostDuration { get; init; }
	public float BoostMultiplier { get; init; }
	public float ChaosOnFeed { get; init; }
	public int PelletCount { get; init; }
	public string PelletColor { get; init; }
	public float BobMulWhileBoost { get; init; }
	public float SpeedMulWhileBoost { get; init; }
	/// <summary>Playtime seconds required before unlock is available (0 = start).</summary>
	public float UnlockPlaytime { get; init; }
	/// <summary>Optional inventory / shop icon sprite id (falls back to inv_food).</summary>
	public string SpriteId { get; init; }
	/// <summary>Optional tank pellet sprite id (falls back to colored circle).</summary>
	public string PelletSpriteId { get; init; }

	/// <summary>Inventory icon — custom art or generic food bag.</summary>
	public string InvSpriteId =>
		!string.IsNullOrWhiteSpace( SpriteId ) && SpriteCatalog.Has( SpriteId )
			? SpriteId
			: "inv_food";

	public bool HasPelletSprite =>
		!string.IsNullOrWhiteSpace( PelletSpriteId ) && SpriteCatalog.Has( PelletSpriteId );

	public static readonly FoodDef Flakes = new()
	{
		Id = "flakes",
		Name = "Fish Flakes",
		InvName = "Fish Flakes",
		Note = "grow + boost",
		UnlockCost = 6,
		BoostDuration = 12f,
		BoostMultiplier = 1.65f,
		ChaosOnFeed = 0f,
		PelletCount = 6,
		PelletColor = "#e8c060",
		BobMulWhileBoost = 1f,
		SpeedMulWhileBoost = 1f,
		UnlockPlaytime = 0f
	};

	public static readonly FoodDef Pellets = new()
	{
		Id = "pellets",
		Name = "Pellets",
		Note = "crunch · grow",
		UnlockCost = 20,
		BoostDuration = 12f,
		BoostMultiplier = 1.9f,
		ChaosOnFeed = 0f,
		PelletCount = 7,
		PelletColor = "#d09040",
		BobMulWhileBoost = 1.05f,
		SpeedMulWhileBoost = 1.05f,
		UnlockPlaytime = 90f // ~1.5 min
	};

	public static readonly FoodDef Bloodworms = new()
	{
		Id = "bloodworms",
		Name = "Bloodworms",
		Note = "grow fast",
		UnlockCost = 35,
		BoostDuration = 13f,
		BoostMultiplier = 2.15f,
		ChaosOnFeed = 0.2f,
		PelletCount = 8,
		PelletColor = "#a02030",
		BobMulWhileBoost = 1.08f,
		SpeedMulWhileBoost = 1.08f,
		UnlockPlaytime = 4f * 60f, // ~4 min
		SpriteId = "inv_bloodworms",
		PelletSpriteId = "pellet_bloodworm"
	};

	public static readonly FoodDef FreezeDried = new()
	{
		Id = "freeze_dried",
		Name = "Freeze-Dried",
		Note = "fancy grow",
		UnlockCost = 60,
		BoostDuration = 10f,
		BoostMultiplier = 2.0f,
		ChaosOnFeed = 0.5f,
		PelletCount = 7,
		PelletColor = "#c07050",
		BobMulWhileBoost = 1.1f,
		SpeedMulWhileBoost = 1.1f,
		UnlockPlaytime = 14f * 60f
	};

	public static readonly FoodDef BrineShrimp = new()
	{
		Id = "brine_shrimp",
		Name = "Brine Shrimp",
		Note = "salt snack",
		UnlockCost = 85,
		BoostDuration = 11f,
		BoostMultiplier = 2.15f,
		ChaosOnFeed = 0.8f,
		PelletCount = 8,
		PelletColor = "#e07050",
		BobMulWhileBoost = 1.12f,
		SpeedMulWhileBoost = 1.12f,
		UnlockPlaytime = 20f * 60f
	};

	public static readonly FoodDef FriedChicken = new()
	{
		Id = "fried_chicken",
		Name = "Fried Chicken",
		Note = "greasy",
		UnlockCost = 120,
		BoostDuration = 11f,
		BoostMultiplier = 2.3f,
		ChaosOnFeed = 2f,
		PelletCount = 5,
		PelletColor = "#e8a040",
		BobMulWhileBoost = 0.85f,
		SpeedMulWhileBoost = 0.9f,
		UnlockPlaytime = 30f * 60f
	};

	public static readonly FoodDef HotCheetos = new()
	{
		Id = "hot_cheetos",
		Name = "Hot Cheetos",
		Note = "flamin'",
		UnlockCost = 150,
		BoostDuration = 12f,
		BoostMultiplier = 2.4f,
		ChaosOnFeed = 3f,
		PelletCount = 6,
		PelletColor = "#e03020",
		BobMulWhileBoost = 1.15f,
		SpeedMulWhileBoost = 1.2f,
		UnlockPlaytime = 40f * 60f
	};

	public static readonly FoodDef WeedGummies = new()
	{
		Id = "weed_gummies",
		Name = "Weed Gummies",
		InvName = "Gummies",
		Note = "trippy",
		UnlockCost = 180,
		BoostDuration = 14f,
		BoostMultiplier = 2.2f,
		ChaosOnFeed = 4f,
		PelletCount = 6,
		PelletColor = "#50d060",
		BobMulWhileBoost = 0.5f,
		SpeedMulWhileBoost = 0.55f,
		UnlockPlaytime = 80f * 60f // ~1h20
	};

	public static readonly FoodDef OtherFish = new()
	{
		Id = "other_fish",
		Name = "Other Fish",
		Note = "cannibal",
		UnlockCost = 280,
		BoostDuration = 12f,
		BoostMultiplier = 2.7f,
		ChaosOnFeed = 6f,
		PelletCount = 4,
		PelletColor = "#e06070",
		BobMulWhileBoost = 1.2f,
		SpeedMulWhileBoost = 1.15f,
		UnlockPlaytime = 2f * 3600f // ~2h
	};

	public static readonly FoodDef Hand = new()
	{
		Id = "hand",
		Name = "Dismembered Hand",
		Note = "hand",
		UnlockCost = 450,
		BoostDuration = 13f,
		BoostMultiplier = 3.1f,
		ChaosOnFeed = 10f,
		PelletCount = 3,
		PelletColor = "#d08070",
		BobMulWhileBoost = 1.3f,
		SpeedMulWhileBoost = 1.2f,
		UnlockPlaytime = 2.75f * 3600f // ~2h45
	};

	public static readonly FoodDef HomelessMan = new()
	{
		Id = "homeless_man",
		Name = "Homeless Man",
		Note = "unhinged",
		UnlockCost = 700,
		BoostDuration = 15f,
		BoostMultiplier = 3.5f,
		ChaosOnFeed = 15f,
		PelletCount = 4,
		PelletColor = "#a09080",
		BobMulWhileBoost = 1.4f,
		SpeedMulWhileBoost = 1.25f,
		UnlockPlaytime = 3.5f * 3600f // ~3h30
	};

	public static IReadOnlyList<FoodDef> Catalog { get; } = new[]
	{
		Flakes,
		Pellets,
		Bloodworms,
		FreezeDried,
		BrineShrimp,
		FriedChicken,
		HotCheetos,
		WeedGummies,
		OtherFish,
		Hand,
		HomelessMan
	};

	public static FoodDef Find( string id ) =>
		Catalog.FirstOrDefault( f => f.Id == id ) ?? Flakes;

	/// <summary>Lookup without flakes fallback (null if unknown / empty).</summary>
	public static FoodDef FindExact( string id ) =>
		string.IsNullOrWhiteSpace( id ) ? null : Catalog.FirstOrDefault( f => f.Id == id );
}