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