LegendaryFootballAnimation.cs

Pose layer for the legendary football ability animations. Holds a table of motion clips with anticipation, contact and recovery timings, samples a frame from the footballer's ability state and ball trajectory, then applies procedural limb, torso and foot IK to a list of figure parts for each of the eight abilities.

using System;
using System.Collections.Generic;

namespace Socer;

public sealed record LegendaryMotionClip(LegendaryAbility Kind,string CatalogId,string Name,
    float Anticipation,float Contact,float Recovery,float Duration,string ContactSurface);

// Original football choreography, not imported game motion. Times are event
// seconds; render sampling never changes root simulation or consumes its RNG.
public static class LegendaryFootballAnimation
{
    static readonly LegendaryMotionClip[] clips={
        new(LegendaryAbility.LaserFinish,"PRO-12","AN_Haaland_PlantedDrive",.42f,.42f,.62f,1.16f,"instep"),
        new(LegendaryAbility.OrderSeven,"PRO-24","AN_Mbappe_TwoRunnerSignal",.14f,.24f,.42f,1.46f,"pass then two run cues"),
        new(LegendaryAbility.OtherGravity,"PRO-23","AN_Messi_DoubleTouchEscape",.12f,.30f,.34f,.94f,"inside boots"),
        new(LegendaryAbility.RoyalReception,"PRO-54","AN_Zlatan_ScissorVolley",.30f,0,1.30f,1.72f,"raised instep"),
        new(LegendaryAbility.ChessPass,"PRO-55","AN_Pirlo_DisguisedRelease",.18f,.20f,.52f,1.14f,"inside foot"),
        new(LegendaryAbility.EntryDenied,"PRO-56","AN_Maldini_LaneInterception",.20f,.42f,.48f,1.18f,"inside foot"),
        new(LegendaryAbility.NotToday,"PRO-57","AN_Buffon_RecoveryReset",.10f,.34f,.42f,1.24f,"boot plant and set"),
        new(LegendaryAbility.SkySiu,"PRO-53","AN_Ronaldo_KneeDriveHeader",.30f,0,.64f,.94f,"forehead")
    };
    public static IReadOnlyList<LegendaryMotionClip> Clips=>clips;
    public static LegendaryMotionClip Get(LegendaryAbility kind)
    {
        int i=(int)kind-1;return (uint)i<(uint)clips.Length?clips[i]:null;
    }
    public readonly record struct Frame(LegendaryAbility Kind,float Time,float Weight,Point Contact,float ContactHeight,bool Anticipating)
    {public bool Active=>Weight>0;}
    static float Ease(float t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}
    public static Frame Sample(Footballer p,MatchSimulation m)
    {
        var identity=LegendaryAbilities.Get(p.Profile);
        bool committed=p.IsAcrobaticCommitted(m.Seconds);
        if(identity==null || p.SentOff || !p.InPlay || m.GoalHoldRemaining>0 && !committed ||
            m.InjuryAnimationSeconds(p)<p.FallUntil || m.Seconds-p.LastSlideAt<2.1f)return default;
        var kind=identity.Kind;float time=m.Seconds-p.LegendaryActivatedAt;
        if(kind==LegendaryAbility.RoyalReception && p.AcrobaticUntil>p.AcrobaticAt && p.AcrobaticUntil>0)
            time=m.Seconds-p.AcrobaticAt;
        bool active=p.ActiveLegendaryAbility==kind && time>=0 && (committed || m.Seconds<p.LegendaryActiveUntil);
        Point contact=p.HeaderContactPoint;float height=p.HeaderContactHeight;
        bool anticipate=false;
        if(kind is LegendaryAbility.RoyalReception or LegendaryAbility.SkySiu)
        {
            if(!active && m.LegendaryReady(p,kind) && m.Owner<0 && m.LastTouch==p.Team)
            {
                float arriving=p.VisualHeaderAt-m.Seconds;
                if(arriving>=0 && arriving<=.30f && (p.VisualHeaderPoint-p.Position).Length<.60f)
                {time=-arriving;contact=p.VisualHeaderPoint;height=p.VisualHeaderHeight;anticipate=true;}
                else if(kind==LegendaryAbility.RoyalReception && m.BallHeight is >=1.55f and <=2.5f)
                {
                    float velocity=Point.Dot(m.BallVelocity,m.BallVelocity);
                    float t=Point.Dot(p.Position-m.Ball,m.BallVelocity)/Math.Max(1,velocity);
                    var point=m.Ball+m.BallVelocity*t;
                    if(velocity>9 && t is >=0 and <=.30f && (point-p.Position).Length<.45f &&
                        (p.Position-new Point(m.Direction(p.Team)>0?105:0,34)).Length<26)
                    {time=-t;contact=point;height=m.BallHeight+m.VerticalSpeed*t-4.905f*t*t;anticipate=true;}
                }
            }
            if(!active && !anticipate)return default;
            if(height<1.5f || height>2.55f)return default;
        }
        else if(!active)return default;
        var clip=Get(kind);
        if(time>clip.Duration)return default;
        float weight=anticipate?Ease((time+clip.Anticipation)/clip.Anticipation):
            kind is LegendaryAbility.RoyalReception or LegendaryAbility.SkySiu?1:
            Ease(time/(kind==LegendaryAbility.NotToday?.15f:.12f));
        weight*=1-Ease((time-(clip.Duration-.22f))/.22f);
        return new(kind,time,weight,contact,height,anticipate);
    }
}

public static partial class ReplayFigure
{
    // Final pose layer. The owner invokes it after ordinary football actions,
    // before the shared anatomical validator. Goal/injury/save contact wins.
    public static bool ApplyLegendaryFootballAnimation(Footballer p,MatchSimulation m,List<Part> parts)
    {
        var frame=LegendaryFootballAnimation.Sample(p,m);
        if(!frame.Active || parts.Count<17)return false;
        var forward=new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing));
        var side=new Point(-forward.Y,forward.X);
        bool save=p.Keeper && (p.DiveRemaining>0 || m.KeeperBlockPoseWeight(p)>.001f);
        if(save)return false;
        if(frame.Kind==LegendaryAbility.EntryDenied &&
            (m.WindupPlayer==p.Id || m.Seconds-p.LastKickAt<.40f))return false;
        if(frame.Kind is not (LegendaryAbility.RoyalReception or LegendaryAbility.SkySiu or LegendaryAbility.OtherGravity or LegendaryAbility.EntryDenied) &&
            (m.Seconds<p.FeintUntil || m.Seconds<p.StandingTackleAt+.72f || m.Seconds<p.InterceptUntil))return false;
        // Older lateral skill layers shifted the shirt's root while leaving
        // both hip sockets behind. Rejoin the whole ribcage before adding any
        // signature lean; the original bone lengths and attachments survive.
        var hipCentre=(parts[0].A+parts[3].A)*.5f;
        float hipHeight=(parts[0].ZA+parts[3].ZA)*.5f;
        var join=hipCentre-parts[6].A;float joinZ=hipHeight-parts[6].ZA;
        for(int i=6;i<parts.Count;i++)
        {var v=parts[i];parts[i]=v with{A=v.A+join,B=v.B+join,ZA=v.ZA+joinZ,ZB=v.ZB+joinZ};}
        switch(frame.Kind)
        {
            case LegendaryAbility.LaserFinish:LegendPlantedDrive(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.OrderSeven:LegendCommander(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.OtherGravity:LegendDoubleTouch(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.RoyalReception:LegendScissorVolley(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.ChessPass:LegendDisguisedPass(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.EntryDenied:LegendLaneIntercept(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.NotToday:LegendKeeperReset(p,m,parts,forward,side,frame);break;
            case LegendaryAbility.SkySiu:LegendPowerHeader(p,m,parts,forward,side,frame);break;
        }
        KeepKeeperAboveGround(parts);
        return true;
    }

    static float LegendPulse(float time,float start,float peak,float end)=>time<peak?
        Smooth((time-start)/(peak-start)):1-Smooth((time-peak)/(end-peak));
    static void LegendTranslate(List<Part> parts,Point shift,float height)
    {
        for(int i=0;i<parts.Count;i++)
        {var v=parts[i];parts[i]=v with{A=v.A+shift,B=v.B+shift,ZA=v.ZA+height,ZB=v.ZB+height};}
    }
    static void LegendTorso(List<Part> parts,Point forward,Point side,float pitch,float yaw=0,float roll=0)
    {
        var root=parts[6].A;float rootZ=parts[6].ZA;
        (Point,float) Rotate(Point at,float z)
        {
            var d=at-root;float a=Point.Dot(d,forward),s=Point.Dot(d,side),h=z-rootZ;
            float a1=a*MathF.Cos(pitch)+h*MathF.Sin(pitch),h1=h*MathF.Cos(pitch)-a*MathF.Sin(pitch);
            float s1=s*MathF.Cos(roll)+h1*MathF.Sin(roll),h2=h1*MathF.Cos(roll)-s*MathF.Sin(roll);
            return (root+forward*(a1*MathF.Cos(yaw)-s1*MathF.Sin(yaw))+side*(s1*MathF.Cos(yaw)+a1*MathF.Sin(yaw)),rootZ+h2);
        }
        for(int i=6;i<parts.Count;i++)
        {var v=parts[i];var a=Rotate(v.A,v.ZA);var b=Rotate(v.B,v.ZB);parts[i]=v with{A=a.Item1,ZA=a.Item2,B=b.Item1,ZB=b.Item2};}
    }
    static void LegendLower(List<Part> parts,Point forward,float amount)
    {
        var a=parts[1];var b=parts[4];
        LegendTranslate(parts,new Point(),-amount);
        KeeperLeg(parts,0,a.B,a.ZB,forward,0,forward,0);
        KeeperLeg(parts,1,b.B,b.ZB,forward,0,forward,0);
    }
    static void LegendFoot(List<Part> parts,int leg,Point target,float height,Point pole,float weight,Point boot,bool highArc=false)
    {
        var old=parts[leg*3+1];
        var hip=parts[leg*3];
        var from=new System.Numerics.Vector3(old.B.X-hip.A.X,old.B.Y-hip.A.Y,old.ZB-hip.ZA);
        var to=new System.Numerics.Vector3(target.X-hip.A.X,target.Y-hip.A.Y,height-hip.ZA);
        float reach=from.Length()*(1-weight)+to.Length()*weight;
        // A high boot swings around its hip. Interpolating ankle positions
        // directly between ground and overhead passes through the hip socket,
        // forcing the knee to flip its plane at zero reach.
        var preferred=new System.Numerics.Vector3(boot.X,boot.Y,0);
        var v=BlendFallBoneDirection(from,to,weight,Math.Max(.08f,reach),preferred);
        if(highArc)
        {
            // The deliberate forward passage fixes the hemisphere of a nearly
            // opposed ankle arc; tiny incoming-ball offsets cannot reverse it.
            var middle=System.Numerics.Vector3.Normalize(preferred+new System.Numerics.Vector3(pole.X,pole.Y,0)*.12f)*Math.Max(.08f,reach);
            v=weight<.5f?BlendFallBoneDirection(from,middle,weight*2,Math.Max(.08f,reach),preferred):
                BlendFallBoneDirection(middle,to,(weight-.5f)*2,Math.Max(.08f,reach),preferred);
        }
        KeeperLeg(parts,leg,hip.A+new Point(v.X,v.Y),hip.ZA+v.Z,
            pole,0,boot,0);
    }
    static void LegendHeadTurn(List<Part> parts,float angle)
    {
        var centre=parts[13].A;
        for(int i=13;i<parts.Count;i++)
        {
            var v=parts[i];
            Point Turn(Point at){var d=at-centre;return centre+new Point(d.X*MathF.Cos(angle)-d.Y*MathF.Sin(angle),d.X*MathF.Sin(angle)+d.Y*MathF.Cos(angle));}
            parts[i]=v with{A=Turn(v.A),B=Turn(v.B)};
        }
    }
    static void LegendBalanceArms(List<Part> parts,int strong,float drive,float weight)
    {
        for(int arm=0;arm<2;arm++)
        {
            int sign=arm==0?-1:1;
            bool opposite=sign!=strong;
            ApplyArmFk(parts,7+arm*3,new ArmAngles(opposite?32+drive*30:-12+drive*12,
                opposite?30:22,opposite?-15:8,opposite?85-drive*20:100,0,-5),weight);
        }
    }

    static void LegendPlantedDrive(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        float t=f.Time,w=f.Weight;
        float contact=m.WindupPlayer==p.Id?Math.Max(t+.02f,t+m.WindupRemaining):.42f;
        float draw=Smooth(t/Math.Max(.1f,contact))*(1-Smooth((t-contact)/.18f));
        float through=LegendPulse(t,contact-.03f,contact+.15f,contact+.62f);
        LegendLower(parts,forward,(.055f*draw+.025f*through)*w);
        LegendTorso(parts,forward,side,(.08f*draw+.14f*through)*w,
            p.KickFoot*(-.12f*draw+.22f*through)*w);
        // The shared strike rig already contains phase-linked backswing,
        // contralateral balance and across-body follow-through. Carry those
        // arms with the loaded torso instead of replacing them with a pose.
        // The supporting sole remains beside the ball during the loaded drive.
        int support=p.KickFoot<0?1:0;
        LegendFoot(parts,support,p.Position-side*(p.KickFoot*.19f)-forward*.05f,.065f,forward,w*(1-Smooth((t-contact-.16f)/.25f)),forward);
    }
    static void LegendCommander(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        if(m.WindupPlayer==p.Id)return;
        float first=LegendPulse(f.Time,.06f,.30f,.69f),second=LegendPulse(f.Time,.52f,.82f,1.23f);
        var aimA=p.LegendaryTargetA-p.Position;var aimB=p.LegendaryTargetB-p.Position;
        float Yaw(Point aim)=>aim.Length>.1f?Math.Clamp(MathF.Atan2(Point.Dot(aim,side),Point.Dot(aim,forward)),-.60f,.60f):0;
        float yaw=(Yaw(aimA)*first+Yaw(aimB)*second)/Math.Max(.001f,first+second);
        float cue=Math.Max(first,second)*f.Weight;
        LegendTorso(parts,forward,side,.025f*cue,yaw*.35f*cue);
        LegendHeadTurn(parts,yaw*.45f*cue);
        int firstArm=Point.Dot(aimA,side)<0?7:10,secondArm=Point.Dot(aimB,side)<0?7:10;
        for(int arm=7;arm<=10;arm+=3)
        {
            ApplyArmFk(parts,arm,new ArmAngles(8,18,0,100),cue*.65f);
            float signal=Math.Max(arm==firstArm?first:0,arm==secondArm?second:0)*f.Weight;
            ApplyArmFk(parts,arm,new ArmAngles(78,24,5,20,0,-7),signal);
        }
    }
    static void LegendDoubleTouch(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        float t=Math.Clamp(f.Time/.70f,0,1),w=f.Weight;
        float load=LegendPulse(f.Time,0,.18f,.72f),change=MathF.Sin(t*MathF.Tau);
        int exit=p.SkillSide==0?1:p.SkillSide;
        LegendLower(parts,forward,.105f*load*w);
        LegendTorso(parts,forward,side,.08f*load*w,-exit*.18f*change*w,exit*.14f*change*w);
        // Shoulders feint away from the exit; elbows stay compact and bent.
        for(int arm=0;arm<2;arm++)
        {
            int sign=arm==0?-1:1;
            ApplyArmFk(parts,7+arm*3,new ArmAngles(10+sign*exit*change*18,22+Math.Abs(change)*9,
                sign*exit*10,92+sign*exit*change*12,0,-3),w*load);
        }
    }
    static void LegendDisguisedPass(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        float pass=m.Seconds-p.LastKickAt;
        float release=LegendPulse(f.Time,.02f,.24f,.90f);
        if(pass>=0 && p.LastKickAt>=p.LegendaryActivatedAt-.30f)
            release=Math.Max(release,Smooth(pass/.06f)*(1-Smooth((pass-.30f)/.35f)));
        float scan=LegendPulse(f.Time,.02f,.18f,.55f);
        int foot=p.KickFoot;float w=f.Weight;
        LegendTorso(parts,forward,side,.035f*release*w,-foot*.17f*release*w);
        LegendHeadTurn(parts,foot*.34f*scan*w);
        ApplyArmFk(parts,foot<0?10:7,new ArmAngles(32,25,-8,52,10,-7),release*w);
        ApplyArmFk(parts,foot<0?7:10,new ArmAngles(4,12,0,96),release*w);
    }
    static void LegendLaneIntercept(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        float t=f.Time,w=f.Weight;
        float brace=LegendPulse(t,0,.32f,.98f);
        LegendLower(parts,forward,.095f*brace*w);
        var direction=p.LegendaryTargetA-p.Position;
        int reach=Point.Dot(direction,side)>=0?1:-1;
        LegendTorso(parts,forward,side,.07f*brace*w,-reach*.20f*brace*w,reach*.10f*brace*w);
        ApplyArmFk(parts,reach>0?7:10,new ArmAngles(20,38,-5,85),brace*w);
        ApplyArmFk(parts,reach>0?10:7,new ArmAngles(12,24,5,100),brace*w);
        // Approach remains live locomotion. Extend only on its actual contact.
        float age=m.Seconds-p.InterceptAt;
        float touch=p.InterceptUntil>m.Seconds && age>=0?LegendPulse(age,0,.18f,.52f):0;
        if(touch>0)
        {
            int leg=p.InterceptFoot<0?0:1;
            var target=p.InterceptAim;var delta=target-p.Position;
            if(delta.Length>.68f)target=p.Position+delta.Unit*.68f;
            LegendFoot(parts,leg,target,Math.Clamp(p.InterceptHeight,.065f,.55f),forward,touch*w,forward);
        }
    }
    static void LegendKeeperReset(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        // The committed save and held ball own both palms until the body has
        // returned to its feet. Then one compact split-step resets the stance.
        if(!p.Keeper || m.Owner==p.Id || m.Seconds-p.DiveEndedAt<HumanFigure.DiveRecoverySeconds(p,m))return;
        float age=m.Seconds-p.DiveEndedAt-HumanFigure.DiveRecoverySeconds(p,m);
        float set=LegendPulse(age,0,.18f,.58f)*f.Weight;
        LegendLower(parts,forward,.115f*set);
        LegendTorso(parts,forward,side,.08f*set);
        for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(28,25,0,92,-20,-5),set);
        for(int leg=0;leg<2;leg++)
        {
            int sign=leg==0?-1:1;
            LegendFoot(parts,leg,p.Position+side*(sign*.24f)+forward*.025f,.065f,forward,set,forward);
        }
    }

    static void LegendScissorVolley(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        float t=f.Time,w=f.Weight;
        int strike=p.KickFoot<0?0:1,sign=strike==0?-1:1;
        float flight=t<0?Smooth((t+.30f)/.30f):1-Smooth(t/.47f);
        float land=t>0?LegendPulse(t,.28f,.50f,1.25f):0;
        float rising=t<0?0:Smooth((t-.76f)/.78f);
        float targetRoot=t<.28f?Math.Clamp(f.ContactHeight-.68f,.92f,1.65f):
            .31f+(.83f-.31f)*rising;
        if(t>=0 && t<.47f)
        {
            float u=Smooth((t-.10f)/.37f);
            targetRoot=(Math.Clamp(f.ContactHeight-.68f,.92f,1.65f))*(1-u)+.31f*u;
        }
        var root=parts[6];
        Point behind=forward*(-.30f*flight)+side*(sign*.035f*land);
        LegendTranslate(parts,behind*w,(targetRoot-root.ZA)*w);
        float pitch=(-1.05f*flight-.25f*land)*(1-rising)+.28f*LegendPulse(t,.70f,1.10f,1.55f);
        float roll=sign*(.18f*flight+1.12f*land)*(1-rising);
        LegendTorso(parts,forward,side,pitch*w,sign*.12f*flight*w,roll*w);
        var contact=f.Contact-forward*.055f;
        if((contact-p.Position).Length>.6f)contact=p.Position+(contact-p.Position).Unit*.6f;
        float recovery=Smooth(Math.Max(0,t)/.42f);
        Point strikeTarget=contact*(1-recovery)+(p.Position+forward*.14f+side*(sign*.23f))*recovery;
        float strikeZ=f.ContactHeight*(1-recovery)+.065f*recovery;
        if(t<0)
        {
            float pull=1-Smooth((t+.24f)/.24f);
            strikeTarget-=forward*(pull*.13f);strikeZ-=pull*.26f;
        }
        LegendFoot(parts,strike,strikeTarget,strikeZ,side*sign+forward*.15f,w,forward,true);
        float counterLift=(.50f*flight+.08f*land)*(1-rising);
        var counter=p.Position-forward*(.28f*flight)+side*(-sign*.20f);
        LegendFoot(parts,1-strike,counter,.065f+counterLift,forward,w,forward);
        for(int arm=0;arm<2;arm++)
        {
            int armSign=arm==0?-1:1;
            ApplyArmFk(parts,7+arm*3,new ArmAngles(22+flight*10,42+flight*18,
                armSign*12,72+land*25),w*(1-rising*.65f));
        }
        float supportWeight=Smooth((t-.45f)/.12f)*(1-Smooth((t-.92f)/.24f))*w;
        if(supportWeight>.001f)
        {
            int support=sign>0?10:7;
            var wrist=p.Position+side*(sign*.54f)+forward*.22f;
            ApplyArmContact(parts,support,wrist,.12f,forward,-.25f,supportWeight*.85f);
        }
    }
    static void LegendPowerHeader(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,LegendaryFootballAnimation.Frame f)
    {
        float t=f.Time,w=f.Weight;
        float airborne=t<0?Smooth((t+.30f)/.30f):1-Smooth(t/.40f);
        float absorb=LegendPulse(t,.28f,.46f,.72f);
        float desired=Math.Clamp(f.ContactHeight-.78f,.82f,1.58f);
        float standing=.82f;
        var root=parts[6];
        float targetRoot=standing+(desired-standing)*airborne-.10f*absorb;
        LegendTranslate(parts,new Point(),(targetRoot-root.ZA)*w);
        float nod=LegendPulse(t,-.08f,.055f,.25f);
        LegendTorso(parts,forward,side,(-.13f*airborne+.29f*nod+.10f*absorb)*w,0,p.StrongFoot*.035f*airborne*w);
        int drive=p.StrongFoot<0?0:1;
        for(int leg=0;leg<2;leg++)
        {
            int sign=leg==0?-1:1;
            bool lead=leg==drive;
            var foot=p.Position+side*(sign*.18f)+forward*((lead?.25f:-.16f)*airborne);
            // The ankle rises with the pelvis before the knee drives. An
            // absolute 35 cm ankle target left a jumping striker's .81 m leg
            // overextended from a 1.2 m hip and erased the intended knee lift.
            float raised=Math.Max(.065f,parts[leg*3].ZA-(lead?.48f:.70f));
            float z=.065f*(1-airborne)+raised*airborne;
            LegendFoot(parts,leg,foot,z,forward,w,forward);
        }
        for(int arm=0;arm<2;arm++)
        {
            bool opposite=arm!=drive;
            ApplyArmFk(parts,7+arm*3,new ArmAngles(opposite?40:22,30+airborne*14,0,
                95-airborne*18,0,-4),w);
        }
        // The actual forehead meets the approaching surface of the ball. Move
        // the airborne rig together; never drag a detached head towards it.
        float contact=LegendPulse(t,-.12f,0,.16f)*w;
        if(contact>0)
        {
            var forehead=(parts[15].A+parts[16].A)*.5f;
            float foreheadZ=(parts[15].ZA+parts[16].ZA)*.5f;
            var correction=f.Contact-forward*.11f-forehead;
            if(correction.Length>.22f)correction=correction.Unit*.22f;
            LegendTranslate(parts,correction*contact,(f.ContactHeight-foreheadZ)*contact);
        }
    }
}