Data model for a lake or coastline used by the water tool. Defines WaterLake and WaterLakeKind, stores control points, properties like Height, Depth, Waves, EntranceRadius, and utility methods (centre, point access, signed area, span, stable-hash addition).
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);
}
}