ui/ShopPanel.razor

A UI razor component for the in-game Shop panel. It renders tabs for Guns, Charms, Gems, shows coin total, lists shop items filtered and ordered by availability/unlocked/upgradeable, and handles buying/upgrading items and some debug actions.

NetworkingFile Access
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@using System;
@using System.Linq;
@inherits Panel
@attribute [StyleSheet("ShopPanel.razor.scss")]

<root>
	<div class="hide_button" onclick=@(() => Close())></div>
	<div class="title_label">Shop</div>

	<div class="coin_header">
		<div class="coin_icon"></div>
		<label class="coin_amount">@ProgressManager.GetCoins()</label>
	</div>

	<div class="debug_buttons">
		<div class="debug_btn" onclick=@(() => DebugClearCategory(ShopItemCategory.Gun))>Clear Guns</div>
		<div class="debug_btn" onclick=@(() => DebugClearCategory(ShopItemCategory.Charm))>Clear Charms</div>
		<div class="debug_btn" onclick=@(() => DebugClearCategory(ShopItemCategory.Gem))>Clear Gems</div>
		<div class="debug_btn" onclick=@(() => ProgressManager.AddCoins(1000))>+1000 Coins</div>
		<div class="debug_btn" onclick=@(() => DebugClearCoins())>Clear Coins</div>
	</div>

	<div class="tabs">
		@for(int t = 0; t < TabNames.Length; t++)
		{
			var tabIndex = t;
			<div class="tab @(_selectedTab == tabIndex ? "active" : "")" onclick=@(() => SelectTab(tabIndex))>
				@TabNames[tabIndex]
			</div>
		}
	</div>

	<div class="entries_container">
		<div class="entries">
			@foreach(var item in GetItemsForTab(_selectedTab))
			{
				var itemCapture = item;
				<LoadoutItemPanel Item=@itemCapture IsShopMode=@true OnClick=@(() => OnBuyItem(itemCapture)) />
			}
		</div>
	</div>
</root>

@code
{
	static readonly string[] TabNames = { "Guns", "Charms", "Gems" };

	// Maps tab index to ShopItemCategory
	static readonly ShopItemCategory[] TabCategories = { ShopItemCategory.Gun, ShopItemCategory.Charm, ShopItemCategory.Gem };

	static int _selectedTab = 0;
	static bool _initialTabSet = false;

	// Cache item lists per tab to avoid re-filtering every frame
	List<List<ShopItemDef>> _tabItems;
	bool _wasVisible = false;

	protected override void OnAfterTreeRender(bool firstTime)
	{
		base.OnAfterTreeRender(firstTime);
		var isVisible = Manager.Instance.ShowShopPanel;
		if(firstTime || (!_wasVisible && isVisible))
			RebuildItemLists();
		if(firstTime && !_initialTabSet)
		{
			_selectedTab = BestStartingTab();
			_initialTabSet = true;
		}
		_wasVisible = isVisible;
	}

	int BestStartingTab()
	{
		for(int i = 0; i < TabCategories.Length; i++)
			if(GetItemsForTab(i).Count > 0)
				return i;
		return 0;
	}

	void RebuildItemLists()
	{
		_tabItems = TabCategories
			.Select( cat =>
			{
				var all = ProgressManager.ShopItems.Where( x => x.Category == cat );

				// Fully done = hide on next open: owned gun/charm, or owned+maxed gem
				bool IsFullyDone(ShopItemDef x) =>
					ProgressManager.IsItemOwned( x.Id ) &&
					(cat != ShopItemCategory.Gem || ProgressManager.IsGemMaxed( x.Id ));

				var remaining = all.Where( x => !IsFullyDone( x ) );

				var available = remaining
					.Where( x => !ProgressManager.IsItemOwned( x.Id ) && ProgressManager.IsItemUnlocked( x.Id ) )
					.OrderBy( x => x.RequiredPurchases )
					.ThenBy( x => x.Price )
					.ToList();

				var locked = remaining
					.Where( x => !ProgressManager.IsItemOwned( x.Id ) && !ProgressManager.IsItemUnlocked( x.Id ) )
					.OrderBy( x => ProgressManager.GetPurchasesNeeded( x.Id ) )
					.ToList();

				// Gems owned but not yet maxed — still upgradeable
				var upgradeable = remaining
					.Where( x => ProgressManager.IsItemOwned( x.Id ) )
					.ToList();

				return available.Concat( locked ).Concat( upgradeable ).ToList();
			} )
			.ToList();
	}

	List<ShopItemDef> GetItemsForTab(int tab)
	{
		if(_tabItems == null || tab < 0 || tab >= _tabItems.Count)
			return new List<ShopItemDef>();
		return _tabItems[tab];
	}

	void OnBuyItem(ShopItemDef item)
	{
		if ( !ProgressManager.IsItemUnlocked( item.Id ) ) return;

		if(item.Category == ShopItemCategory.Gem && ProgressManager.IsItemOwned(item.Id))
		{
			// Owned gem: try to upgrade instead of buy
			if(ProgressManager.UpgradeGem(item.Id))
			{
				Log.Info($"Upgraded {item.Name} to level {ProgressManager.GetGemLevel(item.Id) + 1}");
				StateHasChanged();
			}
			return;
		}

		if(ProgressManager.IsItemOwned(item.Id)) return;
		if(ProgressManager.GetCoins() < item.Price) return;

		if(ProgressManager.BuyItem(item.Id))
		{
			Log.Info($"Purchased {item.Name} for {item.Price} coins");
			StateHasChanged();
		}
	}

	void SelectTab(int tabIndex)
	{
		if(_selectedTab == tabIndex) return;
		_selectedTab = tabIndex;
		StateHasChanged();
	}

	void DebugClearCoins()
	{
		ProgressManager.AddCoins( -ProgressManager.GetCoins() );
		StateHasChanged();
	}

	void DebugClearCategory(ShopItemCategory category)
	{
		ProgressManager.DebugClearOwnedByCategory(category);
		RebuildItemLists();
		StateHasChanged();
	}

	void Close()
	{
		Manager.Instance.ShowShopPanel = false;
	}

	protected override int BuildHash()
	{
		return System.HashCode.Combine(
			Manager.Instance.ShowShopPanel,
			_selectedTab,
			ProgressManager.StateVersion
		);
	}
}