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.
@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>