TrainingHud.razor
@using Sandbox

@using Sandbox.UI

@using PaintballBaddies

@inherits PanelComponent

@namespace PaintballBaddies



<root>
    @if (LoadingArena)
    {
        <div class="arena-loading">
            <label class="loading-title">BADDIES VS SAUSAGES</label>
            <label class="loading-status">@LoadingMessage</label>
            <button onclick=@(()=>Game.Close())>QUIT TO S&amp;BOX</button>
        </div>
    }
    @if (Controls?.Open == true && Controls.Capturing is not null)
    { <BindingCapturePanel Controls=@Controls /> }

    @if (Controls?.Open == true)
    {
        <div class="paint-menu @(MenuPage=="home" ? "home" : "inside")">
            <div class="menu-art"></div><div class="menu-shade"></div>
            <div class="paint-fleck teal"></div><div class="paint-fleck pink"></div>
            @if(MenuPage=="home")
            {
                
                <div class="home-actions @(Session?.InRound==true ? "in-match" : "")">
                    @if(Session?.InRound==true){<button class="paint-button primary" onclick=@(()=>Controls.ResumeGame())>RESUME</button><button class="paint-button" onclick=@(()=>MenuPage="standings")>STANDINGS</button><button class="paint-button" onclick=@(()=>MenuPage="arenas")>CHANGE ARENA</button>}
                    <button class="paint-button @(Session?.InRound!=true ? "primary" : "")" onclick=@(()=>ChooseMode(ArenaGameMode.Overdrive))>PLAY</button>
                    <button class="paint-button" onclick=@(()=>MenuPage="settings")>SETTINGS</button>
                    <button class="paint-button" onclick=@(()=>MenuPage="how")>HOW TO PLAY</button>
                    <button class="paint-button quiet" onclick=@(()=>Controls.QuitGame())>QUIT</button>
                </div>
                <div class="home-footer"><label>SOLOVOLT GAMES · BADDIES VS SAUSAGES</label><div><button onclick=@OpenFeedback>GIVE FEEDBACK</button><button onclick=@(()=>MenuPage="credits")>CREDITS</button></div></div>
            }
            else
            {
                <div class="paint-sheet">
                    <div class="paint-heading"><button class="back-button" onclick=@BackMenu>BACK</button><label>@PageTitle</label></div>
                    <div class="paint-content">
                    @if(MenuPage=="arenas")
                    {
                        <label class="paint-caption">@MultiplayerSession.ModeTitle(Session?.Mode ?? ArenaGameMode.PaintRush) · CHOOSE YOUR ARENA</label>
                        <div class="choice-row restaurant-choices">
                            <button class="choice-card marine" onclick=@(()=>ChooseArena(ArenaSelection.Restaurant))><label class="card-kicker">THE DINER</label><label class="card-title">MISSING LINK</label><label>Great food. Hostile service.</label><label class="card-foot">@ArenaChoiceLabel(ArenaSelection.Restaurant)</label></button>
                            <div class="choice-card foundry"><label class="card-kicker">COMING SOON</label><label class="card-title">THE GRIND HOUSE</label><label>A sausage factory. They're making more.</label><label class="card-foot">NEXT LEVEL</label></div>
                        </div>
                        <label class="paint-note">The Missing Link Diner: adapted from Los Carne Hermanos by EthanTheDoodler.</label>
                    }
                    else if(MenuPage=="matches")
                    {
                        <label class="paint-caption">@($"{MultiplayerSession.ModeTitle(Session?.Mode ?? ArenaGameMode.PaintRush)} / {ArenaMap.DisplayTitle(Scene).ToUpperInvariant()}")</label>
                        @if(Session?.Mode!=ArenaGameMode.Overdrive){
                        <label class="paint-note">@(Session?.InRound==true ? "LENGTH OF NEXT ROUND" : "MATCH LENGTH")</label>
                        <div class="duration-row">
                            @if(CanConfigure){@foreach(var minutes in ArenaMatch.RoundMinuteOptions)
                            {<button class="@(Match?.RoundDuration==minutes*60 ? "selected" : "")" onclick=@(()=>{if(CanConfigure)Match?.SetRoundMinutes(minutes);})>@minutes MIN</button>}
                            }else{<label class="paint-note">@($"{Session?.Minutes ?? 3} MIN · THE HOST SETS THE MATCH LENGTH")</label>}
                        </div>
                        }else{<label class="paint-note">3 WAVES · EARN SPECIALS WITH HEADSHOTS · CLEAR THE DINER</label>}
                        @if(Session?.QuickPlaying==true || Networking.IsConnecting)
                        {<label class="paint-caption">@(Networking.IsConnecting ? "CONNECTING..." : "FINDING YOUR MATCH...")</label><button onclick=@(()=>Session?.CancelMenuConnection())>CANCEL</button>}
                        else if(Session?.CanQuickPlay==true)
                        {
                            <button class="paint-button primary launch" onclick=@Play>@(Session?.Mode==ArenaGameMode.Overdrive ? "START THE FIGHT" : "PLAY ONLINE")</button>
                            <label class="paint-note">Join or host a public match. AI fills empty places; people can join anytime.</label>
                            <div class="small-actions"><button onclick=@(()=>MenuPage="browse")>FIND A MATCH</button><button onclick=@PrivateMatch>FRIENDS ONLY</button></div>
                        }
                        else
                        {
                            @if(Networking.IsHost){<button class="paint-button primary launch" onclick=@(()=>Controls.StartMatch())>@(Session?.InRound==true ? "RESTART ROUND" : "START NEXT ROUND")</button>}
                            else{<label class="paint-note">Waiting for the host to start the next round.</label>}
                            <button class="paint-button" onclick=@(()=>Controls.ResumeGame())>BACK TO GAME</button>
                            
                        }
                        <label class="paint-note">@(MultiplayerSession.Online ? ConnectionHint : "6 SEATS · HUMANS + AI")</label>
                    }
                    else if(MenuPage=="browse")
                    {
                        <button onclick=@BrowseGames>@(Session?.Searching==true ? "SEARCHING..." : "REFRESH MATCHES")</button>
                        @if(Session is not null)
                        {
                            @foreach(var lobby in Session.Lobbies)
                            {<button onclick=@(()=>{Session.JoinLobby(lobby);MenuPage="matches";})>@MultiplayerSession.LobbyLabel(lobby)</button>}
                            <TextEntry @ref=JoinEntry class="player-name" Value:[email protected] Placeholder="Host Steam ID or lobby ID" />
                            <button onclick=@(()=>{Session.JoinTyped();if(Networking.IsConnecting)MenuPage="matches";})>JOIN BY ID</button>
                        }
                    }
                    else if(MenuPage=="settings")
                    {
                        <div class="settings-grid">
                            <button class="paint-button" onclick=@(()=>MenuPage="controls")>CONTROLS</button><button class="paint-button" onclick=@(()=>MenuPage="audio")>AUDIO + VOICE</button>
                            <button class="paint-button" onclick=@(()=>MenuPage="display")>DISPLAY</button><button class="paint-button" onclick=@(()=>MenuPage="profile")>YOUR NAME</button>
                        </div>
                    }
                    else if(MenuPage=="controls")
                    {
                        <div class="small-actions"><button class="@(Controls.UsesMovementLayout(false) ? "selected" : "")" onclick=@(()=>Controls.SetMovementLayout(false))>WASD MOVEMENT</button><button class="@(Controls.UsesMovementLayout(true) ? "selected" : "")" onclick=@(()=>Controls.SetMovementLayout(true))>ARROW KEY MOVEMENT</button></div>
                        <button onclick=@(()=>AutoRun?.ToggleAutomatic())>AUTO-RUN: @(AutoRun?.Automatic==true ? "ON" : "OFF")</button>
                        <label class="paint-note">Click a binding, then press a key. Hold forward 1.25s to run.</label>
                        <div class="how-row"><label class="how-key">MOUSE WHEEL</label><label>Scroll up or down to switch between dual pistols and explosive crossbow.</label></div>
                        <label class="paint-note">Or press @Key("slot1") for dual pistols / @Key("slot2") for explosive crossbow. These keys can be changed below. ESC / M opens this menu.</label>
                        <div class="binding-list">
                            @foreach(var action in Controls.Bindings.Keys.Where(key=>(!key.StartsWith("slot") || key is "slot1" or "slot2") && key!="use" && (Session?.Mode!=ArenaGameMode.Overdrive || key is not ("reload" or "roundlength"))))
                            {<div class="binding-row"><label>@Controls.Titles[action]</label><button onclick=@(()=>Controls.Capture(action))>@(Controls.Capturing==action ? "PRESS A KEY..." : Controls.Label(action))</button></div>}
                        </div>
                        <button onclick=@(()=>Controls.ResetBindings())>RESTORE DEFAULT BINDINGS</button>
                    }
                    else if(MenuPage=="audio")
                    {
                        <button onclick=@(()=>PlayerVoiceChat.Toggle())>VOICE CHAT: @(PlayerVoiceChat.ChatEnabled ? "ON" : "OFF")</button>
                        <button onclick=@(()=>PlayerVoiceChat.ToggleMode())>MIC: @(PlayerVoiceChat.HoldToTalk ? "HOLD TO TALK" : "TAP ON / TAP OFF")</button>
                        <label class="paint-note">@(PlayerVoiceChat.HoldToTalk ? $"Hold {Key("voice")} to talk. Release to mute." : $"Tap {Key("voice")} to talk. Tap again to mute.") Everyone in your match can hear you. Opening this menu mutes your mic.</label>
                        <button onclick=@(()=>Game.Overlay.ShowSettingsModal("audio"))>VOLUME + MICROPHONE SETTINGS</button>
                    }
                    else if(MenuPage=="display")
                    {
                        <button onclick=@(()=>Controls.ToggleGameplayText())>MATCH ANNOUNCEMENTS: @(Controls.ShowGameplayText ? "ON" : "OFF")</button>
                        <label class="paint-note">Controls stay in the menu. @Key("togglescoreboard") opens / closes standings during play. The timer, aiming, danger and recovery cues stay visible.</label>
                        <button onclick=@(()=>Game.Overlay.ShowSettingsModal("graphics"))>GRAPHICS SETTINGS</button>
                    }
                    else if(MenuPage=="standings")
                    {
                        <label class="paint-caption">@(Session?.IsCooperative==true ? "TEAM CONTRIBUTIONS" : "MATCH STANDINGS")</label>
                        <MatchStandings Match=@Match Cooperative=@(Session?.IsCooperative==true) />
                    }
                    else if(MenuPage=="profile")
                    {
                        <label class="paint-caption">WHAT SHOULD WE CALL YOU?</label>
                        <TextEntry @ref=NameEntry class="player-name" Value:[email protected] [email protected] Placeholder="Your player name" />
                        <label class="paint-note">Your name appears in the scoreboard. Your character is randomly assigned when you join.</label>
                    }
                    else if(MenuPage=="how")
                    {
                        <div class="how-row"><label class="how-key">@Key("forward") @Key("left") @Key("backward") @Key("right")</label><label>Move. Keep holding forward to run.</label></div>
                        <div class="how-row"><label class="how-key">@Key("attack1")</label><label>Shoot. In cover, peek and shoot automatically.</label></div>
                        <div class="how-row"><label class="how-key">@Key("cover")</label><label>Take / leave cover. In open space, crouch / stand.</label></div>
                        <div class="how-row"><label class="how-key">@Key("attack2")</label><label>Hold to focus. When downed, tap 5 times to get up.</label></div>
                        <div class="how-row"><label class="how-key">@Key("vault")</label><label>Vault low cover.</label></div>
                        <div class="how-row"><label class="how-key">ESC / M</label><label>Menu, settings and quit.</label></div>
                        <div class="how-row"><label class="how-key">@Key("togglescoreboard")</label><label>Open / close standings. Also available from this menu and after each round.</label></div>
                        <label class="paint-note">SAUSAGE SURVIVAL: clear three waves to win. Every five personal headshot kills earns a special: explosive grenades alternate with flash-freeze grenades. Press @Key("grenade") to use the queued special shown on screen. Crossbow bolts beep for 2 seconds, then explode. Hold primary fire too long and it rests for 2.5 seconds; tap for controlled shots.</label>
                        <label class="paint-note">Shoot incoming plates, knives and eggs. Nearby incoming plates guide your aim: click to shatter them. Cover and shields block frontal attacks; friendly fire is off.</label>
                        <label class="paint-note">@Key("slot1") dual pistols · @Key("slot2") explosive crossbow · mouse wheel cycles weapons. Two head hits finish a sausage; the first leaves it wounded and slower. Bolts count when they explode. Freeze enemies, then shoot them to shatter the ice.</label>
                    }
                    else if(MenuPage=="credits")
                    {
                        <label class="paint-caption">SOLOVOLT GAMES</label>
                        <label class="paint-note">A solo creator building with AI and community feedback.</label>
                        <label class="paint-note">s&amp;box, Citizen rig, animations and library assets: Facepunch. Marine Base: Three Thieves / Tov. Restaurant: Los Carne Hermanos by EthanTheDoodler; shelves by caro; refrigerator by Tones; plates by Ambi (CC0). Used through the s&amp;box workshop.</label>
                        <label class="paint-note">Audio: CiciFyre, creator-provided ElevenLabs recordings, Joseph Sardin / BigSoundBank, Arq1998 and Kenney. Materials: ambientCG. Animation fallback: Quaternius.</label>
                        <label class="paint-note">Generative AI art and audio are used. Character models: Hi3D, adapted and rigged for this game; earlier assets: Meshy. Title artwork: OpenAI. Permanent Marker font: Font Diner, Apache 2.0.</label>
                        <label class="paint-note">Original game code is MIT licensed. Third-party assets keep their own terms. Full credits and modifications are in ASSET_CREDITS.md.</label>
                        <button onclick=@OpenFeedback>FEEDBACK + COMMUNITY</button>
                    }
                    @if(!string.IsNullOrEmpty(MenuNotice)){<label class="paint-note notice">@MenuNotice</label>}
                    @if(MenuPage=="controls" && !string.IsNullOrEmpty(Controls.Notice)){<label class="paint-note notice">@Controls.Notice</label>}
                    @if(!string.IsNullOrEmpty(Session?.Notice) && (MenuPage=="matches" || MenuPage=="browse")){<label class="paint-note notice">@Session.Notice</label>}
                    </div>
                </div>
            }
        </div>
    }
    @if(Controls?.Open==true && ShowFeedback)
    {
        <div class="feedback-overlay"><div class="feedback-window">
            <div class="feedback-header"><label>FEEDBACK / BUG REPORTS</label><button onclick=@(()=>ShowFeedback=false)>BACK TO GAME MENU</button></div>
            <label class="menu-help">Tell us what happened, whether you hosted or joined, and how to repeat it.</label>
            <WebPanel class="feedback-browser" Url=@FeedbackUrl />
        </div></div>
    }

    @if (GameplayVisible)
    {
        <div class="match-strip @(Session?.IsPopRiot==true ? "cooperative" : "")">
            <label class="compact-clock @(ClockSeconds<=10 ? "urgent" : ClockSeconds<=30 ? "warning" : "")">@Clock</label>
            @if(Session?.IsPopRiot!=true){<label class="compact-score">@(Match.PlayerState?.Points ?? 0) PTS</label>}
        </div>
        @if (IntroRemaining>0 && ShowGameplayText)
        {<div class="round-intro @(IntroRemaining<.65f ? "fading" : "")"><label>@(Session?.IsOverdrive==true ? "CLEAR THREE WAVES · KEEP YOUR SQUAD STANDING" : Session?.IsPopRiot==true ? "SURVIVE TOGETHER · PROTECT TEAM LIVES" : "LAND PAINT · AVOID PAINT · MOST POINTS WINS")</label></div>}
        @if (!string.IsNullOrEmpty(ShieldStatus))
        {<div class="shield-status"><label>@ShieldStatus</label></div>}
        @if (Contact?.FeedbackRemaining>0 && ShowGameplayText)
        {<div class="combat-feedback"><label>@Contact.FeedbackText</label></div>}
    }

    @if (ShowIncomingDirection)
    {
        <div class="incoming-direction" style="@IncomingDirectionStyle"><div class="incoming-arc"></div></div>
    }

    @if (MultiplayerSession.Online && PlayerVoiceChat.ChatEnabled && !string.IsNullOrEmpty(SpeakingNames))
    {
        <div class="voice-indicator"><label>@(LocalVoice?.Talking==true ? PlayerVoiceChat.HoldToTalk ? "MIC ON / RELEASE TO MUTE" : $"MIC ON / {Key("voice")} TO MUTE" : "VOICE") · @SpeakingNames</label></div>
    }

    @if (ShowRecoveryPrompt)
    {
        <div class="recovery-prompt"><div class="recovery-card">
            <label class="recovery-title">@(Contact?.Recovering==true ? "GETTING UP" : "KNOCKED DOWN")</label>
            <label class="recovery-instruction">@RecoveryMessage</label>
            <div class="recovery-progress">
                @for(var i=0;i<CloseCombat.RequiredRecoveryPresses;i++)
                {
                    <div class="recovery-pip @(i<RecoveryProgress ? "filled" : "")"></div>
                }
                <label>@RecoveryProgress / @CloseCombat.RequiredRecoveryPresses</label>
            </div>
        </div></div>
    }

    @if (GameplayVisible && Controls?.ShowScoreboard==true)
    {
        <div class="standings-overlay"><div class="standings-card">
            <div class="standings-heading"><label>@(Session?.IsCooperative==true ? "TEAM CONTRIBUTIONS" : "STANDINGS")</label><label class="standings-clock">@Clock</label></div>
            <MatchStandings Match=@Match Cooperative=@(Session?.IsCooperative==true) />
            <label class="standings-dismiss">@Key("togglescoreboard") TO CLOSE</label>
        </div></div>
    }

    @if (Controls?.Open!=true && Match is not null && !Match.InRound && Match.Status == "TIME UP")

    {

        <div class="paint-menu inside results-menu"><div class="menu-art"></div><div class="menu-shade"></div><div class="paint-sheet"><div class="paint-heading"><label>ROUND RESULTS</label></div><div class="paint-content result-content">

            <label class="result-reason">@(Session?.IsOverdrive==true ? Session.OverdriveResult : Session?.IsPopRiot==true ? Session.RiotResult : "TIME LIMIT REACHED")</label>
            @if(Session?.IsCooperative==true){<label class="paint-caption">TEAM SCORE: @Session.TeamScore</label>}
            @if(Session?.IsOverdrive==true){<label class="result-reason">WAVE @Session.OverdriveWave / 3</label>}
            @if(Session?.IsPopRiot==true){<label class="team-best">@TeamBestMessage</label>}

            <label class="result-progress">@(Session?.IsCooperative==true ? $"YOUR CONTRIBUTION: {Match.PlayerState?.PopScore ?? 0} PTS" : $"YOUR RANK: {PlayerRank} / 6   |   {Match.PlayerState?.Points ?? 0} PTS")</label>
            <MatchStandings Match=@Match Cooperative=@(Session?.IsCooperative==true) />
            <label class="personal-best @(Highlights.NewBest ? "new-best" : "")">@PersonalBestMessage</label>
            <div class="round-awards">
                @foreach(var award in RoundHighlights.Awards(Match.Standings))
                {
                    <div class="award-card"><label class="award-title">@award.Title</label><label class="award-winners">@award.Winners</label><label class="award-detail">@award.Detail</label></div>
                }
            </div>

            <label class="result-reason">@(Session?.IsCooperative==true ? "ONE TEAM · SHARED SCORE · FRIENDLY FIRE OFF" : "POINTS = HITS LANDED - HITS RECEIVED / KNOCKDOWNS +2 / FALLS -2")</label>

            <label class="result-reason">@(Session?.IsOverdrive==true ? "CLEAR THREE WAVES · EARN SPECIALS WITH HEADSHOT KILLS" : Session?.IsPopRiot==true ? "CLEAR A WAVE TO RESTORE A LIFE · ACCURATE HITS BUILD THE TEAM MULTIPLIER" : "FEWER HITS RECEIVED BREAKS TIES / EXACT TIES SHARE A RANK")</label>

            @if(Session?.IsOverdrive!=true)
            {
                <div class="result-stats"><label>@(Weapon?.Shots ?? 0) SHOTS</label><label>@Accuracy ACCURACY</label></div>
            }
            else
            {
                <label class="result-reason">HEADSHOTS = HEADSHOT KILLS / DOWNED = TIMES KNOCKED DOWN</label>
            }

            <button class="result-feedback" onclick=@OpenFeedback>FEEDBACK / REPORT A BUG</button>



        </div>
            <div class="result-actions">
                @if (MultiplayerSession.Online && !Networking.IsHost)
                {
                    <div class="result-action primary unavailable"><label>WAITING FOR HOST</label></div>
                }
                else
                {
                    <button class="result-action primary" onclick=@(()=>Controls?.StartMatch())>ONE MORE ROUND</button>
                }
                <button class="result-action" onclick=@OpenResultMenu>MATCH OPTIONS</button>
                <button class="result-action" onclick=@(()=>Controls.QuitGame())>QUIT TO S&amp;BOX</button>
            </div>

        </div></div>

    }

    @if (Controls?.Open!=true && (Match is null || Match.InRound || (Match.Status == "TRAINING" || Match.Status == "LOBBY")))

    {

        <div class="crosshair @(Weapon?.HitFlash > 0 ? "hit" : "") @(Weapon?.EliminationFlash > 0 ? "kill" : "") @(MeleeHint ? "melee-ready" : "")"><div class="reticle-horizontal"></div><div class="reticle-vertical"></div>
            @if(Weapon?.EliminationFlash > 0){<div class="kill-tick tl"></div><div class="kill-tick tr"></div><div class="kill-tick bl"></div><div class="kill-tick br"></div>}
            @if(MeleeHint){<div class="melee-bracket left"></div><div class="melee-bracket right"></div>}
        </div>

    }

</root>



@code {

    PaintballControls Controls;

    TextEntry NameEntry;

    TextEntry JoinEntry;

    bool ShowJoin,ShowFeedback;
    string MenuPage="home";
    public string MenuLocation=>MenuPage;
    string PlayLabel=>Session?.CanQuickPlay!=false ? "PLAY" : Session?.Status=="TIME UP" ? "VIEW RESULTS" : "RESUME";
    void Play()
    {
        if(!string.IsNullOrEmpty(Session?.SpawnReadinessMessage)){MenuNotice=Session.SpawnReadinessMessage;return;}
        if(Session?.CanQuickPlay==true)Session.QuickPlay();else Controls?.ResumeGame();
    }
    string MenuNotice="";
    bool CanConfigure=>!MultiplayerSession.Online || Networking.IsHost;
    string PageTitle=>MenuPage switch { "modes"=>"PLAY", "arenas"=>"PICK A PLAYGROUND", "matches"=>"LET'S PLAY", "browse"=>"FIND A MATCH", "settings"=>"MAKE IT YOURS", "controls"=>"CONTROLS", "audio"=>"AUDIO + VOICE", "display"=>"YOUR VIEW", "profile"=>"YOUR NAME", "how"=>"HOW TO PLAY", "standings"=>"STANDINGS", _=>"CREDITS" };
    void BackMenu()
    {
        if(Session?.Searching==true || Networking.IsConnecting)Session?.CancelMenuConnection();
        if(MenuPage=="profile")Controls?.SavePlayerName();
        MenuNotice="";ShowJoin=false;
        MenuPage=MenuPage switch { "arenas"=>"home", "matches"=>"home", "browse"=>"matches", "controls" or "audio" or "display" or "profile"=>"settings", _=>"home" };
    }
    void ChooseMode(ArenaGameMode mode)
    {
        if(CanConfigure)Session?.SelectMode(mode);
        MenuNotice="";MenuPage="arenas";
    }
    string ArenaChoiceLabel(string path)=>ArenaSelection.Current(Scene)==path ? "CURRENT ARENA" : "SELECT ARENA";
    void ChooseArena(string path)
    {
        MenuNotice="";
        if(path==ArenaSelection.Current(Scene)){MenuPage="matches";return;}
        if(!CanConfigure){MenuNotice="The host chooses the arena.";return;}
        if(!ArenaSelection.Change(Scene,path))MenuNotice="The arena could not be loaded. Try again.";
    }
    void PrivateMatch()
    {
        if(!string.IsNullOrEmpty(Session?.SpawnReadinessMessage)){MenuNotice=Session.SpawnReadinessMessage;return;}
        if(!MultiplayerSession.Online)Session?.Host(false);
        else Controls?.ResumeGame();
    }
    public const string FeedbackUrl="https://sbox.game/solovolt/paintball-baddies/forum/3434/1/";
    readonly RoundHighlights Highlights=new();
    readonly RoundHighlights TeamHighlights=new("paintball-team-bests-v1");
    string TeamRecordKey=>RoundHighlights.RecordKey(ArenaMap.DisplayTitle(Scene),true,Match?.PlayedMinutes ?? 3)+"|pop_riot_team";
    string TeamBestMessage=>TeamHighlights.NewBest ? "NEW TEAM BEST! HOW FAR CAN YOU PUSH IT?" : TeamHighlights.Best is int best
        ? $"YOUR BEST TEAM RUN: {best} · {System.Math.Max(1,best-(Session?.TeamScore ?? 0)+1)} MORE POINTS TO BEAT IT" : "SET YOUR FIRST TEAM RECORD";
    public Dictionary<string,int> PersonalRecords=>Highlights.Snapshot();
    public void OpenFeedback(){if(Controls?.Open!=true)Controls?.Toggle();ShowFeedback=true;}
    void BrowseGames(){ShowJoin=true;if(Session?.Searching!=true)Session?.RefreshLobbies();}
    string RecordKey(int minutes,bool selected=false)=>RoundHighlights.RecordKey(ArenaMap.DisplayTitle(Scene),true,minutes)+((selected ? Session?.Mode==ArenaGameMode.Overdrive : Session?.IsOverdrive==true) ? "|overdrive" : (selected ? Session?.Mode==ArenaGameMode.PopRiot : Session?.IsPopRiot==true) ? "|pop_riot" : "");
    string MenuPersonalBest => Highlights.Read(RecordKey((int)(Match?.RoundDuration ?? 180)/60,true)) is int best
        ? $"YOUR BEST: {best} PTS / {(int)(Match?.RoundDuration ?? 180)/60} MIN / MULTIPLAYER" : "YOUR BEST: FINISH A ROUND TO SET A SCORE";
    string QuickControls=>$"{Key("forward")}/{Key("left")}/{Key("backward")}/{Key("right")} MOVE / HOLD FORWARD TO RUN / {Key("attack1")} FIRE / {Key("cover")} COVER / CROUCH / ESC or M MENU";
    string PersonalBestMessage=>Highlights.Best is int best ? $"{(Highlights.NewBest ? "NEW PERSONAL BEST" : "PERSONAL BEST")}: {best} PTS / {Match?.PlayedMinutes} MIN / MULTIPLAYER" : "PERSONAL BEST: COMPLETE A ROUND TO SET YOUR SCORE";
    bool ShowGameplayText => Controls?.ShowGameplayText != false;
    public bool GameplayVisible => !LoadingArena && Controls?.Open!=true && Match?.InRound==true;
    public float IntroRemaining {get;private set;}
    private int introRound=-1;
    PlayerVoiceChat LocalVoice => MultiplayerSession.LocalPlayer(Scene)?.Components.Get<PlayerVoiceChat>();
    string SpeakingNames => string.Join(", ",Scene.GetAllComponents<PlayerVoiceChat>()
        .Where(x=>x.Talking).Select(x=>x.IsProxy ? x.Components.Get<PlayerProfile>()?.DisplayName ?? "Player" : "YOU").Take(6));

    PaintballCombatant IncomingState=>MultiplayerSession.LocalPlayer(Scene)?.Components.Get<PaintballCombatant>();
    bool ShowIncomingDirection=>Controls?.Open!=true && IncomingState is {} state && state.DirectionIndicatorRemaining>0
        && state.LastHitDirection.Length>.01f && Match?.Status!="TIME UP";
    string IncomingDirectionStyle=>!ShowIncomingDirection ? "" : System.FormattableString.Invariant(
        $"transform: rotate({IncomingState.HitDirectionAngle(Scene.Camera?.WorldRotation ?? Rotation.Identity):0.0}deg); opacity: {System.Math.Clamp(IncomingState.DirectionIndicatorRemaining/.85f,0,1):0.00};");

    string ConnectionHint=>$"{(Networking.IsHost ? "YOU ARE HOSTING" : "CONNECTED")} / {(Session?.PublicLobby==true ? "PUBLIC" : "FRIENDS ONLY")} / {Session?.Players.Count() ?? 0} HUMAN / {Session?.Bots.Count() ?? 0} AI / 6 TOTAL SEATS";

    string AutoRunLabel=>$"AUTO-RUN: {(AutoRun?.Automatic==true ? "ON" : "OFF")} / HOLD FORWARD 1.25s";

    MultiplayerSession Session=>MultiplayerSession.Find(Scene);

    public bool LoadingArena {get;private set;}=true;
    public string LoadingMessage {get;private set;}="SETTING THE TABLE…";
    private bool arenaRevealed;
    private float loadingPoll,readyFrames;
    private void UpdateArenaLoading()
    {
        // MapInstance and the local restaurant dressing load independently on
        // a joining peer. Never expose the pawn against the empty scene.
        if(arenaRevealed)return;
        loadingPoll-=RealTime.Delta;
        if(loadingPoll>0)return;
        loadingPoll=.1f;
        string message="";
        var venue=Scene.GetAllComponents<RestaurantArena>().FirstOrDefault();
        if(Scene.GetAllComponents<MapInstance>().Any(m=>m.Active && !m.IsLoaded))message="LOADING THE ARENA…";
        else if(venue.IsValid() && !string.IsNullOrEmpty(venue.Error))message=venue.Error;
        else if(venue.IsValid() && (!venue.Ready || !venue.SourcePrepared))message="SETTING THE TABLE…";
        else if(venue?.EnableServiceExpansion==true && Scene.GetAllComponents<RestaurantServiceExpansion>().FirstOrDefault()?.Ready!=true)
            message=Scene.GetAllComponents<RestaurantServiceExpansion>().FirstOrDefault()?.Error is {Length:>0} error ? error : "PREPARING THE KITCHEN AND DOCK…";
        else if(!MultiplayerSession.LocalPlayer(Scene).IsValid())message="JOINING THE SQUAD…";
        else if(MultiplayerSession.LocalPlayer(Scene).Components.Get<NetworkPawn>() is {} pawn && !pawn.LocalWorldReady)message="PREPARING YOUR SPAWN…";
        else if(MultiplayerSession.LocalPlayer(Scene).Components.Get<CitizenPlayerPresentation>()?.IsReady!=true)message="GETTING YOUR BADDIE READY…";
        if(message.Length>0){readyFrames=0;LoadingMessage=message;return;}
        // Let the character and camera settle for a few rendered frames.
        readyFrames+=loadingPoll;
        if(readyFrames>=.3f){arenaRevealed=true;LoadingArena=false;LoadingMessage="";}
    }

    protected override void OnUpdate()
    {
        UpdateArenaLoading();
        if(Controls.IsValid())Controls.EditingName=NameEntry?.HasFocus==true || (MenuPage=="browse" && JoinEntry?.HasFocus==true) || ShowFeedback;
        if(Controls?.Open!=true){ShowFeedback=false;MenuPage="home";ShowJoin=false;}
        var match=Match;
        if(match?.InRound==true && match.RoundSerial!=introRound){introRound=match.RoundSerial;IntroRemaining=5;}
        if(GameplayVisible)IntroRemaining=System.MathF.Max(0,IntroRemaining-Time.Delta);
        if(match?.InRound==true){Highlights.WatchActiveRound();if(Session?.IsPopRiot==true)TeamHighlights.WatchActiveRound();}
        else if(match?.Status=="TIME UP" && match.Standings.FirstOrDefault(x=>x.IsPlayer) is {} row)
            Highlights.Complete(RecordKey(match.PlayedMinutes),match.RoundSerial,row.Points);
        else {Highlights.ClearObservation();TeamHighlights.ClearObservation();}
        if(match?.Status=="TIME UP" && Session?.IsPopRiot==true)TeamHighlights.Complete(TeamRecordKey,match.RoundSerial,Session.TeamScore);
    }

    protected override void OnStart(){Controls=Components.Get<PaintballControls>() ?? Components.Create<PaintballControls>();if(!Components.Get<PopRiotHud>().IsValid())Components.Create<PopRiotHud>();if(!Components.Get<OverdriveHud>().IsValid())Components.Create<OverdriveHud>();if(ArenaSelection.TakeSetupRequest())MenuPage="matches";}

    string Key(string name)=>Controls?.Label(name) ?? Input.GetButtonOrigin(name);

    ForwardAutoRun AutoRun=>MultiplayerSession.LocalPlayer(Scene)?.Components.Get<ForwardAutoRun>();

    string MovementHint=>$"{Key("forward")}/{Key("left")}/{Key("backward")}/{Key("right")} MOVE / {(AutoRun?.Automatic==true ? $"HOLD {Key("forward")} 1.25s TO RUN / " : "")}{Key("cover")} COVER / TOGGLE CROUCH / HOLD {Key("duck")} CROUCH";

    string ActionHint=>$"{Key("attack1")} FIRE (COVER: PEEK & FIRE) / {(Session?.IsPopRiot==true ? "FRIENDLY FIRE OFF" : "CLOSE CLICK: GUN BUTT")} / HOLD {Key("attack2")} FOCUS / {Key("shoulder")} SHOULDER / RUN+{Key("cover")} SLIDE / {Key("vault")} VAULT / {Key("togglehud")} TEXT / {Key("togglescoreboard")} SCOREBOARD / M MENU{(MultiplayerSession.Online ? $" / {(PlayerVoiceChat.HoldToTalk ? "HOLD " : "")}{Key("voice")} VOICE" : "")}";
    bool MeleeHint => Cover?.Attached!=true && !Input.Down("attack2") && !Input.Down("attack1") && Contact?.MeleeAvailable==true;
    PaintballMarker Weapon => MultiplayerSession.LocalPlayer(Scene)?.Components.Get<PaintballMarker>();

    RosterSelection Roster => MultiplayerSession.LocalPlayer(Scene)?.Components.Get<RosterSelection>();

    ArenaMatch Match => MultiplayerSession.LocalPlayer(Scene)?.Components.Get<ArenaMatch>();

    CoverController Cover => MultiplayerSession.LocalPlayer(Scene)?.Components.Get<CoverController>();

    VaultController Vault => MultiplayerSession.LocalPlayer(Scene)?.Components.Get<VaultController>();

    string DamageHint {

        get {

            var player=MultiplayerSession.LocalPlayer(Scene);

            var direction=player is null ? "" : Match?.PlayerState?.HitDirectionLabel(Rotation.FromYaw(player.EyeAngles.yaw));

            return string.IsNullOrEmpty(direction) ? "TAGGED - KEEP MOVING" : $"TAGGED FROM {direction} - FIND COVER";

        }

    }

    string ShieldStatus => Scene.GetAllComponents<ArenaShield>().FirstOrDefault(x=>x.Owner.IsValid() && x.Owner.GameObject==Weapon?.GameObject) is {} shield ? $"SHIELD {shield.Remaining}/{ArenaShield.Capacity}" : "";

    string AmmoStatus => Weapon?.UnlimitedAmmo == true ? "UNLIMITED PAINT / NO RELOAD" : Weapon?.ReloadRemaining > 0 ? "RELOADING" : "READY";

    string RoundHint => MultiplayerSession.Online ? (Networking.IsHost ? "M - MULTIPLAYER MENU / B - START ROUND" : "WAITING FOR THE HOST TO START") : $"{Key("flashlight")} - START PAINT RUSH / {Key("roundlength")} - ROUND LENGTH: {(Match?.RoundDuration ?? 180)/60:0} MIN";

    string Accuracy => Weapon?.Shots > 0 ? $"{System.Math.Clamp((int)System.Math.Round(100.0 * (Session?.IsCooperative==true ? Match.PlayerState?.PopHits ?? 0 : Weapon.Hits) / Weapon.Shots),0,100)}%" : "-";

    int ClockSeconds => (int)System.Math.Ceiling(Match?.Remaining ?? 0);

    string Clock => $"{ClockSeconds / 60:00}:{ClockSeconds % 60:00}";

    CloseCombat Contact => Weapon?.Components.Get<CloseCombat>();

    bool ShowRecoveryPrompt => Controls?.Open!=true && Contact?.Incapacitated==true && Match?.Status!="TIME UP";
    string RecoveryKey => (Input.UsingController ? Input.GetButtonOrigin("attack2") : Key("attack2")) switch
    {
        "MOUSE2" or "MOUSE 2" or "mouse2" => "RIGHT MOUSE",
        var binding => binding
    };
    int RecoveryProgress => Contact?.Recovering==true ? CloseCombat.RequiredRecoveryPresses : Contact?.RecoveryPresses ?? 0;
    string RecoveryMessage => Contact?.Recovering==true ? "BACK IN THE FIGHT" : Contact?.ReadyToRecover==true
        ? $"QUICKLY TAP {RecoveryKey} TO GET UP" : $"GET READY TO TAP {RecoveryKey}";
    string PlayerRank => Match is {} match && match.Standings.FirstOrDefault(x=>x.IsPlayer) is {} row ? match.RankOf(row).ToString() : "-";
    string RestartHint => MultiplayerSession.Online && !Networking.IsHost ? "WAITING FOR HOST TO START THE NEXT ROUND / M MENU"
        : $"{Key("flashlight")} - PLAY AGAIN / M MENU";
    void OpenResultMenu()
    {
        if(Controls?.Open!=true)Controls?.Toggle();
        MenuPage="matches";
    }
    string ContextHint => Vault?.IsVaulting == true ? "VAULTING" : string.Join(" / ",new[]{Vault?.Hint,Cover?.Hint}.Where(h=>!string.IsNullOrEmpty(h)));

    protected override int BuildHash() => System.HashCode.Combine(

        System.HashCode.Combine(GameplayVisible,IntroRemaining>0,IntroRemaining<.65f,(Weapon?.HitFlash>0)==true ? 1+((Weapon?.EliminationFlash>0)==true ? 1 : 0) : ((Weapon?.EliminationFlash>0)==true ? 2 : 0),Contact?.FeedbackRemaining>0,Contact?.FeedbackText,System.HashCode.Combine(ShowGameplayText,LoadingArena,LoadingMessage)),

        System.HashCode.Combine(Match?.Status,Highlights.Best,Highlights.NewBest,MenuPage,ShowJoin,Session?.QuickPlaying,ShowFeedback,Session?.Mode),Match?.StandingsHash,ClockSeconds,Match?.RoundDuration,Roster?.Selected,System.HashCode.Combine(ContextHint,IncomingDirectionStyle,ShowRecoveryPrompt,RecoveryMessage,RecoveryProgress,SpeakingNames,PlayerVoiceChat.ChatEnabled,System.HashCode.Combine(PlayerVoiceChat.HoldToTalk,MeleeHint)),System.HashCode.Combine(System.HashCode.Combine(Session?.Notice+Session?.RiotResult+MenuNotice+TeamBestMessage,Session?.Players.Count(),Session?.Bots.Count(),Session?.LobbyRevision,DamageHint,PlayLabel,Controls?.ShowScoreboard),Controls?.Open,Controls?.Capturing,Controls?.Notice,MovementHint,ActionHint,ShieldStatus));

}