UI component for the in-game HUD and menus. Manages HUD state, chat, leaderboard fetching, menus (settings, workshop, spawn), survival/evade UI data, and exposes helper accessors for player/meta/score information used by the Razor view.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;
using Sandbox.UI;
using Sandbox.Services;
namespace Sandbox;
public partial class PushHud
{
public class EntryItem
{
public int Rank { get; set; }
public string DisplayName { get; set; }
public double Value { get; set; }
public ulong SteamId { get; set; }
}
[Property] public PushAbility PushAbility { get; set; }
public PlayerHealth LocalHealth { get; private set; }
private TimeSince _survivalTime;
private bool _isNextbotMode;
private bool _isGamePaused;
private float _timeScaleBeforePause = 1f;
private float _nearestBotDistance = float.MaxValue;
private TimeSince _timeSinceSurvivalReward = 0f;
public bool IsGracePeriod => _isNextbotMode && NextbotSpawner.IsGracePeriodActive;
public float GracePeriodRemaining => NextbotSpawner.GracePeriodRemaining;
public float NearestBotDistance => _nearestBotDistance;
public bool IsTerrorActive => _isNextbotMode && !IsGracePeriod && _nearestBotDistance < 900f;
public bool IsMenuOpen { get; set; } = false;
public bool IsSettingsOpen { get; set; } = false;
public bool IsWorkshopMenuOpen { get; set; } = false;
public bool IsQSpawnMenuOpen { get; set; } = false;
public string ActiveTab { get; set; } = "PROPS";
public static string CurrentLanguage { get; set; } = "EN";
public struct DownedBeaconInfo
{
public string Name;
public float DistanceMeters;
public float BleedoutRemaining;
public float ScreenX;
public float ScreenY;
public bool IsVisibleOnScreen;
}
public List<DownedBeaconInfo> GetDownedBeacons()
{
var list = new List<DownedBeaconInfo>();
if ( !Scene.Camera.IsValid() ) return list;
foreach ( var p in PlayerHealth.ActiveDownedPlayers )
{
if ( !p.IsValid() || p == LocalHealth ) continue;
var pos = p.WorldPosition + Vector3.Up * 40f;
var dir = pos - Scene.Camera.WorldPosition;
if ( Vector3.Dot( Scene.Camera.WorldRotation.Forward, dir ) <= 0.1f ) continue;
var screenPos = Scene.Camera.PointToScreenPixels( pos );
list.Add( new DownedBeaconInfo
{
Name = p.GameObject.Name,
DistanceMeters = dir.Length / 50f,
BleedoutRemaining = Math.Max( 0, p.RespawnTimer ),
ScreenX = screenPos.x,
ScreenY = screenPos.y,
IsVisibleOnScreen = true
} );
}
return list;
}
public bool IsLbLoading { get; set; } = true;
public string CurrentLbStat { get; set; } = "pushes_count";
public List<EntryItem> LbEntries { get; set; } = new();
public struct ChatMessage
{
public string Author { get; set; }
public string Text { get; set; }
}
public List<ChatMessage> CanvasMessages { get; set; } = new();
public bool IsChatOpen { get; set; } = false;
private TextEntry ChatInput { get; set; }
public struct PlayerInfo
{
public string Name { get; set; }
public int Score { get; set; }
public bool IsLocal { get; set; }
}
private string L( string ru, string en ) => CurrentLanguage == "RU" ? ru : en;
private bool IsReady => PushAbility?.IsReady ?? false;
private MetaProgression GetMeta() => Scene.GetAllComponents<MetaProgression>().FirstOrDefault();
private int GetCredits() => GetMeta()?.Credits ?? 0;
private int GetForceLevel() => GetMeta()?.ForceLevel ?? 0;
private int GetMassLevel() => GetMeta()?.MassLevel ?? 0;
private int GetEnergyLevel() => GetMeta()?.EnergyLevel ?? 0;
private int GetPrestigeLevel() => GetMeta()?.PrestigeLevel ?? 0;
private int GetPrestigeTokens() => GetMeta()?.PrestigeTokens ?? 0;
private bool CanPrestige() => GetMeta()?.CanPrestige() ?? false;
private void TryPrestige() => GetMeta()?.TryPrestige();
private int GetForceCost() => GetMeta()?.GetUpgradeCost(GetForceLevel()) ?? 0;
private int GetMassCost() => GetMeta()?.GetUpgradeCost(GetMassLevel()) ?? 0;
private int GetEnergyCost() => GetMeta()?.GetUpgradeCost(GetEnergyLevel()) ?? 0;
private void TryUpgradeForce() => GetMeta()?.TryUpgradeForce();
private void TryUpgradeMass() => GetMeta()?.TryUpgradeMass();
private void TryUpgradeEnergy() => GetMeta()?.TryUpgradeEnergy();
private float GetHealth() => LocalHealth?.CurrentHealth ?? 100f;
private float GetMaxHealth() => LocalHealth?.MaxHealth ?? 100f;
private float GetHealthPercent() => MathX.Clamp( (GetHealth() / GetMaxHealth()) * 100f, 0f, 100f );
private void SelectTab( string tab )
{
ActiveTab = tab;
StateHasChanged();
}
protected override void OnEnabled()
{
base.OnEnabled();
if ( Scene.IsValid() )
{
_isNextbotMode = Scene.GetAllComponents<NextbotAI>().Any( x => x.IsValid() ) ||
Scene.GetAllComponents<NextbotSpawner>().Any( x => x.IsValid() );
if ( _isNextbotMode ) _survivalTime = 0f;
}
AddMessage( L( "Система", "System" ), L( "Чат готов. Нажмите Enter для ввода.", "Chat ready. Press Enter to type." ) );
ApplyMouseState();
if ( !_isNextbotMode )
_ = RefreshLeaderboard();
}
private async Task SelectLbStat( string statIdent )
{
if ( CurrentLbStat == statIdent ) return;
CurrentLbStat = statIdent;
await RefreshLeaderboard();
}
private async Task RefreshLeaderboard()
{
IsLbLoading = true;
StateHasChanged();
try
{
var board = Sandbox.Services.Leaderboards.GetFromStat( "seilordstudio.one_more_push", CurrentLbStat );
board.SetAggregationSum();
board.MaxEntries = 10;
await board.Refresh();
if ( board.Entries != null )
{
LbEntries = board.Entries.Select( x => new EntryItem
{
Rank = Convert.ToInt32( x.Rank ),
DisplayName = x.DisplayName ?? "Player",
Value = Convert.ToDouble( x.Value ),
SteamId = Convert.ToUInt64( x.SteamId )
} ).ToList();
}
else
{
LbEntries = new();
}
}
catch ( System.Exception ex )
{
Log.Warning( $"Leaderboard fetch error: {ex.Message}" );
}
IsLbLoading = false;
StateHasChanged();
}
private string GetRankBadge( int rank )
{
return rank switch
{
1 => "🥇 1",
2 => "🥈 2",
3 => "🥉 3",
_ => rank.ToString()
};
}
private bool IsEPressed()
{
return Input.Down( "use" );
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( (!PushAbility.IsValid() || PushAbility.IsProxy) && global::PushAbility.LocalInstance.IsValid() )
PushAbility = global::PushAbility.LocalInstance;
if ( (!LocalHealth.IsValid() || LocalHealth.IsProxy) && global::PlayerHealth.LocalInstance.IsValid() )
LocalHealth = global::PlayerHealth.LocalInstance;
if ( _isNextbotMode )
{
Vector3 playerPos = LocalHealth.IsValid() ? LocalHealth.WorldPosition :
Scene.Camera.IsValid() ? Scene.Camera.WorldPosition : Vector3.Zero;
_nearestBotDistance = NextbotAI.GetClosestBotDistance( playerPos );
if ( LocalHealth.IsValid() && !LocalHealth.IsDead && !LocalHealth.IsDowned )
{
if ( _timeSinceSurvivalReward >= 15f )
{
_timeSinceSurvivalReward = 0f;
GetMeta()?.AddCredits( 15 );
ScoreManager.Instance?.AddScore( 15 );
AddMessage( L( "Выживание", "Survival" ), L( "+15 монет за выживание!", "+15 credits for surviving!" ) );
}
}
}
bool eDown = IsEPressed();
if ( eDown && !_isNextbotMode )
{
if ( !LocalHealth.IsValid() || !LocalHealth.IsDowned )
{
if ( !IsQSpawnMenuOpen && !IsChatOpen && !IsSettingsOpen && !IsMenuOpen )
{
IsWorkshopMenuOpen = false;
IsQSpawnMenuOpen = true;
ApplyMouseState();
StateHasChanged();
}
}
}
else
{
if ( IsQSpawnMenuOpen )
{
CloseQSpawnMenu();
}
}
if ( Input.EscapePressed || Input.Pressed( "menu" ) )
{
Input.EscapePressed = false;
ToggleMenu();
}
if ( Input.Pressed( "reload" ) )
{
if ( !IsMenuOpen && !IsSettingsOpen && !IsChatOpen && !IsQSpawnMenuOpen )
{
ToggleWorkshopMenu();
}
}
if ( Input.Pressed( "chat" ) )
{
if ( !IsMenuOpen && !IsSettingsOpen && !IsWorkshopMenuOpen && !IsQSpawnMenuOpen )
{
if ( !IsChatOpen ) OpenChat();
}
}
ApplyMouseState();
}
private void SetLanguage( string lang )
{
CurrentLanguage = lang;
StateHasChanged();
}
private void ApplyMouseState()
{
bool needsMouse = IsMenuOpen || IsSettingsOpen || IsChatOpen || IsWorkshopMenuOpen || IsQSpawnMenuOpen;
Mouse.Visibility = needsMouse ? MouseVisibility.Visible : MouseVisibility.Hidden;
if ( !needsMouse && Panel != null )
{
Panel.Focus();
}
}
private void ToggleWorkshopMenu()
{
IsWorkshopMenuOpen = !IsWorkshopMenuOpen;
IsQSpawnMenuOpen = false;
ApplyMouseState();
StateHasChanged();
}
private void CloseWorkshopMenu()
{
IsWorkshopMenuOpen = false;
ApplyMouseState();
StateHasChanged();
}
private void CloseQSpawnMenu()
{
IsQSpawnMenuOpen = false;
ApplyMouseState();
StateHasChanged();
}
private void SelectAndSpawnProp( int index )
{
if ( PropSpawner.Instance != null )
{
PropSpawner.Instance.SpawnByIndex( index );
}
if ( IsWorkshopMenuOpen ) CloseWorkshopMenu();
}
private void ToggleMenu()
{
if ( IsSettingsOpen ) CloseSettings();
else if ( IsWorkshopMenuOpen ) CloseWorkshopMenu();
else if ( IsQSpawnMenuOpen ) CloseQSpawnMenu();
else if ( IsMenuOpen ) StartGame();
else OpenMenu();
}
private void StartGame()
{
IsMenuOpen = false;
IsSettingsOpen = false;
IsWorkshopMenuOpen = false;
IsQSpawnMenuOpen = false;
ResumeGame();
ApplyMouseState();
}
private void OpenMenu()
{
PauseGame();
IsMenuOpen = true;
IsSettingsOpen = false;
IsWorkshopMenuOpen = false;
IsQSpawnMenuOpen = false;
ApplyMouseState();
if ( !_isNextbotMode )
_ = RefreshLeaderboard();
}
private void OpenSettings()
{
IsMenuOpen = false;
IsSettingsOpen = true;
IsWorkshopMenuOpen = false;
IsQSpawnMenuOpen = false;
ApplyMouseState();
}
private void CloseSettings()
{
IsSettingsOpen = false;
IsMenuOpen = true;
IsWorkshopMenuOpen = false;
IsQSpawnMenuOpen = false;
ApplyMouseState();
}
private void QuitGame()
{
MusicManager.StopAllMusic();
ResumeGame();
Game.ActiveScene.LoadFromFile( "scenes/MainMenu.scene" );
}
private void PauseGame()
{
if ( _isGamePaused ) return;
_timeScaleBeforePause = Scene.TimeScale;
Scene.TimeScale = 0f;
_isGamePaused = true;
}
private void ResumeGame()
{
if ( !_isGamePaused ) return;
Scene.TimeScale = _timeScaleBeforePause;
_isGamePaused = false;
}
private string GetSurvivalTime()
{
int totalSeconds = Math.Max( 0, (int)_survivalTime );
return $"{totalSeconds / 60:00}:{totalSeconds % 60:00}";
}
public bool HasEvadeRoundManager => EvadeRoundManager.Instance.IsValid();
public EvadePhase CurrentEvadePhase => EvadeRoundManager.Instance?.CurrentPhase ?? EvadePhase.Survival;
public string EvadeFormattedTime => EvadeRoundManager.Instance?.GetFormattedPhaseTime() ?? GetSurvivalTime();
public int EvadeAliveCount => EvadeRoundManager.Instance?.AliveCount ?? 1;
public int EvadeDownedCount => EvadeRoundManager.Instance?.DownedCount ?? 0;
public int EvadeDeadCount => EvadeRoundManager.Instance?.DeadCount ?? 0;
public int EvadeRevivesCount => EvadeRoundManager.Instance?.TotalRevives ?? 0;
public bool IsSpectating => EvadeRoundManager.Instance?.IsSpectating ?? false;
public string SpectateTargetName => EvadeRoundManager.Instance?.SpectateTargetName ?? "Nobody";
public string RoundOutcome => EvadeRoundManager.Instance?.RoundOutcome ?? "";
public PlayerHealth GetNearbyDownedTeammate()
{
if ( !LocalHealth.IsValid() || LocalHealth.IsDowned || LocalHealth.IsDead ) return null;
foreach ( var p in PlayerHealth.ActiveDownedPlayers )
{
if ( !p.IsValid() || p == LocalHealth ) continue;
if ( Vector3.DistanceBetween( LocalHealth.WorldPosition, p.WorldPosition ) <= 130f )
return p;
}
return null;
}
private int GetScore() => ScoreManager.Instance?.Score ?? 0;
private string GetFormattedTime()
{
var shrinker = Scene.GetAllComponents<ArenaShrinker>().FirstOrDefault();
if ( !shrinker.IsValid() ) return "02:00";
int minutes = (int)Math.Max( 0, shrinker.TimeRemaining ) / 60;
int seconds = (int)Math.Max( 0, shrinker.TimeRemaining ) % 60;
return $"{minutes:00}:{seconds:00}";
}
private float GetPercent()
{
if ( PushAbility == null ) return 0f;
return MathX.Clamp( PushAbility.CooldownProgress * 100f, 0f, 100f );
}
private void ToggleBots()
{
BotSpawner.EnableBots = !BotSpawner.EnableBots;
StateHasChanged();
}
private int GetHighScore() => SaveManager.Current?.HighScore ?? 0;
private void OpenChat()
{
IsChatOpen = true;
IsWorkshopMenuOpen = false;
IsQSpawnMenuOpen = false;
ApplyMouseState();
StateHasChanged();
ChatInput?.Focus();
}
private void SendChatMessage()
{
if ( ChatInput == null || string.IsNullOrWhiteSpace( ChatInput.Text ) )
{
IsChatOpen = false;
ApplyMouseState();
return;
}
string name = Connection.Local?.DisplayName ?? L( "Вы", "You" );
AddMessage( name, ChatInput.Text );
ChatInput.Text = "";
IsChatOpen = false;
ApplyMouseState();
}
public void AddMessage( string author, string text )
{
CanvasMessages.Add( new ChatMessage { Author = author, Text = text } );
if ( CanvasMessages.Count > 6 )
{
CanvasMessages.RemoveAt( 0 );
}
StateHasChanged();
}
private IEnumerable<PlayerInfo> GetPlayerList()
{
if ( Connection.All != null && Connection.All.Any() )
{
foreach ( var conn in Connection.All )
{
bool isLocal = conn == Connection.Local;
int score = isLocal ? GetScore() : 0;
yield return new PlayerInfo
{
Name = string.IsNullOrEmpty( conn.DisplayName ) ? "Player" : conn.DisplayName,
Score = score,
IsLocal = isLocal
};
}
}
else
{
yield return new PlayerInfo
{
Name = L( "Вы", "You" ),
Score = GetScore(),
IsLocal = true
};
}
}
private string GetWeatherTitle( WeatherType type ) => type switch
{
WeatherType.LowGravity => L( "НЕВЕСОМОСТЬ", "LOW GRAVITY" ),
WeatherType.SlipperyIce => L( "ГОЛОЛЁД", "SLIPPERY ICE" ),
WeatherType.StrongWind => L( "ШТОРМОВОЙ ВЕТЕР", "STRONG WIND" ),
_ => ""
};
private string GetWeatherIcon( WeatherType type ) => type switch
{
WeatherType.LowGravity => "🚀",
WeatherType.SlipperyIce => "❄️",
WeatherType.StrongWind => "🌪️",
_ => "⚠️"
};
private string GetWeatherClass( WeatherType type ) => type switch
{
WeatherType.LowGravity => "gravity",
WeatherType.SlipperyIce => "ice",
WeatherType.StrongWind => "wind",
_ => ""
};
private int GetDeaths() => ScoreManager.Instance?.Deaths ?? 0;
public static bool EnableMusic { get; set; } = false;
private void ToggleMusic()
{
EnableMusic = !EnableMusic;
MusicManager.Instance?.ToggleMusic( EnableMusic );
StateHasChanged();
}
protected override int BuildHash()
{
var hash = new HashCode();
hash.Add( GetPercent() );
hash.Add( GetHealthPercent() );
hash.Add( LocalHealth?.IsDead );
hash.Add( (int)(LocalHealth?.RespawnTimer * 10f ?? 0) );
hash.Add( IsReady );
hash.Add( MapEventManager.Instance?.CurrentEvent );
hash.Add( (int)(MapEventManager.Instance?.TimeRemaining * 10f ?? 0) );
hash.Add( EnableMusic );
hash.Add( PushAbility?.ChargeProgress );
hash.Add( PushAbility?.IsCharging );
hash.Add( GetScore() );
hash.Add( GetDeaths() );
hash.Add( GetFormattedTime() );
hash.Add( _isNextbotMode );
hash.Add( _isNextbotMode ? (int)_survivalTime : 0 );
hash.Add( RoundManager.Instance?.CurrentRound );
hash.Add( IsMenuOpen );
hash.Add( IsWorkshopMenuOpen );
hash.Add( IsQSpawnMenuOpen );
hash.Add( ActiveTab );
hash.Add( GetCredits() );
hash.Add( GetForceLevel() );
hash.Add( GetMassLevel() );
hash.Add( GetEnergyLevel() );
hash.Add( GetPrestigeLevel() );
hash.Add( GetPrestigeTokens() );
hash.Add( CanPrestige() );
hash.Add( WeaponManager.Instance?.CurrentWeapon?.Id );
hash.Add( ShopManager.Instance?.SelectedItem?.Id );
hash.Add( IsSettingsOpen );
hash.Add( CurrentLanguage );
hash.Add( IsChatOpen );
hash.Add( CanvasMessages.Count );
hash.Add( Connection.All?.Count() ?? 0 );
hash.Add( Killfeed.Instance?.Entries.Count );
hash.Add( CurrentLbStat );
hash.Add( IsLbLoading );
hash.Add( LbEntries.Count );
hash.Add( IsGracePeriod );
hash.Add( (int)(GracePeriodRemaining * 10f) );
hash.Add( IsTerrorActive );
hash.Add( (int)(_nearestBotDistance / 50f) );
hash.Add( LocalHealth?.IsRescuing );
hash.Add( (int)(LocalHealth?.RescueProgress * 100f ?? 0) );
hash.Add( HasEvadeRoundManager );
hash.Add( CurrentEvadePhase );
hash.Add( EvadeAliveCount );
hash.Add( EvadeDownedCount );
hash.Add( EvadeDeadCount );
hash.Add( EvadeRevivesCount );
hash.Add( IsSpectating );
hash.Add( SpectateTargetName );
hash.Add( PlayerHealth.ActiveDownedPlayers.Count );
return hash.ToHashCode();
}
}