TrainingHud.razor
@using Sandbox

@using Sandbox.UI

@using PaintballBaddies

@inherits PanelComponent

@namespace PaintballBaddies



<root>

    @if (Controls?.Open == true)

    {

        <div class="controls-overlay"><div class="controls-menu">

            <label class="brand">@(Controls.Welcome ? "PAINTBALL BADDIES" : "GAME SETTINGS")</label>

            <label class="menu-help">PUBLIC BETA 0.1 / NEON FOUNDRY — help shape the next update.</label>
            <label>YOUR PLAYER NAME</label>

            <TextEntry @ref=NameEntry class="player-name" Value:[email protected] [email protected] Placeholder="Choose your name" />

            <label class="menu-help">Land paint and avoid hits. Shields block five front paintball hits or two gun-butt strikes. Getting knocked down loses your shield.</label>

            <label class="menu-help">@Key("attack2"): gun-butt strike nearby / aim at range / peek from cover. Downed: press it 5 times quickly.</label>
            <label class="menu-help">Double-tap @Key("backward") to quickly turn around. Hold it normally to walk backward.</label>
            <label>@Controls.Notice</label>

            <label>@(Match?.InRound == true ? "NEXT MATCH DURATION" : "MATCH DURATION")</label>

            @if(!MultiplayerSession.Online || Networking.IsHost)

            {

            <div class="timer-options">

                @foreach(var minutes in new[]{1,2,3})

                {

                    <button class="@(Match?.RoundDuration == minutes*60 ? "selected" : "")" onclick=@(()=>Match?.SetRoundMinutes(minutes))>@minutes MIN</button>

                }

            </div>

            }

            else { <label>@($"{Session?.Minutes ?? 3} MINUTES / CHOSEN BY THE HOST")</label> }

            @if(Match?.InRound == true)

            {

                <label>Applies to your next match or restart. The current countdown stays unchanged.</label>

            }

            @if(!MultiplayerSession.Online)

            {

                <button onclick=@(()=>Controls.StartMatch())>SINGLE PLAYER / 5 AI OPPONENTS</button>

                <button onclick=@(()=>Controls.StartPractice())>PRACTICE / EXPLORE ARENA</button>

                <button onclick=@(()=>Session?.Host())>HOST PUBLIC GAME / UP TO 6 PLAYERS</button>
                <button onclick=@(()=>Session?.Host(false))>HOST FRIENDS-ONLY GAME</button>

                <button disabled=@(Session?.Searching==true) onclick=@(()=>Session?.RefreshLobbies())>@(Session?.Searching==true ? "SEARCHING..." : "BROWSE MULTIPLAYER GAMES")</button>

                @if(Session is not null)

                {

                    @foreach(var lobby in Session.Lobbies)

                    {

                        <button disabled=@(lobby.IsFull || !MultiplayerSession.CompatibleLobby(lobby)) onclick=@(()=>Session.JoinLobby(lobby))>@MultiplayerSession.LobbyLabel(lobby)</button>

                    }

                    <TextEntry @ref=JoinEntry class="player-name" Value:[email protected] Placeholder="Host Steam ID or lobby ID" />

                    <button onclick=@(()=>Session.JoinTyped())>JOIN BY ID</button>

                }

            }

            else

            {

                <label>@ConnectionHint</label>

                <label class="menu-help">@($"YOUR BADDIE: {Roster?.CharacterName} / CHARACTERS ARE ASSIGNED BY PLAYER SLOT")</label>

                @if(Networking.IsHost)

                {

                    <label class="menu-help">HOST STEAM ID: @Connection.Host.SteamId.ValueUnsigned</label>

                    <button onclick=@(()=>Controls.StartMatch())>@(Session?.InRound==true ? "RESTART ROUND" : "START MULTIPLAYER ROUND")</button>

                }

                else { <label>The host chooses the timer and starts each round.</label> }

                <button onclick=@(()=>Controls.StartPractice())>BACK TO GAME</button>

                <button onclick=@(()=>Session?.Leave())>LEAVE MULTIPLAYER</button>

            }

            <label class="menu-help">@Session?.Notice</label>

            <button onclick=@(()=>ShowCredits=!ShowCredits)>@(ShowCredits ? "HIDE CREDITS" : "CREDITS / COMMUNITY")</button>
            @if(ShowCredits)
            {
                <div class="credits">
                    <label>PAINTBALL BADDIES / PUBLIC BETA 0.1</label>
                    <label class="menu-help">Created by SoloVolt Games — a solo creator building with AI and community contributions.</label>
                    <label class="menu-help">Game code: Paintball Baddies contributors (MIT). Engine, Citizen rig/animations and library assets: Facepunch.</label>
                    <label class="menu-help">Voices: CiciFyre. Interface/contact sound sources: Kenney. Marker recordings: Joseph Sardin / BigSoundBank and Arq1998. Material sources: ambientCG. Fallback animation: Quaternius.</label>
                    <label class="menu-help">Generative AI art: Meshy models and generated visual/texture sources, adapted for the game. Full asset terms and modifications are recorded in ASSET_CREDITS.md.</label>
                    <label class="menu-help">Please leave feedback on our s&box game page: sbox.game/solovolt/paintball-baddies. Include Beta 0.1, host/join/solo, what happened and steps to repeat it. Screenshots or short clips help.</label>
                    <label class="menu-help">This beta is still being tested for Internet joining, animation and long-session stability. Player-hosted matches end when the host leaves.</label>
                    <label class="menu-help">Community maps and fixes are welcome. Submitted work is reviewed and playtested before official inclusion, with author credit. Original game code is MIT licensed; third-party art retains its own terms.</label>
                </div>
            }

            <button onclick=@(()=>AutoRun?.ToggleAutomatic())>AUTO-RUN: @(AutoRun?.Automatic==true ? "ON" : "OFF") / HOLD FORWARD 1.25s</button>

            <div class="binding-list">

                @foreach(var action in Controls.Bindings.Keys.Where(key=>!MultiplayerSession.Online || !key.StartsWith("slot")))

                {

                    <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>

            <div class="binding-buttons"><button onclick=@(()=>Controls.ResetBindings())>RESTORE DEFAULTS</button><button onclick=@(()=>Controls.Toggle())>BACK TO GAME</button></div>

        </div></div>

    }

    <div class="title"><label class="brand">PAINTBALL BADDIES</label><label class="location">@ArenaMap.DisplayTitle(Scene).ToUpperInvariant() / @(Match?.Status ?? "TRAINING")</label></div>

    <div class="match @(Match?.InRound == true ? "active" : "")">

        @if (Match?.InRound == true)

        {

            <label class="round-clock @(ClockSeconds <= 10 ? "urgent" : ClockSeconds <= 30 ? "warning" : "")">@Clock</label>

            <div class="round-detail"><label>@Match.PlayerState.HitsLanded LANDED / @Match.PlayerState.PaintHits RECEIVED</label><label class="round-objective">LAND PAINT / AVOID PAINT / EVERYONE STAYS IN</label></div>

        }

        else

        {

            <label>@RoundHint</label>

        }

    </div>

    @if (Match?.InRound == true && Match.PlayerState.HitFeedbackRemaining > 0)

    {

        <div class="damage"><label>@DamageHint</label></div>

    }

    @if (Match is not null && (Match.InRound || Match.Status == "TIME UP"))

    {

        <div class="leaderboard">

            <label class="board-title">@(Match.InRound ? "LIVE STANDINGS" : "FINAL STANDINGS")</label>

            <div class="board-row board-heading"><label>PLAYER</label><label>HIT</label><label>TAKEN</label><label>PTS</label></div>

            @foreach(var row in Match.Standings)

            {

                <div class="board-row @(row.IsPlayer ? "you" : "")"><label>@(Match.RankOf(row)). @row.Name @(row.IsPlayer ? "(YOU)" : "")</label><label>@row.Landed</label><label>@row.Received</label><label>@row.Points</label></div>

            }

        </div>

    }

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

    {

        <div class="result">

            <label class="result-title">TIME UP</label>

            <label class="result-progress">YOUR RANK: @Match.RankOf(Match.Standings.First(x=>x.IsPlayer)) / 6</label>

            <label class="result-reason">POINTS = HITS LANDED - HITS RECEIVED</label>

            <label class="result-reason">FEWER HITS RECEIVED BREAKS TIES / EXACT TIES SHARE A RANK</label>

            <div class="result-stats"><label>@(Weapon?.Shots ?? 0) SHOTS</label><label>@Match.PlayerState.HitsLanded TAGS</label><label>@Accuracy ACCURACY</label></div>

            <label class="retry">@Key("flashlight") - PLAY AGAIN / @Key("use") - TRAINING</label>

        </div>

    }

    @if (Match is null || Match.InRound || (Match.Status == "TRAINING" || Match.Status == "LOBBY"))

    {

        <div class="crosshair @(Weapon?.HitFlash > 0 ? "hit" : "")">+</div>

    }

    <div class="roster"><label>@(Roster?.CharacterName ?? "Viper")</label>

        @if (Match?.Status == "TRAINING")

        {

            <label class="hint">@string.Join(" / ",RosterSelection.Names.Select((name,i)=>$"{Key($"slot{i+1}")} {name.ToUpperInvariant()}"))</label>

        }

    </div>

    <div class="stats"><label>@(Match is not null && Match.Status != "TRAINING" ? $"POINTS {Match.PlayerState.HitsLanded-Match.PlayerState.PaintHits}" : $"PRACTICE SCORE {Weapon?.Score ?? 0}")</label></div>

    <div class="weapon"><label class="name">VX-9 HEARTLINE</label><div class="ammo"><label>@(Weapon?.UnlimitedAmmo == true ? "∞" : $"{Weapon?.Ammo ?? 40}")</label></div><label>@ShieldStatus@AmmoStatus</label></div>

    @if (!string.IsNullOrEmpty(ContextHint))

    {

        <div class="context-prompt"><label>@ContextHint</label></div>

    }

    <div class="controls"><label>@MovementHint</label><label>@ActionHint</label></div>

</root>



@code {

    PaintballControls Controls;

    TextEntry NameEntry;

    TextEntry JoinEntry;

    bool ShowCredits;

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

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

    MultiplayerSession Session=>MultiplayerSession.Find(Scene);

    protected override void OnUpdate(){if(Controls.IsValid())Controls.EditingName=NameEntry?.HasFocus == true || JoinEntry?.HasFocus==true;}

    protected override void OnStart(){Controls=Components.Get<PaintballControls>() ?? Components.Create<PaintballControls>();}

    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("run")} RUN / {Key("duck")} CROUCH / {Key("cover")} COVER";

    string ActionHint=>$"{Key("attack1")} FIRE / HOLD {Key("attack2")} AIM / TAP GUN BUTT / {Key("shoulder")} SHOULDER / RUN+{Key("cover")} SLIDE / {Key("vault")} VAULT / M MENU";
    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 * 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>();

    string ContextHint => Contact?.Downed==true ? (Contact.ReadyToRecover ? $"TAP {Key("attack2")} QUICKLY - GET UP {Contact.RecoveryPresses}/{CloseCombat.RequiredRecoveryPresses}" : "KNOCKED DOWN") : Contact?.Recovering==true ? "GETTING UP" : 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(Weapon?.Ammo,Weapon?.Shots,Weapon?.Hits,Weapon?.HitFlash>0,Weapon?.UnlimitedAmmo),

        Match?.Status,Match?.StandingsHash,ClockSeconds,Match?.RoundDuration,Roster?.Selected,ContextHint,System.HashCode.Combine(System.HashCode.Combine(Session?.Notice,Session?.Players.Count(),Session?.LobbyRevision,DamageHint,ShowCredits),Controls?.Open,Controls?.Capturing,Controls?.Notice,MovementHint,ActionHint,ShieldStatus));

}