CharacterSeparation.cs
using Sandbox;
using System;
namespace PaintballBaddies;

/// <summary>Characters never physically collide with each other (see
/// Collision.config: that caused launches and jitter). Instead this gently
/// slides overlapping characters apart every frame, so the crew and the
/// sausages never stand inside each other and nobody gets flung.
/// Each object is only moved by whoever simulates it (the host for AI and
/// sausages, each player for themselves), never through walls.</summary>
public sealed class CharacterSeparation : Component
{
    const float ActorRadius=15,SausageRadius=16,MaxSpeed=140,Response=9;
    public static void Ensure(Scene scene)
    {
        if(scene is null || scene.GetAllComponents<CharacterSeparation>().Any())return;
        new GameObject(true,"Character separation"){NetworkMode=NetworkMode.Never}.Components.Create<CharacterSeparation>();
    }
    private readonly List<(GameObject Go,float Radius,float Give,NavMeshAgent Agent)> bodies=new();
    protected override void OnFixedUpdate()
    {
        bodies.Clear();
        foreach(var c in Scene.GetAllComponents<PaintballCombatant>())
        {
            if(!c.IsValid() || !c.Active || c.Eliminated)continue;
            // Humans give way less than AI, so the player never feels shoved.
            bool human=c.Components.Get<PlayerController>().IsValid();
            bool anchored=c.Components.Get<CoverController>() is {Attached:true} or {Sliding:true}
                || c.Components.Get<VaultController>()?.IsVaulting==true
                || c.Components.Get<CloseCombat>()?.Incapacitated==true
                || c.Components.Get<ArenaOpponent>()?.UsingCover==true;
            bodies.Add((c.GameObject,ActorRadius,anchored ? 0 : human ? .25f : 1,c.Components.Get<NavMeshAgent>()));
        }
        foreach(var e in Scene.GetAllComponents<OverdriveEnemy>())
            if(e.IsValid() && e.Active && !e.Dead)bodies.Add((e.GameObject,SausageRadius,e.FrozenRemaining>0 ? 0 : 1,e.Components.Get<NavMeshAgent>()));
        if(bodies.Count<2)return;
        var push=new Vector3[bodies.Count];
        for(int i=0;i<bodies.Count;i++)
            for(int j=i+1;j<bodies.Count;j++)
            {
                var a=bodies[i];var b=bodies[j];
                var d=(a.Go.WorldPosition-b.Go.WorldPosition);
                if(MathF.Abs(d.z)>60)continue;
                d=d.WithZ(0);float min=a.Radius+b.Radius;float len=d.Length;
                if(len>=min)continue;
                // Exactly on top of each other: split along a stable direction.
                var n=len>.01f ? d/len : Rotation.FromYaw((i*73+j*151)%360).Forward;
                float overlap=min-len,total=a.Give+b.Give;
                if(total<=0)continue;
                push[i]+=n*overlap*(a.Give/total);push[j]-=n*overlap*(b.Give/total);
            }
        for(int i=0;i<bodies.Count;i++)
        {
            if(push[i].Length<.05f)continue;
            var b=bodies[i];
            if(b.Go.IsProxy)continue;
            var step=push[i]*MathF.Min(1,Response*Time.Delta);
            float max=MaxSpeed*Time.Delta;if(step.Length>max)step=step.Normal*max;
            var from=b.Go.WorldPosition+Vector3.Up*24;
            var hit=Scene.Trace.Sphere(8,from,from+step).WithoutTags("paintball_actor","overdrive_enemy","paintball_debris","trigger").Run();
            if(hit.Hit || hit.StartedSolid)continue;
            var to=b.Go.WorldPosition+step;
            if(b.Agent.IsValid() && b.Agent.UpdatePosition)b.Agent.SetAgentPosition(to);
            else b.Go.WorldPosition=to;
        }
    }
}