MatchInjurySubstitutions.cs

Part of MatchController that handles injury substitutions in the football match sim: detects a pending injury for the home team, pauses and opens the replacement choice, auto-picks for quick matches, and applies the swap to the match roster and lineup. Also handles continuing with no available substitute and offers an emergency keeper when no goalkeeper is on the bench.

Rough Code
using System;
using System.Linq;
using System.Collections.Generic;
using Socer;

public sealed partial class MatchController
{
    MatchSimulation injuryManagedMatch;
    int shownInjurySlot=-1;
    bool pausedBeforeInjuryChoice;
    public int InjuredMatchSlot=>Match?.PendingInjurySubstitutionTeam==0?Match.PendingInjurySubstitutionSlot:-1;
    public bool InjurySubstitutionRequired=>InjuredMatchSlot>=0;
    public bool InjuryChoiceOpen=>InjurySubstitutionRequired && !QuickMatchRunning;
    public Footballer InjuredMatchPlayer=>InjurySubstitutionRequired?Match.MedicalPatient:null;
    public bool InjuryNeedsEmergencyKeeper=>InjuredMatchPlayer?.Keeper==true && !AvailableSubstitutes.Any(c=>c.Position=="ВР");
    public IEnumerable<PlayerCard> InjuryReplacementCandidates=>!InjurySubstitutionRequired || SubstitutionsUsed>=SubstitutionLimit
        ?Enumerable.Empty<PlayerCard>():AvailableSubstitutes.Where(c=>!InjuredMatchPlayer.Keeper?c.Position!="ВР":
            c.Position=="ВР" || InjuryNeedsEmergencyKeeper)
        .OrderByDescending(c=>InjuredMatchPlayer.Keeper?c.Position=="ВР"?2f:c.Keeping/99f:Formation.Fit(c,ActiveMatchRoster.Shape,InjuredMatchSlot))
        .ThenByDescending(c=>c.Overall).ThenBy(c=>c.Id);

    void EnsureInjuryControl()
    {
        if(Match==null)return;
        if(injuryManagedMatch!=Match)
        {injuryManagedMatch=Match;shownInjurySlot=-1;}
        bool laboratory=FootballLabActive || StrategyLabOpen || (MatchTutorialActive && !MatchTutorialInjuryLesson) || MediaLeagueActive;
        Match.ManualHomeInjurySubstitution=!laboratory;
        Match.InjurySubstitutionsUsed[0]=SubstitutionsUsed;
        // The injury lesson owns one real prepared medical incident; it does
        // not enable random extra injuries while the learner is practising.
        if(laboratory || MatchTutorialActive)Match.InjuriesEnabled=false;
        if(QuickMatchRunning)CollectMedicalBroadcastEvents();
        if(!InjurySubstitutionRequired)return;
        if(QuickMatchRunning)
        {
            var candidate=InjuryReplacementCandidates.FirstOrDefault();
            bool replaced=candidate!=null && ReplaceInjuredPlayer(candidate.Id);
            if(!replaced)ContinueAfterInjuryWithoutReserve();
            CollectMedicalBroadcastEvents();
            return;
        }
        if(shownInjurySlot!=InjuredMatchSlot)
        {pausedBeforeInjuryChoice=Paused;shownInjurySlot=InjuredMatchSlot;CloseCommandWheel();CancelDirectOrder();}
        ClubOpen=false;Paused=true;accumulator=0;
    }

    public bool ReplaceInjuredPlayer(int cardId)
    {
        if(!InjurySubstitutionRequired || !InjuryReplacementCandidates.Any(c=>c.Id==cardId))return false;
        int slot=InjuredMatchSlot;var outgoing=InjuredMatchPlayer;var old=MatchCard(slot);
        var card=ActiveMatchRoster.Cards.First(c=>c.Id==cardId);
        var replacement=new Footballer{Id=slot,Team=0,Number=outgoing.Number,Home=outgoing.Home,
            Position=Match.MedicalExitPoint,Facing=outgoing.Facing,Instruction=outgoing.Instruction};
        ActiveMatchRoster.ApplyCard(replacement,card,slot);
        replacement.EmergencyKeeper=outgoing.Keeper && slot!=0;
        // The entrant joins the same tactical slot. His walking animation is
        // driven by the normal movement controller after the medical restart.
        replacement.Position=Match.World(new Point(Math.Min(outgoing.Home.X,48),outgoing.Home.Y),0);
        if(!Match.ResolveInjurySubstitution(slot,replacement))return false;
        Match.NotifyFootballSubstitution(0,slot);
        matchLineup??=ActiveMatchRoster.Lineup.ToArray();matchLineup[slot]=cardId;
        if(old!=null)substitutedOut.Add(old.Id);
        SubstitutionsUsed++;SubstitutionOpen=false;
        if(SelectedPlayer==slot || OrderedPlayer==slot){SelectedPlayer=OrderedPlayer=slot;AwaitingOrder=DirectOrderKind.None;}
        SubstitutionNotice=$"{card.Name} replaces injured {old?.Name??outgoing.Profile.Name}.";
        substitutionNoticeRussian=$"{card.Name} вместо травмированного {old?.Name??outgoing.Profile.Name}.";
        shownInjurySlot=-1;accumulator=0;
        if(!QuickMatchRunning)Paused=pausedBeforeInjuryChoice;
        return true;
    }

    public bool CanContinueAfterInjuryWithoutReserve=>InjurySubstitutionRequired && !InjuryReplacementCandidates.Any();
    public bool ContinueAfterInjuryWithoutReserve()
    {
        if(!CanContinueAfterInjuryWithoutReserve)return false;
        int slot=InjuredMatchSlot;var old=MatchCard(slot);
        if(!Match.ResolveInjurySubstitution(slot,null))return false;
        if(old!=null)substitutedOut.Add(old.Id);
        if(SelectedPlayer==slot || OrderedPlayer==slot)
        {SelectedPlayer=OrderedPlayer=Match.ActivePlayers.FirstOrDefault(p=>p.Team==0 && !p.Keeper)?.Id??-1;AwaitingOrder=DirectOrderKind.None;}
        shownInjurySlot=-1;accumulator=0;SubstitutionOpen=false;
        if(!QuickMatchRunning)Paused=pausedBeforeInjuryChoice;
        return true;
    }
}