UI/Shop.razor

A UI Razor component for the in-game shop. It renders lists of upgrades grouped as buyable, locked, or already bought, handles opening/closing via ShopBus events, allows purchasing upgrades, and computes a build hash for UI diffing.

File Access
@using System
@using System.Linq
@using System.Collections.Generic
@using Sandbox
@using Sandbox.UI
@using BrickJam.Upgrading
@namespace BrickJam.UI
@inherits Panel

<root class="@(open ? "open" : "")">
	<div class="window">
		<div class="header">
			<div class="image"></div>
		</div>
		<div class="container">
			<div class="container-content">
				<div class="items">
					@{
						var player = Player.Local;
						var possible = new List<Upgrade>();
						var locked = new List<Upgrade>();
						var bought = new List<Upgrade>();

						if ( player is not null )
						{
							foreach ( var u in Upgrade.All )
							{
								if ( player.HasUpgrade( u.Identifier ) )
									bought.Add( u );
								else if ( CanBuy( u ) )
									possible.Add( u );
								else
									locked.Add( u );
							}
						}
					}

					@foreach ( var upgrade in possible )
					{
						<div class="item">
							<div class="info">
								<div class="header">
									@upgrade.Title
									<div class="icon" style="background-image: url('@upgrade.Texture');"></div>
								</div>
								<div class="description">
									@upgrade.Description
								</div>
							</div>
							<div class="buy-button" @onclick=@( () => Buy( upgrade ) )>
								<div class="buy">
									BUY!
								</div>
								<div class="price">
									[email protected]
								</div>
							</div>
						</div>
					}

					@foreach ( var upgrade in locked )
					{
						<div class="item locked">
							<div class="info">
								<div class="header">
									@upgrade.Title
									<div class="icon locked" style="background-image: url('@upgrade.Texture');"></div>
								</div>
								<div class="description">
									@upgrade.Description
								</div>
							</div>
							<div class="buy-button locked">
								<div class="price">
									[email protected]
								</div>
							</div>
						</div>
					}

					@foreach ( var upgrade in bought )
					{
						<div class="item locked">
							<div class="info">
								<div class="header">
									@upgrade.Title
									<div class="icon locked" style="background-image: url('@upgrade.Texture');"></div>
								</div>
								<div class="description">
									@upgrade.Description
								</div>
							</div>
							<div class="buy-button locked bought">
								<div class="price">
									BOUGHT
								</div>
							</div>
						</div>
					}
				</div>
			</div>
			<div class="border-overlay"></div>
		</div>
	</div>
</root>

@code {
	private bool open;

	public Shop()
	{
		ShopBus.OnOpen += Show;
		ShopBus.OnClose += Hide;
	}

	public override void OnDeleted()
	{
		base.OnDeleted();
		ShopBus.OnOpen -= Show;
		ShopBus.OnClose -= Hide;
	}

	private void Show() => open = true;
	private void Hide() => open = false;

	private bool CanBuy( Upgrade upgrade )
	{
		var player = Player.Local;
		if ( player is null || player.HasUpgrade( upgrade.Identifier ) || upgrade.Price > player.Money )
			return false;

		return upgrade.Dependencies.All( dep => player.HasUpgrade( dep ) );
	}

	private void Buy( Upgrade upgrade )
	{
		if ( CanBuy( upgrade ) )
			Player.Local.BuyUpgrade( upgrade.Identifier );
	}

	public override void Tick()
	{
		if ( open && Input.Pressed( "use" ) )
			Hide();
	}

	protected override int BuildHash() => HashCode.Combine(
		open, Player.Local?.Money ?? 0, Player.Local?.Upgrades.Count ?? 0 );
}

<style>
	Shop {
		position: absolute;
		left: 0;
		top: 0;

		width: 100%;
		height: 100%;

		justify-content: center;
		align-items: center;

		opacity: 0;
		z-index: 1;

		transition: opacity 0.3s ease;

		&.open {
			opacity: 1;
			pointer-events: all;
		}

		.window {
			width: 800px;
			height: 900px;

			background-color: #61577C;

			border-right: #25213A solid 5px;
			border-bottom: #25213A solid 5px;

			padding: 8px;

			flex-direction: column;

			.header {
				width: 100%;
				height: 155px;

				justify-content: space-between;
				margin-bottom: 10px;

				.image {
					width: 100%;
					height: 100%;

					image-rendering: pixelated;
					background-image: url("ui/shop/header.png");
					background-size: 100%;
					background-repeat: no-repeat;
				}
			}

			.container {
				width: 100%;
				height: 100%;

				image-rendering: pixelated;
				background-image: url("ui/shop/background.png");
				background-size: 200% 200%;
				background-position: center;

				.border-overlay {
					z-index: 30;

					position: absolute;

					image-rendering: pixelated;
					background-image: url("ui/shop/border.png");
					background-size: 100%;
					background-position: center;

					pointer-events: none;
				}

				.container-content {
					padding: 35px;

					.items {
						width: 100%;
						height: 100%;

						pointer-events: all;

						justify-content: center;
						align-items: center;

						flex-wrap: wrap;
						overflow: scroll;

						.item {
							flex-shrink: 0;
							margin-bottom: 15px;

							padding: 18px;

							width: 100%;
							height: 120px;

							image-rendering: pixelated;
							background-size: 100%;
							background-position: center;

							justify-content: space-between;

							&.locked {
								background-image: url("ui/shop/upgrade-locked.png");
							}

							&:not(.locked) {
								background-image: url("ui/shop/upgrade.png");
							}

							.info {
								width: 70%;

								flex-direction: column;
								justify-content: flex-start;
								align-items: flex-start;

								.header {
									height: 30px;
									width: 100%;
									font-size: 30px;

									justify-content: flex-start;

									.icon {
										margin-left: 10px;

										width: 30px;
										height: 30px;

										image-rendering: pixelated;
										background-size: 100%;
										background-position: center;

										&.locked {
											filter: brightness(0.2);
										}
									}
								}

								.description {
									font-size: 20px;
								}
							}

							.buy-button {
								width: 30%;
								height: 100%;

								image-rendering: pixelated;
								background-size: 100%;
								background-position: center;
								background-image: url("ui/shop/buy-button.png");

								color: white;

								justify-content: center;
								align-items: center;

								.buy {
									font-size: 30px;
								}

								.price {
									margin-left: 10px;
									font-size: 20px;
								}

								&.locked {
									.price {
										color: #381313;
										margin-left: 0;
										font-size: 30px;
									}
								}

								&.bought {
									.price {
										color: #000000;
										margin-left: 0;
										font-size: 30px;
									}
								}

								&:not(.locked):hover {
									background-image: url("ui/shop/buy-button.hover.png");
								}
							}
						}
					}
				}
			}
		}
	}
</style>