ReplayFigure.cs

Static helper that builds the footballer's whole-body pose as a list of coloured capsule parts (Part records: two points, Z heights, width, colour) for live broadcast and replay cameras. Handles gait phase sampling, two-bone leg IK with fixed 0.405 m thigh/shin lengths, arm FK via ArmAngles, head/hair/eyes, and overlays for striking, headers, chest control, dribbling, tackling, keeper poses and goal celebrations.

using System;
using System.Collections.Generic;

namespace Socer;

// Shared world-space rig for live broadcast and every replay camera.
public static partial class ReplayFigure
{
    public readonly record struct Part(Point A,float ZA,Point B,float ZB,float Width,string Color,float PalmRoll=0,bool ArticulatedHand=false);
    public static List<Part> Pose(Footballer player,MatchSimulation match,List<Part> parts)
    {
        parts.Clear();
        float facing=player.Facing;
        var forward=new Point(MathF.Cos(facing),MathF.Sin(facing));
        var side=new Point(-forward.Y,forward.X);
        // Presentation heading is advanced with the root, so a stopped frame
        // retains the previous support direction instead of snapping to facing.
        var travel=player.VisualTravel.Length>.08f?player.VisualTravel.Unit:
            player.VisualVelocity.Length>.08f?player.VisualVelocity.Unit:forward;
        float activity=Math.Clamp(player.VisualSpeed/.8f,0,1);
        // Settle the last stride before travel direction becomes undefined at
        // a stop. Otherwise the ankle jumps when travel falls back to facing.
        if(match.GoalHoldRemaining>0)activity*=Smooth(Math.Min(player.VisualSpeed,player.VisualVelocity.Length)/1.4f);
        bool active=!player.Keeper && (match.Owner==player.Id || player.RunUntil>match.Seconds ||
            player.FootballRunUntil>match.Seconds || player.Action is "Прессинг" or "Рывок" or "Рывок за мячом" or "Открывание" or "Ведение");
        float jog=Math.Clamp((player.VisualSpeed-(active?.8f:1.5f))/2.5f,0,.7f);
        float sprint=Math.Max(jog,Math.Clamp((player.VisualSpeed-3)/5,0,1));
        bool carrying=match.Owner==player.Id && !player.Keeper;
        float braking=Math.Clamp(-Point.Dot(player.VisualAcceleration,travel)/8,0,1)*activity;
        float lateral=Math.Abs(Point.Dot(travel,side));
        float shuffle=lateral*(1-Smooth((player.VisualSpeed-3.2f)/2.5f));
        if(match.GoalHoldRemaining>0)shuffle*=activity;
        float stride=Math.Clamp((1.3f+player.VisualSpeed*.28f)*(.17f+jog*.025f),.20f,.42f)*activity;
        float bob=(.5f-.5f*MathF.Cos(player.GaitPhase*4*MathF.PI))*(.025f+.045f*sprint)*activity;
        bool goalReaction=match.GoalHoldRemaining>0 && match.GoalScorerId>=0;
        // This exact raised-foot event may finish under a frozen whistle or
        // treatment clock. Age its ordinary base with the committed body clock;
        // immutable ball-contact timestamps still belong to the simulation.
        bool royalContact=player.AcrobaticUntil>0 && player.LastKickKind==StrikeKind.Driven &&
            player.LastKickAt==player.HeaderAt && LegendaryAbilities.Get(player.Profile)?.Kind==LegendaryAbility.RoyalReception;
        float bodyClockOffset=royalContact?Math.Max(0,player.HeaderAt-player.AcrobaticAt):0;
        var kick=goalReaction || royalContact && match.WindupPlayer!=player.Id?default:HumanFigure.StrikePose(player,match);
        float strikeWeight=kick.Active?StrikeBlend(player,match):0;
        float preparing=!player.Keeper && !kick.Active && match.Seconds<player.ReceiveReadyUntil?
            Smooth(Math.Clamp((player.ReceiveReadyUntil-match.Seconds)*3,0,1)):0;
        float runAge=match.Seconds-player.FootballRunStarted;
        float pushOff=!kick.Active && !goalReaction && player.FootballRunUntil>match.Seconds && runAge>=0 && runAge<.35f?
            MathF.Sin(runAge/.35f*MathF.PI):0;
        bool jostling=player.Action=="Борется за позицию" && !kick.Active;
        float actualHeaderAge=match.Seconds-player.HeaderAt+bodyClockOffset;
        bool actualHeader=actualHeaderAge>=0 && actualHeaderAge<.70f;
        float preparedAge=match.Seconds-player.VisualHeaderAt+
            (player.VisualHeaderAt<=player.HeaderAt+.05f?bodyClockOffset:0);
        bool headerPrepared=!player.Keeper && preparedAge>=-.30f && preparedAge<.70f;
        float headerAge=actualHeader?actualHeaderAge:preparedAge;
        float header=actualHeader?1-Smooth(headerAge/.55f):headerPrepared?
            headerAge<0?1-Smooth(-headerAge/.30f):1-Smooth(headerAge/.55f):0;
        float headerLeap=actualHeader?player.HeaderLeap:headerPrepared?player.VisualHeaderLift:0;
        var headerPoint=actualHeader?player.HeaderContactPoint:player.VisualHeaderPoint;
        float headerHeight=actualHeader?player.HeaderContactHeight:player.VisualHeaderHeight;
        float headerLanding=headerLeap>.05f && headerAge>.24f && headerAge<.66f?
            MathF.Sin((headerAge-.24f)/.42f*MathF.PI):0;
        float controlAge=match.Seconds-player.LastControlAt;
        float controlLength=Math.Clamp(player.ReceiveUntil-player.LastControlAt,.28f,.55f);
        float control=!player.Keeper && match.Owner==player.Id && !kick.Active && controlAge>=0 && controlAge<controlLength?
            Smooth(controlAge/.055f)*(1-Smooth((controlAge-(controlLength-.14f))/.14f)):0;
        float chestControl=!player.Keeper && match.Seconds<player.ChestUntil && controlAge>=0?
            Smooth(controlAge/.045f)*(1-Smooth((match.Seconds-(player.ChestUntil-.13f))/.13f)):0;
        var centre=player.Position;
        float draw=kick.Active?Math.Clamp(-kick.Forward/.7f,0,1)*strikeWeight:0;
        float follow=kick.Active?Math.Clamp((kick.Forward-.11f)/.55f,0,1)*strikeWeight:0;
        float effort=kick.Active?kick.Strength*strikeWeight:0;
        var kind=match.WindupPlayer==player.Id?match.Strike:player.LastKickKind;
        int kickFoot=player.KickFoot;
        bool insidePass=!player.Keeper && kick.Active && match.WindupPlayer!=player.Id &&
            !player.LastKickWasShot && kind is not (StrikeKind.Header or StrikeKind.Cross) &&
            match.Seconds-player.LastPuntAt>=.8f;
        float insidePassWeight=insidePass?strikeWeight*(1-Smooth((match.Seconds-player.LastKickAt)/.22f)):0;
        float opening=(draw*.48f-follow*(kind==StrikeKind.Curl?.65f:.38f))*kickFoot;
        float settle=effort*.035f;
        // Load the supporting leg, open the hip, then carry the torso through the ball.
        var pelvis=centre-side*(effort*.055f*kickFoot)+forward*(follow*.055f)-travel*(braking*.045f+pushOff*.035f);
        var hipSide=side*MathF.Cos(opening*.5f)+forward*MathF.Sin(opening*.5f);
        bool defending=match.Owner>=0 && match.Owner!=player.Id && match.Players[match.Owner].Team!=player.Team &&
            (player.Action.StartsWith("Сдерживает") || player.Action.StartsWith("Выбирает момент отбора") ||
             player.Action.StartsWith("Закрывает линию удара") || player.Action.StartsWith("Отбор стоя"));
        float guard=defending && !kick.Active && !player.Keeper?
            Math.Clamp((3.4f-(player.Position-match.Ball).Length)/1.8f,0,1)*
            Math.Clamp((3.5f-player.VisualSpeed)/2.5f,0,1)*Math.Clamp((player.Profile.Defence-.55f)/.4f,0,1):0;
        float tackleAge=match.Seconds-player.StandingTackleAt;
        float tackleReach=tackleAge>=0 && tackleAge<.72f && !kick.Active && !goalReaction?
            TacklePoseWeight(tackleAge):0;
        float missedTackle=player.StandingTackleResult<0 && match.Seconds>=player.StandingTackleResolvedAt &&
            match.Seconds<player.StandingTackleResolvedAt+.48f?
            MathF.Sin((match.Seconds-player.StandingTackleResolvedAt)/.48f*MathF.PI):0;
        // Keep room for knee flexion. The old .86 + bob hip height exceeded the
        // .81 m legs with forward feet, so IK clamped almost every step straight.
        float hipZ=.86f-activity*.085f-bob*.45f-settle-braking*.035f-preparing*.025f-shuffle*.025f-pushOff*.035f-
            guard*.10f-tackleReach*.035f-missedTackle*.055f-headerLanding*.075f-control*.025f;
        bool officer=PlayerAppearance.HasOfficerTunic(player.Profile.CatalogId,player.Profile.Edition);
        string shirt=officer?"#686b57":player.Team==2?"#c49ae9":player.Keeper?"#f5dc68":player.Team==0?"#46c7ed":"#fc8b61";
        string shorts=player.Team==2?"#242b37":player.Keeper?"#72602e":player.Team==0?"#193d60":"#f3e4cb";
        string skin=PlayerHeadProfiles.Skin(player.Profile.CatalogId,player.Profile.SkinTone,player.Id);
        string hair=PlayerHeadProfiles.Hair(player.Profile.CatalogId,player.Profile.HairTone);
        void Add(Point a,float za,Point b,float zb,float width,string color)=>parts.Add(new(a,za,b,zb,width,color));
        for(int s=-1;s<=1;s+=2)
        {
            float phase=(player.GaitPhase+(s==1?0:.5f))%1;
            var stepPose=SampleFootballStep(phase,player.VisualSpeed,activity,sprint,active,carrying,braking);
            float stance=stepPose.Stance;
            float recovery=Math.Clamp((phase-stance)/(1-stance),0,1);
            float swing=phase<stance?0:MathF.Sin(recovery*MathF.PI);
            float offset=stepPose.Offset;
            var hip=pelvis+hipSide*(s*.12f);
            var foot=centre+side*(s*(.18f+lateral*.035f+braking*.025f))+travel*offset;
            foot+=side*(s*.15f*guard);
            float footZ=.06f+stepPose.Lift;
            // The trailing foot closes a lateral shuffle while the leading foot
            // opens it; both no longer lift outwards at the same instant.
            float lateralTravel=Point.Dot(travel,side);
            var shuffleFoot=centre+side*(s*.18f+lateralTravel*offset)+forward*(s*.035f);
            foot=foot*(1-shuffle)+shuffleFoot*shuffle;
            footZ=footZ*(1-shuffle)+(.06f+swing*.12f*activity)*shuffle;
            if(preparing>0 && !kick.Active)
            {
                var reception=player.ReceiveDirection.Length>.1f?player.ReceiveDirection:forward;
                var setFoot=centre+side*(s*.20f)+reception*(s==player.StrongFoot?.06f:-.10f);
                float plant=preparing*(1-Smooth(player.VisualSpeed/2));
                foot=foot*(1-plant)+setFoot*plant;
            }
            if(kick.Active)
            {
                float plant=Math.Clamp((kick.Forward+.15f)/.26f,0,1);
                var strikeFoot=centre+side*(s==kickFoot?kickFoot*.16f*(1-plant):-kickFoot*.16f)+
                    forward*(s==kickFoot?Math.Max(-.48f,kick.Forward):-.08f);
                float strikeZ=s==kickFoot?Math.Max(kick.Lift,.06f+draw*.42f):.06f;
                foot=foot*(1-strikeWeight)+strikeFoot*strikeWeight;
                footZ=footZ*(1-strikeWeight)+strikeZ*strikeWeight;
            }
            if(control>0 && chestControl<=0 && s==player.StrongFoot)
            {
                var contact=player.ControlPoint-centre;
                if(contact.Length>.43f)contact=contact.Unit*.43f;
                var exit=player.ReceiveDirection.Length>.1f?player.ReceiveDirection.Unit:forward;
                float cushion=Smooth(controlAge/.22f);
                var target=centre+contact*(1-cushion*.65f)+exit*(.10f*cushion);
                foot=foot*(1-control)+target*control;
                footZ=footZ*(1-control)+(.075f+.07f*MathF.Sin(Math.Clamp(controlAge/controlLength,0,1)*MathF.PI))*control;
            }
            if(header>0)
            {
                float tuck=header*Smooth(headerLeap/.08f)*(1-Smooth(Math.Max(0,headerAge)/.27f));
                foot=foot*(1-tuck)+(centre+side*(s*.16f)-forward*(s==player.StrongFoot?.18f:.08f))*tuck;
                footZ+=tuck*(s==player.StrongFoot?.10f:.16f);
            }
            if(s==kickFoot && insidePassWeight>0)
            {
                // Controlled passes present the inside edge of an opened boot.
                // Keep the ball tangent to its capsule at contact: ball radius
                // .11m + boot radius .065m, with the .05m centre-height offset.
                // This changes the ankle by only a few centimetres, not the root.
                const float yaw=55*MathF.PI/180;
                var opened=forward*MathF.Cos(yaw)+side*(kickFoot*MathF.Sin(yaw));
                var contactNormal=forward*MathF.Sin(yaw)-side*(kickFoot*MathF.Cos(yaw));
                float contactGap=MathF.Sqrt(.175f*.175f-.05f*.05f);
                var contact=foot+forward*.24f;
                var heel=contact-contactNormal*contactGap-opened*.12f;
                foot=foot*(1-insidePassWeight)+heel*insidePassWeight;
            }
            float touchAge=match.Seconds-player.LastDribbleTouch;
            // A skill has its own IK contact and exit pose. The generic dribble
            // overlay used to seize the boot on the very frame SkillStage changed,
            // then seize it again as the skill ended.
            bool skillContact=player.FeintVariant!=0 && match.Seconds>=player.FeintStartedAt &&
                match.Seconds<player.FeintUntil+.24f;
            if(!kick.Active && !goalReaction && !skillContact && match.Owner==player.Id &&
                touchAge>=0 && touchAge<.27f && s==player.DribbleFoot)
            {
                // Keep the contact relative to the runner: the world-space touch point
                // falls behind a sprinting player before the boot has finished moving.
                float distanceToContact=player.DribbleContactOffset.Length;
                float blend=Smooth(touchAge/.075f)*(1-Smooth((touchAge-.11f)/.16f))*
                    (1-Smooth(Math.Clamp((distanceToContact-.42f)/.18f,0,1)));
                var contact=player.DribbleContactOffset-forward*.12f;
                if(contact.Length>.42f)contact=contact.Unit*.42f;
                var followThrough=forward*(Smooth(Math.Clamp(touchAge/.12f,0,1))*.14f);
                foot=foot*(1-blend)+(centre+contact+followThrough)*blend;
                footZ=footZ*(1-blend)+(.08f+MathF.Sin(touchAge/.24f*MathF.PI)*.08f)*blend;
            }
            // Fixed anatomical lengths. Unreachable targets are clamped, never solved by growing bones.
            const float upperLeg=.405f,lowerLeg=.405f;
            var delta=foot-hip;float dz=footZ-hipZ;
            float distance=MathF.Sqrt(delta.Length*delta.Length+dz*dz);
            float maximum=upperLeg+lowerLeg-.0001f;
            if(distance>maximum)
            {
                float scale=maximum/distance;delta*=scale;dz*=scale;
                foot=hip+delta;footZ=hipZ+dz;distance=maximum;
            }
            distance=Math.Max(.0001f,distance);
            var direction=delta*(1/distance);float uz=dz/distance;
            float along=(upperLeg*upperLeg-lowerLeg*lowerLeg+distance*distance)/(2*distance);
            float bend=MathF.Sqrt(Math.Max(0,upperLeg*upperLeg-along*along));
            var bendForward=s==kickFoot && insidePassWeight>0?
                (forward+side*(kickFoot*.40f*insidePassWeight)).Unit:forward;
            float dot=Point.Dot(bendForward,direction);
            var pole=bendForward-direction*dot;float poleZ=-uz*dot;
            float magnitude=MathF.Sqrt(pole.Length*pole.Length+poleZ*poleZ);
            if(magnitude<.001f){pole=side;poleZ=0;magnitude=1;}
            var knee=hip+direction*along+pole*(bend/magnitude);
            float kneeZ=hipZ+uz*along+poleZ*bend/magnitude;
            Add(hip,hipZ,knee,kneeZ,.175f,shorts);
            Add(knee,kneeZ,foot,footZ,.12f,player.Team==0?"#8fe5f2":"#fff0d6");
            var bootDirection=(forward*(1-activity*.65f)+travel*(activity*.65f)).Unit;
            float bootPitch=stepPose.ToePitch*(1-strikeWeight)*(1-shuffle);
            if(kick.Active)bootDirection=bootDirection*(1-strikeWeight)+forward*strikeWeight;
            if(s==kickFoot && insidePassWeight>0)
            {
                float yaw=55*MathF.PI/180*insidePassWeight;
                bootDirection=forward*MathF.Cos(yaw)+side*(kickFoot*MathF.Sin(yaw));
            }
            if(bootDirection.Length<.01f)bootDirection=forward;else bootDirection=bootDirection.Unit;
            Add(foot,footZ,foot+bootDirection*(.24f*MathF.Cos(bootPitch)),
                footZ+.24f*MathF.Sin(bootPitch),.13f,"#17282b");
        }
        float locomotion=activity*(1-strikeWeight)*(1-header);
        float step=MathF.Cos(player.GaitPhase*MathF.Tau)*locomotion;
        float twist=step*(.10f+sprint*.15f)+opening+player.TurnLean*.12f;
        var shoulderSide=side*MathF.Cos(twist)+forward*MathF.Sin(twist);
        var chest=pelvis+forward*(.035f-draw*.14f+follow*.18f)+side*(step*(.018f+sprint*.016f)-effort*.045f*kickFoot-player.TurnLean*.055f*activity);
        chest+=forward*(tackleReach*.13f+missedTackle*.17f);
        if(jostling)chest+=side*(MathF.Sin(match.Seconds*8+player.Id*1.7f)*.045f)+forward*.025f;
        chest+=travel*(pushOff*.055f);
        if(!kick.Active && match.Seconds<player.ImpactUntil && !player.ImpactSlide)
        {
            float contact=MathF.Sin(Math.Clamp((match.Seconds-player.ImpactAt)/.48f,0,1)*MathF.PI);
            chest+=player.ImpactDirection*(contact*.07f);
        }
        float idleBreath=MathF.Sin(match.Seconds*1.8f+player.Id*.713f)*.0035f*(1-activity)*(1-strikeWeight);
        float chestZ=hipZ+.56f-follow*.045f+idleBreath;
        if(chestControl>0)
        {
            float absorb=Smooth(controlAge/.18f);
            chest+=forward*((-.12f*(1-absorb)+.025f*absorb)*chestControl);
            chestZ+=Math.Clamp(player.ChestHeight-1.40f,-.06f,.08f)*chestControl;
        }
        if(kick.Active && kind==StrikeKind.Chip)chest-=forward*(follow*.13f);
        // A header moves the ribcage towards the actual contact, not just the head.
        if(header>0)
        {
            var reach=headerPoint-chest;
            if(reach.Length>.16f)reach=reach.Unit*.16f;
            chest+=reach*(header*.55f);
            chestZ+=Math.Clamp(headerHeight-1.72f,0,.13f)*header*.55f;
        }
        if(!kick.Active && header<=0 && match.Owner==player.Id && !player.Keeper)
        {
            var reach=match.Ball-centre-forward*.4f;
            if(player.VisualSpeed>1)reach-=forward*Math.Min(0,Point.Dot(reach,forward));
            if(reach.Length<.7f)chest+=reach*.17f;
        }
        if(!kick.Active && match.Seconds<player.ShieldUntil){chest-=player.ShieldDirection*.12f;chestZ-=.045f;}
        if(!kick.Active && match.Seconds<player.ReceiveReadyUntil){chest+=player.ReceiveDirection*.04f;chestZ-=.025f;}
        Add(pelvis,hipZ,chest,chestZ,.40f,shirt);
        if(player.Keeper)
        {
        for(int s=-1;s<=1;s+=2)
        {
            var shoulder=chest+shoulderSide*(s*.215f);
            float armSwing=-step*s*(.19f+sprint*.25f);
            var elbow=chest+shoulderSide*(s*.32f)+forward*armSwing;
            var hand=chest+shoulderSide*(s*.30f)+forward*(armSwing*.95f+.07f+sprint*.13f);
            float shoulderRoll=step*s*.026f;
            float elbowZ=chestZ-.30f+shoulderRoll+Math.Abs(armSwing)*.20f;
            float handZ=chestZ-.48f+jog*.22f+sprint*.18f+Math.Max(0,armSwing)*.42f;
            float shoulderZ=chestZ-.06f+shoulderRoll;
            // Rotate anatomical segments, rather than stretching independent hand/elbow targets.
            if(!jostling)
            {
                float armAngle=-step*s*(.24f+jog*.42f+sprint*.18f)*(1-shuffle*.65f);
                float elbowFlex=.16f+activity*(.15f+jog*.9f+sprint*.5f);
                float spread=.045f+Math.Clamp(Math.Abs(player.TurnLean)*.08f+braking*.07f,0,.13f)*activity+shuffle*.16f+preparing*.08f;
                var upper=forward*MathF.Sin(armAngle)+side*(s*spread);
                float uz=-MathF.Cos(armAngle);
                float norm=MathF.Sqrt(upper.Length*upper.Length+uz*uz);
                elbow=shoulder+upper*(.27f/norm);elbowZ=shoulderZ+uz*.27f/norm;
                var lower=forward*MathF.Sin(armAngle+elbowFlex)+side*(s*spread*.6f);
                float lz=-MathF.Cos(armAngle+elbowFlex);
                norm=MathF.Sqrt(lower.Length*lower.Length+lz*lz);
                hand=elbow+lower*(.25f/norm);handZ=elbowZ+lz*.25f/norm;
            }
            if(jostling)
            {
                elbow=chest+shoulderSide*(s*.34f)+forward*.075f;
                hand=chest+shoulderSide*(s*.43f)+forward*(.13f+MathF.Sin(match.Seconds*8+player.Id+s)*.035f);
                elbowZ=1.2f+bob;handZ=1.13f+bob;
            }
            if(kick.Active)
            {
                // The opposite arm opens for balance, the striking-side arm counter-rotates.
                var strikeElbow=chest+shoulderSide*(s*(s*kickFoot<0?.38f:.30f))+forward*(s*kickFoot<0?draw*.12f-follow*.15f:-draw*.24f+follow*.12f);
                var strikeHand=chest+shoulderSide*(s*(s*kickFoot<0?.51f:.25f))+forward*(s*kickFoot<0?draw*.30f-follow*.25f:.12f+follow*.26f);
                elbow=elbow*(1-strikeWeight)+strikeElbow*strikeWeight;
                hand=hand*(1-strikeWeight)+strikeHand*strikeWeight;
                elbowZ=elbowZ*(1-strikeWeight)+(chestZ-(s*kickFoot<0?.20f:.33f))*strikeWeight;
                handZ=handZ*(1-strikeWeight)+(chestZ-(s*kickFoot<0?.26f:.24f))*strikeWeight;
            }
            float aerialBrace=header*.82f;
            float receptionBrace=Math.Max(chestControl,aerialBrace);
            if(receptionBrace>0)
            {
                var braceElbow=shoulder+side*(s*.22f)-forward*.07f;
                var braceHand=braceElbow+side*(s*.10f)+forward*.17f;
                elbow=elbow*(1-receptionBrace)+braceElbow*receptionBrace;
                elbowZ=elbowZ*(1-receptionBrace)+(shoulderZ-.12f)*receptionBrace;
                hand=hand*(1-receptionBrace)+braceHand*receptionBrace;
                handZ=handZ*(1-receptionBrace)+(shoulderZ-.20f)*receptionBrace;
            }
            if(guard>0 && !jostling)
            {
                var readyElbow=shoulder+side*(s*.20f)+forward*.10f;
                var readyHand=readyElbow+side*(s*.13f)+forward*.13f;
                elbow=elbow*(1-guard)+readyElbow*guard;elbowZ=elbowZ*(1-guard)+(shoulderZ-.15f)*guard;
                hand=hand*(1-guard)+readyHand*guard;handZ=handZ*(1-guard)+(shoulderZ-.31f)*guard;
            }
            if(tackleReach>0 && !jostling)
            {
                float balance=Math.Max(tackleReach,missedTackle);
                var counterElbow=shoulder+side*(s*.22f)-forward*.07f;
                var counterHand=counterElbow+side*(s*.09f)-forward*.18f;
                elbow=elbow*(1-balance)+counterElbow*balance;elbowZ=elbowZ*(1-balance)+(shoulderZ-.12f)*balance;
                hand=hand*(1-balance)+counterHand*balance;handZ=handZ*(1-balance)+(shoulderZ-.26f)*balance;
            }
            if(!kick.Active && !goalReaction && header<=0 && !player.Keeper && player.Team<2)
            {
                float call=match.Seconds<player.PassCallUntil && match.Owner!=player.Id?
                    Smooth((player.PassCallUntil-match.Seconds)/.3f)*Smooth((3-player.PassCallUntil+match.Seconds)/.25f):0;
                float shield=match.Seconds<player.ShieldUntil?Smooth((player.ShieldUntil-match.Seconds)/.15f):0;
                if((call>0 && (s==player.StrongFoot || player.AngryPassCall)) || shield>0)
                {
                    float weight=Math.Max(call,shield);
                    // Rotate fixed-length arm segments: a raised request or low protective arm.
                    float angle=call>0?(player.AngryPassCall?1.55f:2.25f):1.1f;
                    var gesture=side*(s*.85f)+forward*.35f;gesture=gesture.Unit;
                    var wantedElbow=shoulder+gesture*(MathF.Sin(angle)*.27f);
                    float wantedZ=shoulderZ-MathF.Cos(angle)*.27f;
                    elbow=elbow*(1-weight)+wantedElbow*weight;elbowZ=elbowZ*(1-weight)+wantedZ*weight;
                    hand=hand*(1-weight)+(wantedElbow+gesture*(MathF.Sin(angle+.35f)*.25f))*weight;
                    handZ=handZ*(1-weight)+(wantedZ-MathF.Cos(angle+.35f)*.25f)*weight;
                }
            }
            Add(shoulder,shoulderZ,elbow,elbowZ,.135f,shirt);
            Add(elbow,elbowZ,hand,handZ,.105f,officer?shirt:skin);
            // Stable three-part arms in every rig, including the referee.
            Add(hand,handZ,hand+forward*.03f,handZ,player.Keeper?.23f:.12f,player.Keeper?"#fff8dd":skin);
        }
        }
        else
        {
            // Establish both shoulder roots before FK reads the shared thorax
            // frame. Humerus, hinge forearm and wrist are then one linked chain.
            for(int s=-1;s<=1;s+=2)
            {
                var shoulder=chest+shoulderSide*(s*.215f);float z=chestZ-.06f;
                Add(shoulder,z,shoulder,z-.27f,.135f,shirt);
                Add(shoulder,z-.27f,shoulder,z-.52f,.105f,officer?shirt:skin);
                Add(shoulder,z-.52f,shoulder,z-.55f,.12f,skin);
            }
            for(int s=-1;s<=1;s+=2)
            {
                int arm=s<0?7:10;
                float armFlex=-step*s*(.24f+jog*.42f+sprint*.18f)*(1-shuffle*.65f)/ArmDegrees;
                float elbow=(.18f+activity*(.15f+jog*.9f+sprint*.5f))/ArmDegrees;
                float spread=7+Math.Clamp(Math.Abs(player.TurnLean)*7+braking*6,0,12)*activity+shuffle*13+preparing*6;
                var angles=new ArmAngles(armFlex,spread,0,elbow,10);
                if(jostling)angles=BlendArmAngles(angles,new(24,36,-10,86,18),.85f);
                if(kick.Active)
                {
                    float armStrike=strikeWeight;
                    if(match.WindupPlayer!=player.Id && !player.LastKickWasShot)
                        armStrike*=Smooth((match.Seconds-player.LastKickAt)/.065f);
                    angles=BlendArmAngles(angles,SampleStrikeArm(player,match,arm),armStrike);
                }
                float receptionBrace=Math.Max(chestControl,header*.82f);
                angles=BlendArmAngles(angles,new(23,53,-12,76,18),receptionBrace);
                if(!jostling)angles=BlendArmAngles(angles,new(22,32,-8,92,22),guard);
                if(tackleReach>0 && !jostling)
                    angles=BlendArmAngles(angles,new(-16,36,8,63,18),Math.Max(tackleReach,missedTackle));
                if(!kick.Active && !goalReaction && header<=0 && player.Team<2)
                {
                    float call=match.Seconds<player.PassCallUntil && match.Owner!=player.Id?
                        Smooth((player.PassCallUntil-match.Seconds)/.3f)*Smooth((3-player.PassCallUntil+match.Seconds)/.25f):0;
                    float shield=match.Seconds<player.ShieldUntil?Smooth((player.ShieldUntil-match.Seconds)/.15f):0;
                    if(call>0 && (s==player.StrongFoot || player.AngryPassCall))
                        angles=BlendArmAngles(angles,player.AngryPassCall?new(48,64,-10,88,35):new(132,28,4,28,45),call);
                    if(shield>0)angles=BlendArmAngles(angles,new(36,38,-4,68,28),shield);
                }
                ApplyArmFk(parts,arm,angles);
            }
        }
        float headDrop=goalReaction && player.Team!=match.Players[match.GoalScorerId].Team?.10f:0;
        // A small air gap is intentional: the stylised head is carried by the pose,
        // without the narrow neck that was prominent in the close replay camera.
        // Header contact must be earned by getting the body under the ball.
        var headerDelta=headerPoint-chest;
        if(headerDelta.Length>.14f)headerDelta=headerDelta.Unit*.14f;
        var head=chest+headerDelta*header;
        float headZ=chestZ+.22f-headDrop;
        headZ+=Math.Clamp(headerHeight-headZ-headerLeap,-.09f,.13f)*header;
        Add(head,headZ,head,headZ,.28f,skin);
        Add(head-forward*.01f,headZ+.10f,head-forward*.01f,headZ+.10f,.215f,hair);
        for(int eye=-1;eye<=1;eye+=2)
        {
            var eyePoint=head+forward*.151f+side*(eye*.052f);
            Add(eyePoint,headZ+.026f,eyePoint,headZ+.026f,.023f,"#243038");
        }
        float contestAge=match.Seconds-player.AerialContestAt+
            (player.AerialContestAt<=player.HeaderAt+.05f?bodyClockOffset:0);
        float contest=contestAge>=0?1-Smooth(contestAge/.4f):0;
        if(player.AerialChasing)contest*=Smooth(contestAge/.13f);
        float jump=Math.Max(headerLeap*header,Math.Max(.10f,player.HeaderLeap)*contest);
        if(jump>0)
            for(int i=0;i<parts.Count;i++)
            {var part=parts[i];parts[i]=part with{ZA=part.ZA+jump,ZB=part.ZB+jump};}
        if(!ApplyGoalCelebration(player,match,parts,forward,side))
        {
            ApplyActions(player,match,parts,forward,side);
            ApplyFootballEmotion(player,match,parts,forward,side);
            ApplyLegendaryFootballAnimation(player,match,parts);
        }
        // Keeper's separately solved glove contact retains its exact endpoint.
        // Authored outfield FK is repaired only if a legacy overlay invalidates it.
        if(!player.Keeper)RepairArmAnatomy(parts);
        return parts;
    }
    static float Smooth(float t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}
    // The contact still occurs at 0.26 s; the boot takes time to load, reach,
    // and recover. Use the same curve for torso and IK target to avoid a split pose.
    static float TacklePoseWeight(float age)=>Smooth((age-.035f)/.215f)*
        (1-Smooth((age-.34f)/.38f));
    static float ActionEnvelope(float age,float duration,float enter=.16f,float exit=.19f)=>
        Smooth(age/enter)*(1-Smooth((age-(duration-exit))/exit));
}