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