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.
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);
}