PixelFx.cs
namespace NoChillquarium;

public enum PixelFxLayer
{
	Tank,
	Shop
}

public sealed class PixelSpark
{
	public float X;
	public float Y;
	public float Vx;
	public float Vy;
	public float Life;
	public float MaxLife;
	public int Size;
	public string Color;
	public PixelFxLayer Layer;
}

/// <summary>
/// Hard-pixel spark bits. GBA confetti — squares, integer positions, no bloom.
/// Shop layer sits on the unfold card; tank layer sits in the glass.
/// </summary>
public static class PixelFx
{
	const int Cap = 64;
	static readonly Random _rng = new();
	static readonly List<PixelSpark> _sparks = new();
	static int _version;

	/// <summary>Evenly spaced scanline tops (percent). Overlay, not a shader.</summary>
	public static readonly string[] ScanTops =
	{
		"0%", "6%", "12%", "18%", "25%", "31%", "37%", "43%",
		"50%", "56%", "62%", "68%", "75%", "81%", "87%", "93%"
	};

	public static int Version => _version;

	public static IEnumerable<PixelSpark> TankSparks
	{
		get
		{
			foreach ( var s in _sparks )
			{
				if ( s.Layer == PixelFxLayer.Tank )
					yield return s;
			}
		}
	}

	public static IEnumerable<PixelSpark> ShopSparks
	{
		get
		{
			foreach ( var s in _sparks )
			{
				if ( s.Layer == PixelFxLayer.Shop )
					yield return s;
			}
		}
	}

	public static void Clear()
	{
		_sparks.Clear();
		_version++;
	}

	public static void Tick( float dt )
	{
		if ( _sparks.Count == 0 )
			return;
		if ( dt <= 0f )
			return;
		if ( dt > 0.05f )
			dt = 0.05f;

		for ( var i = _sparks.Count - 1; i >= 0; i-- )
		{
			var s = _sparks[i];
			s.Life -= dt;
			if ( s.Life <= 0f )
			{
				_sparks.RemoveAt( i );
				continue;
			}
			s.X += s.Vx * dt;
			s.Y += s.Vy * dt;
			s.Vy += 90f * dt;
		}
		_version++;
	}

	/// <summary>Pack-open / hatch / fusion burst in rarity colors.</summary>
	public static void BurstRarity( PixelFxLayer layer, float x, float y, FishRarity rarity )
	{
		var count = rarity switch
		{
			FishRarity.Common => 4,
			FishRarity.Uncommon => 6,
			FishRarity.Rare => 9,
			FishRarity.PaintedRare => 12,
			FishRarity.Legendary => 16,
			_ => 20
		};
		if ( GameSettings.ReduceStrobe )
			count = Math.Max( 3, count / 2 );

		var spd = rarity >= FishRarity.Legendary ? 140f : rarity >= FishRarity.Rare ? 110f : 80f;
		Burst( layer, x, y, count, ColorsFor( rarity ), spd, size: rarity >= FishRarity.PaintedRare ? 4 : 3 );
	}

	public static void Burst( PixelFxLayer layer, float x, float y, int count, string[] colors, float speed, int size = 3 )
	{
		if ( colors is null || colors.Length == 0 )
			return;
		count = Math.Clamp( count, 1, 28 );
		size = Math.Clamp( size, 2, 5 );
		for ( var i = 0; i < count; i++ )
		{
			if ( _sparks.Count >= Cap )
				_sparks.RemoveAt( 0 );
			var ang = (float)(_rng.NextDouble() * Math.PI * 2.0);
			var spd = speed * (0.45f + (float)_rng.NextDouble() * 0.7f);
			var life = 0.22f + (float)_rng.NextDouble() * 0.28f;
			_sparks.Add( new PixelSpark
			{
				X = x + (float)(_rng.NextDouble() * 6.0 - 3.0),
				Y = y + (float)(_rng.NextDouble() * 6.0 - 3.0),
				Vx = MathF.Cos( ang ) * spd,
				Vy = MathF.Sin( ang ) * spd - 28f,
				Life = life,
				MaxLife = life,
				Size = size,
				Color = colors[i % colors.Length],
				Layer = layer
			} );
		}
		_version++;
	}

	static readonly string[] CommonCols = { "#8a9a90", "#c8d0c8" };
	static readonly string[] UncommonCols = { "#7ad8b0", "#d0fff0" };
	static readonly string[] RareCols = { "#7ab8ff", "#d0e8ff" };
	static readonly string[] PaintedCols = { "#f090e0", "#40e0ff", "#ff60c8" };
	static readonly string[] GoldenCols = { "#ffd24a", "#ffe8a0", "#c8a020" };
	static readonly string[] RainbowCols = { "#ff4040", "#ffd24a", "#40d070", "#4080ff", "#d060ff" };

	static string[] ColorsFor( FishRarity r ) => r switch
	{
		FishRarity.Uncommon => UncommonCols,
		FishRarity.Rare => RareCols,
		FishRarity.PaintedRare => PaintedCols,
		FishRarity.Legendary => GoldenCols,
		FishRarity.Mythical => RainbowCols,
		_ => CommonCols
	};
}