PushHud.razor.cs

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.

NetworkingFile Access
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();
    }
}