InjurySubstitutionPanel.razor

Razor panel for the injury substitution prompt in a soccer match. Shows the injured player's portrait and name, lists eligible replacement cards sorted by suitability with keyboard navigation, and confirms either the substitution or continuing short-handed when no reserve is available.

Rough Code
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Linq
@inherits Panel

<div class="injury-choice localized-text">
    <label class="injury-kicker">@Text("MEDICAL STOPPAGE", "МЕДИЦИНСКАЯ ОСТАНОВКА")</label>
    <label class="injury-title">@Text("A substitution is required", "Нужна замена")</label>
    <div class="injured-player">
        @if(Outgoing!=null){<img src="@PlayerAppearance.Portrait(Outgoing)" />}
        <div><label>@(Outgoing?.Name??Game.InjuredMatchPlayer?.Profile.Name)</label><label class="injury-copy">@Text("Cannot continue this match. Taken off on a stretcher.", "Не может продолжить матч. Унесён с поля на носилках.")</label></div>
    </div>
    @if(Candidates.Length>0)
    {
        <label class="injury-copy">@(Game.InjuryNeedsEmergencyKeeper?Text("No reserve goalkeeper. Choose an outfield player to take the gloves.", "Запасного вратаря нет. Выберите полевого игрока, который встанет в ворота."):Text("Choose who comes on. Players are ranked by suitability for this position.", "Выберите, кто выйдет. Запасные отсортированы по пригодности для этой позиции."))</label>
        <div class="injury-candidates">
            @foreach(var card in Candidates)
            {
                <button class="injury-candidate @(Selected==card.Id?"chosen":"")" onclick=@(()=>Selected=card.Id)>
                    <img src="@PlayerAppearance.Portrait(card)" />
                    <div><label>@card.Name</label><label class="candidate-role">@(PlayerPositions.Card(card)+" · "+card.Overall)</label>
                        @if(card.Id==Candidates[0].Id){<label class="candidate-best">@Text("Best fit", "Лучше подходит")</label>}
                    </div>
                </button>
            }
        </div>
        <div class="injury-footer"><label>@($"{Game.SubstitutionsUsed}/{MatchController.SubstitutionLimit} "+Text("substitutions used", "замен использовано"))</label><button class="primary injury-confirm" disabled="@(!Candidates.Any(c=>c.Id==Selected))" onclick=@Confirm>@Text("Bring on →", "Выпустить →")</button></div>
    }
    else
    {
        <label class="injury-copy">@(Game.SubstitutionsUsed>=MatchController.SubstitutionLimit?Text("All five substitutions have been used. The team will continue short-handed.", "Все пять замен использованы. Команда продолжит в меньшинстве."):Text("There is no eligible reserve. The team will continue short-handed.", "Подходящих запасных нет. Команда продолжит в меньшинстве."))</label>
        <button class="primary injury-confirm" onclick=@(()=>Game.ContinueAfterInjuryWithoutReserve())>@Text("Continue match →", "Продолжить матч →")</button>
    }
</div>

@code {
    [Parameter] public MatchController Game {get;set;}
    int Selected=-1;
    PlayerCard Outgoing=>Game.MatchCard(Game.InjuredMatchSlot);
    PlayerCard[] Candidates=>Game.InjuryReplacementCandidates.ToArray();
    static string Text(string en,string ru)=>GamePreferences.Russian?ru:en;
    void Confirm(){if(Game.ReplaceInjuredPlayer(Selected))Selected=-1;}
    public bool HandleHotkey(string id)
    {
        var candidates=Candidates;
        if(id=="app.confirm")
        {
            if(candidates.Length==0)return Game.ContinueAfterInjuryWithoutReserve();
            if(!candidates.Any(c=>c.Id==Selected))return false;
            Confirm();return true;
        }
        if(id is not ("player.previous" or "player.next") || candidates.Length==0)return false;
        int step=id=="player.previous"?-1:1,index=Array.FindIndex(candidates,c=>c.Id==Selected);
        Selected=candidates[index<0?(step>0?0:candidates.Length-1):(index+step+candidates.Length)%candidates.Length].Id;return true;
    }
    // The compulsory choice freezes the match: avoid frame-driven rebuilds
    // which would repeatedly replace already-localized labels.
    protected override int BuildHash()=>HashCode.Combine(Game?.Match,Game?.InjuredMatchSlot,Game?.SubstitutionsUsed,Selected,GamePreferences.Revision);
}