A Blazor-style Sandbox UI panel (PerkChoice) that renders a perk card with icon, title, description, rarity, cost and interactive hover/slot behavior. It reads player state and perk metadata to compute visuals, hover state, and a build hash for UI invalidation.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("PerkChoice.razor.scss")]
@{
var player = ViewedPlayer.IsValid() ? ViewedPlayer : Manager.Instance.LocalPlayer;
var isReadOnlyChoice = IsReadOnlyChoice( player );
var isViewedPlayerChoiceHovered = IsViewedPlayerChoiceHovered( player );
if ( PerkType == null )
{
return;
}
var playerPerkLevel = player.IsValid() ? player.GetDisplayedPerkLevel( PerkType ) : 0;
var perkLevel = IsChoice
? playerPerkLevel + 1
: TooltipLevel;
var attrib = PerkType.GetAttribute<PerkAttribute>();
var rarity = attrib.Rarity;
var curse = attrib.Curse;
CurseWrongPerkInfo.WrongPerkInfo? wrongInfo = null;
if ( IsChoice && !IsUnknown && player.IsValid() )
wrongInfo = player.GetDisplayedPerkChoiceWrongInfo( Slot );
var displayNameType = (wrongInfo?.Aspect == CurseWrongPerkInfo.WrongAspect.Name) ? wrongInfo.Value.WrongType?.TargetType ?? PerkType.TargetType : PerkType.TargetType;
var displayDescType = (wrongInfo?.Aspect == CurseWrongPerkInfo.WrongAspect.Description) ? wrongInfo.Value.WrongType?.TargetType ?? PerkType.TargetType : PerkType.TargetType;
var displayIconType = (wrongInfo?.Aspect == CurseWrongPerkInfo.WrongAspect.Icon) ? wrongInfo.Value.WrongType?.TargetType ?? PerkType.TargetType : PerkType.TargetType;
var parentRotAngle = OnlyShowIcon && player.IsValid()
? -6f + ((player.Level + player.NumPerkPointsAvailable + player.NumRerollAvailable + Slot * 3) % 6) * 2f
: 0f;
var iconRotAngle = (OnlyShowIcon || !player.IsValid())
? 0f
: Utils.Map( (player.PerkRandomRotationSeed + player.NumRerollAvailable + Slot * 3) % 6, 0, 5, 3f, 9f, EasingType.SineOut );
if ( player.IsValid() && (player.PerkRandomRotationSeed + player.NumRerollAvailable + Slot) % 4 != 0 )
iconRotAngle *= -1f;
var curseRarityHpCostType = TypeLibrary.GetType( typeof( CurseRarityHpCost ) );
bool hasCurseRarityHpCost = IsChoice && !curse && player.IsValid() && player.HasDisplayedPerk( curseRarityHpCostType );
int hpCost = hasCurseRarityHpCost ? CurseRarityHpCost.GetHpCostForRarity( rarity ) : 0;
var displayTitle = IsChoice
? player.GetDisplayedPerkChoiceName( Slot, TypeLibrary.GetType( displayNameType ), IsUnknown )
: (Perk.GetName( displayNameType ) ?? "");
var displayDescription = IsChoice
? player.GetDisplayedPerkChoiceDescription( Slot, TypeLibrary.GetType( displayDescType ), perkLevel, isChoice: true, IsUnknown, rarity )
: (ShowUpgradeDescription && perkLevel > 1 ? Perk.GetUpgradeDescription( displayDescType, perkLevel ) : null)
?? Perk.GetDescription( displayDescType, perkLevel ) ?? "";
var iconPathOverride = IsChoice
? player.GetDisplayedPerkChoiceIconPath( Slot, TypeLibrary.GetType( displayIconType ), IsUnknown )
: null;
}
<root class="@(isReadOnlyChoice ? "spectator-readonly" : "") @(isViewedPlayerChoiceHovered ? "remote-hovered" : "")" style="transform: rotate(@(parentRotAngle)deg) scale(@(player.IsValid() && player.IsBanishMode ? 0.96 : 1)); @(OnlyShowIcon ? "width:64px; height:64px;" : "width:250px; height: 130px;") ">
@if ( !OnlyShowIcon )
{
<div class="bg_color" style="background-color:@RarityColorFade( PerkManager.GetCardRarityColor( rarity, curse ) ).Rgba;"> </div>
<div class="frame"> </div>
}
<div class="top_row" style="@(OnlyShowIcon ? "" : "pointer-events: none;")">
<PerkIconStatic style="transform: rotate(@(OnlyShowIcon ? 0 : iconRotAngle)deg) translateY(@(OnlyShowIcon ? 0 : -12)px) translateX(@(iconRotAngle <= 0f ? 0 : Utils.Map(iconRotAngle, 3f, 8f, -2f, -6f))px);" PerkType=@PerkType IconOverrideType=@(wrongInfo?.Aspect == CurseWrongPerkInfo.WrongAspect.Icon ? wrongInfo.Value.WrongType : null) IconPathOverride=@iconPathOverride Level=@perkLevel ProgressLevel=@perkLevel NoTips=@(!OnlyShowIcon) IsChoice=@IsChoice IsUnknown=@IsUnknown />
@if ( !OnlyShowIcon )
{
<div class="top_text">
<label class="title_label">@displayTitle</label>
<label class="rarity_label" style="color:@PerkManager.GetFontRarityColor( rarity, curse ).Rgba;">@PerkManager.GetCardRarityName( rarity, curse )</label>
</div>
}
</div>
@if ( !OnlyShowIcon )
{
var lineHeight = Perk.GetDescriptionLineHeight( displayDescType );
var fontSize = Perk.GetDescriptionFontSize( displayDescType );
var imageSize = Perk.GetDescriptionImageSize( displayDescType );
var descriptionStyle = "";
if ( lineHeight.HasValue ) descriptionStyle += $"line-height: {lineHeight.Value}px; ";
if ( fontSize.HasValue ) descriptionStyle += $"font-size: {fontSize.Value}px;";
<div class="middle">
<RichText class="description" style="@descriptionStyle" Text=@displayDescription ImageSize=@imageSize />
</div>
@if ( hpCost > 0 )
{
<div class="hp_cost_label_container">
<label class="hp_cost_label">-@(hpCost) hp</label>
</div>
}
@if ( InfoRows != null && InfoRows.Count > 0 )
{
<div class="info_rows_container">
@foreach ( var row in InfoRows )
{
<div class="info_row"><label>@row</label></div>
}
</div>
}
}
@if ( player.IsValid() && player.IsBanishMode && TooltipLevel == 0 )
{
float fontSize = MathX.FloorToInt( 200f + Utils.FastSin( RealTime.Now * 8f ) * 8f ) * (OnlyShowIcon ? 0.6f : 1f);
<div class="banish_x" style="font-size: @(fontSize)px;">X</div>
}
@if ( Slot >= 0 && Slot <= 8 )
{
<InputHint class="inputbutton @(OnlyShowIcon ? "icon-only" : "")" Button=@($"Slot{(Slot + 1)}") />
}
else if ( TooltipLevel > 0 && Slot == -1 )
{
<div class="level_indicator" style="color:@PerkManager.GetFontRarityColor( rarity, curse ).Rgba;">
<label>@TooltipLevel</label><label style="opacity: 0.4;">/</label><label>@PerkManager.GetMaxLevelForRarity( rarity )</label>
</div>
}
@if ( player.IsValid() && player.RealTimeSinceBanishChoice < 0.5f && Slot == player.BanishChoiceIndex )
{
<panel class="banish_overlay" style="opacity:@(Utils.Map( player.RealTimeSinceBanishChoice, 0f, 0.5f, 1f, 0f, EasingType.SineOut ));"></panel>
}
</root>
@code {
public Player ViewedPlayer { get; set; }
public TypeDescription PerkType { get; set; }
public int Slot { get; set; }
public bool OnlyShowIcon { get; set; }
public bool IsUnknown { get; set; }
public bool IsChoice { get; set; }
public int TooltipLevel { get; set; }
public bool ShowUpgradeDescription { get; set; }
public List<string> InfoRows { get; set; }
private bool IsReadOnlyChoice( Player player )
{
return IsChoice && player.IsValid() && player != Manager.Instance.LocalPlayer;
}
private bool IsViewedPlayerChoiceHovered( Player player )
{
return IsChoice && player.IsValid() && player.HoveredPerkChoiceSlot == Slot;
}
protected override void OnMouseOver( MousePanelEvent e )
{
base.OnMouseOver( e );
var player = ViewedPlayer.IsValid() ? ViewedPlayer : Manager.Instance.LocalPlayer;
var isReadOnlyChoice = IsReadOnlyChoice( player );
if ( IsChoice && player.IsValid() && !isReadOnlyChoice )
player.HoveredPerkChoiceSlot = Slot;
if ( !OnlyShowIcon || PerkType == null || IsUnknown )
return;
var playerPerkLevel = player.IsValid() ? player.GetDisplayedPerkLevel( PerkType ) : 0;
var perkLevel = IsChoice ? playerPerkLevel + 1 : TooltipLevel;
Manager.Instance.HoveredPerkType = PerkType;
Manager.Instance.HoveredPerkPanel = this;
Manager.Instance.HoveredPerkLevel = perkLevel;
Manager.Instance.HoveredPerkViewedPlayer = ViewedPlayer.IsValid() ? ViewedPlayer : null;
Manager.Instance.HoveredPerkChoiceSlot = IsChoice ? Slot : -1;
Manager.Instance.IsHoveredPerkBanished = false;
Manager.Instance.IsHoveredPerkAChoice = IsChoice;
Manager.Instance.IsHoveredPerkHidden = false;
}
protected override void OnMouseOut( MousePanelEvent e )
{
base.OnMouseOut( e );
var player = ViewedPlayer.IsValid() ? ViewedPlayer : Manager.Instance.LocalPlayer;
if ( IsChoice && player.IsValid() && !IsReadOnlyChoice( player ) && player.HoveredPerkChoiceSlot == Slot )
player.HoveredPerkChoiceSlot = -1;
if ( Manager.Instance.HoveredPerkPanel != this )
return;
Manager.Instance.HoveredPerkType = null;
Manager.Instance.HoveredPerkPanel = null;
Manager.Instance.HoveredPerkViewedPlayer = null;
Manager.Instance.HoveredPerkChoiceSlot = -1;
Manager.Instance.IsHoveredPerkBanished = false;
Manager.Instance.IsHoveredPerkAChoice = false;
Manager.Instance.IsHoveredPerkHidden = false;
}
public override void OnDeleted()
{
base.OnDeleted();
var player = ViewedPlayer.IsValid() ? ViewedPlayer : Manager.Instance.LocalPlayer;
if ( IsChoice && player.IsValid() && !IsReadOnlyChoice( player ) && player.HoveredPerkChoiceSlot == Slot )
player.HoveredPerkChoiceSlot = -1;
if ( Manager.Instance.HoveredPerkPanel != this )
return;
Manager.Instance.HoveredPerkType = null;
Manager.Instance.HoveredPerkPanel = null;
Manager.Instance.HoveredPerkViewedPlayer = null;
Manager.Instance.HoveredPerkChoiceSlot = -1;
Manager.Instance.IsHoveredPerkBanished = false;
Manager.Instance.IsHoveredPerkAChoice = false;
Manager.Instance.IsHoveredPerkHidden = false;
}
protected override int BuildHash()
{
var player = ViewedPlayer.IsValid() ? ViewedPlayer : Manager.Instance.LocalPlayer;
if ( !player.IsValid() )
return HashCode.Combine( OnlyShowIcon );
var banishHash = player.RealTimeSinceBanishChoice.Relative < 0.5f ? player.RealTimeSinceBanishChoice.Relative : 0f;
var modeHash = player.IsBanishMode ? RealTime.Now : 0f;
var hasCurseRarityHpCostHash = player.HasDisplayedPerk( TypeLibrary.GetType( typeof( CurseRarityHpCost ) ) );
var wrongInfo = player.GetDisplayedPerkChoiceWrongInfo( Slot );
return HashCode.Combine(
PerkType?.Identity ?? -1,
OnlyShowIcon,
IsUnknown,
IsChoice,
TooltipLevel,
HashCode.Combine(
player.HoveredPerkChoiceSlot == Slot,
wrongInfo?.WrongType?.Identity ?? -1,
wrongInfo?.Aspect ?? (CurseWrongPerkInfo.WrongAspect)(-1),
banishHash,
modeHash,
hasCurseRarityHpCostHash,
player.PerkChoiceHash
)
);
}
private Color RarityColorFade( Color color )
{
return Color.Lerp( Color.Black, color, 0.5f ).WithAlpha( 0.95f );
}
}