Match director for a football match simulation. Chooses playback speed and a Russian caption based on match state: slow motion during shots, faster tempo during buildup, fixed speed during incidents, shootouts and the final whistle. Only changes presentation timing, not simulation steps.
using System;
namespace Socer;
// Changes wall-clock presentation only. Football always advances in identical 1/60 s steps.
public sealed class MatchDirector
{
public bool Automatic=true;
public float ManualSpeed=1;
public float Rate=1;
// Automatic direction uses the livelier baseline; manual labels are exact wall-clock speeds.
public const float BaseTempo=1.25f;
public const float MaximumRate=1.5f;
public float PlaybackRate => Automatic?Math.Min(MaximumRate,Rate*BaseTempo):Math.Clamp(ManualSpeed,.5f,MaximumRate);
public float TickCost => (1f/60f)/PlaybackRate;
public string Caption="ОБЗОР МАТЧА";
float dangerHold;
int seenShot, seenContact;
float shotRealAge, contactRelease;
public void Update(MatchSimulation match,float realDelta)
{
float target;
Rate=Math.Min(Rate,MaximumRate);
if(!Automatic) { Rate=Math.Clamp(ManualSpeed,.5f,MaximumRate); Caption="РУЧНАЯ СКОРОСТЬ"; return; }
if(match.MedicalActive){Rate=1/BaseTempo;Caption="МЕДИЦИНСКАЯ ПОМОЩЬ";return;}
if(match.ShootoutActive){Rate=1/BaseTempo;Caption="СЕРИЯ ПЕНАЛЬТИ";return;}
if(seenShot!=match.ShotSequence) {seenShot=match.ShotSequence;shotRealAge=0;contactRelease=0;}
if(match.ShotContactSequence>0 && seenContact!=match.ShotContactSequence)
{seenContact=match.ShotContactSequence;contactRelease=.7f;}
if(match.Finished) {Rate=1;Caption="ФИНАЛЬНЫЙ СВИСТОК";return;}
if(match.WindupPlayer>=0 || match.RestartRemaining>0 || match.IncidentRemaining>0)
{
contactRelease=0;Rate=1;dangerHold=1.5f;
Caption=match.IncidentRemaining>0?"СУДЬЯ ОСТАНОВИЛ ИГРУ":match.RestartRemaining>0?"СТАНДАРТ":"ПОДГОТОВКА УДАРА";
return;
}
bool slowFlight=match.ShotInFlight && match.ShotContactSequence!=match.ShotSequence && match.ShotCueRemaining>0;
if(contactRelease>0)
{
contactRelease=Math.Max(0,contactRelease-realDelta);
target=1.5f;Caption=$"{match.ShotContactKind} · ВОЗВРАТ ТЕМПА";
}
else if(slowFlight)
{
shotRealAge+=realDelta;
float easeDuration=Math.Clamp((match.ShotFlightAge+match.ShotCueRemaining-.35f)*.5f,.06f,.32f);
float blend=Math.Clamp(shotRealAge/easeDuration,0,1);blend=blend*blend*(3-2*blend);
float progress=match.ShotFlightAge/Math.Max(.01f,match.ShotFlightAge+match.ShotCueRemaining);
target=1-blend*(.76f+.1f*progress);
dangerHold=1.6f;Caption="ПОЛЁТ МЯЧА · СЛОУ-МО";
}
else if(match.GoalHoldRemaining>0) {target=1;Caption="ГОЛ!";}
else
{
if(match.Danger>.18f || match.WindupPlayer>=0 || match.DuelDefender>=0 || match.PickupDelay>0) dangerHold=1.5f;
else dangerHold=Math.Max(0,dangerHold-realDelta);
bool danger=dangerHold>0;
target=danger?1:MaximumRate;
Caption=danger?"ОПАСНАЯ АТАКА":"ПОСТРОЕНИЕ АТАКИ";
}
// Ease into the shot, then accelerate through the first physical contact.
if(slowFlight && contactRelease<=0) Rate=target;
else Rate+=Math.Clamp(target-Rate,-realDelta*4,realDelta*(contactRelease>0?6:2.5f));
if(!slowFlight && contactRelease<=0) Rate=Math.Max(1,Rate);
}
}