Static helper for presentation-only secondary motion in a football game. Computes procedural gait phase, speed and acceleration for a returning player, the ceremony ball deposit and staff jump/load curves, and poses replay figure parts for ball placement and seated sideline leg positions. No networked or persistent state is touched.
using System;
using System.Collections.Generic;
namespace Socer;
// Presentation-only choreography. Root paths never alter football or saves.
public static class FootballSecondaryMotion
{
public readonly record struct ReturnFrame(Point Position,Point Velocity,Point Acceleration,float GaitPhase);
static float Ease(float t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}
public static ReturnFrame Return(Point from,Point target,float age,float duration,int id,float maxApproach=12)
{
duration=Math.Max(.1f,duration);
var delta=target-from;float distance=Math.Min(delta.Length,maxApproach);
var direction=delta.Unit;from=target-direction*distance;
float t=Math.Clamp(age/duration,0,1);
float speed=distance*6*t*(1-t)/duration;
float acceleration=t<=0 || t>=1?0:distance*6*(1-2*t)/(duration*duration);
// Fixed-grid integration is independent of sampling order. Positive
// interpolated rates cannot reverse the gait near a speed threshold.
float cycles=0;
const int samples=64;
float Rate(float q)
{
float v=distance*6*q*(1-q)/duration;
float a=distance*6*(1-2*q)/(duration*duration);
float stride=Math.Clamp(1.3f+v*.28f,1.3f,3.2f);
if(v>1.8f)stride*=.8f;
stride*=1-Math.Clamp(-a/8,0,1)*.22f;
return v/stride;
}
for(int i=0;i<samples;i++)
{
float covered=Math.Clamp(t*samples-i,0,1);
if(covered<=0)break;
float a=Rate(i/(float)samples),b=Rate((i+1)/(float)samples);
cycles+=(a*covered+(b-a)*covered*covered*.5f)*(duration/samples);
}
return new(from+direction*(distance*Ease(t)),direction*speed,direction*acceleration,
(cycles+id*.12f)%1);
}
public static float Deposit(float age)=>Ease((age-2.8f)/.9f);
public static float CeremonyBallHeight(float age)=>1.04f-.93f*Deposit(age);
public static float StaffJump(float age)
{
float t=(age-.24f)/.48f;
return t<=0 || t>=1?0:.065f*4*t*(1-t);
}
public static float StaffLoad(float age)
{
float Pulse(float start,float peak,float end)=>age<peak?Ease((age-start)/(peak-start)):
1-Ease((age-peak)/(end-peak));
return -.035f*Pulse(.02f,.16f,.25f)-.030f*Pulse(.69f,.82f,1.05f);
}
}
public static partial class ReplayFigure
{
public static void PoseCeremonyBallPlacement(List<Part> parts,Point forward,Point ball,float ballZ,float age)
{
float crouch=FootballSecondaryMotion.Deposit(age)*(1-Smooth((age-3.95f)/.75f));
float pitch=1.05f*crouch,lower=.52f*crouch;
var hip=parts[6].A;float hipZ=parts[6].ZA;
var left=parts[1];var right=parts[4];
for(int i=0;i<parts.Count;i++)
{
var v=parts[i];parts[i]=v with{ZA=v.ZA-lower,ZB=v.ZB-lower};
}
(Point,float) Bend(Point at,float z)
{
float along=Point.Dot(at-hip,forward),height=z-(hipZ-lower);
return (at+forward*(along*(MathF.Cos(pitch)-1)+height*MathF.Sin(pitch)),
hipZ-lower+height*MathF.Cos(pitch)-along*MathF.Sin(pitch));
}
for(int i=6;i<parts.Count;i++)
{
var v=parts[i];var a=Bend(v.A,v.ZA);var b=Bend(v.B,v.ZB);
parts[i]=v with{A=a.Item1,ZA=a.Item2,B=b.Item1,ZB=b.Item2};
}
KeeperLeg(parts,0,left.B,left.ZB,forward,0,forward,0);
KeeperLeg(parts,1,right.B,right.ZB,forward,0,forward,0);
var side=new Point(-forward.Y,forward.X);
float release=Smooth((age-3.74f)/.36f);
for(int arm=0;arm<2;arm++)
{
int i=7+arm*3;float sign=arm==0?-1:1;
var hand=parts[i+1];
KeeperArm(parts,i,(ball+side*(sign*.095f))*(1-release)+hand.B*release,
ballZ*(1-release)+hand.ZB*release,side*sign,-.35f,forward,0);
}
KeepKeeperAboveGround(parts);
}
public static void PoseSeatedLeg(List<Part> parts,int leg,Point centre,Point forward,float hipZ,
string trousers,string socks)
{
float sign=leg==0?-1:1;int i=leg*3;var side=new Point(-forward.Y,forward.X);
const float kneeZ=.46f,footZ=.065f;
float thighAhead=MathF.Sqrt(.405f*.405f-(hipZ-kneeZ)*(hipZ-kneeZ)-.04f*.04f);
float shinAhead=MathF.Sqrt(.405f*.405f-(kneeZ-footZ)*(kneeZ-footZ)-.01f*.01f);
var hip=centre+side*(sign*.13f);
var knee=centre+side*(sign*.17f)+forward*thighAhead;
var foot=centre+side*(sign*.18f)+forward*(thighAhead+shinAhead);
parts[i]=parts[i] with{A=hip,ZA=hipZ,B=knee,ZB=kneeZ,Color=trousers};
parts[i+1]=parts[i+1] with{A=knee,ZA=kneeZ,B=foot,ZB=footZ,Color=socks};
parts[i+2]=parts[i+2] with{A=foot,ZA=footZ,B=foot+forward*.23f,ZB=footZ};
}
public static void PoseSidelineSupportLeg(List<Part> parts,int leg,Point foot,float footZ,Point forward)
=>KeeperLeg(parts,leg,foot,footZ,forward,0,forward,0);
}