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));
}