Code/Gameplay/MoveModeSwimFixed.cs

MoveModeSwimFixed is a MoveMode implementation for player swimming on a water network surface. It tracks water contact level, plays enter/exit splashes and footsteps, toggles a swimming loop sound, disables gravity and adjusts damping while swimming, applies water current forces, and allows jump input to ascend.

File Access
using Sandbox;
using Sandbox.Movement;

namespace RedSnail.WaterTool2;

/// <summary>
/// Swimming for the PlayerController, on the water network's surface (waves included): splashes going in and out,
/// footsteps in the shallows, and the current dragging the swimmer along.
/// </summary>
[Title("MoveMode - Swim (Water Tool 2)")]
[Category("Water Tool 2")]
[Icon("scuba_diving")]
public sealed class MoveModeSwimFixed : MoveMode
{
	private bool m_WasInWater;
	private bool m_HasTouchedWater;

	private TimeSince m_TimeSinceStep;

	[Property] public int Priority { get; set; } = 10;

	/// <summary>
	/// How much of the body has to be under water to swim, from the feet (0) to the head (1).
	/// </summary>
	[Property, Range(0, 1)] public float SwimLevel { get; set; } = 0.7f;

	/// <summary>
	/// How much of the body is under water, from the feet (0) to the head (1).
	/// </summary>
	public float WaterLevel { get; private set; }

	/// <summary>
	/// How hard the current grips the swimmer: the rate (per second) the body's velocity is pulled towards the flow.
	/// </summary>
	[Property, Group("Current"), Range(0.1f, 10.0f)] private float CurrentDrag { get; set; } = 1.5f;

	[Property, Group("Sounds")] public SoundEvent WaterSplashEnter { get; set; }
	[Property, Group("Sounds")] public SoundEvent WaterSplashExit { get; set; }
	[Property, Group("Sounds")] public SoundEvent WaterFootsteps { get; set; }
	[Property, Group("Sounds")] public SoundPointComponent WaterSwimmingLoop { get; set; }



	protected override void OnStart()
	{
		if (WaterSwimmingLoop.IsValid())
			WaterSwimmingLoop.Enabled = false;
	}



	public override void UpdateRigidBody(Rigidbody _Body)
	{
		_Body.Gravity = false;
		_Body.LinearDamping = 3.3f;
		_Body.AngularDamping = 1.0f;
	}



	public override int Score(PlayerController _Controller)
	{
		return WaterLevel > SwimLevel ? Priority : -100;
	}



	public override void OnModeBegin()
	{
		Controller.IsSwimming = true;

		if (WaterSwimmingLoop.IsValid())
			WaterSwimmingLoop.Enabled = true;
	}



	public override void OnModeEnd(MoveMode _Next)
	{
		Controller.IsSwimming = false;

		if (WaterSwimmingLoop.IsValid())
			WaterSwimmingLoop.Enabled = false;

		// Jumping out of the water.
		if (Input.Down("Jump"))
			Controller.Jump(Vector3.Up * 300);
	}



	protected override void OnFixedUpdate()
	{
		UpdateWaterLevel();

		if (m_HasTouchedWater != m_WasInWater)
		{
			var splash = m_HasTouchedWater ? WaterSplashEnter : WaterSplashExit;

			if (splash.IsValid())
				Sound.Play(splash, WorldPosition);

			m_WasInWater = m_HasTouchedWater;
		}

		float stepInterval = (Controller.WalkSpeed * 0.5f / (Controller.Velocity.Length + 0.001f)).Clamp(0.2f, 1.0f);

		if (m_HasTouchedWater && Controller.IsOnGround && Controller.Velocity.Length > 10 && m_TimeSinceStep > stepInterval)
		{
			m_TimeSinceStep = 0;

			if (WaterFootsteps.IsValid())
				Sound.Play(WaterFootsteps, WorldPosition);
		}

		if (Controller.IsSwimming)
			ApplyCurrentForce();
	}



	private void UpdateWaterLevel()
	{
		if (Controller?.Body is null)
			return;

		// Something carrying the player (a boat) decides for it.
		if (GameObject.Parent != Scene)
			return;

		var transform = WorldTransform;
		Vector3 head = transform.PointToWorld(new Vector3(0, 0, Controller.CurrentHeight));
		Vector3 foot = transform.Position;

		float surface = WaterNetwork.GetWaterHeightAt(head);
		float level = 0.0f;

		if (surface > float.MinValue)
		{
			level = Vector3.InverseLerp(new Vector3(0, 0, surface), foot.WithX(0).WithY(0), head.WithX(0).WithY(0));
			level = (level * 100).CeilToInt() / 100.0f;
		}

		m_HasTouchedWater = level > 0.0f;
		WaterLevel = level;
	}



	private void ApplyCurrentForce()
	{
		Vector3 flow = WaterNetwork.GetFlowVelocityAt(WorldPosition);

		if (flow.LengthSquared < 0.01f)
			return;

		Vector3 relative = flow - Controller.Body.Velocity.WithZ(0.0f);

		Controller.Body.ApplyForce(relative * (Controller.Body.Mass * CurrentDrag));
	}



	public override Vector3 UpdateMove(Rotation _Eyes, Vector3 _Input)
	{
		if (Input.Down("jump"))
			_Input += Vector3.Up;

		return base.UpdateMove(_Eyes, _Input);
	}
}