PaintballFlight.cs
using Sandbox;
namespace PaintballBaddies;
public static class PaintballFlight
{
    public const float Speed=3360; // 280 feet per second; engine distances are inches.
    public const float Gravity=386.09f;
    // Unlimited paint still needs deliberate timing: at most about 3.6 shots/second.
    public const float ShotInterval=.28f;
    public const float Lifetime=3;
    // Slightly enlarged for readability; keep physical travel speed and gravity.
    public const float Radius=.41f;
    public const float VisibilityScale=1.35f;
    public const float VisualScale=Radius*2*VisibilityScale/50;
    public static SceneTraceResult Trace(Scene scene,Vector3 from,Vector3 to,GameObject shooter)
    {
        var world=scene.Trace.Sphere(Radius,from,to).UseHitPosition(true).UseHitboxes(false)
            .IgnoreGameObjectHierarchy(shooter).WithoutTags("paintball_debris").WithSurfaceMeshes();
        // Movement hulls do not follow arms/legs or crouched poses. Keep them
        // for navigation, and use native animated hitboxes for paintballs.
        foreach(var actor in scene.GetAllComponents<CharacterHitboxes>())
            if(actor.Count>0 && actor.Actor.IsValid())world=world.IgnoreGameObjectHierarchy(actor.Actor);
        var physical=world.Run();
        var character=scene.Trace.Sphere(Radius,from,to).UseHitPosition(true).UsePhysicsWorld(false)
            .UseHitboxes(true).IgnoreGameObjectHierarchy(shooter).Run();
        return character.Hit && (!physical.Hit || character.Fraction<physical.Fraction) ? character : physical;
    }
    public static GameObject CreateBead(GameObject ball,Color tint)
    {
        // Both renderers are local presentation, including on a replicated ball.
        // Keep the trail root at unit scale; only the sphere is paintball-sized.
        var presentation=new GameObject(ball,true,"Paintball presentation"){NetworkMode=NetworkMode.Never};
        var bead=new GameObject(presentation,true,"Paintball bead"){NetworkMode=NetworkMode.Never};
        bead.Tags.Add("paintball_debris");
        bead.LocalScale=new Vector3(VisualScale);
        var renderer=bead.Components.Create<ModelRenderer>();
        renderer.Model=Model.Load("models/dev/sphere.vmdl");
        renderer.MaterialOverride=Material.Load("materials/dev/primary_white_emissive.vmat");
        renderer.RenderType=ModelRenderer.ShadowRenderType.Off;
        renderer.Tint=tint;
        AddTrail(presentation,tint);
        return bead;
    }
    public static void UpdateBead(GameObject bead)
    {
        if(!bead.IsValid())return;
        var camera=bead.Scene.Camera;
        if(!camera.IsValid())return;
        // Enlarge the bead at every distance, including its near/far limits.
        // Presentation remains independent of the swept collision radius.
        float diameter=((bead.WorldPosition-camera.WorldPosition).Length*.0025f).Clamp(Radius*2,5)*VisibilityScale;
        bead.WorldScale=new Vector3(diameter/50);
    }
    // Low ballistic arc to the sight point; collision still decides what is hit.
    public static Vector3 LaunchVelocity(Vector3 from,Vector3 to)
    {
        var offset=to-from;
        var horizontal=offset.WithZ(0);
        float distance=horizontal.Length;
        if(distance<1 || distance>2400)return offset.Normal*Speed;
        float speedSquared=Speed*Speed;
        float discriminant=speedSquared*speedSquared-Gravity*(Gravity*distance*distance+2*offset.z*speedSquared);
        if(discriminant<0)return offset.Normal*Speed;
        // Rationalized low-angle solution avoids cancellation for near-level shots.
        float tangent=(Gravity*distance*distance+2*offset.z*speedSquared)/(distance*(speedSquared+System.MathF.Sqrt(discriminant)));
        float horizontalSpeed=Speed/System.MathF.Sqrt(1+tangent*tangent);
        return horizontal.Normal*horizontalSpeed+Vector3.Up*(horizontalSpeed*tangent);
    }
    public static void AddTrail(GameObject ball,Color tint)
    {
        var trail=ball.Components.Create<TrailRenderer>();
        trail.MaxPoints=8;trail.PointDistance=2;trail.LifeTime=.055f;
        trail.CastShadows=false;trail.Opaque=false;
        trail.Face=SceneLineObject.FaceMode.Camera;trail.BlendMode=BlendMode.Normal;
        trail.Color=Gradient.FromColors(tint.WithAlpha(.9f),tint.WithAlpha(0));
        var width=new Curve();width.AddPoint(0,1.2f);width.AddPoint(1,0);trail.Width=width;
    }
}