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.
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);
}
}