MatchTutorialHud.razor

Tutorial HUD panel for the training match. Renders the lesson card, step progress, projected on-pitch markers for the focused player and target, and the substitution/injury choice controls, with English/Russian text.

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

<root class="match-tutorial-hud localized-text">
@if(Game?.MatchTutorialActive==true && Game.MatchTutorialLesson is not null)
{
    @if(!ChoiceOpen && FocusVisible)
    {
        <div class="tutorial-actor-anchor @(FocusBelow?"below":"")" style="@FocusStyle">
            <div class="tutorial-actor-callout" style="@FocusCardStyle">
                <div class="tutorial-actor-action"><div class="tutorial-actor-beacon"></div><label>@FocusAction</label></div>
                <label class="tutorial-actor-detail">@FocusDetail</label>
            </div>
            <div class="tutorial-actor-pointer"></div>
        </div>
    }
    @if(!ChoiceOpen && TargetVisible)
    {
        <div class="tutorial-target @(Game.MatchTutorialSatisfied?"accepted":"")" style="@TargetStyle">
            <div class="tutorial-target-ring"></div>
            <label class="tutorial-target-caption">@TargetCaption</label>
        </div>
    }
    @if(ChoiceOpen)
    {
        <div class="tutorial-modal-tools"><label>@Text("Training match","Учебный матч")</label><button class="tutorial-match-repeat" onclick=@(()=>Game.RetryMatchTutorialStep())>@Text("Repeat","Повторить")</button><button class="tutorial-match-exit" onclick=@(()=>Game.EndMatchTutorial())>@Text("Leave training","Выйти из обучения")</button></div>
    }
    else
    {
    <div class="tutorial-match-card @(Game.MatchTutorialLesson.Key is "attack" or "shoot" or "run" or "stamina" or "links" or "penalty" or "free-kick" or "injury"?"goal-lesson":"") @(Game.MatchTutorialSetPieceLesson?"set-piece-lesson":"") @(Game.MatchTutorialObserving?"observing":"") @(Game.CanAdvanceMatchTutorial?"complete":"")">
        <div class="tutorial-match-head">
            <label class="tutorial-match-kicker">@Text("TRAINING MATCH","ОБУЧАЮЩИЙ МАТЧ")</label>
            <label class="tutorial-match-counter">@($"{Game.MatchTutorialStep+1} / {Game.MatchTutorialStepCount}")</label>
        </div>
        <div class="tutorial-match-progress">
            @for(int i=0;i<Game.MatchTutorialStepCount;i++)
            {
                <div class="@(i==Game.MatchTutorialStep?"current":Game.IsMatchTutorialLessonCompleted(i)?"done":"")"></div>
            }
        </div>
        <label class="tutorial-match-title">@Game.MatchTutorialLesson.Title(GamePreferences.Russian)</label>
        <label class="tutorial-match-body">@Game.MatchTutorialLesson.Body(GamePreferences.Russian)</label>
        @if(!string.IsNullOrWhiteSpace(Game.MatchTutorialLesson.Hint(GamePreferences.Russian)))
        {
            <div class="tutorial-match-instruction"><label>@Game.MatchTutorialLesson.Hint(GamePreferences.Russian)</label></div>
        }
        @if(Game.MatchTutorialSubstitutionLesson && !Game.MatchTutorialSatisfied)
        {
            <button class="tutorial-open-substitutions" onclick=@(()=>Game.OpenSubstitutions())>@Text("Open Substitutions →","Открыть замены →")</button>
        }
        <div class="tutorial-match-status">
            <div class="tutorial-status-dot"></div>
            <div><label class="tutorial-match-phase">@Phase</label><label class="tutorial-match-feedback">@Feedback</label></div>
        </div>
        <div class="tutorial-match-actions">
            <button class="tutorial-match-repeat" onclick=@(()=>Game.RetryMatchTutorialStep())>@Text("Repeat","Повторить")</button>
            <button class="tutorial-match-next @(Game.CanAdvanceMatchTutorial?"ready":"disabled")" disabled="@(!Game.CanAdvanceMatchTutorial)" onclick=@(()=>Game.AdvanceMatchTutorial())>@(LastStep?Text("Finish ✓","Завершить ✓"):Text("Next →","Далее →"))</button>
        </div>
        <button class="tutorial-match-exit" onclick=@(()=>Game.EndMatchTutorial())>@Text("Skip training","Пропустить обучение")</button>
        <button class="tutorial-chapters-toggle" onclick=@(()=>ChaptersOpen=!ChaptersOpen)>@Text(ChaptersOpen?"Hide topics ↑":"Choose a topic ↓",ChaptersOpen?"Скрыть темы ↑":"Выбрать тему ↓")</button>
        @if(ChaptersOpen)
        {
            <div class="tutorial-chapters">
            @foreach(var chapter in MatchTutorialLesson.Chapters)
            {
                var choice=chapter;
                <button class="tutorial-chapter [email protected]" onclick=@(()=>{ChaptersOpen=false;Game.JumpToMatchTutorialChapter(choice.Key);})>@chapter.Title(GamePreferences.Russian)</button>
            }
            </div>
        }
    </div>
    }
}
</root>

@code {
    public MatchController Game {get;set;}
    public FootballWorldPanel World {get;set;}
    bool ChaptersOpen;
    bool ChoiceOpen=>Game.SubstitutionOpen || Game.InjuryChoiceOpen;
    public override void Tick(){base.Tick();if(Game?.MatchTutorialActive==true)Game.TickMatchTutorial();}
    bool LastStep=>Game.MatchTutorialStep>=Game.MatchTutorialStepCount-1;
    static string Text(string en,string ru)=>GamePreferences.Russian?ru:en;
    string Phase=>Game.MatchTutorialInjuryLesson && Game.Match.MedicalActive && !Game.InjuryChoiceOpen?Text("Medical staff on the pitch","Медики на поле"):
        Game.CanAdvanceMatchTutorial?Game.MatchTutorialLesson.IsTask?Text("Lesson complete","Шаг выполнен"):Text("Ready to continue","Можно продолжить"):
        Game.MatchTutorialObserving?Game.MatchTutorialLesson.IsTask?Text("Order accepted","Команда принята"):Text("Watch the moment","Наблюдение"):
        Game.MatchTutorialLesson.IsTask?Text("Your turn","Ваше действие"):Text("Look around","Знакомство");
    string Feedback=>!string.IsNullOrWhiteSpace(Game.MatchTutorialFeedback)?Game.MatchTutorialFeedback:
        Game.MatchTutorialInjuryLesson && Game.Match.MedicalActive?Text("The compulsory replacement screen opens after the player is carried off.","Окно обязательной замены откроется, когда игрока унесут с поля."):
        Game.MatchTutorialObserving?Text("Watch the players respond on the pitch.","Посмотрите, как игроки выполняют команду."):
        Game.CanAdvanceMatchTutorial?Text("You can move on or repeat this moment.","Можно продолжить или повторить эпизод."):
        Game.MatchTutorialLesson.IsTask?Text("Follow the instruction above.","Выполните действие из подсказки выше."):
        Text("Press Next when you are ready.","Нажмите «Далее», когда будете готовы.");
    Point? Target
    {
        get
        {
            int player=Game.MatchTutorialTargetPlayer;
            if(player>=0 && player<Game.Match.Players.Count && !Game.Match.Players[player].SentOff)
                return Game.Match.Players[player].Position;
            return Game.MatchTutorialTarget;
        }
    }
    int FocusPlayer=>Game.MatchTutorialFocusPlayer;
    bool FocusPending=>Game.MatchTutorialNeedsAction && FocusPlayer>=0 && FocusPlayer<Game.Match.Players.Count &&
        !Game.Match.Players[FocusPlayer].SentOff && World?.CallInput?.Dragging!=true && !Game.CommandWheelOpen;
    Vector2 FocusScreen=>FocusPending && World is not null?World.Project(Game.Match.Players[FocusPlayer].Position,2.65f):new Vector2(-10,-10);
    float HudWidth=>Math.Max(1,Box.Rect.Width*ScaleFromScreen);
    float HudHeight=>Math.Max(1,Box.Rect.Height*ScaleFromScreen);
    bool FocusVisible=>FocusPending && FocusScreen.x>.02f && FocusScreen.x<.98f && FocusScreen.y>.02f && FocusScreen.y<.88f;
    bool FocusBelow=>FocusScreen.y*HudHeight<94;
    string FocusStyle=>FormattableString.Invariant($"left:{FocusScreen.x*100:0.###}%;top:{FocusScreen.y*100:0.###}%;");
    string FocusCardStyle=>FormattableString.Invariant($"left:{Math.Clamp(FocusScreen.x*HudWidth-136,12,Math.Max(12,HudWidth-284))-FocusScreen.x*HudWidth:0.##}px;");
    string FocusAction=>Game.MatchTutorialLesson.Key switch
    {"run" or "zone"=>Text("HOLD LEFT CLICK","ЗАЖМИТЕ ЛКМ"),
        "pass" or "clear"=>Text("RIGHT CLICK","НАЖМИТЕ ПКМ"),
        "press" when FocusPlayer>=11=>Text("RIGHT CLICK","НАЖМИТЕ ПКМ"),
        _=>Text("LEFT CLICK","НАЖМИТЕ ЛКМ")};
    string FocusDetail=>Game.MatchTutorialLesson.Key switch
    {"run" or "zone"=>Text($"Drag #{Game.Match.Players[FocusPlayer].Number} to TARGET",$"Потяните №{Game.Match.Players[FocusPlayer].Number} к метке «Цель»"),
        "pass"=>Text("Then choose Pass here","Затем выберите «Пас сюда»"),
        "clear"=>Text("Then choose Clear order","Затем выберите «Снять приказ»"),
        "press" when FocusPlayer>=11=>Text("Then choose Press","Затем выберите «Прессинг»"),
        "mark" when FocusPlayer>=11=>Text($"Mark opposing #{Game.Match.Players[FocusPlayer].Number}",$"Опекайте соперника №{Game.Match.Players[FocusPlayer].Number}"),
        _=>Text($"Select your #{Game.Match.Players[FocusPlayer].Number}",$"Выберите своего №{Game.Match.Players[FocusPlayer].Number}")};
    Vector2 TargetScreen=>Target is Point p && World is not null?World.Project(p,.06f):new Vector2(-10,-10);
    bool TargetVisible=>!(FocusPending && Game.MatchTutorialTargetPlayer==FocusPlayer) && TargetScreen.x>.025f && TargetScreen.x<.975f && TargetScreen.y>.05f && TargetScreen.y<.93f;
    string TargetStyle=>FormattableString.Invariant($"left:{TargetScreen.x*100:0.###}%;top:{TargetScreen.y*100:0.###}%;");
    string TargetCaption=>Game.MatchTutorialTargetPlayer>=0 && Game.MatchTutorialTargetPlayer<Game.Match.Players.Count?
        $"#{Game.Match.Players[Game.MatchTutorialTargetPlayer].Number}":Text("TARGET","ЦЕЛЬ");
    protected override int BuildHash()=>Game?.MatchTutorialActive!=true?0:
        HashCode.Combine(HashCode.Combine(Game.Revision,Game.MatchTutorialStep,Game.MatchTutorialSatisfied,Game.MatchTutorialObserving,Game.CanAdvanceMatchTutorial,Game.MatchTutorialFeedback,GamePreferences.Revision),
            FocusPlayer,FocusPending,FocusScreen,HudWidth,HudHeight)^HashCode.Combine(ChaptersOpen,ChoiceOpen);
}