CitizenPodReload.cs
using Sandbox;
using Sandbox.Citizen;

namespace PaintballBaddies;

/// <summary>Reusable pod pickup, pour and return choreography for the Citizen human rig.</summary>
public sealed class CitizenPodReload : Component
{
    public GameObject refillPod;
    private GameObject refillLid, refillPouch, reloadRightTarget;
    private readonly List<GameObject> refillBalls = new();
    public bool podHeld, podReturned;
    private bool pickupStarted;
    private Transform pickupStart;
    private Rotation carryHandRotation;
    private bool carryHandCaptured;
    public float pickupError=-1, returnError=-1, minimumReturnGap=float.MaxValue;
    public float MinimumPickupGap { get; private set; } = float.MaxValue;
    public float returnReachDistance, returnArmLength;
    public int pourSamples, pourAlignedSamples;
    public float maximumPourError, totalPourError;
    private float podGripWeight;
    private GameObject subject => GameObject;
    public void CreateGear()
    {
        if (refillPod.IsValid()) return;
                reloadRightTarget = new GameObject(subject) { Name="Native reload marker tuck target" };
                refillPod = new GameObject(subject) { Name="Existing refill pod reach review" };
                refillPod.Components.Create<ModelRenderer>().Model=Model.Load("models/gear/refill_pod/body.vmdl");
                refillPouch=new GameObject(subject) { Name="Existing belt pod pouch" };
                refillPouch.Components.Create<ModelRenderer>().Model=Model.Load("models/gear/refill_pod/pouch.vmdl");
                refillLid = new GameObject(refillPod) { Name="Existing hinged refill lid" };
                refillLid.Components.Create<ModelRenderer>().Model=Model.Load("models/gear/refill_pod/lid.vmdl");
                for(int i=0;i<2;i++)
                {
                    var ball=new GameObject(subject) { Name="Existing reload paintball stream" };
                    var renderer=ball.Components.Create<ModelRenderer>();
                    renderer.Model=Model.Load("models/dev/sphere.vmdl");
                    renderer.Tint=new Color(.95f,.28f,.025f);
                    ball.WorldScale=new Vector3(.0063f);
                    ball.Enabled=false;
                    refillBalls.Add(ball);
                }
    }
    public void ResetCycle()
    {
        pickupStarted=false;podHeld=false;podReturned=false;podGripWeight=0;
        carryHandCaptured=false;
        pickupError=-1;returnError=-1;minimumReturnGap=float.MaxValue;
        MinimumPickupGap=float.MaxValue;
        returnReachDistance=0;returnArmLength=0;
        pourSamples=0;pourAlignedSamples=0;maximumPourError=0;totalPourError=0;
    }
    public void StopPresentation()
    {
        podHeld=false;
        if(refillPod.IsValid()) refillPod.Enabled=false;
        foreach(var ball in refillBalls) if(ball.IsValid()) ball.Enabled=false;
    }
    public void DropHeldPod(Vector3 velocity)
    {
        if(!podHeld || !refillPod.IsValid())return;
        var dropped=new GameObject {Name="Released paintball pod"};
        dropped.Tags.Add( "paintball_debris" );
        dropped.WorldTransform=refillPod.WorldTransform;
        dropped.Components.Create<ModelRenderer>().Model=Model.Load("models/gear/refill_pod/body.vmdl");
        if(refillLid.IsValid())
        {
            var lid=new GameObject(dropped){Name="Pod lid"};lid.WorldTransform=refillLid.WorldTransform;
            lid.Components.Create<ModelRenderer>().Model=Model.Load("models/gear/refill_pod/lid.vmdl");
        }
        var collider=dropped.Components.Create<BoxCollider>();
        collider.Scale=new Vector3(3,3,9);collider.Center=new Vector3(0,0,4.5f);
        var physics=dropped.Components.Create<Rigidbody>();
        physics.MassOverride=.08f;physics.EnableImpactDamage=false;physics.Gravity=true;
        physics.Velocity=velocity;physics.AngularVelocity=new Vector3(1,2,.5f);
        dropped.Components.Create<DroppedReloadPod>();
        StopPresentation();
    }
    public bool HasActiveReloadProps => (refillPod.IsValid() && refillPod.Enabled)
        || refillBalls.Any(ball=>ball.IsValid() && ball.Enabled);
    public void SetCarryVisible(bool visible)
    {
        if(refillPod.IsValid())refillPod.Enabled=visible;
        if(refillPouch.IsValid())refillPouch.Enabled=visible;
        if(!visible)foreach(var ball in refillBalls)if(ball.IsValid())ball.Enabled=false;
    }
    protected override void OnDestroy()
    {
        foreach (var ball in refillBalls) if (ball.IsValid()) ball.Destroy();
        refillBalls.Clear();
        if (refillPod.IsValid()) refillPod.Destroy();
        if (refillPouch.IsValid()) refillPouch.Destroy();
        if (reloadRightTarget.IsValid()) reloadRightTarget.Destroy();
    }
    public void Apply(SkinnedModelRenderer body, CitizenAnimationHelper animation, GameObject marker,
        GameObject supportTarget, Rotation supportRotation, float currentGrip, float reviewTime,
        float DuckLevel, bool releaseGrip, bool reloadCancelled, float cancelElapsed,
        Transform cancelHand, Rotation cancelPodRotation, float delta)
    {
        if (!refillPod.IsValid()) return;
            var dockRotation=subject.WorldRotation;
            var dockOrigin=subject.WorldPosition+subject.WorldRotation*new Vector3(3,7,26-18*DuckLevel);
            if (body.TryGetBoneTransform("pelvis",out var beltPelvis))
                dockOrigin=beltPelvis.Position+subject.WorldRotation*new Vector3(3,7,-4);
            var podGrip=new Vector3(1.5f,1.4f,3.88583f);
            var dockHand=dockOrigin+dockRotation*podGrip;
            if(refillPouch is not null) { refillPouch.WorldPosition=dockOrigin;refillPouch.WorldRotation=dockRotation; }
            if (releaseGrip && supportTarget is not null && body.TryGetBoneTransform("hand_L",out var reloadHand))
            {
                var reachIn=((reviewTime-1.35f)/.4f).Clamp(0,1);
                var reachOut=((2.95f-reviewTime)/.35f).Clamp(0,1);
                var blend=System.MathF.Min(reachIn,reachOut);
                blend=blend*blend*(3-2*blend);
                // Existing speed-feed source opening: (-Y 0.045m, Z 0.231m), exported forward X.
                if (body.TryGetBoneTransform("hand_R",out var rightHand))
                {
                    // Preserve the carry orientation instead of copying the rifle
                    // magazine gesture's wrist twist into a hopper-fed marker.
                    if(!carryHandCaptured)
                    {
                        carryHandRotation=subject.WorldRotation.Inverse*rightHand.Rotation;
                        carryHandCaptured=true;
                    }
                    reloadRightTarget.WorldPosition=Vector3.Lerp(rightHand.Position,subject.WorldTransform.PointToWorld(new Vector3(12,-6,44-22*DuckLevel)),blend);
                    reloadRightTarget.WorldRotation=Rotation.Slerp(rightHand.Rotation,subject.WorldRotation*carryHandRotation,blend);
                    animation.IkRightHand=reloadRightTarget;
                }
                var opening=marker.WorldPosition+marker.WorldRotation*new Vector3(1.77165f,0,9.09449f);
                var handTarget=opening+marker.WorldRotation*new Vector3(-1.5f,1.4f,6.1f);
                if(reviewTime>=1.85f && reviewTime<2.55f)
                {
                    var error=(reloadHand.Position-handTarget).Length;
                    pourSamples++;
                    if(error<1) pourAlignedSamples++;
                    totalPourError+=error;
                    maximumPourError=System.MathF.Max(maximumPourError,error);
                }
                if(!pickupStarted) { pickupStart=subject.WorldTransform.ToLocal(reloadHand);pickupStarted=true; }
                var pickup=((reviewTime-.8f)/.4f).Clamp(0,1);
                pickup=pickup*pickup*(3-2*pickup);
                supportTarget.WorldPosition=Vector3.Lerp(subject.WorldTransform.PointToWorld(pickupStart.Position),Vector3.Lerp(dockHand,handTarget,blend),pickup);
                supportTarget.WorldRotation=Rotation.Slerp(reloadHand.Rotation,marker.WorldRotation*supportRotation,blend);

                if(reloadCancelled)
                {
                    var recovery=(cancelElapsed/.5f).Clamp(0,1);
                    recovery=recovery*recovery*(3-2*recovery);
                    supportTarget.WorldPosition=Vector3.Lerp(subject.WorldTransform.PointToWorld(cancelHand.Position),dockHand,recovery);
                    supportTarget.WorldRotation=subject.WorldRotation*cancelHand.Rotation;
                }
                animation.IkLeftHand=supportTarget;
                if(refillPod is not null)
                {
                    refillPod.Enabled=true;
                    refillPod.WorldRotation=Rotation.Slerp(dockRotation,marker.WorldRotation*Rotation.FromPitch(180),blend);
                    if(reloadCancelled)
                    {
                        var recovery=(cancelElapsed/.5f).Clamp(0,1);
                        recovery=recovery*recovery*(3-2*recovery);
                        refillPod.WorldRotation=Rotation.Slerp(subject.WorldRotation*cancelPodRotation,dockRotation,recovery);
                    }
                    // Mouth at 8.386 inches from the pod base; follow the actual hand to expose IK fit errors.
                    var dockError=(reloadHand.Position-dockHand).Length;
                    if(reviewTime>=1.2f && reviewTime<1.35f) MinimumPickupGap=System.MathF.Min(MinimumPickupGap,dockError);
                    // Allow the gloved palm to reach the pod, without requiring the
                    // wrist joint to converge within half an inch in a brief window.
                    if(!podHeld && !podReturned && reviewTime>=1.2f && reviewTime<1.35f && dockError<.75f)
                    {
                        podHeld=true;
                        pickupError=dockError;
                    }
                    if(reviewTime>=2.95f && dockError<minimumReturnGap)
                    {
                        minimumReturnGap=dockError;
                        if(body.TryGetBoneTransform("arm_upper_L",out var returnShoulder) && body.TryGetBoneTransform("arm_lower_L",out var returnElbow))
                        {
                            returnReachDistance=(returnShoulder.Position-dockHand).Length;
                            returnArmLength=(returnShoulder.Position-returnElbow.Position).Length+(returnElbow.Position-reloadHand.Position).Length;
                        }
                    }
                    if(podHeld && reviewTime>=2.95f && dockError<.75f)
                    {
                        podHeld=false;
                        podReturned=true;
                        returnError=dockError;
                    }
                    podGripWeight += ((podHeld ? 1 : 0)-podGripWeight)*(1-System.MathF.Exp(-25*delta));
                    body.Set("pb_left_grip",System.MathF.Max(currentGrip,podGripWeight));
                    if(!podHeld) refillPod.WorldRotation=dockRotation;
                    refillPod.WorldPosition=podHeld ? reloadHand.Position-refillPod.WorldRotation*podGrip : dockOrigin;
                    var open=((reviewTime-1.4f)/.3f).Clamp(0,1);
                    open=open*open*(3-2*open);
                    var close=((reviewTime-2.65f)/.25f).Clamp(0,1);
                    close=close*close*(3-2*close);
                    refillLid.WorldPosition=refillPod.WorldPosition+refillPod.WorldRotation*new Vector3(-1.33858f,0,8.46457f);
                    refillLid.WorldRotation=refillPod.WorldRotation*Rotation.FromPitch(115*open*(1-close));
                    var mouth=refillPod.WorldPosition+refillPod.WorldRotation.Up*8.38583f;
                    for(int i=0;i<refillBalls.Count;i++)
                    {
                        var ball=refillBalls[i];
                        ball.Enabled=podHeld && reviewTime>=1.85f && reviewTime<2.55f && (reloadHand.Position-handTarget).Length<1;
                        var t=(reviewTime*5+i/(float)refillBalls.Count)%1;
                        var jitter=marker.WorldRotation*new Vector3(System.MathF.Sin(i*2.4f)*.13f,System.MathF.Cos(i*2.4f)*.13f,0);
                        ball.WorldPosition=Vector3.Lerp(mouth,opening,t*t)+jitter;
                    }
                }
            }
            else if(refillPod is not null)
            {
                refillPod.Enabled=true;
                if(!podHeld) { refillPod.WorldPosition=dockOrigin; refillPod.WorldRotation=dockRotation; }
                else if(body.TryGetBoneTransform("hand_L",out var retainedHand))
                    refillPod.WorldPosition=retainedHand.Position-refillPod.WorldRotation*podGrip;
                refillLid.WorldPosition=refillPod.WorldPosition+refillPod.WorldRotation*new Vector3(-1.33858f,0,8.46457f);
                refillLid.WorldRotation=refillPod.WorldRotation;
                animation.IkRightHand=null;
                foreach(var ball in refillBalls) ball.Enabled=false;
            }
    }
}