s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (19 total matches found)
fish.scc / code/Examples/ShrimpleFlyer.cs
Game library
using Sandbox.Citizen;

namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleFlyer : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range(400f, 1600f)]
    public float WalkSpeed { get; set; } = 800f;

    [Property]
    [Range(800f, 4000f)]
    public float RunSpeed { get; set; } = 2400f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Camera = new GameObject(true, "Camera");
        Camera.SetParent(GameObject);
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var isDucking = Input.Down("Duck");
        var isRunning = Input.Down("Run");
        var ascending = Input.Down("Jump") ? 1f : 0f;
        var descending = Input.Down("Duck") ? -1f : 0f;
        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;
        var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();

        Controller.WishVelocity = wishDirection * wishSpeed;
        Controller.Move();
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -40f, 40f));

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 760f;
        Camera.WorldRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
    }
}
fish.scc / code/ShrimpleCollisionResult.cs
Game library
namespace ShrimpleCharacterController;

public struct ShrimpleCollisionResult
{
    public Vector3 HitPosition;
    public Vector3 HitNormal;
    public Vector3 HitVelocityBefore;
    public Vector3 HitVelocityAfter;
    public float Angle;
    public GameObject HitObject;
    public Surface HitSurface;

    public ShrimpleCollisionResult() { }

    public ShrimpleCollisionResult(Vector3 hitPosition, Vector3 hitNormal, Vector3 hitVelocityBefore, Vector3 hitVelocityAfter, float angle, GameObject hitObject, Surface hitSurface)
    {
        HitPosition = hitPosition;
        HitNormal = hitNormal;
        HitVelocityBefore = hitVelocityBefore;
        HitVelocityAfter = hitVelocityAfter;
        Angle = angle;
        HitObject = hitObject;
        HitSurface = hitSurface;
    }

    public ShrimpleCollisionResult WithHitPosition(Vector3 hitPosition)
    {
        var result = this;
        result.HitPosition = hitPosition;
        return result;
    }

    public ShrimpleCollisionResult WithHitNormal(Vector3 hitNormal)
    {
        var result = this;
        result.HitNormal = hitNormal;
        return result;
    }

    public ShrimpleCollisionResult WithHitVelocityBefore(Vector3 hitVelocityBefore)
    {
        var result = this;
        result.HitVelocityBefore = hitVelocityBefore;
        return result;
    }

    public ShrimpleCollisionResult WithHitVelocityAfter(Vector3 hitVelocityAfter)
    {
        var result = this;
        result.HitVelocityAfter = hitVelocityAfter;
        return result;
    }

    public ShrimpleCollisionResult WithAngle(float angle)
    {
        var result = this;
        result.Angle = angle;
        return result;
    }

    public ShrimpleCollisionResult WithHitObject(GameObject hitObject)
    {
        var result = this;
        result.HitObject = hitObject;
        return result;
    }

    public ShrimpleCollisionResult WithHitSurface(Surface hitSurface)
    {
        var result = this;
        result.HitSurface = hitSurface;
        return result;
    }
}
fish.scc / code/ShrimpleCharacterController.Physical.cs
Game library
namespace ShrimpleCharacterController;

public partial class ShrimpleCharacterController
{
    bool _didStep;
    Vector3 _stepPosition;

    // Set by Move() when ManuallyUpdate is enabled in physical mode, consumed by the next physics step
    bool _physicalMovePending;
    float _physicalMoveDelta;

    // The delta this physics step should simulate (the manual Move() delta, otherwise the fixed step)
    float ActiveDelta => ( ManuallyUpdate && _physicalMovePending ) ? _physicalMoveDelta : ActiveAutoTimeDelta;

    // The solver always integrates over Time.Delta, so we scale velocity to cover ActiveDelta worth of distance
    float MoveScale => Time.Delta > 0f ? ActiveDelta / Time.Delta : 1f;

    void IScenePhysicsEvents.PrePhysicsStep()
    {
        if ( !PhysicallySimulated ) return;
        if ( !Body.IsValid() ) return;
        if ( ManuallyUpdate && !_physicalMovePending ) return; // Wait for a manual Move() call to drive us

        _didStep = false;

        UpdateBodyPhysics();
        UpdateMassCenter();
        UpdateMovement();
    }

    void IScenePhysicsEvents.PostPhysicsStep()
    {
        if ( !Body.IsValid() )
        {
            WorldPosition += GroundVelocity * Time.Delta;
            return;
        }

        if ( !PhysicallySimulated ) return;
        if ( ManuallyUpdate && !_physicalMovePending ) return;

        if ( _didStep )
            Body.WorldPosition = _stepPosition;

        CategorizePhysicalGround();
        ApplyGroundVelocity();
        Velocity = Body.Velocity / MoveScale; // De-scale back to logical units/second

        _physicalMovePending = false;
    }

    private void UpdateMassCenter()
    {
        if ( !Body.IsValid() ) return;

        var wishSpeed = WishVelocity.WithZ( 0 ).Length;
        var halfHeight = AppliedHeight * 0.5f;
        float massCenter = IsOnGround ? wishSpeed.Clamp( 0, halfHeight ) : halfHeight;

        Body.MassCenterOverride = new Vector3( 0, 0, massCenter );
        Body.OverrideMassCenter = true;
    }

    private void UpdateBodyPhysics()
    {
        if ( !Collider.IsValid() || !Body.IsValid() ) return;

        var onDynamic = false;
        if ( IsOnGround && GroundObject.IsValid() )
        {
            var groundBody = GroundObject.GetComponent<Rigidbody>();
            onDynamic = groundBody != null && groundBody.PhysicsBody.BodyType == PhysicsBodyType.Dynamic;
        }

        Body.Gravity = false;

        var velocityHorizontal = Body.Velocity.WithZ( 0 ).Length;
        bool wantsBrakes = IsOnGround && WishVelocity.Length < 1f && velocityHorizontal < 10f;
        Body.LinearDamping = wantsBrakes ? 10f : 0.1f;
        Body.AngularDamping = 1f;

        float friction = 0f;
        if ( IsOnGround && WishVelocity.Length < 0.1f )
            friction = onDynamic ? 0.5f : 1f;
        Collider.Friction = friction;
    }

    private void AddWishVelocity()
    {
        var z = Body.Velocity.z;
        var velocity = CalculateGoalVelocity( ActiveDelta );

        if ( IsOnGround && GroundStickEnabled && !GroundNormal.IsNearlyZero( 0.01f ) )
        {
            if ( MathF.Abs( GroundNormal.z ) > 0.01f )
                velocity = velocity.WithZ( -( GroundNormal.x * velocity.x + GroundNormal.y * velocity.y ) / GroundNormal.z );

            var isGoingUphill = Vector3.Dot( velocity, AppliedGravity ) < 0f;
            var slopeMultiplier = isGoingUphill ? GetSlopeVelocityMultiplier( GroundAngle ) : 1f;
            velocity *= slopeMultiplier;
        }

        if ( IsOnGround && !GroundStickEnabled )
            velocity.z = z;

        if ( GravityEnabled && ( !IsOnGround || IsSlipping || !GroundStickEnabled ) )
            velocity += AppliedGravity * ActiveDelta;

        Body.Velocity = velocity * MoveScale; // Scale so the solver covers ActiveDelta worth of distance over Time.Delta
    }

    private void UpdateMovement()
    {
        if ( IsOnGround && GroundStickEnabled && !IsSlipping )
            StickToGround();

        AddWishVelocity();

        var wishHorizontal = WishVelocity.WithZ( 0 );
        var bodyHorizontal = Body.Velocity.WithZ( 0 );
        var moveDir = wishHorizontal.IsNearlyZero( 0.1f ) ? bodyHorizontal : wishHorizontal;
        if ( moveDir.IsNearlyZero( 0.1f ) ) return;

        var speed = MathF.Max( bodyHorizontal.Length * Time.Delta, StepDepth );
        var vel = moveDir.Normal * speed;
        var forwardTrace = BuildTrace( _shrunkenBounds, WorldPosition + _offset - vel.Normal * SkinWidth, WorldPosition + _offset + vel );

        if ( !forwardTrace.Hit || forwardTrace.StartedSolid ) return;

        var forwardAngle = Vector3.GetAngle( -AppliedGravity.Normal, forwardTrace.Normal );
        if ( IsAngleStandable( forwardAngle ) && Vector3.Dot( Body.Velocity, forwardTrace.Normal ) < 0f )
            Body.Velocity = Vector3.VectorPlaneProject( Body.Velocity, forwardTrace.Normal );

        if ( StepsEnabled && IsOnGround )
            TryStepUp( forwardTrace, vel );
    }

    private void TryStepUp( SceneTraceResult forwardTrace, Vector3 vel )
    {
        if ( forwardTrace.StartedSolid || !forwardTrace.Hit ) return;

        var angle = Vector3.GetAngle( -GravityNormal, forwardTrace.Normal );
        if ( angle < 90f - StepTolerance || angle > 90f + StepTolerance ) return;

        var stepHorizontal = Vector3.VectorPlaneProject( vel.Normal, GravityNormal ).Normal * StepDepth;
        var stepVertical = -GravityNormal * ( StepHeight + SkinWidth );
        var stepTrace = BuildTrace( _shrunkenBounds, forwardTrace.EndPosition + stepHorizontal + stepVertical, forwardTrace.EndPosition + stepHorizontal );

        if ( stepTrace.StartedSolid || !stepTrace.Hit ) return;
        if ( !IsAngleStandable( Vector3.GetAngle( stepTrace.Normal, -GravityNormal ) ) ) return;

        var newFeetPos = stepTrace.EndPosition - _offset + _upAxis * SkinWidth;
        if ( newFeetPos.z - WorldPosition.z < 0.5f ) return;

        _didStep = true;
        _stepPosition = newFeetPos;
        Body.WorldPosition = _stepPosition;
        Body.Velocity = Body.Velocity.WithZ( 0 );
    }

    private void ApplyGroundVelocity()
    {
        if ( !IsOnGround || !GroundStickEnabled || IsSlipping ) return;

        var groundVelocity = PlatformVelocity * Time.Delta + SurfaceVelocity * ActiveDelta;
        if ( groundVelocity.IsNearZeroLength ) return;

        Body.WorldPosition += groundVelocity;
    }

    private void StickToGround()
    {
        var currentPosition = WorldPosition;
        var from = currentPosition + _offset - GravityNormal * StepHeight;
        var to = currentPosition + _offset + GravityNormal * ( GroundStickDistance + SkinWidth );

        var trace = BuildTrace( _shrunkenBounds, from, to );

        if ( trace.StartedSolid ) return;

        if ( trace.Hit )
        {
            var surfaceAngle = Vector3.GetAngle( -GravityNormal, trace.Normal );
            if ( !IsAngleStandable( surfaceAngle ) ) return;

            var targetFeetPos = trace.EndPosition - _offset + _upAxis * SkinWidth;

            Body.WorldPosition = targetFeetPos;
            Body.Sleeping = false;

            if ( Body.Velocity.z > 0f )
                Body.Velocity = Body.Velocity.WithZ( 0 );

            IsOnGround = true;
            GroundNormal = trace.Normal;
            GroundSurface = trace.Surface;
            GroundObject = trace.GameObject;
            IsSlipping = false;
        }
    }

    private void CategorizePhysicalGround()
    {
        var position = WorldPosition + _offset;
        var shortCylinderOffset = AppliedWidth / 2f;
        var shortCylinderBounds = new BBox( _shrunkenBounds.Mins, new Vector3( _shrunkenBounds.Maxs.x, _shrunkenBounds.Maxs.y, _shrunkenBounds.Maxs.z - shortCylinderOffset ) );
        var groundTrace = BuildTrace( shortCylinderBounds, position + GravityNormal * shortCylinderOffset / 2f, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );

        if ( groundTrace.StartedSolid )
        {
            var stuckCheck = BuildTrace( _shrunkenBounds.Grow( -SkinWidth ), position, position, useCapsule: true );

            if ( stuckCheck.StartedSolid )
            {
                IsStuck = true;

                if ( UnstuckEnabled && TryUnstuck( position, out var unstuckResult ) )
                {
                    IsStuck = false;
                    Body.WorldPosition = unstuckResult - _offset;
                    Body.Velocity = Vector3.Zero;
                }

                var fallbackTrace = BuildTrace( _shrunkenBounds, position + -GravityNormal * 4f, position + GravityNormal * 2f );

                if ( fallbackTrace.Hit && !fallbackTrace.StartedSolid && Vector3.Dot( fallbackTrace.Normal, -GravityNormal ) > 0f )
                {
                    var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, fallbackTrace.Normal ) );
                    IsOnGround = true;
                    GroundNormal = fallbackTrace.Normal;
                    GroundSurface = fallbackTrace.Surface;
                    GroundObject = fallbackTrace.GameObject;
                    IsSlipping = !standable;
                }
                return;
            }

            groundTrace = BuildTrace( _shrunkenBounds, position, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );
        }

        IsStuck = false;

        if ( groundTrace.Hit )
        {
            var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, groundTrace.Normal ) );
            var landingAngle = Vector3.Dot( Velocity.Normal, groundTrace.Normal );
            var verticalAngle = Vector3.Dot( Velocity.Normal, GravityNormal );
            var goingUp = verticalAngle < 0.1f;
            var hasLanded = !IsOnGround && landingAngle <= ( MaxGroundAngle.Max / 180f ) && groundTrace.Distance <= SkinWidth * ( goingUp ? 6f : 2f ) + StepHeight;

            IsOnGround = IsOnGround || hasLanded;
            GroundNormal = groundTrace.Normal;
            GroundSurface = groundTrace.Surface;
            GroundObject = groundTrace.GameObject;
            IsSlipping = IsOnGround && !standable;

            if ( GroundStickEnabled && !IsSlipping && IsOnGround )
            {
                var targetFeetPos = groundTrace.EndPosition - _offset + _upAxis * ( SkinWidth + shortCylinderOffset / 2f );
                var delta = WorldPosition - targetFeetPos;

                if ( delta.z >= -0.1f && !delta.IsNearlyZero( 0.001f ) )
                {
                    Body.WorldPosition = targetFeetPos;
                    Body.Sleeping = false;

                    if ( Body.Velocity.z > 0f )
                        Body.Velocity = Body.Velocity.WithZ( 0 );
                }
            }

            return;
        }

        IsOnGround = false;
        GroundNormal = Vector3.Zero;
        GroundSurface = null;
        GroundObject = null;
        IsSlipping = false;
    }

    protected void CreateBody( bool recreate = false )
    {
        if ( recreate )
        {
            if ( Collider.IsValid() ) Collider.Destroy();
            if ( Body.IsValid() ) Body.Destroy();
            if ( BodyObject.IsValid() ) BodyObject.Destroy();
        }

        CreateCollider( recreate );
        CreateRigidbody( recreate );
    }

    protected void DestroyBody()
    {
        if ( Collider.IsValid() ) Collider.Destroy();
        if ( Body.IsValid() ) Body.Destroy();
        if ( BodyObject.IsValid() ) BodyObject.Destroy();
    }

    protected void CreateCollider( bool recreate = false )
    {
        if ( recreate && Collider.IsValid() )
            Collider.Destroy();

        if ( PhysicallySimulated )
        {
            if ( !Collider.IsValid() || Collider is not CapsuleCollider )
            {
                if ( Collider.IsValid() )
                    Collider.Destroy();

                Collider = GameObject.GetOrAddComponent<CapsuleCollider>();
            }
        }
        else
        {
            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )
            {
                if ( !Collider.IsValid() || Collider is not BoxCollider )
                {
                    if ( Collider.IsValid() )
                        Collider.Destroy();

                    Collider = GameObject.GetOrAddComponent<BoxCollider>();
                }
            }
            else if ( TraceShape == TraceType.Cylinder )
            {
                if ( !Collider.IsValid() || Collider is not HullCollider )
                {
                    if ( Collider.IsValid() )
                        Collider.Destroy();

                    Collider = GameObject.GetOrAddComponent<HullCollider>();
                }
            }
            else if ( TraceShape == TraceType.Sphere )
            {
                if ( !Collider.IsValid() || Collider is not SphereCollider )
                {
                    if ( Collider.IsValid() )
                        Collider.Destroy();

                    Collider = GameObject.GetOrAddComponent<SphereCollider>();
                }
            }
        }

        if ( HidePhysicalComponents )
            Collider.Flags |= ComponentFlags.Hidden;
        else
            Collider.Flags &= ~ComponentFlags.Hidden;

        UpdateCollider();
    }

    protected void UpdateCollider()
    {
        if ( !Collider.IsValid() ) return;

        if ( Collider is CapsuleCollider capsuleCollider )
        {
            var radius = ( TraceWidth - SkinWidth ) / 2f;
            var height = TraceHeight - SkinWidth;
            capsuleCollider.Radius = radius;
            capsuleCollider.Start = Vector3.Up * radius;
            capsuleCollider.End = Vector3.Up * ( height - radius );
        }
        else if ( Collider is BoxCollider boxCollider )
        {
            var bounds = BuildBounds();
            boxCollider.Scale = bounds.Size - SkinWidth;
            boxCollider.Center = Vector3.Up * ( TraceHeight / 2f );
        }
        else if ( Collider is HullCollider hullCollider )
        {
            hullCollider.Type = HullCollider.PrimitiveType.Cylinder;
            hullCollider.Height = TraceHeight - SkinWidth;
            hullCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;
            hullCollider.Center = Vector3.Up * ( TraceHeight / 2f );
        }
        else if ( Collider is SphereCollider sphereCollider )
        {
            sphereCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;
            sphereCollider.Center = Vector3.Up * sphereCollider.Radius;
        }
    }

    protected void CreateRigidbody( bool recreate = false )
    {
        if ( recreate && Body.IsValid() )
            Body.Destroy();

        if ( !Body.IsValid() && GameObject.Components.TryGet<Rigidbody>( out var existingBody ) )
            Body = existingBody;
        if ( !Body.IsValid() )
            Body = GameObject.AddComponent<Rigidbody>();

        Body.Locking = new PhysicsLock() { Pitch = true, Roll = true, Yaw = true };
        Body.Gravity = false;
        Body.MassOverride = BodyMassOverride;
        Body.RigidbodyFlags |= RigidbodyFlags.DisableCollisionSounds;
        Body.EnhancedCcd = EnableCCD;

        if ( HidePhysicalComponents )
            Body.Flags |= ComponentFlags.Hidden;
        else
            Body.Flags &= ~ComponentFlags.Hidden;
    }

    public void OnCollisionStart( Collision collision ) { }
}
fish.scc / Examples/ShrimpleRoller.cs
Game library
namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleRoller : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }

    public ModelRenderer Renderer { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range(500f, 2000f)]
    public float WalkSpeed { get; set; } = 1000f;

    [Property]
    [Range(1000f, 5000f)]
    public float RunSpeed { get; set; } = 3000f;

    [Property]
    [Range(200f, 500f)]
    public float JumpStrength { get; set; } = 350f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Renderer = Components.Get<ModelRenderer>(FindMode.EnabledInSelfAndDescendants);

        Camera = new GameObject(true, "Camera");
        Camera.SetParent(GameObject);
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw(EyeAngles.yaw);
        var isDucking = Input.Down("Duck");
        var isRunning = Input.Down("Run");
        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;

        Controller.WishVelocity = wishDirection * wishSpeed;
        Controller.Move();

        if (Input.Pressed("Jump") && Controller.IsOnGround)
            Controller.Punch(Vector3.Up * JumpStrength);
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));

        float pitchRotation = Controller.Velocity.x * Time.Delta;
        float rollRotation = -Controller.Velocity.y * Time.Delta;
        var ballPitch = Rotation.FromPitch(pitchRotation);
        var ballRoll = Rotation.FromRoll(rollRotation);
        Renderer.WorldRotation = ballPitch * ballRoll * Renderer.WorldRotation;

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 260f;
        Camera.WorldRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
    }
}
fish.scc / Examples/ShrimpleWalker.cs
Game library
namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleWalker : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }
    [Property]
    public SkinnedModelRenderer Renderer { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range( 50f, 200f )]
    public float WalkSpeed { get; set; } = 100f;

    [Property]
    [Range( 100f, 500f )]
    public float RunSpeed { get; set; } = 300f;

    [Property]
    [Range( 25f, 100f )]
    public float DuckSpeed { get; set; } = 50f;

    [Property]
    [Range( 200f, 500f )]
    public float JumpStrength { get; set; } = 350f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Renderer = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
        Camera = new GameObject( true, "Camera" );
        Camera.SetParent( GameObject );
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw( EyeAngles.yaw ) * GameObject.WorldRotation;
        var isDucking = Input.Down( "Duck" );
        var isRunning = Input.Down( "Run" );
        var wishSpeed = isDucking ? DuckSpeed :
            isRunning ? RunSpeed : WalkSpeed;

        Controller.WishVelocity = wishDirection * wishSpeed;

        if ( Input.Pressed( "Jump" ) && Controller.IsOnGround )
        {
            Controller.Punch( -Controller.AppliedGravity.Normal * JumpStrength );
            Renderer?.Set( "b_jump", true );
        }

        if ( !Renderer.IsValid() ) return;

        var oldX = Renderer.GetFloat( "move_x" );
        var oldY = Renderer.GetFloat( "move_y" );

        var velocity = Controller.Velocity;
        var runScale = 1f / Renderer.Transform.World.UniformScale;
        var moveX = Vector3.Dot( velocity, Renderer.WorldRotation.Forward ) * runScale;
        var moveY = Vector3.Dot( velocity, Renderer.WorldRotation.Right ) * runScale;

        var newX = MathX.Lerp( oldX, moveX, Time.Delta * 10f );
        var newY = MathX.Lerp( oldY, moveY, Time.Delta * 10f );

        Renderer.Set( "move_x", newX );
        Renderer.Set( "move_y", newY );

        Renderer.Set( "b_grounded", Controller.IsOnGround );
        Renderer.Set( "duck", isDucking ? 1f : 0f );
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch( MathX.Clamp( EyeAngles.pitch, -10f, 40f ) );
        Renderer.LocalRotation = Rotation.FromYaw( EyeAngles.yaw );

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 280f;
        Camera.LocalRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.LocalRotation;
    }
}
fish.scc / code/Assembly.cs
Game library
global using Sandbox;
global using System;
global using System.Collections.Generic;
global using System.Linq;
namespace ShrimpleCharacterController;
fish.scc / code/PlatformTest.cs
Game library
using Sandbox;

[Hide]
public sealed class PlatformTest : Component
{
    protected override void OnFixedUpdate()
    {
        WorldPosition += Vector3.Right * (float)Math.Sin(Time.Now * 2f) * 400f * Time.Delta;
        //WorldPosition += Vector3.Up * (float)Math.Sin(Time.Now * 2f) * 200f * Time.Delta;
    }
}
fish.scc / code/Util/Extensions/Vector3Extensions.cs
Game library
namespace ShrimpleCharacterController;

public static class Vector3Extensions
{
    /// <summary>
    /// Move a vector3 towards a goal by a fixed distance
    /// </summary>
    /// <param name="value"></param>
    /// <param name="target"></param>
    /// <param name="travelSpeed"></param>
    /// <returns></returns>
    public static Vector3 MoveTowards(this Vector3 value, Vector3 target, float travelSpeed)
    {
        var difference = target - value;
        var distance = difference.Length;
        var normal = difference.Normal;

        if (distance <= travelSpeed || distance == 0f)
        {
            return target;
        }

        return value + normal * travelSpeed;
    }

    /// <summary>
    /// Project a vector along a plane (normal) and scale it back to its original length
    /// </summary>
    /// <param name="value"></param>
    /// <param name="normal"></param>
    /// <returns></returns>
    public static Vector3 ProjectAndScale(this Vector3 value, Vector3 normal)
    {
        var length = value.Length;
        value = Vector3.VectorPlaneProject(value, normal).Normal;
        value *= length;

        return value;
    }
}
fish.scc / Examples/ShrimpleFlyer.cs
Game library
using Sandbox.Citizen;

namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleFlyer : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range(400f, 1600f)]
    public float WalkSpeed { get; set; } = 800f;

    [Property]
    [Range(800f, 4000f)]
    public float RunSpeed { get; set; } = 2400f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Camera = new GameObject(true, "Camera");
        Camera.SetParent(GameObject);
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var isDucking = Input.Down("Duck");
        var isRunning = Input.Down("Run");
        var ascending = Input.Down("Jump") ? 1f : 0f;
        var descending = Input.Down("Duck") ? -1f : 0f;
        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;
        var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();

        Controller.WishVelocity = wishDirection * wishSpeed;
        Controller.Move();
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -40f, 40f));

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 760f;
        Camera.WorldRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
    }
}
fish.scc / PlatformTest.cs
Game library
using Sandbox;

[Hide]
public sealed class PlatformTest : Component
{
    protected override void OnFixedUpdate()
    {
        WorldPosition += Vector3.Right * (float)Math.Sin(Time.Now * 2f) * 400f * Time.Delta;
        //WorldPosition += Vector3.Up * (float)Math.Sin(Time.Now * 2f) * 200f * Time.Delta;
    }
}
fish.scc / ShrimpleCollisionResult.cs
Game library
namespace ShrimpleCharacterController;

public struct ShrimpleCollisionResult
{
    public Vector3 HitPosition;
    public Vector3 HitNormal;
    public Vector3 HitVelocityBefore;
    public Vector3 HitVelocityAfter;
    public float Angle;
    public GameObject HitObject;
    public Surface HitSurface;

    public ShrimpleCollisionResult() { }

    public ShrimpleCollisionResult(Vector3 hitPosition, Vector3 hitNormal, Vector3 hitVelocityBefore, Vector3 hitVelocityAfter, float angle, GameObject hitObject, Surface hitSurface)
    {
        HitPosition = hitPosition;
        HitNormal = hitNormal;
        HitVelocityBefore = hitVelocityBefore;
        HitVelocityAfter = hitVelocityAfter;
        Angle = angle;
        HitObject = hitObject;
        HitSurface = hitSurface;
    }

    public ShrimpleCollisionResult WithHitPosition(Vector3 hitPosition)
    {
        var result = this;
        result.HitPosition = hitPosition;
        return result;
    }

    public ShrimpleCollisionResult WithHitNormal(Vector3 hitNormal)
    {
        var result = this;
        result.HitNormal = hitNormal;
        return result;
    }

    public ShrimpleCollisionResult WithHitVelocityBefore(Vector3 hitVelocityBefore)
    {
        var result = this;
        result.HitVelocityBefore = hitVelocityBefore;
        return result;
    }

    public ShrimpleCollisionResult WithHitVelocityAfter(Vector3 hitVelocityAfter)
    {
        var result = this;
        result.HitVelocityAfter = hitVelocityAfter;
        return result;
    }

    public ShrimpleCollisionResult WithAngle(float angle)
    {
        var result = this;
        result.Angle = angle;
        return result;
    }

    public ShrimpleCollisionResult WithHitObject(GameObject hitObject)
    {
        var result = this;
        result.HitObject = hitObject;
        return result;
    }

    public ShrimpleCollisionResult WithHitSurface(Surface hitSurface)
    {
        var result = this;
        result.HitSurface = hitSurface;
        return result;
    }
}
fish.scc / code/Examples/ShrimpleRoller.cs
Game library
namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleRoller : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }

    public ModelRenderer Renderer { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range(500f, 2000f)]
    public float WalkSpeed { get; set; } = 1000f;

    [Property]
    [Range(1000f, 5000f)]
    public float RunSpeed { get; set; } = 3000f;

    [Property]
    [Range(200f, 500f)]
    public float JumpStrength { get; set; } = 350f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Renderer = Components.Get<ModelRenderer>(FindMode.EnabledInSelfAndDescendants);

        Camera = new GameObject(true, "Camera");
        Camera.SetParent(GameObject);
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw(EyeAngles.yaw);
        var isDucking = Input.Down("Duck");
        var isRunning = Input.Down("Run");
        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;

        Controller.WishVelocity = wishDirection * wishSpeed;
        Controller.Move();

        if (Input.Pressed("Jump") && Controller.IsOnGround)
            Controller.Punch(Vector3.Up * JumpStrength);
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));

        float pitchRotation = Controller.Velocity.x * Time.Delta;
        float rollRotation = -Controller.Velocity.y * Time.Delta;
        var ballPitch = Rotation.FromPitch(pitchRotation);
        var ballRoll = Rotation.FromRoll(rollRotation);
        Renderer.WorldRotation = ballPitch * ballRoll * Renderer.WorldRotation;

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 260f;
        Camera.WorldRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
    }
}
fish.scc / Util/Extensions/Vector3Extensions.cs
Game library
namespace ShrimpleCharacterController;

public static class Vector3Extensions
{
    /// <summary>
    /// Move a vector3 towards a goal by a fixed distance
    /// </summary>
    /// <param name="value"></param>
    /// <param name="target"></param>
    /// <param name="travelSpeed"></param>
    /// <returns></returns>
    public static Vector3 MoveTowards(this Vector3 value, Vector3 target, float travelSpeed)
    {
        var difference = target - value;
        var distance = difference.Length;
        var normal = difference.Normal;

        if (distance <= travelSpeed || distance == 0f)
        {
            return target;
        }

        return value + normal * travelSpeed;
    }

    /// <summary>
    /// Project a vector along a plane (normal) and scale it back to its original length
    /// </summary>
    /// <param name="value"></param>
    /// <param name="normal"></param>
    /// <returns></returns>
    public static Vector3 ProjectAndScale(this Vector3 value, Vector3 normal)
    {
        var length = value.Length;
        value = Vector3.VectorPlaneProject(value, normal).Normal;
        value *= length;

        return value;
    }
}
fish.scc / ShrimpleCharacterController.Physical.cs
Game library
namespace ShrimpleCharacterController;

public partial class ShrimpleCharacterController
{
    bool _didStep;
    Vector3 _stepPosition;

    // Set by Move() when ManuallyUpdate is enabled in physical mode, consumed by the next physics step
    bool _physicalMovePending;
    float _physicalMoveDelta;

    // The delta this physics step should simulate (the manual Move() delta, otherwise the fixed step)
    float ActiveDelta => ( ManuallyUpdate && _physicalMovePending ) ? _physicalMoveDelta : ActiveAutoTimeDelta;

    // The solver always integrates over Time.Delta, so we scale velocity to cover ActiveDelta worth of distance
    float MoveScale => Time.Delta > 0f ? ActiveDelta / Time.Delta : 1f;

    void IScenePhysicsEvents.PrePhysicsStep()
    {
        if ( !PhysicallySimulated ) return;
        if ( !Body.IsValid() ) return;
        if ( ManuallyUpdate && !_physicalMovePending ) return; // Wait for a manual Move() call to drive us

        _didStep = false;

        UpdateBodyPhysics();
        UpdateMassCenter();
        UpdateMovement();
    }

    void IScenePhysicsEvents.PostPhysicsStep()
    {
        if ( !Body.IsValid() )
        {
            WorldPosition += GroundVelocity * Time.Delta;
            return;
        }

        if ( !PhysicallySimulated ) return;
        if ( ManuallyUpdate && !_physicalMovePending ) return;

        if ( _didStep )
            Body.WorldPosition = _stepPosition;

        CategorizePhysicalGround();
        ApplyGroundVelocity();
        Velocity = Body.Velocity / MoveScale; // De-scale back to logical units/second

        _physicalMovePending = false;
    }

    private void UpdateMassCenter()
    {
        if ( !Body.IsValid() ) return;

        var wishSpeed = WishVelocity.WithZ( 0 ).Length;
        var halfHeight = AppliedHeight * 0.5f;
        float massCenter = IsOnGround ? wishSpeed.Clamp( 0, halfHeight ) : halfHeight;

        Body.MassCenterOverride = new Vector3( 0, 0, massCenter );
        Body.OverrideMassCenter = true;
    }

    private void UpdateBodyPhysics()
    {
        if ( !Collider.IsValid() || !Body.IsValid() ) return;

        var onDynamic = false;
        if ( IsOnGround && GroundObject.IsValid() )
        {
            var groundBody = GroundObject.GetComponent<Rigidbody>();
            onDynamic = groundBody != null && groundBody.PhysicsBody.BodyType == PhysicsBodyType.Dynamic;
        }

        Body.Gravity = false;

        var velocityHorizontal = Body.Velocity.WithZ( 0 ).Length;
        bool wantsBrakes = IsOnGround && WishVelocity.Length < 1f && velocityHorizontal < 10f;
        Body.LinearDamping = wantsBrakes ? 10f : 0.1f;
        Body.AngularDamping = 1f;

        float friction = 0f;
        if ( IsOnGround && WishVelocity.Length < 0.1f )
            friction = onDynamic ? 0.5f : 1f;
        Collider.Friction = friction;
    }

    private void AddWishVelocity()
    {
        var z = Body.Velocity.z;
        var velocity = CalculateGoalVelocity( ActiveDelta );

        if ( IsOnGround && GroundStickEnabled && !GroundNormal.IsNearlyZero( 0.01f ) )
        {
            if ( MathF.Abs( GroundNormal.z ) > 0.01f )
                velocity = velocity.WithZ( -( GroundNormal.x * velocity.x + GroundNormal.y * velocity.y ) / GroundNormal.z );

            var isGoingUphill = Vector3.Dot( velocity, AppliedGravity ) < 0f;
            var slopeMultiplier = isGoingUphill ? GetSlopeVelocityMultiplier( GroundAngle ) : 1f;
            velocity *= slopeMultiplier;
        }

        if ( IsOnGround && !GroundStickEnabled )
            velocity.z = z;

        if ( GravityEnabled && ( !IsOnGround || IsSlipping || !GroundStickEnabled ) )
            velocity += AppliedGravity * ActiveDelta;

        Body.Velocity = velocity * MoveScale; // Scale so the solver covers ActiveDelta worth of distance over Time.Delta
    }

    private void UpdateMovement()
    {
        if ( IsOnGround && GroundStickEnabled && !IsSlipping )
            StickToGround();

        AddWishVelocity();

        var wishHorizontal = WishVelocity.WithZ( 0 );
        var bodyHorizontal = Body.Velocity.WithZ( 0 );
        var moveDir = wishHorizontal.IsNearlyZero( 0.1f ) ? bodyHorizontal : wishHorizontal;
        if ( moveDir.IsNearlyZero( 0.1f ) ) return;

        var speed = MathF.Max( bodyHorizontal.Length * Time.Delta, StepDepth );
        var vel = moveDir.Normal * speed;
        var forwardTrace = BuildTrace( _shrunkenBounds, WorldPosition + _offset - vel.Normal * SkinWidth, WorldPosition + _offset + vel );

        if ( !forwardTrace.Hit || forwardTrace.StartedSolid ) return;

        var forwardAngle = Vector3.GetAngle( -AppliedGravity.Normal, forwardTrace.Normal );
        if ( IsAngleStandable( forwardAngle ) && Vector3.Dot( Body.Velocity, forwardTrace.Normal ) < 0f )
            Body.Velocity = Vector3.VectorPlaneProject( Body.Velocity, forwardTrace.Normal );

        if ( StepsEnabled && IsOnGround )
            TryStepUp( forwardTrace, vel );
    }

    private void TryStepUp( SceneTraceResult forwardTrace, Vector3 vel )
    {
        if ( forwardTrace.StartedSolid || !forwardTrace.Hit ) return;

        var angle = Vector3.GetAngle( -GravityNormal, forwardTrace.Normal );
        if ( angle < 90f - StepTolerance || angle > 90f + StepTolerance ) return;

        var stepHorizontal = Vector3.VectorPlaneProject( vel.Normal, GravityNormal ).Normal * StepDepth;
        var stepVertical = -GravityNormal * ( StepHeight + SkinWidth );
        var stepTrace = BuildTrace( _shrunkenBounds, forwardTrace.EndPosition + stepHorizontal + stepVertical, forwardTrace.EndPosition + stepHorizontal );

        if ( stepTrace.StartedSolid || !stepTrace.Hit ) return;
        if ( !IsAngleStandable( Vector3.GetAngle( stepTrace.Normal, -GravityNormal ) ) ) return;

        var newFeetPos = stepTrace.EndPosition - _offset + _upAxis * SkinWidth;
        if ( newFeetPos.z - WorldPosition.z < 0.5f ) return;

        _didStep = true;
        _stepPosition = newFeetPos;
        Body.WorldPosition = _stepPosition;
        Body.Velocity = Body.Velocity.WithZ( 0 );
    }

    private void ApplyGroundVelocity()
    {
        if ( !IsOnGround || !GroundStickEnabled || IsSlipping ) return;

        var groundVelocity = PlatformVelocity * Time.Delta + SurfaceVelocity * ActiveDelta;
        if ( groundVelocity.IsNearZeroLength ) return;

        Body.WorldPosition += groundVelocity;
    }

    private void StickToGround()
    {
        var currentPosition = WorldPosition;
        var from = currentPosition + _offset - GravityNormal * StepHeight;
        var to = currentPosition + _offset + GravityNormal * ( GroundStickDistance + SkinWidth );

        var trace = BuildTrace( _shrunkenBounds, from, to );

        if ( trace.StartedSolid ) return;

        if ( trace.Hit )
        {
            var surfaceAngle = Vector3.GetAngle( -GravityNormal, trace.Normal );
            if ( !IsAngleStandable( surfaceAngle ) ) return;

            var targetFeetPos = trace.EndPosition - _offset + _upAxis * SkinWidth;

            Body.WorldPosition = targetFeetPos;
            Body.Sleeping = false;

            if ( Body.Velocity.z > 0f )
                Body.Velocity = Body.Velocity.WithZ( 0 );

            IsOnGround = true;
            GroundNormal = trace.Normal;
            GroundSurface = trace.Surface;
            GroundObject = trace.GameObject;
            IsSlipping = false;
        }
    }

    private void CategorizePhysicalGround()
    {
        var position = WorldPosition + _offset;
        var shortCylinderOffset = AppliedWidth / 2f;
        var shortCylinderBounds = new BBox( _shrunkenBounds.Mins, new Vector3( _shrunkenBounds.Maxs.x, _shrunkenBounds.Maxs.y, _shrunkenBounds.Maxs.z - shortCylinderOffset ) );
        var groundTrace = BuildTrace( shortCylinderBounds, position + GravityNormal * shortCylinderOffset / 2f, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );

        if ( groundTrace.StartedSolid )
        {
            var stuckCheck = BuildTrace( _shrunkenBounds.Grow( -SkinWidth ), position, position, useCapsule: true );

            if ( stuckCheck.StartedSolid )
            {
                IsStuck = true;

                if ( UnstuckEnabled && TryUnstuck( position, out var unstuckResult ) )
                {
                    IsStuck = false;
                    Body.WorldPosition = unstuckResult - _offset;
                    Body.Velocity = Vector3.Zero;
                }

                var fallbackTrace = BuildTrace( _shrunkenBounds, position + -GravityNormal * 4f, position + GravityNormal * 2f );

                if ( fallbackTrace.Hit && !fallbackTrace.StartedSolid && Vector3.Dot( fallbackTrace.Normal, -GravityNormal ) > 0f )
                {
                    var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, fallbackTrace.Normal ) );
                    IsOnGround = true;
                    GroundNormal = fallbackTrace.Normal;
                    GroundSurface = fallbackTrace.Surface;
                    GroundObject = fallbackTrace.GameObject;
                    IsSlipping = !standable;
                }
                return;
            }

            groundTrace = BuildTrace( _shrunkenBounds, position, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );
        }

        IsStuck = false;

        if ( groundTrace.Hit )
        {
            var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, groundTrace.Normal ) );
            var landingAngle = Vector3.Dot( Velocity.Normal, groundTrace.Normal );
            var verticalAngle = Vector3.Dot( Velocity.Normal, GravityNormal );
            var goingUp = verticalAngle < 0.1f;
            var hasLanded = !IsOnGround && landingAngle <= ( MaxGroundAngle.Max / 180f ) && groundTrace.Distance <= SkinWidth * ( goingUp ? 6f : 2f ) + StepHeight;

            IsOnGround = IsOnGround || hasLanded;
            GroundNormal = groundTrace.Normal;
            GroundSurface = groundTrace.Surface;
            GroundObject = groundTrace.GameObject;
            IsSlipping = IsOnGround && !standable;

            if ( GroundStickEnabled && !IsSlipping && IsOnGround )
            {
                var targetFeetPos = groundTrace.EndPosition - _offset + _upAxis * ( SkinWidth + shortCylinderOffset / 2f );
                var delta = WorldPosition - targetFeetPos;

                if ( delta.z >= -0.1f && !delta.IsNearlyZero( 0.001f ) )
                {
                    Body.WorldPosition = targetFeetPos;
                    Body.Sleeping = false;

                    if ( Body.Velocity.z > 0f )
                        Body.Velocity = Body.Velocity.WithZ( 0 );
                }
            }

            return;
        }

        IsOnGround = false;
        GroundNormal = Vector3.Zero;
        GroundSurface = null;
        GroundObject = null;
        IsSlipping = false;
    }

    protected void CreateBody( bool recreate = false )
    {
        if ( recreate )
        {
            if ( Collider.IsValid() ) Collider.Destroy();
            if ( Body.IsValid() ) Body.Destroy();
            if ( BodyObject.IsValid() ) BodyObject.Destroy();
        }

        CreateCollider( recreate );
        CreateRigidbody( recreate );
    }

    protected void DestroyBody()
    {
        if ( Collider.IsValid() ) Collider.Destroy();
        if ( Body.IsValid() ) Body.Destroy();
        if ( BodyObject.IsValid() ) BodyObject.Destroy();
    }

    protected void CreateCollider( bool recreate = false )
    {
        if ( recreate && Collider.IsValid() )
            Collider.Destroy();

        if ( PhysicallySimulated )
        {
            if ( !Collider.IsValid() || Collider is not CapsuleCollider )
            {
                if ( Collider.IsValid() )
                    Collider.Destroy();

                Collider = GameObject.GetOrAddComponent<CapsuleCollider>();
            }
        }
        else
        {
            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )
            {
                if ( !Collider.IsValid() || Collider is not BoxCollider )
                {
                    if ( Collider.IsValid() )
                        Collider.Destroy();

                    Collider = GameObject.GetOrAddComponent<BoxCollider>();
                }
            }
            else if ( TraceShape == TraceType.Cylinder )
            {
                if ( !Collider.IsValid() || Collider is not HullCollider )
                {
                    if ( Collider.IsValid() )
                        Collider.Destroy();

                    Collider = GameObject.GetOrAddComponent<HullCollider>();
                }
            }
            else if ( TraceShape == TraceType.Sphere )
            {
                if ( !Collider.IsValid() || Collider is not SphereCollider )
                {
                    if ( Collider.IsValid() )
                        Collider.Destroy();

                    Collider = GameObject.GetOrAddComponent<SphereCollider>();
                }
            }
        }

        if ( HidePhysicalComponents )
            Collider.Flags |= ComponentFlags.Hidden;
        else
            Collider.Flags &= ~ComponentFlags.Hidden;

        UpdateCollider();
    }

    protected void UpdateCollider()
    {
        if ( !Collider.IsValid() ) return;

        if ( Collider is CapsuleCollider capsuleCollider )
        {
            var radius = ( TraceWidth - SkinWidth ) / 2f;
            var height = TraceHeight - SkinWidth;
            capsuleCollider.Radius = radius;
            capsuleCollider.Start = Vector3.Up * radius;
            capsuleCollider.End = Vector3.Up * ( height - radius );
        }
        else if ( Collider is BoxCollider boxCollider )
        {
            var bounds = BuildBounds();
            boxCollider.Scale = bounds.Size - SkinWidth;
            boxCollider.Center = Vector3.Up * ( TraceHeight / 2f );
        }
        else if ( Collider is HullCollider hullCollider )
        {
            hullCollider.Type = HullCollider.PrimitiveType.Cylinder;
            hullCollider.Height = TraceHeight - SkinWidth;
            hullCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;
            hullCollider.Center = Vector3.Up * ( TraceHeight / 2f );
        }
        else if ( Collider is SphereCollider sphereCollider )
        {
            sphereCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;
            sphereCollider.Center = Vector3.Up * sphereCollider.Radius;
        }
    }

    protected void CreateRigidbody( bool recreate = false )
    {
        if ( recreate && Body.IsValid() )
            Body.Destroy();

        if ( !Body.IsValid() && GameObject.Components.TryGet<Rigidbody>( out var existingBody ) )
            Body = existingBody;
        if ( !Body.IsValid() )
            Body = GameObject.AddComponent<Rigidbody>();

        Body.Locking = new PhysicsLock() { Pitch = true, Roll = true, Yaw = true };
        Body.Gravity = false;
        Body.MassOverride = BodyMassOverride;
        Body.RigidbodyFlags |= RigidbodyFlags.DisableCollisionSounds;
        Body.EnhancedCcd = EnableCCD;

        if ( HidePhysicalComponents )
            Body.Flags |= ComponentFlags.Hidden;
        else
            Body.Flags &= ~ComponentFlags.Hidden;
    }

    public void OnCollisionStart( Collision collision ) { }
}
fish.scc / Assembly.cs
Game library
global using Sandbox;
global using System;
global using System.Collections.Generic;
global using System.Linq;
namespace ShrimpleCharacterController;
fish.scc / code/Examples/ShrimpleWalker.cs
Game library
namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleWalker : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }
    [Property]
    public SkinnedModelRenderer Renderer { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range( 50f, 200f )]
    public float WalkSpeed { get; set; } = 100f;

    [Property]
    [Range( 100f, 500f )]
    public float RunSpeed { get; set; } = 300f;

    [Property]
    [Range( 25f, 100f )]
    public float DuckSpeed { get; set; } = 50f;

    [Property]
    [Range( 200f, 500f )]
    public float JumpStrength { get; set; } = 350f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Renderer = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );
        Camera = new GameObject( true, "Camera" );
        Camera.SetParent( GameObject );
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw( EyeAngles.yaw ) * GameObject.WorldRotation;
        var isDucking = Input.Down( "Duck" );
        var isRunning = Input.Down( "Run" );
        var wishSpeed = isDucking ? DuckSpeed :
            isRunning ? RunSpeed : WalkSpeed;

        Controller.WishVelocity = wishDirection * wishSpeed;

        if ( Input.Pressed( "Jump" ) && Controller.IsOnGround )
        {
            Controller.Punch( -Controller.AppliedGravity.Normal * JumpStrength );
            Renderer?.Set( "b_jump", true );
        }

        if ( !Renderer.IsValid() ) return;

        var oldX = Renderer.GetFloat( "move_x" );
        var oldY = Renderer.GetFloat( "move_y" );

        var velocity = Controller.Velocity;
        var runScale = 1f / Renderer.Transform.World.UniformScale;
        var moveX = Vector3.Dot( velocity, Renderer.WorldRotation.Forward ) * runScale;
        var moveY = Vector3.Dot( velocity, Renderer.WorldRotation.Right ) * runScale;

        var newX = MathX.Lerp( oldX, moveX, Time.Delta * 10f );
        var newY = MathX.Lerp( oldY, moveY, Time.Delta * 10f );

        Renderer.Set( "move_x", newX );
        Renderer.Set( "move_y", newY );

        Renderer.Set( "b_grounded", Controller.IsOnGround );
        Renderer.Set( "duck", isDucking ? 1f : 0f );
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch( MathX.Clamp( EyeAngles.pitch, -10f, 40f ) );
        Renderer.LocalRotation = Rotation.FromYaw( EyeAngles.yaw );

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 280f;
        Camera.LocalRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.LocalRotation;
    }
}
fish.scc / ShrimpleCharacterController.cs
Game library
using System.Text.Json.Nodes;

namespace ShrimpleCharacterController;

[Icon( "nordic_walking" )]
public partial class ShrimpleCharacterController : Component, IScenePhysicsEvents, ISceneEvent<IScenePhysicsEvents>, Component.ICollisionListener
{
    /// <summary>
    /// Manually update this by calling Move() or let it always be simulated.
    /// In physical mode, Move() drives the next physics step instead of running the trace-based path.
    /// </summary>
    [Property]
    [Group( "Options" )]
    public bool ManuallyUpdate { get; set; } = false;

    [Property]
    [FeatureEnabled( "Physical" )]
    public bool PhysicallySimulated
    {
        get;
        set
        {
            field = value;

            if ( value )
                CreateBody();
            else
                DestroyBody();
        }
    }

    private bool isPhysical( object _ ) => !PhysicallySimulated;

    [Property]
    [Hide]
    public GameObject BodyObject { get; protected set; }

    [Property]
    [Feature( "Physical" )]
    [Header( "Collider" )]
    [Validate( nameof( isPhysical ), "Physical mode enabled - controller can interact with and be pushed by physics objects", LogLevel.Info )]
    public Surface ColliderSurface
    {
        get => Collider.IsValid() ? Collider.Surface : null;
        set
        {
            if ( Collider.IsValid() )
                Collider.Surface = value;
        }
    }

    [Property]
    [Feature( "Physical" )]
    public float? ColliderElasticity
    {
        get => Collider.IsValid() ? Collider.Elasticity : null;
        set
        {
            if ( Collider.IsValid() )
                Collider.Elasticity = value;
        }
    }

    [Property]
    [Feature( "Physical" )]
    [ShowIf( "HidePhysicalComponents", false )]
    public Collider Collider { get; protected set; }

    /// <summary>
    /// Kilos, tweak based on your surroundings, default boxes are like 600kg!
    /// </summary>
    [Property]
    [Feature( "Physical" )]
    [Header( "Rigid Body" )]
    public float BodyMassOverride
    {
        get;
        set
        {
            field = value;

            if ( Body.IsValid() )
                Body.MassOverride = value;
        }
    } = 100f;

    [Property]
    [Feature( "Physical" )]
    public RigidbodyFlags BodyRigidbodyFlags
    {
        get => Body.IsValid() ? Body.RigidbodyFlags : 0;
        set
        {
            if ( Body.IsValid() )
                Body.RigidbodyFlags = value;
        }
    }

    /// <summary>
    /// Enable Continuous Collision Detection (CCD) for the physics body.
    /// Useful for fast-moving characters to prevent tunneling through thin objects.
    /// </summary>
    [Property]
    [Feature( "Physical" )]
    [Title( "Enable CCD" )]
    public bool EnableCCD
    {
        get;
        set
        {
            field = value;
            if ( Body.IsValid() )
                Body.EnhancedCcd = value;
        }
    }

    [Property]
    [Feature( "Physical" )]
    [ShowIf( "HidePhysicalComponents", false )]
    public Rigidbody Body { get; protected set; }

    /// <summary>
    /// Hide the <see cref="Body"/> and <see cref="Collider"/> components in the inspector
    /// </summary>
    [Property]
    [Feature( "Physical" )]
    [Title( "Hide Components" )]
    [Space]
    [Validate( nameof( isPhysical ), "Make sure to go over the other features to see any warning regarding physical simulation!", LogLevel.Warn )]
    public bool HidePhysicalComponents
    {
        get;
        protected set
        {
            field = value;

            if ( value )
            {
                if ( Body.IsValid() )
                    Body.Flags |= ComponentFlags.Hidden;
                if ( Collider.IsValid() )
                    Collider.Flags |= ComponentFlags.Hidden;
            }
            else
            {
                if ( Body.IsValid() )
                    Body.Flags &= ~ComponentFlags.Hidden;
                if ( Collider.IsValid() )
                    Collider.Flags &= ~ComponentFlags.Hidden;
            }
        }
    } = true;

    [Feature( "Physical" )]
    [Button( "Recreate Components", "sync" )]
    public void RefreshBody() => CreateBody( true );

    /// <summary>
    /// If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners)
    /// </summary>
    [Property]
    [Group( "Options" )]
    public bool ScaleAgainstWalls { get; set; } = true;

    /// <summary>
    /// Rotate the trace with the gameobject
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [Validate( nameof( isPhysical ), "Physical mode uses a capsule collider to prevent snagging on terrain.", LogLevel.Info )]
    public bool RotateWithGameObject { get; set; } = true;

    public enum TraceType
    {
        /// <summary>
        /// This has no drawback, enjoy!
        /// </summary>
        [Icon( "📦" )]
        Box,
        /// <summary>
        /// This is a PHYSICAL TRACE, so it's more expensive than the normal box or sphere trace
        /// </summary>
        [Icon( "🛢" )]
        Cylinder,
        /// <summary>
        /// This will disable <see cref="StepsEnabled"/> as it's impossible to get the angle reliably
        /// </summary>
        [Icon( "🏐" )]
        Sphere,
        /// <summary>
        /// No drawbacks, box but you can define exact bounds
        /// </summary>
        [Icon( "🧊" )]
        Bounds
    }

    [Property]
    [Group( "Trace" )]
    [HideIf( nameof( PhysicallySimulated ), true )]
    public TraceType TraceShape
    {
        get;
        set
        {
            field = value;
            RebuildBounds();
            CreateCollider();
        }
    } = TraceType.Box;

    /// <summary>
    /// Width of our trace
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [ShowIf( nameof( _showWidthHeight ), true )]
    [Range( 1f, 128f, false, true )]
    [Sync]
    public float TraceWidth
    {
        get;
        set
        {
            field = value;
            RebuildBounds();
            UpdateCollider();
        }
    } = 16f;

    // Show width/height if Physical mode OR if not using Bounds shape
    bool _showWidthHeight => PhysicallySimulated || TraceShape != TraceType.Bounds;
    // Show height if Physical mode OR if using Box/Cylinder shape
    bool _showHeight => PhysicallySimulated || TraceShape == TraceType.Box || TraceShape == TraceType.Cylinder;

    /// <summary>
    /// Height of our trace
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [ShowIf( nameof( _showHeight ), true )]
    [Range( 1f, 256f, false, true )]
    [Sync]
    public float TraceHeight
    {
        get;
        set
        {
            field = value;
            RebuildBounds();
            UpdateCollider();
        }
    } = 72f;

    /// <summary>
    /// Bounds of our trace
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [ShowIf( "TraceShape", TraceType.Bounds )]
    [Sync]
    public BBox TraceBounds
    {
        get => TraceShape == TraceType.Bounds ? field : Bounds;
        set
        {
            field = value;
            RebuildBounds();
            UpdateCollider();
        }
    } = BBox.FromPositionAndSize( Vector3.Up * 36f, new Vector3( 32f, 32f, 72f ) );

    private bool ignoreTagsAndPhysical( TagSet tagSet ) => tagSet.IsEmpty || !PhysicallySimulated;

    /// <summary>
    /// Which tags it should ignore
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [Validate( nameof( ignoreTagsAndPhysical ), "Contoller is physical! Ignore tags must be set on the collision matrix", LogLevel.Warn )]
    public TagSet IgnoreTags { get; set; } = new TagSet();

    /// <summary>
    /// Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces)
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [Range( 1, 20, true, true )]
    public int MaxBounces { get; set; } = 5;

    /// <summary>
    /// Acceleration and Deceleration
    /// </summary>
    [FeatureEnabled( "Acceleration" )]
    [Property]
    public bool AccelerationEnabled { get; set; } = true;

    /// <summary>
    /// How fast you accelerate while on the ground (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float GroundAcceleration { get; set; } = 1000f;

    /// <summary>
    /// How fast you accelerate while in the air (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float AirAcceleration { get; set; } = 300f;

    /// <summary>
    /// Use the fixed acceleration value instead of curves
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    public bool FixedAcceleration { get; set; } = true;

    protected static readonly Curve DefaultAcceleration = new Curve( new List<Curve.Frame>
    {
        new Curve.Frame(0f, 0.2f, 0f, 1.5f),
        new Curve.Frame(1f, 1f, 0f, 0f)
    } )
    {
        TimeRange = new Vector2( 0f, 500f ),
        ValueRange = new Vector2( 0f, 1f ),
    };

    /// <summary>
    /// How much acceleration based on the current velocity<br/>
    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>
    /// Y axis *= Acceleration/><br/>
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [ValueRange( 0f, 1f, false )]
    [HideIf( "FixedAcceleration", true )]
    public Curve AccelerationCurve { get; set; } = DefaultAcceleration;

    /// <summary>
    /// How fast you decelerate while on the ground (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float GroundDeceleration { get; set; } = 1500f;

    /// <summary>
    /// How fast you decelerate while in the air (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float AirDeceleration { get; set; } = 0f;

    /// <summary>
    /// Use the fixed deceleration value instead of curves
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    public bool FixedDeceleration { get; set; } = true;

    protected static readonly Curve DefaultDeceleration = new Curve( new List<Curve.Frame>
    {
        new Curve.Frame(0f, 1f, 0f, -2.5f),
        new Curve.Frame(1f, 0.2f, 0f, 0f)
    } )
    {
        TimeRange = new Vector2( 0f, 500f ),
        ValueRange = new Vector2( 0f, 1f ),
    };

    /// <summary>
    /// How much deceleration based on the current velocity<br/>
    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>
    /// Y axis *= Deceleration/><br/>
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [ValueRange( 0f, 1f, false )]
    [HideIf( "FixedDeceleration", true )]
    public Curve DecelerationCurve { get; set; } = DefaultDeceleration;

    /// <summary>
    /// Do we ignore the friction of the surface you're standing on or not?
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    public bool IgnoreGroundSurface { get; set; } = false;

    /// <summary>
    /// Use Time.Delta as the time step when automatically simulating (disable to set a fixed TimeDelta)
    /// </summary>
    [Property]
    [Group( "Options" )]
    [HideIf( nameof( ManuallyUpdate ), true )]
    public bool UseTimeDelta { get; set; } = true;

    /// <summary>
    /// Fixed time step used when automatically simulating and UseTimeDelta is disabled
    /// </summary>
    [Property]
    [Group( "Options" )]
    [ShowIf( nameof( _showTimeDelta ), true )]
    public float TimeDelta { get; set; } = 1f / 60f;

    private bool _showTimeDelta => !ManuallyUpdate && !UseTimeDelta;
    private float ActiveAutoTimeDelta => UseTimeDelta ? Time.Delta : TimeDelta;

    /// <summary>
    /// Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip)
    /// </summary>
    [Property]
    [Group( "Movement" )]
    public bool IgnoreZ { get; set; } = true;

    /// <summary>
    /// Do we ignore Z when it's near 0 (So that gravity affects you when not moving)
    /// </summary>
    [Property]
    [Title( "Ignore Z When Zero" )]
    [Group( "Movement" )]
    [HideIf( "IgnoreZ", true )]
    public bool IgnoreZWhenZero { get; set; } = true;

    /// <summary>
    /// Tolerance from a 90° surface before it's considered a wall (Ex. Tolerance 1 = Between 89° and 91° can be a wall, 0.1 = 89.9° to 90.1°)
    /// </summary>
    [Group( "Movement" )]
    [Property]
    [Range( 0f, 10f, false )]
    public float WallTolerance { get; set; } = 1f;

    /// <summary>
    /// Player feels like it's gripping walls too much? Try more Grip Factor Reduction!
    /// </summary>
    [Group( "Movement" )]
    [Property]
    [Range( 1f, 10f, true )]
    [Validate( nameof( isPhysical ), "Physical mode uses collider friction for wall interaction", LogLevel.Info )]
    public float GripFactorReduction { get; set; } = 1f;

    /// <summary>
    /// Bouncing off walls and ground on collision
    /// </summary>
    [FeatureEnabled( "Bouncing" )]
    [Property]
    public bool BouncingEnabled { get; set; } = false;

    /// <summary>
    /// How much the MoveHelper will "bounce" off walls when colliding with them<br/>
    /// (0f = No bounce, 1f = Full bounce)<br/>
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    [Range( 0f, 1f, true )]
    [Validate( nameof( isPhysical ), "Contoller is physical! Elasticity must be done through the collider", LogLevel.Error )]
    public float HorizontalElasticity { get; set; } = 0f;

    /// <summary>
    /// How much the MoveHelper will "bounce" off the ground when colliding with it<br/>
    /// (0f = No bounce, 1f = Full bounce)<br/>
    /// <see cref="GroundStickEnabled"/> will override this value when sticking to the ground<br/>
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    [Range( 0f, 1f, true )]
    public float VerticalElasticity { get; set; } = 0f;

    /// <summary>
    /// Include the ground elasticity when bouncing off the ground<br/>
    /// Make sure the ground surface doesn't have a very low elasticity!
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    public bool IncludeGroundElasticity { get; set; } = false;

    /// <summary>
    /// Minimum velocity to allow bouncing
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    [Range( 0f, 100f, true )]
    public float ElasticityThreshold { get; set; } = 30f;

    /// <summary>
    /// Stick the MoveHelper to the ground (IsOnGround will default to false if disabled)
    /// </summary>
    [FeatureEnabled( "GroundStick" )]
    [Property]
    public bool GroundStickEnabled { get; set; } = true;

    /// <summary>
    /// How steep terrain can be for you to stand on without slipping.<br/>
    /// Min = Angle where velocity starts to drop off<br/>
    /// Max = Max angle where velocity is fully blocked (you can still slide down with gravity)
    /// </summary>
    [Property, Step( 0.1f )]
    [Feature( "GroundStick" )]
    public RangedFloat MaxGroundAngle
    {
        get;
        set
        {
            if ( value.Range == RangedFloat.RangeType.Between )
            {
                var min = MathX.Clamp( value.Min, 0f, 89f );
                var max = MathX.Clamp( value.Max, 0f, 89f );

                field = new RangedFloat( MathF.Min( min, max ), MathF.Max( min, max ) );
            }
            else
            {
                field = new RangedFloat( MathX.Clamp( value.FixedValue, 0f, 89f ) );
            }
        }
    } = new RangedFloat( 30f, 60f );

    /// <summary>
    /// How far from the ground the MoveHelper is going to stick (Useful for going down stairs!)
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    [Range( 1f, 32f, false )]
    public float GroundStickDistance { get; set; } = 12f;

    /// <summary>
    /// Stick to any <see cref="GroundObject"/> when standing on it
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    public bool StickToPlatforms { get; set; } = true;

    /// <summary>
    /// When the surface you're on has a surface velocity, apply it to the controller
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    public bool ApplySurfaceVelocity { get; set; } = true;

    /// <summary>
    /// When the surface you're on is slippery, scale the max ground angled relative to the friction<br/>
    /// Example: Ice with friction 0.5f, MaxGroundAngle will be 30f instead of 60f
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    public bool ScaleGroundAngleBySurfaceFriction { get; set; } = true;

    /// <summary>
    /// Enable steps climbing (+1 Trace call)
    /// </summary>
    [FeatureEnabled( "Steps" )]
    [Property]
    public bool StepsEnabled { get; set; } = true;

    /// <summary>
    /// How high steps can be for you to climb on
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    [Range( 1f, 32f, false )]
    public float StepHeight { get; set; } = 12f;

    /// <summary>
    /// How deep it checks for steps (Minimum depth)
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    [Range( 0.1f, 8f, false )]
    public float StepDepth { get; set; } = 2f;

    /// <summary>
    /// Tolerance from a 90° surface before it's considered a valid step (Ex. Tolerance 1 = Between 89° and 91° can be a step, 0.1 = 89.9° to 90.1°)
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    [Range( 0f, 10f, false )]
    public float StepTolerance { get; set; } = 1f;

    /// <summary>
    /// Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain)
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    public bool PseudoStepsEnabled { get; set; } = true;

    /// <summary>
    /// Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!)
    /// </summary>
    [FeatureEnabled( "Push" )]
    [Property]
    public bool PushEnabled { get; set; } = false;

    /// <summary>
    /// Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call)
    /// </summary>
    [Property]
    [Feature( "Push" )]
    [Validate( nameof( isPhysical ), "Contoller is physical! Make sure the tags are ignored on the collision matrix", LogLevel.Warn )]
    [Sync]
    public Dictionary<string, float> PushTagsWeight
    {
        get;
        set
        {
            field = value;
            _pushTags = BuildPushTags();
        }
    } = new Dictionary<string, float>() { { "player", 1f } };

    /// <summary>
    /// Apply gravity to this MoveHelper when not on the ground
    /// </summary>
    [FeatureEnabled( "Gravity" )]
    [Property]
    public bool GravityEnabled { get; set; } = true;

    /// <summary>
    /// Use the scene's gravity or our own
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    public bool UseSceneGravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    } = true;

    /// <summary>
    /// Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity)
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    [HideIf( "UseSceneGravity", true )]
    public bool UseVectorGravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    }

    private bool _usingFloatGravity => !UseVectorGravity && !UseSceneGravity;
    private bool _usingVectorGravity => UseVectorGravity && !UseSceneGravity;

    /// <summary>
    /// Units per second squared (Default is -850f)
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    [Range( -2000, 2000, false )]
    [ShowIf( "_usingFloatGravity", true )]
    public float Gravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    } = -850f;

    /// <summary>
    /// Units per second squared (Default is 0f, 0f, -850f)<br/>
    /// Changes which way <see cref="GroundStickEnabled"/> sticks to the ground
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    [ShowIf( "_usingVectorGravity", true )]
    public Vector3 VectorGravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    } = new Vector3( 0f, 0f, -850f );

    private Vector3 _appliedGravity;
    public Vector3 GravityNormal { get; private set; }
    public Vector3 AppliedGravity => _appliedGravity;

    /// <summary>
    /// Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck)
    /// </summary>
    [FeatureEnabled( "Unstuck" )]
    [Property]
    public bool UnstuckEnabled { get; set; } = true;

    /// <summary>
    /// How many trace calls it will attempt to get the MoveHelper unstuck
    /// </summary>
    [Property]
    [Feature( "Unstuck" )]
    [Range( 1, 100, false )]
    [Step( 1f )]
    public int MaxUnstuckTries { get; set; } = 40;

    /// <summary>
    /// The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside)
    /// </summary>
    [Sync] public Vector3 WishVelocity { get; set; }

    /// <summary>
    /// The resulting velocity after the simulation is done (Units per second)
    /// </summary>
    [Sync]
    public Vector3 Velocity
    {
        get;
        set
        {
            if ( PhysicallySimulated && Body.IsValid() )
                Body.Velocity = value;

            field = value;
        }
    }

    /// <summary>
    /// Velocity controlled by outside factors, such as knockback, rootmotion, etc.
    /// It is only applied to our final position and doesn't affect our Velocity.
    /// It should be handled outside the controller.
    /// </summary>
    [Sync] public Vector3 ExternalVelocity { get; set; }

    /// <summary>
    /// Is the MoveHelper currently touching the ground
    /// </summary>
    [Sync] public bool IsOnGround { get; set; }

    /// <summary>
    /// The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false)
    /// </summary>
    public Vector3 GroundNormal { get; private set; } = Vector3.Zero;

    /// <summary>
    /// The current ground angle you're standing on (Always 0f if IsOnGround false)
    /// </summary>
    public float GroundAngle => Vector3.GetAngle( GroundNormal, -GravityNormal );

    /// <summary>
    /// Gets the velocity multiplier for a given slope angle based on MaxGroundAngle range.<br/>
    /// Returns 1.0 below the start angle, 0.0 at/above the max angle, and lerps between.
    /// </summary>
    public float GetSlopeVelocityMultiplier( float angle )
    {
        if ( angle <= MaxGroundAngle.Min ) return 1f;
        if ( angle >= MaxGroundAngle.Max ) return 0f;
        return 1f - MathX.LerpInverse( angle, MaxGroundAngle.Min, MaxGroundAngle.Max );
    }

    /// <summary>
    /// Whether the given angle is considered standable (below max ground angle)
    /// </summary>
    public bool IsAngleStandable( float angle ) => angle <= MaxGroundAngle.Max * ( ScaleGroundAngleBySurfaceFriction ? GroundSurface?.Friction ?? 1f : 1f );

    /// <summary>
    /// The current surface you're standing on
    /// </summary>
    public Surface GroundSurface { get; private set; }

    /// <summary>
    /// The gameobject you're currently standing on
    /// </summary>
    public GameObject GroundObject { get; set; }

    /// <summary>
    /// Is the MoveHelper currently pushing against a wall
    /// </summary>
    public bool IsPushingAgainstWall { get; private set; }

    /// <summary>
    /// The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false)
    /// </summary>
    public Vector3 WallNormal { get; private set; } = Vector3.Zero;

    /// <summary>
    /// The gameobject you're currently pushing on
    /// </summary>
    public GameObject WallObject { get; set; }

    /// <summary>
    /// Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false)
    /// </summary>
    [Sync] public bool IsSlipping { get; private set; } // TODO IMPLEMENT

    /// <summary>
    /// The MoveHelper is stuck and we can't get it out
    /// </summary>
    [Sync] public bool IsStuck { get; private set; }

    /// <summary>
    /// To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later
    /// </summary>
    public float SkinWidth => Math.Min( Math.Max( 0.1f, TraceWidth * 0.05f ), GroundStickDistance );

    public float AppliedWidth => TraceWidth * WorldScale.x; // The width of the MoveHelper in world units
    public float AppliedDepth => TraceWidth * WorldScale.y; // The depth of the MoveHelper in world units
    public float AppliedHeight => TraceShape == TraceType.Sphere ? AppliedWidth :
        TraceShape == TraceType.Bounds ? TraceBounds.Size.z * WorldScale.z : TraceHeight * WorldScale.z; // The height of the MoveHelper in world units
    private Vector3 _upAxis => RotateWithGameObject ? WorldRotation.Up : Vector3.Up;
    private Vector3 _offset => ( RotateWithGameObject ? WorldRotation.Up : Vector3.Up ) * ( TraceShape == TraceType.Sphere || TraceShape == TraceType.Bounds ? 0f : AppliedHeight / 2f ); // The position of the MoveHelper in world units

    /// <summary>
    /// The bounds of this MoveHelper generated from the TraceWidth and TraceHeight
    /// </summary>
    public BBox Bounds { get; set; }
    private BBox _shrunkenBounds;
    private string[] _pushTags;
    private Vector3 _lastVelocity;
    private float _minimumTolerance => MathX.Clamp( Time.Delta / 2f, 0.005f, 0.1f ); // Floating precision tolerance, too high if used inside of OnUpdate so tied to update rate

    /// <summary>
    /// If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us
    /// </summary>
    public ShrimpleCharacterController UnstuckTarget;

    public Action<ShrimpleCollisionResult> OnCollide { get; set; }
    public bool IsOnPlatform => IsOnGround && GroundStickEnabled && !IsSlipping && StickToPlatforms && GroundObject.IsValid();
    public Vector3 PlatformVelocity => IsOnPlatform ? GroundObject.GetComponent<Collider>()?.GetVelocityAtPoint( WorldPosition ) ?? Vector3.Zero : Vector3.Zero;
    public bool IsOnSurfaceWithVelocity => IsOnGround && GroundStickEnabled && !IsSlipping && ApplySurfaceVelocity && GroundObject.IsValid();
    public Vector3 SurfaceVelocity => IsOnSurfaceWithVelocity ? GroundObject.GetComponent<Collider>()?.SurfaceVelocity * GroundObject.WorldRotation ?? Vector3.Zero : Vector3.Zero;
    public Vector3 GroundVelocity => PlatformVelocity + SurfaceVelocity;

    protected override void OnStart()
    {
        RebuildBounds();
        BuildGravity();
        _pushTags = BuildPushTags();
    }

    protected override void DrawGizmos()
    {
        if ( Gizmo.IsSelected )
        {
            Gizmo.GizmoDraw draw = Gizmo.Draw;
            draw.Color = Color.Blue;

            if ( TraceShape == TraceType.Box )
                draw.LineBBox( Bounds.Translate( Vector3.Up * Bounds.Size.z / 2f ) );
            if ( TraceShape == TraceType.Cylinder )
                draw.LineCylinder( Vector3.Zero, Vector3.Up * ( Bounds.Maxs.z - Bounds.Mins.z ), Bounds.Maxs.x, Bounds.Maxs.x, 24 );
            if ( TraceShape == TraceType.Sphere )
                draw.LineSphere( Vector3.Up * Bounds.Maxs.x, Bounds.Maxs.x );
            if ( TraceShape == TraceType.Bounds )
                draw.LineBBox( TraceBounds );

            if ( !UseSceneGravity && UseVectorGravity )
            {
                draw.Color = Color.Red;
                draw.LineThickness = 5f;
                draw.IgnoreDepth = true;
                draw.Arrow( Vector3.Zero, GravityNormal * 50f );
            }
        }
    }

    private BBox BuildBounds()
    {
        if ( TraceShape == TraceType.Bounds )
            return new BBox( TraceBounds.Mins * GameObject.WorldScale, TraceBounds.Maxs * GameObject.WorldScale );

        var x = GameObject.WorldScale.x;
        var y = GameObject.WorldScale.y;
        var z = GameObject.WorldScale.z;

        var width = TraceWidth / 2f * x;
        var depth = TraceWidth / 2f * y;
        var halfHeight = TraceHeight / 2f * z;

        // For cylinders we want the bounds to start at Z = 0 and extend up to the full height
        if ( TraceShape == TraceType.Cylinder )
            return new BBox( new Vector3( -width, -depth, 0f ), new Vector3( width, depth, halfHeight * 2f ) );

        return new BBox( new Vector3( -width, -depth, -halfHeight ), new Vector3( width, depth, halfHeight ) );
    }

    private void RebuildBounds()
    {
        Bounds = BuildBounds();
        _shrunkenBounds = Bounds.Grow( -SkinWidth );
    }

    private void BuildGravity()
    {
        _appliedGravity = UseSceneGravity ? Scene.PhysicsWorld.Gravity : UseVectorGravity ? VectorGravity : new Vector3( 0f, 0f, Gravity );
        GravityNormal = _appliedGravity.Normal;
    }

    private string[] BuildPushTags()
    {
        return PushTagsWeight.Keys.ToArray();
    }

    /// <summary>
    /// Casts the current bounds from to and returns the scene trace result
    /// </summary>
    /// <param name="bounds"></param>
    /// <param name="from"></param>
    /// <param name="to"></param>
    /// <param name="useCapsule">Use a capsule shape instead of the defaults</param>
    /// <returns></returns>
    public SceneTraceResult BuildTrace( BBox bounds, Vector3 from, Vector3 to, bool useCapsule = false )
    {
        SceneTrace builder = new SceneTrace(); // Empty trace builder

        if ( useCapsule )
        {
            var radius = bounds.Maxs.x;
            var halfHeight = ( bounds.Maxs.z - bounds.Mins.z ) / 2f;
            var hemiOffset = MathF.Max( halfHeight - radius, 0f );
            var capsule = new Capsule( Vector3.Down * hemiOffset, Vector3.Up * hemiOffset, radius ); // Do not rotate Vector3, it's rotated 17 lines down!
            builder = Game.SceneTrace.Capsule( capsule, from, to );
        }
        else
        {
            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )
                builder = Game.SceneTrace.Box( bounds, from, to );
            if ( TraceShape == TraceType.Cylinder )
                builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );
            if ( TraceShape == TraceType.Sphere )
                builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from + Vector3.Up * bounds.Maxs.x, to + Vector3.Up * bounds.Maxs.x );
        }

        builder = builder
            .IgnoreGameObjectHierarchy( GameObject )
            .WithoutTags( IgnoreTags );

        if ( RotateWithGameObject )
            builder = builder.Rotated( PhysicallySimulated ? Collider.WorldRotation : GameObject.WorldRotation );

        return builder.Run();
    }

    private SceneTraceResult BuildPushTrace( BBox bounds, Vector3 from, Vector3 to )
    {
        SceneTrace builder = new SceneTrace(); // Empty trace builder

        if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )
            builder = Game.SceneTrace.Box( bounds, from, to );
        if ( TraceShape == TraceType.Cylinder )
            builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );
        if ( TraceShape == TraceType.Sphere )
            builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from, to );

        builder = builder
            .IgnoreGameObjectHierarchy( GameObject )
            .WithAnyTags( _pushTags ); // Check for only the push tags

        if ( RotateWithGameObject )
            builder = builder.Rotated( GameObject.WorldRotation );

        return builder.Run();
    }

    /// <summary>
    /// Detach the MoveHelper from the ground and launch it somewhere (Units per second)
    /// </summary>
    /// <param name="amount"></param>
    public void Punch( in Vector3 amount )
    {
        var groundVel = GroundVelocity; // Capture before ungrounding
        IsOnGround = false;

        if ( PhysicallySimulated && Body.IsValid() )
            Body.Velocity = Body.Velocity + amount + groundVel;

        Velocity += amount + groundVel;
    }

    /// <summary>
    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
    /// </summary>
    /// <param name="manualUpdate">Just calculate but don't update position</param>
    public MoveHelperResult Move( bool manualUpdate = false ) => Move( Time.Delta, manualUpdate );

    /// <summary>
    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
    /// </summary>
    /// <param name="delta">The time step</param>
    /// <param name="manualUpdate">Just calculate but don't update position</param>
    public MoveHelperResult Move( float delta, bool manualUpdate = false )
    {
        // In physical mode the simulation runs inside the physics step callbacks.
        // Queue this call so the next PrePhysicsStep/PostPhysicsStep drives the body with the requested delta.
        if ( PhysicallySimulated )
        {
            if ( Body.IsValid() && !manualUpdate )
            {
                _physicalMoveDelta = delta;
                _physicalMovePending = true;
            }

            return new MoveHelperResult( WorldPosition, Velocity );
        }

        var goalVelocity = CalculateGoalVelocity( delta ); // Calculate the goal velocity using our Acceleration and Deceleration values

        // SIMULATE PUSH FORCES //
        if ( PushEnabled )
        {
            var pushTrace = BuildPushTrace( Bounds, WorldPosition + _offset, WorldPosition ); // Build a trace but using the Push tags instead of the Ignore tags

            if ( pushTrace.Hit ) // We're inside any of the push tags
            {
                foreach ( var tag in pushTrace.GameObject.Tags )
                {
                    if ( PushTagsWeight.TryGetValue( tag, out var tagWeight ) )
                    {
                        var otherPosition = pushTrace.GameObject.WorldPosition.WithZ( WorldPosition.z ); // Only horizontal pushing
                        var pushDirection = ( otherPosition - WorldPosition ).Normal;
                        var pushVelocity = pushDirection * tagWeight * 50f; // I find 50 u/s to be a good amount to push if the weight is 1.0 (!!!)

                        goalVelocity -= pushVelocity;
                    }
                }
            }
        }

        var finalPosition = WorldPosition;

        var moveHelperResult = CollideAndSlide( goalVelocity, finalPosition + _offset, delta ); // Simulate the MoveHelper

        finalPosition = moveHelperResult.Position;
        var finalVelocity = moveHelperResult.Velocity;
        // SIMULATE GRAVITY //
        if ( GravityEnabled && Gravity != 0f )
        {
            if ( !IsOnGround || IsSlipping || !GroundStickEnabled )
            {
                var gravity = AppliedGravity * delta;
                var gravityResult = CollideAndSlide( gravity, moveHelperResult.Position, delta, gravityPass: true ); // Apply and simulate the gravity step

                finalPosition = gravityResult.Position;
                finalVelocity += gravityResult.Velocity;
            }
        }

        if ( !ExternalVelocity.IsNearZeroLength )
        {
            moveHelperResult = CollideAndSlide( ExternalVelocity, finalPosition, delta );
        }

        finalPosition = moveHelperResult.Position - _offset; // Compensate for the offset we added at the beginning
        _lastVelocity = Velocity * delta;

        if ( !manualUpdate )
        {
            Velocity = finalVelocity;
            WorldPosition = finalPosition + SurfaceVelocity * delta; // Actually updating the position is "expensive" so we only do it once at the end
        }

        return new MoveHelperResult( finalPosition, finalVelocity, moveHelperResult.Offset );
    }

    /// <summary>
    /// Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity
    /// </summary>
    public struct MoveHelperResult
    {
        public Vector3 Position;
        public Vector3 Velocity;
        internal Vector3 Offset;
        internal float Leftover = 1f;

        public MoveHelperResult( Vector3 position, Vector3 velocity )
        {
            Position = position;
            Velocity = velocity;
        }

        internal MoveHelperResult( Vector3 position, Vector3 velocity, Vector3 offset, float leftover = 1f )
        {
            Position = position;
            Velocity = velocity;
            Offset = offset;
            Leftover = leftover;
        }
    }

    bool _bounced = false;

    private MoveHelperResult CollideAndSlide( Vector3 velocity, Vector3 position, float delta, int depth = 0, bool gravityPass = false ) =>
        CollideAndSlide( new MoveHelperResult( position, velocity ), delta, depth, gravityPass );

    private MoveHelperResult CollideAndSlide( MoveHelperResult current, float delta, int depth = 0, bool gravityPass = false )
    {
        if ( depth >= MaxBounces )
            return current;

        var velocity = current.Velocity * delta; // I like to set Velocity as units/second but we have to deal with units/tick here
        var position = current.Position;

        // GROUND AND UNSTUCK CHECK //
        if ( depth == 0 ) // Only check for the first step since it's impossible to get stuck on other steps
        {
            var groundTrace = BuildTrace( _shrunkenBounds, position, position + AppliedGravity.Normal * ( GroundStickDistance + SkinWidth * 1.1f ) ); // Compensate for floating inaccuracy

            if ( groundTrace.StartedSolid )
            {
                IsStuck = true;
                if ( UnstuckEnabled )
                {
                    if ( UnstuckTarget == null )
                    {
                        IsStuck = !TryUnstuck( position, out var result );

                        if ( !IsStuck )
                        {
                            position = result; // Update the new position

                            if ( PhysicallySimulated && Body.IsValid() )
                            {
                                Body.WorldPosition = result;
                                Body.Velocity = Vector3.Zero;
                            }

                            if ( groundTrace.GameObject != null )
                                if ( groundTrace.GameObject.Components.TryGet<ShrimpleCharacterController>( out var otherHelper ) )
                                    otherHelper.UnstuckTarget = this; // We already solved this, no need to unstuck the other helper
                        }
                        else
                        {
                            return new MoveHelperResult( position, Vector3.Zero ); // Mission failed, bail out!
                        }
                    }
                    else
                    {
                        UnstuckTarget = null; // Alright the other MoveHelper got us unstuck so just do nothing
                    }
                }
            }
            else
            {
                var hasLanded = !IsOnGround && Vector3.Dot( Velocity, AppliedGravity ) >= 0f && groundTrace.Hit && groundTrace.Distance <= SkinWidth * 2f; // Wasn't on the ground and now is
                var isGrounded = IsOnGround && groundTrace.Hit; // Was already on the ground and still is, this helps stick when going down stairs

                IsOnGround = hasLanded || isGrounded;
                GroundSurface = IsOnGround ? groundTrace.Surface : null;
                GroundNormal = IsOnGround ? groundTrace.Normal : -AppliedGravity.Normal;
                GroundObject = IsOnGround ? groundTrace.GameObject : null;
                IsSlipping = IsOnGround && !IsAngleStandable( GroundAngle );

                if ( IsSlipping && !gravityPass && Vector3.Dot( velocity, AppliedGravity ) < 0f )
                    velocity = velocity.WithZ( 0f ); // If we're slipping ignore any extra velocity we had

                // FIX: Clamp velocity when slipping to prevent buildup
                // Project velocity onto the slope and limit accumulation
                if ( IsSlipping )
                {
                    var slopeDir = Vector3.VectorPlaneProject( AppliedGravity.Normal, GroundNormal ).Normal;
                    var slopeSpeed = Vector3.Dot( velocity, slopeDir );
                    var maxSlipSpeed = AppliedGravity.Length * delta * 2f; // Reasonable max slip speed per frame

                    if ( slopeSpeed > maxSlipSpeed )
                    {
                        // Clamp the velocity component along the slope
                        velocity = velocity - slopeDir * ( slopeSpeed - maxSlipSpeed );
                    }
                }

                if ( IsOnGround && GroundStickEnabled && !IsSlipping )
                {
                    position = groundTrace.EndPosition + -AppliedGravity.Normal * SkinWidth; // Place on the ground
                    velocity = Vector3.VectorPlaneProject( velocity, GroundNormal ); // Follow the ground you're on without projecting Z
                }

                IsStuck = false;
            }
        }

        if ( velocity.IsNearlyZero( _minimumTolerance ) ) // Not worth continuing, reduces small stutter
        {
            return new MoveHelperResult( position, Vector3.Zero, current.Offset );
        }

        var toTravel = velocity.Length * current.Leftover + SkinWidth;
        var targetPosition = position + velocity.Normal * toTravel;
        var travelTrace = BuildTrace( _shrunkenBounds, position, targetPosition );

        if ( travelTrace.Hit )
        {
            var travelled = velocity.Normal * Math.Max( travelTrace.Distance - SkinWidth, 0f );
            var leftover = velocity - travelled; // How much leftover velocity still needs to be simulated
            var speed = leftover.Length;
            var angle = Vector3.GetAngle( -AppliedGravity.Normal, travelTrace.Normal );

            if ( toTravel >= SkinWidth && travelTrace.Distance < SkinWidth )
                travelled = Vector3.Zero;

            var elasticityDirection = MathX.Lerp( HorizontalElasticity, VerticalElasticity, angle / 90f );
            var elasticity = PhysicallySimulated ? 0f : BouncingEnabled ?
                velocity.Length / delta <= ElasticityThreshold ?
                    0f : elasticityDirection * ( IncludeGroundElasticity ? GroundSurface?.Elasticity ?? 1f : 1f )
                : 0f; // Allah help me format this better

            if ( IsAngleStandable( angle ) ) // Terrain we can walk on
            {
                var projectedLeftover = leftover;

                if ( gravityPass || !IsOnGround )
                    projectedLeftover = Vector3.VectorPlaneProject( projectedLeftover, travelTrace.Normal ); // Don't project the vertical velocity after landing else it boosts your horizontal velocity
                else
                    projectedLeftover = projectedLeftover.ProjectAndScale( travelTrace.Normal ); // Project the velocity along the terrain

                // Apply slope velocity reduction only when going uphill (against gravity)
                var isGoingUphill = Vector3.Dot( projectedLeftover, AppliedGravity ) < 0f;
                if ( isGoingUphill )
                {
                    var slopeMultiplier = GetSlopeVelocityMultiplier( angle );
                    projectedLeftover *= slopeMultiplier;
                }

                if ( elasticity > 0 )
                    leftover = Vector3.Lerp( projectedLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );
                else
                    leftover = projectedLeftover;

                IsPushingAgainstWall = false;
                WallObject = null;
            }
            else
            {
                var climbedStair = false;

                if ( angle >= 90f - WallTolerance && angle <= 90f + WallTolerance ) // Check for walls
                    IsPushingAgainstWall = true; // We're pushing against a wall

                if ( StepsEnabled )
                {
                    var isStep = angle >= 90f - StepTolerance && angle <= 90f + StepTolerance;

                    if ( isStep || PseudoStepsEnabled ) // Check for steps
                    {
                        if ( IsOnGround ) // Stairs VVV
                        {
                            var stepHorizontal = Vector3.VectorPlaneProject( velocity.Normal, AppliedGravity.Normal ).Normal * StepDepth; // How far in front we're looking for steps
                            var stepVertical = -AppliedGravity.Normal * ( StepHeight + SkinWidth ); // How high we're looking for steps + Some to compensate for floating inaccuracy
                            var stepTrace = BuildTrace( _shrunkenBounds, travelTrace.EndPosition + stepHorizontal + stepVertical, travelTrace.EndPosition + stepHorizontal );
                            var stepAngle = Vector3.GetAngle( stepTrace.Normal, -AppliedGravity.Normal );

                            if ( !stepTrace.StartedSolid && stepTrace.Hit && IsAngleStandable( stepAngle ) ) // We found a step!
                            {
                                if ( isStep || !IsSlipping && PseudoStepsEnabled )
                                {
                                    var stepDistance = stepTrace.EndPosition - travelTrace.EndPosition;
                                    var stepTravelled = -AppliedGravity.Normal * stepDistance;
                                    position += stepTravelled; // Offset our position by the height of the step climbed
                                    climbedStair = true;

                                    IsPushingAgainstWall = false; // Nevermind, we're not against a wall, we climbed a step!
                                    WallObject = null;
                                }
                            }
                        }
                    }
                }

                if ( IsPushingAgainstWall )
                {
                    // Scale our leftover velocity based on the angle of approach relative to the wall
                    // (Perpendicular = 0%, Parallel = 100%)
                    var scale = ScaleAgainstWalls ? 1f - Vector3.Dot( -travelTrace.Normal.Normal / GripFactorReduction, velocity.Normal ) : 1f;
                    var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal.Normal ) : leftover.ProjectAndScale( travelTrace.Normal.Normal );

                    if ( elasticity > 0 )
                        leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );
                    else
                        leftover = ( wallLeftover * scale ).WithZ( wallLeftover.z );

                    WallObject = travelTrace.GameObject;
                    WallNormal = travelTrace.Normal;
                }
                else
                {
                    if ( !climbedStair )
                    {
                        var scale = IsSlipping ? 1f : 1f - Vector3.Dot( -travelTrace.Normal / GripFactorReduction, velocity.Normal );
                        var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal ) * scale : leftover.ProjectAndScale( travelTrace.Normal );

                        if ( elasticity > 0 )
                            leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );
                        else
                            leftover = wallLeftover;
                    }

                }


            }

            var previousVelocity = velocity;
            var leftoverSpeed = leftover.Length / speed; // How much speed we had left after the collision
            velocity = leftover.Normal * velocity.Length * leftoverSpeed;

            if ( travelled.Length <= _minimumTolerance && leftover.Length <= _minimumTolerance )
                return new MoveHelperResult( position + travelled, velocity / delta, current.Offset );

            if ( elasticity > 0 )
                _bounced = true;

            var offset = position - current.Position;
            var newResult = CollideAndSlide( new MoveHelperResult( position + travelled, velocity / delta, offset, leftover.Length / velocity.Length ), delta, depth + 1, gravityPass ); // Simulate another bounce for the leftover velocity from the latest position

            if ( depth == 0 && !gravityPass )
            {
                var collision = new ShrimpleCollisionResult()
                    .WithHitNormal( travelTrace.Normal )
                    .WithHitPosition( travelTrace.HitPosition )
                    .WithAngle( angle )
                    .WithHitObject( travelTrace.GameObject )
                    .WithHitSurface( travelTrace.Surface )
                    .WithHitVelocityBefore( previousVelocity / delta )
                    .WithHitVelocityAfter( newResult.Velocity );

                OnCollide?.Invoke( collision );
            }

            return newResult;
        }

        if ( depth == 0 && !gravityPass )
        {
            IsPushingAgainstWall = false;
            WallObject = null;
        }

        if ( gravityPass && _bounced )
        {
            _bounced = false;
            velocity -= AppliedGravity * delta * delta * 1.4f; // Evil hack so it doesn't lose velocity due to gravity passes after each bounce
        }

        return new MoveHelperResult( position + velocity, velocity / delta, current.Offset ); // We didn't hit anything? Ok just keep going then :-)
    }

    private float CalculateGoalSpeed( Vector3 wishVelocity, Vector3 velocity, bool isAccelerating, float delta )
    {
        if ( !AccelerationEnabled ) return 999999999f; // Should be enough for the freaks that don't want acceleration

        float goalSpeed;

        var isSameDirection = velocity.IsNearlyZero( 1f ) || Vector3.Dot( wishVelocity.WithZ( 0f ).Normal, velocity.WithZ( 0f ).Normal ) >= 0f; // Is our wishVelocity roughly moving towards our velocity already?
        var currentSpeed = Math.Max( Velocity.WithZ( 0f ).Length, 10f );
        var acceleration = ( IsOnGround ? GroundAcceleration : AirAcceleration ) * ( FixedAcceleration ? 1f : AccelerationCurve.Evaluate( currentSpeed ) );
        var deceleration = ( IsOnGround ? GroundDeceleration : AirDeceleration ) * ( FixedDeceleration ? 1f : DecelerationCurve.Evaluate( currentSpeed ) );

        if ( isAccelerating )
            goalSpeed = acceleration;
        else
            goalSpeed = !isSameDirection ? Math.Max( acceleration, deceleration ) : deceleration; // Makes movement more responsive especially for flying or rolling

        if ( !IgnoreGroundSurface && GroundSurface != null )
            goalSpeed *= GroundSurface.Friction; // Take into account the ground's friction

        goalSpeed *= delta;

        return goalSpeed;
    }

    private Vector3 CalculateGoalVelocity( float delta )
    {
        bool shouldIgnoreZ = IgnoreZ || ( IgnoreZWhenZero && WishVelocity.z.AlmostEqual( 0f ) );
        var wishVelocity = shouldIgnoreZ ? ( WishVelocity.Normal * WishVelocity.Length ).WithZ( Velocity.z ) : WishVelocity;
        var isAccelerating = shouldIgnoreZ ? wishVelocity.WithZ( 0f ).Length >= Velocity.WithZ( 0f ).Length : wishVelocity.Length >= Velocity.Length;

        var goalSpeed = CalculateGoalSpeed( wishVelocity, Velocity, isAccelerating, delta );
        var goalVelocity = Velocity.MoveTowards( wishVelocity, goalSpeed );

        return shouldIgnoreZ ? goalVelocity.WithZ( Velocity.z ) : goalVelocity;
    }

    /// <summary>
    /// Attempts to return the nearest standable position until you're not longer inside other colliders<br/>
    /// If it fails too frequently, consider increasing <see cref="MaxUnstuckTries"/>
    /// </summary>
    /// <param name="position">The start position</param>
    /// <param name="result">The final standable position</param>
    /// <returns>True if it found one, false if it failed.</returns>
    public bool TryUnstuck( Vector3 position, out Vector3 result )
    {
        if ( _lastVelocity == Vector3.Zero )
            _lastVelocity = -AppliedGravity.Normal;

        var velocityLength = _lastVelocity.Length + SkinWidth;
        var startPos = position - _lastVelocity.Normal * velocityLength; // Try undoing the last velocity 1st
        var endPos = position;

        for ( int i = 0; i < MaxUnstuckTries + 1; i++ )
        {
            if ( i == 1 )
                startPos = position + -AppliedGravity.Normal * 2f; // Try going up 2nd

            if ( i > 1 && i < MaxUnstuckTries / 2f )
                startPos = position + Vector3.Random.Normal * ( (float)i / 2f ); // Start randomly checking 3rd

            if ( i >= MaxUnstuckTries / 2f )
                startPos = position + Vector3.Random.Normal * i; // Ok at this point start checking further

            if ( startPos - endPos == Vector3.Zero ) // No difference!
                continue;

            var unstuckTrace = BuildTrace( _shrunkenBounds, startPos, endPos, PhysicallySimulated );

            if ( !unstuckTrace.StartedSolid )
            {
                result = unstuckTrace.EndPosition - _lastVelocity.Normal * SkinWidth / 4f;
                _lastVelocity = Vector3.Zero;
                return true;
            }
        }

        result = position;
        return false;
    }

    /// <summary>
    /// Debug don't use
    /// </summary>
    /// <param name="position"></param>
    /// <param name="title"></param>
    /// <returns></returns>
    private bool TestPosition( Vector3 position, string title )
    {
        var testTrace = BuildTrace( _shrunkenBounds, position, position );

        if ( testTrace.StartedSolid )
        {
            Log.Info( $"[{RealTime.Now}]{title} {GameObject.Name} started solid at {position} against {testTrace.GameObject}" );
            return true;
        }

        return false;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        if ( !ManuallyUpdate && Active && !PhysicallySimulated ) // If we're not physically simulated we Move inside of OnFixedUpdate
            Move( ActiveAutoTimeDelta );
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();

        Collider?.Destroy();
        Body?.Destroy();
    }

    protected override void OnDisabled()
    {
        base.OnDisabled();

        Collider?.Enabled = false;
        Body?.Enabled = false;
    }

    protected override void OnEnabled()
    {
        base.OnEnabled();

        Collider?.Enabled = true;
        Body?.Enabled = true;
    }

    public override int ComponentVersion => 4;

    [JsonUpgrader( typeof( ShrimpleCharacterController ), 3 )]
    private static void UpdateTraceShape( JsonObject json )
    {
        if ( json.Remove( "CylinderTrace", out var newNode ) && newNode is JsonValue boolNode && boolNode.TryGetValue<bool>( out var isCylinder ) )
            json["TraceShape"] = isCylinder ? "Cylinder" : "Box";
        else
            json["TraceShape"] = "Box";
    }

    [JsonUpgrader( typeof( ShrimpleCharacterController ), 4 )]
    private static void UpdateMaxGroundAngle( JsonObject json )
    {
        // Old default was 60f, new default is (30f, 60f)
        if ( json.TryGetPropertyValue( "MaxGroundAngle", out var oldValue ) && oldValue is JsonValue floatValue && floatValue.TryGetValue<float>( out var oldAngle ) )
        {
            json.Remove( "MaxGroundAngle" );
            var startAngle = oldAngle * 0.75f;
            json["MaxGroundAngle"] = $"{startAngle:F2} {oldAngle:F2}";
        }
    }
}
fish.scc / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Shrimple Character Controller" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "scc" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "fish" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "fish.scc" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-27T12:22:40.9761893Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.111.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.111.0")]
fish.scc / code/ShrimpleCharacterController.cs
Game library
using System.Text.Json.Nodes;

namespace ShrimpleCharacterController;

[Icon( "nordic_walking" )]
public partial class ShrimpleCharacterController : Component, IScenePhysicsEvents, ISceneEvent<IScenePhysicsEvents>, Component.ICollisionListener
{
    /// <summary>
    /// Manually update this by calling Move() or let it always be simulated.
    /// In physical mode, Move() drives the next physics step instead of running the trace-based path.
    /// </summary>
    [Property]
    [Group( "Options" )]
    public bool ManuallyUpdate { get; set; } = false;

    [Property]
    [FeatureEnabled( "Physical" )]
    public bool PhysicallySimulated
    {
        get;
        set
        {
            field = value;

            if ( value )
                CreateBody();
            else
                DestroyBody();
        }
    }

    private bool isPhysical( object _ ) => !PhysicallySimulated;

    [Property]
    [Hide]
    public GameObject BodyObject { get; protected set; }

    [Property]
    [Feature( "Physical" )]
    [Header( "Collider" )]
    [Validate( nameof( isPhysical ), "Physical mode enabled - controller can interact with and be pushed by physics objects", LogLevel.Info )]
    public Surface ColliderSurface
    {
        get => Collider.IsValid() ? Collider.Surface : null;
        set
        {
            if ( Collider.IsValid() )
                Collider.Surface = value;
        }
    }

    [Property]
    [Feature( "Physical" )]
    public float? ColliderElasticity
    {
        get => Collider.IsValid() ? Collider.Elasticity : null;
        set
        {
            if ( Collider.IsValid() )
                Collider.Elasticity = value;
        }
    }

    [Property]
    [Feature( "Physical" )]
    [ShowIf( "HidePhysicalComponents", false )]
    public Collider Collider { get; protected set; }

    /// <summary>
    /// Kilos, tweak based on your surroundings, default boxes are like 600kg!
    /// </summary>
    [Property]
    [Feature( "Physical" )]
    [Header( "Rigid Body" )]
    public float BodyMassOverride
    {
        get;
        set
        {
            field = value;

            if ( Body.IsValid() )
                Body.MassOverride = value;
        }
    } = 100f;

    [Property]
    [Feature( "Physical" )]
    public RigidbodyFlags BodyRigidbodyFlags
    {
        get => Body.IsValid() ? Body.RigidbodyFlags : 0;
        set
        {
            if ( Body.IsValid() )
                Body.RigidbodyFlags = value;
        }
    }

    /// <summary>
    /// Enable Continuous Collision Detection (CCD) for the physics body.
    /// Useful for fast-moving characters to prevent tunneling through thin objects.
    /// </summary>
    [Property]
    [Feature( "Physical" )]
    [Title( "Enable CCD" )]
    public bool EnableCCD
    {
        get;
        set
        {
            field = value;
            if ( Body.IsValid() )
                Body.EnhancedCcd = value;
        }
    }

    [Property]
    [Feature( "Physical" )]
    [ShowIf( "HidePhysicalComponents", false )]
    public Rigidbody Body { get; protected set; }

    /// <summary>
    /// Hide the <see cref="Body"/> and <see cref="Collider"/> components in the inspector
    /// </summary>
    [Property]
    [Feature( "Physical" )]
    [Title( "Hide Components" )]
    [Space]
    [Validate( nameof( isPhysical ), "Make sure to go over the other features to see any warning regarding physical simulation!", LogLevel.Warn )]
    public bool HidePhysicalComponents
    {
        get;
        protected set
        {
            field = value;

            if ( value )
            {
                if ( Body.IsValid() )
                    Body.Flags |= ComponentFlags.Hidden;
                if ( Collider.IsValid() )
                    Collider.Flags |= ComponentFlags.Hidden;
            }
            else
            {
                if ( Body.IsValid() )
                    Body.Flags &= ~ComponentFlags.Hidden;
                if ( Collider.IsValid() )
                    Collider.Flags &= ~ComponentFlags.Hidden;
            }
        }
    } = true;

    [Feature( "Physical" )]
    [Button( "Recreate Components", "sync" )]
    public void RefreshBody() => CreateBody( true );

    /// <summary>
    /// If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners)
    /// </summary>
    [Property]
    [Group( "Options" )]
    public bool ScaleAgainstWalls { get; set; } = true;

    /// <summary>
    /// Rotate the trace with the gameobject
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [Validate( nameof( isPhysical ), "Physical mode uses a capsule collider to prevent snagging on terrain.", LogLevel.Info )]
    public bool RotateWithGameObject { get; set; } = true;

    public enum TraceType
    {
        /// <summary>
        /// This has no drawback, enjoy!
        /// </summary>
        [Icon( "📦" )]
        Box,
        /// <summary>
        /// This is a PHYSICAL TRACE, so it's more expensive than the normal box or sphere trace
        /// </summary>
        [Icon( "🛢" )]
        Cylinder,
        /// <summary>
        /// This will disable <see cref="StepsEnabled"/> as it's impossible to get the angle reliably
        /// </summary>
        [Icon( "🏐" )]
        Sphere,
        /// <summary>
        /// No drawbacks, box but you can define exact bounds
        /// </summary>
        [Icon( "🧊" )]
        Bounds
    }

    [Property]
    [Group( "Trace" )]
    [HideIf( nameof( PhysicallySimulated ), true )]
    public TraceType TraceShape
    {
        get;
        set
        {
            field = value;
            RebuildBounds();
            CreateCollider();
        }
    } = TraceType.Box;

    /// <summary>
    /// Width of our trace
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [ShowIf( nameof( _showWidthHeight ), true )]
    [Range( 1f, 128f, false, true )]
    [Sync]
    public float TraceWidth
    {
        get;
        set
        {
            field = value;
            RebuildBounds();
            UpdateCollider();
        }
    } = 16f;

    // Show width/height if Physical mode OR if not using Bounds shape
    bool _showWidthHeight => PhysicallySimulated || TraceShape != TraceType.Bounds;
    // Show height if Physical mode OR if using Box/Cylinder shape
    bool _showHeight => PhysicallySimulated || TraceShape == TraceType.Box || TraceShape == TraceType.Cylinder;

    /// <summary>
    /// Height of our trace
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [ShowIf( nameof( _showHeight ), true )]
    [Range( 1f, 256f, false, true )]
    [Sync]
    public float TraceHeight
    {
        get;
        set
        {
            field = value;
            RebuildBounds();
            UpdateCollider();
        }
    } = 72f;

    /// <summary>
    /// Bounds of our trace
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [ShowIf( "TraceShape", TraceType.Bounds )]
    [Sync]
    public BBox TraceBounds
    {
        get => TraceShape == TraceType.Bounds ? field : Bounds;
        set
        {
            field = value;
            RebuildBounds();
            UpdateCollider();
        }
    } = BBox.FromPositionAndSize( Vector3.Up * 36f, new Vector3( 32f, 32f, 72f ) );

    private bool ignoreTagsAndPhysical( TagSet tagSet ) => tagSet.IsEmpty || !PhysicallySimulated;

    /// <summary>
    /// Which tags it should ignore
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [Validate( nameof( ignoreTagsAndPhysical ), "Contoller is physical! Ignore tags must be set on the collision matrix", LogLevel.Warn )]
    public TagSet IgnoreTags { get; set; } = new TagSet();

    /// <summary>
    /// Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces)
    /// </summary>
    [Property]
    [Group( "Trace" )]
    [Range( 1, 20, true, true )]
    public int MaxBounces { get; set; } = 5;

    /// <summary>
    /// Acceleration and Deceleration
    /// </summary>
    [FeatureEnabled( "Acceleration" )]
    [Property]
    public bool AccelerationEnabled { get; set; } = true;

    /// <summary>
    /// How fast you accelerate while on the ground (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float GroundAcceleration { get; set; } = 1000f;

    /// <summary>
    /// How fast you accelerate while in the air (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float AirAcceleration { get; set; } = 300f;

    /// <summary>
    /// Use the fixed acceleration value instead of curves
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    public bool FixedAcceleration { get; set; } = true;

    protected static readonly Curve DefaultAcceleration = new Curve( new List<Curve.Frame>
    {
        new Curve.Frame(0f, 0.2f, 0f, 1.5f),
        new Curve.Frame(1f, 1f, 0f, 0f)
    } )
    {
        TimeRange = new Vector2( 0f, 500f ),
        ValueRange = new Vector2( 0f, 1f ),
    };

    /// <summary>
    /// How much acceleration based on the current velocity<br/>
    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>
    /// Y axis *= Acceleration/><br/>
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [ValueRange( 0f, 1f, false )]
    [HideIf( "FixedAcceleration", true )]
    public Curve AccelerationCurve { get; set; } = DefaultAcceleration;

    /// <summary>
    /// How fast you decelerate while on the ground (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float GroundDeceleration { get; set; } = 1500f;

    /// <summary>
    /// How fast you decelerate while in the air (Units per second)
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [Range( 0f, 3000f, false )]
    public float AirDeceleration { get; set; } = 0f;

    /// <summary>
    /// Use the fixed deceleration value instead of curves
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    public bool FixedDeceleration { get; set; } = true;

    protected static readonly Curve DefaultDeceleration = new Curve( new List<Curve.Frame>
    {
        new Curve.Frame(0f, 1f, 0f, -2.5f),
        new Curve.Frame(1f, 0.2f, 0f, 0f)
    } )
    {
        TimeRange = new Vector2( 0f, 500f ),
        ValueRange = new Vector2( 0f, 1f ),
    };

    /// <summary>
    /// How much deceleration based on the current velocity<br/>
    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>
    /// Y axis *= Deceleration/><br/>
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    [ValueRange( 0f, 1f, false )]
    [HideIf( "FixedDeceleration", true )]
    public Curve DecelerationCurve { get; set; } = DefaultDeceleration;

    /// <summary>
    /// Do we ignore the friction of the surface you're standing on or not?
    /// </summary>
    [Property]
    [Feature( "Acceleration" )]
    public bool IgnoreGroundSurface { get; set; } = false;

    /// <summary>
    /// Use Time.Delta as the time step when automatically simulating (disable to set a fixed TimeDelta)
    /// </summary>
    [Property]
    [Group( "Options" )]
    [HideIf( nameof( ManuallyUpdate ), true )]
    public bool UseTimeDelta { get; set; } = true;

    /// <summary>
    /// Fixed time step used when automatically simulating and UseTimeDelta is disabled
    /// </summary>
    [Property]
    [Group( "Options" )]
    [ShowIf( nameof( _showTimeDelta ), true )]
    public float TimeDelta { get; set; } = 1f / 60f;

    private bool _showTimeDelta => !ManuallyUpdate && !UseTimeDelta;
    private float ActiveAutoTimeDelta => UseTimeDelta ? Time.Delta : TimeDelta;

    /// <summary>
    /// Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip)
    /// </summary>
    [Property]
    [Group( "Movement" )]
    public bool IgnoreZ { get; set; } = true;

    /// <summary>
    /// Do we ignore Z when it's near 0 (So that gravity affects you when not moving)
    /// </summary>
    [Property]
    [Title( "Ignore Z When Zero" )]
    [Group( "Movement" )]
    [HideIf( "IgnoreZ", true )]
    public bool IgnoreZWhenZero { get; set; } = true;

    /// <summary>
    /// Tolerance from a 90° surface before it's considered a wall (Ex. Tolerance 1 = Between 89° and 91° can be a wall, 0.1 = 89.9° to 90.1°)
    /// </summary>
    [Group( "Movement" )]
    [Property]
    [Range( 0f, 10f, false )]
    public float WallTolerance { get; set; } = 1f;

    /// <summary>
    /// Player feels like it's gripping walls too much? Try more Grip Factor Reduction!
    /// </summary>
    [Group( "Movement" )]
    [Property]
    [Range( 1f, 10f, true )]
    [Validate( nameof( isPhysical ), "Physical mode uses collider friction for wall interaction", LogLevel.Info )]
    public float GripFactorReduction { get; set; } = 1f;

    /// <summary>
    /// Bouncing off walls and ground on collision
    /// </summary>
    [FeatureEnabled( "Bouncing" )]
    [Property]
    public bool BouncingEnabled { get; set; } = false;

    /// <summary>
    /// How much the MoveHelper will "bounce" off walls when colliding with them<br/>
    /// (0f = No bounce, 1f = Full bounce)<br/>
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    [Range( 0f, 1f, true )]
    [Validate( nameof( isPhysical ), "Contoller is physical! Elasticity must be done through the collider", LogLevel.Error )]
    public float HorizontalElasticity { get; set; } = 0f;

    /// <summary>
    /// How much the MoveHelper will "bounce" off the ground when colliding with it<br/>
    /// (0f = No bounce, 1f = Full bounce)<br/>
    /// <see cref="GroundStickEnabled"/> will override this value when sticking to the ground<br/>
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    [Range( 0f, 1f, true )]
    public float VerticalElasticity { get; set; } = 0f;

    /// <summary>
    /// Include the ground elasticity when bouncing off the ground<br/>
    /// Make sure the ground surface doesn't have a very low elasticity!
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    public bool IncludeGroundElasticity { get; set; } = false;

    /// <summary>
    /// Minimum velocity to allow bouncing
    /// </summary>
    [Property]
    [Feature( "Bouncing" )]
    [Range( 0f, 100f, true )]
    public float ElasticityThreshold { get; set; } = 30f;

    /// <summary>
    /// Stick the MoveHelper to the ground (IsOnGround will default to false if disabled)
    /// </summary>
    [FeatureEnabled( "GroundStick" )]
    [Property]
    public bool GroundStickEnabled { get; set; } = true;

    /// <summary>
    /// How steep terrain can be for you to stand on without slipping.<br/>
    /// Min = Angle where velocity starts to drop off<br/>
    /// Max = Max angle where velocity is fully blocked (you can still slide down with gravity)
    /// </summary>
    [Property, Step( 0.1f )]
    [Feature( "GroundStick" )]
    public RangedFloat MaxGroundAngle
    {
        get;
        set
        {
            if ( value.Range == RangedFloat.RangeType.Between )
            {
                var min = MathX.Clamp( value.Min, 0f, 89f );
                var max = MathX.Clamp( value.Max, 0f, 89f );

                field = new RangedFloat( MathF.Min( min, max ), MathF.Max( min, max ) );
            }
            else
            {
                field = new RangedFloat( MathX.Clamp( value.FixedValue, 0f, 89f ) );
            }
        }
    } = new RangedFloat( 30f, 60f );

    /// <summary>
    /// How far from the ground the MoveHelper is going to stick (Useful for going down stairs!)
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    [Range( 1f, 32f, false )]
    public float GroundStickDistance { get; set; } = 12f;

    /// <summary>
    /// Stick to any <see cref="GroundObject"/> when standing on it
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    public bool StickToPlatforms { get; set; } = true;

    /// <summary>
    /// When the surface you're on has a surface velocity, apply it to the controller
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    public bool ApplySurfaceVelocity { get; set; } = true;

    /// <summary>
    /// When the surface you're on is slippery, scale the max ground angled relative to the friction<br/>
    /// Example: Ice with friction 0.5f, MaxGroundAngle will be 30f instead of 60f
    /// </summary>
    [Property]
    [Feature( "GroundStick" )]
    public bool ScaleGroundAngleBySurfaceFriction { get; set; } = true;

    /// <summary>
    /// Enable steps climbing (+1 Trace call)
    /// </summary>
    [FeatureEnabled( "Steps" )]
    [Property]
    public bool StepsEnabled { get; set; } = true;

    /// <summary>
    /// How high steps can be for you to climb on
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    [Range( 1f, 32f, false )]
    public float StepHeight { get; set; } = 12f;

    /// <summary>
    /// How deep it checks for steps (Minimum depth)
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    [Range( 0.1f, 8f, false )]
    public float StepDepth { get; set; } = 2f;

    /// <summary>
    /// Tolerance from a 90° surface before it's considered a valid step (Ex. Tolerance 1 = Between 89° and 91° can be a step, 0.1 = 89.9° to 90.1°)
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    [Range( 0f, 10f, false )]
    public float StepTolerance { get; set; } = 1f;

    /// <summary>
    /// Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain)
    /// </summary>
    [Feature( "Steps" )]
    [Property]
    public bool PseudoStepsEnabled { get; set; } = true;

    /// <summary>
    /// Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!)
    /// </summary>
    [FeatureEnabled( "Push" )]
    [Property]
    public bool PushEnabled { get; set; } = false;

    /// <summary>
    /// Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call)
    /// </summary>
    [Property]
    [Feature( "Push" )]
    [Validate( nameof( isPhysical ), "Contoller is physical! Make sure the tags are ignored on the collision matrix", LogLevel.Warn )]
    [Sync]
    public Dictionary<string, float> PushTagsWeight
    {
        get;
        set
        {
            field = value;
            _pushTags = BuildPushTags();
        }
    } = new Dictionary<string, float>() { { "player", 1f } };

    /// <summary>
    /// Apply gravity to this MoveHelper when not on the ground
    /// </summary>
    [FeatureEnabled( "Gravity" )]
    [Property]
    public bool GravityEnabled { get; set; } = true;

    /// <summary>
    /// Use the scene's gravity or our own
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    public bool UseSceneGravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    } = true;

    /// <summary>
    /// Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity)
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    [HideIf( "UseSceneGravity", true )]
    public bool UseVectorGravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    }

    private bool _usingFloatGravity => !UseVectorGravity && !UseSceneGravity;
    private bool _usingVectorGravity => UseVectorGravity && !UseSceneGravity;

    /// <summary>
    /// Units per second squared (Default is -850f)
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    [Range( -2000, 2000, false )]
    [ShowIf( "_usingFloatGravity", true )]
    public float Gravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    } = -850f;

    /// <summary>
    /// Units per second squared (Default is 0f, 0f, -850f)<br/>
    /// Changes which way <see cref="GroundStickEnabled"/> sticks to the ground
    /// </summary>
    [Property]
    [Feature( "Gravity" )]
    [ShowIf( "_usingVectorGravity", true )]
    public Vector3 VectorGravity
    {
        get;
        set
        {
            field = value;
            BuildGravity();
        }
    } = new Vector3( 0f, 0f, -850f );

    private Vector3 _appliedGravity;
    public Vector3 GravityNormal { get; private set; }
    public Vector3 AppliedGravity => _appliedGravity;

    /// <summary>
    /// Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck)
    /// </summary>
    [FeatureEnabled( "Unstuck" )]
    [Property]
    public bool UnstuckEnabled { get; set; } = true;

    /// <summary>
    /// How many trace calls it will attempt to get the MoveHelper unstuck
    /// </summary>
    [Property]
    [Feature( "Unstuck" )]
    [Range( 1, 100, false )]
    [Step( 1f )]
    public int MaxUnstuckTries { get; set; } = 40;

    /// <summary>
    /// The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside)
    /// </summary>
    [Sync] public Vector3 WishVelocity { get; set; }

    /// <summary>
    /// The resulting velocity after the simulation is done (Units per second)
    /// </summary>
    [Sync]
    public Vector3 Velocity
    {
        get;
        set
        {
            if ( PhysicallySimulated && Body.IsValid() )
                Body.Velocity = value;

            field = value;
        }
    }

    /// <summary>
    /// Velocity controlled by outside factors, such as knockback, rootmotion, etc.
    /// It is only applied to our final position and doesn't affect our Velocity.
    /// It should be handled outside the controller.
    /// </summary>
    [Sync] public Vector3 ExternalVelocity { get; set; }

    /// <summary>
    /// Is the MoveHelper currently touching the ground
    /// </summary>
    [Sync] public bool IsOnGround { get; set; }

    /// <summary>
    /// The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false)
    /// </summary>
    public Vector3 GroundNormal { get; private set; } = Vector3.Zero;

    /// <summary>
    /// The current ground angle you're standing on (Always 0f if IsOnGround false)
    /// </summary>
    public float GroundAngle => Vector3.GetAngle( GroundNormal, -GravityNormal );

    /// <summary>
    /// Gets the velocity multiplier for a given slope angle based on MaxGroundAngle range.<br/>
    /// Returns 1.0 below the start angle, 0.0 at/above the max angle, and lerps between.
    /// </summary>
    public float GetSlopeVelocityMultiplier( float angle )
    {
        if ( angle <= MaxGroundAngle.Min ) return 1f;
        if ( angle >= MaxGroundAngle.Max ) return 0f;
        return 1f - MathX.LerpInverse( angle, MaxGroundAngle.Min, MaxGroundAngle.Max );
    }

    /// <summary>
    /// Whether the given angle is considered standable (below max ground angle)
    /// </summary>
    public bool IsAngleStandable( float angle ) => angle <= MaxGroundAngle.Max * ( ScaleGroundAngleBySurfaceFriction ? GroundSurface?.Friction ?? 1f : 1f );

    /// <summary>
    /// The current surface you're standing on
    /// </summary>
    public Surface GroundSurface { get; private set; }

    /// <summary>
    /// The gameobject you're currently standing on
    /// </summary>
    public GameObject GroundObject { get; set; }

    /// <summary>
    /// Is the MoveHelper currently pushing against a wall
    /// </summary>
    public bool IsPushingAgainstWall { get; private set; }

    /// <summary>
    /// The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false)
    /// </summary>
    public Vector3 WallNormal { get; private set; } = Vector3.Zero;

    /// <summary>
    /// The gameobject you're currently pushing on
    /// </summary>
    public GameObject WallObject { get; set; }

    /// <summary>
    /// Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false)
    /// </summary>
    [Sync] public bool IsSlipping { get; private set; } // TODO IMPLEMENT

    /// <summary>
    /// The MoveHelper is stuck and we can't get it out
    /// </summary>
    [Sync] public bool IsStuck { get; private set; }

    /// <summary>
    /// To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later
    /// </summary>
    public float SkinWidth => Math.Min( Math.Max( 0.1f, TraceWidth * 0.05f ), GroundStickDistance );

    public float AppliedWidth => TraceWidth * WorldScale.x; // The width of the MoveHelper in world units
    public float AppliedDepth => TraceWidth * WorldScale.y; // The depth of the MoveHelper in world units
    public float AppliedHeight => TraceShape == TraceType.Sphere ? AppliedWidth :
        TraceShape == TraceType.Bounds ? TraceBounds.Size.z * WorldScale.z : TraceHeight * WorldScale.z; // The height of the MoveHelper in world units
    private Vector3 _upAxis => RotateWithGameObject ? WorldRotation.Up : Vector3.Up;
    private Vector3 _offset => ( RotateWithGameObject ? WorldRotation.Up : Vector3.Up ) * ( TraceShape == TraceType.Sphere || TraceShape == TraceType.Bounds ? 0f : AppliedHeight / 2f ); // The position of the MoveHelper in world units

    /// <summary>
    /// The bounds of this MoveHelper generated from the TraceWidth and TraceHeight
    /// </summary>
    public BBox Bounds { get; set; }
    private BBox _shrunkenBounds;
    private string[] _pushTags;
    private Vector3 _lastVelocity;
    private float _minimumTolerance => MathX.Clamp( Time.Delta / 2f, 0.005f, 0.1f ); // Floating precision tolerance, too high if used inside of OnUpdate so tied to update rate

    /// <summary>
    /// If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us
    /// </summary>
    public ShrimpleCharacterController UnstuckTarget;

    public Action<ShrimpleCollisionResult> OnCollide { get; set; }
    public bool IsOnPlatform => IsOnGround && GroundStickEnabled && !IsSlipping && StickToPlatforms && GroundObject.IsValid();
    public Vector3 PlatformVelocity => IsOnPlatform ? GroundObject.GetComponent<Collider>()?.GetVelocityAtPoint( WorldPosition ) ?? Vector3.Zero : Vector3.Zero;
    public bool IsOnSurfaceWithVelocity => IsOnGround && GroundStickEnabled && !IsSlipping && ApplySurfaceVelocity && GroundObject.IsValid();
    public Vector3 SurfaceVelocity => IsOnSurfaceWithVelocity ? GroundObject.GetComponent<Collider>()?.SurfaceVelocity * GroundObject.WorldRotation ?? Vector3.Zero : Vector3.Zero;
    public Vector3 GroundVelocity => PlatformVelocity + SurfaceVelocity;

    protected override void OnStart()
    {
        RebuildBounds();
        BuildGravity();
        _pushTags = BuildPushTags();
    }

    protected override void DrawGizmos()
    {
        if ( Gizmo.IsSelected )
        {
            Gizmo.GizmoDraw draw = Gizmo.Draw;
            draw.Color = Color.Blue;

            if ( TraceShape == TraceType.Box )
                draw.LineBBox( Bounds.Translate( Vector3.Up * Bounds.Size.z / 2f ) );
            if ( TraceShape == TraceType.Cylinder )
                draw.LineCylinder( Vector3.Zero, Vector3.Up * ( Bounds.Maxs.z - Bounds.Mins.z ), Bounds.Maxs.x, Bounds.Maxs.x, 24 );
            if ( TraceShape == TraceType.Sphere )
                draw.LineSphere( Vector3.Up * Bounds.Maxs.x, Bounds.Maxs.x );
            if ( TraceShape == TraceType.Bounds )
                draw.LineBBox( TraceBounds );

            if ( !UseSceneGravity && UseVectorGravity )
            {
                draw.Color = Color.Red;
                draw.LineThickness = 5f;
                draw.IgnoreDepth = true;
                draw.Arrow( Vector3.Zero, GravityNormal * 50f );
            }
        }
    }

    private BBox BuildBounds()
    {
        if ( TraceShape == TraceType.Bounds )
            return new BBox( TraceBounds.Mins * GameObject.WorldScale, TraceBounds.Maxs * GameObject.WorldScale );

        var x = GameObject.WorldScale.x;
        var y = GameObject.WorldScale.y;
        var z = GameObject.WorldScale.z;

        var width = TraceWidth / 2f * x;
        var depth = TraceWidth / 2f * y;
        var halfHeight = TraceHeight / 2f * z;

        // For cylinders we want the bounds to start at Z = 0 and extend up to the full height
        if ( TraceShape == TraceType.Cylinder )
            return new BBox( new Vector3( -width, -depth, 0f ), new Vector3( width, depth, halfHeight * 2f ) );

        return new BBox( new Vector3( -width, -depth, -halfHeight ), new Vector3( width, depth, halfHeight ) );
    }

    private void RebuildBounds()
    {
        Bounds = BuildBounds();
        _shrunkenBounds = Bounds.Grow( -SkinWidth );
    }

    private void BuildGravity()
    {
        _appliedGravity = UseSceneGravity ? Scene.PhysicsWorld.Gravity : UseVectorGravity ? VectorGravity : new Vector3( 0f, 0f, Gravity );
        GravityNormal = _appliedGravity.Normal;
    }

    private string[] BuildPushTags()
    {
        return PushTagsWeight.Keys.ToArray();
    }

    /// <summary>
    /// Casts the current bounds from to and returns the scene trace result
    /// </summary>
    /// <param name="bounds"></param>
    /// <param name="from"></param>
    /// <param name="to"></param>
    /// <param name="useCapsule">Use a capsule shape instead of the defaults</param>
    /// <returns></returns>
    public SceneTraceResult BuildTrace( BBox bounds, Vector3 from, Vector3 to, bool useCapsule = false )
    {
        SceneTrace builder = new SceneTrace(); // Empty trace builder

        if ( useCapsule )
        {
            var radius = bounds.Maxs.x;
            var halfHeight = ( bounds.Maxs.z - bounds.Mins.z ) / 2f;
            var hemiOffset = MathF.Max( halfHeight - radius, 0f );
            var capsule = new Capsule( Vector3.Down * hemiOffset, Vector3.Up * hemiOffset, radius ); // Do not rotate Vector3, it's rotated 17 lines down!
            builder = Game.SceneTrace.Capsule( capsule, from, to );
        }
        else
        {
            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )
                builder = Game.SceneTrace.Box( bounds, from, to );
            if ( TraceShape == TraceType.Cylinder )
                builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );
            if ( TraceShape == TraceType.Sphere )
                builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from + Vector3.Up * bounds.Maxs.x, to + Vector3.Up * bounds.Maxs.x );
        }

        builder = builder
            .IgnoreGameObjectHierarchy( GameObject )
            .WithoutTags( IgnoreTags );

        if ( RotateWithGameObject )
            builder = builder.Rotated( PhysicallySimulated ? Collider.WorldRotation : GameObject.WorldRotation );

        return builder.Run();
    }

    private SceneTraceResult BuildPushTrace( BBox bounds, Vector3 from, Vector3 to )
    {
        SceneTrace builder = new SceneTrace(); // Empty trace builder

        if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )
            builder = Game.SceneTrace.Box( bounds, from, to );
        if ( TraceShape == TraceType.Cylinder )
            builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );
        if ( TraceShape == TraceType.Sphere )
            builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from, to );

        builder = builder
            .IgnoreGameObjectHierarchy( GameObject )
            .WithAnyTags( _pushTags ); // Check for only the push tags

        if ( RotateWithGameObject )
            builder = builder.Rotated( GameObject.WorldRotation );

        return builder.Run();
    }

    /// <summary>
    /// Detach the MoveHelper from the ground and launch it somewhere (Units per second)
    /// </summary>
    /// <param name="amount"></param>
    public void Punch( in Vector3 amount )
    {
        var groundVel = GroundVelocity; // Capture before ungrounding
        IsOnGround = false;

        if ( PhysicallySimulated && Body.IsValid() )
            Body.Velocity = Body.Velocity + amount + groundVel;

        Velocity += amount + groundVel;
    }

    /// <summary>
    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
    /// </summary>
    /// <param name="manualUpdate">Just calculate but don't update position</param>
    public MoveHelperResult Move( bool manualUpdate = false ) => Move( Time.Delta, manualUpdate );

    /// <summary>
    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
    /// </summary>
    /// <param name="delta">The time step</param>
    /// <param name="manualUpdate">Just calculate but don't update position</param>
    public MoveHelperResult Move( float delta, bool manualUpdate = false )
    {
        // In physical mode the simulation runs inside the physics step callbacks.
        // Queue this call so the next PrePhysicsStep/PostPhysicsStep drives the body with the requested delta.
        if ( PhysicallySimulated )
        {
            if ( Body.IsValid() && !manualUpdate )
            {
                _physicalMoveDelta = delta;
                _physicalMovePending = true;
            }

            return new MoveHelperResult( WorldPosition, Velocity );
        }

        var goalVelocity = CalculateGoalVelocity( delta ); // Calculate the goal velocity using our Acceleration and Deceleration values

        // SIMULATE PUSH FORCES //
        if ( PushEnabled )
        {
            var pushTrace = BuildPushTrace( Bounds, WorldPosition + _offset, WorldPosition ); // Build a trace but using the Push tags instead of the Ignore tags

            if ( pushTrace.Hit ) // We're inside any of the push tags
            {
                foreach ( var tag in pushTrace.GameObject.Tags )
                {
                    if ( PushTagsWeight.TryGetValue( tag, out var tagWeight ) )
                    {
                        var otherPosition = pushTrace.GameObject.WorldPosition.WithZ( WorldPosition.z ); // Only horizontal pushing
                        var pushDirection = ( otherPosition - WorldPosition ).Normal;
                        var pushVelocity = pushDirection * tagWeight * 50f; // I find 50 u/s to be a good amount to push if the weight is 1.0 (!!!)

                        goalVelocity -= pushVelocity;
                    }
                }
            }
        }

        var finalPosition = WorldPosition;

        var moveHelperResult = CollideAndSlide( goalVelocity, finalPosition + _offset, delta ); // Simulate the MoveHelper

        finalPosition = moveHelperResult.Position;
        var finalVelocity = moveHelperResult.Velocity;
        // SIMULATE GRAVITY //
        if ( GravityEnabled && Gravity != 0f )
        {
            if ( !IsOnGround || IsSlipping || !GroundStickEnabled )
            {
                var gravity = AppliedGravity * delta;
                var gravityResult = CollideAndSlide( gravity, moveHelperResult.Position, delta, gravityPass: true ); // Apply and simulate the gravity step

                finalPosition = gravityResult.Position;
                finalVelocity += gravityResult.Velocity;
            }
        }

        if ( !ExternalVelocity.IsNearZeroLength )
        {
            moveHelperResult = CollideAndSlide( ExternalVelocity, finalPosition, delta );
        }

        finalPosition = moveHelperResult.Position - _offset; // Compensate for the offset we added at the beginning
        _lastVelocity = Velocity * delta;

        if ( !manualUpdate )
        {
            Velocity = finalVelocity;
            WorldPosition = finalPosition + SurfaceVelocity * delta; // Actually updating the position is "expensive" so we only do it once at the end
        }

        return new MoveHelperResult( finalPosition, finalVelocity, moveHelperResult.Offset );
    }

    /// <summary>
    /// Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity
    /// </summary>
    public struct MoveHelperResult
    {
        public Vector3 Position;
        public Vector3 Velocity;
        internal Vector3 Offset;
        internal float Leftover = 1f;

        public MoveHelperResult( Vector3 position, Vector3 velocity )
        {
            Position = position;
            Velocity = velocity;
        }

        internal MoveHelperResult( Vector3 position, Vector3 velocity, Vector3 offset, float leftover = 1f )
        {
            Position = position;
            Velocity = velocity;
            Offset = offset;
            Leftover = leftover;
        }
    }

    bool _bounced = false;

    private MoveHelperResult CollideAndSlide( Vector3 velocity, Vector3 position, float delta, int depth = 0, bool gravityPass = false ) =>
        CollideAndSlide( new MoveHelperResult( position, velocity ), delta, depth, gravityPass );

    private MoveHelperResult CollideAndSlide( MoveHelperResult current, float delta, int depth = 0, bool gravityPass = false )
    {
        if ( depth >= MaxBounces )
            return current;

        var velocity = current.Velocity * delta; // I like to set Velocity as units/second but we have to deal with units/tick here
        var position = current.Position;

        // GROUND AND UNSTUCK CHECK //
        if ( depth == 0 ) // Only check for the first step since it's impossible to get stuck on other steps
        {
            var groundTrace = BuildTrace( _shrunkenBounds, position, position + AppliedGravity.Normal * ( GroundStickDistance + SkinWidth * 1.1f ) ); // Compensate for floating inaccuracy

            if ( groundTrace.StartedSolid )
            {
                IsStuck = true;
                if ( UnstuckEnabled )
                {
                    if ( UnstuckTarget == null )
                    {
                        IsStuck = !TryUnstuck( position, out var result );

                        if ( !IsStuck )
                        {
                            position = result; // Update the new position

                            if ( PhysicallySimulated && Body.IsValid() )
                            {
                                Body.WorldPosition = result;
                                Body.Velocity = Vector3.Zero;
                            }

                            if ( groundTrace.GameObject != null )
                                if ( groundTrace.GameObject.Components.TryGet<ShrimpleCharacterController>( out var otherHelper ) )
                                    otherHelper.UnstuckTarget = this; // We already solved this, no need to unstuck the other helper
                        }
                        else
                        {
                            return new MoveHelperResult( position, Vector3.Zero ); // Mission failed, bail out!
                        }
                    }
                    else
                    {
                        UnstuckTarget = null; // Alright the other MoveHelper got us unstuck so just do nothing
                    }
                }
            }
            else
            {
                var hasLanded = !IsOnGround && Vector3.Dot( Velocity, AppliedGravity ) >= 0f && groundTrace.Hit && groundTrace.Distance <= SkinWidth * 2f; // Wasn't on the ground and now is
                var isGrounded = IsOnGround && groundTrace.Hit; // Was already on the ground and still is, this helps stick when going down stairs

                IsOnGround = hasLanded || isGrounded;
                GroundSurface = IsOnGround ? groundTrace.Surface : null;
                GroundNormal = IsOnGround ? groundTrace.Normal : -AppliedGravity.Normal;
                GroundObject = IsOnGround ? groundTrace.GameObject : null;
                IsSlipping = IsOnGround && !IsAngleStandable( GroundAngle );

                if ( IsSlipping && !gravityPass && Vector3.Dot( velocity, AppliedGravity ) < 0f )
                    velocity = velocity.WithZ( 0f ); // If we're slipping ignore any extra velocity we had

                // FIX: Clamp velocity when slipping to prevent buildup
                // Project velocity onto the slope and limit accumulation
                if ( IsSlipping )
                {
                    var slopeDir = Vector3.VectorPlaneProject( AppliedGravity.Normal, GroundNormal ).Normal;
                    var slopeSpeed = Vector3.Dot( velocity, slopeDir );
                    var maxSlipSpeed = AppliedGravity.Length * delta * 2f; // Reasonable max slip speed per frame

                    if ( slopeSpeed > maxSlipSpeed )
                    {
                        // Clamp the velocity component along the slope
                        velocity = velocity - slopeDir * ( slopeSpeed - maxSlipSpeed );
                    }
                }

                if ( IsOnGround && GroundStickEnabled && !IsSlipping )
                {
                    position = groundTrace.EndPosition + -AppliedGravity.Normal * SkinWidth; // Place on the ground
                    velocity = Vector3.VectorPlaneProject( velocity, GroundNormal ); // Follow the ground you're on without projecting Z
                }

                IsStuck = false;
            }
        }

        if ( velocity.IsNearlyZero( _minimumTolerance ) ) // Not worth continuing, reduces small stutter
        {
            return new MoveHelperResult( position, Vector3.Zero, current.Offset );
        }

        var toTravel = velocity.Length * current.Leftover + SkinWidth;
        var targetPosition = position + velocity.Normal * toTravel;
        var travelTrace = BuildTrace( _shrunkenBounds, position, targetPosition );

        if ( travelTrace.Hit )
        {
            var travelled = velocity.Normal * Math.Max( travelTrace.Distance - SkinWidth, 0f );
            var leftover = velocity - travelled; // How much leftover velocity still needs to be simulated
            var speed = leftover.Length;
            var angle = Vector3.GetAngle( -AppliedGravity.Normal, travelTrace.Normal );

            if ( toTravel >= SkinWidth && travelTrace.Distance < SkinWidth )
                travelled = Vector3.Zero;

            var elasticityDirection = MathX.Lerp( HorizontalElasticity, VerticalElasticity, angle / 90f );
            var elasticity = PhysicallySimulated ? 0f : BouncingEnabled ?
                velocity.Length / delta <= ElasticityThreshold ?
                    0f : elasticityDirection * ( IncludeGroundElasticity ? GroundSurface?.Elasticity ?? 1f : 1f )
                : 0f; // Allah help me format this better

            if ( IsAngleStandable( angle ) ) // Terrain we can walk on
            {
                var projectedLeftover = leftover;

                if ( gravityPass || !IsOnGround )
                    projectedLeftover = Vector3.VectorPlaneProject( projectedLeftover, travelTrace.Normal ); // Don't project the vertical velocity after landing else it boosts your horizontal velocity
                else
                    projectedLeftover = projectedLeftover.ProjectAndScale( travelTrace.Normal ); // Project the velocity along the terrain

                // Apply slope velocity reduction only when going uphill (against gravity)
                var isGoingUphill = Vector3.Dot( projectedLeftover, AppliedGravity ) < 0f;
                if ( isGoingUphill )
                {
                    var slopeMultiplier = GetSlopeVelocityMultiplier( angle );
                    projectedLeftover *= slopeMultiplier;
                }

                if ( elasticity > 0 )
                    leftover = Vector3.Lerp( projectedLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );
                else
                    leftover = projectedLeftover;

                IsPushingAgainstWall = false;
                WallObject = null;
            }
            else
            {
                var climbedStair = false;

                if ( angle >= 90f - WallTolerance && angle <= 90f + WallTolerance ) // Check for walls
                    IsPushingAgainstWall = true; // We're pushing against a wall

                if ( StepsEnabled )
                {
                    var isStep = angle >= 90f - StepTolerance && angle <= 90f + StepTolerance;

                    if ( isStep || PseudoStepsEnabled ) // Check for steps
                    {
                        if ( IsOnGround ) // Stairs VVV
                        {
                            var stepHorizontal = Vector3.VectorPlaneProject( velocity.Normal, AppliedGravity.Normal ).Normal * StepDepth; // How far in front we're looking for steps
                            var stepVertical = -AppliedGravity.Normal * ( StepHeight + SkinWidth ); // How high we're looking for steps + Some to compensate for floating inaccuracy
                            var stepTrace = BuildTrace( _shrunkenBounds, travelTrace.EndPosition + stepHorizontal + stepVertical, travelTrace.EndPosition + stepHorizontal );
                            var stepAngle = Vector3.GetAngle( stepTrace.Normal, -AppliedGravity.Normal );

                            if ( !stepTrace.StartedSolid && stepTrace.Hit && IsAngleStandable( stepAngle ) ) // We found a step!
                            {
                                if ( isStep || !IsSlipping && PseudoStepsEnabled )
                                {
                                    var stepDistance = stepTrace.EndPosition - travelTrace.EndPosition;
                                    var stepTravelled = -AppliedGravity.Normal * stepDistance;
                                    position += stepTravelled; // Offset our position by the height of the step climbed
                                    climbedStair = true;

                                    IsPushingAgainstWall = false; // Nevermind, we're not against a wall, we climbed a step!
                                    WallObject = null;
                                }
                            }
                        }
                    }
                }

                if ( IsPushingAgainstWall )
                {
                    // Scale our leftover velocity based on the angle of approach relative to the wall
                    // (Perpendicular = 0%, Parallel = 100%)
                    var scale = ScaleAgainstWalls ? 1f - Vector3.Dot( -travelTrace.Normal.Normal / GripFactorReduction, velocity.Normal ) : 1f;
                    var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal.Normal ) : leftover.ProjectAndScale( travelTrace.Normal.Normal );

                    if ( elasticity > 0 )
                        leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );
                    else
                        leftover = ( wallLeftover * scale ).WithZ( wallLeftover.z );

                    WallObject = travelTrace.GameObject;
                    WallNormal = travelTrace.Normal;
                }
                else
                {
                    if ( !climbedStair )
                    {
                        var scale = IsSlipping ? 1f : 1f - Vector3.Dot( -travelTrace.Normal / GripFactorReduction, velocity.Normal );
                        var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal ) * scale : leftover.ProjectAndScale( travelTrace.Normal );

                        if ( elasticity > 0 )
                            leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );
                        else
                            leftover = wallLeftover;
                    }

                }


            }

            var previousVelocity = velocity;
            var leftoverSpeed = leftover.Length / speed; // How much speed we had left after the collision
            velocity = leftover.Normal * velocity.Length * leftoverSpeed;

            if ( travelled.Length <= _minimumTolerance && leftover.Length <= _minimumTolerance )
                return new MoveHelperResult( position + travelled, velocity / delta, current.Offset );

            if ( elasticity > 0 )
                _bounced = true;

            var offset = position - current.Position;
            var newResult = CollideAndSlide( new MoveHelperResult( position + travelled, velocity / delta, offset, leftover.Length / velocity.Length ), delta, depth + 1, gravityPass ); // Simulate another bounce for the leftover velocity from the latest position

            if ( depth == 0 && !gravityPass )
            {
                var collision = new ShrimpleCollisionResult()
                    .WithHitNormal( travelTrace.Normal )
                    .WithHitPosition( travelTrace.HitPosition )
                    .WithAngle( angle )
                    .WithHitObject( travelTrace.GameObject )
                    .WithHitSurface( travelTrace.Surface )
                    .WithHitVelocityBefore( previousVelocity / delta )
                    .WithHitVelocityAfter( newResult.Velocity );

                OnCollide?.Invoke( collision );
            }

            return newResult;
        }

        if ( depth == 0 && !gravityPass )
        {
            IsPushingAgainstWall = false;
            WallObject = null;
        }

        if ( gravityPass && _bounced )
        {
            _bounced = false;
            velocity -= AppliedGravity * delta * delta * 1.4f; // Evil hack so it doesn't lose velocity due to gravity passes after each bounce
        }

        return new MoveHelperResult( position + velocity, velocity / delta, current.Offset ); // We didn't hit anything? Ok just keep going then :-)
    }

    private float CalculateGoalSpeed( Vector3 wishVelocity, Vector3 velocity, bool isAccelerating, float delta )
    {
        if ( !AccelerationEnabled ) return 999999999f; // Should be enough for the freaks that don't want acceleration

        float goalSpeed;

        var isSameDirection = velocity.IsNearlyZero( 1f ) || Vector3.Dot( wishVelocity.WithZ( 0f ).Normal, velocity.WithZ( 0f ).Normal ) >= 0f; // Is our wishVelocity roughly moving towards our velocity already?
        var currentSpeed = Math.Max( Velocity.WithZ( 0f ).Length, 10f );
        var acceleration = ( IsOnGround ? GroundAcceleration : AirAcceleration ) * ( FixedAcceleration ? 1f : AccelerationCurve.Evaluate( currentSpeed ) );
        var deceleration = ( IsOnGround ? GroundDeceleration : AirDeceleration ) * ( FixedDeceleration ? 1f : DecelerationCurve.Evaluate( currentSpeed ) );

        if ( isAccelerating )
            goalSpeed = acceleration;
        else
            goalSpeed = !isSameDirection ? Math.Max( acceleration, deceleration ) : deceleration; // Makes movement more responsive especially for flying or rolling

        if ( !IgnoreGroundSurface && GroundSurface != null )
            goalSpeed *= GroundSurface.Friction; // Take into account the ground's friction

        goalSpeed *= delta;

        return goalSpeed;
    }

    private Vector3 CalculateGoalVelocity( float delta )
    {
        bool shouldIgnoreZ = IgnoreZ || ( IgnoreZWhenZero && WishVelocity.z.AlmostEqual( 0f ) );
        var wishVelocity = shouldIgnoreZ ? ( WishVelocity.Normal * WishVelocity.Length ).WithZ( Velocity.z ) : WishVelocity;
        var isAccelerating = shouldIgnoreZ ? wishVelocity.WithZ( 0f ).Length >= Velocity.WithZ( 0f ).Length : wishVelocity.Length >= Velocity.Length;

        var goalSpeed = CalculateGoalSpeed( wishVelocity, Velocity, isAccelerating, delta );
        var goalVelocity = Velocity.MoveTowards( wishVelocity, goalSpeed );

        return shouldIgnoreZ ? goalVelocity.WithZ( Velocity.z ) : goalVelocity;
    }

    /// <summary>
    /// Attempts to return the nearest standable position until you're not longer inside other colliders<br/>
    /// If it fails too frequently, consider increasing <see cref="MaxUnstuckTries"/>
    /// </summary>
    /// <param name="position">The start position</param>
    /// <param name="result">The final standable position</param>
    /// <returns>True if it found one, false if it failed.</returns>
    public bool TryUnstuck( Vector3 position, out Vector3 result )
    {
        if ( _lastVelocity == Vector3.Zero )
            _lastVelocity = -AppliedGravity.Normal;

        var velocityLength = _lastVelocity.Length + SkinWidth;
        var startPos = position - _lastVelocity.Normal * velocityLength; // Try undoing the last velocity 1st
        var endPos = position;

        for ( int i = 0; i < MaxUnstuckTries + 1; i++ )
        {
            if ( i == 1 )
                startPos = position + -AppliedGravity.Normal * 2f; // Try going up 2nd

            if ( i > 1 && i < MaxUnstuckTries / 2f )
                startPos = position + Vector3.Random.Normal * ( (float)i / 2f ); // Start randomly checking 3rd

            if ( i >= MaxUnstuckTries / 2f )
                startPos = position + Vector3.Random.Normal * i; // Ok at this point start checking further

            if ( startPos - endPos == Vector3.Zero ) // No difference!
                continue;

            var unstuckTrace = BuildTrace( _shrunkenBounds, startPos, endPos, PhysicallySimulated );

            if ( !unstuckTrace.StartedSolid )
            {
                result = unstuckTrace.EndPosition - _lastVelocity.Normal * SkinWidth / 4f;
                _lastVelocity = Vector3.Zero;
                return true;
            }
        }

        result = position;
        return false;
    }

    /// <summary>
    /// Debug don't use
    /// </summary>
    /// <param name="position"></param>
    /// <param name="title"></param>
    /// <returns></returns>
    private bool TestPosition( Vector3 position, string title )
    {
        var testTrace = BuildTrace( _shrunkenBounds, position, position );

        if ( testTrace.StartedSolid )
        {
            Log.Info( $"[{RealTime.Now}]{title} {GameObject.Name} started solid at {position} against {testTrace.GameObject}" );
            return true;
        }

        return false;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        if ( !ManuallyUpdate && Active && !PhysicallySimulated ) // If we're not physically simulated we Move inside of OnFixedUpdate
            Move( ActiveAutoTimeDelta );
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();

        Collider?.Destroy();
        Body?.Destroy();
    }

    protected override void OnDisabled()
    {
        base.OnDisabled();

        Collider?.Enabled = false;
        Body?.Enabled = false;
    }

    protected override void OnEnabled()
    {
        base.OnEnabled();

        Collider?.Enabled = true;
        Body?.Enabled = true;
    }

    public override int ComponentVersion => 4;

    [JsonUpgrader( typeof( ShrimpleCharacterController ), 3 )]
    private static void UpdateTraceShape( JsonObject json )
    {
        if ( json.Remove( "CylinderTrace", out var newNode ) && newNode is JsonValue boolNode && boolNode.TryGetValue<bool>( out var isCylinder ) )
            json["TraceShape"] = isCylinder ? "Cylinder" : "Box";
        else
            json["TraceShape"] = "Box";
    }

    [JsonUpgrader( typeof( ShrimpleCharacterController ), 4 )]
    private static void UpdateMaxGroundAngle( JsonObject json )
    {
        // Old default was 60f, new default is (30f, 60f)
        if ( json.TryGetPropertyValue( "MaxGroundAngle", out var oldValue ) && oldValue is JsonValue floatValue && floatValue.TryGetValue<float>( out var oldAngle ) )
        {
            json.Remove( "MaxGroundAngle" );
            var startAngle = oldAngle * 0.75f;
            json["MaxGroundAngle"] = $"{startAngle:F2} {oldAngle:F2}";
        }
    }
}
Debug: View Raw JSON Response
{
    "TotalCount": 19,
    "Files": [
        {
            "Ident": "fish.scc",
            "Path": "code/Examples/ShrimpleFlyer.cs",
            "FileName": "ShrimpleFlyer.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "using Sandbox.Citizen;\r\n\r\nnamespace ShrimpleCharacterController;\r\n\r\n[Hide]\r\npublic sealed class ShrimpleFlyer : Component\r\n{\r\n    [RequireComponent]\r\n    public ShrimpleCharacterController Controller { get; set; }\r\n    public GameObject Camera { get; set; }\r\n\r\n    [Property]\r\n    [Range(400f, 1600f)]\r\n    public float WalkSpeed { get; set; } = 800f;\r\n\r\n    [Property]\r\n    [Range(800f, 4000f)]\r\n    public float RunSpeed { get; set; } = 2400f;\r\n\r\n    public Angles EyeAngles { get; set; }\r\n\r\n    protected override void OnStart()\r\n    {\r\n        base.OnStart();\r\n\r\n        Camera = new GameObject(true, \"Camera\");\r\n        Camera.SetParent(GameObject);\r\n        var cameraComponent = Camera.Components.Create<CameraComponent>();\r\n        cameraComponent.ZFar = 32768f;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        var isDucking = Input.Down(\"Duck\");\r\n        var isRunning = Input.Down(\"Run\");\r\n        var ascending = Input.Down(\"Jump\") ? 1f : 0f;\r\n        var descending = Input.Down(\"Duck\") ? -1f : 0f;\r\n        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;\r\n        var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();\r\n\r\n        Controller.WishVelocity = wishDirection * wishSpeed;\r\n        Controller.Move();\r\n    }\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n        base.OnUpdate();\r\n\r\n        EyeAngles += Input.AnalogLook;\r\n        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -40f, 40f));\r\n\r\n        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 760f;\r\n        Camera.WorldRotation = EyeAngles.ToRotation();\r\n        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/ShrimpleCollisionResult.cs",
            "FileName": "ShrimpleCollisionResult.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\npublic struct ShrimpleCollisionResult\r\n{\r\n    public Vector3 HitPosition;\r\n    public Vector3 HitNormal;\r\n    public Vector3 HitVelocityBefore;\r\n    public Vector3 HitVelocityAfter;\r\n    public float Angle;\r\n    public GameObject HitObject;\r\n    public Surface HitSurface;\r\n\r\n    public ShrimpleCollisionResult() { }\r\n\r\n    public ShrimpleCollisionResult(Vector3 hitPosition, Vector3 hitNormal, Vector3 hitVelocityBefore, Vector3 hitVelocityAfter, float angle, GameObject hitObject, Surface hitSurface)\r\n    {\r\n        HitPosition = hitPosition;\r\n        HitNormal = hitNormal;\r\n        HitVelocityBefore = hitVelocityBefore;\r\n        HitVelocityAfter = hitVelocityAfter;\r\n        Angle = angle;\r\n        HitObject = hitObject;\r\n        HitSurface = hitSurface;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitPosition(Vector3 hitPosition)\r\n    {\r\n        var result = this;\r\n        result.HitPosition = hitPosition;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitNormal(Vector3 hitNormal)\r\n    {\r\n        var result = this;\r\n        result.HitNormal = hitNormal;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitVelocityBefore(Vector3 hitVelocityBefore)\r\n    {\r\n        var result = this;\r\n        result.HitVelocityBefore = hitVelocityBefore;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitVelocityAfter(Vector3 hitVelocityAfter)\r\n    {\r\n        var result = this;\r\n        result.HitVelocityAfter = hitVelocityAfter;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithAngle(float angle)\r\n    {\r\n        var result = this;\r\n        result.Angle = angle;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitObject(GameObject hitObject)\r\n    {\r\n        var result = this;\r\n        result.HitObject = hitObject;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitSurface(Surface hitSurface)\r\n    {\r\n        var result = this;\r\n        result.HitSurface = hitSurface;\r\n        return result;\r\n    }\r\n}"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/ShrimpleCharacterController.Physical.cs",
            "FileName": "ShrimpleCharacterController.Physical.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\npublic partial class ShrimpleCharacterController\r\n{\r\n    bool _didStep;\r\n    Vector3 _stepPosition;\r\n\r\n    // Set by Move() when ManuallyUpdate is enabled in physical mode, consumed by the next physics step\r\n    bool _physicalMovePending;\r\n    float _physicalMoveDelta;\r\n\r\n    // The delta this physics step should simulate (the manual Move() delta, otherwise the fixed step)\r\n    float ActiveDelta => ( ManuallyUpdate && _physicalMovePending ) ? _physicalMoveDelta : ActiveAutoTimeDelta;\r\n\r\n    // The solver always integrates over Time.Delta, so we scale velocity to cover ActiveDelta worth of distance\r\n    float MoveScale => Time.Delta > 0f ? ActiveDelta / Time.Delta : 1f;\r\n\r\n    void IScenePhysicsEvents.PrePhysicsStep()\r\n    {\r\n        if ( !PhysicallySimulated ) return;\r\n        if ( !Body.IsValid() ) return;\r\n        if ( ManuallyUpdate && !_physicalMovePending ) return; // Wait for a manual Move() call to drive us\r\n\r\n        _didStep = false;\r\n\r\n        UpdateBodyPhysics();\r\n        UpdateMassCenter();\r\n        UpdateMovement();\r\n    }\r\n\r\n    void IScenePhysicsEvents.PostPhysicsStep()\r\n    {\r\n        if ( !Body.IsValid() )\r\n        {\r\n            WorldPosition += GroundVelocity * Time.Delta;\r\n            return;\r\n        }\r\n\r\n        if ( !PhysicallySimulated ) return;\r\n        if ( ManuallyUpdate && !_physicalMovePending ) return;\r\n\r\n        if ( _didStep )\r\n            Body.WorldPosition = _stepPosition;\r\n\r\n        CategorizePhysicalGround();\r\n        ApplyGroundVelocity();\r\n        Velocity = Body.Velocity / MoveScale; // De-scale back to logical units/second\r\n\r\n        _physicalMovePending = false;\r\n    }\r\n\r\n    private void UpdateMassCenter()\r\n    {\r\n        if ( !Body.IsValid() ) return;\r\n\r\n        var wishSpeed = WishVelocity.WithZ( 0 ).Length;\r\n        var halfHeight = AppliedHeight * 0.5f;\r\n        float massCenter = IsOnGround ? wishSpeed.Clamp( 0, halfHeight ) : halfHeight;\r\n\r\n        Body.MassCenterOverride = new Vector3( 0, 0, massCenter );\r\n        Body.OverrideMassCenter = true;\r\n    }\r\n\r\n    private void UpdateBodyPhysics()\r\n    {\r\n        if ( !Collider.IsValid() || !Body.IsValid() ) return;\r\n\r\n        var onDynamic = false;\r\n        if ( IsOnGround && GroundObject.IsValid() )\r\n        {\r\n            var groundBody = GroundObject.GetComponent<Rigidbody>();\r\n            onDynamic = groundBody != null && groundBody.PhysicsBody.BodyType == PhysicsBodyType.Dynamic;\r\n        }\r\n\r\n        Body.Gravity = false;\r\n\r\n        var velocityHorizontal = Body.Velocity.WithZ( 0 ).Length;\r\n        bool wantsBrakes = IsOnGround && WishVelocity.Length < 1f && velocityHorizontal < 10f;\r\n        Body.LinearDamping = wantsBrakes ? 10f : 0.1f;\r\n        Body.AngularDamping = 1f;\r\n\r\n        float friction = 0f;\r\n        if ( IsOnGround && WishVelocity.Length < 0.1f )\r\n            friction = onDynamic ? 0.5f : 1f;\r\n        Collider.Friction = friction;\r\n    }\r\n\r\n    private void AddWishVelocity()\r\n    {\r\n        var z = Body.Velocity.z;\r\n        var velocity = CalculateGoalVelocity( ActiveDelta );\r\n\r\n        if ( IsOnGround && GroundStickEnabled && !GroundNormal.IsNearlyZero( 0.01f ) )\r\n        {\r\n            if ( MathF.Abs( GroundNormal.z ) > 0.01f )\r\n                velocity = velocity.WithZ( -( GroundNormal.x * velocity.x + GroundNormal.y * velocity.y ) / GroundNormal.z );\r\n\r\n            var isGoingUphill = Vector3.Dot( velocity, AppliedGravity ) < 0f;\r\n            var slopeMultiplier = isGoingUphill ? GetSlopeVelocityMultiplier( GroundAngle ) : 1f;\r\n            velocity *= slopeMultiplier;\r\n        }\r\n\r\n        if ( IsOnGround && !GroundStickEnabled )\r\n            velocity.z = z;\r\n\r\n        if ( GravityEnabled && ( !IsOnGround || IsSlipping || !GroundStickEnabled ) )\r\n            velocity += AppliedGravity * ActiveDelta;\r\n\r\n        Body.Velocity = velocity * MoveScale; // Scale so the solver covers ActiveDelta worth of distance over Time.Delta\r\n    }\r\n\r\n    private void UpdateMovement()\r\n    {\r\n        if ( IsOnGround && GroundStickEnabled && !IsSlipping )\r\n            StickToGround();\r\n\r\n        AddWishVelocity();\r\n\r\n        var wishHorizontal = WishVelocity.WithZ( 0 );\r\n        var bodyHorizontal = Body.Velocity.WithZ( 0 );\r\n        var moveDir = wishHorizontal.IsNearlyZero( 0.1f ) ? bodyHorizontal : wishHorizontal;\r\n        if ( moveDir.IsNearlyZero( 0.1f ) ) return;\r\n\r\n        var speed = MathF.Max( bodyHorizontal.Length * Time.Delta, StepDepth );\r\n        var vel = moveDir.Normal * speed;\r\n        var forwardTrace = BuildTrace( _shrunkenBounds, WorldPosition + _offset - vel.Normal * SkinWidth, WorldPosition + _offset + vel );\r\n\r\n        if ( !forwardTrace.Hit || forwardTrace.StartedSolid ) return;\r\n\r\n        var forwardAngle = Vector3.GetAngle( -AppliedGravity.Normal, forwardTrace.Normal );\r\n        if ( IsAngleStandable( forwardAngle ) && Vector3.Dot( Body.Velocity, forwardTrace.Normal ) < 0f )\r\n            Body.Velocity = Vector3.VectorPlaneProject( Body.Velocity, forwardTrace.Normal );\r\n\r\n        if ( StepsEnabled && IsOnGround )\r\n            TryStepUp( forwardTrace, vel );\r\n    }\r\n\r\n    private void TryStepUp( SceneTraceResult forwardTrace, Vector3 vel )\r\n    {\r\n        if ( forwardTrace.StartedSolid || !forwardTrace.Hit ) return;\r\n\r\n        var angle = Vector3.GetAngle( -GravityNormal, forwardTrace.Normal );\r\n        if ( angle < 90f - StepTolerance || angle > 90f + StepTolerance ) return;\r\n\r\n        var stepHorizontal = Vector3.VectorPlaneProject( vel.Normal, GravityNormal ).Normal * StepDepth;\r\n        var stepVertical = -GravityNormal * ( StepHeight + SkinWidth );\r\n        var stepTrace = BuildTrace( _shrunkenBounds, forwardTrace.EndPosition + stepHorizontal + stepVertical, forwardTrace.EndPosition + stepHorizontal );\r\n\r\n        if ( stepTrace.StartedSolid || !stepTrace.Hit ) return;\r\n        if ( !IsAngleStandable( Vector3.GetAngle( stepTrace.Normal, -GravityNormal ) ) ) return;\r\n\r\n        var newFeetPos = stepTrace.EndPosition - _offset + _upAxis * SkinWidth;\r\n        if ( newFeetPos.z - WorldPosition.z < 0.5f ) return;\r\n\r\n        _didStep = true;\r\n        _stepPosition = newFeetPos;\r\n        Body.WorldPosition = _stepPosition;\r\n        Body.Velocity = Body.Velocity.WithZ( 0 );\r\n    }\r\n\r\n    private void ApplyGroundVelocity()\r\n    {\r\n        if ( !IsOnGround || !GroundStickEnabled || IsSlipping ) return;\r\n\r\n        var groundVelocity = PlatformVelocity * Time.Delta + SurfaceVelocity * ActiveDelta;\r\n        if ( groundVelocity.IsNearZeroLength ) return;\r\n\r\n        Body.WorldPosition += groundVelocity;\r\n    }\r\n\r\n    private void StickToGround()\r\n    {\r\n        var currentPosition = WorldPosition;\r\n        var from = currentPosition + _offset - GravityNormal * StepHeight;\r\n        var to = currentPosition + _offset + GravityNormal * ( GroundStickDistance + SkinWidth );\r\n\r\n        var trace = BuildTrace( _shrunkenBounds, from, to );\r\n\r\n        if ( trace.StartedSolid ) return;\r\n\r\n        if ( trace.Hit )\r\n        {\r\n            var surfaceAngle = Vector3.GetAngle( -GravityNormal, trace.Normal );\r\n            if ( !IsAngleStandable( surfaceAngle ) ) return;\r\n\r\n            var targetFeetPos = trace.EndPosition - _offset + _upAxis * SkinWidth;\r\n\r\n            Body.WorldPosition = targetFeetPos;\r\n            Body.Sleeping = false;\r\n\r\n            if ( Body.Velocity.z > 0f )\r\n                Body.Velocity = Body.Velocity.WithZ( 0 );\r\n\r\n            IsOnGround = true;\r\n            GroundNormal = trace.Normal;\r\n            GroundSurface = trace.Surface;\r\n            GroundObject = trace.GameObject;\r\n            IsSlipping = false;\r\n        }\r\n    }\r\n\r\n    private void CategorizePhysicalGround()\r\n    {\r\n        var position = WorldPosition + _offset;\r\n        var shortCylinderOffset = AppliedWidth / 2f;\r\n        var shortCylinderBounds = new BBox( _shrunkenBounds.Mins, new Vector3( _shrunkenBounds.Maxs.x, _shrunkenBounds.Maxs.y, _shrunkenBounds.Maxs.z - shortCylinderOffset ) );\r\n        var groundTrace = BuildTrace( shortCylinderBounds, position + GravityNormal * shortCylinderOffset / 2f, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );\r\n\r\n        if ( groundTrace.StartedSolid )\r\n        {\r\n            var stuckCheck = BuildTrace( _shrunkenBounds.Grow( -SkinWidth ), position, position, useCapsule: true );\r\n\r\n            if ( stuckCheck.StartedSolid )\r\n            {\r\n                IsStuck = true;\r\n\r\n                if ( UnstuckEnabled && TryUnstuck( position, out var unstuckResult ) )\r\n                {\r\n                    IsStuck = false;\r\n                    Body.WorldPosition = unstuckResult - _offset;\r\n                    Body.Velocity = Vector3.Zero;\r\n                }\r\n\r\n                var fallbackTrace = BuildTrace( _shrunkenBounds, position + -GravityNormal * 4f, position + GravityNormal * 2f );\r\n\r\n                if ( fallbackTrace.Hit && !fallbackTrace.StartedSolid && Vector3.Dot( fallbackTrace.Normal, -GravityNormal ) > 0f )\r\n                {\r\n                    var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, fallbackTrace.Normal ) );\r\n                    IsOnGround = true;\r\n                    GroundNormal = fallbackTrace.Normal;\r\n                    GroundSurface = fallbackTrace.Surface;\r\n                    GroundObject = fallbackTrace.GameObject;\r\n                    IsSlipping = !standable;\r\n                }\r\n                return;\r\n            }\r\n\r\n            groundTrace = BuildTrace( _shrunkenBounds, position, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );\r\n        }\r\n\r\n        IsStuck = false;\r\n\r\n        if ( groundTrace.Hit )\r\n        {\r\n            var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, groundTrace.Normal ) );\r\n            var landingAngle = Vector3.Dot( Velocity.Normal, groundTrace.Normal );\r\n            var verticalAngle = Vector3.Dot( Velocity.Normal, GravityNormal );\r\n            var goingUp = verticalAngle < 0.1f;\r\n            var hasLanded = !IsOnGround && landingAngle <= ( MaxGroundAngle.Max / 180f ) && groundTrace.Distance <= SkinWidth * ( goingUp ? 6f : 2f ) + StepHeight;\r\n\r\n            IsOnGround = IsOnGround || hasLanded;\r\n            GroundNormal = groundTrace.Normal;\r\n            GroundSurface = groundTrace.Surface;\r\n            GroundObject = groundTrace.GameObject;\r\n            IsSlipping = IsOnGround && !standable;\r\n\r\n            if ( GroundStickEnabled && !IsSlipping && IsOnGround )\r\n            {\r\n                var targetFeetPos = groundTrace.EndPosition - _offset + _upAxis * ( SkinWidth + shortCylinderOffset / 2f );\r\n                var delta = WorldPosition - targetFeetPos;\r\n\r\n                if ( delta.z >= -0.1f && !delta.IsNearlyZero( 0.001f ) )\r\n                {\r\n                    Body.WorldPosition = targetFeetPos;\r\n                    Body.Sleeping = false;\r\n\r\n                    if ( Body.Velocity.z > 0f )\r\n                        Body.Velocity = Body.Velocity.WithZ( 0 );\r\n                }\r\n            }\r\n\r\n            return;\r\n        }\r\n\r\n        IsOnGround = false;\r\n        GroundNormal = Vector3.Zero;\r\n        GroundSurface = null;\r\n        GroundObject = null;\r\n        IsSlipping = false;\r\n    }\r\n\r\n    protected void CreateBody( bool recreate = false )\r\n    {\r\n        if ( recreate )\r\n        {\r\n            if ( Collider.IsValid() ) Collider.Destroy();\r\n            if ( Body.IsValid() ) Body.Destroy();\r\n            if ( BodyObject.IsValid() ) BodyObject.Destroy();\r\n        }\r\n\r\n        CreateCollider( recreate );\r\n        CreateRigidbody( recreate );\r\n    }\r\n\r\n    protected void DestroyBody()\r\n    {\r\n        if ( Collider.IsValid() ) Collider.Destroy();\r\n        if ( Body.IsValid() ) Body.Destroy();\r\n        if ( BodyObject.IsValid() ) BodyObject.Destroy();\r\n    }\r\n\r\n    protected void CreateCollider( bool recreate = false )\r\n    {\r\n        if ( recreate && Collider.IsValid() )\r\n            Collider.Destroy();\r\n\r\n        if ( PhysicallySimulated )\r\n        {\r\n            if ( !Collider.IsValid() || Collider is not CapsuleCollider )\r\n            {\r\n                if ( Collider.IsValid() )\r\n                    Collider.Destroy();\r\n\r\n                Collider = GameObject.GetOrAddComponent<CapsuleCollider>();\r\n            }\r\n        }\r\n        else\r\n        {\r\n            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )\r\n            {\r\n                if ( !Collider.IsValid() || Collider is not BoxCollider )\r\n                {\r\n                    if ( Collider.IsValid() )\r\n                        Collider.Destroy();\r\n\r\n                    Collider = GameObject.GetOrAddComponent<BoxCollider>();\r\n                }\r\n            }\r\n            else if ( TraceShape == TraceType.Cylinder )\r\n            {\r\n                if ( !Collider.IsValid() || Collider is not HullCollider )\r\n                {\r\n                    if ( Collider.IsValid() )\r\n                        Collider.Destroy();\r\n\r\n                    Collider = GameObject.GetOrAddComponent<HullCollider>();\r\n                }\r\n            }\r\n            else if ( TraceShape == TraceType.Sphere )\r\n            {\r\n                if ( !Collider.IsValid() || Collider is not SphereCollider )\r\n                {\r\n                    if ( Collider.IsValid() )\r\n                        Collider.Destroy();\r\n\r\n                    Collider = GameObject.GetOrAddComponent<SphereCollider>();\r\n                }\r\n            }\r\n        }\r\n\r\n        if ( HidePhysicalComponents )\r\n            Collider.Flags |= ComponentFlags.Hidden;\r\n        else\r\n            Collider.Flags &= ~ComponentFlags.Hidden;\r\n\r\n        UpdateCollider();\r\n    }\r\n\r\n    protected void UpdateCollider()\r\n    {\r\n        if ( !Collider.IsValid() ) return;\r\n\r\n        if ( Collider is CapsuleCollider capsuleCollider )\r\n        {\r\n            var radius = ( TraceWidth - SkinWidth ) / 2f;\r\n            var height = TraceHeight - SkinWidth;\r\n            capsuleCollider.Radius = radius;\r\n            capsuleCollider.Start = Vector3.Up * radius;\r\n            capsuleCollider.End = Vector3.Up * ( height - radius );\r\n        }\r\n        else if ( Collider is BoxCollider boxCollider )\r\n        {\r\n            var bounds = BuildBounds();\r\n            boxCollider.Scale = bounds.Size - SkinWidth;\r\n            boxCollider.Center = Vector3.Up * ( TraceHeight / 2f );\r\n        }\r\n        else if ( Collider is HullCollider hullCollider )\r\n        {\r\n            hullCollider.Type = HullCollider.PrimitiveType.Cylinder;\r\n            hullCollider.Height = TraceHeight - SkinWidth;\r\n            hullCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;\r\n            hullCollider.Center = Vector3.Up * ( TraceHeight / 2f );\r\n        }\r\n        else if ( Collider is SphereCollider sphereCollider )\r\n        {\r\n            sphereCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;\r\n            sphereCollider.Center = Vector3.Up * sphereCollider.Radius;\r\n        }\r\n    }\r\n\r\n    protected void CreateRigidbody( bool recreate = false )\r\n    {\r\n        if ( recreate && Body.IsValid() )\r\n            Body.Destroy();\r\n\r\n        if ( !Body.IsValid() && GameObject.Components.TryGet<Rigidbody>( out var existingBody ) )\r\n            Body = existingBody;\r\n        if ( !Body.IsValid() )\r\n            Body = GameObject.AddComponent<Rigidbody>();\r\n\r\n        Body.Locking = new PhysicsLock() { Pitch = true, Roll = true, Yaw = true };\r\n        Body.Gravity = false;\r\n        Body.MassOverride = BodyMassOverride;\r\n        Body.RigidbodyFlags |= RigidbodyFlags.DisableCollisionSounds;\r\n        Body.EnhancedCcd = EnableCCD;\r\n\r\n        if ( HidePhysicalComponents )\r\n            Body.Flags |= ComponentFlags.Hidden;\r\n        else\r\n            Body.Flags &= ~ComponentFlags.Hidden;\r\n    }\r\n\r\n    public void OnCollisionStart( Collision collision ) { }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "Examples/ShrimpleRoller.cs",
            "FileName": "ShrimpleRoller.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\n[Hide]\r\npublic sealed class ShrimpleRoller : Component\r\n{\r\n    [RequireComponent]\r\n    public ShrimpleCharacterController Controller { get; set; }\r\n\r\n    public ModelRenderer Renderer { get; set; }\r\n    public GameObject Camera { get; set; }\r\n\r\n    [Property]\r\n    [Range(500f, 2000f)]\r\n    public float WalkSpeed { get; set; } = 1000f;\r\n\r\n    [Property]\r\n    [Range(1000f, 5000f)]\r\n    public float RunSpeed { get; set; } = 3000f;\r\n\r\n    [Property]\r\n    [Range(200f, 500f)]\r\n    public float JumpStrength { get; set; } = 350f;\r\n\r\n    public Angles EyeAngles { get; set; }\r\n\r\n    protected override void OnStart()\r\n    {\r\n        base.OnStart();\r\n\r\n        Renderer = Components.Get<ModelRenderer>(FindMode.EnabledInSelfAndDescendants);\r\n\r\n        Camera = new GameObject(true, \"Camera\");\r\n        Camera.SetParent(GameObject);\r\n        var cameraComponent = Camera.Components.Create<CameraComponent>();\r\n        cameraComponent.ZFar = 32768f;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw(EyeAngles.yaw);\r\n        var isDucking = Input.Down(\"Duck\");\r\n        var isRunning = Input.Down(\"Run\");\r\n        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;\r\n\r\n        Controller.WishVelocity = wishDirection * wishSpeed;\r\n        Controller.Move();\r\n\r\n        if (Input.Pressed(\"Jump\") && Controller.IsOnGround)\r\n            Controller.Punch(Vector3.Up * JumpStrength);\r\n    }\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n        base.OnUpdate();\r\n\r\n        EyeAngles += Input.AnalogLook;\r\n        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));\r\n\r\n        float pitchRotation = Controller.Velocity.x * Time.Delta;\r\n        float rollRotation = -Controller.Velocity.y * Time.Delta;\r\n        var ballPitch = Rotation.FromPitch(pitchRotation);\r\n        var ballRoll = Rotation.FromRoll(rollRotation);\r\n        Renderer.WorldRotation = ballPitch * ballRoll * Renderer.WorldRotation;\r\n\r\n        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 260f;\r\n        Camera.WorldRotation = EyeAngles.ToRotation();\r\n        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "Examples/ShrimpleWalker.cs",
            "FileName": "ShrimpleWalker.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\n[Hide]\r\npublic sealed class ShrimpleWalker : Component\r\n{\r\n    [RequireComponent]\r\n    public ShrimpleCharacterController Controller { get; set; }\r\n    [Property]\r\n    public SkinnedModelRenderer Renderer { get; set; }\r\n    public GameObject Camera { get; set; }\r\n\r\n    [Property]\r\n    [Range( 50f, 200f )]\r\n    public float WalkSpeed { get; set; } = 100f;\r\n\r\n    [Property]\r\n    [Range( 100f, 500f )]\r\n    public float RunSpeed { get; set; } = 300f;\r\n\r\n    [Property]\r\n    [Range( 25f, 100f )]\r\n    public float DuckSpeed { get; set; } = 50f;\r\n\r\n    [Property]\r\n    [Range( 200f, 500f )]\r\n    public float JumpStrength { get; set; } = 350f;\r\n\r\n    public Angles EyeAngles { get; set; }\r\n\r\n    protected override void OnStart()\r\n    {\r\n        base.OnStart();\r\n\r\n        Renderer = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );\r\n        Camera = new GameObject( true, \"Camera\" );\r\n        Camera.SetParent( GameObject );\r\n        var cameraComponent = Camera.Components.Create<CameraComponent>();\r\n        cameraComponent.ZFar = 32768f;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw( EyeAngles.yaw ) * GameObject.WorldRotation;\r\n        var isDucking = Input.Down( \"Duck\" );\r\n        var isRunning = Input.Down( \"Run\" );\r\n        var wishSpeed = isDucking ? DuckSpeed :\r\n            isRunning ? RunSpeed : WalkSpeed;\r\n\r\n        Controller.WishVelocity = wishDirection * wishSpeed;\r\n\r\n        if ( Input.Pressed( \"Jump\" ) && Controller.IsOnGround )\r\n        {\r\n            Controller.Punch( -Controller.AppliedGravity.Normal * JumpStrength );\r\n            Renderer?.Set( \"b_jump\", true );\r\n        }\r\n\r\n        if ( !Renderer.IsValid() ) return;\r\n\r\n        var oldX = Renderer.GetFloat( \"move_x\" );\r\n        var oldY = Renderer.GetFloat( \"move_y\" );\r\n\r\n        var velocity = Controller.Velocity;\r\n        var runScale = 1f / Renderer.Transform.World.UniformScale;\r\n        var moveX = Vector3.Dot( velocity, Renderer.WorldRotation.Forward ) * runScale;\r\n        var moveY = Vector3.Dot( velocity, Renderer.WorldRotation.Right ) * runScale;\r\n\r\n        var newX = MathX.Lerp( oldX, moveX, Time.Delta * 10f );\r\n        var newY = MathX.Lerp( oldY, moveY, Time.Delta * 10f );\r\n\r\n        Renderer.Set( \"move_x\", newX );\r\n        Renderer.Set( \"move_y\", newY );\r\n\r\n        Renderer.Set( \"b_grounded\", Controller.IsOnGround );\r\n        Renderer.Set( \"duck\", isDucking ? 1f : 0f );\r\n    }\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n        base.OnUpdate();\r\n\r\n        EyeAngles += Input.AnalogLook;\r\n        EyeAngles = EyeAngles.WithPitch( MathX.Clamp( EyeAngles.pitch, -10f, 40f ) );\r\n        Renderer.LocalRotation = Rotation.FromYaw( EyeAngles.yaw );\r\n\r\n        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 280f;\r\n        Camera.LocalRotation = EyeAngles.ToRotation();\r\n        Camera.LocalPosition = cameraOffset * Camera.LocalRotation;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "global using Sandbox;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\nnamespace ShrimpleCharacterController;\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/PlatformTest.cs",
            "FileName": "PlatformTest.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\n[Hide]\r\npublic sealed class PlatformTest : Component\r\n{\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        WorldPosition += Vector3.Right * (float)Math.Sin(Time.Now * 2f) * 400f * Time.Delta;\r\n        //WorldPosition += Vector3.Up * (float)Math.Sin(Time.Now * 2f) * 200f * Time.Delta;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/Util/Extensions/Vector3Extensions.cs",
            "FileName": "Vector3Extensions.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\npublic static class Vector3Extensions\r\n{\r\n    /// <summary>\r\n    /// Move a vector3 towards a goal by a fixed distance\r\n    /// </summary>\r\n    /// <param name=\"value\"></param>\r\n    /// <param name=\"target\"></param>\r\n    /// <param name=\"travelSpeed\"></param>\r\n    /// <returns></returns>\r\n    public static Vector3 MoveTowards(this Vector3 value, Vector3 target, float travelSpeed)\r\n    {\r\n        var difference = target - value;\r\n        var distance = difference.Length;\r\n        var normal = difference.Normal;\r\n\r\n        if (distance <= travelSpeed || distance == 0f)\r\n        {\r\n            return target;\r\n        }\r\n\r\n        return value + normal * travelSpeed;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Project a vector along a plane (normal) and scale it back to its original length\r\n    /// </summary>\r\n    /// <param name=\"value\"></param>\r\n    /// <param name=\"normal\"></param>\r\n    /// <returns></returns>\r\n    public static Vector3 ProjectAndScale(this Vector3 value, Vector3 normal)\r\n    {\r\n        var length = value.Length;\r\n        value = Vector3.VectorPlaneProject(value, normal).Normal;\r\n        value *= length;\r\n\r\n        return value;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "Examples/ShrimpleFlyer.cs",
            "FileName": "ShrimpleFlyer.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "using Sandbox.Citizen;\r\n\r\nnamespace ShrimpleCharacterController;\r\n\r\n[Hide]\r\npublic sealed class ShrimpleFlyer : Component\r\n{\r\n    [RequireComponent]\r\n    public ShrimpleCharacterController Controller { get; set; }\r\n    public GameObject Camera { get; set; }\r\n\r\n    [Property]\r\n    [Range(400f, 1600f)]\r\n    public float WalkSpeed { get; set; } = 800f;\r\n\r\n    [Property]\r\n    [Range(800f, 4000f)]\r\n    public float RunSpeed { get; set; } = 2400f;\r\n\r\n    public Angles EyeAngles { get; set; }\r\n\r\n    protected override void OnStart()\r\n    {\r\n        base.OnStart();\r\n\r\n        Camera = new GameObject(true, \"Camera\");\r\n        Camera.SetParent(GameObject);\r\n        var cameraComponent = Camera.Components.Create<CameraComponent>();\r\n        cameraComponent.ZFar = 32768f;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        var isDucking = Input.Down(\"Duck\");\r\n        var isRunning = Input.Down(\"Run\");\r\n        var ascending = Input.Down(\"Jump\") ? 1f : 0f;\r\n        var descending = Input.Down(\"Duck\") ? -1f : 0f;\r\n        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;\r\n        var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();\r\n\r\n        Controller.WishVelocity = wishDirection * wishSpeed;\r\n        Controller.Move();\r\n    }\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n        base.OnUpdate();\r\n\r\n        EyeAngles += Input.AnalogLook;\r\n        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -40f, 40f));\r\n\r\n        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 760f;\r\n        Camera.WorldRotation = EyeAngles.ToRotation();\r\n        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "PlatformTest.cs",
            "FileName": "PlatformTest.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "using Sandbox;\r\n\r\n[Hide]\r\npublic sealed class PlatformTest : Component\r\n{\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        WorldPosition += Vector3.Right * (float)Math.Sin(Time.Now * 2f) * 400f * Time.Delta;\r\n        //WorldPosition += Vector3.Up * (float)Math.Sin(Time.Now * 2f) * 200f * Time.Delta;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "ShrimpleCollisionResult.cs",
            "FileName": "ShrimpleCollisionResult.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\npublic struct ShrimpleCollisionResult\r\n{\r\n    public Vector3 HitPosition;\r\n    public Vector3 HitNormal;\r\n    public Vector3 HitVelocityBefore;\r\n    public Vector3 HitVelocityAfter;\r\n    public float Angle;\r\n    public GameObject HitObject;\r\n    public Surface HitSurface;\r\n\r\n    public ShrimpleCollisionResult() { }\r\n\r\n    public ShrimpleCollisionResult(Vector3 hitPosition, Vector3 hitNormal, Vector3 hitVelocityBefore, Vector3 hitVelocityAfter, float angle, GameObject hitObject, Surface hitSurface)\r\n    {\r\n        HitPosition = hitPosition;\r\n        HitNormal = hitNormal;\r\n        HitVelocityBefore = hitVelocityBefore;\r\n        HitVelocityAfter = hitVelocityAfter;\r\n        Angle = angle;\r\n        HitObject = hitObject;\r\n        HitSurface = hitSurface;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitPosition(Vector3 hitPosition)\r\n    {\r\n        var result = this;\r\n        result.HitPosition = hitPosition;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitNormal(Vector3 hitNormal)\r\n    {\r\n        var result = this;\r\n        result.HitNormal = hitNormal;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitVelocityBefore(Vector3 hitVelocityBefore)\r\n    {\r\n        var result = this;\r\n        result.HitVelocityBefore = hitVelocityBefore;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitVelocityAfter(Vector3 hitVelocityAfter)\r\n    {\r\n        var result = this;\r\n        result.HitVelocityAfter = hitVelocityAfter;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithAngle(float angle)\r\n    {\r\n        var result = this;\r\n        result.Angle = angle;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitObject(GameObject hitObject)\r\n    {\r\n        var result = this;\r\n        result.HitObject = hitObject;\r\n        return result;\r\n    }\r\n\r\n    public ShrimpleCollisionResult WithHitSurface(Surface hitSurface)\r\n    {\r\n        var result = this;\r\n        result.HitSurface = hitSurface;\r\n        return result;\r\n    }\r\n}"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/Examples/ShrimpleRoller.cs",
            "FileName": "ShrimpleRoller.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\n[Hide]\r\npublic sealed class ShrimpleRoller : Component\r\n{\r\n    [RequireComponent]\r\n    public ShrimpleCharacterController Controller { get; set; }\r\n\r\n    public ModelRenderer Renderer { get; set; }\r\n    public GameObject Camera { get; set; }\r\n\r\n    [Property]\r\n    [Range(500f, 2000f)]\r\n    public float WalkSpeed { get; set; } = 1000f;\r\n\r\n    [Property]\r\n    [Range(1000f, 5000f)]\r\n    public float RunSpeed { get; set; } = 3000f;\r\n\r\n    [Property]\r\n    [Range(200f, 500f)]\r\n    public float JumpStrength { get; set; } = 350f;\r\n\r\n    public Angles EyeAngles { get; set; }\r\n\r\n    protected override void OnStart()\r\n    {\r\n        base.OnStart();\r\n\r\n        Renderer = Components.Get<ModelRenderer>(FindMode.EnabledInSelfAndDescendants);\r\n\r\n        Camera = new GameObject(true, \"Camera\");\r\n        Camera.SetParent(GameObject);\r\n        var cameraComponent = Camera.Components.Create<CameraComponent>();\r\n        cameraComponent.ZFar = 32768f;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw(EyeAngles.yaw);\r\n        var isDucking = Input.Down(\"Duck\");\r\n        var isRunning = Input.Down(\"Run\");\r\n        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;\r\n\r\n        Controller.WishVelocity = wishDirection * wishSpeed;\r\n        Controller.Move();\r\n\r\n        if (Input.Pressed(\"Jump\") && Controller.IsOnGround)\r\n            Controller.Punch(Vector3.Up * JumpStrength);\r\n    }\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n        base.OnUpdate();\r\n\r\n        EyeAngles += Input.AnalogLook;\r\n        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));\r\n\r\n        float pitchRotation = Controller.Velocity.x * Time.Delta;\r\n        float rollRotation = -Controller.Velocity.y * Time.Delta;\r\n        var ballPitch = Rotation.FromPitch(pitchRotation);\r\n        var ballRoll = Rotation.FromRoll(rollRotation);\r\n        Renderer.WorldRotation = ballPitch * ballRoll * Renderer.WorldRotation;\r\n\r\n        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 260f;\r\n        Camera.WorldRotation = EyeAngles.ToRotation();\r\n        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "Util/Extensions/Vector3Extensions.cs",
            "FileName": "Vector3Extensions.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\npublic static class Vector3Extensions\r\n{\r\n    /// <summary>\r\n    /// Move a vector3 towards a goal by a fixed distance\r\n    /// </summary>\r\n    /// <param name=\"value\"></param>\r\n    /// <param name=\"target\"></param>\r\n    /// <param name=\"travelSpeed\"></param>\r\n    /// <returns></returns>\r\n    public static Vector3 MoveTowards(this Vector3 value, Vector3 target, float travelSpeed)\r\n    {\r\n        var difference = target - value;\r\n        var distance = difference.Length;\r\n        var normal = difference.Normal;\r\n\r\n        if (distance <= travelSpeed || distance == 0f)\r\n        {\r\n            return target;\r\n        }\r\n\r\n        return value + normal * travelSpeed;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Project a vector along a plane (normal) and scale it back to its original length\r\n    /// </summary>\r\n    /// <param name=\"value\"></param>\r\n    /// <param name=\"normal\"></param>\r\n    /// <returns></returns>\r\n    public static Vector3 ProjectAndScale(this Vector3 value, Vector3 normal)\r\n    {\r\n        var length = value.Length;\r\n        value = Vector3.VectorPlaneProject(value, normal).Normal;\r\n        value *= length;\r\n\r\n        return value;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "ShrimpleCharacterController.Physical.cs",
            "FileName": "ShrimpleCharacterController.Physical.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\npublic partial class ShrimpleCharacterController\r\n{\r\n    bool _didStep;\r\n    Vector3 _stepPosition;\r\n\r\n    // Set by Move() when ManuallyUpdate is enabled in physical mode, consumed by the next physics step\r\n    bool _physicalMovePending;\r\n    float _physicalMoveDelta;\r\n\r\n    // The delta this physics step should simulate (the manual Move() delta, otherwise the fixed step)\r\n    float ActiveDelta => ( ManuallyUpdate && _physicalMovePending ) ? _physicalMoveDelta : ActiveAutoTimeDelta;\r\n\r\n    // The solver always integrates over Time.Delta, so we scale velocity to cover ActiveDelta worth of distance\r\n    float MoveScale => Time.Delta > 0f ? ActiveDelta / Time.Delta : 1f;\r\n\r\n    void IScenePhysicsEvents.PrePhysicsStep()\r\n    {\r\n        if ( !PhysicallySimulated ) return;\r\n        if ( !Body.IsValid() ) return;\r\n        if ( ManuallyUpdate && !_physicalMovePending ) return; // Wait for a manual Move() call to drive us\r\n\r\n        _didStep = false;\r\n\r\n        UpdateBodyPhysics();\r\n        UpdateMassCenter();\r\n        UpdateMovement();\r\n    }\r\n\r\n    void IScenePhysicsEvents.PostPhysicsStep()\r\n    {\r\n        if ( !Body.IsValid() )\r\n        {\r\n            WorldPosition += GroundVelocity * Time.Delta;\r\n            return;\r\n        }\r\n\r\n        if ( !PhysicallySimulated ) return;\r\n        if ( ManuallyUpdate && !_physicalMovePending ) return;\r\n\r\n        if ( _didStep )\r\n            Body.WorldPosition = _stepPosition;\r\n\r\n        CategorizePhysicalGround();\r\n        ApplyGroundVelocity();\r\n        Velocity = Body.Velocity / MoveScale; // De-scale back to logical units/second\r\n\r\n        _physicalMovePending = false;\r\n    }\r\n\r\n    private void UpdateMassCenter()\r\n    {\r\n        if ( !Body.IsValid() ) return;\r\n\r\n        var wishSpeed = WishVelocity.WithZ( 0 ).Length;\r\n        var halfHeight = AppliedHeight * 0.5f;\r\n        float massCenter = IsOnGround ? wishSpeed.Clamp( 0, halfHeight ) : halfHeight;\r\n\r\n        Body.MassCenterOverride = new Vector3( 0, 0, massCenter );\r\n        Body.OverrideMassCenter = true;\r\n    }\r\n\r\n    private void UpdateBodyPhysics()\r\n    {\r\n        if ( !Collider.IsValid() || !Body.IsValid() ) return;\r\n\r\n        var onDynamic = false;\r\n        if ( IsOnGround && GroundObject.IsValid() )\r\n        {\r\n            var groundBody = GroundObject.GetComponent<Rigidbody>();\r\n            onDynamic = groundBody != null && groundBody.PhysicsBody.BodyType == PhysicsBodyType.Dynamic;\r\n        }\r\n\r\n        Body.Gravity = false;\r\n\r\n        var velocityHorizontal = Body.Velocity.WithZ( 0 ).Length;\r\n        bool wantsBrakes = IsOnGround && WishVelocity.Length < 1f && velocityHorizontal < 10f;\r\n        Body.LinearDamping = wantsBrakes ? 10f : 0.1f;\r\n        Body.AngularDamping = 1f;\r\n\r\n        float friction = 0f;\r\n        if ( IsOnGround && WishVelocity.Length < 0.1f )\r\n            friction = onDynamic ? 0.5f : 1f;\r\n        Collider.Friction = friction;\r\n    }\r\n\r\n    private void AddWishVelocity()\r\n    {\r\n        var z = Body.Velocity.z;\r\n        var velocity = CalculateGoalVelocity( ActiveDelta );\r\n\r\n        if ( IsOnGround && GroundStickEnabled && !GroundNormal.IsNearlyZero( 0.01f ) )\r\n        {\r\n            if ( MathF.Abs( GroundNormal.z ) > 0.01f )\r\n                velocity = velocity.WithZ( -( GroundNormal.x * velocity.x + GroundNormal.y * velocity.y ) / GroundNormal.z );\r\n\r\n            var isGoingUphill = Vector3.Dot( velocity, AppliedGravity ) < 0f;\r\n            var slopeMultiplier = isGoingUphill ? GetSlopeVelocityMultiplier( GroundAngle ) : 1f;\r\n            velocity *= slopeMultiplier;\r\n        }\r\n\r\n        if ( IsOnGround && !GroundStickEnabled )\r\n            velocity.z = z;\r\n\r\n        if ( GravityEnabled && ( !IsOnGround || IsSlipping || !GroundStickEnabled ) )\r\n            velocity += AppliedGravity * ActiveDelta;\r\n\r\n        Body.Velocity = velocity * MoveScale; // Scale so the solver covers ActiveDelta worth of distance over Time.Delta\r\n    }\r\n\r\n    private void UpdateMovement()\r\n    {\r\n        if ( IsOnGround && GroundStickEnabled && !IsSlipping )\r\n            StickToGround();\r\n\r\n        AddWishVelocity();\r\n\r\n        var wishHorizontal = WishVelocity.WithZ( 0 );\r\n        var bodyHorizontal = Body.Velocity.WithZ( 0 );\r\n        var moveDir = wishHorizontal.IsNearlyZero( 0.1f ) ? bodyHorizontal : wishHorizontal;\r\n        if ( moveDir.IsNearlyZero( 0.1f ) ) return;\r\n\r\n        var speed = MathF.Max( bodyHorizontal.Length * Time.Delta, StepDepth );\r\n        var vel = moveDir.Normal * speed;\r\n        var forwardTrace = BuildTrace( _shrunkenBounds, WorldPosition + _offset - vel.Normal * SkinWidth, WorldPosition + _offset + vel );\r\n\r\n        if ( !forwardTrace.Hit || forwardTrace.StartedSolid ) return;\r\n\r\n        var forwardAngle = Vector3.GetAngle( -AppliedGravity.Normal, forwardTrace.Normal );\r\n        if ( IsAngleStandable( forwardAngle ) && Vector3.Dot( Body.Velocity, forwardTrace.Normal ) < 0f )\r\n            Body.Velocity = Vector3.VectorPlaneProject( Body.Velocity, forwardTrace.Normal );\r\n\r\n        if ( StepsEnabled && IsOnGround )\r\n            TryStepUp( forwardTrace, vel );\r\n    }\r\n\r\n    private void TryStepUp( SceneTraceResult forwardTrace, Vector3 vel )\r\n    {\r\n        if ( forwardTrace.StartedSolid || !forwardTrace.Hit ) return;\r\n\r\n        var angle = Vector3.GetAngle( -GravityNormal, forwardTrace.Normal );\r\n        if ( angle < 90f - StepTolerance || angle > 90f + StepTolerance ) return;\r\n\r\n        var stepHorizontal = Vector3.VectorPlaneProject( vel.Normal, GravityNormal ).Normal * StepDepth;\r\n        var stepVertical = -GravityNormal * ( StepHeight + SkinWidth );\r\n        var stepTrace = BuildTrace( _shrunkenBounds, forwardTrace.EndPosition + stepHorizontal + stepVertical, forwardTrace.EndPosition + stepHorizontal );\r\n\r\n        if ( stepTrace.StartedSolid || !stepTrace.Hit ) return;\r\n        if ( !IsAngleStandable( Vector3.GetAngle( stepTrace.Normal, -GravityNormal ) ) ) return;\r\n\r\n        var newFeetPos = stepTrace.EndPosition - _offset + _upAxis * SkinWidth;\r\n        if ( newFeetPos.z - WorldPosition.z < 0.5f ) return;\r\n\r\n        _didStep = true;\r\n        _stepPosition = newFeetPos;\r\n        Body.WorldPosition = _stepPosition;\r\n        Body.Velocity = Body.Velocity.WithZ( 0 );\r\n    }\r\n\r\n    private void ApplyGroundVelocity()\r\n    {\r\n        if ( !IsOnGround || !GroundStickEnabled || IsSlipping ) return;\r\n\r\n        var groundVelocity = PlatformVelocity * Time.Delta + SurfaceVelocity * ActiveDelta;\r\n        if ( groundVelocity.IsNearZeroLength ) return;\r\n\r\n        Body.WorldPosition += groundVelocity;\r\n    }\r\n\r\n    private void StickToGround()\r\n    {\r\n        var currentPosition = WorldPosition;\r\n        var from = currentPosition + _offset - GravityNormal * StepHeight;\r\n        var to = currentPosition + _offset + GravityNormal * ( GroundStickDistance + SkinWidth );\r\n\r\n        var trace = BuildTrace( _shrunkenBounds, from, to );\r\n\r\n        if ( trace.StartedSolid ) return;\r\n\r\n        if ( trace.Hit )\r\n        {\r\n            var surfaceAngle = Vector3.GetAngle( -GravityNormal, trace.Normal );\r\n            if ( !IsAngleStandable( surfaceAngle ) ) return;\r\n\r\n            var targetFeetPos = trace.EndPosition - _offset + _upAxis * SkinWidth;\r\n\r\n            Body.WorldPosition = targetFeetPos;\r\n            Body.Sleeping = false;\r\n\r\n            if ( Body.Velocity.z > 0f )\r\n                Body.Velocity = Body.Velocity.WithZ( 0 );\r\n\r\n            IsOnGround = true;\r\n            GroundNormal = trace.Normal;\r\n            GroundSurface = trace.Surface;\r\n            GroundObject = trace.GameObject;\r\n            IsSlipping = false;\r\n        }\r\n    }\r\n\r\n    private void CategorizePhysicalGround()\r\n    {\r\n        var position = WorldPosition + _offset;\r\n        var shortCylinderOffset = AppliedWidth / 2f;\r\n        var shortCylinderBounds = new BBox( _shrunkenBounds.Mins, new Vector3( _shrunkenBounds.Maxs.x, _shrunkenBounds.Maxs.y, _shrunkenBounds.Maxs.z - shortCylinderOffset ) );\r\n        var groundTrace = BuildTrace( shortCylinderBounds, position + GravityNormal * shortCylinderOffset / 2f, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );\r\n\r\n        if ( groundTrace.StartedSolid )\r\n        {\r\n            var stuckCheck = BuildTrace( _shrunkenBounds.Grow( -SkinWidth ), position, position, useCapsule: true );\r\n\r\n            if ( stuckCheck.StartedSolid )\r\n            {\r\n                IsStuck = true;\r\n\r\n                if ( UnstuckEnabled && TryUnstuck( position, out var unstuckResult ) )\r\n                {\r\n                    IsStuck = false;\r\n                    Body.WorldPosition = unstuckResult - _offset;\r\n                    Body.Velocity = Vector3.Zero;\r\n                }\r\n\r\n                var fallbackTrace = BuildTrace( _shrunkenBounds, position + -GravityNormal * 4f, position + GravityNormal * 2f );\r\n\r\n                if ( fallbackTrace.Hit && !fallbackTrace.StartedSolid && Vector3.Dot( fallbackTrace.Normal, -GravityNormal ) > 0f )\r\n                {\r\n                    var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, fallbackTrace.Normal ) );\r\n                    IsOnGround = true;\r\n                    GroundNormal = fallbackTrace.Normal;\r\n                    GroundSurface = fallbackTrace.Surface;\r\n                    GroundObject = fallbackTrace.GameObject;\r\n                    IsSlipping = !standable;\r\n                }\r\n                return;\r\n            }\r\n\r\n            groundTrace = BuildTrace( _shrunkenBounds, position, position + GravityNormal * ( GroundStickDistance + SkinWidth ) );\r\n        }\r\n\r\n        IsStuck = false;\r\n\r\n        if ( groundTrace.Hit )\r\n        {\r\n            var standable = IsAngleStandable( Vector3.GetAngle( Vector3.Up, groundTrace.Normal ) );\r\n            var landingAngle = Vector3.Dot( Velocity.Normal, groundTrace.Normal );\r\n            var verticalAngle = Vector3.Dot( Velocity.Normal, GravityNormal );\r\n            var goingUp = verticalAngle < 0.1f;\r\n            var hasLanded = !IsOnGround && landingAngle <= ( MaxGroundAngle.Max / 180f ) && groundTrace.Distance <= SkinWidth * ( goingUp ? 6f : 2f ) + StepHeight;\r\n\r\n            IsOnGround = IsOnGround || hasLanded;\r\n            GroundNormal = groundTrace.Normal;\r\n            GroundSurface = groundTrace.Surface;\r\n            GroundObject = groundTrace.GameObject;\r\n            IsSlipping = IsOnGround && !standable;\r\n\r\n            if ( GroundStickEnabled && !IsSlipping && IsOnGround )\r\n            {\r\n                var targetFeetPos = groundTrace.EndPosition - _offset + _upAxis * ( SkinWidth + shortCylinderOffset / 2f );\r\n                var delta = WorldPosition - targetFeetPos;\r\n\r\n                if ( delta.z >= -0.1f && !delta.IsNearlyZero( 0.001f ) )\r\n                {\r\n                    Body.WorldPosition = targetFeetPos;\r\n                    Body.Sleeping = false;\r\n\r\n                    if ( Body.Velocity.z > 0f )\r\n                        Body.Velocity = Body.Velocity.WithZ( 0 );\r\n                }\r\n            }\r\n\r\n            return;\r\n        }\r\n\r\n        IsOnGround = false;\r\n        GroundNormal = Vector3.Zero;\r\n        GroundSurface = null;\r\n        GroundObject = null;\r\n        IsSlipping = false;\r\n    }\r\n\r\n    protected void CreateBody( bool recreate = false )\r\n    {\r\n        if ( recreate )\r\n        {\r\n            if ( Collider.IsValid() ) Collider.Destroy();\r\n            if ( Body.IsValid() ) Body.Destroy();\r\n            if ( BodyObject.IsValid() ) BodyObject.Destroy();\r\n        }\r\n\r\n        CreateCollider( recreate );\r\n        CreateRigidbody( recreate );\r\n    }\r\n\r\n    protected void DestroyBody()\r\n    {\r\n        if ( Collider.IsValid() ) Collider.Destroy();\r\n        if ( Body.IsValid() ) Body.Destroy();\r\n        if ( BodyObject.IsValid() ) BodyObject.Destroy();\r\n    }\r\n\r\n    protected void CreateCollider( bool recreate = false )\r\n    {\r\n        if ( recreate && Collider.IsValid() )\r\n            Collider.Destroy();\r\n\r\n        if ( PhysicallySimulated )\r\n        {\r\n            if ( !Collider.IsValid() || Collider is not CapsuleCollider )\r\n            {\r\n                if ( Collider.IsValid() )\r\n                    Collider.Destroy();\r\n\r\n                Collider = GameObject.GetOrAddComponent<CapsuleCollider>();\r\n            }\r\n        }\r\n        else\r\n        {\r\n            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )\r\n            {\r\n                if ( !Collider.IsValid() || Collider is not BoxCollider )\r\n                {\r\n                    if ( Collider.IsValid() )\r\n                        Collider.Destroy();\r\n\r\n                    Collider = GameObject.GetOrAddComponent<BoxCollider>();\r\n                }\r\n            }\r\n            else if ( TraceShape == TraceType.Cylinder )\r\n            {\r\n                if ( !Collider.IsValid() || Collider is not HullCollider )\r\n                {\r\n                    if ( Collider.IsValid() )\r\n                        Collider.Destroy();\r\n\r\n                    Collider = GameObject.GetOrAddComponent<HullCollider>();\r\n                }\r\n            }\r\n            else if ( TraceShape == TraceType.Sphere )\r\n            {\r\n                if ( !Collider.IsValid() || Collider is not SphereCollider )\r\n                {\r\n                    if ( Collider.IsValid() )\r\n                        Collider.Destroy();\r\n\r\n                    Collider = GameObject.GetOrAddComponent<SphereCollider>();\r\n                }\r\n            }\r\n        }\r\n\r\n        if ( HidePhysicalComponents )\r\n            Collider.Flags |= ComponentFlags.Hidden;\r\n        else\r\n            Collider.Flags &= ~ComponentFlags.Hidden;\r\n\r\n        UpdateCollider();\r\n    }\r\n\r\n    protected void UpdateCollider()\r\n    {\r\n        if ( !Collider.IsValid() ) return;\r\n\r\n        if ( Collider is CapsuleCollider capsuleCollider )\r\n        {\r\n            var radius = ( TraceWidth - SkinWidth ) / 2f;\r\n            var height = TraceHeight - SkinWidth;\r\n            capsuleCollider.Radius = radius;\r\n            capsuleCollider.Start = Vector3.Up * radius;\r\n            capsuleCollider.End = Vector3.Up * ( height - radius );\r\n        }\r\n        else if ( Collider is BoxCollider boxCollider )\r\n        {\r\n            var bounds = BuildBounds();\r\n            boxCollider.Scale = bounds.Size - SkinWidth;\r\n            boxCollider.Center = Vector3.Up * ( TraceHeight / 2f );\r\n        }\r\n        else if ( Collider is HullCollider hullCollider )\r\n        {\r\n            hullCollider.Type = HullCollider.PrimitiveType.Cylinder;\r\n            hullCollider.Height = TraceHeight - SkinWidth;\r\n            hullCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;\r\n            hullCollider.Center = Vector3.Up * ( TraceHeight / 2f );\r\n        }\r\n        else if ( Collider is SphereCollider sphereCollider )\r\n        {\r\n            sphereCollider.Radius = ( TraceWidth - SkinWidth ) / 2f;\r\n            sphereCollider.Center = Vector3.Up * sphereCollider.Radius;\r\n        }\r\n    }\r\n\r\n    protected void CreateRigidbody( bool recreate = false )\r\n    {\r\n        if ( recreate && Body.IsValid() )\r\n            Body.Destroy();\r\n\r\n        if ( !Body.IsValid() && GameObject.Components.TryGet<Rigidbody>( out var existingBody ) )\r\n            Body = existingBody;\r\n        if ( !Body.IsValid() )\r\n            Body = GameObject.AddComponent<Rigidbody>();\r\n\r\n        Body.Locking = new PhysicsLock() { Pitch = true, Roll = true, Yaw = true };\r\n        Body.Gravity = false;\r\n        Body.MassOverride = BodyMassOverride;\r\n        Body.RigidbodyFlags |= RigidbodyFlags.DisableCollisionSounds;\r\n        Body.EnhancedCcd = EnableCCD;\r\n\r\n        if ( HidePhysicalComponents )\r\n            Body.Flags |= ComponentFlags.Hidden;\r\n        else\r\n            Body.Flags &= ~ComponentFlags.Hidden;\r\n    }\r\n\r\n    public void OnCollisionStart( Collision collision ) { }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "Assembly.cs",
            "FileName": "Assembly.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "global using Sandbox;\r\nglobal using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\nnamespace ShrimpleCharacterController;\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/Examples/ShrimpleWalker.cs",
            "FileName": "ShrimpleWalker.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "namespace ShrimpleCharacterController;\r\n\r\n[Hide]\r\npublic sealed class ShrimpleWalker : Component\r\n{\r\n    [RequireComponent]\r\n    public ShrimpleCharacterController Controller { get; set; }\r\n    [Property]\r\n    public SkinnedModelRenderer Renderer { get; set; }\r\n    public GameObject Camera { get; set; }\r\n\r\n    [Property]\r\n    [Range( 50f, 200f )]\r\n    public float WalkSpeed { get; set; } = 100f;\r\n\r\n    [Property]\r\n    [Range( 100f, 500f )]\r\n    public float RunSpeed { get; set; } = 300f;\r\n\r\n    [Property]\r\n    [Range( 25f, 100f )]\r\n    public float DuckSpeed { get; set; } = 50f;\r\n\r\n    [Property]\r\n    [Range( 200f, 500f )]\r\n    public float JumpStrength { get; set; } = 350f;\r\n\r\n    public Angles EyeAngles { get; set; }\r\n\r\n    protected override void OnStart()\r\n    {\r\n        base.OnStart();\r\n\r\n        Renderer = Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants );\r\n        Camera = new GameObject( true, \"Camera\" );\r\n        Camera.SetParent( GameObject );\r\n        var cameraComponent = Camera.Components.Create<CameraComponent>();\r\n        cameraComponent.ZFar = 32768f;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw( EyeAngles.yaw ) * GameObject.WorldRotation;\r\n        var isDucking = Input.Down( \"Duck\" );\r\n        var isRunning = Input.Down( \"Run\" );\r\n        var wishSpeed = isDucking ? DuckSpeed :\r\n            isRunning ? RunSpeed : WalkSpeed;\r\n\r\n        Controller.WishVelocity = wishDirection * wishSpeed;\r\n\r\n        if ( Input.Pressed( \"Jump\" ) && Controller.IsOnGround )\r\n        {\r\n            Controller.Punch( -Controller.AppliedGravity.Normal * JumpStrength );\r\n            Renderer?.Set( \"b_jump\", true );\r\n        }\r\n\r\n        if ( !Renderer.IsValid() ) return;\r\n\r\n        var oldX = Renderer.GetFloat( \"move_x\" );\r\n        var oldY = Renderer.GetFloat( \"move_y\" );\r\n\r\n        var velocity = Controller.Velocity;\r\n        var runScale = 1f / Renderer.Transform.World.UniformScale;\r\n        var moveX = Vector3.Dot( velocity, Renderer.WorldRotation.Forward ) * runScale;\r\n        var moveY = Vector3.Dot( velocity, Renderer.WorldRotation.Right ) * runScale;\r\n\r\n        var newX = MathX.Lerp( oldX, moveX, Time.Delta * 10f );\r\n        var newY = MathX.Lerp( oldY, moveY, Time.Delta * 10f );\r\n\r\n        Renderer.Set( \"move_x\", newX );\r\n        Renderer.Set( \"move_y\", newY );\r\n\r\n        Renderer.Set( \"b_grounded\", Controller.IsOnGround );\r\n        Renderer.Set( \"duck\", isDucking ? 1f : 0f );\r\n    }\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n        base.OnUpdate();\r\n\r\n        EyeAngles += Input.AnalogLook;\r\n        EyeAngles = EyeAngles.WithPitch( MathX.Clamp( EyeAngles.pitch, -10f, 40f ) );\r\n        Renderer.LocalRotation = Rotation.FromYaw( EyeAngles.yaw );\r\n\r\n        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 280f;\r\n        Camera.LocalRotation = EyeAngles.ToRotation();\r\n        Camera.LocalPosition = cameraOffset * Camera.LocalRotation;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": "ShrimpleCharacterController.cs",
            "FileName": "ShrimpleCharacterController.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "using System.Text.Json.Nodes;\r\n\r\nnamespace ShrimpleCharacterController;\r\n\r\n[Icon( \"nordic_walking\" )]\r\npublic partial class ShrimpleCharacterController : Component, IScenePhysicsEvents, ISceneEvent<IScenePhysicsEvents>, Component.ICollisionListener\r\n{\r\n    /// <summary>\r\n    /// Manually update this by calling Move() or let it always be simulated.\r\n    /// In physical mode, Move() drives the next physics step instead of running the trace-based path.\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    public bool ManuallyUpdate { get; set; } = false;\r\n\r\n    [Property]\r\n    [FeatureEnabled( \"Physical\" )]\r\n    public bool PhysicallySimulated\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n\r\n            if ( value )\r\n                CreateBody();\r\n            else\r\n                DestroyBody();\r\n        }\r\n    }\r\n\r\n    private bool isPhysical( object _ ) => !PhysicallySimulated;\r\n\r\n    [Property]\r\n    [Hide]\r\n    public GameObject BodyObject { get; protected set; }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Header( \"Collider\" )]\r\n    [Validate( nameof( isPhysical ), \"Physical mode enabled - controller can interact with and be pushed by physics objects\", LogLevel.Info )]\r\n    public Surface ColliderSurface\r\n    {\r\n        get => Collider.IsValid() ? Collider.Surface : null;\r\n        set\r\n        {\r\n            if ( Collider.IsValid() )\r\n                Collider.Surface = value;\r\n        }\r\n    }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    public float? ColliderElasticity\r\n    {\r\n        get => Collider.IsValid() ? Collider.Elasticity : null;\r\n        set\r\n        {\r\n            if ( Collider.IsValid() )\r\n                Collider.Elasticity = value;\r\n        }\r\n    }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [ShowIf( \"HidePhysicalComponents\", false )]\r\n    public Collider Collider { get; protected set; }\r\n\r\n    /// <summary>\r\n    /// Kilos, tweak based on your surroundings, default boxes are like 600kg!\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Header( \"Rigid Body\" )]\r\n    public float BodyMassOverride\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n\r\n            if ( Body.IsValid() )\r\n                Body.MassOverride = value;\r\n        }\r\n    } = 100f;\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    public RigidbodyFlags BodyRigidbodyFlags\r\n    {\r\n        get => Body.IsValid() ? Body.RigidbodyFlags : 0;\r\n        set\r\n        {\r\n            if ( Body.IsValid() )\r\n                Body.RigidbodyFlags = value;\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Enable Continuous Collision Detection (CCD) for the physics body.\r\n    /// Useful for fast-moving characters to prevent tunneling through thin objects.\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Title( \"Enable CCD\" )]\r\n    public bool EnableCCD\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            if ( Body.IsValid() )\r\n                Body.EnhancedCcd = value;\r\n        }\r\n    }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [ShowIf( \"HidePhysicalComponents\", false )]\r\n    public Rigidbody Body { get; protected set; }\r\n\r\n    /// <summary>\r\n    /// Hide the <see cref=\"Body\"/> and <see cref=\"Collider\"/> components in the inspector\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Title( \"Hide Components\" )]\r\n    [Space]\r\n    [Validate( nameof( isPhysical ), \"Make sure to go over the other features to see any warning regarding physical simulation!\", LogLevel.Warn )]\r\n    public bool HidePhysicalComponents\r\n    {\r\n        get;\r\n        protected set\r\n        {\r\n            field = value;\r\n\r\n            if ( value )\r\n            {\r\n                if ( Body.IsValid() )\r\n                    Body.Flags |= ComponentFlags.Hidden;\r\n                if ( Collider.IsValid() )\r\n                    Collider.Flags |= ComponentFlags.Hidden;\r\n            }\r\n            else\r\n            {\r\n                if ( Body.IsValid() )\r\n                    Body.Flags &= ~ComponentFlags.Hidden;\r\n                if ( Collider.IsValid() )\r\n                    Collider.Flags &= ~ComponentFlags.Hidden;\r\n            }\r\n        }\r\n    } = true;\r\n\r\n    [Feature( \"Physical\" )]\r\n    [Button( \"Recreate Components\", \"sync\" )]\r\n    public void RefreshBody() => CreateBody( true );\r\n\r\n    /// <summary>\r\n    /// If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    public bool ScaleAgainstWalls { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Rotate the trace with the gameobject\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [Validate( nameof( isPhysical ), \"Physical mode uses a capsule collider to prevent snagging on terrain.\", LogLevel.Info )]\r\n    public bool RotateWithGameObject { get; set; } = true;\r\n\r\n    public enum TraceType\r\n    {\r\n        /// <summary>\r\n        /// This has no drawback, enjoy!\r\n        /// </summary>\r\n        [Icon( \"\ud83d\udce6\" )]\r\n        Box,\r\n        /// <summary>\r\n        /// This is a PHYSICAL TRACE, so it's more expensive than the normal box or sphere trace\r\n        /// </summary>\r\n        [Icon( \"\ud83d\udee2\" )]\r\n        Cylinder,\r\n        /// <summary>\r\n        /// This will disable <see cref=\"StepsEnabled\"/> as it's impossible to get the angle reliably\r\n        /// </summary>\r\n        [Icon( \"\ud83c\udfd0\" )]\r\n        Sphere,\r\n        /// <summary>\r\n        /// No drawbacks, box but you can define exact bounds\r\n        /// </summary>\r\n        [Icon( \"\ud83e\uddca\" )]\r\n        Bounds\r\n    }\r\n\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [HideIf( nameof( PhysicallySimulated ), true )]\r\n    public TraceType TraceShape\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            CreateCollider();\r\n        }\r\n    } = TraceType.Box;\r\n\r\n    /// <summary>\r\n    /// Width of our trace\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [ShowIf( nameof( _showWidthHeight ), true )]\r\n    [Range( 1f, 128f, false, true )]\r\n    [Sync]\r\n    public float TraceWidth\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            UpdateCollider();\r\n        }\r\n    } = 16f;\r\n\r\n    // Show width/height if Physical mode OR if not using Bounds shape\r\n    bool _showWidthHeight => PhysicallySimulated || TraceShape != TraceType.Bounds;\r\n    // Show height if Physical mode OR if using Box/Cylinder shape\r\n    bool _showHeight => PhysicallySimulated || TraceShape == TraceType.Box || TraceShape == TraceType.Cylinder;\r\n\r\n    /// <summary>\r\n    /// Height of our trace\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [ShowIf( nameof( _showHeight ), true )]\r\n    [Range( 1f, 256f, false, true )]\r\n    [Sync]\r\n    public float TraceHeight\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            UpdateCollider();\r\n        }\r\n    } = 72f;\r\n\r\n    /// <summary>\r\n    /// Bounds of our trace\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [ShowIf( \"TraceShape\", TraceType.Bounds )]\r\n    [Sync]\r\n    public BBox TraceBounds\r\n    {\r\n        get => TraceShape == TraceType.Bounds ? field : Bounds;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            UpdateCollider();\r\n        }\r\n    } = BBox.FromPositionAndSize( Vector3.Up * 36f, new Vector3( 32f, 32f, 72f ) );\r\n\r\n    private bool ignoreTagsAndPhysical( TagSet tagSet ) => tagSet.IsEmpty || !PhysicallySimulated;\r\n\r\n    /// <summary>\r\n    /// Which tags it should ignore\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [Validate( nameof( ignoreTagsAndPhysical ), \"Contoller is physical! Ignore tags must be set on the collision matrix\", LogLevel.Warn )]\r\n    public TagSet IgnoreTags { get; set; } = new TagSet();\r\n\r\n    /// <summary>\r\n    /// Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [Range( 1, 20, true, true )]\r\n    public int MaxBounces { get; set; } = 5;\r\n\r\n    /// <summary>\r\n    /// Acceleration and Deceleration\r\n    /// </summary>\r\n    [FeatureEnabled( \"Acceleration\" )]\r\n    [Property]\r\n    public bool AccelerationEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How fast you accelerate while on the ground (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float GroundAcceleration { get; set; } = 1000f;\r\n\r\n    /// <summary>\r\n    /// How fast you accelerate while in the air (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float AirAcceleration { get; set; } = 300f;\r\n\r\n    /// <summary>\r\n    /// Use the fixed acceleration value instead of curves\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    public bool FixedAcceleration { get; set; } = true;\r\n\r\n    protected static readonly Curve DefaultAcceleration = new Curve( new List<Curve.Frame>\r\n    {\r\n        new Curve.Frame(0f, 0.2f, 0f, 1.5f),\r\n        new Curve.Frame(1f, 1f, 0f, 0f)\r\n    } )\r\n    {\r\n        TimeRange = new Vector2( 0f, 500f ),\r\n        ValueRange = new Vector2( 0f, 1f ),\r\n    };\r\n\r\n    /// <summary>\r\n    /// How much acceleration based on the current velocity<br/>\r\n    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>\r\n    /// Y axis *= Acceleration/><br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [ValueRange( 0f, 1f, false )]\r\n    [HideIf( \"FixedAcceleration\", true )]\r\n    public Curve AccelerationCurve { get; set; } = DefaultAcceleration;\r\n\r\n    /// <summary>\r\n    /// How fast you decelerate while on the ground (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float GroundDeceleration { get; set; } = 1500f;\r\n\r\n    /// <summary>\r\n    /// How fast you decelerate while in the air (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float AirDeceleration { get; set; } = 0f;\r\n\r\n    /// <summary>\r\n    /// Use the fixed deceleration value instead of curves\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    public bool FixedDeceleration { get; set; } = true;\r\n\r\n    protected static readonly Curve DefaultDeceleration = new Curve( new List<Curve.Frame>\r\n    {\r\n        new Curve.Frame(0f, 1f, 0f, -2.5f),\r\n        new Curve.Frame(1f, 0.2f, 0f, 0f)\r\n    } )\r\n    {\r\n        TimeRange = new Vector2( 0f, 500f ),\r\n        ValueRange = new Vector2( 0f, 1f ),\r\n    };\r\n\r\n    /// <summary>\r\n    /// How much deceleration based on the current velocity<br/>\r\n    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>\r\n    /// Y axis *= Deceleration/><br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [ValueRange( 0f, 1f, false )]\r\n    [HideIf( \"FixedDeceleration\", true )]\r\n    public Curve DecelerationCurve { get; set; } = DefaultDeceleration;\r\n\r\n    /// <summary>\r\n    /// Do we ignore the friction of the surface you're standing on or not?\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    public bool IgnoreGroundSurface { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// Use Time.Delta as the time step when automatically simulating (disable to set a fixed TimeDelta)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    [HideIf( nameof( ManuallyUpdate ), true )]\r\n    public bool UseTimeDelta { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Fixed time step used when automatically simulating and UseTimeDelta is disabled\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    [ShowIf( nameof( _showTimeDelta ), true )]\r\n    public float TimeDelta { get; set; } = 1f / 60f;\r\n\r\n    private bool _showTimeDelta => !ManuallyUpdate && !UseTimeDelta;\r\n    private float ActiveAutoTimeDelta => UseTimeDelta ? Time.Delta : TimeDelta;\r\n\r\n    /// <summary>\r\n    /// Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Movement\" )]\r\n    public bool IgnoreZ { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Do we ignore Z when it's near 0 (So that gravity affects you when not moving)\r\n    /// </summary>\r\n    [Property]\r\n    [Title( \"Ignore Z When Zero\" )]\r\n    [Group( \"Movement\" )]\r\n    [HideIf( \"IgnoreZ\", true )]\r\n    public bool IgnoreZWhenZero { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Tolerance from a 90\u00b0 surface before it's considered a wall (Ex. Tolerance 1 = Between 89\u00b0 and 91\u00b0 can be a wall, 0.1 = 89.9\u00b0 to 90.1\u00b0)\r\n    /// </summary>\r\n    [Group( \"Movement\" )]\r\n    [Property]\r\n    [Range( 0f, 10f, false )]\r\n    public float WallTolerance { get; set; } = 1f;\r\n\r\n    /// <summary>\r\n    /// Player feels like it's gripping walls too much? Try more Grip Factor Reduction!\r\n    /// </summary>\r\n    [Group( \"Movement\" )]\r\n    [Property]\r\n    [Range( 1f, 10f, true )]\r\n    [Validate( nameof( isPhysical ), \"Physical mode uses collider friction for wall interaction\", LogLevel.Info )]\r\n    public float GripFactorReduction { get; set; } = 1f;\r\n\r\n    /// <summary>\r\n    /// Bouncing off walls and ground on collision\r\n    /// </summary>\r\n    [FeatureEnabled( \"Bouncing\" )]\r\n    [Property]\r\n    public bool BouncingEnabled { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// How much the MoveHelper will \"bounce\" off walls when colliding with them<br/>\r\n    /// (0f = No bounce, 1f = Full bounce)<br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    [Range( 0f, 1f, true )]\r\n    [Validate( nameof( isPhysical ), \"Contoller is physical! Elasticity must be done through the collider\", LogLevel.Error )]\r\n    public float HorizontalElasticity { get; set; } = 0f;\r\n\r\n    /// <summary>\r\n    /// How much the MoveHelper will \"bounce\" off the ground when colliding with it<br/>\r\n    /// (0f = No bounce, 1f = Full bounce)<br/>\r\n    /// <see cref=\"GroundStickEnabled\"/> will override this value when sticking to the ground<br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    [Range( 0f, 1f, true )]\r\n    public float VerticalElasticity { get; set; } = 0f;\r\n\r\n    /// <summary>\r\n    /// Include the ground elasticity when bouncing off the ground<br/>\r\n    /// Make sure the ground surface doesn't have a very low elasticity!\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    public bool IncludeGroundElasticity { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// Minimum velocity to allow bouncing\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    [Range( 0f, 100f, true )]\r\n    public float ElasticityThreshold { get; set; } = 30f;\r\n\r\n    /// <summary>\r\n    /// Stick the MoveHelper to the ground (IsOnGround will default to false if disabled)\r\n    /// </summary>\r\n    [FeatureEnabled( \"GroundStick\" )]\r\n    [Property]\r\n    public bool GroundStickEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How steep terrain can be for you to stand on without slipping.<br/>\r\n    /// Min = Angle where velocity starts to drop off<br/>\r\n    /// Max = Max angle where velocity is fully blocked (you can still slide down with gravity)\r\n    /// </summary>\r\n    [Property, Step( 0.1f )]\r\n    [Feature( \"GroundStick\" )]\r\n    public RangedFloat MaxGroundAngle\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            if ( value.Range == RangedFloat.RangeType.Between )\r\n            {\r\n                var min = MathX.Clamp( value.Min, 0f, 89f );\r\n                var max = MathX.Clamp( value.Max, 0f, 89f );\r\n\r\n                field = new RangedFloat( MathF.Min( min, max ), MathF.Max( min, max ) );\r\n            }\r\n            else\r\n            {\r\n                field = new RangedFloat( MathX.Clamp( value.FixedValue, 0f, 89f ) );\r\n            }\r\n        }\r\n    } = new RangedFloat( 30f, 60f );\r\n\r\n    /// <summary>\r\n    /// How far from the ground the MoveHelper is going to stick (Useful for going down stairs!)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    [Range( 1f, 32f, false )]\r\n    public float GroundStickDistance { get; set; } = 12f;\r\n\r\n    /// <summary>\r\n    /// Stick to any <see cref=\"GroundObject\"/> when standing on it\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    public bool StickToPlatforms { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// When the surface you're on has a surface velocity, apply it to the controller\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    public bool ApplySurfaceVelocity { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// When the surface you're on is slippery, scale the max ground angled relative to the friction<br/>\r\n    /// Example: Ice with friction 0.5f, MaxGroundAngle will be 30f instead of 60f\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    public bool ScaleGroundAngleBySurfaceFriction { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Enable steps climbing (+1 Trace call)\r\n    /// </summary>\r\n    [FeatureEnabled( \"Steps\" )]\r\n    [Property]\r\n    public bool StepsEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How high steps can be for you to climb on\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    [Range( 1f, 32f, false )]\r\n    public float StepHeight { get; set; } = 12f;\r\n\r\n    /// <summary>\r\n    /// How deep it checks for steps (Minimum depth)\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    [Range( 0.1f, 8f, false )]\r\n    public float StepDepth { get; set; } = 2f;\r\n\r\n    /// <summary>\r\n    /// Tolerance from a 90\u00b0 surface before it's considered a valid step (Ex. Tolerance 1 = Between 89\u00b0 and 91\u00b0 can be a step, 0.1 = 89.9\u00b0 to 90.1\u00b0)\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    [Range( 0f, 10f, false )]\r\n    public float StepTolerance { get; set; } = 1f;\r\n\r\n    /// <summary>\r\n    /// Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain)\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    public bool PseudoStepsEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!)\r\n    /// </summary>\r\n    [FeatureEnabled( \"Push\" )]\r\n    [Property]\r\n    public bool PushEnabled { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Push\" )]\r\n    [Validate( nameof( isPhysical ), \"Contoller is physical! Make sure the tags are ignored on the collision matrix\", LogLevel.Warn )]\r\n    [Sync]\r\n    public Dictionary<string, float> PushTagsWeight\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            _pushTags = BuildPushTags();\r\n        }\r\n    } = new Dictionary<string, float>() { { \"player\", 1f } };\r\n\r\n    /// <summary>\r\n    /// Apply gravity to this MoveHelper when not on the ground\r\n    /// </summary>\r\n    [FeatureEnabled( \"Gravity\" )]\r\n    [Property]\r\n    public bool GravityEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Use the scene's gravity or our own\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    public bool UseSceneGravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    } = true;\r\n\r\n    /// <summary>\r\n    /// Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    [HideIf( \"UseSceneGravity\", true )]\r\n    public bool UseVectorGravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    }\r\n\r\n    private bool _usingFloatGravity => !UseVectorGravity && !UseSceneGravity;\r\n    private bool _usingVectorGravity => UseVectorGravity && !UseSceneGravity;\r\n\r\n    /// <summary>\r\n    /// Units per second squared (Default is -850f)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    [Range( -2000, 2000, false )]\r\n    [ShowIf( \"_usingFloatGravity\", true )]\r\n    public float Gravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    } = -850f;\r\n\r\n    /// <summary>\r\n    /// Units per second squared (Default is 0f, 0f, -850f)<br/>\r\n    /// Changes which way <see cref=\"GroundStickEnabled\"/> sticks to the ground\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    [ShowIf( \"_usingVectorGravity\", true )]\r\n    public Vector3 VectorGravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    } = new Vector3( 0f, 0f, -850f );\r\n\r\n    private Vector3 _appliedGravity;\r\n    public Vector3 GravityNormal { get; private set; }\r\n    public Vector3 AppliedGravity => _appliedGravity;\r\n\r\n    /// <summary>\r\n    /// Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck)\r\n    /// </summary>\r\n    [FeatureEnabled( \"Unstuck\" )]\r\n    [Property]\r\n    public bool UnstuckEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How many trace calls it will attempt to get the MoveHelper unstuck\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Unstuck\" )]\r\n    [Range( 1, 100, false )]\r\n    [Step( 1f )]\r\n    public int MaxUnstuckTries { get; set; } = 40;\r\n\r\n    /// <summary>\r\n    /// The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside)\r\n    /// </summary>\r\n    [Sync] public Vector3 WishVelocity { get; set; }\r\n\r\n    /// <summary>\r\n    /// The resulting velocity after the simulation is done (Units per second)\r\n    /// </summary>\r\n    [Sync]\r\n    public Vector3 Velocity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            if ( PhysicallySimulated && Body.IsValid() )\r\n                Body.Velocity = value;\r\n\r\n            field = value;\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Velocity controlled by outside factors, such as knockback, rootmotion, etc.\r\n    /// It is only applied to our final position and doesn't affect our Velocity.\r\n    /// It should be handled outside the controller.\r\n    /// </summary>\r\n    [Sync] public Vector3 ExternalVelocity { get; set; }\r\n\r\n    /// <summary>\r\n    /// Is the MoveHelper currently touching the ground\r\n    /// </summary>\r\n    [Sync] public bool IsOnGround { get; set; }\r\n\r\n    /// <summary>\r\n    /// The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false)\r\n    /// </summary>\r\n    public Vector3 GroundNormal { get; private set; } = Vector3.Zero;\r\n\r\n    /// <summary>\r\n    /// The current ground angle you're standing on (Always 0f if IsOnGround false)\r\n    /// </summary>\r\n    public float GroundAngle => Vector3.GetAngle( GroundNormal, -GravityNormal );\r\n\r\n    /// <summary>\r\n    /// Gets the velocity multiplier for a given slope angle based on MaxGroundAngle range.<br/>\r\n    /// Returns 1.0 below the start angle, 0.0 at/above the max angle, and lerps between.\r\n    /// </summary>\r\n    public float GetSlopeVelocityMultiplier( float angle )\r\n    {\r\n        if ( angle <= MaxGroundAngle.Min ) return 1f;\r\n        if ( angle >= MaxGroundAngle.Max ) return 0f;\r\n        return 1f - MathX.LerpInverse( angle, MaxGroundAngle.Min, MaxGroundAngle.Max );\r\n    }\r\n\r\n    /// <summary>\r\n    /// Whether the given angle is considered standable (below max ground angle)\r\n    /// </summary>\r\n    public bool IsAngleStandable( float angle ) => angle <= MaxGroundAngle.Max * ( ScaleGroundAngleBySurfaceFriction ? GroundSurface?.Friction ?? 1f : 1f );\r\n\r\n    /// <summary>\r\n    /// The current surface you're standing on\r\n    /// </summary>\r\n    public Surface GroundSurface { get; private set; }\r\n\r\n    /// <summary>\r\n    /// The gameobject you're currently standing on\r\n    /// </summary>\r\n    public GameObject GroundObject { get; set; }\r\n\r\n    /// <summary>\r\n    /// Is the MoveHelper currently pushing against a wall\r\n    /// </summary>\r\n    public bool IsPushingAgainstWall { get; private set; }\r\n\r\n    /// <summary>\r\n    /// The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false)\r\n    /// </summary>\r\n    public Vector3 WallNormal { get; private set; } = Vector3.Zero;\r\n\r\n    /// <summary>\r\n    /// The gameobject you're currently pushing on\r\n    /// </summary>\r\n    public GameObject WallObject { get; set; }\r\n\r\n    /// <summary>\r\n    /// Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false)\r\n    /// </summary>\r\n    [Sync] public bool IsSlipping { get; private set; } // TODO IMPLEMENT\r\n\r\n    /// <summary>\r\n    /// The MoveHelper is stuck and we can't get it out\r\n    /// </summary>\r\n    [Sync] public bool IsStuck { get; private set; }\r\n\r\n    /// <summary>\r\n    /// To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later\r\n    /// </summary>\r\n    public float SkinWidth => Math.Min( Math.Max( 0.1f, TraceWidth * 0.05f ), GroundStickDistance );\r\n\r\n    public float AppliedWidth => TraceWidth * WorldScale.x; // The width of the MoveHelper in world units\r\n    public float AppliedDepth => TraceWidth * WorldScale.y; // The depth of the MoveHelper in world units\r\n    public float AppliedHeight => TraceShape == TraceType.Sphere ? AppliedWidth :\r\n        TraceShape == TraceType.Bounds ? TraceBounds.Size.z * WorldScale.z : TraceHeight * WorldScale.z; // The height of the MoveHelper in world units\r\n    private Vector3 _upAxis => RotateWithGameObject ? WorldRotation.Up : Vector3.Up;\r\n    private Vector3 _offset => ( RotateWithGameObject ? WorldRotation.Up : Vector3.Up ) * ( TraceShape == TraceType.Sphere || TraceShape == TraceType.Bounds ? 0f : AppliedHeight / 2f ); // The position of the MoveHelper in world units\r\n\r\n    /// <summary>\r\n    /// The bounds of this MoveHelper generated from the TraceWidth and TraceHeight\r\n    /// </summary>\r\n    public BBox Bounds { get; set; }\r\n    private BBox _shrunkenBounds;\r\n    private string[] _pushTags;\r\n    private Vector3 _lastVelocity;\r\n    private float _minimumTolerance => MathX.Clamp( Time.Delta / 2f, 0.005f, 0.1f ); // Floating precision tolerance, too high if used inside of OnUpdate so tied to update rate\r\n\r\n    /// <summary>\r\n    /// If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us\r\n    /// </summary>\r\n    public ShrimpleCharacterController UnstuckTarget;\r\n\r\n    public Action<ShrimpleCollisionResult> OnCollide { get; set; }\r\n    public bool IsOnPlatform => IsOnGround && GroundStickEnabled && !IsSlipping && StickToPlatforms && GroundObject.IsValid();\r\n    public Vector3 PlatformVelocity => IsOnPlatform ? GroundObject.GetComponent<Collider>()?.GetVelocityAtPoint( WorldPosition ) ?? Vector3.Zero : Vector3.Zero;\r\n    public bool IsOnSurfaceWithVelocity => IsOnGround && GroundStickEnabled && !IsSlipping && ApplySurfaceVelocity && GroundObject.IsValid();\r\n    public Vector3 SurfaceVelocity => IsOnSurfaceWithVelocity ? GroundObject.GetComponent<Collider>()?.SurfaceVelocity * GroundObject.WorldRotation ?? Vector3.Zero : Vector3.Zero;\r\n    public Vector3 GroundVelocity => PlatformVelocity + SurfaceVelocity;\r\n\r\n    protected override void OnStart()\r\n    {\r\n        RebuildBounds();\r\n        BuildGravity();\r\n        _pushTags = BuildPushTags();\r\n    }\r\n\r\n    protected override void DrawGizmos()\r\n    {\r\n        if ( Gizmo.IsSelected )\r\n        {\r\n            Gizmo.GizmoDraw draw = Gizmo.Draw;\r\n            draw.Color = Color.Blue;\r\n\r\n            if ( TraceShape == TraceType.Box )\r\n                draw.LineBBox( Bounds.Translate( Vector3.Up * Bounds.Size.z / 2f ) );\r\n            if ( TraceShape == TraceType.Cylinder )\r\n                draw.LineCylinder( Vector3.Zero, Vector3.Up * ( Bounds.Maxs.z - Bounds.Mins.z ), Bounds.Maxs.x, Bounds.Maxs.x, 24 );\r\n            if ( TraceShape == TraceType.Sphere )\r\n                draw.LineSphere( Vector3.Up * Bounds.Maxs.x, Bounds.Maxs.x );\r\n            if ( TraceShape == TraceType.Bounds )\r\n                draw.LineBBox( TraceBounds );\r\n\r\n            if ( !UseSceneGravity && UseVectorGravity )\r\n            {\r\n                draw.Color = Color.Red;\r\n                draw.LineThickness = 5f;\r\n                draw.IgnoreDepth = true;\r\n                draw.Arrow( Vector3.Zero, GravityNormal * 50f );\r\n            }\r\n        }\r\n    }\r\n\r\n    private BBox BuildBounds()\r\n    {\r\n        if ( TraceShape == TraceType.Bounds )\r\n            return new BBox( TraceBounds.Mins * GameObject.WorldScale, TraceBounds.Maxs * GameObject.WorldScale );\r\n\r\n        var x = GameObject.WorldScale.x;\r\n        var y = GameObject.WorldScale.y;\r\n        var z = GameObject.WorldScale.z;\r\n\r\n        var width = TraceWidth / 2f * x;\r\n        var depth = TraceWidth / 2f * y;\r\n        var halfHeight = TraceHeight / 2f * z;\r\n\r\n        // For cylinders we want the bounds to start at Z = 0 and extend up to the full height\r\n        if ( TraceShape == TraceType.Cylinder )\r\n            return new BBox( new Vector3( -width, -depth, 0f ), new Vector3( width, depth, halfHeight * 2f ) );\r\n\r\n        return new BBox( new Vector3( -width, -depth, -halfHeight ), new Vector3( width, depth, halfHeight ) );\r\n    }\r\n\r\n    private void RebuildBounds()\r\n    {\r\n        Bounds = BuildBounds();\r\n        _shrunkenBounds = Bounds.Grow( -SkinWidth );\r\n    }\r\n\r\n    private void BuildGravity()\r\n    {\r\n        _appliedGravity = UseSceneGravity ? Scene.PhysicsWorld.Gravity : UseVectorGravity ? VectorGravity : new Vector3( 0f, 0f, Gravity );\r\n        GravityNormal = _appliedGravity.Normal;\r\n    }\r\n\r\n    private string[] BuildPushTags()\r\n    {\r\n        return PushTagsWeight.Keys.ToArray();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Casts the current bounds from to and returns the scene trace result\r\n    /// </summary>\r\n    /// <param name=\"bounds\"></param>\r\n    /// <param name=\"from\"></param>\r\n    /// <param name=\"to\"></param>\r\n    /// <param name=\"useCapsule\">Use a capsule shape instead of the defaults</param>\r\n    /// <returns></returns>\r\n    public SceneTraceResult BuildTrace( BBox bounds, Vector3 from, Vector3 to, bool useCapsule = false )\r\n    {\r\n        SceneTrace builder = new SceneTrace(); // Empty trace builder\r\n\r\n        if ( useCapsule )\r\n        {\r\n            var radius = bounds.Maxs.x;\r\n            var halfHeight = ( bounds.Maxs.z - bounds.Mins.z ) / 2f;\r\n            var hemiOffset = MathF.Max( halfHeight - radius, 0f );\r\n            var capsule = new Capsule( Vector3.Down * hemiOffset, Vector3.Up * hemiOffset, radius ); // Do not rotate Vector3, it's rotated 17 lines down!\r\n            builder = Game.SceneTrace.Capsule( capsule, from, to );\r\n        }\r\n        else\r\n        {\r\n            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )\r\n                builder = Game.SceneTrace.Box( bounds, from, to );\r\n            if ( TraceShape == TraceType.Cylinder )\r\n                builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );\r\n            if ( TraceShape == TraceType.Sphere )\r\n                builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from + Vector3.Up * bounds.Maxs.x, to + Vector3.Up * bounds.Maxs.x );\r\n        }\r\n\r\n        builder = builder\r\n            .IgnoreGameObjectHierarchy( GameObject )\r\n            .WithoutTags( IgnoreTags );\r\n\r\n        if ( RotateWithGameObject )\r\n            builder = builder.Rotated( PhysicallySimulated ? Collider.WorldRotation : GameObject.WorldRotation );\r\n\r\n        return builder.Run();\r\n    }\r\n\r\n    private SceneTraceResult BuildPushTrace( BBox bounds, Vector3 from, Vector3 to )\r\n    {\r\n        SceneTrace builder = new SceneTrace(); // Empty trace builder\r\n\r\n        if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )\r\n            builder = Game.SceneTrace.Box( bounds, from, to );\r\n        if ( TraceShape == TraceType.Cylinder )\r\n            builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );\r\n        if ( TraceShape == TraceType.Sphere )\r\n            builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from, to );\r\n\r\n        builder = builder\r\n            .IgnoreGameObjectHierarchy( GameObject )\r\n            .WithAnyTags( _pushTags ); // Check for only the push tags\r\n\r\n        if ( RotateWithGameObject )\r\n            builder = builder.Rotated( GameObject.WorldRotation );\r\n\r\n        return builder.Run();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Detach the MoveHelper from the ground and launch it somewhere (Units per second)\r\n    /// </summary>\r\n    /// <param name=\"amount\"></param>\r\n    public void Punch( in Vector3 amount )\r\n    {\r\n        var groundVel = GroundVelocity; // Capture before ungrounding\r\n        IsOnGround = false;\r\n\r\n        if ( PhysicallySimulated && Body.IsValid() )\r\n            Body.Velocity = Body.Velocity + amount + groundVel;\r\n\r\n        Velocity += amount + groundVel;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper\r\n    /// </summary>\r\n    /// <param name=\"manualUpdate\">Just calculate but don't update position</param>\r\n    public MoveHelperResult Move( bool manualUpdate = false ) => Move( Time.Delta, manualUpdate );\r\n\r\n    /// <summary>\r\n    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper\r\n    /// </summary>\r\n    /// <param name=\"delta\">The time step</param>\r\n    /// <param name=\"manualUpdate\">Just calculate but don't update position</param>\r\n    public MoveHelperResult Move( float delta, bool manualUpdate = false )\r\n    {\r\n        // In physical mode the simulation runs inside the physics step callbacks.\r\n        // Queue this call so the next PrePhysicsStep/PostPhysicsStep drives the body with the requested delta.\r\n        if ( PhysicallySimulated )\r\n        {\r\n            if ( Body.IsValid() && !manualUpdate )\r\n            {\r\n                _physicalMoveDelta = delta;\r\n                _physicalMovePending = true;\r\n            }\r\n\r\n            return new MoveHelperResult( WorldPosition, Velocity );\r\n        }\r\n\r\n        var goalVelocity = CalculateGoalVelocity( delta ); // Calculate the goal velocity using our Acceleration and Deceleration values\r\n\r\n        // SIMULATE PUSH FORCES //\r\n        if ( PushEnabled )\r\n        {\r\n            var pushTrace = BuildPushTrace( Bounds, WorldPosition + _offset, WorldPosition ); // Build a trace but using the Push tags instead of the Ignore tags\r\n\r\n            if ( pushTrace.Hit ) // We're inside any of the push tags\r\n            {\r\n                foreach ( var tag in pushTrace.GameObject.Tags )\r\n                {\r\n                    if ( PushTagsWeight.TryGetValue( tag, out var tagWeight ) )\r\n                    {\r\n                        var otherPosition = pushTrace.GameObject.WorldPosition.WithZ( WorldPosition.z ); // Only horizontal pushing\r\n                        var pushDirection = ( otherPosition - WorldPosition ).Normal;\r\n                        var pushVelocity = pushDirection * tagWeight * 50f; // I find 50 u/s to be a good amount to push if the weight is 1.0 (!!!)\r\n\r\n                        goalVelocity -= pushVelocity;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        var finalPosition = WorldPosition;\r\n\r\n        var moveHelperResult = CollideAndSlide( goalVelocity, finalPosition + _offset, delta ); // Simulate the MoveHelper\r\n\r\n        finalPosition = moveHelperResult.Position;\r\n        var finalVelocity = moveHelperResult.Velocity;\r\n        // SIMULATE GRAVITY //\r\n        if ( GravityEnabled && Gravity != 0f )\r\n        {\r\n            if ( !IsOnGround || IsSlipping || !GroundStickEnabled )\r\n            {\r\n                var gravity = AppliedGravity * delta;\r\n                var gravityResult = CollideAndSlide( gravity, moveHelperResult.Position, delta, gravityPass: true ); // Apply and simulate the gravity step\r\n\r\n                finalPosition = gravityResult.Position;\r\n                finalVelocity += gravityResult.Velocity;\r\n            }\r\n        }\r\n\r\n        if ( !ExternalVelocity.IsNearZeroLength )\r\n        {\r\n            moveHelperResult = CollideAndSlide( ExternalVelocity, finalPosition, delta );\r\n        }\r\n\r\n        finalPosition = moveHelperResult.Position - _offset; // Compensate for the offset we added at the beginning\r\n        _lastVelocity = Velocity * delta;\r\n\r\n        if ( !manualUpdate )\r\n        {\r\n            Velocity = finalVelocity;\r\n            WorldPosition = finalPosition + SurfaceVelocity * delta; // Actually updating the position is \"expensive\" so we only do it once at the end\r\n        }\r\n\r\n        return new MoveHelperResult( finalPosition, finalVelocity, moveHelperResult.Offset );\r\n    }\r\n\r\n    /// <summary>\r\n    /// Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity\r\n    /// </summary>\r\n    public struct MoveHelperResult\r\n    {\r\n        public Vector3 Position;\r\n        public Vector3 Velocity;\r\n        internal Vector3 Offset;\r\n        internal float Leftover = 1f;\r\n\r\n        public MoveHelperResult( Vector3 position, Vector3 velocity )\r\n        {\r\n            Position = position;\r\n            Velocity = velocity;\r\n        }\r\n\r\n        internal MoveHelperResult( Vector3 position, Vector3 velocity, Vector3 offset, float leftover = 1f )\r\n        {\r\n            Position = position;\r\n            Velocity = velocity;\r\n            Offset = offset;\r\n            Leftover = leftover;\r\n        }\r\n    }\r\n\r\n    bool _bounced = false;\r\n\r\n    private MoveHelperResult CollideAndSlide( Vector3 velocity, Vector3 position, float delta, int depth = 0, bool gravityPass = false ) =>\r\n        CollideAndSlide( new MoveHelperResult( position, velocity ), delta, depth, gravityPass );\r\n\r\n    private MoveHelperResult CollideAndSlide( MoveHelperResult current, float delta, int depth = 0, bool gravityPass = false )\r\n    {\r\n        if ( depth >= MaxBounces )\r\n            return current;\r\n\r\n        var velocity = current.Velocity * delta; // I like to set Velocity as units/second but we have to deal with units/tick here\r\n        var position = current.Position;\r\n\r\n        // GROUND AND UNSTUCK CHECK //\r\n        if ( depth == 0 ) // Only check for the first step since it's impossible to get stuck on other steps\r\n        {\r\n            var groundTrace = BuildTrace( _shrunkenBounds, position, position + AppliedGravity.Normal * ( GroundStickDistance + SkinWidth * 1.1f ) ); // Compensate for floating inaccuracy\r\n\r\n            if ( groundTrace.StartedSolid )\r\n            {\r\n                IsStuck = true;\r\n                if ( UnstuckEnabled )\r\n                {\r\n                    if ( UnstuckTarget == null )\r\n                    {\r\n                        IsStuck = !TryUnstuck( position, out var result );\r\n\r\n                        if ( !IsStuck )\r\n                        {\r\n                            position = result; // Update the new position\r\n\r\n                            if ( PhysicallySimulated && Body.IsValid() )\r\n                            {\r\n                                Body.WorldPosition = result;\r\n                                Body.Velocity = Vector3.Zero;\r\n                            }\r\n\r\n                            if ( groundTrace.GameObject != null )\r\n                                if ( groundTrace.GameObject.Components.TryGet<ShrimpleCharacterController>( out var otherHelper ) )\r\n                                    otherHelper.UnstuckTarget = this; // We already solved this, no need to unstuck the other helper\r\n                        }\r\n                        else\r\n                        {\r\n                            return new MoveHelperResult( position, Vector3.Zero ); // Mission failed, bail out!\r\n                        }\r\n                    }\r\n                    else\r\n                    {\r\n                        UnstuckTarget = null; // Alright the other MoveHelper got us unstuck so just do nothing\r\n                    }\r\n                }\r\n            }\r\n            else\r\n            {\r\n                var hasLanded = !IsOnGround && Vector3.Dot( Velocity, AppliedGravity ) >= 0f && groundTrace.Hit && groundTrace.Distance <= SkinWidth * 2f; // Wasn't on the ground and now is\r\n                var isGrounded = IsOnGround && groundTrace.Hit; // Was already on the ground and still is, this helps stick when going down stairs\r\n\r\n                IsOnGround = hasLanded || isGrounded;\r\n                GroundSurface = IsOnGround ? groundTrace.Surface : null;\r\n                GroundNormal = IsOnGround ? groundTrace.Normal : -AppliedGravity.Normal;\r\n                GroundObject = IsOnGround ? groundTrace.GameObject : null;\r\n                IsSlipping = IsOnGround && !IsAngleStandable( GroundAngle );\r\n\r\n                if ( IsSlipping && !gravityPass && Vector3.Dot( velocity, AppliedGravity ) < 0f )\r\n                    velocity = velocity.WithZ( 0f ); // If we're slipping ignore any extra velocity we had\r\n\r\n                // FIX: Clamp velocity when slipping to prevent buildup\r\n                // Project velocity onto the slope and limit accumulation\r\n                if ( IsSlipping )\r\n                {\r\n                    var slopeDir = Vector3.VectorPlaneProject( AppliedGravity.Normal, GroundNormal ).Normal;\r\n                    var slopeSpeed = Vector3.Dot( velocity, slopeDir );\r\n                    var maxSlipSpeed = AppliedGravity.Length * delta * 2f; // Reasonable max slip speed per frame\r\n\r\n                    if ( slopeSpeed > maxSlipSpeed )\r\n                    {\r\n                        // Clamp the velocity component along the slope\r\n                        velocity = velocity - slopeDir * ( slopeSpeed - maxSlipSpeed );\r\n                    }\r\n                }\r\n\r\n                if ( IsOnGround && GroundStickEnabled && !IsSlipping )\r\n                {\r\n                    position = groundTrace.EndPosition + -AppliedGravity.Normal * SkinWidth; // Place on the ground\r\n                    velocity = Vector3.VectorPlaneProject( velocity, GroundNormal ); // Follow the ground you're on without projecting Z\r\n                }\r\n\r\n                IsStuck = false;\r\n            }\r\n        }\r\n\r\n        if ( velocity.IsNearlyZero( _minimumTolerance ) ) // Not worth continuing, reduces small stutter\r\n        {\r\n            return new MoveHelperResult( position, Vector3.Zero, current.Offset );\r\n        }\r\n\r\n        var toTravel = velocity.Length * current.Leftover + SkinWidth;\r\n        var targetPosition = position + velocity.Normal * toTravel;\r\n        var travelTrace = BuildTrace( _shrunkenBounds, position, targetPosition );\r\n\r\n        if ( travelTrace.Hit )\r\n        {\r\n            var travelled = velocity.Normal * Math.Max( travelTrace.Distance - SkinWidth, 0f );\r\n            var leftover = velocity - travelled; // How much leftover velocity still needs to be simulated\r\n            var speed = leftover.Length;\r\n            var angle = Vector3.GetAngle( -AppliedGravity.Normal, travelTrace.Normal );\r\n\r\n            if ( toTravel >= SkinWidth && travelTrace.Distance < SkinWidth )\r\n                travelled = Vector3.Zero;\r\n\r\n            var elasticityDirection = MathX.Lerp( HorizontalElasticity, VerticalElasticity, angle / 90f );\r\n            var elasticity = PhysicallySimulated ? 0f : BouncingEnabled ?\r\n                velocity.Length / delta <= ElasticityThreshold ?\r\n                    0f : elasticityDirection * ( IncludeGroundElasticity ? GroundSurface?.Elasticity ?? 1f : 1f )\r\n                : 0f; // Allah help me format this better\r\n\r\n            if ( IsAngleStandable( angle ) ) // Terrain we can walk on\r\n            {\r\n                var projectedLeftover = leftover;\r\n\r\n                if ( gravityPass || !IsOnGround )\r\n                    projectedLeftover = Vector3.VectorPlaneProject( projectedLeftover, travelTrace.Normal ); // Don't project the vertical velocity after landing else it boosts your horizontal velocity\r\n                else\r\n                    projectedLeftover = projectedLeftover.ProjectAndScale( travelTrace.Normal ); // Project the velocity along the terrain\r\n\r\n                // Apply slope velocity reduction only when going uphill (against gravity)\r\n                var isGoingUphill = Vector3.Dot( projectedLeftover, AppliedGravity ) < 0f;\r\n                if ( isGoingUphill )\r\n                {\r\n                    var slopeMultiplier = GetSlopeVelocityMultiplier( angle );\r\n                    projectedLeftover *= slopeMultiplier;\r\n                }\r\n\r\n                if ( elasticity > 0 )\r\n                    leftover = Vector3.Lerp( projectedLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );\r\n                else\r\n                    leftover = projectedLeftover;\r\n\r\n                IsPushingAgainstWall = false;\r\n                WallObject = null;\r\n            }\r\n            else\r\n            {\r\n                var climbedStair = false;\r\n\r\n                if ( angle >= 90f - WallTolerance && angle <= 90f + WallTolerance ) // Check for walls\r\n                    IsPushingAgainstWall = true; // We're pushing against a wall\r\n\r\n                if ( StepsEnabled )\r\n                {\r\n                    var isStep = angle >= 90f - StepTolerance && angle <= 90f + StepTolerance;\r\n\r\n                    if ( isStep || PseudoStepsEnabled ) // Check for steps\r\n                    {\r\n                        if ( IsOnGround ) // Stairs VVV\r\n                        {\r\n                            var stepHorizontal = Vector3.VectorPlaneProject( velocity.Normal, AppliedGravity.Normal ).Normal * StepDepth; // How far in front we're looking for steps\r\n                            var stepVertical = -AppliedGravity.Normal * ( StepHeight + SkinWidth ); // How high we're looking for steps + Some to compensate for floating inaccuracy\r\n                            var stepTrace = BuildTrace( _shrunkenBounds, travelTrace.EndPosition + stepHorizontal + stepVertical, travelTrace.EndPosition + stepHorizontal );\r\n                            var stepAngle = Vector3.GetAngle( stepTrace.Normal, -AppliedGravity.Normal );\r\n\r\n                            if ( !stepTrace.StartedSolid && stepTrace.Hit && IsAngleStandable( stepAngle ) ) // We found a step!\r\n                            {\r\n                                if ( isStep || !IsSlipping && PseudoStepsEnabled )\r\n                                {\r\n                                    var stepDistance = stepTrace.EndPosition - travelTrace.EndPosition;\r\n                                    var stepTravelled = -AppliedGravity.Normal * stepDistance;\r\n                                    position += stepTravelled; // Offset our position by the height of the step climbed\r\n                                    climbedStair = true;\r\n\r\n                                    IsPushingAgainstWall = false; // Nevermind, we're not against a wall, we climbed a step!\r\n                                    WallObject = null;\r\n                                }\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n\r\n                if ( IsPushingAgainstWall )\r\n                {\r\n                    // Scale our leftover velocity based on the angle of approach relative to the wall\r\n                    // (Perpendicular = 0%, Parallel = 100%)\r\n                    var scale = ScaleAgainstWalls ? 1f - Vector3.Dot( -travelTrace.Normal.Normal / GripFactorReduction, velocity.Normal ) : 1f;\r\n                    var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal.Normal ) : leftover.ProjectAndScale( travelTrace.Normal.Normal );\r\n\r\n                    if ( elasticity > 0 )\r\n                        leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );\r\n                    else\r\n                        leftover = ( wallLeftover * scale ).WithZ( wallLeftover.z );\r\n\r\n                    WallObject = travelTrace.GameObject;\r\n                    WallNormal = travelTrace.Normal;\r\n                }\r\n                else\r\n                {\r\n                    if ( !climbedStair )\r\n                    {\r\n                        var scale = IsSlipping ? 1f : 1f - Vector3.Dot( -travelTrace.Normal / GripFactorReduction, velocity.Normal );\r\n                        var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal ) * scale : leftover.ProjectAndScale( travelTrace.Normal );\r\n\r\n                        if ( elasticity > 0 )\r\n                            leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );\r\n                        else\r\n                            leftover = wallLeftover;\r\n                    }\r\n\r\n                }\r\n\r\n\r\n            }\r\n\r\n            var previousVelocity = velocity;\r\n            var leftoverSpeed = leftover.Length / speed; // How much speed we had left after the collision\r\n            velocity = leftover.Normal * velocity.Length * leftoverSpeed;\r\n\r\n            if ( travelled.Length <= _minimumTolerance && leftover.Length <= _minimumTolerance )\r\n                return new MoveHelperResult( position + travelled, velocity / delta, current.Offset );\r\n\r\n            if ( elasticity > 0 )\r\n                _bounced = true;\r\n\r\n            var offset = position - current.Position;\r\n            var newResult = CollideAndSlide( new MoveHelperResult( position + travelled, velocity / delta, offset, leftover.Length / velocity.Length ), delta, depth + 1, gravityPass ); // Simulate another bounce for the leftover velocity from the latest position\r\n\r\n            if ( depth == 0 && !gravityPass )\r\n            {\r\n                var collision = new ShrimpleCollisionResult()\r\n                    .WithHitNormal( travelTrace.Normal )\r\n                    .WithHitPosition( travelTrace.HitPosition )\r\n                    .WithAngle( angle )\r\n                    .WithHitObject( travelTrace.GameObject )\r\n                    .WithHitSurface( travelTrace.Surface )\r\n                    .WithHitVelocityBefore( previousVelocity / delta )\r\n                    .WithHitVelocityAfter( newResult.Velocity );\r\n\r\n                OnCollide?.Invoke( collision );\r\n            }\r\n\r\n            return newResult;\r\n        }\r\n\r\n        if ( depth == 0 && !gravityPass )\r\n        {\r\n            IsPushingAgainstWall = false;\r\n            WallObject = null;\r\n        }\r\n\r\n        if ( gravityPass && _bounced )\r\n        {\r\n            _bounced = false;\r\n            velocity -= AppliedGravity * delta * delta * 1.4f; // Evil hack so it doesn't lose velocity due to gravity passes after each bounce\r\n        }\r\n\r\n        return new MoveHelperResult( position + velocity, velocity / delta, current.Offset ); // We didn't hit anything? Ok just keep going then :-)\r\n    }\r\n\r\n    private float CalculateGoalSpeed( Vector3 wishVelocity, Vector3 velocity, bool isAccelerating, float delta )\r\n    {\r\n        if ( !AccelerationEnabled ) return 999999999f; // Should be enough for the freaks that don't want acceleration\r\n\r\n        float goalSpeed;\r\n\r\n        var isSameDirection = velocity.IsNearlyZero( 1f ) || Vector3.Dot( wishVelocity.WithZ( 0f ).Normal, velocity.WithZ( 0f ).Normal ) >= 0f; // Is our wishVelocity roughly moving towards our velocity already?\r\n        var currentSpeed = Math.Max( Velocity.WithZ( 0f ).Length, 10f );\r\n        var acceleration = ( IsOnGround ? GroundAcceleration : AirAcceleration ) * ( FixedAcceleration ? 1f : AccelerationCurve.Evaluate( currentSpeed ) );\r\n        var deceleration = ( IsOnGround ? GroundDeceleration : AirDeceleration ) * ( FixedDeceleration ? 1f : DecelerationCurve.Evaluate( currentSpeed ) );\r\n\r\n        if ( isAccelerating )\r\n            goalSpeed = acceleration;\r\n        else\r\n            goalSpeed = !isSameDirection ? Math.Max( acceleration, deceleration ) : deceleration; // Makes movement more responsive especially for flying or rolling\r\n\r\n        if ( !IgnoreGroundSurface && GroundSurface != null )\r\n            goalSpeed *= GroundSurface.Friction; // Take into account the ground's friction\r\n\r\n        goalSpeed *= delta;\r\n\r\n        return goalSpeed;\r\n    }\r\n\r\n    private Vector3 CalculateGoalVelocity( float delta )\r\n    {\r\n        bool shouldIgnoreZ = IgnoreZ || ( IgnoreZWhenZero && WishVelocity.z.AlmostEqual( 0f ) );\r\n        var wishVelocity = shouldIgnoreZ ? ( WishVelocity.Normal * WishVelocity.Length ).WithZ( Velocity.z ) : WishVelocity;\r\n        var isAccelerating = shouldIgnoreZ ? wishVelocity.WithZ( 0f ).Length >= Velocity.WithZ( 0f ).Length : wishVelocity.Length >= Velocity.Length;\r\n\r\n        var goalSpeed = CalculateGoalSpeed( wishVelocity, Velocity, isAccelerating, delta );\r\n        var goalVelocity = Velocity.MoveTowards( wishVelocity, goalSpeed );\r\n\r\n        return shouldIgnoreZ ? goalVelocity.WithZ( Velocity.z ) : goalVelocity;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Attempts to return the nearest standable position until you're not longer inside other colliders<br/>\r\n    /// If it fails too frequently, consider increasing <see cref=\"MaxUnstuckTries\"/>\r\n    /// </summary>\r\n    /// <param name=\"position\">The start position</param>\r\n    /// <param name=\"result\">The final standable position</param>\r\n    /// <returns>True if it found one, false if it failed.</returns>\r\n    public bool TryUnstuck( Vector3 position, out Vector3 result )\r\n    {\r\n        if ( _lastVelocity == Vector3.Zero )\r\n            _lastVelocity = -AppliedGravity.Normal;\r\n\r\n        var velocityLength = _lastVelocity.Length + SkinWidth;\r\n        var startPos = position - _lastVelocity.Normal * velocityLength; // Try undoing the last velocity 1st\r\n        var endPos = position;\r\n\r\n        for ( int i = 0; i < MaxUnstuckTries + 1; i++ )\r\n        {\r\n            if ( i == 1 )\r\n                startPos = position + -AppliedGravity.Normal * 2f; // Try going up 2nd\r\n\r\n            if ( i > 1 && i < MaxUnstuckTries / 2f )\r\n                startPos = position + Vector3.Random.Normal * ( (float)i / 2f ); // Start randomly checking 3rd\r\n\r\n            if ( i >= MaxUnstuckTries / 2f )\r\n                startPos = position + Vector3.Random.Normal * i; // Ok at this point start checking further\r\n\r\n            if ( startPos - endPos == Vector3.Zero ) // No difference!\r\n                continue;\r\n\r\n            var unstuckTrace = BuildTrace( _shrunkenBounds, startPos, endPos, PhysicallySimulated );\r\n\r\n            if ( !unstuckTrace.StartedSolid )\r\n            {\r\n                result = unstuckTrace.EndPosition - _lastVelocity.Normal * SkinWidth / 4f;\r\n                _lastVelocity = Vector3.Zero;\r\n                return true;\r\n            }\r\n        }\r\n\r\n        result = position;\r\n        return false;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Debug don't use\r\n    /// </summary>\r\n    /// <param name=\"position\"></param>\r\n    /// <param name=\"title\"></param>\r\n    /// <returns></returns>\r\n    private bool TestPosition( Vector3 position, string title )\r\n    {\r\n        var testTrace = BuildTrace( _shrunkenBounds, position, position );\r\n\r\n        if ( testTrace.StartedSolid )\r\n        {\r\n            Log.Info( $\"[{RealTime.Now}]{title} {GameObject.Name} started solid at {position} against {testTrace.GameObject}\" );\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        if ( !ManuallyUpdate && Active && !PhysicallySimulated ) // If we're not physically simulated we Move inside of OnFixedUpdate\r\n            Move( ActiveAutoTimeDelta );\r\n    }\r\n\r\n    protected override void OnDestroy()\r\n    {\r\n        base.OnDestroy();\r\n\r\n        Collider?.Destroy();\r\n        Body?.Destroy();\r\n    }\r\n\r\n    protected override void OnDisabled()\r\n    {\r\n        base.OnDisabled();\r\n\r\n        Collider?.Enabled = false;\r\n        Body?.Enabled = false;\r\n    }\r\n\r\n    protected override void OnEnabled()\r\n    {\r\n        base.OnEnabled();\r\n\r\n        Collider?.Enabled = true;\r\n        Body?.Enabled = true;\r\n    }\r\n\r\n    public override int ComponentVersion => 4;\r\n\r\n    [JsonUpgrader( typeof( ShrimpleCharacterController ), 3 )]\r\n    private static void UpdateTraceShape( JsonObject json )\r\n    {\r\n        if ( json.Remove( \"CylinderTrace\", out var newNode ) && newNode is JsonValue boolNode && boolNode.TryGetValue<bool>( out var isCylinder ) )\r\n            json[\"TraceShape\"] = isCylinder ? \"Cylinder\" : \"Box\";\r\n        else\r\n            json[\"TraceShape\"] = \"Box\";\r\n    }\r\n\r\n    [JsonUpgrader( typeof( ShrimpleCharacterController ), 4 )]\r\n    private static void UpdateMaxGroundAngle( JsonObject json )\r\n    {\r\n        // Old default was 60f, new default is (30f, 60f)\r\n        if ( json.TryGetPropertyValue( \"MaxGroundAngle\", out var oldValue ) && oldValue is JsonValue floatValue && floatValue.TryGetValue<float>( out var oldAngle ) )\r\n        {\r\n            json.Remove( \"MaxGroundAngle\" );\r\n            var startAngle = oldAngle * 0.75f;\r\n            json[\"MaxGroundAngle\"] = $\"{startAngle:F2} {oldAngle:F2}\";\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "fish.scc",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"Shrimple Character Controller\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"scc\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"fish\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"fish.scc\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"26\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-06-27T12:22:40.9761893Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.111.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.111.0\")]"
        },
        {
            "Ident": "fish.scc",
            "Path": "code/ShrimpleCharacterController.cs",
            "FileName": "ShrimpleCharacterController.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 301969,
            "IsPrivate": false,
            "Code": "using System.Text.Json.Nodes;\r\n\r\nnamespace ShrimpleCharacterController;\r\n\r\n[Icon( \"nordic_walking\" )]\r\npublic partial class ShrimpleCharacterController : Component, IScenePhysicsEvents, ISceneEvent<IScenePhysicsEvents>, Component.ICollisionListener\r\n{\r\n    /// <summary>\r\n    /// Manually update this by calling Move() or let it always be simulated.\r\n    /// In physical mode, Move() drives the next physics step instead of running the trace-based path.\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    public bool ManuallyUpdate { get; set; } = false;\r\n\r\n    [Property]\r\n    [FeatureEnabled( \"Physical\" )]\r\n    public bool PhysicallySimulated\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n\r\n            if ( value )\r\n                CreateBody();\r\n            else\r\n                DestroyBody();\r\n        }\r\n    }\r\n\r\n    private bool isPhysical( object _ ) => !PhysicallySimulated;\r\n\r\n    [Property]\r\n    [Hide]\r\n    public GameObject BodyObject { get; protected set; }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Header( \"Collider\" )]\r\n    [Validate( nameof( isPhysical ), \"Physical mode enabled - controller can interact with and be pushed by physics objects\", LogLevel.Info )]\r\n    public Surface ColliderSurface\r\n    {\r\n        get => Collider.IsValid() ? Collider.Surface : null;\r\n        set\r\n        {\r\n            if ( Collider.IsValid() )\r\n                Collider.Surface = value;\r\n        }\r\n    }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    public float? ColliderElasticity\r\n    {\r\n        get => Collider.IsValid() ? Collider.Elasticity : null;\r\n        set\r\n        {\r\n            if ( Collider.IsValid() )\r\n                Collider.Elasticity = value;\r\n        }\r\n    }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [ShowIf( \"HidePhysicalComponents\", false )]\r\n    public Collider Collider { get; protected set; }\r\n\r\n    /// <summary>\r\n    /// Kilos, tweak based on your surroundings, default boxes are like 600kg!\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Header( \"Rigid Body\" )]\r\n    public float BodyMassOverride\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n\r\n            if ( Body.IsValid() )\r\n                Body.MassOverride = value;\r\n        }\r\n    } = 100f;\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    public RigidbodyFlags BodyRigidbodyFlags\r\n    {\r\n        get => Body.IsValid() ? Body.RigidbodyFlags : 0;\r\n        set\r\n        {\r\n            if ( Body.IsValid() )\r\n                Body.RigidbodyFlags = value;\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Enable Continuous Collision Detection (CCD) for the physics body.\r\n    /// Useful for fast-moving characters to prevent tunneling through thin objects.\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Title( \"Enable CCD\" )]\r\n    public bool EnableCCD\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            if ( Body.IsValid() )\r\n                Body.EnhancedCcd = value;\r\n        }\r\n    }\r\n\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [ShowIf( \"HidePhysicalComponents\", false )]\r\n    public Rigidbody Body { get; protected set; }\r\n\r\n    /// <summary>\r\n    /// Hide the <see cref=\"Body\"/> and <see cref=\"Collider\"/> components in the inspector\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Physical\" )]\r\n    [Title( \"Hide Components\" )]\r\n    [Space]\r\n    [Validate( nameof( isPhysical ), \"Make sure to go over the other features to see any warning regarding physical simulation!\", LogLevel.Warn )]\r\n    public bool HidePhysicalComponents\r\n    {\r\n        get;\r\n        protected set\r\n        {\r\n            field = value;\r\n\r\n            if ( value )\r\n            {\r\n                if ( Body.IsValid() )\r\n                    Body.Flags |= ComponentFlags.Hidden;\r\n                if ( Collider.IsValid() )\r\n                    Collider.Flags |= ComponentFlags.Hidden;\r\n            }\r\n            else\r\n            {\r\n                if ( Body.IsValid() )\r\n                    Body.Flags &= ~ComponentFlags.Hidden;\r\n                if ( Collider.IsValid() )\r\n                    Collider.Flags &= ~ComponentFlags.Hidden;\r\n            }\r\n        }\r\n    } = true;\r\n\r\n    [Feature( \"Physical\" )]\r\n    [Button( \"Recreate Components\", \"sync\" )]\r\n    public void RefreshBody() => CreateBody( true );\r\n\r\n    /// <summary>\r\n    /// If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    public bool ScaleAgainstWalls { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Rotate the trace with the gameobject\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [Validate( nameof( isPhysical ), \"Physical mode uses a capsule collider to prevent snagging on terrain.\", LogLevel.Info )]\r\n    public bool RotateWithGameObject { get; set; } = true;\r\n\r\n    public enum TraceType\r\n    {\r\n        /// <summary>\r\n        /// This has no drawback, enjoy!\r\n        /// </summary>\r\n        [Icon( \"\ud83d\udce6\" )]\r\n        Box,\r\n        /// <summary>\r\n        /// This is a PHYSICAL TRACE, so it's more expensive than the normal box or sphere trace\r\n        /// </summary>\r\n        [Icon( \"\ud83d\udee2\" )]\r\n        Cylinder,\r\n        /// <summary>\r\n        /// This will disable <see cref=\"StepsEnabled\"/> as it's impossible to get the angle reliably\r\n        /// </summary>\r\n        [Icon( \"\ud83c\udfd0\" )]\r\n        Sphere,\r\n        /// <summary>\r\n        /// No drawbacks, box but you can define exact bounds\r\n        /// </summary>\r\n        [Icon( \"\ud83e\uddca\" )]\r\n        Bounds\r\n    }\r\n\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [HideIf( nameof( PhysicallySimulated ), true )]\r\n    public TraceType TraceShape\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            CreateCollider();\r\n        }\r\n    } = TraceType.Box;\r\n\r\n    /// <summary>\r\n    /// Width of our trace\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [ShowIf( nameof( _showWidthHeight ), true )]\r\n    [Range( 1f, 128f, false, true )]\r\n    [Sync]\r\n    public float TraceWidth\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            UpdateCollider();\r\n        }\r\n    } = 16f;\r\n\r\n    // Show width/height if Physical mode OR if not using Bounds shape\r\n    bool _showWidthHeight => PhysicallySimulated || TraceShape != TraceType.Bounds;\r\n    // Show height if Physical mode OR if using Box/Cylinder shape\r\n    bool _showHeight => PhysicallySimulated || TraceShape == TraceType.Box || TraceShape == TraceType.Cylinder;\r\n\r\n    /// <summary>\r\n    /// Height of our trace\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [ShowIf( nameof( _showHeight ), true )]\r\n    [Range( 1f, 256f, false, true )]\r\n    [Sync]\r\n    public float TraceHeight\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            UpdateCollider();\r\n        }\r\n    } = 72f;\r\n\r\n    /// <summary>\r\n    /// Bounds of our trace\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [ShowIf( \"TraceShape\", TraceType.Bounds )]\r\n    [Sync]\r\n    public BBox TraceBounds\r\n    {\r\n        get => TraceShape == TraceType.Bounds ? field : Bounds;\r\n        set\r\n        {\r\n            field = value;\r\n            RebuildBounds();\r\n            UpdateCollider();\r\n        }\r\n    } = BBox.FromPositionAndSize( Vector3.Up * 36f, new Vector3( 32f, 32f, 72f ) );\r\n\r\n    private bool ignoreTagsAndPhysical( TagSet tagSet ) => tagSet.IsEmpty || !PhysicallySimulated;\r\n\r\n    /// <summary>\r\n    /// Which tags it should ignore\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [Validate( nameof( ignoreTagsAndPhysical ), \"Contoller is physical! Ignore tags must be set on the collision matrix\", LogLevel.Warn )]\r\n    public TagSet IgnoreTags { get; set; } = new TagSet();\r\n\r\n    /// <summary>\r\n    /// Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Trace\" )]\r\n    [Range( 1, 20, true, true )]\r\n    public int MaxBounces { get; set; } = 5;\r\n\r\n    /// <summary>\r\n    /// Acceleration and Deceleration\r\n    /// </summary>\r\n    [FeatureEnabled( \"Acceleration\" )]\r\n    [Property]\r\n    public bool AccelerationEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How fast you accelerate while on the ground (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float GroundAcceleration { get; set; } = 1000f;\r\n\r\n    /// <summary>\r\n    /// How fast you accelerate while in the air (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float AirAcceleration { get; set; } = 300f;\r\n\r\n    /// <summary>\r\n    /// Use the fixed acceleration value instead of curves\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    public bool FixedAcceleration { get; set; } = true;\r\n\r\n    protected static readonly Curve DefaultAcceleration = new Curve( new List<Curve.Frame>\r\n    {\r\n        new Curve.Frame(0f, 0.2f, 0f, 1.5f),\r\n        new Curve.Frame(1f, 1f, 0f, 0f)\r\n    } )\r\n    {\r\n        TimeRange = new Vector2( 0f, 500f ),\r\n        ValueRange = new Vector2( 0f, 1f ),\r\n    };\r\n\r\n    /// <summary>\r\n    /// How much acceleration based on the current velocity<br/>\r\n    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>\r\n    /// Y axis *= Acceleration/><br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [ValueRange( 0f, 1f, false )]\r\n    [HideIf( \"FixedAcceleration\", true )]\r\n    public Curve AccelerationCurve { get; set; } = DefaultAcceleration;\r\n\r\n    /// <summary>\r\n    /// How fast you decelerate while on the ground (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float GroundDeceleration { get; set; } = 1500f;\r\n\r\n    /// <summary>\r\n    /// How fast you decelerate while in the air (Units per second)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [Range( 0f, 3000f, false )]\r\n    public float AirDeceleration { get; set; } = 0f;\r\n\r\n    /// <summary>\r\n    /// Use the fixed deceleration value instead of curves\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    public bool FixedDeceleration { get; set; } = true;\r\n\r\n    protected static readonly Curve DefaultDeceleration = new Curve( new List<Curve.Frame>\r\n    {\r\n        new Curve.Frame(0f, 1f, 0f, -2.5f),\r\n        new Curve.Frame(1f, 0.2f, 0f, 0f)\r\n    } )\r\n    {\r\n        TimeRange = new Vector2( 0f, 500f ),\r\n        ValueRange = new Vector2( 0f, 1f ),\r\n    };\r\n\r\n    /// <summary>\r\n    /// How much deceleration based on the current velocity<br/>\r\n    /// X axis = Current Velocity (Maxes at 500 by default but you can modify)<br/>\r\n    /// Y axis *= Deceleration/><br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    [ValueRange( 0f, 1f, false )]\r\n    [HideIf( \"FixedDeceleration\", true )]\r\n    public Curve DecelerationCurve { get; set; } = DefaultDeceleration;\r\n\r\n    /// <summary>\r\n    /// Do we ignore the friction of the surface you're standing on or not?\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Acceleration\" )]\r\n    public bool IgnoreGroundSurface { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// Use Time.Delta as the time step when automatically simulating (disable to set a fixed TimeDelta)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    [HideIf( nameof( ManuallyUpdate ), true )]\r\n    public bool UseTimeDelta { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Fixed time step used when automatically simulating and UseTimeDelta is disabled\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Options\" )]\r\n    [ShowIf( nameof( _showTimeDelta ), true )]\r\n    public float TimeDelta { get; set; } = 1f / 60f;\r\n\r\n    private bool _showTimeDelta => !ManuallyUpdate && !UseTimeDelta;\r\n    private float ActiveAutoTimeDelta => UseTimeDelta ? Time.Delta : TimeDelta;\r\n\r\n    /// <summary>\r\n    /// Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip)\r\n    /// </summary>\r\n    [Property]\r\n    [Group( \"Movement\" )]\r\n    public bool IgnoreZ { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Do we ignore Z when it's near 0 (So that gravity affects you when not moving)\r\n    /// </summary>\r\n    [Property]\r\n    [Title( \"Ignore Z When Zero\" )]\r\n    [Group( \"Movement\" )]\r\n    [HideIf( \"IgnoreZ\", true )]\r\n    public bool IgnoreZWhenZero { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Tolerance from a 90\u00b0 surface before it's considered a wall (Ex. Tolerance 1 = Between 89\u00b0 and 91\u00b0 can be a wall, 0.1 = 89.9\u00b0 to 90.1\u00b0)\r\n    /// </summary>\r\n    [Group( \"Movement\" )]\r\n    [Property]\r\n    [Range( 0f, 10f, false )]\r\n    public float WallTolerance { get; set; } = 1f;\r\n\r\n    /// <summary>\r\n    /// Player feels like it's gripping walls too much? Try more Grip Factor Reduction!\r\n    /// </summary>\r\n    [Group( \"Movement\" )]\r\n    [Property]\r\n    [Range( 1f, 10f, true )]\r\n    [Validate( nameof( isPhysical ), \"Physical mode uses collider friction for wall interaction\", LogLevel.Info )]\r\n    public float GripFactorReduction { get; set; } = 1f;\r\n\r\n    /// <summary>\r\n    /// Bouncing off walls and ground on collision\r\n    /// </summary>\r\n    [FeatureEnabled( \"Bouncing\" )]\r\n    [Property]\r\n    public bool BouncingEnabled { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// How much the MoveHelper will \"bounce\" off walls when colliding with them<br/>\r\n    /// (0f = No bounce, 1f = Full bounce)<br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    [Range( 0f, 1f, true )]\r\n    [Validate( nameof( isPhysical ), \"Contoller is physical! Elasticity must be done through the collider\", LogLevel.Error )]\r\n    public float HorizontalElasticity { get; set; } = 0f;\r\n\r\n    /// <summary>\r\n    /// How much the MoveHelper will \"bounce\" off the ground when colliding with it<br/>\r\n    /// (0f = No bounce, 1f = Full bounce)<br/>\r\n    /// <see cref=\"GroundStickEnabled\"/> will override this value when sticking to the ground<br/>\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    [Range( 0f, 1f, true )]\r\n    public float VerticalElasticity { get; set; } = 0f;\r\n\r\n    /// <summary>\r\n    /// Include the ground elasticity when bouncing off the ground<br/>\r\n    /// Make sure the ground surface doesn't have a very low elasticity!\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    public bool IncludeGroundElasticity { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// Minimum velocity to allow bouncing\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Bouncing\" )]\r\n    [Range( 0f, 100f, true )]\r\n    public float ElasticityThreshold { get; set; } = 30f;\r\n\r\n    /// <summary>\r\n    /// Stick the MoveHelper to the ground (IsOnGround will default to false if disabled)\r\n    /// </summary>\r\n    [FeatureEnabled( \"GroundStick\" )]\r\n    [Property]\r\n    public bool GroundStickEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How steep terrain can be for you to stand on without slipping.<br/>\r\n    /// Min = Angle where velocity starts to drop off<br/>\r\n    /// Max = Max angle where velocity is fully blocked (you can still slide down with gravity)\r\n    /// </summary>\r\n    [Property, Step( 0.1f )]\r\n    [Feature( \"GroundStick\" )]\r\n    public RangedFloat MaxGroundAngle\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            if ( value.Range == RangedFloat.RangeType.Between )\r\n            {\r\n                var min = MathX.Clamp( value.Min, 0f, 89f );\r\n                var max = MathX.Clamp( value.Max, 0f, 89f );\r\n\r\n                field = new RangedFloat( MathF.Min( min, max ), MathF.Max( min, max ) );\r\n            }\r\n            else\r\n            {\r\n                field = new RangedFloat( MathX.Clamp( value.FixedValue, 0f, 89f ) );\r\n            }\r\n        }\r\n    } = new RangedFloat( 30f, 60f );\r\n\r\n    /// <summary>\r\n    /// How far from the ground the MoveHelper is going to stick (Useful for going down stairs!)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    [Range( 1f, 32f, false )]\r\n    public float GroundStickDistance { get; set; } = 12f;\r\n\r\n    /// <summary>\r\n    /// Stick to any <see cref=\"GroundObject\"/> when standing on it\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    public bool StickToPlatforms { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// When the surface you're on has a surface velocity, apply it to the controller\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    public bool ApplySurfaceVelocity { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// When the surface you're on is slippery, scale the max ground angled relative to the friction<br/>\r\n    /// Example: Ice with friction 0.5f, MaxGroundAngle will be 30f instead of 60f\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"GroundStick\" )]\r\n    public bool ScaleGroundAngleBySurfaceFriction { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Enable steps climbing (+1 Trace call)\r\n    /// </summary>\r\n    [FeatureEnabled( \"Steps\" )]\r\n    [Property]\r\n    public bool StepsEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How high steps can be for you to climb on\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    [Range( 1f, 32f, false )]\r\n    public float StepHeight { get; set; } = 12f;\r\n\r\n    /// <summary>\r\n    /// How deep it checks for steps (Minimum depth)\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    [Range( 0.1f, 8f, false )]\r\n    public float StepDepth { get; set; } = 2f;\r\n\r\n    /// <summary>\r\n    /// Tolerance from a 90\u00b0 surface before it's considered a valid step (Ex. Tolerance 1 = Between 89\u00b0 and 91\u00b0 can be a step, 0.1 = 89.9\u00b0 to 90.1\u00b0)\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    [Range( 0f, 10f, false )]\r\n    public float StepTolerance { get; set; } = 1f;\r\n\r\n    /// <summary>\r\n    /// Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain)\r\n    /// </summary>\r\n    [Feature( \"Steps\" )]\r\n    [Property]\r\n    public bool PseudoStepsEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!)\r\n    /// </summary>\r\n    [FeatureEnabled( \"Push\" )]\r\n    [Property]\r\n    public bool PushEnabled { get; set; } = false;\r\n\r\n    /// <summary>\r\n    /// Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Push\" )]\r\n    [Validate( nameof( isPhysical ), \"Contoller is physical! Make sure the tags are ignored on the collision matrix\", LogLevel.Warn )]\r\n    [Sync]\r\n    public Dictionary<string, float> PushTagsWeight\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            _pushTags = BuildPushTags();\r\n        }\r\n    } = new Dictionary<string, float>() { { \"player\", 1f } };\r\n\r\n    /// <summary>\r\n    /// Apply gravity to this MoveHelper when not on the ground\r\n    /// </summary>\r\n    [FeatureEnabled( \"Gravity\" )]\r\n    [Property]\r\n    public bool GravityEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// Use the scene's gravity or our own\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    public bool UseSceneGravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    } = true;\r\n\r\n    /// <summary>\r\n    /// Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    [HideIf( \"UseSceneGravity\", true )]\r\n    public bool UseVectorGravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    }\r\n\r\n    private bool _usingFloatGravity => !UseVectorGravity && !UseSceneGravity;\r\n    private bool _usingVectorGravity => UseVectorGravity && !UseSceneGravity;\r\n\r\n    /// <summary>\r\n    /// Units per second squared (Default is -850f)\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    [Range( -2000, 2000, false )]\r\n    [ShowIf( \"_usingFloatGravity\", true )]\r\n    public float Gravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    } = -850f;\r\n\r\n    /// <summary>\r\n    /// Units per second squared (Default is 0f, 0f, -850f)<br/>\r\n    /// Changes which way <see cref=\"GroundStickEnabled\"/> sticks to the ground\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Gravity\" )]\r\n    [ShowIf( \"_usingVectorGravity\", true )]\r\n    public Vector3 VectorGravity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            field = value;\r\n            BuildGravity();\r\n        }\r\n    } = new Vector3( 0f, 0f, -850f );\r\n\r\n    private Vector3 _appliedGravity;\r\n    public Vector3 GravityNormal { get; private set; }\r\n    public Vector3 AppliedGravity => _appliedGravity;\r\n\r\n    /// <summary>\r\n    /// Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck)\r\n    /// </summary>\r\n    [FeatureEnabled( \"Unstuck\" )]\r\n    [Property]\r\n    public bool UnstuckEnabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// How many trace calls it will attempt to get the MoveHelper unstuck\r\n    /// </summary>\r\n    [Property]\r\n    [Feature( \"Unstuck\" )]\r\n    [Range( 1, 100, false )]\r\n    [Step( 1f )]\r\n    public int MaxUnstuckTries { get; set; } = 40;\r\n\r\n    /// <summary>\r\n    /// The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside)\r\n    /// </summary>\r\n    [Sync] public Vector3 WishVelocity { get; set; }\r\n\r\n    /// <summary>\r\n    /// The resulting velocity after the simulation is done (Units per second)\r\n    /// </summary>\r\n    [Sync]\r\n    public Vector3 Velocity\r\n    {\r\n        get;\r\n        set\r\n        {\r\n            if ( PhysicallySimulated && Body.IsValid() )\r\n                Body.Velocity = value;\r\n\r\n            field = value;\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Velocity controlled by outside factors, such as knockback, rootmotion, etc.\r\n    /// It is only applied to our final position and doesn't affect our Velocity.\r\n    /// It should be handled outside the controller.\r\n    /// </summary>\r\n    [Sync] public Vector3 ExternalVelocity { get; set; }\r\n\r\n    /// <summary>\r\n    /// Is the MoveHelper currently touching the ground\r\n    /// </summary>\r\n    [Sync] public bool IsOnGround { get; set; }\r\n\r\n    /// <summary>\r\n    /// The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false)\r\n    /// </summary>\r\n    public Vector3 GroundNormal { get; private set; } = Vector3.Zero;\r\n\r\n    /// <summary>\r\n    /// The current ground angle you're standing on (Always 0f if IsOnGround false)\r\n    /// </summary>\r\n    public float GroundAngle => Vector3.GetAngle( GroundNormal, -GravityNormal );\r\n\r\n    /// <summary>\r\n    /// Gets the velocity multiplier for a given slope angle based on MaxGroundAngle range.<br/>\r\n    /// Returns 1.0 below the start angle, 0.0 at/above the max angle, and lerps between.\r\n    /// </summary>\r\n    public float GetSlopeVelocityMultiplier( float angle )\r\n    {\r\n        if ( angle <= MaxGroundAngle.Min ) return 1f;\r\n        if ( angle >= MaxGroundAngle.Max ) return 0f;\r\n        return 1f - MathX.LerpInverse( angle, MaxGroundAngle.Min, MaxGroundAngle.Max );\r\n    }\r\n\r\n    /// <summary>\r\n    /// Whether the given angle is considered standable (below max ground angle)\r\n    /// </summary>\r\n    public bool IsAngleStandable( float angle ) => angle <= MaxGroundAngle.Max * ( ScaleGroundAngleBySurfaceFriction ? GroundSurface?.Friction ?? 1f : 1f );\r\n\r\n    /// <summary>\r\n    /// The current surface you're standing on\r\n    /// </summary>\r\n    public Surface GroundSurface { get; private set; }\r\n\r\n    /// <summary>\r\n    /// The gameobject you're currently standing on\r\n    /// </summary>\r\n    public GameObject GroundObject { get; set; }\r\n\r\n    /// <summary>\r\n    /// Is the MoveHelper currently pushing against a wall\r\n    /// </summary>\r\n    public bool IsPushingAgainstWall { get; private set; }\r\n\r\n    /// <summary>\r\n    /// The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false)\r\n    /// </summary>\r\n    public Vector3 WallNormal { get; private set; } = Vector3.Zero;\r\n\r\n    /// <summary>\r\n    /// The gameobject you're currently pushing on\r\n    /// </summary>\r\n    public GameObject WallObject { get; set; }\r\n\r\n    /// <summary>\r\n    /// Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false)\r\n    /// </summary>\r\n    [Sync] public bool IsSlipping { get; private set; } // TODO IMPLEMENT\r\n\r\n    /// <summary>\r\n    /// The MoveHelper is stuck and we can't get it out\r\n    /// </summary>\r\n    [Sync] public bool IsStuck { get; private set; }\r\n\r\n    /// <summary>\r\n    /// To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later\r\n    /// </summary>\r\n    public float SkinWidth => Math.Min( Math.Max( 0.1f, TraceWidth * 0.05f ), GroundStickDistance );\r\n\r\n    public float AppliedWidth => TraceWidth * WorldScale.x; // The width of the MoveHelper in world units\r\n    public float AppliedDepth => TraceWidth * WorldScale.y; // The depth of the MoveHelper in world units\r\n    public float AppliedHeight => TraceShape == TraceType.Sphere ? AppliedWidth :\r\n        TraceShape == TraceType.Bounds ? TraceBounds.Size.z * WorldScale.z : TraceHeight * WorldScale.z; // The height of the MoveHelper in world units\r\n    private Vector3 _upAxis => RotateWithGameObject ? WorldRotation.Up : Vector3.Up;\r\n    private Vector3 _offset => ( RotateWithGameObject ? WorldRotation.Up : Vector3.Up ) * ( TraceShape == TraceType.Sphere || TraceShape == TraceType.Bounds ? 0f : AppliedHeight / 2f ); // The position of the MoveHelper in world units\r\n\r\n    /// <summary>\r\n    /// The bounds of this MoveHelper generated from the TraceWidth and TraceHeight\r\n    /// </summary>\r\n    public BBox Bounds { get; set; }\r\n    private BBox _shrunkenBounds;\r\n    private string[] _pushTags;\r\n    private Vector3 _lastVelocity;\r\n    private float _minimumTolerance => MathX.Clamp( Time.Delta / 2f, 0.005f, 0.1f ); // Floating precision tolerance, too high if used inside of OnUpdate so tied to update rate\r\n\r\n    /// <summary>\r\n    /// If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us\r\n    /// </summary>\r\n    public ShrimpleCharacterController UnstuckTarget;\r\n\r\n    public Action<ShrimpleCollisionResult> OnCollide { get; set; }\r\n    public bool IsOnPlatform => IsOnGround && GroundStickEnabled && !IsSlipping && StickToPlatforms && GroundObject.IsValid();\r\n    public Vector3 PlatformVelocity => IsOnPlatform ? GroundObject.GetComponent<Collider>()?.GetVelocityAtPoint( WorldPosition ) ?? Vector3.Zero : Vector3.Zero;\r\n    public bool IsOnSurfaceWithVelocity => IsOnGround && GroundStickEnabled && !IsSlipping && ApplySurfaceVelocity && GroundObject.IsValid();\r\n    public Vector3 SurfaceVelocity => IsOnSurfaceWithVelocity ? GroundObject.GetComponent<Collider>()?.SurfaceVelocity * GroundObject.WorldRotation ?? Vector3.Zero : Vector3.Zero;\r\n    public Vector3 GroundVelocity => PlatformVelocity + SurfaceVelocity;\r\n\r\n    protected override void OnStart()\r\n    {\r\n        RebuildBounds();\r\n        BuildGravity();\r\n        _pushTags = BuildPushTags();\r\n    }\r\n\r\n    protected override void DrawGizmos()\r\n    {\r\n        if ( Gizmo.IsSelected )\r\n        {\r\n            Gizmo.GizmoDraw draw = Gizmo.Draw;\r\n            draw.Color = Color.Blue;\r\n\r\n            if ( TraceShape == TraceType.Box )\r\n                draw.LineBBox( Bounds.Translate( Vector3.Up * Bounds.Size.z / 2f ) );\r\n            if ( TraceShape == TraceType.Cylinder )\r\n                draw.LineCylinder( Vector3.Zero, Vector3.Up * ( Bounds.Maxs.z - Bounds.Mins.z ), Bounds.Maxs.x, Bounds.Maxs.x, 24 );\r\n            if ( TraceShape == TraceType.Sphere )\r\n                draw.LineSphere( Vector3.Up * Bounds.Maxs.x, Bounds.Maxs.x );\r\n            if ( TraceShape == TraceType.Bounds )\r\n                draw.LineBBox( TraceBounds );\r\n\r\n            if ( !UseSceneGravity && UseVectorGravity )\r\n            {\r\n                draw.Color = Color.Red;\r\n                draw.LineThickness = 5f;\r\n                draw.IgnoreDepth = true;\r\n                draw.Arrow( Vector3.Zero, GravityNormal * 50f );\r\n            }\r\n        }\r\n    }\r\n\r\n    private BBox BuildBounds()\r\n    {\r\n        if ( TraceShape == TraceType.Bounds )\r\n            return new BBox( TraceBounds.Mins * GameObject.WorldScale, TraceBounds.Maxs * GameObject.WorldScale );\r\n\r\n        var x = GameObject.WorldScale.x;\r\n        var y = GameObject.WorldScale.y;\r\n        var z = GameObject.WorldScale.z;\r\n\r\n        var width = TraceWidth / 2f * x;\r\n        var depth = TraceWidth / 2f * y;\r\n        var halfHeight = TraceHeight / 2f * z;\r\n\r\n        // For cylinders we want the bounds to start at Z = 0 and extend up to the full height\r\n        if ( TraceShape == TraceType.Cylinder )\r\n            return new BBox( new Vector3( -width, -depth, 0f ), new Vector3( width, depth, halfHeight * 2f ) );\r\n\r\n        return new BBox( new Vector3( -width, -depth, -halfHeight ), new Vector3( width, depth, halfHeight ) );\r\n    }\r\n\r\n    private void RebuildBounds()\r\n    {\r\n        Bounds = BuildBounds();\r\n        _shrunkenBounds = Bounds.Grow( -SkinWidth );\r\n    }\r\n\r\n    private void BuildGravity()\r\n    {\r\n        _appliedGravity = UseSceneGravity ? Scene.PhysicsWorld.Gravity : UseVectorGravity ? VectorGravity : new Vector3( 0f, 0f, Gravity );\r\n        GravityNormal = _appliedGravity.Normal;\r\n    }\r\n\r\n    private string[] BuildPushTags()\r\n    {\r\n        return PushTagsWeight.Keys.ToArray();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Casts the current bounds from to and returns the scene trace result\r\n    /// </summary>\r\n    /// <param name=\"bounds\"></param>\r\n    /// <param name=\"from\"></param>\r\n    /// <param name=\"to\"></param>\r\n    /// <param name=\"useCapsule\">Use a capsule shape instead of the defaults</param>\r\n    /// <returns></returns>\r\n    public SceneTraceResult BuildTrace( BBox bounds, Vector3 from, Vector3 to, bool useCapsule = false )\r\n    {\r\n        SceneTrace builder = new SceneTrace(); // Empty trace builder\r\n\r\n        if ( useCapsule )\r\n        {\r\n            var radius = bounds.Maxs.x;\r\n            var halfHeight = ( bounds.Maxs.z - bounds.Mins.z ) / 2f;\r\n            var hemiOffset = MathF.Max( halfHeight - radius, 0f );\r\n            var capsule = new Capsule( Vector3.Down * hemiOffset, Vector3.Up * hemiOffset, radius ); // Do not rotate Vector3, it's rotated 17 lines down!\r\n            builder = Game.SceneTrace.Capsule( capsule, from, to );\r\n        }\r\n        else\r\n        {\r\n            if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )\r\n                builder = Game.SceneTrace.Box( bounds, from, to );\r\n            if ( TraceShape == TraceType.Cylinder )\r\n                builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );\r\n            if ( TraceShape == TraceType.Sphere )\r\n                builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from + Vector3.Up * bounds.Maxs.x, to + Vector3.Up * bounds.Maxs.x );\r\n        }\r\n\r\n        builder = builder\r\n            .IgnoreGameObjectHierarchy( GameObject )\r\n            .WithoutTags( IgnoreTags );\r\n\r\n        if ( RotateWithGameObject )\r\n            builder = builder.Rotated( PhysicallySimulated ? Collider.WorldRotation : GameObject.WorldRotation );\r\n\r\n        return builder.Run();\r\n    }\r\n\r\n    private SceneTraceResult BuildPushTrace( BBox bounds, Vector3 from, Vector3 to )\r\n    {\r\n        SceneTrace builder = new SceneTrace(); // Empty trace builder\r\n\r\n        if ( TraceShape == TraceType.Box || TraceShape == TraceType.Bounds )\r\n            builder = Game.SceneTrace.Box( bounds, from, to );\r\n        if ( TraceShape == TraceType.Cylinder )\r\n            builder = Game.SceneTrace.Cylinder( bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to );\r\n        if ( TraceShape == TraceType.Sphere )\r\n            builder = Game.SceneTrace.Sphere( bounds.Maxs.x, from, to );\r\n\r\n        builder = builder\r\n            .IgnoreGameObjectHierarchy( GameObject )\r\n            .WithAnyTags( _pushTags ); // Check for only the push tags\r\n\r\n        if ( RotateWithGameObject )\r\n            builder = builder.Rotated( GameObject.WorldRotation );\r\n\r\n        return builder.Run();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Detach the MoveHelper from the ground and launch it somewhere (Units per second)\r\n    /// </summary>\r\n    /// <param name=\"amount\"></param>\r\n    public void Punch( in Vector3 amount )\r\n    {\r\n        var groundVel = GroundVelocity; // Capture before ungrounding\r\n        IsOnGround = false;\r\n\r\n        if ( PhysicallySimulated && Body.IsValid() )\r\n            Body.Velocity = Body.Velocity + amount + groundVel;\r\n\r\n        Velocity += amount + groundVel;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper\r\n    /// </summary>\r\n    /// <param name=\"manualUpdate\">Just calculate but don't update position</param>\r\n    public MoveHelperResult Move( bool manualUpdate = false ) => Move( Time.Delta, manualUpdate );\r\n\r\n    /// <summary>\r\n    /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper\r\n    /// </summary>\r\n    /// <param name=\"delta\">The time step</param>\r\n    /// <param name=\"manualUpdate\">Just calculate but don't update position</param>\r\n    public MoveHelperResult Move( float delta, bool manualUpdate = false )\r\n    {\r\n        // In physical mode the simulation runs inside the physics step callbacks.\r\n        // Queue this call so the next PrePhysicsStep/PostPhysicsStep drives the body with the requested delta.\r\n        if ( PhysicallySimulated )\r\n        {\r\n            if ( Body.IsValid() && !manualUpdate )\r\n            {\r\n                _physicalMoveDelta = delta;\r\n                _physicalMovePending = true;\r\n            }\r\n\r\n            return new MoveHelperResult( WorldPosition, Velocity );\r\n        }\r\n\r\n        var goalVelocity = CalculateGoalVelocity( delta ); // Calculate the goal velocity using our Acceleration and Deceleration values\r\n\r\n        // SIMULATE PUSH FORCES //\r\n        if ( PushEnabled )\r\n        {\r\n            var pushTrace = BuildPushTrace( Bounds, WorldPosition + _offset, WorldPosition ); // Build a trace but using the Push tags instead of the Ignore tags\r\n\r\n            if ( pushTrace.Hit ) // We're inside any of the push tags\r\n            {\r\n                foreach ( var tag in pushTrace.GameObject.Tags )\r\n                {\r\n                    if ( PushTagsWeight.TryGetValue( tag, out var tagWeight ) )\r\n                    {\r\n                        var otherPosition = pushTrace.GameObject.WorldPosition.WithZ( WorldPosition.z ); // Only horizontal pushing\r\n                        var pushDirection = ( otherPosition - WorldPosition ).Normal;\r\n                        var pushVelocity = pushDirection * tagWeight * 50f; // I find 50 u/s to be a good amount to push if the weight is 1.0 (!!!)\r\n\r\n                        goalVelocity -= pushVelocity;\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        var finalPosition = WorldPosition;\r\n\r\n        var moveHelperResult = CollideAndSlide( goalVelocity, finalPosition + _offset, delta ); // Simulate the MoveHelper\r\n\r\n        finalPosition = moveHelperResult.Position;\r\n        var finalVelocity = moveHelperResult.Velocity;\r\n        // SIMULATE GRAVITY //\r\n        if ( GravityEnabled && Gravity != 0f )\r\n        {\r\n            if ( !IsOnGround || IsSlipping || !GroundStickEnabled )\r\n            {\r\n                var gravity = AppliedGravity * delta;\r\n                var gravityResult = CollideAndSlide( gravity, moveHelperResult.Position, delta, gravityPass: true ); // Apply and simulate the gravity step\r\n\r\n                finalPosition = gravityResult.Position;\r\n                finalVelocity += gravityResult.Velocity;\r\n            }\r\n        }\r\n\r\n        if ( !ExternalVelocity.IsNearZeroLength )\r\n        {\r\n            moveHelperResult = CollideAndSlide( ExternalVelocity, finalPosition, delta );\r\n        }\r\n\r\n        finalPosition = moveHelperResult.Position - _offset; // Compensate for the offset we added at the beginning\r\n        _lastVelocity = Velocity * delta;\r\n\r\n        if ( !manualUpdate )\r\n        {\r\n            Velocity = finalVelocity;\r\n            WorldPosition = finalPosition + SurfaceVelocity * delta; // Actually updating the position is \"expensive\" so we only do it once at the end\r\n        }\r\n\r\n        return new MoveHelperResult( finalPosition, finalVelocity, moveHelperResult.Offset );\r\n    }\r\n\r\n    /// <summary>\r\n    /// Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity\r\n    /// </summary>\r\n    public struct MoveHelperResult\r\n    {\r\n        public Vector3 Position;\r\n        public Vector3 Velocity;\r\n        internal Vector3 Offset;\r\n        internal float Leftover = 1f;\r\n\r\n        public MoveHelperResult( Vector3 position, Vector3 velocity )\r\n        {\r\n            Position = position;\r\n            Velocity = velocity;\r\n        }\r\n\r\n        internal MoveHelperResult( Vector3 position, Vector3 velocity, Vector3 offset, float leftover = 1f )\r\n        {\r\n            Position = position;\r\n            Velocity = velocity;\r\n            Offset = offset;\r\n            Leftover = leftover;\r\n        }\r\n    }\r\n\r\n    bool _bounced = false;\r\n\r\n    private MoveHelperResult CollideAndSlide( Vector3 velocity, Vector3 position, float delta, int depth = 0, bool gravityPass = false ) =>\r\n        CollideAndSlide( new MoveHelperResult( position, velocity ), delta, depth, gravityPass );\r\n\r\n    private MoveHelperResult CollideAndSlide( MoveHelperResult current, float delta, int depth = 0, bool gravityPass = false )\r\n    {\r\n        if ( depth >= MaxBounces )\r\n            return current;\r\n\r\n        var velocity = current.Velocity * delta; // I like to set Velocity as units/second but we have to deal with units/tick here\r\n        var position = current.Position;\r\n\r\n        // GROUND AND UNSTUCK CHECK //\r\n        if ( depth == 0 ) // Only check for the first step since it's impossible to get stuck on other steps\r\n        {\r\n            var groundTrace = BuildTrace( _shrunkenBounds, position, position + AppliedGravity.Normal * ( GroundStickDistance + SkinWidth * 1.1f ) ); // Compensate for floating inaccuracy\r\n\r\n            if ( groundTrace.StartedSolid )\r\n            {\r\n                IsStuck = true;\r\n                if ( UnstuckEnabled )\r\n                {\r\n                    if ( UnstuckTarget == null )\r\n                    {\r\n                        IsStuck = !TryUnstuck( position, out var result );\r\n\r\n                        if ( !IsStuck )\r\n                        {\r\n                            position = result; // Update the new position\r\n\r\n                            if ( PhysicallySimulated && Body.IsValid() )\r\n                            {\r\n                                Body.WorldPosition = result;\r\n                                Body.Velocity = Vector3.Zero;\r\n                            }\r\n\r\n                            if ( groundTrace.GameObject != null )\r\n                                if ( groundTrace.GameObject.Components.TryGet<ShrimpleCharacterController>( out var otherHelper ) )\r\n                                    otherHelper.UnstuckTarget = this; // We already solved this, no need to unstuck the other helper\r\n                        }\r\n                        else\r\n                        {\r\n                            return new MoveHelperResult( position, Vector3.Zero ); // Mission failed, bail out!\r\n                        }\r\n                    }\r\n                    else\r\n                    {\r\n                        UnstuckTarget = null; // Alright the other MoveHelper got us unstuck so just do nothing\r\n                    }\r\n                }\r\n            }\r\n            else\r\n            {\r\n                var hasLanded = !IsOnGround && Vector3.Dot( Velocity, AppliedGravity ) >= 0f && groundTrace.Hit && groundTrace.Distance <= SkinWidth * 2f; // Wasn't on the ground and now is\r\n                var isGrounded = IsOnGround && groundTrace.Hit; // Was already on the ground and still is, this helps stick when going down stairs\r\n\r\n                IsOnGround = hasLanded || isGrounded;\r\n                GroundSurface = IsOnGround ? groundTrace.Surface : null;\r\n                GroundNormal = IsOnGround ? groundTrace.Normal : -AppliedGravity.Normal;\r\n                GroundObject = IsOnGround ? groundTrace.GameObject : null;\r\n                IsSlipping = IsOnGround && !IsAngleStandable( GroundAngle );\r\n\r\n                if ( IsSlipping && !gravityPass && Vector3.Dot( velocity, AppliedGravity ) < 0f )\r\n                    velocity = velocity.WithZ( 0f ); // If we're slipping ignore any extra velocity we had\r\n\r\n                // FIX: Clamp velocity when slipping to prevent buildup\r\n                // Project velocity onto the slope and limit accumulation\r\n                if ( IsSlipping )\r\n                {\r\n                    var slopeDir = Vector3.VectorPlaneProject( AppliedGravity.Normal, GroundNormal ).Normal;\r\n                    var slopeSpeed = Vector3.Dot( velocity, slopeDir );\r\n                    var maxSlipSpeed = AppliedGravity.Length * delta * 2f; // Reasonable max slip speed per frame\r\n\r\n                    if ( slopeSpeed > maxSlipSpeed )\r\n                    {\r\n                        // Clamp the velocity component along the slope\r\n                        velocity = velocity - slopeDir * ( slopeSpeed - maxSlipSpeed );\r\n                    }\r\n                }\r\n\r\n                if ( IsOnGround && GroundStickEnabled && !IsSlipping )\r\n                {\r\n                    position = groundTrace.EndPosition + -AppliedGravity.Normal * SkinWidth; // Place on the ground\r\n                    velocity = Vector3.VectorPlaneProject( velocity, GroundNormal ); // Follow the ground you're on without projecting Z\r\n                }\r\n\r\n                IsStuck = false;\r\n            }\r\n        }\r\n\r\n        if ( velocity.IsNearlyZero( _minimumTolerance ) ) // Not worth continuing, reduces small stutter\r\n        {\r\n            return new MoveHelperResult( position, Vector3.Zero, current.Offset );\r\n        }\r\n\r\n        var toTravel = velocity.Length * current.Leftover + SkinWidth;\r\n        var targetPosition = position + velocity.Normal * toTravel;\r\n        var travelTrace = BuildTrace( _shrunkenBounds, position, targetPosition );\r\n\r\n        if ( travelTrace.Hit )\r\n        {\r\n            var travelled = velocity.Normal * Math.Max( travelTrace.Distance - SkinWidth, 0f );\r\n            var leftover = velocity - travelled; // How much leftover velocity still needs to be simulated\r\n            var speed = leftover.Length;\r\n            var angle = Vector3.GetAngle( -AppliedGravity.Normal, travelTrace.Normal );\r\n\r\n            if ( toTravel >= SkinWidth && travelTrace.Distance < SkinWidth )\r\n                travelled = Vector3.Zero;\r\n\r\n            var elasticityDirection = MathX.Lerp( HorizontalElasticity, VerticalElasticity, angle / 90f );\r\n            var elasticity = PhysicallySimulated ? 0f : BouncingEnabled ?\r\n                velocity.Length / delta <= ElasticityThreshold ?\r\n                    0f : elasticityDirection * ( IncludeGroundElasticity ? GroundSurface?.Elasticity ?? 1f : 1f )\r\n                : 0f; // Allah help me format this better\r\n\r\n            if ( IsAngleStandable( angle ) ) // Terrain we can walk on\r\n            {\r\n                var projectedLeftover = leftover;\r\n\r\n                if ( gravityPass || !IsOnGround )\r\n                    projectedLeftover = Vector3.VectorPlaneProject( projectedLeftover, travelTrace.Normal ); // Don't project the vertical velocity after landing else it boosts your horizontal velocity\r\n                else\r\n                    projectedLeftover = projectedLeftover.ProjectAndScale( travelTrace.Normal ); // Project the velocity along the terrain\r\n\r\n                // Apply slope velocity reduction only when going uphill (against gravity)\r\n                var isGoingUphill = Vector3.Dot( projectedLeftover, AppliedGravity ) < 0f;\r\n                if ( isGoingUphill )\r\n                {\r\n                    var slopeMultiplier = GetSlopeVelocityMultiplier( angle );\r\n                    projectedLeftover *= slopeMultiplier;\r\n                }\r\n\r\n                if ( elasticity > 0 )\r\n                    leftover = Vector3.Lerp( projectedLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );\r\n                else\r\n                    leftover = projectedLeftover;\r\n\r\n                IsPushingAgainstWall = false;\r\n                WallObject = null;\r\n            }\r\n            else\r\n            {\r\n                var climbedStair = false;\r\n\r\n                if ( angle >= 90f - WallTolerance && angle <= 90f + WallTolerance ) // Check for walls\r\n                    IsPushingAgainstWall = true; // We're pushing against a wall\r\n\r\n                if ( StepsEnabled )\r\n                {\r\n                    var isStep = angle >= 90f - StepTolerance && angle <= 90f + StepTolerance;\r\n\r\n                    if ( isStep || PseudoStepsEnabled ) // Check for steps\r\n                    {\r\n                        if ( IsOnGround ) // Stairs VVV\r\n                        {\r\n                            var stepHorizontal = Vector3.VectorPlaneProject( velocity.Normal, AppliedGravity.Normal ).Normal * StepDepth; // How far in front we're looking for steps\r\n                            var stepVertical = -AppliedGravity.Normal * ( StepHeight + SkinWidth ); // How high we're looking for steps + Some to compensate for floating inaccuracy\r\n                            var stepTrace = BuildTrace( _shrunkenBounds, travelTrace.EndPosition + stepHorizontal + stepVertical, travelTrace.EndPosition + stepHorizontal );\r\n                            var stepAngle = Vector3.GetAngle( stepTrace.Normal, -AppliedGravity.Normal );\r\n\r\n                            if ( !stepTrace.StartedSolid && stepTrace.Hit && IsAngleStandable( stepAngle ) ) // We found a step!\r\n                            {\r\n                                if ( isStep || !IsSlipping && PseudoStepsEnabled )\r\n                                {\r\n                                    var stepDistance = stepTrace.EndPosition - travelTrace.EndPosition;\r\n                                    var stepTravelled = -AppliedGravity.Normal * stepDistance;\r\n                                    position += stepTravelled; // Offset our position by the height of the step climbed\r\n                                    climbedStair = true;\r\n\r\n                                    IsPushingAgainstWall = false; // Nevermind, we're not against a wall, we climbed a step!\r\n                                    WallObject = null;\r\n                                }\r\n                            }\r\n                        }\r\n                    }\r\n                }\r\n\r\n                if ( IsPushingAgainstWall )\r\n                {\r\n                    // Scale our leftover velocity based on the angle of approach relative to the wall\r\n                    // (Perpendicular = 0%, Parallel = 100%)\r\n                    var scale = ScaleAgainstWalls ? 1f - Vector3.Dot( -travelTrace.Normal.Normal / GripFactorReduction, velocity.Normal ) : 1f;\r\n                    var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal.Normal ) : leftover.ProjectAndScale( travelTrace.Normal.Normal );\r\n\r\n                    if ( elasticity > 0 )\r\n                        leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );\r\n                    else\r\n                        leftover = ( wallLeftover * scale ).WithZ( wallLeftover.z );\r\n\r\n                    WallObject = travelTrace.GameObject;\r\n                    WallNormal = travelTrace.Normal;\r\n                }\r\n                else\r\n                {\r\n                    if ( !climbedStair )\r\n                    {\r\n                        var scale = IsSlipping ? 1f : 1f - Vector3.Dot( -travelTrace.Normal / GripFactorReduction, velocity.Normal );\r\n                        var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject( leftover, travelTrace.Normal ) * scale : leftover.ProjectAndScale( travelTrace.Normal );\r\n\r\n                        if ( elasticity > 0 )\r\n                            leftover = Vector3.Lerp( wallLeftover, Vector3.Reflect( leftover, travelTrace.Normal ), elasticity, false );\r\n                        else\r\n                            leftover = wallLeftover;\r\n                    }\r\n\r\n                }\r\n\r\n\r\n            }\r\n\r\n            var previousVelocity = velocity;\r\n            var leftoverSpeed = leftover.Length / speed; // How much speed we had left after the collision\r\n            velocity = leftover.Normal * velocity.Length * leftoverSpeed;\r\n\r\n            if ( travelled.Length <= _minimumTolerance && leftover.Length <= _minimumTolerance )\r\n                return new MoveHelperResult( position + travelled, velocity / delta, current.Offset );\r\n\r\n            if ( elasticity > 0 )\r\n                _bounced = true;\r\n\r\n            var offset = position - current.Position;\r\n            var newResult = CollideAndSlide( new MoveHelperResult( position + travelled, velocity / delta, offset, leftover.Length / velocity.Length ), delta, depth + 1, gravityPass ); // Simulate another bounce for the leftover velocity from the latest position\r\n\r\n            if ( depth == 0 && !gravityPass )\r\n            {\r\n                var collision = new ShrimpleCollisionResult()\r\n                    .WithHitNormal( travelTrace.Normal )\r\n                    .WithHitPosition( travelTrace.HitPosition )\r\n                    .WithAngle( angle )\r\n                    .WithHitObject( travelTrace.GameObject )\r\n                    .WithHitSurface( travelTrace.Surface )\r\n                    .WithHitVelocityBefore( previousVelocity / delta )\r\n                    .WithHitVelocityAfter( newResult.Velocity );\r\n\r\n                OnCollide?.Invoke( collision );\r\n            }\r\n\r\n            return newResult;\r\n        }\r\n\r\n        if ( depth == 0 && !gravityPass )\r\n        {\r\n            IsPushingAgainstWall = false;\r\n            WallObject = null;\r\n        }\r\n\r\n        if ( gravityPass && _bounced )\r\n        {\r\n            _bounced = false;\r\n            velocity -= AppliedGravity * delta * delta * 1.4f; // Evil hack so it doesn't lose velocity due to gravity passes after each bounce\r\n        }\r\n\r\n        return new MoveHelperResult( position + velocity, velocity / delta, current.Offset ); // We didn't hit anything? Ok just keep going then :-)\r\n    }\r\n\r\n    private float CalculateGoalSpeed( Vector3 wishVelocity, Vector3 velocity, bool isAccelerating, float delta )\r\n    {\r\n        if ( !AccelerationEnabled ) return 999999999f; // Should be enough for the freaks that don't want acceleration\r\n\r\n        float goalSpeed;\r\n\r\n        var isSameDirection = velocity.IsNearlyZero( 1f ) || Vector3.Dot( wishVelocity.WithZ( 0f ).Normal, velocity.WithZ( 0f ).Normal ) >= 0f; // Is our wishVelocity roughly moving towards our velocity already?\r\n        var currentSpeed = Math.Max( Velocity.WithZ( 0f ).Length, 10f );\r\n        var acceleration = ( IsOnGround ? GroundAcceleration : AirAcceleration ) * ( FixedAcceleration ? 1f : AccelerationCurve.Evaluate( currentSpeed ) );\r\n        var deceleration = ( IsOnGround ? GroundDeceleration : AirDeceleration ) * ( FixedDeceleration ? 1f : DecelerationCurve.Evaluate( currentSpeed ) );\r\n\r\n        if ( isAccelerating )\r\n            goalSpeed = acceleration;\r\n        else\r\n            goalSpeed = !isSameDirection ? Math.Max( acceleration, deceleration ) : deceleration; // Makes movement more responsive especially for flying or rolling\r\n\r\n        if ( !IgnoreGroundSurface && GroundSurface != null )\r\n            goalSpeed *= GroundSurface.Friction; // Take into account the ground's friction\r\n\r\n        goalSpeed *= delta;\r\n\r\n        return goalSpeed;\r\n    }\r\n\r\n    private Vector3 CalculateGoalVelocity( float delta )\r\n    {\r\n        bool shouldIgnoreZ = IgnoreZ || ( IgnoreZWhenZero && WishVelocity.z.AlmostEqual( 0f ) );\r\n        var wishVelocity = shouldIgnoreZ ? ( WishVelocity.Normal * WishVelocity.Length ).WithZ( Velocity.z ) : WishVelocity;\r\n        var isAccelerating = shouldIgnoreZ ? wishVelocity.WithZ( 0f ).Length >= Velocity.WithZ( 0f ).Length : wishVelocity.Length >= Velocity.Length;\r\n\r\n        var goalSpeed = CalculateGoalSpeed( wishVelocity, Velocity, isAccelerating, delta );\r\n        var goalVelocity = Velocity.MoveTowards( wishVelocity, goalSpeed );\r\n\r\n        return shouldIgnoreZ ? goalVelocity.WithZ( Velocity.z ) : goalVelocity;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Attempts to return the nearest standable position until you're not longer inside other colliders<br/>\r\n    /// If it fails too frequently, consider increasing <see cref=\"MaxUnstuckTries\"/>\r\n    /// </summary>\r\n    /// <param name=\"position\">The start position</param>\r\n    /// <param name=\"result\">The final standable position</param>\r\n    /// <returns>True if it found one, false if it failed.</returns>\r\n    public bool TryUnstuck( Vector3 position, out Vector3 result )\r\n    {\r\n        if ( _lastVelocity == Vector3.Zero )\r\n            _lastVelocity = -AppliedGravity.Normal;\r\n\r\n        var velocityLength = _lastVelocity.Length + SkinWidth;\r\n        var startPos = position - _lastVelocity.Normal * velocityLength; // Try undoing the last velocity 1st\r\n        var endPos = position;\r\n\r\n        for ( int i = 0; i < MaxUnstuckTries + 1; i++ )\r\n        {\r\n            if ( i == 1 )\r\n                startPos = position + -AppliedGravity.Normal * 2f; // Try going up 2nd\r\n\r\n            if ( i > 1 && i < MaxUnstuckTries / 2f )\r\n                startPos = position + Vector3.Random.Normal * ( (float)i / 2f ); // Start randomly checking 3rd\r\n\r\n            if ( i >= MaxUnstuckTries / 2f )\r\n                startPos = position + Vector3.Random.Normal * i; // Ok at this point start checking further\r\n\r\n            if ( startPos - endPos == Vector3.Zero ) // No difference!\r\n                continue;\r\n\r\n            var unstuckTrace = BuildTrace( _shrunkenBounds, startPos, endPos, PhysicallySimulated );\r\n\r\n            if ( !unstuckTrace.StartedSolid )\r\n            {\r\n                result = unstuckTrace.EndPosition - _lastVelocity.Normal * SkinWidth / 4f;\r\n                _lastVelocity = Vector3.Zero;\r\n                return true;\r\n            }\r\n        }\r\n\r\n        result = position;\r\n        return false;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Debug don't use\r\n    /// </summary>\r\n    /// <param name=\"position\"></param>\r\n    /// <param name=\"title\"></param>\r\n    /// <returns></returns>\r\n    private bool TestPosition( Vector3 position, string title )\r\n    {\r\n        var testTrace = BuildTrace( _shrunkenBounds, position, position );\r\n\r\n        if ( testTrace.StartedSolid )\r\n        {\r\n            Log.Info( $\"[{RealTime.Now}]{title} {GameObject.Name} started solid at {position} against {testTrace.GameObject}\" );\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n        base.OnFixedUpdate();\r\n\r\n        if ( !ManuallyUpdate && Active && !PhysicallySimulated ) // If we're not physically simulated we Move inside of OnFixedUpdate\r\n            Move( ActiveAutoTimeDelta );\r\n    }\r\n\r\n    protected override void OnDestroy()\r\n    {\r\n        base.OnDestroy();\r\n\r\n        Collider?.Destroy();\r\n        Body?.Destroy();\r\n    }\r\n\r\n    protected override void OnDisabled()\r\n    {\r\n        base.OnDisabled();\r\n\r\n        Collider?.Enabled = false;\r\n        Body?.Enabled = false;\r\n    }\r\n\r\n    protected override void OnEnabled()\r\n    {\r\n        base.OnEnabled();\r\n\r\n        Collider?.Enabled = true;\r\n        Body?.Enabled = true;\r\n    }\r\n\r\n    public override int ComponentVersion => 4;\r\n\r\n    [JsonUpgrader( typeof( ShrimpleCharacterController ), 3 )]\r\n    private static void UpdateTraceShape( JsonObject json )\r\n    {\r\n        if ( json.Remove( \"CylinderTrace\", out var newNode ) && newNode is JsonValue boolNode && boolNode.TryGetValue<bool>( out var isCylinder ) )\r\n            json[\"TraceShape\"] = isCylinder ? \"Cylinder\" : \"Box\";\r\n        else\r\n            json[\"TraceShape\"] = \"Box\";\r\n    }\r\n\r\n    [JsonUpgrader( typeof( ShrimpleCharacterController ), 4 )]\r\n    private static void UpdateMaxGroundAngle( JsonObject json )\r\n    {\r\n        // Old default was 60f, new default is (30f, 60f)\r\n        if ( json.TryGetPropertyValue( \"MaxGroundAngle\", out var oldValue ) && oldValue is JsonValue floatValue && floatValue.TryGetValue<float>( out var oldAngle ) )\r\n        {\r\n            json.Remove( \"MaxGroundAngle\" );\r\n            var startAngle = oldAngle * 0.75f;\r\n            json[\"MaxGroundAngle\"] = $\"{startAngle:F2} {oldAngle:F2}\";\r\n        }\r\n    }\r\n}\r\n"
        }
    ]
}