Static helper that builds procedural footballer poses. Computes strike and kick poses from timers and kick events (windup, punt, keeper kick, header), computes running leg knee and foot positions, and projects a shared articulated world rig from ReplayFigure.Pose into the 2.5D view.
using System;
using System.Collections.Generic;
namespace Socer;
// Procedural pose driven by distance travelled and actual kick events, never by a shared clock.
public static partial class HumanFigure
{
public const float GroundScale=.64f;
public readonly record struct Part(Point A, Point B, float Width, string Color);
public readonly record struct KickPose(bool Active,float Forward,float Lift,float Strength);
public static KickPose StrikePose(Footballer p,MatchSimulation m)
{
if(m.KeeperKickPlayer==p.Id)
{
float keeperAge=Math.Max(0,m.Seconds-m.KeeperKickAt);
float draw=Smooth(keeperAge/.66f);
float swing=Smooth((keeperAge-.66f)/.30f);
float contact=m.KeeperKickFromHands?.22f:.11f;
return new(true,-.48f*draw*(1-swing)+contact*swing,
.06f+draw*.23f*(1-swing)+(m.KeeperKickFromHands?.28f:.06f)*swing, .9f*draw);
}
float puntAge=m.Seconds-p.LastPuntAt;
if(puntAge>=0 && puntAge<.65f)
{
float t=puntAge/.65f;
float swing=t<.35f?Smooth(t/.35f):1-Smooth((t-.35f)/.65f);
return new(true,.22f*(1-Smooth(t))+.38f*swing,.34f*(1-Smooth(t))+.26f*swing,.9f*(1-t));
}
if(m.WindupPlayer==p.Id)
{
float duration=Math.Max(.01f,m.Seconds-p.WindupStartedAt+m.WindupRemaining);
float t=Math.Clamp((m.Seconds-p.WindupStartedAt)/duration,0,1);
float strength=m.Strike==StrikeKind.Power?1:m.Strike==StrikeKind.Chip?.55f:.8f;
// Draw back, then swing through during the final fifth of the windup.
float leg=t<.75f?-.7f*strength*MathF.Sin(t/.75f*MathF.PI*.5f):-.7f*strength+(.11f+.7f*strength)*Smooth((t-.75f)/.25f);
return new(true,leg,.06f+Math.Max(0,-leg)*.22f,strength);
}
if(p.LastKickKind==StrikeKind.Header)return new(false,0,0,0);
float age=m.Seconds-p.LastKickAt;
float length=p.LastKickWasShot?.5f:p.LastKickKind==StrikeKind.Cross?.45f:.3f;
if(age<0 || age>=length) return new(false,0,0,0);
float power=p.LastKickWasShot?(p.LastKickKind==StrikeKind.Power?1:.8f):p.LastKickKind==StrikeKind.Cross?.7f:.4f;
float phase=age/length;
float forward=phase<.35f?.11f+power*.55f*Smooth(phase/.35f):(.11f+power*.55f)*(1-Smooth((phase-.35f)/.65f));
float lift=(p.LastKickKind==StrikeKind.Chip?.4f:p.LastKickKind==StrikeKind.Cross?.3f:p.LastKickKind==StrikeKind.Driven?.12f:.22f)*MathF.Sin(phase*MathF.PI);
return new(true,forward,.06f+lift,power*(1-phase));
}
static float Smooth(float t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}
// In this 2.5D projection the hip and foot define the leg's resting line.
// Keep the knee near that line in support; lift it only with the swing foot.
// The earlier fixed-length IK bent even an idle player's knees far forward.
public readonly record struct RunLeg(Point Knee,Point Foot,float KneeHeight,float FootHeight);
public static RunLeg RunningLeg(float phase,float speed,float activity,float sprint,
Point hipGround,float hipHeight,Point lateralGround,Point travel)
{
phase=(phase%1+1)%1;
float stride=Math.Clamp((1.3f+speed*.28f)*.16f,.2f,.42f)*activity;
float offset=phase<.5f?stride*(1-4*phase):stride*(-1+2*Smooth((phase-.5f)*2));
float swing=phase<.5f?0:MathF.Sin((phase-.5f)*2*MathF.PI);
float lift=swing*(.09f+sprint*.28f)*activity;
float footHeight=.06f+lift;
Point footGround=lateralGround+travel*offset;
float supportBend=phase<.5f?.025f*MathF.Sin(phase*2*MathF.PI):0;
float swingBend=swing*(.05f+.08f*sprint);
Point kneeGround=hipGround+(footGround-hipGround)*.49f+travel*((supportBend+swingBend)*activity);
float kneeHeight=hipHeight+(footHeight-hipHeight)*.49f+swing*(.025f+.05f*sprint)*activity;
Point ProjectJoint(Point ground,float z)=>new(ground.X,ground.Y*GroundScale-z*.77f);
return new(ProjectJoint(kneeGround,kneeHeight),ProjectJoint(footGround,footHeight),kneeHeight,footHeight);
}
public static List<Part> Pose(Footballer p, MatchSimulation match, bool referee=false,List<Part> buffer=null,List<ReplayFigure.Part> worldBuffer=null)
{
// Every view projects the same articulated world rig. This also keeps
// left-footed strikes, contact/recovery and skill timing identical in
// the tactical view, live broadcast and stored replay poses.
var result=buffer??new List<Part>(24);
result.Clear();
var world=ReplayFigure.Pose(p,match,worldBuffer??new List<ReplayFigure.Part>(24));
Point Project(Point at,float z)
{var local=at-p.Position;return new(local.X,local.Y*GroundScale-z*.77f);}
foreach(var part in world)
result.Add(new(Project(part.A,part.ZA),Project(part.B,part.ZB),part.Width,part.Color));
return result;
}
}