Code/Network/WaterLake.cs

A data model representing a still body of water (lake or coastline) for the water tool. It stores control points, kind (Lake or Coast), height, smoothing, wave/depth/entrance parameters, openings, and provides utility methods (centre, point at index, signed area, span) plus hashing support.

File Access
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Sandbox;

namespace RedSnail.WaterTool2;

/// <summary>
/// What a shore drawn round a shape is: a lake's, the water inside it, or a coast's, the land inside it and the sea
/// all round.
/// </summary>
public enum WaterLakeKind
{
	/// <summary>
	/// The water fills the shore, at the lake's own level.
	/// </summary>
	Lake,

	/// <summary>
	/// A coastline drawn round land: the sea lies outside it, at the network's sea level, and stays out of everything
	/// inside it however low the ground there. Rivers open into the sea through it the way they open into a lake.
	/// </summary>
	Coast
}



/// <summary>
/// A still body of water with a shore of any shape: a lake, a pond, a pool, a reservoir. It is flat (everything sits at
/// <see cref="Height"/>) and rivers open into it, or out of it, where their nodes sit on its shore. Like a road
/// network's parking lot, it is a place rivers end at rather than pass through. A <see cref="WaterLakeKind.Coast"/> is
/// the same shape turned inside out: the land is inside, the sea outside, and rivers open into the sea through it.
/// </summary>
public sealed class WaterLake
{
	public const int MinPoints = 3;

	public int Id { get; set; }

	/// <summary>
	/// A lake, or a coastline with the sea outside it.
	/// </summary>
	public WaterLakeKind Kind { get; set; } = WaterLakeKind.Lake;

	[Hide, JsonIgnore] public bool IsCoast => Kind == WaterLakeKind.Coast;

	/// <summary>
	/// The shore's control points, in network space and in order round the lake. Only their X and Y count: the whole
	/// lake sits at <see cref="Height"/>.
	/// </summary>
	public List<Vector3> Points { get; set; } = new();

	/// <summary>
	/// The water level, in network space. A coast's is always the network's sea level.
	/// </summary>
	public float Height { get; set; }

	/// <summary>
	/// On, the shore is a smooth curve through the points (a lake, a pond). Off, it runs straight from point to point
	/// (a pool, a basin, a harbour).
	/// </summary>
	public bool Smooth { get; set; } = true;

	/// <summary>
	/// Wave strength, relative to the network's wave profile.
	/// </summary>
	[Range(0.0f, 2.0f)] public float Waves { get; set { field = value.Clamp(0.0f, 10.0f); } } = 1.0f;

	/// <summary>
	/// How deep the water is under its surface. Only gameplay reads it: swimming, drowning and buoyancy.
	/// </summary>
	[Range(10.0f, 5000.0f)] public float Depth { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;

	/// <summary>
	/// How far the shore flares out into each river that opens into the lake, the way a junction's banks are rounded.
	/// The lake's surface fills the flare, and the river starts at its far end.
	/// </summary>
	[Range(0.0f, 2000.0f)] public float EntranceRadius { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 200.0f;

	/// <summary>
	/// The nodes on the shore that rivers open into the lake at. They are kept on the shore as the lake is moved and
	/// reshaped, and the rivers at them are made to meet the shore square on.
	/// </summary>
	public List<int> Openings { get; set; } = new();



	/// <summary>
	/// The middle of the lake's control points, at its water level.
	/// </summary>
	[Hide, JsonIgnore]
	public Vector3 Centre
	{
		get
		{
			if (Points.Count == 0)
				return new Vector3(0.0f, 0.0f, Height);

			Vector3 sum = Vector3.Zero;

			foreach (var point in Points)
				sum += point;

			return (sum / Points.Count).WithZ(Height);
		}
	}



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add((int)Kind);
		_Hash.Add(Points.Count);

		foreach (var point in Points)
		{
			_Hash.Add(point.x);
			_Hash.Add(point.y);
		}

		_Hash.Add(Height);
		_Hash.Add(Smooth);
		_Hash.Add(Waves);
		_Hash.Add(EntranceRadius);
	}



	/// <summary>
	/// The control points as they sit on the water: their own X and Y, at the lake's height.
	/// </summary>
	public Vector3 PointAt(int _Index)
	{
		int count = Points.Count;

		if (count == 0)
			return new Vector3(0.0f, 0.0f, Height);

		return Points[((_Index % count) + count) % count].WithZ(Height);
	}



	/// <summary>
	/// Whether a lake shape of this many points can be built at all.
	/// </summary>
	[Hide, JsonIgnore] public bool IsValid => Points is not null && Points.Count >= MinPoints;



	/// <summary>
	/// The signed area of the control polygon, in plan: positive when the points run counter-clockwise seen from above.
	/// </summary>
	public float SignedArea()
	{
		float area = 0.0f;
		int count = Points.Count;

		for (int i = 0; i < count; i++)
		{
			Vector3 a = Points[i];
			Vector3 b = Points[(i + 1) % count];

			area += a.x * b.y - b.x * a.y;
		}

		return area * 0.5f;
	}



	/// <summary>
	/// Rough size of the lake, for picking and gizmos: the largest span of its control points.
	/// </summary>
	public float Span()
	{
		if (Points.Count == 0)
			return 0.0f;

		var bounds = new BBox(Points[0].WithZ(0.0f), Points[0].WithZ(0.0f));

		foreach (var point in Points)
			bounds = bounds.AddPoint(point.WithZ(0.0f));

		return MathF.Max(bounds.Size.x, bounds.Size.y);
	}
}