StoppageTime.cs

Partial MatchSimulation class that tracks stoppage time for each half. Counts added time from incidents, restarts and goals, then exposes the broadcast clock and referee added-time signals.

Rough Code
using System;

namespace Socer;

public sealed partial class MatchSimulation
{
    // Four simulation seconds represent one displayed football minute.
    public int FirstHalfAddedMinutes=-1,SecondHalfAddedMinutes=-1;
    public float HalfStoppageSeconds;
    public int AddedMinutes => SecondHalf?Math.Max(0,SecondHalfAddedMinutes):Math.Max(0,FirstHalfAddedMinutes);
    public bool InAddedTime => !SecondHalf
        ? FirstHalfAddedMinutes>=0 && Seconds>=180
        : SecondHalfAddedMinutes>=0 && Seconds>=360+Math.Max(0,FirstHalfAddedMinutes)*4;
    public string BroadcastClock
    {
        get
        {
            if((ShootoutActive || ShootoutComplete) && shootoutMatchClock!=null)return shootoutMatchClock;
            float elapsed=Seconds-(SecondHalf?Math.Max(0,FirstHalfAddedMinutes)*4:0);
            int total=Math.Max(0,(int)MathF.Floor(elapsed*15));
            return $"{total/60:00}:{total%60:00}";
        }
    }
    public string ClockLabel
    {
        get
        {
            if((ShootoutActive || ShootoutComplete) && shootoutMatchClockLabel!=null)return shootoutMatchClockLabel;
            if(!SecondHalf)
                return InAddedTime?$"45+{Math.Clamp((int)Math.Floor((Seconds-180)/4)+1,1,Math.Max(1,FirstHalfAddedMinutes))}":$"{Minute:00}";
            float regularEnd=360+Math.Max(0,FirstHalfAddedMinutes)*4;
            return InAddedTime?$"90+{Math.Clamp((int)Math.Floor((Seconds-regularEnd)/4)+1,1,Math.Max(1,SecondHalfAddedMinutes))}":$"{Minute:00}";
        }
    }
    void CountStoppage(float dt)
    {
        if((SecondHalf?SecondHalfAddedMinutes:FirstHalfAddedMinutes)>=0)return;
        if(IncidentRemaining>0)HalfStoppageSeconds+=dt;
        else if(RestartRemaining>0 && SetPiece!=RestartKind.KickOff)HalfStoppageSeconds+=dt*.45f;
    }
    void CountGoalStoppage(float dt)
    {
        if((SecondHalf?SecondHalfAddedMinutes:FirstHalfAddedMinutes)<0)HalfStoppageSeconds+=dt*.8f;
    }
    static int AddedFor(float stoppage,int maximum)=>Math.Clamp((int)MathF.Ceiling(stoppage/4),0,maximum);
    void AnnounceAddedTime()
    {
        if(!SecondHalf && Seconds>=180 && FirstHalfAddedMinutes<0)
        {
            FirstHalfAddedMinutes=AddedFor(HalfStoppageSeconds,5);
            RefereeSignal=$"+{FirstHalfAddedMinutes} МИН";RefereeSignalUntil=Seconds+3;ShownCard=0;
            AddEvent($"Судья добавил {FirstHalfAddedMinutes} мин. к первому тайму");
        }
        else if(SecondHalf && Seconds>=360+Math.Max(0,FirstHalfAddedMinutes)*4 && SecondHalfAddedMinutes<0)
        {
            SecondHalfAddedMinutes=AddedFor(HalfStoppageSeconds,7);
            RefereeSignal=$"+{SecondHalfAddedMinutes} МИН";RefereeSignalUntil=Seconds+3;ShownCard=0;
            AddEvent($"Судья добавил {SecondHalfAddedMinutes} мин. ко второму тайму");
        }
    }
}