DiamondAtmosphere.Shatter.cs
using System;

namespace Diamonds;

public sealed partial class DiamondAtmosphere
{
	const int MaxShatterPushes = 8;
	float shatterPushCooldown;
	float? shatterPushInterval;
	public float ShatterPushInterval { get => shatterPushInterval ?? 0.1f; set => shatterPushInterval = Math.Clamp( value, 0, 1 ); }

	void PushShatterDye( DiamondBoard board, int first )
	{
		int count = board.ShatterBursts.Count - first;
		if ( count <= 0 || shatterPushCooldown > 0 ) return;
		shatterPushCooldown = ShatterPushInterval;
		int pushes = Math.Min( count, MaxShatterPushes );
		// Sample across large batches without allocating or queuing delayed explosions.
		// Emission still happens for every gem, even when its push is skipped.
		for ( int i = 0; i < pushes; i++ )
		{
			int index = pushes == 1 ? first : first + i * (count - 1) / (pushes - 1);
			PushShatter( board.ShatterBursts[index].Cell );
		}
	}

	void PushShatter( DiamondBoard.Diamond cell )
	{
		const float radius = 126f, strength = 8f;
		float cx = DiamondBoard.LaneX( cell.Lane ), cy = cell.Y;
		var (gx, gy) = WorldToGrid( cx, cy );
		float reach = radius / CellSize;
		int left = Math.Max( 1, (int)MathF.Floor( gx - reach ) );
		int right = Math.Min( Columns - 2, (int)MathF.Ceiling( gx + reach ) );
		int top = Math.Max( 1, (int)MathF.Floor( gy - reach ) );
		int bottom = Math.Min( Rows - 2, (int)MathF.Ceiling( gy + reach ) );
		for ( int y = top; y <= bottom; y++ )
		for ( int x = left; x <= right; x++ )
		{
			int n = y * Columns + x;
			if ( wallDistance[n] < 0 ) continue;
			var point = GridToWorld( x, y );
			float dx = (point.x - cx) / radius, dy = (point.y - cy) / radius;
			float distanceSquared = dx * dx + dy * dy;
			if ( distanceSquared >= 1 ) continue;
			// Smooth radial impulse, zero at the center and perimeter. Reuse the
			// normal 30 Hz solver and obstacle-aware transport; no extra fluid pass.
			float falloff = 1 - distanceSquared;
			float force = strength * 3.5f * falloff * falloff;
			float vx = velocityX[n] + dx * force, vy = velocityY[n] + dy * force;
			float speedSquared = vx * vx + vy * vy;
			float scale = speedSquared > MaxFlowVelocity * MaxFlowVelocity ? MaxFlowVelocity / MathF.Sqrt( speedSquared ) : 1;
			velocityX[n] = vx * scale; velocityY[n] = vy * scale;
			influence[n] = MathF.Max( influence[n], falloff );
		}
	}
}