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

// Local view assistance only. Firing, occlusion and interception remain host-authoritative.
public sealed class ArcadePlateAssist
{
    public SausageProjectile Target {get;private set;}
    public int Acquisitions {get;private set;}
    private float releaseCooldown;
    public bool Locked=>Target.IsValid() && !Target.Resolved;
    public void Clear(){Target=null;}
    public void Update(PlayerController player,bool allowed,float delta)
    {
        releaseCooldown=MathF.Max(0,releaseCooldown-delta);
        if(!allowed || !player.IsValid() || player.IsProxy || MultiplayerSession.Find(player.Scene)?.IsOverdrive!=true){Clear();return;}
        // Moving the mouse away releases assistance, without immediately
        // snapping back on the next frame to the same incoming plate.
        if(MathF.Abs(Input.AnalogLook.pitch)+MathF.Abs(Input.AnalogLook.yaw)>10){Clear();releaseCooldown=.6f;return;}
        if(releaseCooldown>0){Clear();return;}
        if(!Locked || !Eligible(Target,player,false)){Clear();}
        if(!Locked)
        {
            Target=player.Scene.GetAllComponents<SausageProjectile>().Where(p=>Eligible(p,player,true))
                .OrderBy(p=>(p.WorldPosition-player.EyePosition).Length/MathF.Max(1,p.Velocity.Length)).FirstOrDefault();
            if(!Target.IsValid())return;
            Acquisitions++;
        }
        // Track the threat until it is broken, passes the player, or is occluded.
        var origin=player.Scene.Camera?.WorldPosition ?? player.EyePosition;
        var desired=Rotation.LookAt(Target.WorldPosition-origin);
        player.EyeAngles=Rotation.Slerp(player.EyeAngles.ToRotation(),desired,1-MathF.Exp(-24*delta)).Angles().WithRoll(0);
    }
    public static bool Eligible(SausageProjectile p,PlayerController player,bool acquiring)
    {
        if(!player.IsValid() || !p.IsValid() || p.Resolved || !(p.Plate || p.Egg) || p.IntendedTarget!=player.GameObject)return false;
        var to=player.EyePosition-p.WorldPosition;
        // Plates and eggs: lock on earlier and from a wider angle so incoming
        // throws can be shot out of the air before they arrive.
        if(to.Length>820 || (to.Length>340 && p.JourneyFraction<.3f))return false;
        if(Vector3.Dot(p.Velocity.Normal,to.Normal)<.5f)return false;
        if(Vector3.Dot(player.EyeAngles.ToRotation().Forward,-to.Normal)<(acquiring ? -.1f : -.35f))return false;
        var ray=player.Scene.Trace.Ray(player.EyePosition,p.WorldPosition).WithSurfaceMeshes()
            .IgnoreGameObjectHierarchy(player.GameObject).IgnoreGameObjectHierarchy(p.GameObject).WithoutTags("paintball_debris","paintball_actor").Run();
        return !ray.Hit;
    }

    // Reconcile the short network delay of an explicitly locked plate. The host
    // accepts only a nearby reported point on this player's incoming target,
    // then traces the normal shot toward its current position; cover still wins.
    public static Vector3 Reconcile(PlayerController player,GameObject target,Vector3 origin,Vector3 reportedAim)
    {
        var plate=target.IsValid() ? target.Components.Get<SausageProjectile>() : null;
        if(!MultiplayerSession.Authority || !player.IsValid() || !Eligible(plate,player,true))return reportedAim;
        float tolerance=12+plate.Velocity.Length*.2f;
        if((reportedAim-plate.WorldPosition).Length>tolerance)return reportedAim;
        var lane=player.Scene.Trace.Ray(origin,plate.WorldPosition).WithSurfaceMeshes()
            .IgnoreGameObjectHierarchy(player.GameObject).IgnoreGameObjectHierarchy(plate.GameObject)
            .WithoutTags("paintball_debris","paintball_actor").Run();
        return lane.Hit ? reportedAim : AimPoint(player,plate,origin);
    }
    public static Vector3 AimPoint(PlayerController player,SausageProjectile plate,Vector3 origin)
    {
        if(player.Components.Get<PaintballCombatant>()?.HasCrossbow!=true)return plate.WorldPosition;
        // Lead a moving plate by the bolt's physical flight time; never teleport
        // the arrow or grant an interception through intervening geometry.
        float flight=(plate.WorldPosition-origin).Length/2400;
        for(int i=0;i<3;i++)flight=(plate.WorldPosition+plate.Velocity*flight-origin).Length/2400;
        return plate.WorldPosition+plate.Velocity*MathF.Min(.3f,flight);
    }
}