Water/WaterBody.cs
using System;
using Sandbox;
using Sandbox.Volumes;
namespace RedSnail.WaterTool;
/// <summary>
/// Defines a discrete body of water that participates in a renderer-driven water system.
/// Provides volume bounds, a physics hull for buoyancy/swimming, and renderer inclusion in one component.
/// Requires a WaterQuadRenderer present in the scene to produce a visible water surface.
/// </summary>
[Title("Water Body")]
[Category("Water")]
[Icon("water_drop")]
public sealed class WaterBody : VolumeComponent, Component.ExecuteInEditor
{
private HullCollider m_HullCollider;
private BBox m_LastLocalBounds;
[Property, Group("General")] public WaterBodyType WaterType { get; set; } = WaterBodyType.Ocean;
protected override void OnEnabled()
{
WaterManager.Current?.Register(this);
UpdateColliderState();
m_LastLocalBounds = SceneVolume.GetBounds();
}
protected override void OnDisabled()
{
WaterManager.Current?.Unregister(this);
m_HullCollider?.Destroy();
m_HullCollider = null;
}
protected override void OnUpdate()
{
BBox localBounds = SceneVolume.GetBounds();
if (localBounds != m_LastLocalBounds)
{
UpdateColliderState();
m_LastLocalBounds = localBounds;
}
}
protected override void DrawGizmos()
{
if (!Gizmo.IsSelected || !m_HullCollider.IsValid())
return;
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.LineBBox(m_HullCollider.LocalBounds);
}
// Bounds
public void SetBounds(BBox bounds)
{
SceneVolume = SceneVolume with { Box = bounds };
}
public float GetSurfaceHeight()
{
BBox local = SceneVolume.GetBounds();
return WorldTransform.PointToWorld(new Vector3(local.Center.x, local.Center.y, local.Maxs.z)).z;
}
public bool ContainsPointXY(Vector3 worldPosition)
{
BBox local = SceneVolume.GetBounds();
Vector3 point = WorldTransform.PointToLocal(worldPosition);
Vector3 half = local.Size * 0.5f;
return MathF.Abs(point.x - local.Center.x) <= half.x && MathF.Abs(point.y - local.Center.y) <= half.y;
}
public bool ContainsPointInVolume(Vector3 worldPosition)
{
BBox local = SceneVolume.GetBounds();
Vector3 point = WorldTransform.PointToLocal(worldPosition);
Vector3 half = local.Size * 0.5f;
return MathF.Abs(point.x - local.Center.x) <= half.x &&
MathF.Abs(point.y - local.Center.y) <= half.y &&
MathF.Abs(point.z - local.Center.z) <= half.z;
}
public (Vector3 Center, Vector3 Forward, Vector3 Up, Vector3 HalfExtents) GetWorldOBB()
{
BBox local = SceneVolume.GetBounds();
return (WorldTransform.PointToWorld(local.Center), WorldRotation.Forward, WorldTransform.Up, local.Size * 0.5f);
}
// Wave queries
public Vector3 GetWaveDisplacementAt(Vector3 _WorldPosition)
{
WaterDefinition profile = WaterManager.GetWaveProfile(WaterType);
return profile.IsValid() ? WaterWaveUtility.ComputeDisplacementAt(_WorldPosition, profile) : Vector3.Zero;
}
public Vector3 GetWaveVelocityAt(Vector3 _WorldPosition)
{
WaterDefinition profile = WaterManager.GetWaveProfile(WaterType);
return profile.IsValid() ? WaterWaveUtility.ComputeVelocityAt(_WorldPosition, profile) : Vector3.Zero;
}
public float GetWaveHeightAt(Vector3 _WorldPosition) => GetSurfaceHeight() + GetWaveDisplacementAt(_WorldPosition).z;
internal float GetVerticalDistanceToSurface(Vector3 _WorldPosition) => MathF.Abs(_WorldPosition.z - GetSurfaceHeight());
private void UpdateColliderState()
{
BBox local = SceneVolume.GetBounds();
m_HullCollider = GetOrAddComponent<HullCollider>();
m_HullCollider.Flags |= ComponentFlags.Hidden;
m_HullCollider.Static = true;
m_HullCollider.Type = HullCollider.PrimitiveType.Box;
m_HullCollider.Center = local.Center;
m_HullCollider.BoxSize = local.Size;
m_HullCollider.IsTrigger = true;
Tags.Add("water");
}
}