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