Network/WaterNetwork.Queries.cs

Defines a WaterSample struct holding water state at a point (rest height, wave displacement, flow, waves, foam, depth, sea blend) and adds sampling/query methods on WaterNetwork to find the nearest water piece at a world position, compute displacement/height/flow and helper static accessors (TrySample, GetWaterHeightAt, GetFlatWaterHeightAt, GetFlowVelocityAt, GetWaveDisplacementAt, GetWaveVelocityAt, IsPositionInsideAny).

NetworkingFile Access
using System;
using Sandbox;

namespace RedSnail.WaterTool2;

/// <summary>
/// The water at a point, as gameplay sees it: where its surface is (at rest, and with its waves and ripples), how it
/// flows, and how deep it is.
/// </summary>
public readonly struct WaterSample
{
	/// <summary>
	/// The surface's height without its waves.
	/// </summary>
	public readonly float RestHeight;

	/// <summary>
	/// The surface's height with its waves and ripples: what floats sits on this.
	/// </summary>
	public readonly float Height;

	/// <summary>
	/// How the waves and ripples move the surface at this point: XY the waves' orbital motion, Z their height.
	/// </summary>
	public readonly Vector3 Displacement;

	/// <summary>
	/// The current, level, in units per second.
	/// </summary>
	public readonly Vector3 Flow;

	/// <summary>
	/// The wave strength here, relative to the network's wave profile.
	/// </summary>
	public readonly float Waves;

	public readonly float Foam;

	/// <summary>
	/// How deep the water is under its surface at rest.
	/// </summary>
	public readonly float Depth;

	/// <summary>
	/// How far the water here has handed over to the sea: 0 a river's or a lake's own, 1 the sea's.
	/// </summary>
	public readonly float SeaBlend;

	/// <summary>
	/// Where the water stops at the bottom.
	/// </summary>
	public float Bottom => RestHeight - Depth;



	public WaterSample(float _RestHeight, Vector3 _Displacement, Vector3 _Flow, float _Waves, float _Foam, float _Depth, float _SeaBlend = 0.0f)
	{
		RestHeight = _RestHeight;
		Height = _RestHeight + _Displacement.z;
		Displacement = _Displacement;
		Flow = _Flow;
		Waves = _Waves;
		Foam = _Foam;
		Depth = _Depth;
		SeaBlend = _SeaBlend;
	}
}



public sealed partial class WaterNetwork
{
	/// <summary>
	/// The water at a world position: the piece over or under it whose water column (surface down to bed) is nearest,
	/// else the open sea. False when there is no water there, or the position is below the bottom of the water.
	/// </summary>
	public bool Sample(Vector3 _Position, out WaterSample _Sample)
	{
		_Sample = default;

		var point = new Vector2(_Position.x, _Position.y);
		WaterPiece best = null;
		SurfacePoint surface = default;
		float bestDistance = float.MaxValue;

		foreach (var piece in m_Pieces.Values)
		{
			if (!piece.Mesh.TrySample(point, out SurfacePoint candidate))
				continue;

			// Water has a bottom: below it this is not the water we are in.
			if (_Position.z < candidate.Height - piece.Depth)
				continue;

			float distance = MathF.Abs(_Position.z - candidate.Height);

			if (distance >= bestDistance)
				continue;

			best = piece;
			surface = candidate;
			bestDistance = distance;
		}

		if (best is null)
			return SampleSea(_Position, out _Sample);

		Vector3 displacement = WaterWaves.FlowingDisplacement(point, surface.Velocity, ResolvedProfile, Time.Now, FlowCycle) * surface.Waves;

		// Where a river hands over to the sea, its waves give way to the sea's.
		if (surface.SeaBlend > 0.0f)
			displacement = Vector3.Lerp(displacement, SeaDisplacementAt(point), surface.SeaBlend);

		displacement.z += ComputeRippleHeight(point);

		_Sample = new WaterSample(surface.Height, displacement, new Vector3(surface.Velocity.x, surface.Velocity.y, 0.0f), surface.Waves, surface.Foam, best.Depth, surface.SeaBlend);

		return true;
	}



	/// <summary>
	/// The water at a world position, in the current network. False when there is none.
	/// </summary>
	public static bool TrySample(Vector3 _Position, out WaterSample _Sample)
	{
		_Sample = default;

		return Current.IsValid() && Current.Sample(_Position, out _Sample);
	}



	/// <summary>
	/// The height of the water's surface (waves and ripples included) over or under a position, or float.MinValue
	/// where there is no water.
	/// </summary>
	public static float GetWaterHeightAt(Vector3 _Position)
	{
		return TrySample(_Position, out var sample) ? sample.Height : float.MinValue;
	}



	/// <summary>
	/// The height of the water's surface at rest, without waves, or float.MinValue where there is no water.
	/// </summary>
	public static float GetFlatWaterHeightAt(Vector3 _Position)
	{
		return TrySample(_Position, out var sample) ? sample.RestHeight : float.MinValue;
	}



	/// <summary>
	/// The current at a position (level, in units per second). Zero where there is no water, or the water is still.
	/// </summary>
	public static Vector3 GetFlowVelocityAt(Vector3 _Position)
	{
		return TrySample(_Position, out var sample) ? sample.Flow : Vector3.Zero;
	}



	/// <summary>
	/// How the waves and ripples move the surface at a position: XY the waves' orbital motion, Z their height.
	/// </summary>
	public static Vector3 GetWaveDisplacementAt(Vector3 _Position)
	{
		return TrySample(_Position, out var sample) ? sample.Displacement : Vector3.Zero;
	}



	/// <summary>
	/// How fast the surface itself moves at a position: the waves' orbital motion (the current comes from
	/// <see cref="GetFlowVelocityAt"/>).
	/// </summary>
	public static Vector3 GetWaveVelocityAt(Vector3 _Position)
	{
		if (!Current.IsValid() || !Current.Sample(_Position, out var sample))
			return Vector3.Zero;

		var point = new Vector2(_Position.x, _Position.y);
		var network = Current;

		Vector3 own = sample.SeaBlend < 1.0f
			? WaterWaves.SurfaceVelocity(point, new Vector2(sample.Flow.x, sample.Flow.y), sample.Waves, network.ResolvedProfile, Time.Now, network.FlowCycle)
			: Vector3.Zero;

		if (sample.SeaBlend <= 0.0f)
			return own;

		Vector3 sea = WaterWaves.SurfaceVelocity(point, Vector2.Zero, network.SeaWaves, network.ResolvedSeaProfile, Time.Now, network.FlowCycle);

		return Vector3.Lerp(own, sea, sample.SeaBlend);
	}



	/// <summary>
	/// Whether a position is in the water: under its surface, waves included, and above its bottom.
	/// </summary>
	public static bool IsPositionInsideAny(Vector3 _Position)
	{
		return TrySample(_Position, out var sample) && _Position.z <= sample.Height;
	}
}