Razor panel for the in-match substitution screen. Lists the 11 on-pitch players and the available bench, filters the bench by search text and position group, handles previous/next/confirm hotkeys, and calls MatchController.Substitute on confirm. Bilingual English/Russian strings and a hash based rebuild.
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@using System.Linq
@inherits Panel
<div class="substitution-panel localized-text">
<div class="sub-header"><div><label class="sub-kicker">@(Text("MATCH PAUSED", "МАТЧ НА ПАУЗЕ")+" · "+ClubIdentity.ShortName(0))</label><label class="sub-title">@Text("Substitutions","Замены")</label></div><button class="sub-close" onclick=@(()=>Game.CloseSubstitutions())>×</button></div>
<div class="sub-meter"><label>@($"{Game.SubstitutionsUsed} / {MatchController.SubstitutionLimit} "+Text("used","использовано"))</label><label>@($"{MatchController.SubstitutionLimit-Game.SubstitutionsUsed} "+Text("available","доступно"))</label></div>
@if(Game.MatchTutorialSubstitutionLesson){<label class="sub-notice">@Text("Practice: replace tired #10. Select him on the left, then an outfield reserve on the right.","Практика: замените уставшего №10. Выберите его слева, затем полевого запасного справа.")</label>}
<div class="sub-columns">
<div class="sub-column"><label class="sub-heading">@Text("ON THE PITCH · SELECT OUT","НА ПОЛЕ · КОГО ЗАМЕНИТЬ")</label><div class="sub-list">
@for(int i=0;i<11;i++)
{
int slot=i;var card=Game.MatchCard(slot);var actor=Game.Match.Players[slot];
if(card!=null){<button class="sub-row sub-out-@slot @(OutSlot==slot?"chosen":"")" disabled="@(actor.SentOff || Game.SubstitutionsUsed>=MatchController.SubstitutionLimit)" onclick=@(()=>OutSlot=slot)><span class="sub-ovr">@card.Overall</span><span class="sub-name">@($"#{actor.Number} · {card.Name}")<small>@($"{PlayerPositions.Slot(Game.ActiveMatchRoster.Shape,slot)} · {(int)(actor.Stamina*100)}% "+Text("energy","сил"))</small></span></button>}
}
</div></div>
<div class="sub-column"><label class="sub-heading">@Text("BENCH · SELECT IN","СКАМЕЙКА · КТО ВЫЙДЕТ")</label><div class="sub-filter">
<TextEntry Value:bind="@Search" Placeholder="@Text("Search bench","Поиск запасного")" />
<div>@foreach(var option in new[]{"All","GK","DEF","MID","ATT"}){var filter=option;<button class="@(Filter==filter?"chosen":"")" onclick=@(()=>Filter=filter)>@FilterName(filter)</button>}</div>
</div><div class="sub-list">
@foreach(var card in Bench)
{<button class="sub-row [email protected] @(InCard==card.Id?"chosen":"")" disabled="@(OutSlot>=0 && !Game.CanSubstitute(OutSlot,card.Id))" onclick=@(()=>InCard=card.Id)><span class="sub-ovr">@card.Overall</span><span class="sub-name">@card.Name<small>@($"{PlayerPositions.Card(card)} · {card.Flag} {Local(card.Country)}")</small></span></button>}
@if(!Bench.Any()){<label class="sub-empty">@Text("No available players match the filter.","Нет доступных игроков с таким фильтром.")</label>}
</div></div>
</div>
<label class="sub-notice">@Game.LocalizedSubstitutionNotice</label>
<label class="sub-notice">@($"{GameHotkeys.Display("player.previous")} / {GameHotkeys.Display("player.next")} · "+(HotkeyBench?Text("choose reserve","выбрать запасного"):Text("choose outgoing player","выбрать игрока на поле"))+$" · {GameHotkeys.Display("app.confirm")} · "+Text("continue / confirm","далее / подтвердить"))</label>
<div class="sub-actions"><button onclick=@(()=>Game.CloseSubstitutions())>@Text("Back to match","Вернуться к матчу")</button><button class="primary sub-confirm" disabled="@(OutSlot<0 || InCard<0 || !Game.CanSubstitute(OutSlot,InCard))" onclick=@Confirm>@Text("Confirm substitution →","Подтвердить замену →")</button></div>
</div>
@code {
[Parameter] public MatchController Game {get;set;}
int OutSlot=-1,InCard=-1;
string Search="",Filter="All";
static string Text(string en,string ru)=>GamePreferences.Russian?ru:en;
static string Local(string source)=>GamePreferences.Russian?UiRussian.Translate(source):source;
static string FilterName(string filter)=>filter switch {"All"=>Text("All","Все"),"GK"=>Text("GK","ВР"),"DEF"=>Text("DEF","ЗАЩ"),"MID"=>Text("MID","ПЗ"),_=>Text("ATT","НАП")};
bool Matches(PlayerCard card)=>Filter switch
{
"GK"=>card.Position=="ВР",
"DEF"=>card.Position is "КЗ" or "ЦЗ",
"MID"=>card.Position is "ЦП" or "КП",
"ATT"=>card.Position=="НП",
_=>true
};
PlayerCard[] Bench=>Game.AvailableSubstitutes.Where(c=>Matches(c) && (string.IsNullOrWhiteSpace(Search) || c.Name.Contains(Search,StringComparison.OrdinalIgnoreCase) || c.Country.Contains(Search,StringComparison.OrdinalIgnoreCase))).OrderByDescending(c=>c.Overall).ToArray();
void Confirm(){if(Game.Substitute(OutSlot,InCard)){OutSlot=-1;InCard=-1;}}
bool HotkeyBench;
public bool HandleHotkey(string id)
{
if(id=="app.confirm")
{
if(OutSlot<0)return false;
if(!HotkeyBench && InCard<0){HotkeyBench=true;return true;}
if(InCard<0 || !Game.CanSubstitute(OutSlot,InCard))return false;
Confirm();HotkeyBench=false;return true;
}
if(id is not ("player.previous" or "player.next"))return false;
int step=id=="player.previous"?-1:1;
if(HotkeyBench && OutSlot>=0)
{
var candidates=Bench.Where(c=>Game.CanSubstitute(OutSlot,c.Id)).ToArray();
if(candidates.Length==0)return false;
int index=Array.FindIndex(candidates,c=>c.Id==InCard);
InCard=candidates[(index<0?(step>0?0:candidates.Length-1):(index+step+candidates.Length)%candidates.Length)].Id;
}
else
{
var slots=Enumerable.Range(0,11).Where(i=>Game.MatchCard(i)!=null && Game.Match.Players[i].InPlay).ToArray();
if(slots.Length==0)return false;
int index=Array.IndexOf(slots,OutSlot);
OutSlot=slots[index<0?(step>0?0:slots.Length-1):(index+step+slots.Length)%slots.Length];InCard=-1;
}
return true;
}
// The match is paused here. A frame revision would rebuild source text
// continuously and compete with the parent's post-build localization pass.
protected override int BuildHash()=>HashCode.Combine(Game?.Match,Game?.SubstitutionsUsed,Game?.SubstitutionNotice,OutSlot,InCard,Search,Filter,GamePreferences.Revision)^HashCode.Combine(HotkeyBench,GameHotkeys.Revision);
}