FishRarity.cs

Defines a FishRarity enum and a static FishRarityInfo helper with labels, short tags, CSS class, income/sell multipliers, pity tracking, weighted rolling logic, shop odds text, parent luck boost, and save/load conversion.

Reflection
namespace NoChillquarium;

/// <summary>
/// Chillquarium-style rarity. Rolled on spawn/buy; multiplies income + sell.
/// </summary>
public enum FishRarity
{
	Common = 0,
	Uncommon = 1,
	Rare = 2,
	PaintedRare = 3,
	Legendary = 4,
	Mythical = 5
}

/// <summary>Static tables for rarity labels, weights, and income mults.</summary>
public static class FishRarityInfo
{
	public static string Label( FishRarity r ) => r switch
	{
		FishRarity.Common => "Common",
		FishRarity.Uncommon => "Uncommon",
		FishRarity.Rare => "Rare",
		FishRarity.PaintedRare => "Painted Rare",
		FishRarity.Legendary => "Legendary",
		FishRarity.Mythical => "Mythical",
		_ => "Common"
	};

	/// <summary>Short shop / HUD tag.</summary>
	public static string Short( FishRarity r ) => r switch
	{
		FishRarity.Common => "C",
		FishRarity.Uncommon => "U",
		FishRarity.Rare => "R",
		FishRarity.PaintedRare => "PR",
		FishRarity.Legendary => "L",
		FishRarity.Mythical => "M",
		_ => "C"
	};

	/// <summary>CSS class suffix: rarity-common, rarity-mythical, …</summary>
	public static string CssClass( FishRarity r ) =>
		"rarity-" + r.ToString().ToLowerInvariant();

	/// <summary>Idle income multiplier — rares are the Chillquarium keepers.</summary>
	public static float IncomeMult( FishRarity r ) => r switch
	{
		FishRarity.Common => 1f,
		FishRarity.Uncommon => 1.75f,
		FishRarity.Rare => 3.4f,
		FishRarity.PaintedRare => 5.5f,
		FishRarity.Legendary => 11f,
		FishRarity.Mythical => 24f,
		_ => 1f
	};

	/// <summary>Sell price multiplier — flip commons, jackpot rares.</summary>
	public static float SellMult( FishRarity r ) => r switch
	{
		FishRarity.Common => 1f,
		FishRarity.Uncommon => 1.55f,
		FishRarity.Rare => 2.6f,
		FishRarity.PaintedRare => 3.8f,
		FishRarity.Legendary => 7f,
		FishRarity.Mythical => 14f,
		_ => 1f
	};

	/// <summary>Commons in a row before pity forces Uncommon+ (session-only).</summary>
	const int PityAfterCommons = 7;
	static int _commonStreak;
	static int _lifetimeRareHits;

	public static int CommonStreak => _commonStreak;
	public static int LifetimeRareHits => _lifetimeRareHits;

	public static void ResetPity()
	{
		_commonStreak = 0;
	}

	/// <summary>
	/// Weighted roll. <paramref name="luck"/> &gt; 1 slightly favors higher tiers
	/// (habitat rank / late game), still mostly Commons. Pity soft-guarantees spice.
	/// </summary>
	public static FishRarity Roll( Random rng, float luck = 1f )
	{
		rng ??= new Random();
		luck = Math.Clamp( luck, 0.5f, 2.8f );

		// Base weights. Luck + mild common compression for chase feel.
		var wCommon = 52f / luck;
		var wUncommon = 24f;
		var wRare = 13f * MathF.Sqrt( luck );
		var wPainted = 6f * luck;
		var wLegend = 3.2f * luck;
		var wMyth = 1.5f * luck * luck;

		// Pity: after a dry streak, dump common weight into uncommon+.
		if ( _commonStreak >= PityAfterCommons )
		{
			wUncommon += wCommon * 0.55f;
			wRare += wCommon * 0.25f;
			wCommon *= 0.2f;
		}
		else if ( _commonStreak >= PityAfterCommons - 2 )
		{
			wUncommon += wCommon * 0.2f;
			wCommon *= 0.8f;
		}

		var total = wCommon + wUncommon + wRare + wPainted + wLegend + wMyth;
		var roll = (float)rng.NextDouble() * total;
		FishRarity result;
		if ( (roll -= wCommon) <= 0 ) result = FishRarity.Common;
		else if ( (roll -= wUncommon) <= 0 ) result = FishRarity.Uncommon;
		else if ( (roll -= wRare) <= 0 ) result = FishRarity.Rare;
		else if ( (roll -= wPainted) <= 0 ) result = FishRarity.PaintedRare;
		else if ( (roll -= wLegend) <= 0 ) result = FishRarity.Legendary;
		else result = FishRarity.Mythical;

		if ( result == FishRarity.Common )
			_commonStreak++;
		else
		{
			_commonStreak = 0;
			if ( result >= FishRarity.Rare )
				_lifetimeRareHits++;
		}

		return result;
	}

	/// <summary>Short shop line: roughly how spicy buys feel right now.</summary>
	public static string ShopOddsLine( float luck = 1f )
	{
		luck = Math.Clamp( luck, 0.5f, 2.8f );
		var rarePct = Math.Clamp( 8f + luck * 6f + (_commonStreak >= 5 ? 8f : 0f), 8f, 28f );
		var pity = _commonStreak >= PityAfterCommons - 1
			? " · pity hot"
			: _commonStreak >= 4
				? " · warming"
				: "";
		return "rare~" + (int)rarePct + "%" + pity;
	}

	/// <summary>Breed/hatch tilt from parent rarities (max parent, slight bump).</summary>
	public static float ParentLuckBoost( FishRarity a, FishRarity b )
	{
		var top = (int)a > (int)b ? a : b;
		return 1f + (int)top * 0.12f;
	}

	public static FishRarity FromSave( int raw )
	{
		if ( raw < 0 || raw > (int)FishRarity.Mythical )
			return FishRarity.Common;
		return (FishRarity)raw;
	}
}