A particle effect system that simulates pixel water drops and jets for two layers (Tank and Battle). It spawns, updates physics (gravity, bounce), lifetimes, splashes and crack-driven leaks, and exposes enumerables and counters for external use.
namespace NoChillquarium;
public enum WaterLayer
{
Tank,
Battle
}
public sealed class WaterDrop
{
public float X;
public float Y;
public float Vx;
public float Vy;
public float W;
public float H;
public float Life;
public float MaxLife;
public string Sprite = "water_drop";
public WaterLayer Layer;
public bool Jet;
public float Spin;
}
/// <summary>
/// Pixel water — jets from cracked glass, splashes on hits and booms.
/// </summary>
public static class WaterFx
{
const int Cap = 110;
const float Grav = 520f;
static readonly Random _rng = new();
static readonly List<WaterDrop> _drops = new();
static readonly (float X, float Y, float DirX)[] CrackSites =
{
(154f, 80f, -1f),
(520f, 210f, 0.2f),
(1120f, 90f, 1f),
(140f, 500f, -0.8f),
(720f, 70f, 0.4f),
(640f, 300f, 0f)
};
static int _version;
static float _leakTimer;
public static int Version => _version;
public static int Count => _drops.Count;
public static IEnumerable<WaterDrop> TankDrops
{
get
{
foreach ( var d in _drops )
{
if ( d.Layer == WaterLayer.Tank )
yield return d;
}
}
}
public static IEnumerable<WaterDrop> BattleDrops
{
get
{
foreach ( var d in _drops )
{
if ( d.Layer == WaterLayer.Battle )
yield return d;
}
}
}
public static void Clear()
{
_drops.Clear();
_leakTimer = 0f;
_version++;
}
public static void Tick( float dt )
{
if ( dt <= 0f )
return;
if ( dt > 0.05f )
dt = 0.05f;
if ( MethRush.Cracks > 0 && TankSim.IsActive )
{
_leakTimer -= dt;
if ( _leakTimer <= 0f )
{
_leakTimer = Math.Clamp( 0.42f - MethRush.Cracks * 0.05f, 0.16f, 0.45f );
DripFromCracks();
}
}
var floor = TankSim.Height - 18f;
for ( var i = _drops.Count - 1; i >= 0; i-- )
{
var d = _drops[i];
d.Vy += Grav * dt;
d.X += d.Vx * dt;
d.Y += d.Vy * dt;
d.Life -= dt;
d.Spin += dt * 8f;
if ( d.Layer == WaterLayer.Tank )
{
if ( d.Y > floor && d.Vy > 0f )
{
d.Y = floor;
d.Vy *= -0.28f;
d.Vx *= 0.55f;
if ( MathF.Abs( d.Vy ) < 28f )
d.Life = MathF.Min( d.Life, 0.08f );
}
}
else
{
if ( d.Y > BattleSystem.ArenaH - 16f && d.Vy > 0f )
{
d.Y = BattleSystem.ArenaH - 16f;
d.Vy *= -0.22f;
d.Vx *= 0.5f;
}
}
if ( d.Life <= 0f || d.Y > 900f || d.X < -80f || d.X > 1400f )
_drops.RemoveAt( i );
}
if ( _drops.Count > 0 )
_version++;
}
/// <summary>Glass just cracked — water shoots out of that wound.</summary>
public static void CrackJet( int crackIndex )
{
var i = Math.Clamp( crackIndex, 0, CrackSites.Length - 1 );
var site = CrackSites[i];
var n = 14 + crackIndex * 3;
Squirt( site.X, site.Y, site.DirX, -0.35f, n, 220f, WaterLayer.Tank );
Splash( site.X, site.Y, 10, WaterLayer.Tank );
}
public static void Splash( float x, float y, int count, WaterLayer layer )
{
count = Math.Clamp( count, 4, 28 );
for ( var i = 0; i < count; i++ )
Add( x, y, layer, jet: false, power: 140f + (float)_rng.NextDouble() * 180f );
_version++;
}
public static void Squirt( float x, float y, float dirX, float dirY, int count, float power, WaterLayer layer )
{
var len = MathF.Sqrt( dirX * dirX + dirY * dirY );
if ( len < 0.01f )
{
dirX = 0f;
dirY = -1f;
len = 1f;
}
dirX /= len;
dirY /= len;
count = Math.Clamp( count, 4, 32 );
for ( var i = 0; i < count; i++ )
{
var spread = ((float)_rng.NextDouble() - 0.5f) * 0.7f;
var px = dirX - dirY * spread;
var py = dirY + dirX * spread;
var spd = power * (0.55f + (float)_rng.NextDouble() * 0.7f);
Push( new WaterDrop
{
X = x + (float)(_rng.NextDouble() * 10 - 5),
Y = y + (float)(_rng.NextDouble() * 8 - 4),
Vx = px * spd,
Vy = py * spd - 40f,
W = 10f + (float)_rng.NextDouble() * 10f,
H = 14f + (float)_rng.NextDouble() * 12f,
Life = 0.45f + (float)_rng.NextDouble() * 0.45f,
Sprite = i % 3 == 0 ? "water_jet" : "water_drop",
Layer = layer,
Jet = true
} );
}
_version++;
}
public static void BoomSplash( float x, float y )
{
Splash( x, y, 22, WaterLayer.Tank );
Squirt( x, y, 0f, -1f, 10, 280f, WaterLayer.Tank );
}
public static void FightHit( float x, float y, bool hard )
{
Splash( x, y, hard ? 18 : 11, WaterLayer.Battle );
if ( hard )
Squirt( x, y, 0f, -1f, 8, 240f, WaterLayer.Battle );
}
static void DripFromCracks()
{
var n = Math.Clamp( MethRush.Cracks, 1, 5 );
for ( var i = 0; i < n; i++ )
{
if ( _rng.NextDouble() > 0.55 )
continue;
var site = CrackSites[Math.Min( i, CrackSites.Length - 1 )];
Add( site.X + (float)(_rng.NextDouble() * 24 - 12), site.Y, WaterLayer.Tank, jet: false, power: 40f );
if ( MethRush.Cracks >= 3 && _rng.NextDouble() < 0.35 )
Squirt( site.X, site.Y, site.DirX, 0.2f, 4, 160f, WaterLayer.Tank );
}
}
static void Add( float x, float y, WaterLayer layer, bool jet, float power )
{
var ang = (float)(_rng.NextDouble() * Math.PI * 2);
var spd = power * (0.45f + (float)_rng.NextDouble() * 0.7f);
var spr = jet ? "water_jet"
: (_rng.Next( 3 ) switch
{
0 => "water_blob",
1 => "water_splash",
_ => "water_drop"
});
Push( new WaterDrop
{
X = x,
Y = y,
Vx = MathF.Cos( ang ) * spd,
Vy = MathF.Sin( ang ) * spd - 80f,
W = jet ? 16f : 8f + (float)_rng.NextDouble() * 12f,
H = jet ? 22f : 8f + (float)_rng.NextDouble() * 12f,
Life = 0.35f + (float)_rng.NextDouble() * 0.5f,
Sprite = spr,
Layer = layer,
Jet = jet
} );
}
static void Push( WaterDrop d )
{
d.MaxLife = d.Life;
if ( _drops.Count >= Cap )
_drops.RemoveAt( 0 );
_drops.Add( d );
}
}