WorldActionPose.cs

Part of ReplayFigure, the procedural pose layer for the footballer rig. Takes the simulated match state and rewrites the body parts list for the current action: slides, injury falls, keeper saves and dives, feints and skill moves, shoulder duels, standing tackles, interceptions, headers and referee signals, keeping limb lengths fixed.

using System;
using System.Collections.Generic;
namespace Socer;

public static partial class ReplayFigure
{
    // Event parameters are captured by simulation, so all cameras and replays see
    // the same slide side, impact direction and recovery instead of new random poses.
    static void ApplyActions(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side)
    {
        void Move(int i,Point shift,float z)
        {var v=parts[i];parts[i]=v with{A=v.A+shift,B=v.B+shift,ZA=v.ZA+z,ZB=v.ZB+z};}
        void Foot(int leg,Point at,float z)
        {
            int i=leg*3;var thigh=parts[i];var shin=parts[i+1];
            var hip=thigh.A;float hipZ=thigh.ZA;
            var delta=at-hip;float dz=z-hipZ;
            float distance=MathF.Sqrt(delta.Length*delta.Length+dz*dz);
            const float length=.405f;
            if(distance>length*2-.0001f)
            {float scale=(length*2-.0001f)/distance;delta*=scale;dz*=scale;at=hip+delta;z=hipZ+dz;distance=length*2-.0001f;}
            distance=Math.Max(.0001f,distance);
            var direction=delta*(1/distance);float uz=dz/distance;
            float dot=Point.Dot(forward,direction);var pole=forward-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;}
            float bend=MathF.Sqrt(Math.Max(0,length*length-distance*distance*.25f));
            var knee=hip+delta*.5f+pole*(bend/magnitude);float kneeZ=hipZ+dz*.5f+poleZ*bend/magnitude;
            parts[i]=thigh with{B=knee,ZB=kneeZ};parts[i+1]=shin with{A=knee,ZA=kneeZ,B=at,ZB=z};
            var shoe=parts[i+2];parts[i+2]=shoe with{A=at,B=at+forward*.24f,ZA=z,ZB=z};
        }
        bool down=m.InjuryAnimationSeconds(p)<p.FallUntil ||
            (p.InjuryCannotContinue && !p.InjuryRemovedFromField);
        if(down){ApplyImpactFall(p,m,parts,forward,side);return;}
        float slideAge=m.Seconds-p.LastSlideAt;
        bool sliding=slideAge>=0 && slideAge<2.1f;
        if(sliding)
        {
            var slideForward=p.SlideDirection.Length>.01f?p.SlideDirection:forward;
            var slideSide=new Point(-slideForward.Y,slideForward.X);
            float enter=Smooth(slideAge/.22f),rise=Smooth((slideAge-.85f)/1.25f);
            float crouch=MathF.Sin(rise*MathF.PI);
            float pitch=(-1.12f*(1-Smooth(rise/.38f))+crouch*1.08f)*enter;
            var oldHip=parts[6].A;float oldZ=parts[6].ZA;
            float hipHeight=oldZ*(1-enter)+(.27f+(oldZ-.27f)*Smooth((rise-.30f)/.70f))*enter;
            (Point,float) SlideJoint(Point at,float z)
            {
                var local=at-oldHip;float along=Point.Dot(local,slideForward),across=Point.Dot(local,slideSide),height=z-oldZ;
                return (oldHip+slideSide*across+slideForward*(along*MathF.Cos(pitch)+height*MathF.Sin(pitch)),
                    hipHeight+height*MathF.Cos(pitch)-along*MathF.Sin(pitch));
            }
            // Upper body leans behind the leading boot, then folds over the planted knee.
            for(int i=0;i<parts.Count;i++)
            {var v=parts[i];var a=SlideJoint(v.A,v.ZA);var b=SlideJoint(v.B,v.ZB);parts[i]=v with{A=a.Item1,ZA=a.Item2,B=b.Item1,ZB=b.Item2};}
            int extended=p.SlideVariant==1?0:1;
            for(int leg=0;leg<2;leg++)
            {
                var wanted=p.Position+slideForward*((leg==extended?.65f:.12f)*(1-rise))+slideSide*((leg==0?-1:1)*(.18f+crouch*.04f));
                Foot(leg,parts[leg*3+1].B*(1-enter)+wanted*enter,.06f);
            }
            // Hand support is strongest in the middle of the recovery, not a standing snap.
            int arm=extended==0?10:7;
            if(rise>.05f && rise<.95f)
            {
                var v=parts[arm+1];var support=p.Position+slideForward*.30f+slideSide*(extended==0?.29f:-.29f);
                float weight=MathF.Sin(Math.Clamp((rise-.07f)/.58f,0,1)*MathF.PI)*.85f;var hand=v.B*(1-weight)+support*weight;float z=v.ZB*(1-weight)+.10f*weight;
                parts[arm+1]=v with{B=hand,ZB=z};parts[arm+2]=parts[arm+2] with{A=hand,B=hand+slideForward*.03f,ZA=z,ZB=z};
            }
            return;
        }
        // Saves move the pelvis and upper body together and solve fixed-length
        // limbs before ordinary locomotion leans can alter the action pose.
        if(p.Keeper && ApplyKeeperSave(p,m,parts,forward,side))return;
        bool dive=p.Keeper && (p.DiveRemaining>0 || (m.Seconds>=p.DiveEndedAt &&
            m.Seconds-p.DiveEndedAt<HumanFigure.DiveRecoverySeconds(p,m)));
        if(m.Seconds<p.NutmeggedUntil)
        {
            float react=Smooth((p.NutmeggedUntil-m.Seconds)/.16f);
            Foot(0,p.Position-side*(.37f*react)-forward*.06f,.06f);
            Foot(1,p.Position+side*(.37f*react)-forward*.06f,.06f);
        }
        float feintDuration=p.FeintVariant==3?.85f:p.FeintVariant==4?SkillMoves.Get(p.SkillMove).Duration:.65f;
        float feintAge=m.Seconds-p.FeintStartedAt;
        // Interrupted skills retain a short release. A lost ball or a defender
        // ending FeintUntil early must not replace a raised boot in one frame.
        float feintRelease=1-Smooth((m.Seconds-p.FeintUntil)/.18f);
        float feint=feintAge>=0 && feintAge<feintDuration && feintRelease>0?
            Math.Clamp(feintAge/feintDuration,0,1):0;
        float feintWeight=feint>0?ActionEnvelope(feintAge,feintDuration)*feintRelease:0;
        if(!down && !sliding && !dive)
        {
            float sway=feint>0?MathF.Sin(feint*MathF.Tau)*(p.FeintVariant==2?.21f:.13f)*feintWeight:0;
            float stumble=m.Seconds<p.StumbleUntil?MathF.Sin((p.StumbleUntil-m.Seconds)*MathF.PI/.5f)*.18f:0;
            float lean=Math.Clamp(p.VisualSpeed*.030f,0,.20f)+stumble;
            var travel=p.VisualVelocity.Length>.1f?p.VisualVelocity.Unit:forward;
            var inertia=p.VisualAcceleration*.016f;
            if(inertia.Length>.14f)inertia=inertia.Unit*.14f;
            var bodyLean=travel*lean+inertia;
            if(feint>0 && p.FeintVariant==3)bodyLean+=side*(-p.ScissorsSide*MathF.Sin(feint*MathF.Tau)*.28f)*feintWeight;
            if(p.InterceptBody && m.Seconds<p.InterceptUntil)
                bodyLean+=(p.InterceptAim-p.Position).Unit*(.20f*Smooth((m.Seconds-p.InterceptAt-.05f)/.13f));
            bool inShoulderDuel=m.BodyChallenge && (p.Id==m.DuelDefender || p.Id==m.DuelCarrier);
            float shoulderBlend=inShoulderDuel?
                Smooth((m.Seconds-m.DuelStartedAt)/.13f)*Smooth(m.DuelRemaining/.16f):0;
            float shoulderAfter= !inShoulderDuel && m.Seconds<p.BodyLeanUntil?
                Smooth((m.Seconds-(p.BodyLeanUntil-.38f))/.10f)*
                (1-Smooth((m.Seconds-(p.BodyLeanUntil-.16f))/.16f)):0;
            bodyLean+=p.BodyLeanDirection*(.22f+m.BodyPressureStrength*.65f)*Math.Max(shoulderBlend,shoulderAfter);
            float strikeSettle=StrikeBlend(p,m);
            lean*=1-strikeSettle*.75f;bodyLean*=1-strikeSettle*.75f;
            // A feint is a lean about the pelvis, not a translation of the
            // entire upper rig away from the hip sockets.
            bodyLean+=side*(sway/.56f);
            int upperEnd=parts.Count; // Eyes follow the same rigid header/torso tilt as the head.
            for(int i=6;i<upperEnd;i++)
            {
                var v=parts[i];
                var pivot=parts[6].A;float pivotZ=parts[6].ZA;
                (Point,float) Tilt(Point at,float z)
                {
                    float angle=MathF.Atan(bodyLean.Length);var axis=bodyLean.Unit;
                    var d=at-pivot;float along=Point.Dot(d,axis),height=z-pivotZ;
                    return (at+axis*(along*(MathF.Cos(angle)-1)+height*MathF.Sin(angle)),
                        pivotZ+height*MathF.Cos(angle)-along*MathF.Sin(angle));
                }
                var a=Tilt(v.A,v.ZA);var b=Tilt(v.B,v.ZB);
                parts[i]=v with{A=a.Item1,ZA=a.Item2,B=b.Item1,ZB=b.Item2};
            }
            if(inShoulderDuel && shoulderBlend>.001f)
            {
                // Brace the outside boot and hold the near arm across the other
                // player's shoulder instead of playing an ordinary running pose.
                var other=m.Players[p.Id==m.DuelDefender?m.DuelCarrier:m.DuelDefender];
                var contact=(other.Position-p.Position).Unit;
                int arm=Point.Dot(contact,side)<0?7:10;
                int braceLeg=arm==7?1:0;
                var resting=parts[braceLeg*3+1];
                Foot(braceLeg,resting.B*(1-shoulderBlend)+
                    (p.Position-contact*.28f-forward*.10f)*shoulderBlend,
                    resting.ZB*(1-shoulderBlend)+.06f*shoulderBlend);
                var upper=parts[arm];var shoulder=upper.A;
                var elbow=shoulder+contact*.20f+forward*.05f;
                float elbowZ=upper.ZA-.12f;
                var hand=elbow+contact*.12f-forward*.06f;
                float handZ=elbowZ-.10f;
                elbow=upper.B*(1-shoulderBlend)+elbow*shoulderBlend;
                elbowZ=upper.ZB*(1-shoulderBlend)+elbowZ*shoulderBlend;
                var oldHand=parts[arm+1];
                hand=oldHand.B*(1-shoulderBlend)+hand*shoulderBlend;
                handZ=oldHand.ZB*(1-shoulderBlend)+handZ*shoulderBlend;
                parts[arm]=upper with{B=elbow,ZB=elbowZ};
                parts[arm+1]=oldHand with{A=elbow,ZA=elbowZ,B=hand,ZB=handZ};
                parts[arm+2]=parts[arm+2] with{A=hand,ZA=handZ,B=hand+forward*.03f,ZB=handZ};
            }
            if(feint>0)
            {
                if(p.FeintVariant==3)
                {
                    // Alternate boots circle the ball, followed by the outside-foot cut.
                    float cycle=Math.Clamp(feint/.62f,0,1)*2;
                    if(feint<.62f)
                    {
                        int step=Math.Min(1,(int)cycle);
                        int sign=step==0?-p.ScissorsSide:p.ScissorsSide;
                        float t=cycle-step;
                        int leg=sign<0?0:1;
                        float stepWeight=MathF.Sin(t*MathF.PI)*feintWeight;
                        var rest=parts[leg*3+1].B;
                        var boot=p.Position+forward*(.15f+MathF.Sin(t*MathF.PI)*.23f)+side*(sign*MathF.Cos(t*MathF.PI)*.25f);
                        Foot(leg,rest*(1-stepWeight)+boot*stepWeight,
                            parts[leg*3+1].ZB*(1-stepWeight)+(.07f+MathF.Sin(t*MathF.PI)*.16f)*stepWeight);
                    }
                    // Twist shoulders and bent arms together, preserving segment lengths.
                    float yaw=-p.ScissorsSide*MathF.Sin(feint*MathF.Tau)*.26f*feintWeight;
                    RotateFootballUpperBody(parts,yaw);
                }
                else if(p.FeintVariant==4)
                {
                    int leadingSign=p.SkillMove is SkillMoveKind.ReverseElastico or SkillMoveKind.CruyffTurn?
                        -p.StrongFoot:p.StrongFoot;
                    int lead=leadingSign<0?0:1;
                    var target=FootballSkillTarget(p,feint,forward,side,leadingSign);
                    var resting=parts[lead*3+1];
                    var boot=target.ground;float height=target.height;
                    // The authored action meets the recorded ball touch before
                    // it continues along its own recovery arc. Captured offsets
                    // follow the moving runner and survive replay sampling.
                    float contactAge=m.Seconds-p.LastDribbleTouch;
                    if(contactAge>=0 && contactAge<.17f && p.DribbleContactOffset.Length<.56f)
                    {
                        float contact=Smooth(contactAge/.075f)*(1-Smooth((contactAge-.085f)/.10f))*.45f;
                        boot=boot*(1-contact)+(p.Position+p.DribbleContactOffset-forward*.12f)*contact;
                        height=height*(1-contact)+.085f*contact;
                    }
                    Foot(lead,resting.B*(1-feintWeight)+boot*feintWeight,
                        resting.ZB*(1-feintWeight)+height*feintWeight);
                    // Weight remains over a planted opposite boot. Only the
                    // rainbow's gathered two-foot flick has an airborne support.
                    var support=parts[(1-lead)*3+1];
                    float supportWeight=feintWeight*(1-Smooth(p.VisualSpeed/4));
                    var supportAt=p.Position-side*(leadingSign*.19f)-forward*.10f;
                    float supportZ=.06f;
                    if(p.SkillMove==SkillMoveKind.RainbowFlick)
                    {
                        float gather=MathF.Sin(Math.Clamp(feint/.65f,0,1)*MathF.PI);
                        supportAt+=forward*(-.12f*gather);supportZ+=.13f*gather;
                    }
                    if(p.SkillMove==SkillMoveKind.LaCroqueta && feint>.48f)
                    {
                        float exchange=MathF.Sin(Math.Clamp((feint-.48f)/.45f,0,1)*MathF.PI)*feintWeight;
                        var exchangeAt=p.Position+forward*.22f+side*(p.SkillSide*.24f);
                        supportAt=supportAt*(1-exchange)+exchangeAt*exchange;
                        supportWeight=Math.Max(supportWeight,exchange);supportZ=.06f+.07f*exchange;
                    }
                    Foot(1-lead,support.B*(1-supportWeight)+supportAt*supportWeight,
                        support.ZB*(1-supportWeight)+supportZ*supportWeight);
                    if(target.turn!=0)
                    {
                        RotateFootballUpperBody(parts,target.turn*feintRelease);
                    }
                }
                else
                {
                    int leg=p.FeintVariant==1?0:1;
                    float around=MathF.Sin(feint*MathF.PI);
                    var resting=parts[leg*3+1];
                    var target=p.Position+forward*(.28f+around*.12f)+side*(MathF.Sin(feint*MathF.Tau)*.27f);
                    Foot(leg,resting.B*(1-feintWeight)+target*feintWeight,
                        resting.ZB*(1-feintWeight)+(.06f+around*.13f)*feintWeight);
                }
            }
            if(m.Seconds-p.HeaderAt>.6f && m.Seconds-p.LastScanAt<.35f && p.Team<2)
            {
                var glance=side*(MathF.Sin((m.Seconds-p.LastScanAt)*MathF.PI/.35f)*.035f);
                for(int head=13;head<parts.Count;head++)Move(head,glance,0);
            }
            if(p.Team==2 && m.RefereeSignalUntil>m.Seconds && m.ShownCard>0)
            {
                parts[10]=parts[10] with{B=p.Position+side*.4f,ZB=1.65f};
                parts[11]=parts[11] with{A=parts[10].B,ZA=1.65f,B=p.Position+side*.42f,ZB=2};
                parts[12]=parts[12] with{A=parts[11].B,B=parts[11].B+forward*.03f,ZA=2,ZB=2};
            }
            bool releasing=m.KeeperKickPlayer==p.Id && m.Seconds-m.KeeperKickAt>MatchSimulation.KeeperDropAt;
            if(p.Keeper && !releasing && m.Seconds-p.LastPuntAt>.65f)
            {
                for(int s=0;s<2;s++)
                {
                    int sign=s==0?-1:1;
                    KeeperArm(parts,7+s*3,m.GlovePosition(p,sign),m.GloveHeight(p),
                        new Point(),-1,forward,0);
                }
            }
            float tackleAge=m.Seconds-p.StandingTackleAt;
            if(tackleAge>=0 && tackleAge<.72f && m.Seconds-p.LastSlideAt>=2.1f)
            {
                // A successful touch can flow straight into a pass. Its new
                // contact owns the boot while the old tackle layer releases.
                float priority=1-StrikeBlend(p,m);
                float reach=TacklePoseWeight(tackleAge)*priority;
                float prepare=Smooth(tackleAge/.085f)*(1-Smooth((tackleAge-.08f)/.14f))*priority;
                var aim=p.StandingTackleAim-p.Position;
                if(aim.Length>.78f)aim=aim.Unit*.78f;
                var target=p.Position+aim;
                if(p.StandingTackleVariant==1)target+=side*(p.StandingTackleFoot<0?-.20f:.20f);
                if(p.StandingTackleVariant==2)target+=forward*.12f;
                int leg=p.StandingTackleFoot<0?0:1;
                var planted=parts[(1-leg)*3+1].B;
                if(reach>.001f)Foot(1-leg,planted,.06f);
                var boot=parts[leg*3+1].B;
                var windup=forward*(-.09f*prepare);
                Foot(leg,boot*(1-reach)+target*reach+windup,
                    parts[leg*3+1].ZB*(1-reach)+(.07f+(p.StandingTackleVariant==2?.18f:.08f))*reach);
            }
            if(m.Seconds<p.ImpactUntil && !p.ImpactSlide)
            {
                float contact=(1-Smooth((m.Seconds-p.ImpactAt)/.32f))*(1-StrikeBlend(p,m));
                int leg=p.KickFoot<0?0:1;var resting=parts[leg*3+1];
                Foot(leg,resting.B*(1-contact)+p.ImpactPoint*contact,
                    resting.ZB*(1-contact)+.09f*contact);
            }
            if(!p.InterceptBody && m.Seconds<p.InterceptUntil && m.Seconds>=p.InterceptAt)
            {
                float age=m.Seconds-p.InterceptAt;
                float weight=Smooth((age-.05f)/.13f)*(1-Smooth((age-.30f)/.20f))*(1-StrikeBlend(p,m));
                var left=parts[1].B;var right=parts[4].B;
                float leftZ=parts[1].ZB,rightZ=parts[4].ZB;
                for(int i=0;i<parts.Count;i++)Move(i,new Point(),-.12f*weight);
                Foot(0,left,leftZ);Foot(1,right,rightZ);
                int leg=p.InterceptFoot<0?0:1;
                var rest=leg==0?left:right;
                Foot(leg,rest*(1-weight)+p.InterceptAim*weight,(leg==0?leftZ:rightZ)*(1-weight)+p.InterceptHeight*weight);
            }

            return;
        }
        Point axis=down?p.FallDirection:sliding?p.SlideDirection:(p.DiveTarget-p.DiveOrigin).Unit;
        if(axis.Length<.01f)axis=forward;
        var lateral=new Point(-axis.Y,axis.X);
        float blend=down?HumanFigure.FallBlend(p,m):sliding?Smooth(slideAge/.16f)*(1-Smooth((slideAge-.5f)/.4f)):HumanFigure.DiveBlend(p,m);
        float angle=blend*(down?1.48f:sliding?1.17f:1.35f);
        float lift=dive && p.DiveRemaining>0?MathF.Sin(Math.Clamp(p.DiveAge/.9f,0,1)*MathF.PI)*(.12f+Math.Min(2.2f,p.DiveHeight)*.35f):0;
        if(sliding)
        {
            int extended=p.SlideVariant==1?0:1;
            Foot(extended,p.Position+axis*(.48f+Math.Min(7,p.SlideSpeed)*.035f),.1f);
            Foot(1-extended,p.Position-axis*.2f+lateral*(extended==0?.25f:-.25f),.18f);
        }
        if(down)
        {
            float sign=p.FallVariant%2==0?1:-1;
            Move(7,lateral*(sign*.13f*blend),-.05f*blend);
            Move(8,lateral*(sign*.2f*blend),-.12f*blend);
            Foot(p.FallVariant%2,p.Position+lateral*(sign*.3f)-axis*.22f,.1f);
        }
        (Point,float) Joint(Point at,float z)
        {
            var local=at-p.Position;float ahead=Point.Dot(local,axis),across=Point.Dot(local,lateral);
            float roll=down && p.FallVariant==3?MathF.Sin(blend*MathF.PI)*.23f:0;
            var ground=p.Position+lateral*(across+roll)+axis*(ahead*MathF.Cos(angle)+(z-.45f)*MathF.Sin(angle));
            float height=.05f+z*MathF.Cos(angle)-ahead*MathF.Sin(angle)+lift;
            return (ground,Math.Max(.08f,height));
        }
        for(int i=0;i<parts.Count;i++)
        {var v=parts[i];var a=Joint(v.A,v.ZA);var b=Joint(v.B,v.ZB);parts[i]=v with{A=a.Item1,ZA=a.Item2,B=b.Item1,ZB=b.Item2};}
        if(sliding && m.Seconds<p.ImpactUntil && m.Seconds-p.ImpactAt<.22f)
            Foot(p.SlideVariant==1?0:1,p.ImpactPoint,.10f);
    }

}