Part of ReplayFigure, the replay pose builder. Poses the goalkeeper for saves: a spread or squat block pose, and a full dive driven by DiveAge, DiveTarget and DiveOrigin, with two-bone IK helpers for arms (shoulder, elbow, glove) and legs (thigh, shin, boot) plus a clamp that keeps every joint above the turf.
using System;
using System.Collections.Generic;
namespace Socer;
public static partial class ReplayFigure
{
static bool ApplyKeeperSave(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side)
{
bool dive=p.DiveRemaining>0 || (m.Seconds>=p.DiveEndedAt &&
m.Seconds-p.DiveEndedAt<HumanFigure.DiveRecoverySeconds(p,m));
if(dive)
{
ApplyKeeperDive(p,m,parts,forward,side);
return true;
}
float block=m.KeeperBlockPoseWeight(p);
if(block<=0 && !(p.KeeperBlockUntil>0 && m.Seconds>=p.KeeperBlockStartedAt &&
m.Seconds<p.KeeperBlockUntil+.22f))return false;
// A low spread is a squat: hips lower with the shirt, and planted boots
// make both knees flex instead of shortening or disconnecting the torso.
var left=parts[1].B;float leftZ=parts[1].ZB;
var right=parts[4].B;float rightZ=parts[4].ZB;
bool low=p.KeeperBlockKind==2;
var shift=forward*((low?.10f:.07f)*block);
float lower=(low?.48f:.20f)*block;
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-lower,ZB=v.ZB-lower};
}
float spread=low?.46f:.32f;
for(int leg=0;leg<2;leg++)
{
int sign=leg==0?-1:1;
var resting=leg==0?left:right;
float restZ=leg==0?leftZ:rightZ;
var wanted=p.Position+side*(sign*spread)+forward*(low?.16f:.09f);
KeeperLeg(parts,leg,resting*(1-block)+wanted*block,restZ*(1-block)+.07f*block,
forward,0,forward,0);
}
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*(1-block),block);
}
KeepKeeperAboveGround(parts);
return true;
}
static void ApplyKeeperDive(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side)
{
var reachVector=p.DiveTarget-p.DiveOrigin;
// A central save (or a forward interception) stays square to the shot.
// Only genuinely lateral reach rolls the body over a supporting side.
bool vertical=reachVector.Length<.18f || Math.Abs(Point.Dot(reachVector.Unit,forward))>.92f;
var axis=vertical?forward:reachVector.Unit;
float blend=HumanFigure.DiveBlend(p,m);
float recovery=p.DiveRemaining>0?0:Math.Clamp((m.Seconds-p.DiveEndedAt)/HumanFigure.DiveRecoverySeconds(p,m),0,1);
// Roll off the landing side first, then drive upwards from a tucked
// leg. Reversing the take-off rotation and root together made the
// goalkeeper levitate from the turf with both legs hanging straight.
float rollBlend=!vertical && recovery>0?1-Smooth(recovery/.60f):blend;
float riseBlend=!vertical && recovery>0?1-Smooth((recovery-.22f)/.78f):blend;
float high=Smooth((p.DiveHeight-.75f)/1.75f);
float angle=vertical?0:rollBlend*(1.38f-high*.42f);
float cos=MathF.Cos(angle),sin=MathF.Sin(angle);
var pelvis=parts[6].A;float pelvisZ=parts[6].ZA;
float flight=p.DiveRemaining>0?Smooth(p.DiveAge/.22f)*
(1-Smooth((p.DiveAge-.52f)/.43f)):0;
float load=Smooth(p.DiveAge/.06f)*(1-Smooth((p.DiveAge-.09f)/.11f));
float landing=Smooth((p.DiveAge-.64f)/.20f);
float rootZ=pelvisZ-(vertical?.12f*(load+landing*blend)*(1-flight):(.60f-high*.10f)*riseBlend);
// A high save jumps before rolling sideways. Root lift carries every
// joint; the forearms never stretch upwards to the target ball height.
float lift=Math.Clamp(p.DiveHeight-(rootZ+.50f*cos)-.22f,0,1.5f)*flight;
(Point,float) Joint(Point at,float z)
{
var local=at-pelvis;
float along=Point.Dot(local,axis),height=z-pelvisZ;
return (at+axis*(along*(cos-1)+height*sin),
rootZ+height*cos-along*sin+lift);
}
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};
}
KeepKeeperAboveGround(parts);
if(!vertical && recovery>0)
{
float kneel=.22f*MathF.Sin(recovery*MathF.PI);
for(int i=0;i<parts.Count;i++)
{var v=parts[i];parts[i]=v with{ZA=v.ZA-kneel,ZB=v.ZB-kneel};}
}
float forwardAlong=Point.Dot(forward,axis);
var bendForward=forward+axis*(forwardAlong*(cos-1));
float bendForwardZ=-forwardAlong*sin;
float trail=Smooth(p.DiveAge/.26f)*(1-Smooth((p.DiveAge-.60f)/.35f))*blend;
for(int leg=0;leg<2;leg++)
{
var ankle=parts[leg*3+1];
var wanted=ankle.B-axis*((vertical?(leg==0?.09f:.12f):(leg==0?.15f:.24f))*trail);
float wantedZ=ankle.ZB+(vertical?.10f:(leg==0?.12f:.20f))*trail;
int sign=leg==0?-1:1;
bool leading=sign==(Point.Dot(axis,side)>=0?1:-1);
if(!vertical && recovery>0)
{
// The near boot folds under the body before the trailing foot
// joins it. Only its reset is airborne; the rising half keeps
// both soles fixed instead of playing a running stride.
float reset=Smooth(recovery/(leading?.28f:.48f));
float crouch=Smooth(recovery/.16f)*(1-Smooth((recovery-.52f)/.38f));
var plant=p.Position+side*(sign*.19f)+forward*((leading?.18f:-.15f)*crouch);
wanted=wanted*(1-reset)+plant*reset;
float bootLift=leading?.07f:.11f;
float swing=MathF.Sin(Math.Clamp(recovery/(leading?.28f:.48f),0,1)*MathF.PI);
wantedZ=wantedZ*(1-reset)+(.065f+bootLift*swing)*reset;
}
else if(p.DiveRemaining>0)
{
// Establish the pushing foot while the inside leg releases.
// The anchor uses the captured take-off point, never a new
// random pose or an offset that travels with the sliding root.
float push=.65f*Smooth(p.DiveAge/.04f)*(1-Smooth((p.DiveAge-.07f)/.07f))*(vertical?1:leading?0:1);
var plant=p.DiveOrigin+side*(sign*.19f)+forward*.025f;
wanted=wanted*(1-push)+plant*push;wantedZ=wantedZ*(1-push)+.065f*push;
}
KeeperLeg(parts,leg,wanted,wantedZ,bendForward,bendForwardZ,bendForward,bendForwardZ);
}
// Reach through the ball, then tuck for landing. The curve is continuous
// when DiveRemaining becomes zero, so recovery cannot reset the arms.
float reach=Smooth((p.DiveAge-.025f)/.17f)*
(1-Smooth((p.DiveAge-.52f)/.30f))*blend;
for(int s=0;s<2;s++)
{
int arm=7+s*3,sign=s==0?-1:1;
var shoulder=parts[arm];
var tucked=shoulder.A+bendForward*.19f+side*(sign*.10f);
float tuckedZ=shoulder.ZA-.29f*cos+bendForwardZ*.19f;
var ready=p.DiveRemaining<=0 && m.Owner==p.Id?m.GlovePosition(p,sign):
p.Position+forward*.24f+side*(sign*.40f);
float readyZ=p.DiveRemaining<=0 && m.Owner==p.Id?m.GloveHeight(p):1.00f;
tucked=ready*(1-blend)+tucked*blend;
tuckedZ=readyZ*(1-blend)+tuckedZ*blend;
var target=m.GlovePosition(p,sign);
var hand=tucked*(1-reach)+target*reach;
float z=tuckedZ*(1-reach)+m.GloveHeight(p)*reach;
// Anatomical down follows the rolled torso. A front or side pole
// can align with the retracting wrist and invert its elbow plane;
// torso-down stays distinct from the forward-reaching glove.
KeeperArm(parts,arm,hand,z,axis*-sin,-cos,forward*(1-blend),blend);
}
if(!vertical && recovery>0 && m.Owner!=p.Id)
{
// A free hand braces the loaded knee while the other arm balances.
// A keeper holding the ball keeps both gloves around it instead.
int lead=Point.Dot(axis,side)>=0?1:0;
int arm=7+lead*3;
var hand=parts[arm+1];var knee=parts[lead*3];
float support=.85f*Smooth(recovery/.34f)*(1-Smooth((recovery-.56f)/.35f));
var shoulder=parts[arm];
SolveKeeperLimb(shoulder.A,shoulder.ZA,knee.B+forward*.035f,knee.ZB+.035f,
.27f,.25f,side*(lead==0?-1:1),0,
out var elbow,out float elbowZ,out var palm,out float palmZ);
GoalArmDirections(parts,arm,(elbow-shoulder.A)*(1/.27f),(elbowZ-shoulder.ZA)/.27f,
(palm-elbow)*(1/.25f),(palmZ-elbowZ)/.25f,forward,support);
}
KeepKeeperAboveGround(parts);
}
static void KeeperArm(List<Part> parts,int arm,Point target,float targetZ,
Point bend,float bendZ,Point handDirection,float handDirectionZ)
{
var upper=parts[arm];var lower=parts[arm+1];var glove=parts[arm+2];
SolveKeeperLimb(upper.A,upper.ZA,target,targetZ,.27f,.25f,bend,bendZ,
out var elbow,out float elbowZ,out var hand,out float handZ);
parts[arm]=upper with{B=elbow,ZB=elbowZ};
parts[arm+1]=lower with{A=elbow,ZA=elbowZ,B=hand,ZB=handZ};
float directionLength=MathF.Sqrt(handDirection.Length*handDirection.Length+handDirectionZ*handDirectionZ);
if(directionLength>.0001f){handDirection*=1/directionLength;handDirectionZ/=directionLength;}
parts[arm+2]=glove with{A=hand,ZA=handZ,B=hand+handDirection*.03f,ZB=handZ+handDirectionZ*.03f};
}
static void KeeperLeg(List<Part> parts,int leg,Point target,float targetZ,
Point bend,float bendZ,Point bootDirection,float bootDirectionZ)
{
int i=leg*3;var thigh=parts[i];var shin=parts[i+1];var boot=parts[i+2];
SolveKeeperLimb(thigh.A,thigh.ZA,target,targetZ,.405f,.405f,bend,bendZ,
out var knee,out float kneeZ,out var ankle,out float ankleZ);
parts[i]=thigh with{B=knee,ZB=kneeZ};
parts[i+1]=shin with{A=knee,ZA=kneeZ,B=ankle,ZB=ankleZ};
parts[i+2]=boot with{A=ankle,ZA=ankleZ,B=ankle+bootDirection*.24f,ZB=ankleZ+bootDirectionZ*.24f};
}
static void SolveKeeperLimb(Point root,float rootZ,Point target,float targetZ,
float upper,float lower,Point bend,float bendZ,
out Point joint,out float jointZ,out Point end,out float endZ)
{
var delta=target-root;float dz=targetZ-rootZ;
float distance=MathF.Sqrt(delta.Length*delta.Length+dz*dz);
if(distance<.0001f){delta=new Point(.0001f,0);dz=0;distance=.0001f;}
float reach=Math.Clamp(distance,Math.Abs(upper-lower)+.0001f,upper+lower-.0001f);
if(upper<.30f)
{
// A hard fully-straight arm limit makes the elbow's bend collapse
// with a square-root cusp. Ease the last nine centimetres of reach
// so a high save can extend smoothly without locking the elbow.
float start=upper+lower-.09f;
if(reach>start)reach=start+.075f*(1-MathF.Exp(-(distance-start)/.075f));
}
var direction=delta*(1/distance);float directionZ=dz/distance;
end=root+direction*reach;endZ=rootZ+directionZ*reach;
float along=(upper*upper-lower*lower+reach*reach)/(2*reach);
float height=MathF.Sqrt(Math.Max(0,upper*upper-along*along));
float dot=Point.Dot(bend,direction)+bendZ*directionZ;
var pole=bend-direction*dot;float poleZ=bendZ-directionZ*dot;
float length=MathF.Sqrt(pole.Length*pole.Length+poleZ*poleZ);
if(length<.001f)
{
var fallback=Math.Abs(directionZ)>.8f?new Point(1,0):new Point(-direction.Y,direction.X);
float fallbackDot=Point.Dot(fallback,direction);
pole=fallback-direction*fallbackDot;poleZ=-directionZ*fallbackDot;
length=MathF.Sqrt(pole.Length*pole.Length+poleZ*poleZ);
}
joint=root+direction*along+pole*(height/Math.Max(.0001f,length));
jointZ=rootZ+directionZ*along+poleZ*height/Math.Max(.0001f,length);
}
static void KeepKeeperAboveGround(List<Part> parts)
{
float minimum=.065f;
for(int i=0;i<parts.Count;i++)minimum=Math.Min(minimum,Math.Min(parts[i].ZA,parts[i].ZB));
float raise=.065f-minimum;
if(raise<=0)return;
for(int i=0;i<parts.Count;i++)
{var v=parts[i];parts[i]=v with{ZA=v.ZA+raise,ZB=v.ZB+raise};}
}
}