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);
}
}