FootballCeremony.cs

Part of the football world panel that renders a pre-match and second-half intro ceremony. Drives the cutscene camera, positions all 22 players plus a referee, places the ball and poses the procedural figure parts for each frame.

Rough Code
using Sandbox;
using Socer;
using System;

public sealed partial class FootballWorldPanel
{
    readonly MatchSimulation ceremony=new(42){Seconds=30,Owner=-1,RestartRemaining=0};
    readonly Footballer ceremonyReferee=new(){Id=22,Team=2,Number=0};
    static float CeremonyEase(float t){t=Math.Clamp(t,0,1);return t*t*(3-2*t);}

    void DrawCeremony(float age,out Point ballPoint,out float ballZ)
    {
        bool second=Game.SecondHalfIntro;
        float travel=CeremonyEase(age/5.7f);
        var focus=new Point(second?52.5f:42+travel*12,34);
        var eye=WorldPoint(focus+new Point(-3,12),3.1f);
        var viewFocus=WorldPoint(focus,1);
        float cameraBlend=second?CeremonyEase((age-5.3f)/1.3f):0;
        if(second)
        {
            var broadcast=Game.Broadcast;
            float distance=broadcast.ViewWidth*1.08f;
            var normalEye=WorldPoint(broadcast.Center+new Point(0,distance*.78f),distance*.63f);
            var normalFocus=WorldPoint(broadcast.Center);
            float ceremonyBlend=CeremonyEase(age/1.3f)*(1-cameraBlend);
            eye=normalEye*(1-ceremonyBlend)+eye*ceremonyBlend;
            viewFocus=normalFocus*(1-ceremonyBlend)+viewFocus*ceremonyBlend;
        }
        sceneCamera.FieldOfView=second?50+6*CeremonyEase(age/1.3f)*(1-cameraBlend):56;
        sceneCamera.WorldPosition=eye;
        sceneCamera.WorldRotation=Rotation.LookAt(viewFocus-eye);
        ballPoint=second?new Point(52.5f,30+4*CeremonyEase(age/2.6f)):new Point(52.5f,34);
        ballZ=FootballSecondaryMotion.CeremonyBallHeight(age);
        ceremony.Seconds=30+age;
        for(int i=0;i<23;i++)
        {
            var p=i==22?ceremonyReferee:ceremony.Players[i];
            if(i<22)
            {
                var source=Game.Match.Players[i];p.Number=source.Number;p.Team=source.Team;
                p.Profile=source.Profile;p.EmergencyKeeper=source.EmergencyKeeper;
                p.PreferredFoot=source.PreferredFoot;
                // The return ceremony uses the live roster after substitutions
                // and dismissals, including a keeper in an outfield shirt.
                if((source.SentOff && Game.Match.Seconds>=source.ImpactUntil) || source.InjuryRemovedFromField)
                {
                    foreach(var part in actors[i])part.Enabled=false;
                    ApplyPlayerDetails(i,p.Profile.CatalogId,p.Profile.Edition,p.Number,false);
                    continue;
                }
                int slot=i%11;
                if(second && Game.Match.HalfTimeReturnPositions.Length==22)
                {
                    var from=Game.Match.HalfTimeReturnPositions[i];
                    // The intro is a presentation cut: show the final approach
                    // rather than racing the full halftime path in 2.8 seconds.
                    var entry=FootballSecondaryMotion.Return(from,source.Position,age,5.2f,i);
                    p.Position=entry.Position;p.VisualVelocity=entry.Velocity;
                    p.VisualAcceleration=entry.Acceleration;p.VisualSpeed=entry.Velocity.Length;
                    p.GaitPhase=entry.GaitPhase;
                }
                else
                {
                    p.Position=second?source.Position:new Point(i<11?38.5f+slot*1.15f:55+slot*1.15f,34);
                    p.VisualVelocity=p.VisualAcceleration=new Point();p.VisualSpeed=0;
                }
            }
            else
            {
                var entry=FootballSecondaryMotion.Return(new Point(52.5f,29.72f),new Point(52.5f,33.72f),age,2.6f,22);
                p.Position=ballPoint-new Point(0,.28f);
                p.VisualVelocity=second?entry.Velocity:new Point();
                p.VisualAcceleration=second?entry.Acceleration:new Point();p.VisualSpeed=p.VisualVelocity.Length;
                p.GaitPhase=entry.GaitPhase;
            }
            p.Facing=second && i<22?Game.Match.Players[i].Facing:MathF.PI*.5f;
            if(second && i<22 && p.VisualVelocity.Length>.01f)
            {
                float travelFacing=MathF.Atan2(p.VisualVelocity.Y,p.VisualVelocity.X);
                float settle=CeremonyEase((age-4.25f)/.95f);
                p.Facing=travelFacing+MathF.Atan2(MathF.Sin(p.Facing-travelFacing),MathF.Cos(p.Facing-travelFacing))*settle;
            }
            ReplayFigure.Pose(p,ceremony,pose);
            if(i==22)PoseCeremonyReferee(age,ballPoint,ballZ);
            else
            {
                // Small breathing/weight shifts, without advancing football or random state.
                float sway=MathF.Sin(age*1.4f+i*.8f)*.012f;
                for(int j=0;j<pose.Count;j++)
                {
                    var v=pose[j];
                    Point Shift(Point at,float z)=>at+new Point(sway*Math.Clamp(z/.9f,0,1),0);
                    pose[j]=v with{A=Shift(v.A,v.ZA),B=Shift(v.B,v.ZB)};
                }
            }
            if(!p.Keeper)ReplayFigure.RepairArmAnatomy(pose);
            for(int j=0;j<actors[i].Length;j++)
            {
                actors[i][j].Enabled=j<pose.Count;
                if(j>=pose.Count)continue;
                // Share the field/replay palette, including the equipped club
                // kit. Keeper and legendary fabrics retain their own colours.
                var v=pose[j];SetPart(actors[i][j],v.A,v.ZA,v.B,v.ZB,v.Width,TeamKitColor(i,v.Color));
                FitPoseBody(actors[i][j],j,pose,p.Keeper);
            }
            ApplyPlayerDetails(i,p.Profile.CatalogId,p.Profile.Edition,p.Number,i<22);
        }
    }

    void PoseCeremonyReferee(float age,Point ballPoint,float ballZ,Point forward=default)
        =>ReplayFigure.PoseCeremonyBallPlacement(pose,forward.Length>.1f?forward:new Point(0,1),ballPoint,ballZ,age);
}