A Razor UI component for the in-game information panel. It renders XP, dash, HP, and armor displays using Player properties and UI stats, includes conditional visuals for curses and temporary dashes, and computes a BuildHash to drive rerendering.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("InfoPanel.razor.scss")]
<root>
@{
// todo: split each ui line into its own component
var xp_percent = Player.ExperienceCurrent / (float)Player.ExperienceRequired;
var xp_transition_enabled = Player.GetUiStat( PlayerStat.DisableXpBarTransition ) == 0f;
var hp_percent = Math.Clamp(Math.Max(Player.Health, 0f) / Player.GetSyncStat(PlayerStat.MaxHp), 0f, 1f);
// var hp_regen = Player.Stats[PlayerStat.HealthRegen] + (Player.IsMoving ? 0f : Player.Stats[PlayerStat.HealthRegenStill]);
var hp_regen = Player.GetHpRegenAmount(forDisplay: true);
var hpBarHidden = Player.GetUiStat( PlayerStat.HpBarHidden ) > 0f;
}
<div class="info_bar">
<div class="bar_container" style=@("mask-image: url(\"/textures/ui/panel/info_panel_xp_mask.png\");")>
<div class="info_bar_overlay @(xp_transition_enabled ? "xp_transition_on" : "")" style="width:@(xp_percent * 100f)%; background-color: #8888ff77;"></div>
@if(Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses && Player.AvailableCurseCount > 0)
{
var cursePercent = Player.IsBeingShownCurseChoices ? 1f : Utils.Map(Player.CurrLevelsUntilCurseChoice, Player.NumLevelsBetweenCurseChoices, 0, 0f, 1f);
var colorSpeed = Player.IsBeingShownCurseChoices ? 15f : Utils.Map(Player.CurrLevelsUntilCurseChoice, Player.NumLevelsBetweenCurseChoices, 0, 3f, 15f, EasingType.SineIn);
var curseColor = Color.Lerp(new Color(0.2f, 0.1f, 0.4f, 0.5f), new Color(0.8f, 0.1f, 0.9f, 0.25f), Utils.Map(Utils.FastSin(RealTime.Now * colorSpeed), -1f, 1f, 0f, 1f));
<div class="info_bar_overlay" style="width:@(cursePercent * 100f)%; background-color: @curseColor.Rgba;"></div>
}
</div>
<div class="info_bar_label">XP</div>
<div class="data_container">
<div class="data" style="opacity: 0.3;">@($"LVL {Player.Level}")</div>
</div>
</div>
@{
var regularMaxDashes = (int)MathF.Round( Player.GetUiStat( PlayerStat.NumDashes ) );
var maxDashes = regularMaxDashes + Player.NumTempDashesAvailable;
var totalAvailable = Player.NumDashesAvailable + Player.NumTempDashesAvailable;
}
@if( maxDashes < 9 )
{
<div class="info_dash_container" style="gap: @(Utils.Map(maxDashes, 1, 8, 8, 4, EasingType.QuadIn))px;">
<div class="dash_bar_container")>
@for(int i = 0; i < maxDashes; i++)
{
// Bars i < regularMaxDashes are regular dashes; i >= regularMaxDashes are temporary (from PerkDashTemporary).
// Temp bars are always shown to the right and colored purple.
var isTemp = i >= regularMaxDashes;
float progress;
bool isRecharging;
if ( isTemp )
{
// Temp dashes are either fully available or gone — no partial recharge animation.
int tempIndex = i - regularMaxDashes;
progress = tempIndex < Player.NumTempDashesAvailable ? 1f : 0f;
isRecharging = false;
}
else
{
// The bar at NumDashesAvailable is the one currently recharging (partial fill).
// Bars below it are full; bars above it are empty.
progress = i == Player.NumDashesAvailable ? Player.DashRechargeProgress : (i < Player.NumDashesAvailable ? 1f : 0f);
isRecharging = i >= Player.NumDashesAvailable;
}
var filledColor = isTemp ? "#b34dff88" : "#00ff0088";
var rechargingColor = isTemp ? "#b34dff44" : "#00ff4444";
<div class="info_bar" style="background-color: #00000077;">
<div class="info_bar_overlay" style="width:@(progress * 100f)%; background-color: @(isRecharging ? rechargingColor : filledColor);"></div>
</div>
}
</div>
@if(maxDashes <= 0)
{
<div class="info_bar">
</div>
}
<div class="info_bar_label" style="left: 8px;">
@("DASH")
</div>
</div>
}
else
{
var dashPercent = (float)totalAvailable / (float)maxDashes;
<div class="info_bar">
<div class="info_bar_overlay" style="width:@(dashPercent * 100f)%; background-color: #00ff0055; transition: all 0.2s ease-in-out;"></div>
<div class="info_bar_label">DASH</div>
<div class="data_container">
<div class="data">@maxDashes</div>
<div class="data">/</div>
<div class="data">@totalAvailable</div>
</div>
</div>
}
<div class="info_bar">
<div class="bar_container" style=@("mask-image: url(\"/textures/ui/panel/info_panel_hp_mask.png\");")>
@if ( hpBarHidden )
{
<div class="info_bar_overlay" style="width: 100%; background-color: #7a0040ff;"></div>
}
else
{
<div class="info_bar_overlay" style="width:@(hp_percent * 100f)%; background-color: #ffffffff; transition: all 0.4s ease-in-out "></div>
<div class="info_bar_overlay" style="width:@(hp_percent * 100f)%; background-color: #ff0000ff; transition: all 0.2s ease-in-out;"></div>
}
</div>
<div class="info_bar_label">HP</div>
<div class="data_container">
<div class="data">@($"{(int)MathF.Round(Player.GetSyncStat(PlayerStat.MaxHp))}")</div>
<div class="data">/</div>
@if ( hpBarHidden )
{
<div class="data">?</div>
}
else
{
<div class="data">@($"{(Math.Max(Player.Health, 0f) > 0f && Math.Max(Player.Health, 0f) < 1f ? (int)Math.Ceiling(Math.Max(Player.Health, 0f)) : (int)Math.Round(Math.Max(Player.Health, 0f)))}")</div>
@if (Math.Abs(hp_regen) > 0f)
{
<div class="data" style="color:@((hp_regen > 0f ? new Color(0f, 1f, 0f) : new Color(1f, 0f, 0f)).Rgba); letter-spacing: 2px; padding-right: 10px;">@($"{(hp_regen > 0f ? "+" : "")}{hp_regen.ToString("0.##")}")</div>
}
}
</div>
</div>
@if (Player.Armor > 0)
{
<div class="armor_icon" style="transform: scale(@(Utils.Map(Player.RealTimeSinceArmorChanged, 0f, 0.5f, 1.35f, 1f, EasingType.QuadOut)));">
<label class="armor_text">@($"{MathX.CeilToInt(Player.Armor)}")</label>
</div>
}
</root>
@code
{
public Player Player { get; set; }
protected override int BuildHash()
{
if( Manager.Instance.Difficulty >= Manager.Instance.FirstDifficultyWithCurses )
{
return HashCode.Combine(RealTime.Now);
}
var hpBarHidden = Player.GetUiStat( PlayerStat.HpBarHidden ) > 0f;
var hpHash = HashCode.Combine(
Player.Health,
Player.GetSyncStat(PlayerStat.MaxHp),
Player.GetSyncStat( PlayerStat.HealthRegen ),
Player.GetSyncStat( PlayerStat.HealthRegenStill ),
Player.IsMoving,
Player.GetHpRegenAmount( forDisplay: true ),
hpBarHidden
);
var armorHash = HashCode.Combine(
Player.RealTimeSinceArmorChanged > 0.5f ? 0f : Player.RealTimeSinceArmorChanged.Relative,
Player.Armor
);
var xpHash = HashCode.Combine(
Player.Level,
Player.ExperienceCurrent,
Player.ExperienceRequired
);
return HashCode.Combine(
Player.DashRechargeProgress,
Player.NumDashesAvailable,
Player.NumTempDashesAvailable,
Player.GetUiStat( PlayerStat.NumDashes ),
hpHash,
armorHash,
xpHash
);
}
}