Example/Complex/PlayerWalkControllerComplex.Movement.Ladders.cs
using Sandbox;
using System;
namespace XMovement;

public partial class PlayerWalkControllerComplex : Component
{
	[Property, FeatureEnabled( "Ladders" )] public bool EnableLadders { get; set; } = true;
	[Property, Feature( "Ladders" )] public float ClimbSpeed { get; set; } = 100.0f;
	[Property, Feature( "Ladders" )] public string LadderTag { get; set; } = "ladder";
	bool IsTouchingLadder = false;
	Vector3 LadderNormal;

	Vector3 LastNonZeroWishLadderVelocity;
	public virtual void CheckLadder()
	{
		if ( !EnableLadders ) { IsTouchingLadder = false; return; }
		var wishvel = new Vector3( WishMove.x.Clamp( -1f, 1f ), WishMove.y.Clamp( -1f, 1f ), 0 );

		// this is for latching
		if ( wishvel.Length > 0 ) LastNonZeroWishLadderVelocity = wishvel;
		if ( TryLatchNextTickCounter > 0 ) wishvel = LastNonZeroWishLadderVelocity * -1;

		wishvel *= EyeAngles.WithPitch( 0 ).ToRotation();
		wishvel = wishvel.Normal;

		if ( IsTouchingLadder )
		{
			if ( Input.Pressed( "jump" ) )
			{
				var sidem = (Math.Abs( Head.WorldRotation.Forward.Abs().z - 1 ) * 3).Clamp( 0, 1 );
				var upm = Head.WorldRotation.Forward.z;

				var Eject = new Vector3();

				Eject.x = LadderNormal.x * sidem;
				Eject.y = LadderNormal.y * sidem;
				Eject.z = (3 * upm).Clamp( 0, 1 );

				Controller.Velocity += (Eject * 180.0f) * WorldScale;

				IsTouchingLadder = false;

				return;

			}
			else if ( Controller.GroundObject != null && LadderNormal.Dot( wishvel ) > 0 )
			{
				IsTouchingLadder = false;

				return;
			}
		}

		const float ladderDistance = 1.0f;
		var start = WorldPosition;
		Vector3 end = start + (IsTouchingLadder ? (LadderNormal * -1.0f) : wishvel) * ladderDistance;

		var pm = Scene.Trace.Ray( start, end )
					.Size( Controller.BoundingBox.Mins, Controller.BoundingBox.Maxs )
					.WithTag( LadderTag )
					.IgnoreGameObjectHierarchy( GameObject )
					.Run();

		// Gizmo.Draw.LineBBox( cc.BoundingBox.Translate( end ) );

		IsTouchingLadder = false;

		if ( pm.Hit )
		{
			IsTouchingLadder = true;
			LadderNormal = pm.Normal;
		}
	}

	public virtual void LadderMove()
	{
		Controller.IsOnGround = false;

		var velocity = Controller.WishVelocity;
		float normalDot = velocity.Dot( LadderNormal );
		var cross = LadderNormal * normalDot;
		Controller.Velocity = (velocity - cross) + (-normalDot * LadderNormal.Cross( Vector3.Up.Cross( LadderNormal ).Normal ));

		Controller.Move( withWishVelocity: false, withGravity: false );
	}

	[ConVar( "debug_movement_ladderlatching" )]
	public static bool LatchDebug { get; set; } = false;

	int TryLatchNextTickCounter = 0;
	Vector3 LastNonZeroWishVelocity;
	private void LadderLatchCheck()
	{
		if ( !EnableLadders ) return;

		if ( Input.Down( JumpAction ) || Input.Released( JumpAction ) )
		{
			TryLatchNextTickCounter = 0;
			return;
		}

		if ( !Controller.WishVelocity.Normal.IsNearlyZero( 0.001f ) )
		{
			LastNonZeroWishVelocity = Controller.WishVelocity;
		}

		if ( TryLatchNextTickCounter > 0 )
		{
			Controller.Velocity = (LastNonZeroWishVelocity.Normal * -100).WithZ( Controller.Velocity.z );
			TryLatchNextTickCounter--;
		}

		if ( Controller.GroundObject != null ) return;
		if ( Controller.PreviousGroundObject == null ) return;

		// Trace downwards and behind the way we are or was walking

		var start = WorldPosition + (Vector3.Down * 16);
		var end = start - (LastNonZeroWishVelocity.Normal * (Controller.Radius / 2));

		var pm = Scene.Trace.Ray( start, end )
					.Size( Controller.BoundingBox.Mins, Controller.BoundingBox.Maxs )
					.WithTag( LadderTag )
					.IgnoreGameObjectHierarchy( GameObject )
					.Run();

		if ( pm.Hit )
		{
			Controller.Velocity = Vector3.Zero.WithZ( Controller.Velocity.z );
			TryLatchNextTickCounter = 10;
		}

		if ( LatchDebug )
		{
			DebugOverlay.Line( WorldPosition, start, Color.Yellow, 10 );
			DebugOverlay.Line( pm.StartPosition, pm.EndPosition, Color.Red, 10 );
		}
	}
}