UI/Inventory.razor

A UI Razor component for the player's inventory. It renders the inventory panel, item icons (LootIcon ScenePanels), hover tooltip with item details and supports drag/throw behavior and toggling visibility via input.

Native Interop
@using System
@using System.Linq
@using System.Collections.Generic
@using Sandbox
@using Sandbox.UI
@namespace BrickJam.UI
@inherits Panel

<root class="@(visible ? "visible" : "")">
	<div class="background" style="background-image: url(@Border);">
		<label class="title">INVENTORY</label>
		<div class="items">
			@* Only build the item icons while the inventory is OPEN. Each LootIcon is a live ScenePanel that *@
			@* re-renders a 3D SceneWorld EVERY frame, so building them while closed wasted Render/UI time. *@
			@if ( visible )
			{
				var items = Player.Local?.Inventory?.Entries
					.OrderByDescending( x => x.entry.Price )
					.ToList();

				if ( items is not null )
				{
					foreach ( var pair in items )
					{
						var entry = pair.entry;
						var count = pair.count;

						<div class="item"
							@onmouseover=@(() => tooltip = (entry, count))
							@onmouseout=@(() =>
							{
								// Keep the ORIGINAL drag data-flow: previousTooltip = the item we just left, which
								// the mouseup handler throws on drop. The ONLY addition over the original is a guard
								// so a stale/bubbled mouseout from a NON-current item (events bubble up from the
								// child LootIcon/amount panels, and adjacent-item moves can deliver our mouseout
								// after the next item's mouseover) doesn't wipe the freshly-shown tooltip -> fixes
								// the "spotty / hover twice" tooltip without touching the drag's previousTooltip.
								if ( tooltip is { } t
									&& t.entry.Prefab?.ResourceName == entry.Prefab?.ResourceName
									&& t.entry.Rarity == entry.Rarity )
								{
									previousTooltip = tooltip;
									tooltip = null;
								}
							})
							@onrightclick=@(() =>
							{
								if ( holdPanel != null ) return;
								tooltip = null; previousTooltip = null;
								DeleteHoldPanel();
								Throw( entry );
							})>
							<LootIcon class="icon" [email protected] />
							<label class="amount">x@(count)</label>
						</div>
					}
				}
			}
		</div>
	</div>

	<div class="tooltip @(tooltip != null || holdPanel != null ? "visible" : "")">
		@{
			var shown = tooltip ?? previousTooltip;
		}
		@if ( shown is { } data && data.entry.Prefab is { } prefab )
		{
			<div class="container">
				<div style="flex-direction: row;">
					<span style="margin-right: 10px;">@data.entry.Name</span>
					<span style="margin-right: 10px;">x@(data.count)</span>
				</div>
				<div style="flex-direction: row; margin-top: 5px;">
					<span class="currency">$</span><span>@($"{data.entry.Price * data.count:n0}")</span>

					@if ( data.count > 1 )
					{
						<span style="color: rgba(215,215,215,1); margin-left: 10px; padding-right: 5px;">/</span>
						<span class="currency">$</span><span style="color: rgba(215,215,215,1);">@($"{data.entry.Price:n0}") ea.</span>
					}
				</div>

				@if ( !string.IsNullOrEmpty( prefab.Description ) )
				{
					<div class="description">@prefab.Description</div>
				}
			</div>

			<LootIcon class="bigicon" Prefab=@prefab />
		}
	</div>
</root>

@code {
	private bool visible;
	private (ItemEntry entry, int count)? tooltip;
	private (ItemEntry entry, int count)? previousTooltip;
	private Panel holdPanel;

	private string Border => (MansionGame.Instance?.CurrentLevelType ?? LevelType.Dungeon) switch
	{
		LevelType.Mansion => "ui/inventory_woods.png",
		LevelType.Dungeon => "ui/inventory_bricks.png",
		LevelType.Bathrooms => "ui/inventory_tiles.png",
		_ => "ui/inventory.png",
	};

	public Inventory()
	{
		AddEventListener( "onmousedown", ( PanelEvent e ) =>
		{
			if ( e is not MousePanelEvent m || !visible || m.MouseButton != MouseButtons.Left || tooltip == null )
				return;

			CreateHoldPanel( tooltip.Value );
		} );

		AddEventListener( "onmouseup", ( PanelEvent e ) =>
		{
			if ( e is not MousePanelEvent m || m.MouseButton != MouseButtons.Left )
				return;

			DeleteHoldPanel();

			// Released inside the inventory panel? keep the item.
			if ( Mouse.Position.x >= Box.Rect.Left && Mouse.Position.x <= Box.Rect.Right
				&& Mouse.Position.y >= Box.Rect.Top && Mouse.Position.y <= Box.Rect.Bottom )
				return;

			if ( previousTooltip is { } dropped )
				Throw( dropped.entry );
		} );
	}

	private void CreateHoldPanel( (ItemEntry entry, int count) pair )
	{
		DeleteHoldPanel();
		holdPanel = AddChild<Panel>( "drag" );

		var icon = holdPanel.AddChild<LootIcon>( "icon" );
		icon.Prefab = pair.entry.Prefab;
	}

	private void DeleteHoldPanel()
	{
		holdPanel?.Delete();
		holdPanel = null;
	}

	private void Throw( ItemEntry entry )
	{
		if ( entry.Prefab is not null )
			Player.Local?.ThrowItem( entry.Prefab.ResourceName, entry.Rarity );
	}

	public override void Tick()
	{
		if ( Input.Pressed( "Inventory" ) )
		{
			ShopBus.Close();
			DeleteHoldPanel();
			tooltip = null;
			previousTooltip = null;
			visible = !visible;
		}

		if ( holdPanel is not null )
		{
			holdPanel.Style.Left = (Mouse.Position.x - Box.Rect.Left) * ScaleFromScreen;
			holdPanel.Style.Top = (Mouse.Position.y - Box.Rect.Top) * ScaleFromScreen;
		}
	}

	protected override int BuildHash() => HashCode.Combine(
		visible, tooltip, previousTooltip, holdPanel != null,
		Player.Local?.Inventory?.Count ?? 0, Border );
}

<style>
	Inventory {
		position: absolute;
		bottom: 0;
		left: 0;
		width: 480px;
		height: 503px;
		color: white;
		pointer-events: none;
		transform: translateY(100%);
		transition: transform 0.2s ease-in-out;

		&.visible {
			transform: translateY(0%);
			pointer-events: all;
		}

		.background {
			width: 100%;
			height: 100%;
			background-image: url(ui/inventory.png);
			background-size: 100% 100%;
			background-repeat: no-repeat;
			image-rendering: pixelated;
		}

		.title {
			position: absolute;
			top: 3.5%;
			left: 7%;
			font-family: alagard;
			font-size: 42px;
			color: white;
			text-shadow: 3px 3px 0px rgba(0,0,0,0.8);
		}

		.items {
			position: absolute;
			top: 28%;
			left: 6%;
			width: 88%;
			height: 68%;
			flex-direction: row;
			flex-wrap: wrap;
			align-content: flex-start;
		}

		.item {
			position: relative;
			width: 20%;
			height: 25%;
			overflow: hidden;
			padding: 9%;
			align-items: center;
			justify-content: center;

			.icon {
				width: 100%;
				height: 100%;
			}

			.amount {
				position: absolute;
				bottom: 3px;
				right: 6px;
				font-size: 18px;
				text-stroke: 3px black;
				text-shadow: 2px 2px 0px rgba(0,0,0,0.8);
				pointer-events: none;
			}

			&:hover {
				background-color: rgba(255,255,255,0.12);
				cursor: pointer;
			}
		}

		.tooltip {
			position: absolute;
			left: 100%;
			bottom: 20px;
			margin-left: 10px;
			transition: transform 0.2s ease-in-out;
			background-color: transparent;
			transform: translateY(200%);
			font-size: 24px;
			text-shadow: 2px 2px 0px rgba(0,0,0,0.8);
			flex-direction: row;
			padding: 16px;
			border-image: url(ui/scroll_border.png) fill;
			border-image-tint: rgba(165,165,165,1);
			align-items: center;
			/* A tooltip is passive - it must never become the hovered/hit panel, or it keeps the cursor
			   captured on screen after the inventory closes (the "tooltips keep my mouse on screen" bug). */
			pointer-events: none;

			.container {
				max-width: 460px;
				flex-direction: column;
				flex-shrink: 0;
			}

			.currency {
				padding-right: 5px;
				font-size: 16px;
				top: 3px;
				color: rgba(50,205,50,1);
			}

			.description {
				margin-top: 8px;
				color: rgba(215,215,215,1);
			}

			.bigicon {
				margin-left: 20px;
				width: 100px;
				height: 100px;
				overflow: hidden;
				z-index: 2;
			}

			&.visible {
				transform: translateY(0%);
			}
		}
	}

	.drag {
		position: absolute;
		width: 90px;
		height: 90px;
		overflow: hidden;
		transform: translate(-50%, -50%);
		pointer-events: none;
		border-image: url(ui/scroll_border.png) fill;
		border-image-tint: rgba(190,190,190,1);
		transform-origin: top left;

		.icon {
			width: 100%;
			height: 100%;
		}
	}
</style>