Code/OceanBuoyancy.cs
using System;

namespace OceanLib;

/// <summary>
/// Floats a rigidbody on the <see cref="Ocean"/> sea, through its CPU replica (<see cref="OceanWaves"/>). A grid of
/// points over the hull, each a spring pushing up by how far under the surface it is, so the hull tilts with the swell
/// under it; then everything the water does to a hull besides holding it up:
///
/// - Lift that knows its limits: it stiffens as the hull sinks towards its deck (the reserve a real hull has in its
///   sides), then stops growing once it is under - it can only push aside the water it fills - so no crest can fling
///   the boat out of the sea.
/// - Inertia: one slider from a cork that sits on the surface to a heavy hull that carries its fall into the water and
///   wallows up out of it, the waterline kept where it was.
/// - The water moving: every point is damped against the water's own motion, not the world's. Vertically that lets the
///   hull ride each wave rather than bounce off it; across the sea it is the wave's orbit carrying the hull - forward
///   on the crests, back in the troughs - and the surface drifting downwind, so a boat left alone in a storm is pushed
///   about and off downwind like the real thing.
/// - Drag shaped like a hull: a boat slips along its length but its keel and sides fight being pushed sideways, both
///   growing with the square of the speed, as water drag does; the turning is damped the same way.
/// - Slamming: a point driving into the water fast throws spray off the hull's side (see <see cref="OceanSpray"/>), and a
///   hard landing throws up a burst of foam round it (see <see cref="OceanFoamSource.Splash"/>).
/// </summary>
[Title( "Ocean Buoyancy" ), Category( "Ocean" ), Icon( "sailing" )]
public sealed class OceanBuoyancy : Component
{
	/// <summary>The sea to float on. Empty takes the scene's.</summary>
	[Property] public Ocean Ocean { get; set; }

	/// <summary>Points along the hull and across it.</summary>
	[Property, Group( "Points" ), Range( 1, 8 )] public int PointsAlong { get; set; } = 4;
	[Property, Group( "Points" ), Range( 1, 6 )] public int PointsAcross { get; set; } = 3;

	/// <summary>How far out to the hull's ends and sides the points reach, as a fraction of its half-size.</summary>
	[Property, Group( "Points" ), Range( 0.1f, 1 )] public Vector2 Spread { get; set; } = new( 0.8f, 0.75f );

	/// <summary>
	/// Raises the waterline the points answer to, in metres: higher floats the boat higher. The points sit at the
	/// middle of the hull's height.
	/// </summary>
	[Property, Group( "Float" ), Range( -3, 3 )] public float SurfaceOffset { get; set; } = 0.0f;

	/// <summary>Upward acceleration per unit of depth with no inertia: stiffer settles shallower and quicker.</summary>
	[Property, Group( "Float" ), Range( 5, 200 )] public float Stiffness { get; set; } = 40.0f;

	/// <summary>Damping of each point's rise and fall against the water's own, with no inertia.</summary>
	[Property, Group( "Float" ), Range( 0, 20 )] public float Damping { get; set; } = 6.0f;

	/// <summary>
	/// 0 floats like a cork on the surface; 1 is a heavy hull that ploughs into the water as it drops off a crest and
	/// rises back slowly. The waterline at rest stays the same.
	/// </summary>
	[Property, Group( "Float" ), Range( 0, 1 )] public float Inertia { get; set; } = 0.4f;

	/// <summary>How much harder the water pushes back as the hull sinks past its resting depth: its sides' reserve.</summary>
	[Property, Group( "Float" ), Range( 0, 4 )] public float Reserve { get; set; } = 1.0f;

	/// <summary>The most the water can lift the hull, as a multiple of its weight.</summary>
	[Property, Group( "Float" ), Range( 1, 10 )] public float MaxLift { get; set; } = 3.0f;

	/// <summary>The fastest the hull is made to follow the surface rising or falling, in metres per second.</summary>
	[Property, Group( "Float" ), Range( 0.5f, 10 )] public float MaxWaterRise { get; set; } = 3.0f;

	/// <summary>How hard the water's own movement carries the hull along: 0 ignores the waves' orbit and the drift.</summary>
	[Property, Group( "Water Movement" ), Range( 0, 1 )] public float WaveTransport { get; set; } = 1.0f;

	/// <summary>How fast the surface drifts downwind in a storm, in metres per second - less in a calmer sea.</summary>
	[Property, Group( "Water Movement" ), Range( 0, 3 )] public float WindDrift { get; set; } = 0.6f;

	/// <summary>Drag along the hull: low, a hull is shaped to slip through the water that way. Per metre.</summary>
	[Property, Group( "Drag" ), Range( 0, 2 )] public float LengthwiseDrag { get; set; } = 0.06f;

	/// <summary>Drag across the hull: high, the keel and sides fight being pushed sideways. Per metre.</summary>
	[Property, Group( "Drag" ), Range( 0, 5 )] public float SidewaysDrag { get; set; } = 0.8f;

	/// <summary>Drag of the water on the hull's turning, per second.</summary>
	[Property, Group( "Drag" ), Range( 0, 10 )] public float AngularDrag { get; set; } = 1.5f;

	/// <summary>How fast a point must hit the water, in metres per second, to throw up foam.</summary>
	[Property, Group( "Splash" ), Range( 0.5f, 10 )] public float SplashSpeed { get; set; } = 2.0f;

	/// <summary>How much foam a hard slam throws up.</summary>
	[Property, Group( "Splash" ), Range( 0, 10 )] public float SplashFoam { get; set; } = 3.0f;

	/// <summary>How fast a point must drive into the water, in metres per second, to throw spray (see <see cref="OceanSpray"/>).</summary>
	[Property, Group( "Splash" ), Range( 0.5f, 10 )] public float SprayImpact { get; set; } = 1.5f;

	/// <summary>Scales the spray this hull throws.</summary>
	[Property, Group( "Splash" ), Range( 0, 4 )] public float SprayAmount { get; set; } = 1.0f;

	/// <summary>How much of the hull's points are under water, 0 to 1.</summary>
	public float Submerged { get; private set; }

	/// <summary>The sea's height under the hull's centre, in world units.</summary>
	public float WaterHeight { get; private set; }

	/// <summary>The water's movement under the hull, in world units per second.</summary>
	public Vector3 WaterVelocity { get; private set; }

	private Rigidbody m_Body;
	private Collider m_Collider;
	private OceanFoamSource m_Foam;
	private float[] m_LastWater = Array.Empty<float>();
	private bool[] m_WasUnder = Array.Empty<bool>();
	private float[] m_SprayCooldown = Array.Empty<float>();
	private Vector3[] m_Points = Array.Empty<Vector3>();

	private const float UnitsPerMetre = Ocean.UnitsPerMetre;

	protected override void OnFixedUpdate()
	{
		if ( IsProxy )
			return;

		var ocean = Ocean.IsValid() ? Ocean : Ocean.Current;
		m_Body ??= Components.Get<Rigidbody>();
		m_Collider ??= Components.Get<Collider>();
		m_Foam ??= Components.Get<OceanFoamSource>();

		if ( !ocean.IsValid() || ocean.Waves is null || !m_Body.IsValid() || !m_Body.MotionEnabled || !m_Collider.IsValid() )
			return;

		LayOutPoints();

		float gravity = Scene.PhysicsWorld.Gravity.Length;
		float mass = m_Body.Mass;
		float share = mass / m_Points.Length;

		// Inertia softens the spring and the damper together, geometrically - halfway is halfway in feel - and lifts the
		// waterline by however much further the softer spring sags, so only the plunge changes, not where it floats.
		float stiffness = Stiffness * MathF.Pow( 0.2f, Inertia );
		float damping = Damping * MathF.Pow( 0.2f, Inertia );
		float rest = gravity / stiffness;
		float offset = SurfaceOffset * UnitsPerMetre + rest - gravity / Stiffness;
		float maxLift = MaxLift * gravity * share;

		WaterHeight = ocean.HeightAt( WorldPosition );

		// The water's own motion under the hull: the waves' orbit, plus the surface drifting downwind in a big sea.
		var orbit = ocean.WaterVelocityAt( WorldPosition ).WithZ( 0 );
		// Full drift in a storm of eight-metre waves.
		float drift = WindDrift * UnitsPerMetre * (ocean.Sea.SignificantHeight / 8.0f).Clamp( 0.0f, 1.5f );
		WaterVelocity = (orbit + ocean.WindDirection * drift) * WaveTransport;

		int under = 0;
		float slam = 0.0f;

		for ( int i = 0; i < m_Points.Length; i++ )
		{
			var point = WorldTransform.PointToWorld( m_Points[i] );
			float water = ocean.HeightAt( point );

			// How fast the sea is rising or falling right here - held to what a real sea's surface does. A sharp crest
			// sweeping under a point makes the height there jump faster than any water moves, and following that
			// would throw the hull clear of the crest.
			float maxRise = MaxWaterRise * UnitsPerMetre;
			float waterSpeed = float.IsNaN( m_LastWater[i] ) ? 0.0f : ((water - m_LastWater[i]) / Time.Delta).Clamp( -maxRise, maxRise );
			m_LastWater[i] = water;

			float depth = water + offset - point.z;
			float pointSpeed = m_Body.GetVelocityAtPoint( point ).z;

			if ( depth <= 0.0f )
			{
				m_WasUnder[i] = false;
				continue;
			}

			under++;

			// Hitting the water: how fast this point is going down into it, relative to the water's own rise.
			float impact = (waterSpeed - pointSpeed) / UnitsPerMetre;

			if ( !m_WasUnder[i] )
				slam = MathF.Max( slam, impact );

			m_WasUnder[i] = true;

			// Cutting into a wave hard enough throws spray off the hull's side here.
			m_SprayCooldown[i] -= Time.Delta;
			if ( impact > SprayImpact && m_SprayCooldown[i] <= 0.0f )
			{
				m_SprayCooldown[i] = 0.15f;
				ThrowSpray( m_Points[i], water, impact / SprayImpact - 1.0f );
			}

			// Linear to the resting depth, then stiffening the further past it - the hull's sides. The water can't push
			// up harder than the cap, lift and damping together, whatever the wave is doing.
			float past = MathF.Max( 0.0f, depth - rest );
			float lift = (depth + Reserve * past * past / rest) * stiffness * share;
			float damper = (waterSpeed - pointSpeed) * damping * share;

			m_Body.ApplyForceAt( point, Vector3.Up * MathF.Min( lift + damper, maxLift ) );
		}

		Submerged = (float)under / m_Points.Length;

		if ( Submerged <= 0.0f )
			return;

		ApplyDrag( mass );

		if ( slam > SplashSpeed && m_Foam.IsValid() )
			m_Foam.Splash( SplashFoam * MathF.Min( slam / SplashSpeed - 1.0f, 2.0f ) );
	}

	protected override void DrawGizmos()
	{
		// The points the sea pushes on: blue under water, white out of it, and the water's movement under the hull.
		for ( int i = 0; i < m_Points.Length; i++ )
		{
			Gizmo.Draw.Color = Game.IsPlaying && m_WasUnder[i] ? Color.Cyan : Color.White;
			Gizmo.Draw.SolidSphere( m_Points[i], 3.0f );
		}

		if ( Game.IsPlaying && Submerged > 0.0f )
		{
			Gizmo.Draw.Color = Color.Cyan;
			Gizmo.Draw.Arrow( Vector3.Zero, WorldTransform.NormalToLocal( WaterVelocity ), 6.0f, 3.0f );
		}
	}

	/// <summary>
	/// Spray off the hull's side nearest a point, at the waterline: thrown out from the side, forward too at the bow, and
	/// carried along with the hull.
	/// </summary>
	private void ThrowSpray( Vector3 localPoint, float waterZ, float strength )
	{
		var spray = OceanSpray.Current;

		if ( !spray.IsValid() || strength <= 0.0f )
			return;

		var bounds = m_Collider.LocalBounds;
		float side = MathF.Abs( localPoint.y - bounds.Center.y ) > 1.0f ? MathF.Sign( localPoint.y - bounds.Center.y ) : (Game.Random.Float() < 0.5f ? -1.0f : 1.0f);
		var edge = new Vector3( localPoint.x, bounds.Center.y + side * bounds.Extents.y * 0.95f, bounds.Center.z );
		var at = WorldTransform.PointToWorld( edge ).WithZ( waterZ );

		// Out from the side, and from the bow forward as well: the bow throws its spray ahead and to both sides.
		float bow = ((localPoint.x - bounds.Center.x) / bounds.Extents.x).Clamp( 0.0f, 1.0f );
		var outward = WorldRotation.Left * side + WorldRotation.Forward * bow * 0.8f;

		spray.Emit( at, m_Body.GetVelocityAtPoint( at ), outward, strength * SprayAmount );
	}

	/// <summary>
	/// The water's drag on the hull moving through it: slight along the hull, strong across it, both with the square of
	/// the speed against the water - so the water's own movement pulls the hull along with it.
	/// </summary>
	private void ApplyDrag( float mass )
	{
		var relative = (m_Body.Velocity - WaterVelocity).WithZ( 0 ) / UnitsPerMetre;
		var forward = WorldRotation.Forward.WithZ( 0 );
		forward = forward.Length > 1e-3f ? forward.Normal : Vector3.Forward;
		var side = new Vector3( -forward.y, forward.x, 0 );

		float along = Vector3.Dot( relative, forward );
		float across = Vector3.Dot( relative, side );

		// Quadratic, as water drag is, with a small linear part so a hull nearly at rest still settles.
		float fAlong = -(LengthwiseDrag * MathF.Abs( along ) + 0.05f) * along;
		float fAcross = -(SidewaysDrag * MathF.Abs( across ) + 0.3f) * across;

		var force = (forward * fAlong + side * fAcross) * mass * UnitsPerMetre * Submerged;
		m_Body.ApplyForce( force );

		var angular = m_Body.AngularVelocity;
		m_Body.AngularVelocity = angular * MathF.Max( 0.0f, 1.0f - AngularDrag * Submerged * Time.Delta * (1.0f + angular.Length * 0.5f) );
	}

	private void LayOutPoints()
	{
		int count = PointsAlong * PointsAcross;

		if ( m_Points.Length == count )
			return;

		var bounds = m_Collider.LocalBounds;
		m_Points = new Vector3[count];
		m_LastWater = new float[count];
		m_WasUnder = new bool[count];
		m_SprayCooldown = new float[count];
		Array.Fill( m_LastWater, float.NaN );
		Array.Fill( m_WasUnder, true );

		for ( int a = 0; a < PointsAlong; a++ )
		{
			for ( int b = 0; b < PointsAcross; b++ )
			{
				float x = PointsAlong == 1 ? 0.0f : (a / (PointsAlong - 1.0f) * 2.0f - 1.0f) * Spread.x;
				float y = PointsAcross == 1 ? 0.0f : (b / (PointsAcross - 1.0f) * 2.0f - 1.0f) * Spread.y;
				m_Points[a * PointsAcross + b] = bounds.Center + new Vector3( x * bounds.Extents.x, y * bounds.Extents.y, 0.0f );
			}
		}
	}
}