GoalCelebrationAnimation.cs

Part of the ReplayFigure pose layer for a football match replay. Drives goal celebration poses by blending arm FK, body and head offsets from timers: scorer variants, legendary-ability signature celebrations, teammate greetings, applause and conceded-goal reactions, with keeper leg and ground constraints.

Rough Code
using System;
using System.Collections.Generic;

namespace Socer;

public static partial class ReplayFigure
{
    // Original authored joint-space clips. The simulation supplies approach,
    // braking and arrival tickets; this layer supplies loaded supports, body
    // phrasing, anatomical arms, genuine contacts and a measured release.
    static bool ApplyGoalCelebration(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side)
    {
        if(m.GoalHoldRemaining<=0 || (uint)m.GoalScorerId>=m.Players.Count || p.Team>=2 || !p.InPlay ||
            m.Seconds<p.FallUntil || m.Seconds<p.ImpactUntil || p.IsAcrobaticCommitted(m.Seconds))return false;
        if(p.Keeper && (p.DiveRemaining>0 || m.Seconds-p.DiveEndedAt<HumanFigure.DiveRecoverySeconds(p,m) || m.Owner==p.Id))return false;
        // A safe goal reaction bypasses the normal goalkeeper glove solver.
        // Repair its initial idle fingers before blending in the first gesture;
        // active ball contacts and save recovery returned above remain untouched.
        if(p.Keeper)RepairArmAnatomy(parts);
        var scorer=m.Players[m.GoalScorerId];
        float age=m.GoalCelebrationElapsed;
        float weight=Smooth(age/.28f)*Smooth(m.GoalHoldRemaining/.45f);
        float bodyReady=(uint)p.Id<m.GoalCelebrationBodyReadyAt.Length?m.GoalCelebrationBodyReadyAt[p.Id]:0;
        if(bodyReady>0)weight*=Smooth((age-bodyReady)/.24f);
        if(p.Team!=scorer.Team)
        {
            if(p.Keeper)return false;
            GoalConceded(p,m,parts,forward,side,age,weight);return true;
        }
        int slot=Array.IndexOf(m.GoalCelebrants,p.Id);
        float arrival=(uint)p.Id<m.GoalCelebrationArrivedAt.Length?m.GoalCelebrationArrivedAt[p.Id]:-1;
        float settled=1-Smooth((p.VisualSpeed-.35f)/1.65f);
        var identity=LegendaryAbilities.Get(p.Profile)?.Kind??LegendaryAbility.None;
        if(p.Id==m.GoalScorerId)
        {
            // Arrival provides an honest planted support before a jump, knee
            // drop or seated clip. While approaching, arms only acknowledge.
            float local=arrival>=0?Math.Max(0,age-arrival):0;
            float planted=Smooth(local/.18f)*settled;
            float greetAt=identity==LegendaryAbility.LaserFinish?3.95f:identity==LegendaryAbility.SkySiu?2.55f:
                m.GoalCelebrationVariant==3?2.65f:1.85f;
            float greeting=Smooth((local-greetAt)/.55f);
            float solo=weight*(1-greeting);
            // Preserve the approach acknowledgement while the arrival ticket
            // first appears. A ticket changes state, never the current pose.
            float acknowledge=weight*.55f*(1-planted);
            ApplyArmFk(parts,7,new ArmAngles(12,40,0,32),acknowledge);
            ApplyArmFk(parts,10,new ArmAngles(20,28,0,82),acknowledge);
            if(identity!=LegendaryAbility.None)
                GoalIdentity(identity,p,m,parts,forward,side,local,solo*planted);
            else GoalOriginal(m.GoalCelebrationVariant,p,parts,forward,side,local,solo*planted);
            if(m.GoalCelebrants.Length>0)GoalGreeting(p,m,parts,forward,side,greeting*weight);
            else EmotionClap(parts,forward,side,local-greetAt,greeting*weight,2);
            return true;
        }
        if(slot>=0 && slot<2)
        {
            float greeting=arrival>=0?Smooth((age-arrival)/.48f):0;
            ApplyArmFk(parts,7,new ArmAngles(15,24,-6,54),weight*.4f*(1-greeting));
            ApplyArmFk(parts,10,new ArmAngles(22,30,0,68),weight*.4f*(1-greeting));
            if(greeting>0)
            {
                float low=GoalScorerLowPose(m);
                EmotionClap(parts,forward,side,age-arrival-.12f,greeting*weight*low,3);
                GoalGreeting(p,m,parts,forward,side,greeting*weight*(1-low));
            }
        }
        else if(slot>=2)
        {
            float greeting=arrival>=0?Smooth((age-arrival)/.32f):0;
            EmotionBody(parts,forward,side,.018f*greeting*weight,.04f*greeting*weight,0,0,0);
            EmotionClap(parts,forward,side,arrival>=0?age-arrival-.12f:0,greeting*weight,3);
        }
        else if(identity==LegendaryAbility.NotToday && p.Keeper)
        {
            // Buffon can celebrate a teammate's goal from his own penalty
            // area, then organises again; live gloves/recovery still win above.
            float roar=1-Smooth((age-1.7f)/.45f);
            GoalIdentity(identity,p,m,parts,forward,side,age,weight*roar);
            EmotionClap(parts,forward,side,age-2.0f,weight*Smooth((age-1.7f)/.4f),3);
        }
        else
        {
            EmotionBody(parts,forward,side,.012f*weight,.035f*EmotionPulse(age,.3f,.8f,1.5f)*weight,0,0,0);
            EmotionClap(parts,forward,side,age-.35f-.10f*(p.Id%4),weight,3);
        }
        return true;
    }

    static float GoalMix(float a,float b,float t)=>a+(b-a)*t;
    static float GoalPulse(float t,float start,float peak,float end)=>EmotionPulse(t,start,peak,end);
    static float GoalJump(float age,float start,float duration)
    {float t=(age-start)/duration;return t<=0 || t>=1?0:4*t*(1-t);}

    static void GoalOriginal(int variant,Footballer p,List<Part> parts,Point forward,Point side,float t,float weight)
    {
        float load=GoalPulse(t,.02f,.22f,.45f),hold=Smooth(t/.3f),s=p.StrongFoot;
        switch(variant%6)
        {
            case 0: // Diagonal fist drive, with opposite shoulder counterweight.
            {
                float drive=GoalPulse(t,.19f,.52f,1.12f);
                EmotionBody(parts,forward,side,.065f*load*weight,-.09f*drive*weight,-s*.13f*drive*weight,s*.025f*drive*weight,0);
                ApplyArmFk(parts,s<0?7:10,new ArmAngles(34+drive*27,20,-8,110-drive*16,20),weight);
                ApplyArmFk(parts,s<0?10:7,new ArmAngles(-8,30,0,35),weight);
                EmotionHead(parts,forward,side,-.08f*drive*weight,0);break;
            }
            case 1: // Double-fist roar: compress, pull elbows down, open chest.
            {
                float release=GoalPulse(t,.18f,.53f,1.35f);
                EmotionBody(parts,forward,side,.075f*load*weight,-.14f*release*weight,0,0,0,.035f*weight);
                for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(20,22,0,118-release*35,15),weight);
                EmotionHead(parts,forward,side,-.12f*release*weight,0);break;
            }
            case 2: // Arms-open supported pivot; no levitating repeated hop.
            {
                float open=Smooth((t-.12f)/.42f);
                EmotionBody(parts,forward,side,.025f*load*weight,-.065f*open*weight,
                    s*.22f*GoalPulse(t,.15f,.70f,1.8f)*weight,0,0,.025f*weight);
                for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(-5,70,0,18),open*weight);
                EmotionHead(parts,forward,side,-.07f*open*weight,s*.10f*open*weight);break;
            }
            case 3: // Planted knee-drop, chest lift, then a supported rise.
            {
                float down=Smooth((t-.10f)/.65f)*(1-Smooth((t-1.45f)/.82f));
                EmotionBody(parts,forward,side,.48f*down*weight,-.16f*down*weight,0,0,0,.04f*down*weight);
                for(int leg=0;leg<2;leg++)
                {
                    var rest=parts[leg*3+1];float sign=leg==0?-1:1;
                    var target=p.Position-forward*.24f+side*(sign*.17f);
                    KeeperLeg(parts,leg,rest.B*(1-down*weight)+target*(down*weight),
                        rest.ZB*(1-down*weight)+.075f*(down*weight),forward,0,forward,0);
                }
                for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(-8,58,0,30),down*weight);
                EmotionHead(parts,forward,side,-.15f*down*weight,0);break;
            }
            case 4: // Audience acknowledgement followed by finite applause.
            {
                float salute=GoalPulse(t,.10f,.48f,.98f);
                EmotionBody(parts,forward,side,.018f*load*weight,0,s*.12f*salute*weight,0,0);
                ApplyArmFk(parts,s<0?7:10,new ArmAngles(135,18,0,20,-8,-5),salute*weight);
                EmotionHead(parts,forward,side,-.07f*salute*weight,s*.12f*salute*weight);
                EmotionClap(parts,forward,side,t-.90f,weight*Smooth((t-.7f)/.3f),2);break;
            }
            default: // Acknowledge the assister, hand to chest, then welcome.
            {
                float point=GoalPulse(t,.05f,.45f,.95f),thanks=GoalPulse(t,.75f,1.12f,1.75f);
                EmotionBody(parts,forward,side,.022f*load*weight,.09f*thanks*weight,s*.09f*point*weight,0,0);
                ApplyArmFk(parts,s<0?7:10,new ArmAngles(79,10,0,18,0,-5),point*weight);
                ApplyArmFk(parts,s<0?10:7,new ArmAngles(28,12,0,112),thanks*weight);
                var chest=parts[6];ApplyArmContact(parts,s<0?10:7,chest.B+forward*.18f,chest.ZB-.12f,side*(-s),-.5f,thanks*weight);
                EmotionHead(parts,forward,side,.12f*thanks*weight,s*.08f*point*weight);break;
            }
        }
        KeepKeeperAboveGround(parts);
    }

    static void GoalIdentity(LegendaryAbility identity,Footballer p,MatchSimulation m,List<Part> parts,
        Point forward,Point side,float t,float weight)
    {
        if(weight<=0)return;
        float load=GoalPulse(t,.03f,.24f,.45f);
        switch(identity)
        {
            case LegendaryAbility.SkySiu:
            {
                float flight=GoalJump(t,.40f,.58f);
                float land=GoalPulse(t,.93f,1.09f,1.42f);
                float rotate=Smooth((t-.40f)/.58f)*(1-Smooth((t-1.85f)/.64f));
                EmotionBody(parts,forward,side,(.095f*load+.085f*land)*weight,-.12f*land*weight,
                    0,0,.27f*flight*weight,.10f*Smooth((t-.65f)/.35f)*weight);
                float spread=Smooth((t-.75f)/.28f);
                for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,
                    new ArmAngles(GoalMix(145,-24,spread),GoalMix(20,42,spread),-5,GoalMix(25,10,spread),15),weight);
                EmotionHead(parts,forward,side,-.10f*land*weight,0);
                GoalTurn(parts,p.Position,MathF.PI*rotate*weight);break;
            }
            case LegendaryAbility.LaserFinish:
            {
                float seated=Smooth((t-.12f)/.95f)*(1-Smooth((t-2.65f)/.95f));
                EmotionBody(parts,forward,side,.68f*seated*weight,.06f*seated*weight,0,0,0);
                for(int leg=0;leg<2;leg++)
                {
                    int sign=leg==0?-1:1;var rest=parts[leg*3+1];
                    // A crossed seated support needs knees OUT and above the
                    // turf. A far opposite ankle plus a downward pole made the
                    // solver put both knees below ground, then lift a kneeling
                    // rig. Keep the ankles near the midline and the pole high.
                    var target=p.Position+forward*.18f-side*(sign*.025f);
                    var boot=parts[leg*3+2];float blend=seated*weight;
                    var bootDirection=((boot.B-boot.A).Unit*(1-blend)-side*(sign*blend)).Unit;
                    KeeperLeg(parts,leg,rest.B*(1-blend)+target*blend,
                        rest.ZB*(1-blend)+.115f*blend,side*sign,.25f,bootDirection,.03f*blend);
                }
                for(int arm=0;arm<2;arm++)
                {
                    int sign=arm==0?-1:1;var knee=parts[arm*3];
                    ApplyArmFk(parts,7+arm*3,new ArmAngles(14,30,-5,22,75,20),seated*weight);
                    ApplyArmContact(parts,7+arm*3,knee.B+forward*.015f,knee.ZB+.055f,
                        side*sign,-.20f,seated*weight);
                }
                EmotionHead(parts,forward,side,.06f*seated*weight,0);break;
            }
            case LegendaryAbility.OrderSeven:
            {
                float fold=Smooth((t-.15f)/.44f)*(1-Smooth((t-1.5f)/.4f));
                EmotionBody(parts,forward,side,.03f*load*weight,-.055f*fold*weight,0,0,0,.025f*weight);
                for(int arm=0;arm<2;arm++)
                {
                    int sign=arm==0?-1:1;
                    ApplyArmFk(parts,7+arm*3,new ArmAngles(58,24,0,112,10),fold*weight);
                    var chest=parts[6];var target=chest.B+forward*(arm==0?.19f:.225f)-side*(sign*.145f);
                    ApplyArmContact(parts,7+arm*3,target,chest.ZB+(arm==0?-.12f:-.045f),side*sign,-.4f,fold*weight);
                }
                EmotionHead(parts,forward,side,-.035f*fold*weight,0);break;
            }
            case LegendaryAbility.OtherGravity:
            {
                float sky=GoalPulse(t,.15f,.72f,1.55f),nod=GoalPulse(t,1.10f,1.40f,1.80f);
                EmotionBody(parts,forward,side,.018f*load*weight,-.025f*sky*weight,0,.015f*sky*weight,0);
                ApplyArmFk(parts,7,new ArmAngles(145,12,0,18,-8),sky*weight);
                ApplyArmFk(parts,10,new ArmAngles(132,8,0,28,-8),sky*weight);
                EmotionHead(parts,forward,side,(-.17f*sky+.10f*nod)*weight,.08f*nod*weight);break;
            }
            case LegendaryAbility.RoyalReception:
            {
                float open=Smooth((t-.10f)/.38f),bow=GoalPulse(t,1.05f,1.42f,1.90f);
                EmotionBody(parts,forward,side,.025f*load*weight,(-.13f*open+.36f*bow)*weight,0,0,0,.03f*weight);
                for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(-8,76-bow*28,0,15+bow*20),weight*open);
                EmotionHead(parts,forward,side,(-.10f*open+.15f*bow)*weight,0);break;
            }
            case LegendaryAbility.ChessPass:
            {
                float nod=GoalPulse(t,.25f,.55f,.88f),scan=GoalPulse(t,.75f,1.15f,1.7f);
                EmotionBody(parts,forward,side,.016f*load*weight,.035f*nod*weight,.10f*scan*weight,0,0);
                ApplyArmFk(parts,7,new ArmAngles(7,9,0,24),weight);
                ApplyArmFk(parts,10,new ArmAngles(20,13,0,88),nod*weight);
                EmotionHead(parts,forward,side,.14f*nod*weight,-.17f*scan*weight);break;
            }
            case LegendaryAbility.EntryDenied:
            {
                float fist=GoalPulse(t,.12f,.48f,.95f),ack=GoalPulse(t,.85f,1.2f,1.7f);
                EmotionBody(parts,forward,side,.03f*load*weight,.055f*ack*weight,-.065f*fist*weight,0,0);
                ApplyArmFk(parts,7,new ArmAngles(30,12,-5,113),fist*weight);
                ApplyArmFk(parts,10,new ArmAngles(70,15,0,24),ack*weight);
                EmotionHead(parts,forward,side,.11f*ack*weight,.08f*ack*weight);break;
            }
            case LegendaryAbility.NotToday:
            {
                float roar=GoalPulse(t,.16f,.55f,1.07f),organise=GoalPulse(t,.95f,1.30f,1.85f);
                EmotionBody(parts,forward,side,.065f*load*weight,-.10f*roar*weight,.13f*organise*weight,0,0,.025f*weight);
                for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(30,19,0,112,15),roar*weight);
                ApplyArmFk(parts,10,new ArmAngles(79,10,0,18,-5,-5),organise*weight);
                EmotionHead(parts,forward,side,-.10f*roar*weight,.18f*organise*weight);break;
            }
        }
        KeepKeeperAboveGround(parts);
    }

    static void GoalGreeting(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,float weight)
    {
        if(weight<=0)return;
        EmotionBody(parts,forward,side,0,.075f*weight,0,0,0);
        for(int arm=0;arm<2;arm++)
        {
            int sign=arm==0?-1:1;
            ApplyArmFk(parts,7+arm*3,new ArmAngles(66,22,0,48,0,-5),weight);
            int targetId=p.Id==m.GoalScorerId?(arm<m.GoalCelebrants.Length?m.GoalCelebrants[arm]:-1):m.GoalScorerId;
            if((uint)targetId>=m.Players.Count)continue;
            var other=m.Players[targetId];var delta=other.Position-p.Position;
            if(delta.Length>1.20f || delta.Length<.20f)continue;
            // Touch the near upper torso, not a fictitious remote hand target.
            var target=other.Position-delta.Unit*.22f+side*(sign*.08f);
            ApplyArmContact(parts,7+arm*3,target,1.22f,side*sign,-.25f,weight*.8f);
        }
    }

    static float GoalScorerLowPose(MatchSimulation m)
    {
        var scorer=m.Players[m.GoalScorerId];
        float arrival=(uint)scorer.Id<m.GoalCelebrationArrivedAt.Length?m.GoalCelebrationArrivedAt[scorer.Id]:-1;
        if(arrival<0)return 0;
        float t=m.GoalCelebrationElapsed-arrival;
        if(LegendaryAbilities.Get(scorer.Profile)?.Kind==LegendaryAbility.LaserFinish)
            return Smooth((t-.12f)/.95f)*(1-Smooth((t-2.65f)/.95f));
        if(LegendaryAbilities.Get(scorer.Profile)==null && m.GoalCelebrationVariant==3)
            return Smooth((t-.10f)/.65f)*(1-Smooth((t-1.45f)/.82f));
        return 0;
    }

    static void GoalConceded(Footballer p,MatchSimulation m,List<Part> parts,Point forward,Point side,float age,float weight)
    {
        float start=.12f+(p.Id%5)*.065f;
        float response=Smooth((age-start)/.45f)*weight;
        int variant=PlayerDialogue.Temperament(p)==PlayerTemperament.Leader?2:p.Id%3;
        float sigh=GoalPulse(age,start,start+.6f,start+1.5f);
        if(variant==0)
        {
            EmotionBody(parts,forward,side,.035f*sigh*weight,.16f*response,0,0,0);
            EmotionHead(parts,forward,side,.16f*response,0);
            float touch=GoalPulse(age,start+.35f,start+.9f,start+2.0f)*weight;
            for(int arm=0;arm<2;arm++)
            {
                ApplyArmFk(parts,7+arm*3,new ArmAngles(126,40,-10,110,10),touch);
                var head=parts[13];int sign=arm==0?-1:1;
                ApplyArmContact(parts,7+arm*3,head.A+side*(sign*.115f)+forward*.04f,head.ZA+.04f,side*sign,-.2f,touch);
            }
        }
        else if(variant==1)
        {
            EmotionBody(parts,forward,side,.03f*sigh*weight,.12f*response,-.04f*response,0,0);
            for(int arm=0;arm<2;arm++)ApplyArmFk(parts,7+arm*3,new ArmAngles(6,28,-5,88,15),response);
            EmotionHead(parts,forward,side,.12f*response,.08f*MathF.Sin(age*2)*GoalPulse(age,.4f,1.1f,2.2f)*weight);
        }
        else
        {
            float rally=GoalPulse(age,.65f,1.25f,2.1f)*weight;
            EmotionBody(parts,forward,side,.02f*sigh*weight,.04f*rally,0,0,0);
            ApplyArmFk(parts,7,new ArmAngles(35,18,0,92,65),rally);
            ApplyArmFk(parts,10,new ArmAngles(63,12,0,55,40),rally);
            EmotionHead(parts,forward,side,.10f*sigh*weight,.15f*rally);
        }
        KeepKeeperAboveGround(parts);
    }

    static void GoalTurn(List<Part> parts,Point root,float yaw)
    {
        float cos=MathF.Cos(yaw),sin=MathF.Sin(yaw);
        Point Rotate(Point p){var d=p-root;return root+new Point(d.X*cos-d.Y*sin,d.X*sin+d.Y*cos);}
        for(int i=0;i<parts.Count;i++){var v=parts[i];parts[i]=v with{A=Rotate(v.A),B=Rotate(v.B)};}
    }
}