DiamondAtmosphere.Static.cs
using System;
using Sandbox;

namespace Diamonds;

public sealed partial class DiamondAtmosphere
{
	bool disableFluid;
	float[] staticDye;
	DiamondBoard staticBoard;
	int staticResetVersion;
	int staticLayoutRevision;
	Random staticRandom;

	/// <summary>Replace live fluid with a sparse, dim field baked once per run.</summary>
	public bool DisableFluid
	{
		get => disableFluid;
		set
		{
			if ( value == disableFluid ) return;
			disableFluid = value;
			DyeVersion++;
			previousBoard = null;
			synchronizeOnEnable = true;
		}
	}

	public float RenderOpacity => Opacity * (DisableFluid ? 0.5f : 1f);

	void PrepareStaticBackground( DiamondBoard board )
	{
		if ( staticDye?.Length == Count * DiamondBoard.ColorCount &&
			staticBoard == board && staticResetVersion == board.ResetVersion && staticLayoutRevision == 2 ) return;
		staticRandom ??= new Random();
		staticDye = new float[Count * DiamondBoard.ColorCount];
		staticBoard = board;
		staticResetVersion = board.ResetVersion;
		staticLayoutRevision = 2;
		float Range( float min, float max ) => min + (max - min) * (float)staticRandom.NextDouble();

		// Five separated seed patches, with their colors shuffled independently of gameplay.
		var centers = new Vector2[DiamondBoard.ColorCount];
		var radii = new Vector2[centers.Length];
		var colors = new int[] { 0, 1, 2, 3, 4 };
		for ( int i = colors.Length - 1; i > 0; i-- )
		{
			int j = staticRandom.Next( i + 1 );
			(colors[i], colors[j]) = (colors[j], colors[i]);
		}
		for ( int i = 0; i < centers.Length; i++ )
		{
			centers[i] = new Vector2( DiamondBoard.BoardWidth * Range( i % 2 == 0 ? 0.12f : 0.54f, i % 2 == 0 ? 0.46f : 0.88f ),
				DiamondBoard.BoardHeight * ((i + Range( 0.2f, 0.8f )) / centers.Length) );
			radii[i] = new Vector2( Range( 170, 245 ), Range( 150, 220 ) );
		}

		var forces = new Vector2[7];
		var reach = new float[forces.Length];
		var turns = new float[forces.Length];
		for ( int i = 0; i < forces.Length; i++ )
		{
			forces[i] = new Vector2( Range( 0, DiamondBoard.BoardWidth ), Range( 0, DiamondBoard.BoardHeight ) );
			reach[i] = Range( 180, 380 );
			turns[i] = Range( 0.65f, 1.15f ) * (staticRandom.Next( 2 ) == 0 ? -1 : 1);
		}
		// Apply weaker swirls together over eight small steps. A single strong twist
		// per force stretched some seeds into sub-cell ribbons that looked dotted.
		// Average four sub-cell samples to filter the remaining narrow edges before
		// the normal Painter texture filtering. All work still happens only at bake time.
		const int steps = 8, samplesPerAxis = 2;
		const float duration = 1.5f;
		// The force field is much broader than a cell. Trace its corners once and
		// interpolate the four dye samples, rather than repeating the costly trace.
		const int traceColumns = Columns + 1;
		var traced = new Vector2[traceColumns * (Rows + 1)];
		for ( int y = 0; y <= Rows; y++ )
		for ( int x = 0; x <= Columns; x++ )
		{
			var point = GridToWorld( x, y ) - new Vector2( CellSize * 0.5f );
			// Keep a filtering margin outside the clipped board; the rest is invisible.
			if ( point.x >= -CellSize * 2 && point.x <= DiamondBoard.BoardWidth + CellSize * 2 &&
				point.y >= -CellSize * 2 && point.y <= DiamondBoard.BoardHeight + CellSize * 2 )
			{
				for ( int step = 0; step < steps; step++ )
				for ( int i = 0; i < forces.Length; i++ )
				{
					var offset = point - forces[i];
					float weight = MathF.Exp( -offset.LengthSquared / (reach[i] * reach[i]) );
					float angle = turns[i] * weight * (duration / steps);
					float sin = MathF.Sin( angle ), cos = MathF.Cos( angle );
					point = forces[i] + new Vector2( offset.x * cos - offset.y * sin, offset.x * sin + offset.y * cos );
				}
			}
			traced[y * traceColumns + x] = point;
		}
		for ( int y = 0; y < Rows; y++ )
		for ( int x = 0; x < Columns; x++ )
		{
			int n = y * traceColumns + x;
			for ( int sy = 0; sy < samplesPerAxis; sy++ )
			for ( int sx = 0; sx < samplesPerAxis; sx++ )
			{
				float u = (sx + 0.5f) / samplesPerAxis, v = (sy + 0.5f) / samplesPerAxis;
				var point = (traced[n] * (1 - u) + traced[n + 1] * u) * (1 - v) +
					(traced[n + traceColumns] * (1 - u) + traced[n + traceColumns + 1] * u) * v;
				for ( int i = 0; i < centers.Length; i++ )
				{
					var offset = (point - centers[i]) / radii[i];
					float amount = MathF.Max( 0, 1 - offset.LengthSquared );
					staticDye[(y * Columns + x) * DiamondBoard.ColorCount + colors[i]] +=
						0.6f * amount * amount / (samplesPerAxis * samplesPerAxis);
				}
			}
		}
		DyeVersion++;
	}
}