ui/Hud.razor

Razor UI component for the game's HUD. It composes many child UI panels (spectator list, info, perks, timers, menus, tooltips) and controls cursor visibility, perk/shop/tooltips placement, dash reminder logic, and a custom build-hash to decide when to re-render.

Http CallsFile Access
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits PanelComponent

@{
	var manager = Manager.Instance;
	var player = manager.LocalPlayer;
	var infoPlayer = manager.IsSpectator && manager.SelectedPlayer.IsValid()
		? manager.SelectedPlayer
		: player;
	// Show the system cursor in any interactive or spectator state.
	var showCursor =
		manager.GameState == GameState.Lobby ||
		manager.IsSpectator ||
		manager.IsPaused ||
		manager.IsHoveringPerkChoicePanel ||
		manager.IsHoveringStatsTab ||
		manager.IsEscMenuOpen ||
		manager.IsOptionsMenuOpen ||
		manager.ShouldShowGameOverScreen ||
		manager.ShouldShowPerkUnlockProgressPanel ||
		manager.ShouldShowQuestProgressPanel ||
		manager.HoveredPerkType != null;

}

<root class="@(Input.UsingController ? "" : "cursor")">

	<SpectatorListPanel />
	@if( manager.GameState == GameState.Lobby )
	{
		<LobbyMenu />

		@if (manager.IsOptionsMenuOpen)
		{
			<OptionsMenu />
		}
	}
	else
	{
		@if ( manager.IsSpectator )
		{
			<SpectatorPanel />
		}

		if(infoPlayer.IsValid())
		{
			<InfoPanel Player=@infoPlayer />
			@if ( manager.DashReminderActive )
			{
				var sp = manager.DashReminderScreenPos;
				var spRel = sp / Screen.Size;
				var panelStyle = $"position: absolute; left: {Length.Fraction(spRel.x)}; top: {Length.Fraction(spRel.y + 40f / Screen.Height)};";
				<DashReminderPanel OnComplete=@(() => {
					manager.DashReminderActive = false;
				}) style=@panelStyle />
			}
			@if ( infoPlayer.IsValid() )
			{
				<BanishedPerks />
			}

			@if(player.IsValid() && player.ShouldShowStats)
			{
				<StatsScreen/>
			}
			else if ( player.IsValid() )
			{
				<StatsTab/>
			}
		}

		<TimerPanel />
		<PlayerList />
		<PlayerPerks />

		@if (manager.IsPaused)
		{
			<PausedOverlay />
		}
		else if (!manager.IsGameOver && !manager.IsEscMenuOpen)
		{
			// hack to make perk choice panel refresh size when onlyShowIcons changing value
			if(!(Manager.Instance.SkipShowingChoicesFrames > 0))
			{
				<PerkChoicePanel />
			}
		}

		@if (manager.ShouldShowPerkUnlockProgressPanel || manager.ShouldShowQuestProgressPanel || manager.ShouldShowGameOverScreen)
		{
			<div style="position: absolute; top: 12px; left: 16px; flex-direction: row; align-items: center; gap: 6px; pointer-events: none; z-index: 10;">
				<div style="width: 28px; height: 28px; background-image: url(/textures/ui/coin.png); background-size: contain; background-repeat: no-repeat; background-position: center;"></div>
				<label style="font-size: 24px; font-family: Cal Sans; color: rgba(255, 220, 80, 0.95); text-shadow: 2px 2px 0px black;">@ProgressManager.GetCoins()</label>
			</div>
		}

		@if (manager.ShouldShowPerkUnlockProgressPanel)
		{
			<PerkUnlockProgressPanel />
		}
		else if (manager.ShouldShowQuestProgressPanel)
		{
			<QuestPanel RunProgressMode=@true />
		}
		else if (manager.ShouldShowGameOverScreen)
		{
			<GameOverScreen />
		}
		else if (manager.IsOptionsMenuOpen)
		{
			<OptionsMenu />
		}
		else if( manager.IsEscMenuOpen )
		{
			<EscMenu />
		}

		@if (manager.ShowBossHealthbar)
		{
			<BossNametag [email protected] />
		}

		@if( Game.IsEditor )
		{
			@* <DebugPanel />
			@if(manager.ShowAllPerks)
			{
				<DebugAllPerksPanel />
			} *@
		}
	}

	@if(manager.HoveredPerkType != null)
	{
		var isChoice = manager.IsHoveredPerkAChoice;
		var hoveredChoicePlayer = manager.HoveredPerkViewedPlayer;
		var hoveredChoiceSlot = isChoice ? manager.HoveredPerkChoiceSlot : -1;
		var hoveredChoiceUnknown = isChoice && hoveredChoicePlayer.IsValid() && hoveredChoiceSlot >= 0
			? hoveredChoicePlayer.GetDisplayedPerkChoiceUnknown( hoveredChoiceSlot )
			: false;
		var tooltipOffsetScale = Math.Clamp( Math.Min( Screen.Width / 1920f, Screen.Height / 1080f ), 0.45f, 1f );
		var choiceMouseOffset = new Vector2( 10f, -160f ) * tooltipOffsetScale;
		var tooltipMousePadding = new Vector2( 20f, 20f ) * tooltipOffsetScale;

		var mousePos = Mouse.Position;
		if(isChoice)
			mousePos += choiceMouseOffset;

		var mousePosRelative = mousePos / Screen.Size;

		string tooltipStyle = "position: absolute; pointer-events: none; transition: all 0s; z-index: 9999;";

		if(mousePos.x > Screen.Width * 0.75f && !manager.ShowPerkUnlockPanel)
		{
			tooltipStyle += $" right:{Length.Fraction(1f - mousePosRelative.x + tooltipMousePadding.x / Screen.Width)};";
		}
		else
		{
			tooltipStyle += $" left:{Length.Fraction(mousePosRelative.x + tooltipMousePadding.x / Screen.Width)};";
		}

		tooltipStyle += $" top: {Length.Fraction(mousePosRelative.y + tooltipMousePadding.y / Screen.Height)};";

		List<string> perkInfoRows = null;
		if ( manager.ShowPerkUnlockPanel )
		{
			var hoveredAttrib = manager.HoveredPerkType.GetAttribute<PerkAttribute>();
			perkInfoRows = new List<string>();
			if ( hoveredAttrib.MultiplayerMode == MultiplayerMode.OnlyMultiplayer )
				perkInfoRows.Add( "Multiplayer only" );
			if ( hoveredAttrib.OnlyUnpausedChoosing )
				perkInfoRows.Add( "Unpaused choosing only" );
			if ( perkInfoRows.Count == 0 ) perkInfoRows = null;
		}

		var showUpgradeDescription = manager.ShowPerkUnlockPanel || (!isChoice && manager.HoveredPerkPanel == null);

		<PerkChoice style=@(tooltipStyle) ViewedPlayer=@hoveredChoicePlayer [email protected] [email protected] IsChoice=@isChoice Slot=@hoveredChoiceSlot IsUnknown=@hoveredChoiceUnknown InfoRows=@perkInfoRows ShowUpgradeDescription=@showUpgradeDescription />
	}

	@if ( manager.HoveredShopItem.HasValue )
	{
		var mousePos = Mouse.Position;
		var mousePosRelative = mousePos / Screen.Size;

		string itemTooltipStyle = "position: absolute; pointer-events: none; transition: all 0s; z-index: 9999;";
		if ( mousePos.x > Screen.Width * 0.75f )
			itemTooltipStyle += $" right:{Length.Fraction(1f - mousePosRelative.x + 20f / Screen.Width)};";
		else
			itemTooltipStyle += $" left:{Length.Fraction(mousePosRelative.x + 20f / Screen.Width)};";
		itemTooltipStyle += $" top: {Length.Fraction(mousePosRelative.y + 20f / Screen.Height)};";

		<LoadoutItemPanel style=@(itemTooltipStyle) [email protected] IsShopMode=@false />
	}

	else if(manager.GameState == GameState.Playing)
	{
		if(manager.HoveredPlayer.IsValid())
		{
			<PlayerTooltip [email protected] ShowIcon=@true />
		}
		else if(manager.HoveredPlayerIcon.IsValid())
		{
			<PlayerTooltip [email protected] ShowIcon=@false />
		}
	}


</root>

@code
{
	protected override int BuildHash()
	{
		var manager = Manager.Instance;
		var player = manager.LocalPlayer;
		var playerHash = player.IsValid() ? player.ShouldShowStats : false;

		var isChoosing = player.IsValid() && player.IsChoosingLevelUpReward;
		var menuHash = System.HashCode.Combine(
			manager.IsPaused,
			manager.IsEscMenuOpen,
			manager.IsOptionsMenuOpen,
			manager.ShouldShowGameOverScreen,
			manager.ShouldShowQuestProgressPanel,
			manager.ShouldShowPerkUnlockProgressPanel
		);

		var hash1 = System.HashCode.Combine(
			menuHash,
			manager.GameState,
			manager.IsGameOver,
			manager.SkipShowingChoicesFrames,
			isChoosing,
			manager.IsHoveringPerkChoicePanel
		);

		var tooltipHash = (manager.HoveredPerkType != null || manager.HoveredShopItem.HasValue) ? Mouse.Position.GetHashCode() : 0;

		var hash2 = System.HashCode.Combine(
			player.IsValid(),
			manager.IsSpectator,
			manager.SelectedPlayer,
			playerHash,
			manager.HoveredPlayer,
			manager.HoveredPlayerIcon,
			manager.HoveredPerkType,
			System.HashCode.Combine(
				manager.HoveredPerkLevel,
				manager.IsHoveredPerkAChoice,
				manager.HoveredPerkViewedPlayer,
				manager.HoveredPerkChoiceSlot,
				manager.HoveredShopItem?.Id,
				manager.ShowBossHealthbar,
				tooltipHash
			)
		);

		return System.HashCode.Combine(
			hash1,
			hash2,
			Input.UsingController,
			manager.IsHoveringStatsTab,
			ProgressManager.StateVersion,
			manager.DashReminderActive,
			manager.DashReminderScreenPos.GetHashCode()
		);
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		var player = Manager.Instance.LocalPlayer;
		if ( player.IsValid() )
		{
			var isInLobby = player.IsInLobby;
			var isDead = player.IsDead;
			var m2 = Manager.Instance;

			if ( isDead )
				m2.DashReminderIdleTime = 0f;
			else
				m2.DashReminderIdleTime += Time.Delta;

			// Hide if dead
			if ( isDead && m2.DashReminderActive )
				m2.DashReminderActive = false;

			// If the player has ever dashed, permanently suppress the reminder
			if ( player.HasEverDashed && m2.DashReminderActive )
				m2.DashReminderActive = false;

			// Show once per run, never if they've dashed this session
			if ( !player.HasEverDashed && !m2.DashReminderShownThisRun && !m2.DashReminderActive )
			{
				if ( m2.GameState == GameState.Playing &&
					!isInLobby &&
					!isDead &&
					m2.DashReminderIdleTime > 90f &&
					(player.NumDashesAvailable > 0 || player.NumTempDashesAvailable > 0) )
				{
					m2.DashReminderShownThisRun = true;
					m2.DashReminderActive = true;
				}
			}

			if ( m2.DashReminderActive )
			{
				var playerScreenPos = Scene.Camera.PointToScreenPixels( player.WorldPosition );
				m2.DashReminderScreenPos = playerScreenPos;
			}
		}

		if ( !Input.UsingController )
		{
			var m = Manager.Instance;
			var p = m.LocalPlayer;
			var hideCursor =
				m.GameState != GameState.Lobby &&
				!m.IsSpectator &&
				!m.IsPaused &&
				!m.IsHoveringPerkChoicePanel &&
				!m.IsHoveringStatsTab &&
				!m.IsEscMenuOpen &&
				!m.IsOptionsMenuOpen &&
				!m.ShouldShowGameOverScreen &&
				!m.ShouldShowQuestProgressPanel &&
				(m.HoveredPerkType == null || m.IsHoveredPerkFromWorldItem);

			Mouse.CursorType = hideCursor ? "none" : "";
		}

		if ( Input.EscapePressed || (Input.UsingController && Input.Pressed("Back") ))
		{
			if ( Manager.Instance.PlayerProfileToShow is not null)
			{
				Manager.Instance.PlayerProfileToShow = null;
				Input.EscapePressed = false;
			}
			else if ( Manager.Instance.RunEntryToShow is not null)
			{
				Manager.Instance.RunEntryToShow = null;
				Input.EscapePressed = false;
			}
		}
	}
}