GameHud.razor
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@inherits PanelComponent
<root>
@if ( Manager.IsValid() )
{
<!-- ВІНЬЄТКА З КОЛЬОРОМ КОМАНДИ (ІНТУЇТИВНА ПІДКАЗКА) -->
@if ( LocalPlayerTeam.IsValid() )
{
<div class="team-vignette" style="box-shadow: inset 0 0 150px @(GetCssColor(LocalPlayerTeam.TeamColor.WithAlpha(0.25f)));"></div>
}
<!-- ІНФОРМАЦІЯ ПРО ГРАВЦЯ (ЗЛІВА ЗВЕРХУ) -->
@if ( LocalPlayerTeam.IsValid() )
{
<div class="player-info-top-left" style="border-left: 5px solid @GetCssColor(LocalPlayerTeam.TeamColor);">
<div class="player-icon-container" @ref="PortraitContainer" style="border-color: @GetCssColor(LocalPlayerTeam.TeamColor);"></div>
<div class="player-hp-bar">
@{
int hpSegments = (int)MathF.Ceiling(LocalPlayerTeam.Health / 20f);
}
@for(int i = 0; i < 5; i++)
{
<div class="hp-segment @(i < hpSegments ? "filled" : "empty")"></div>
}
</div>
</div>
}
<!-- АМУНІЦІЯ (СПРАВА ЗНИЗУ) -->
@if ( LocalPlayerGun.IsValid() && !LocalPlayerTeam.IsDead )
{
<div class="ammo-bottom-right">
<div class="ammo-icon">
<svg viewBox="0 0 24 24" class="ammo-svg" style="color: @GetCssColor(LocalPlayerTeam.TeamColor);">
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/>
</svg>
</div>
<div class="ammo-text">
<span class="ammo-current">@LocalPlayerGun.AmmoInMagazine</span>
<span class="ammo-divider">/</span>
<span class="ammo-reserve">@LocalPlayerGun.AmmoReserve</span>
</div>
@if ( LocalPlayerGun.IsReloading )
{
<div class="reloading-text">RELOADING...</div>
}
</div>
}
<!-- ВЕРХНЯ ЦЕНТРАЛЬНА СМУГА ПРОГРЕСУ -->
<div class="hud-top-center">
<div class="hud-bar-container">
<!-- Ліва смуга: СИНЯ команда -->
<div class="team-bar-left">
<div class="team-fill-left" style="width: @(BluePercent)%;"></div>
<div class="team-label-left">
<span class="team-score">@(MathF.Round(BluePercent))%</span>
</div>
</div>
<!-- Центральний розділювач -->
<div class="hud-divider"></div>
<!-- Права смуга: ЧЕРВОНА команда -->
<div class="team-bar-right">
<div class="team-fill-right" style="width: @(RedPercent)%;"></div>
<div class="team-label-right">
<span class="team-score">@(MathF.Round(RedPercent))%</span>
</div>
</div>
</div>
</div>
<!-- ОКРЕМИЙ ТАЙМЕР СПРАВА ЗВЕРХУ -->
<div class="hud-top-right-timer">
<div class="timer-title">TIME</div>
<div class="timer-value">@FormattedTime</div>
</div>
<!-- CONTROLS HINT / PANEL (BOTTOM LEFT) -->
<div class="controls-bottom-left">
@if ( ShowControls )
{
<div class="controls-panel">
<div class="controls-title">CONTROLS</div>
<div class="control-item"><span class="key-bind">[CTRL]</span> Dive / Swim in Paint</div>
<div class="control-item"><span class="key-bind">[SHIFT]</span> Sprint</div>
<div class="control-item"><span class="key-bind">[R]</span> Reload Weapon</div>
</div>
}
else
{
<div class="press-tab-hint">Hold [TAB] for Controls</div>
}
</div>
<!-- ОВЕРЛЕЙ КІНЦЯ ГРИ -->
@if ( Manager.WinnerTeamId != -1 )
{
<div class="end-game-overlay">
<div class="end-game-card">
<div class="time-up-banner">MATCH FINISHED</div>
@if ( Manager.WinnerTeamId == 0 )
{
<div class="winner-title blue">BLUE TEAM WINS!</div>
}
else if ( Manager.WinnerTeamId == 1 )
{
<div class="winner-title red">RED TEAM WINS!</div>
}
else
{
<div class="winner-title draw">DRAW!</div>
}
<div class="results-visualizer">
<div class="team-result blue-result @(Manager.WinnerTeamId == 0 ? "winner" : "")">
<div class="team-name">BLUE</div>
<div class="percentage">@(BluePercent.ToString("F1"))%</div>
<div class="blobs-count">@Manager.BluePaintCount blobs</div>
</div>
<div class="vs-divider">
<div class="vs-circle">VS</div>
</div>
<div class="team-result red-result @(Manager.WinnerTeamId == 1 ? "winner" : "")">
<div class="team-name">RED</div>
<div class="percentage">@(RedPercent.ToString("F1"))%</div>
<div class="blobs-count">@Manager.RedPaintCount blobs</div>
</div>
</div>
@{
float totalPercent = BluePercent + RedPercent;
float blueRatio = totalPercent > 0 ? (BluePercent / totalPercent) * 100f : 50f;
float redRatio = totalPercent > 0 ? (RedPercent / totalPercent) * 100f : 50f;
}
<div class="comparison-bar">
<div class="comp-fill blue-fill" style="width: @(blueRatio)%;"></div>
<div class="comp-fill red-fill" style="width: @(redRatio)%;"></div>
</div>
<div class="restart-timer">Next round starting soon...</div>
</div>
</div>
}
<!-- ОВЕРЛЕЙ СМЕРТІ ЛОКАЛЬНОГО ГРАВЦЯ -->
@if ( LocalPlayerTeam.IsValid() && LocalPlayerTeam.IsDead )
{
<div class="death-overlay">
<div class="death-card">
<div class="splatted-text">YOU WERE SPLATTED!</div>
<div class="respawn-timer-text">RESPAWNING IN @(MathF.Ceiling(MathF.Max(0f, 5f - LocalPlayerTeam.TimeSinceDeath)))s</div>
</div>
</div>
}
}
</root>
@code {
private string GetCssColor( Color c )
{
return $"rgba({(int)(c.r * 255)}, {(int)(c.g * 255)}, {(int)(c.b * 255)}, {c.a.ToString(System.Globalization.CultureInfo.InvariantCulture)})";
}
private string GetHexColor( Color c )
{
return $"#{(int)(c.r * 255):X2}{(int)(c.g * 255):X2}{(int)(c.b * 255):X2}";
}
/// <summary>
/// Кількість плям слизу для 100% заповнення смуги прогресу
/// </summary>
[Property, Category( "Settings" )]
public int TargetPaintCapacity { get; set; } = 300;
public GameManager Manager { get; private set; }
public PlayerTeam LocalPlayerTeam { get; private set; }
public PaintGun LocalPlayerGun { get; private set; }
public bool ShowControls { get; set; }
public Panel PortraitContainer { get; set; }
protected override void OnStart()
{
// Не показуємо інтерфейс для віддалених гравців у мережевій грі
if ( IsProxy )
{
Enabled = false;
return;
}
// Знаходимо або створюємо GameManager
FindOrCreateManager();
FindLocalPlayer();
}
protected override void OnUpdate()
{
if ( !Manager.IsValid() )
{
FindOrCreateManager();
}
if ( !LocalPlayerTeam.IsValid() )
{
FindLocalPlayer();
}
else
{
if ( PortraitContainer != null )
{
var playerRoot = (LocalPlayerTeam.GameObject.Components.Get<PlayerController>() ?? LocalPlayerTeam.GameObject.Components.GetInAncestors<PlayerController>())?.GameObject ?? LocalPlayerTeam.GameObject;
var portraitRenderer = playerRoot.Components.GetAll<PortraitRenderer>( FindMode.EverythingInSelfAndDescendants ).FirstOrDefault();
if ( portraitRenderer != null && portraitRenderer.RenderedTexture != null )
{
PortraitContainer.Style.SetBackgroundImage( portraitRenderer.RenderedTexture );
PortraitContainer.Style.Dirty();
PortraitContainer.StateHasChanged();
}
}
}
ShowControls = Input.Down( "Score" );
}
protected override void OnDestroy()
{
}
private void FindOrCreateManager()
{
Manager = Scene.GetAllComponents<GameManager>().FirstOrDefault();
if ( !Manager.IsValid() && Scene.IsValid() )
{
Log.Info( "HUD: GameManager not found in scene. Automatically creating fallback GameManager..." );
var go = Scene.CreateObject();
go.Name = "GameManager (Auto)";
Manager = go.Components.Create<GameManager>();
}
}
private void FindLocalPlayer()
{
var localPlayer = Scene.GetAllComponents<PlayerController>().FirstOrDefault( x => !x.IsProxy );
if ( localPlayer.IsValid() )
{
LocalPlayerTeam = localPlayer.GameObject.Components.Get<PlayerTeam>() ?? localPlayer.GameObject.Components.GetInDescendants<PlayerTeam>();
LocalPlayerGun = localPlayer.GameObject.Components.Get<PaintGun>() ?? localPlayer.GameObject.Components.GetInDescendants<PaintGun>();
}
}
// Форматування таймера (наприклад, 120 -> "02:00")
private string FormattedTime
{
get
{
if ( !Manager.IsValid() ) return "00:00";
int totalSeconds = (int)MathF.Max( 0f, Manager.TimeRemaining );
int minutes = totalSeconds / 60;
int seconds = totalSeconds % 60;
return $"{minutes:D2}:{seconds:D2}";
}
}
// Розрахунок відсотка покриття для кожної з команд
private float BluePercent
{
get
{
if ( !Manager.IsValid() ) return 0f;
int capacity = Manager.TargetPaintCapacity > 0 ? Manager.TargetPaintCapacity : 300;
return MathF.Min( 100f, ( Manager.BluePaintCount / (float)capacity ) * 100f );
}
}
private float RedPercent
{
get
{
if ( !Manager.IsValid() ) return 0f;
int capacity = Manager.TargetPaintCapacity > 0 ? Manager.TargetPaintCapacity : 300;
return MathF.Min( 100f, ( Manager.RedPaintCount / (float)capacity ) * 100f );
}
}
// Оновлення інтерфейсу тільки при зміні значень
protected override int BuildHash()
{
if ( !Manager.IsValid() ) return 0;
var localDead = LocalPlayerTeam.IsValid() && LocalPlayerTeam.IsDead;
var localTimeLeft = LocalPlayerTeam.IsValid() ? (int)MathF.Ceiling(MathF.Max(0f, 5f - LocalPlayerTeam.TimeSinceDeath)) : 0;
var hash = new System.HashCode();
hash.Add( Manager.TimeRemaining );
hash.Add( Manager.IsRoundActive );
hash.Add( Manager.WinnerTeamId );
hash.Add( Manager.BluePaintCount );
hash.Add( Manager.RedPaintCount );
hash.Add( localDead );
hash.Add( localTimeLeft );
hash.Add( LocalPlayerGun.IsValid() ? LocalPlayerGun.AmmoInMagazine : 0 );
hash.Add( LocalPlayerGun.IsValid() ? LocalPlayerGun.AmmoReserve : 0 );
hash.Add( LocalPlayerGun.IsValid() ? LocalPlayerGun.IsReloading : false );
hash.Add( LocalPlayerTeam.IsValid() ? LocalPlayerTeam.Health : 0 );
hash.Add( ShowControls );
return hash.ToHashCode();
}
}