ShrimpleCharacterController.Physical.cs
using System;

namespace SWB.Demo;

public partial class ShrimpleCharacterController
{
    bool _didStep;
    Vector3 _stepPosition;

    void IScenePhysicsEvents.PrePhysicsStep()
    {
        if (!PhysicallySimulated) return;
        if (!Body.IsValid()) return;

        _didStep = false;

        UpdateBodyPhysics();
        UpdateMassCenter();
        AddWishVelocity();

        if (StepsEnabled && IsOnGround)
            TryStepUp();
    }

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

        if (!PhysicallySimulated) return;

        if (_didStep)
            Body.WorldPosition = _stepPosition;

        var wasOnGround = IsOnGround;

        CategorizePhysicalGround();

        if (GroundStickEnabled && !IsSlipping)
        {
            var shouldTryStick = IsOnGround;
            if (!shouldTryStick && wasOnGround && Body.Velocity.z <= 0f)
                shouldTryStick = true;

            if (shouldTryStick)
                StickToGround();
        }

        ApplyGroundVelocity();
        Velocity = Body.Velocity;
    }

    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 wish = WishVelocity;
        var z = Body.Velocity.z;
        var velocity = Body.Velocity;

        var currentSpeed = Math.Max(velocity.WithZ(0).Length, 10f);
        var acceleration = (IsOnGround ? GroundAcceleration : AirAcceleration) * (FixedAcceleration ? 1f : AccelerationCurve.Evaluate(currentSpeed));
        var deceleration = (IsOnGround ? GroundDeceleration : AirDeceleration) * (FixedDeceleration ? 1f : DecelerationCurve.Evaluate(currentSpeed));

        if (!IgnoreGroundSurface && GroundSurface != null)
        {
            acceleration *= GroundSurface.Friction;
            deceleration *= GroundSurface.Friction;
        }

        if (!wish.IsNearZeroLength)
        {
            if (AccelerationEnabled)
            {
                var speed = velocity.WithZ(0).Length;
                var maxSpeed = MathF.Max(wish.Length, speed);

                var targetVelocity = wish;
                if (IsOnGround && GroundStickEnabled && !GroundNormal.IsNearlyZero(0.01f))
                {
                    targetVelocity = Vector3.VectorPlaneProject(wish, GroundNormal);
                    if (!targetVelocity.IsNearlyZero(0.01f))
                    {
                        var isGoingUphill = Vector3.Dot(targetVelocity, AppliedGravity) < 0f;
                        var slopeMultiplier = isGoingUphill ? GetSlopeVelocityMultiplier(GroundAngle) : 1f;
                        targetVelocity = targetVelocity.Normal * maxSpeed * slopeMultiplier;
                    }

                    velocity = velocity.MoveTowards(targetVelocity, acceleration * Time.Delta);

                    if (velocity.Length > maxSpeed)
                        velocity = velocity.Normal * maxSpeed;
                }
                else
                {
                    var horizontalVel = velocity.WithZ(0);
                    var targetHorizontal = wish.Normal * maxSpeed;
                    horizontalVel = horizontalVel.MoveTowards(targetHorizontal, acceleration * Time.Delta);

                    if (horizontalVel.Length > maxSpeed)
                        horizontalVel = horizontalVel.Normal * maxSpeed;

                    velocity = horizontalVel.WithZ(velocity.z);
                }
            }
            else
            {
                if (IsOnGround && GroundStickEnabled && !GroundNormal.IsNearlyZero(0.01f))
                {
                    var projectedWish = Vector3.VectorPlaneProject(wish, GroundNormal);
                    if (!projectedWish.IsNearlyZero(0.01f))
                    {
                        var isGoingUphill = Vector3.Dot(projectedWish, AppliedGravity) < 0f;
                        var slopeMultiplier = isGoingUphill ? GetSlopeVelocityMultiplier(GroundAngle) : 1f;
                        velocity = projectedWish.Normal * wish.Length * slopeMultiplier;
                    }
                    else
                        velocity = Vector3.Zero;
                }
                else
                {
                    if (!IsOnGround)
                        velocity = wish.WithZ(velocity.z);
                    else
                        velocity = wish;
                }
            }
        }
        else if (IsOnGround)
        {
            if (AccelerationEnabled)
                velocity = velocity.MoveTowards(Vector3.Zero, deceleration * Time.Delta);
            else
                velocity = Vector3.Zero;
        }

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

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

        Body.Velocity = velocity;
    }

    private void TryStepUp()
    {
        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 from = WorldPosition;
        var speed = MathF.Max(bodyHorizontal.Length * Time.Delta, StepDepth);
        var vel = moveDir.Normal * speed;

        var a = from + _offset - vel.Normal * SkinWidth;
        var b = from + _offset + vel;
        var forwardTrace = BuildTrace(_shrunkenBounds, a, b);

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

        var remainingDist = vel.Length - forwardTrace.Distance;
        if (remainingDist <= 0) remainingDist = StepDepth;
        var remainingVel = vel.Normal * remainingDist;

        from = forwardTrace.EndPosition - _offset;
        var upPoint = from + _offset + Vector3.Up * StepHeight;
        var upTrace = BuildTrace(_shrunkenBounds, from + _offset, upPoint);

        if (upTrace.StartedSolid) return;
        if (upTrace.Distance < 2f) return;

        var raisedPos = upTrace.EndPosition;
        var acrossEnd = raisedPos + remainingVel;
        var acrossTrace = BuildTrace(_shrunkenBounds, raisedPos, acrossEnd);

        if (acrossTrace.StartedSolid) return;

        var top = acrossTrace.EndPosition;
        var bottom = top + Vector3.Down * StepHeight;
        var downTrace = BuildTrace(_shrunkenBounds, top, bottom);

        if (!downTrace.Hit) return;

        var groundAngle = Vector3.GetAngle(Vector3.Up, downTrace.Normal);
        if (!IsAngleStandable(groundAngle)) return;

        var newFeetPos = downTrace.EndPosition - _offset + Vector3.Up * SkinWidth;
        if (MathF.Abs(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 = GroundVelocity;
        if (groundVelocity.IsNearZeroLength) return;

        Body.WorldPosition += groundVelocity * Time.Delta;
    }

    private void StickToGround()
    {
        if (Body.PhysicsBody.Sleeping) return;

        var currentPosition = WorldPosition;
        var from = currentPosition + _offset + Vector3.Up * StepHeight;
        var to = currentPosition + _offset + Vector3.Down * GroundStickDistance;

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

        if (trace.StartedSolid) return;

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

            var targetFeetPos = trace.EndPosition - _offset + Vector3.Up * 0.01f;
            var delta = currentPosition - targetFeetPos;

            if (delta.z < -0.1f) return;
            if (delta.IsNearlyZero(0.001f)) return;

            Body.WorldPosition = targetFeetPos;

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

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

    private void CategorizePhysicalGround()
    {
        var feetPos = WorldPosition;
        var from = feetPos + Vector3.Up * 2f;
        var to = feetPos + Vector3.Down * 2f;

        var groundTrace = Game.SceneTrace.Sphere(SkinWidth * 2f, from, to)
            .IgnoreGameObjectHierarchy(GameObject)
            .WithoutTags(IgnoreTags)
            .Run();

        if (groundTrace.StartedSolid)
        {
            IsStuck = true;
            var fallbackTrace = BuildTrace(_shrunkenBounds, WorldPosition + _offset + Vector3.Up * 4f, WorldPosition + _offset + Vector3.Down * 2f);
            if (fallbackTrace.Hit && !fallbackTrace.StartedSolid)
            {
                var fallbackAngle = Vector3.GetAngle(Vector3.Up, fallbackTrace.Normal);
                if (IsAngleStandable(fallbackAngle))
                {
                    IsOnGround = true;
                    GroundNormal = fallbackTrace.Normal;
                    GroundSurface = fallbackTrace.Surface;
                    GroundObject = fallbackTrace.GameObject;
                    IsSlipping = false;
                    return;
                }
            }
        }
        else
        {
            IsStuck = false;
        }

        if (groundTrace.Hit && !groundTrace.StartedSolid)
        {
            var surfaceAngle = Vector3.GetAngle(Vector3.Up, groundTrace.Normal);
            var isStandable = IsAngleStandable(surfaceAngle);

            if (isStandable)
            {
                IsOnGround = true;
                GroundNormal = groundTrace.Normal;
                GroundSurface = groundTrace.Surface;
                GroundObject = groundTrace.GameObject;
                IsSlipping = false;
            }
            else
            {
                IsOnGround = false;
                GroundNormal = groundTrace.Normal;
                GroundSurface = groundTrace.Surface;
                GroundObject = groundTrace.GameObject;
                IsSlipping = true;
            }
        }
        else
        {
            IsOnGround = false;
            GroundNormal = Vector3.Up;
            GroundSurface = null;
            GroundObject = null;
            IsSlipping = false;
        }
    }

    public bool TryPhysicalUnstuck(Vector3 position, out Vector3 result)
    {
        if (Body.IsValid())
        {
            var bodyPos = Body.WorldPosition;
            var bodyTrace = BuildTrace(_shrunkenBounds, bodyPos + _offset, bodyPos + _offset);
            if (!bodyTrace.StartedSolid)
            {
                result = bodyPos;
                return true;
            }
        }

        for (int i = 1; i <= 10; i++)
        {
            var upPos = position + -AppliedGravity.Normal * (i * 2f);
            var upTrace = BuildTrace(_shrunkenBounds, upPos + _offset, upPos + _offset);
            if (!upTrace.StartedSolid)
            {
                result = upPos;
                if (Body.IsValid())
                {
                    Body.WorldPosition = upPos;
                    Body.Velocity = Vector3.Zero;
                }
                return true;
            }
        }

        if (Body.IsValid() && Body.Velocity.Length > 1f)
        {
            var velDir = Body.Velocity.Normal;
            for (int i = 1; i <= 5; i++)
            {
                var velPos = position + velDir * (i * 4f);
                var velTrace = BuildTrace(_shrunkenBounds, velPos + _offset, velPos + _offset);
                if (!velTrace.StartedSolid)
                {
                    result = velPos;
                    Body.WorldPosition = velPos;
                    return true;
                }
            }
        }

        result = position;
        return false;
    }

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

        CreateCollider();
        CreateRigidbody();
    }

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

    protected void CreateCollider()
    {
        if (Collider.IsValid()) Collider.Destroy();

        if (PhysicallySimulated)
            Collider = GameObject.GetOrAddComponent<CapsuleCollider>();
        else
        {
            if (TraceShape == TraceType.Box || TraceShape == TraceType.Bounds)
                Collider = GameObject.GetOrAddComponent<BoxCollider>();
            else if (TraceShape == TraceType.Cylinder)
                Collider = GameObject.GetOrAddComponent<HullCollider>();
            else if (TraceShape == TraceType.Sphere)
                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()
    {
        if (Body.IsValid()) Body.Destroy();

        if (GameObject.Components.TryGet<Rigidbody>(out var existingBody))
            existingBody.Destroy();

        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) { }
}