A Blazor-style Razor UI component for displaying a shop/loadout item card. It computes visual state (locked, affordable, owned, gem upgrade states), displays icon, price/upgrade cost, descriptions and gem level, and exposes Item and state properties plus an OnClick action.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@inherits Panel
@attribute [StyleSheet("LoadoutItemPanel.razor.scss")]
@{
var isGem = Item.Category == ShopItemCategory.Gem;
var owned = ProgressManager.IsItemOwned( Item.Id );
var coins = ProgressManager.GetCoins();
var canAfford = coins >= Item.Price;
var isLocked = !ProgressManager.IsItemUnlocked( Item.Id );
string stateClass;
if ( IsShopMode )
{
if ( isLocked )
stateClass = "locked";
else if ( isGem )
{
if ( !owned )
stateClass = canAfford ? "affordable" : "too_expensive";
else if ( ProgressManager.IsGemMaxed( Item.Id ) )
stateClass = "gem_maxed";
else
{
var upgCost = ProgressManager.GetGemUpgradeCost( Item.Id );
stateClass = (upgCost.HasValue && coins >= upgCost) ? "gem_upgradeable" : "gem_upgrade_expensive";
}
}
else
stateClass = owned ? "owned" : canAfford ? "affordable" : "too_expensive";
}
else
{
stateClass = IsSelected ? "selected" : IsSlotFull ? "slot_full" : "";
}
// Compute description text
string descText = null;
if ( isGem && (Item.GemDescription != null || Item.GemUpgradeDescription != null) )
{
var displayLevel = ProgressManager.GetGemDisplayLevel( Item.Id );
if ( !owned )
descText = Item.GemDescription?.Invoke( 1 );
else if ( IsShopMode && !ProgressManager.IsGemMaxed( Item.Id ) )
descText = Item.GemUpgradeDescription?.Invoke( displayLevel + 1 ) ?? Item.GemDescription?.Invoke( displayLevel );
else
descText = Item.GemDescription?.Invoke( displayLevel );
}
else if ( Item.ItemDescription != null )
{
descText = Item.ItemDescription();
}
}
<root class="@stateClass" onclick=@OnClick>
@{
var frameBg = Item.Category switch
{
ShopItemCategory.Gun => "/textures/ui/panel/item_gun.png",
ShopItemCategory.Charm => "/textures/ui/panel/item_charm.png",
ShopItemCategory.Gem => "/textures/ui/panel/item_gem.png",
_ => "/textures/ui/panel/panel_02.png",
};
}
<div class="frame" style="background-image: url(@frameBg);"></div>
<div class="top_row">
<div class="item_icon" style="background-image:url(@Item.IconPath);"></div>
<div class="top_text">
<label class="item_name">@Item.Name</label>
@if ( IsShopMode )
{
<div class="item_price">
@if ( isGem && owned )
{
var upgCost = ProgressManager.GetGemUpgradeCost( Item.Id );
@if ( upgCost.HasValue )
{
<div class="price_coin_icon"></div>
<label class="price_amount">@upgCost</label>
}
else
{
<label class="owned_label">Max</label>
}
}
else if ( !owned )
{
<div class="price_coin_icon"></div>
<label class="price_amount">@Item.Price</label>
}
</div>
}
</div>
</div>
<div class="middle">
@if ( descText != null )
{
<RichText class="item_desc" Text=@descText />
}
@if ( IsShopMode && isLocked )
{
var needed = ProgressManager.GetPurchasesNeeded( Item.Id );
var catWord = Item.Category == ShopItemCategory.Gun ? (needed == 1 ? "gun" : "guns")
: Item.Category == ShopItemCategory.Charm ? (needed == 1 ? "charm" : "charms")
: (needed == 1 ? "gem" : "gems");
<label class="lock_label">Buy @needed more @catWord</label>
}
</div>
@if ( isGem )
{
var maxLevel = (Item.UpgradePrices?.Length ?? 0) + 1;
var gemDL = ProgressManager.GetGemDisplayLevel( Item.Id );
// In shop mode: show the level being purchased next (matches the "Lv X" price label).
// In loadout mode: show current level.
var displayLevel = !IsShopMode ? gemDL
: !owned ? 1
: ProgressManager.GetGemUpgradeCost( Item.Id ).HasValue ? gemDL + 1 : gemDL;
<div class="level_indicator">
<label>@displayLevel</label><label class="level_sep">/</label><label>@maxLevel</label>
</div>
}
</root>
@code {
public ShopItemDef Item { get; set; }
public Action OnClick { get; set; }
/// <summary>True when used in the shop — shows price and lock info, computes shop state classes.</summary>
public bool IsShopMode { get; set; }
/// <summary>Loadout mode only — highlights the card as the active selection.</summary>
public bool IsSelected { get; set; }
/// <summary>Loadout mode only — dims the card when the slot is full and this item is not selected.</summary>
public bool IsSlotFull { get; set; }
protected override int BuildHash()
{
return System.HashCode.Combine(
Item.Id,
IsShopMode,
IsSelected,
IsSlotFull,
ProgressManager.StateVersion
);
}
}