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