Part of ReplayFigure, the animation for replays. Holds keyframed arm angle tables for instep shots and passes, samples them with a bounded cubic interpolation, and blends the pass and shot poses based on kick type and windup state.
using System;
namespace Socer;
public static partial class ReplayFigure
{
readonly record struct ArmKey(float Time,ArmAngles Pose);
// Original angular blocking refined into bounded cubic curves. Negative time
// is the windup; zero is ball contact; positive time is the follow-through.
// The support arm loads back/out with the trunk, then folds forwards/inwards.
// Passes use a smaller balance gesture instead of a maximal-shot tension arc.
static readonly ArmKey[] InstepSupportArm={
new(-1,new(7,10,0,26,10)),new(-.72f,new(-14,42,-18,32,12)),
new(-.28f,new(-34,69,-26,24,16)),new(0,new(7,57,-12,43,18)),
new(.22f,new(40,31,8,75,25)),new(.55f,new(25,18,5,58,18)),new(1,new(7,10,0,26,10))};
static readonly ArmKey[] InstepKickSideArm={
new(-1,new(7,10,0,26,10)),new(-.72f,new(26,18,12,72,12)),
new(-.28f,new(37,22,20,85,20)),new(0,new(-16,29,-10,51,16)),
new(.22f,new(-27,21,-12,39,12)),new(.55f,new(9,16,2,43,10)),new(1,new(7,10,0,26,10))};
static readonly ArmKey[] PassSupportArm={
new(-1,new(7,10,0,26,10)),new(-.72f,new(-3,18,-5,42,10)),
new(-.28f,new(-10,31,-10,45,12)),new(0,new(9,28,-5,56,14)),
new(.22f,new(25,22,3,69,18)),new(.55f,new(14,14,2,47,14)),new(1,new(7,10,0,26,10))};
static readonly ArmKey[] PassKickSideArm={
new(-1,new(7,10,0,26,10)),new(-.72f,new(13,12,4,47,10)),
new(-.28f,new(19,16,8,59,12)),new(0,new(-5,18,-4,42,14)),
new(.22f,new(-10,15,-5,36,12)),new(.55f,new(8,12,1,35,10)),new(1,new(7,10,0,26,10))};
static float ArmCubic(float before,float a,float b,float after,float beforeTime,float aTime,float bTime,float afterTime,float t)
{
float span=bTime-aTime;
float ma=(b-before)/Math.Max(.001f,bTime-beforeTime)*span;
float mb=(after-a)/Math.Max(.001f,afterTime-aTime)*span;
float t2=t*t,t3=t2*t;
return Math.Clamp((2*t3-3*t2+1)*a+(t3-2*t2+t)*ma+(-2*t3+3*t2)*b+(t3-t2)*mb,
Math.Min(a,b),Math.Max(a,b));
}
static ArmAngles SampleArmKeys(ArmKey[] keys,float time)
{
if(time<=keys[0].Time)return keys[0].Pose;
if(time>=keys[^1].Time)return keys[^1].Pose;
int i=0;while(i<keys.Length-2 && time>keys[i+1].Time)i++;
var p=keys[Math.Max(0,i-1)];var a=keys[i];var b=keys[i+1];var n=keys[Math.Min(keys.Length-1,i+2)];
float t=(time-a.Time)/(b.Time-a.Time);
float Curve(float p0,float p1,float p2,float p3)=>ArmCubic(p0,p1,p2,p3,p.Time,a.Time,b.Time,n.Time,t);
return new(Curve(p.Pose.Flexion,a.Pose.Flexion,b.Pose.Flexion,n.Pose.Flexion),
Curve(p.Pose.Abduction,a.Pose.Abduction,b.Pose.Abduction,n.Pose.Abduction),
Curve(p.Pose.HumeralTwist,a.Pose.HumeralTwist,b.Pose.HumeralTwist,n.Pose.HumeralTwist),
Curve(p.Pose.ElbowFlexion,a.Pose.ElbowFlexion,b.Pose.ElbowFlexion,n.Pose.ElbowFlexion),
Curve(p.Pose.Pronation,a.Pose.Pronation,b.Pose.Pronation,n.Pose.Pronation),
Curve(p.Pose.WristFlexion,a.Pose.WristFlexion,b.Pose.WristFlexion,n.Pose.WristFlexion),
Curve(p.Pose.WristDeviation,a.Pose.WristDeviation,b.Pose.WristDeviation,n.Pose.WristDeviation));
}
public static ArmAngles SampleStrikeArm(Footballer p,MatchSimulation m,int arm)
{
bool windup=m.WindupPlayer==p.Id;
bool shot=windup || p.LastKickWasShot;
var kind=windup?m.Strike:p.LastKickKind;
float length=shot?.5f:kind==StrikeKind.Cross?.45f:.3f;
float duration=Math.Max(.01f,m.Seconds-p.WindupStartedAt+m.WindupRemaining);
float time=windup?-1+Math.Clamp((m.Seconds-p.WindupStartedAt)/duration,0,1):
Math.Clamp((m.Seconds-p.LastKickAt)/length,0,1);
bool support=(arm==7?-1:1)!=p.KickFoot;
var power=SampleArmKeys(support?InstepSupportArm:InstepKickSideArm,time);
var control=SampleArmKeys(support?PassSupportArm:PassKickSideArm,time);
float effort=!shot?kind==StrikeKind.Cross?.70f:0:kind switch
{StrikeKind.Chip=>.36f,StrikeKind.Driven=>.76f,StrikeKind.Curl=>.90f,_=>1};
return BlendArmAngles(control,power,effort);
}
}