MatchPeriodFlow.cs

Partial MatchSimulation class for a soccer match. Handles the period clock, end-of-half whistle, players and referee walking to the bench, halftime transition, and the final whistle or shootout trigger.

Rough Code
using System;
using System.Linq;

namespace Socer;

public sealed partial class MatchSimulation
{
    // Match time and the presentation walk are separate: the clock stops at the whistle.
    bool managedMatchClock, finalWhistleComplete;
    public float PeriodWalkRemaining { get; private set; }
    public int SecondHalfIntroSequence { get; private set; }
    public Point[] HalfTimeReturnPositions {get;private set;}=Array.Empty<Point>();
    public bool PeriodWhistleWasFinal { get; private set; }

    float PeriodDeadline => SecondHalf
        ? 360+Math.Max(0,FirstHalfAddedMinutes)*4+Math.Max(0,SecondHalfAddedMinutes)*4
        : 180+Math.Max(0,FirstHalfAddedMinutes)*4;

    bool PromisingAttack()
    {
        if(GoalHoldRemaining>0 || ShotMissFlightRemaining>0 || IncidentRemaining>0)return false;
        if(RestartRemaining>0)
        {
            if(SetPiece==RestartKind.Penalty)return true;
            if(SetPiece is RestartKind.FreeKick or RestartKind.Indirect)
                return (World(restartPoint,restartTeam)-new Point(105,34)).Length<32;
            return false;
        }
        if(Ball.X<0 || Ball.X>105 || Ball.Y<0 || Ball.Y>68)return false;
        if(shot || WindupPlayer>=0)return true;
        int team=Owner>=0?Players[Owner].Team:LastTouch;
        var local=World(Ball,team);
        return local.X>74 && Math.Abs(local.Y-34)<29 &&
            (Owner>=0 || Point.Dot(BallVelocity,new Point(Direction(team),0))>2);
    }

    bool CheckPeriodEnd()
    {
        if(MedicalActive)return false;
        if(PeriodWalkRemaining>0 || PeriodDeadline>Seconds)return false;
        // Added time is a minimum. Allow a short attack to conclude; a penalty
        // at time is completed even if its preparation takes longer.
        float extension=RestartRemaining>0 && SetPiece==RestartKind.Penalty?30:12;
        if(Seconds<PeriodDeadline+extension && PromisingAttack())return false;
        PeriodWhistleWasFinal=SecondHalf;
        PeriodWalkRemaining=3.4f;
        Owner=-1;BallVelocity=new();CancelWindup();
        AddEvent(SecondHalf?"Финальный свисток":"Свисток на перерыв");
        return true;
    }

    void AdvancePeriodWalk(float dt)
    {
        PeriodWalkRemaining=Math.Max(0,PeriodWalkRemaining-dt);
        foreach(var p in ActivePlayers)
        {
            if(p.IsAcrobaticCommitted(Seconds))
            {p.Velocity=new Point();p.Action="Поднимается после акробатического удара";continue;}
            float bench=p.Team==0?39.5f:65.5f;
            var target=new Point(bench+(p.Number-6)*.7f,1.2f+(p.Number%3)*.65f);
            var delta=target-p.Position;
            float speed=Math.Min(2.5f,delta.Length*1.4f);
            var desired=delta.Length>.05f?delta.Unit*speed:new Point();
            var change=desired-p.Velocity;
            p.Velocity+=change.Unit*Math.Min(change.Length,4.5f*dt);
            p.Position=Clamp(p.Position+p.Velocity*dt);
            if(delta.Length>.05f)UpdateFacing(p,target,dt);
            p.Action=PeriodWhistleWasFinal?"Идёт к скамейке после матча":"Идёт к скамейке на перерыв";
        }
        var refereeTarget=new Point(52.5f,1.4f);
        var refereeDelta=refereeTarget-RefereePosition;
        var previous=RefereePosition;
        RefereePosition+=refereeDelta.Unit*Math.Min(refereeDelta.Length,2.2f*dt);
        RefereeActor.VisualBefore=previous;RefereeActor.Position=RefereePosition;
        AdvanceGait(RefereeActor,dt);
        if(PeriodWalkRemaining>0)return;
        if(PeriodWhistleWasFinal){if(!BeginShootout())finalWhistleComplete=true;return;}
        SecondHalf=true;HalfStoppageSeconds=0;
        HalfTimeReturnPositions=Players.Select(p=>p.Position).ToArray();
        foreach(var p in ActivePlayers){p.Stamina=Math.Min(1,p.Stamina+.16f);p.BurstCharge=1;}
        Restart(1,new Point(52.5f,34),"Второй тайм · смена ворот",true);
        RefereePosition=new Point(52.5f,34);
        RefereeActor.Position=RefereePosition;
        SecondHalfIntroSequence++;
    }
}