Building/RiverFlow.cs

Represents the computed state of water along a single river, holding sampled speeds, trim/blend/wave parameters and providing lookup and hashing utilities. It computes interpolated speed at distance, blended wave strength at distance, a smooth ramp function, quantization of speeds, and adds relevant fields to a StableHash for change detection.

using System;

namespace RedSnail.WaterTool2;

/// <summary>
/// The state of the water along one river, worked out by the network from the whole graph: how fast it runs at every
/// distance (it arrives with whatever flows into the river, speeds up on slopes and settles back to the river's own
/// speed on the flat), and how its wave strength blends into whatever it meets at each end.
/// </summary>
internal sealed class RiverFlow
{
	/// <summary>
	/// Speeds are kept to a quarter of a unit per second. A small change upstream then dies out a little way down
	/// the network instead of nudging, and rebuilding, every river below it.
	/// </summary>
	public const float SpeedQuantum = 0.25f;

	public float Length;
	public float Step;
	public float[] Speeds;

	/// <summary>
	/// The range of the river that is built as river: past the confluence or the lake at each end.
	/// </summary>
	public float TrimStart;
	public float TrimEnd;

	/// <summary>
	/// The river's own wave strength, the strength it has to arrive at on its first and last rows, and how far from
	/// those rows the blend reaches.
	/// </summary>
	public float Waves;
	public float WavesStart;
	public float WavesEnd;
	public float BlendStart;
	public float BlendEnd;



	public float SpeedAt(float _Distance)
	{
		if (Speeds is null || Speeds.Length == 0)
			return 0.0f;

		if (Speeds.Length == 1 || Step <= 0.0f)
			return Speeds[0];

		float position = Math.Clamp(_Distance, 0.0f, Length) / Step;
		int index = Math.Min((int)position, Speeds.Length - 2);
		float fraction = Math.Clamp(position - index, 0.0f, 1.0f);

		return Speeds[index] + (Speeds[index + 1] - Speeds[index]) * fraction;
	}



	public float WavesAt(float _Distance)
	{
		float waves = Waves;

		float fromStart = _Distance - TrimStart;
		float fromEnd = Length - TrimEnd - _Distance;

		waves += (WavesStart - Waves) * Ramp(fromStart, BlendStart);
		waves += (WavesEnd - Waves) * Ramp(fromEnd, BlendEnd);

		return MathF.Max(0.0f, waves);
	}



	/// <summary>
	/// 1 on the row the blend starts from, easing to 0 at the blend's reach.
	/// </summary>
	private static float Ramp(float _Distance, float _Reach)
	{
		if (_Reach <= 0.001f)
			return _Distance <= 0.001f ? 1.0f : 0.0f;

		float t = Math.Clamp(_Distance / _Reach, 0.0f, 1.0f);

		return 1.0f - t * t * (3.0f - 2.0f * t);
	}



	public static float Quantize(float _Speed)
	{
		return MathF.Round(_Speed / SpeedQuantum) * SpeedQuantum;
	}



	public void AddToHash(ref StableHash _Hash, float _From, float _To)
	{
		_Hash.Add(Length);
		_Hash.Add(Step);
		_Hash.Add(TrimStart);
		_Hash.Add(TrimEnd);
		_Hash.Add(Waves);
		_Hash.Add(WavesStart);
		_Hash.Add(WavesEnd);
		_Hash.Add(BlendStart);
		_Hash.Add(BlendEnd);

		if (Speeds is null || Step <= 0.0f)
			return;

		// Only the samples that reach into the stretch: a speed change further down does not touch it.
		int first = Math.Max(0, (int)MathF.Floor(_From / Step) - 1);
		int last = Math.Min(Speeds.Length - 1, (int)MathF.Ceiling(_To / Step) + 1);

		for (int i = first; i <= last; i++)
			_Hash.Add(Speeds[i]);
	}
}