UI/InventoryHud.razor
@using Sandbox;
@using System;
@using System.Linq;
@using Sandbox.UI;
@inherits PanelComponent
<root class="@(IsInventoryOpen ? "inventory-open" : "")" @ref="RootPanel"
onmouseup=@(e => OnRootMouseUp(e))>
@* ── Inventory Panel (shown/hidden on toggle) ── *@
@if ( IsInventoryOpen )
{
<div class="inventory-overlay" onmousedown=@CloseInventory></div>
<div class="inventory-panel">
@* ── Equipment Panel (left column) ── *@
@if ( Inventory?.UseEquipment == true && Inventory.Equipment != null )
{
<div class="equipment-panel">
<label class="panel-title">Equipment</label>
<div class="equip-layout">
@* Top — Head *@
<div class="equip-row">
@{ var headSlot = Inventory.Equipment.GetSlot( EquipSlot.Head ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Head ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Head && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Head, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Head; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = headSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( headSlot != null && !headSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && headSlot.Durability >= 0 && headSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(headSlot.Durability / headSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Head</label> }
</div>
</div>
@* Middle — WeaponMain, Chest, WeaponOffhand *@
<div class="equip-row">
@{ var mainSlot = Inventory.Equipment.GetSlot( EquipSlot.WeaponMain ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.WeaponMain ? "dragging" : "") @(HoverEquipSlot == EquipSlot.WeaponMain && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.WeaponMain, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.WeaponMain; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = mainSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( mainSlot != null && !mainSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && mainSlot.Durability >= 0 && mainSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(mainSlot.Durability / mainSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Main</label> }
</div>
@{ var chestSlot = Inventory.Equipment.GetSlot( EquipSlot.Chest ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Chest ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Chest && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Chest, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Chest; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = chestSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( chestSlot != null && !chestSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && chestSlot.Durability >= 0 && chestSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(chestSlot.Durability / chestSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Chest</label> }
</div>
@{ var offSlot = Inventory.Equipment.GetSlot( EquipSlot.WeaponOffhand ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.WeaponOffhand ? "dragging" : "") @(HoverEquipSlot == EquipSlot.WeaponOffhand && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.WeaponOffhand, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.WeaponOffhand; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = offSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( offSlot != null && !offSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && offSlot.Durability >= 0 && offSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(offSlot.Durability / offSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Off</label> }
</div>
</div>
@* Lower — Hands, Legs, Accessory1 *@
<div class="equip-row">
@{ var handsSlot = Inventory.Equipment.GetSlot( EquipSlot.Hands ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Hands ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Hands && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Hands, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Hands; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = handsSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( handsSlot != null && !handsSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && handsSlot.Durability >= 0 && handsSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(handsSlot.Durability / handsSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Hands</label> }
</div>
@{ var legsSlot = Inventory.Equipment.GetSlot( EquipSlot.Legs ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Legs ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Legs && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Legs, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Legs; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = legsSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( legsSlot != null && !legsSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && legsSlot.Durability >= 0 && legsSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(legsSlot.Durability / legsSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Legs</label> }
</div>
@{ var acc1Slot = Inventory.Equipment.GetSlot( EquipSlot.Accessory1 ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Accessory1 ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Accessory1 && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Accessory1, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Accessory1; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = acc1Slot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( acc1Slot != null && !acc1Slot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && acc1Slot.Durability >= 0 && acc1Slot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(acc1Slot.Durability / acc1Slot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Ring</label> }
</div>
</div>
@* Bottom — Feet, Accessory2 *@
<div class="equip-row">
@{ var feetSlot = Inventory.Equipment.GetSlot( EquipSlot.Feet ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Feet ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Feet && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Feet, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Feet; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = feetSlot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( feetSlot != null && !feetSlot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && feetSlot.Durability >= 0 && feetSlot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(feetSlot.Durability / feetSlot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Feet</label> }
</div>
@{ var acc2Slot = Inventory.Equipment.GetSlot( EquipSlot.Accessory2 ); }
<div class="equip-slot @(DragFromEquipSlot == EquipSlot.Accessory2 ? "dragging" : "") @(HoverEquipSlot == EquipSlot.Accessory2 && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnEquipSlotMouseDown( EquipSlot.Accessory2, e ))
onmouseover=@(() => { HoverEquipSlot = EquipSlot.Accessory2; HoverBagIndex = -1; HotbarHoverCol = -1; TooltipSlot = acc2Slot; })
onmouseout=@(() => { HoverEquipSlot = EquipSlot.None; TooltipSlot = null; })>
@if ( acc2Slot != null && !acc2Slot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( Inventory?.EnableDurability == true && acc2Slot.Durability >= 0 && acc2Slot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(acc2Slot.Durability / acc2Slot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
else { <label class="equip-label">Ring 2</label> }
</div>
</div>
</div>
</div>
}
@* ── Loot Panel (middle column, only when a container is open) ── *@
@if ( _openContainer != null )
{
<div class="loot-panel @(_openContainer.IsLootContainer ? "loot-panel-world" : "")">
<div class="loot-panel-header">
<label class="panel-title">@_openContainer.ContainerName</label>
@if ( _openContainer.IsLootContainer )
{
<label class="loot-badge">LOOT</label>
}
</div>
<div class="loot-grid-scroll">
<div class="bag-grid">
@for ( int row = 0; row < _openContainer.Height; row++ )
{
<div class="bag-row">
@for ( int col = 0; col < _openContainer.Width; col++ )
{
var localI = row * _openContainer.Width + col;
var slot = _openContainer.Container.GetSlot( localI );
<div class="bag-slot @(DragFromLootIndex == localI ? "dragging" : "") @(HoverLootIndex == localI && IsDragging ? "drag-over" : "")"
onmousedown=@(e => OnLootSlotMouseDown( localI, e ))
onmouseover=@(() => { TooltipSlot = slot; HoverLootIndex = localI; HoverBagIndex = -1; HotbarHoverCol = -1; HoverEquipSlot = EquipSlot.None; })
onmouseout=@(() => { TooltipSlot = null; HoverLootIndex = -1; })>
@if ( !slot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( slot.Quantity > 1 )
{
<label class="item-qty">@slot.Quantity</label>
}
}
</div>
}
</div>
}
</div>
</div>
<div class="loot-footer">
<div class="take-all-btn" onmousedown=@OnTakeAll>Take All</div>
</div>
</div>
}
@* ── Bag Grid (right column) ── *@
<div class="bag-panel">
<label class="panel-title">Inventory</label>
@if ( Inventory?.EnableSearch == true || Inventory?.EnableCategories == true || Inventory?.EnableSorting == true || Inventory?.EnableCrafting == true || Inventory?.EnableQuickStack == true )
{
<div class="bag-toolbar" style="width: @(Inventory.BagWidth * (74 + 5) - 5)px;">
@if ( Inventory?.EnableSearch == true )
{
<input type="text" class="bag-search" placeholder="Search…"
value=@SearchText onchange=@((string v) => { SearchText = v; }) />
}
<div class="toolbar-row">
@if ( Inventory?.EnableCategories == true )
{
var cats = (Inventory.VisibleCategories?.Count > 0)
? Inventory.VisibleCategories
: Enum.GetValues<ItemCategory>().ToList();
<div class="category-tabs">
<div class="category-tab @(CategoryFilter == null ? "active" : "")"
onmousedown=@(() => CategoryFilter = null)>All</div>
@foreach ( var cat in cats )
{
<div class="category-tab @(CategoryFilter == cat ? "active" : "")"
onmousedown=@(() => CategoryFilter = cat)>@cat</div>
}
</div>
}
<div class="toolbar-actions">
@if ( Inventory?.EnableSorting == true )
{
<div class="sort-btn" onmousedown=@(() => Inventory?.SortBag())>Sort</div>
}
@if ( Inventory?.EnableQuickStack == true )
{
<div class="sort-btn" onmousedown=@(() => Inventory?.QuickStackToNearby())>Quick Stack</div>
}
</div>
</div>
</div>
}
<div class="bag-grid">
@for ( int row = 0; row < Inventory.BagHeight; row++ )
{
<div class="bag-row">
@for ( int col = 0; col < Inventory.BagWidth; col++ )
{
var localI = row * Inventory.BagWidth + col;
var slot = Inventory.Bag.GetSlot( localI );
var matches = ItemPassesFilter( slot );
<div class="bag-slot @(DragFromIndex == localI ? "dragging" : "") @(HoverBagIndex == localI && IsDragging ? "drag-over" : "") @(!matches ? "filtered-out" : "")"
onmousedown=@(e => OnBagSlotMouseDown( localI, e ))
onmouseover=@(() => { if ( matches ) TooltipSlot = slot; HoverBagIndex = localI; HotbarHoverCol = -1; HoverEquipSlot = EquipSlot.None; })
onmouseout=@(() => TooltipSlot = null)>
@if ( !slot.IsEmpty && matches )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( slot.Quantity > 1 )
{
<label class="item-qty">@slot.Quantity</label>
}
@if ( Inventory?.EnableDurability == true && slot.Durability >= 0 && slot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(slot.Durability / slot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
</div>
}
</div>
}
</div>
@if ( Inventory?.EnableWeight == true && Inventory.MaxWeight > 0f )
{
var weightPct = Math.Clamp( Inventory.CurrentWeight / Inventory.MaxWeight * 100f, 0f, 100f );
<div class="weight-bar @(Inventory.IsOverEncumbered ? "overencumbered" : "")" style="width: @(Inventory.BagWidth * (74 + 5) - 5)px;">
<div class="weight-fill" style="width: @(weightPct)%"></div>
<label class="weight-label">@($"{Inventory.CurrentWeight:F1} / {Inventory.MaxWeight:F1} kg")</label>
</div>
}
</div>
@* ── Crafting Panel (right column, always shown when EnableCrafting is true) ── *@
@if ( Inventory?.EnableCrafting == true && _openContainer == null )
{
var recipes = GetVisibleRecipes().ToList();
<div class="crafting-panel">
<label class="panel-title">@( _openStation != null ? _openStation.StationName : "Crafting" )</label>
<input type="text" class="bag-search" placeholder="Search recipes…"
value=@_recipeSearch onchange=@((string v) => { _recipeSearch = v; }) />
<div class="recipe-list">
@foreach ( var recipe in recipes )
{
if ( recipe?.ResultItem == null ) continue;
if ( !string.IsNullOrWhiteSpace( _recipeSearch ) &&
!recipe.RecipeName.ToLower().Contains( _recipeSearch.ToLower() ) ) continue;
bool canCraft = Inventory.CanCraft( recipe );
bool selected = _selectedRecipe == recipe;
var localRecipe = recipe;
<div class="recipe-entry @(selected ? "selected" : "") @(!canCraft ? "recipe-unavailable" : "")"
onmousedown=@(() => { _selectedRecipe = localRecipe; _craftQuantity = 1; CancelCraft(); StateHasChanged(); })>
@if ( recipe.ResultItem.Icon != null )
{
<img [email protected] class="recipe-icon" />
}
<div class="recipe-entry-text">
<label class="recipe-name">@recipe.RecipeName</label>
<label class="recipe-result-qty">[email protected]</label>
</div>
</div>
}
</div>
@if ( _selectedRecipe != null )
{
<div class="recipe-detail">
<div class="recipe-ingredients">
@foreach ( var ing in _selectedRecipe.Ingredients )
{
if ( ing.Item == null ) continue;
int have = Inventory.Bag.CountItem( ing.Item );
int need = ing.Amount * _craftQuantity;
bool enough = have >= need;
<div class="ingredient-row">
@if ( ing.Item.Icon != null )
{
<img [email protected] class="ingredient-icon" />
}
<label class="ingredient-name">@ing.Item.ItemName</label>
<label class="ingredient-count @(enough ? "ingredient-ok" : "ingredient-missing")">@have / @need</label>
</div>
}
</div>
<div class="craft-qty-row">
<div class="craft-qty-btn" onmousedown=@(() => { _craftQuantity = Math.Max( 1, _craftQuantity - 1 ); StateHasChanged(); })>-</div>
<label class="craft-qty-label">@_craftQuantity</label>
<div class="craft-qty-btn" onmousedown=@(() => { _craftQuantity = Math.Min( Inventory.GetMaxCraftable( _selectedRecipe ), _craftQuantity + 1 ); StateHasChanged(); })>+</div>
<div class="craft-qty-btn craft-qty-max" onmousedown=@(() => { _craftQuantity = Math.Max( 1, Inventory.GetMaxCraftable( _selectedRecipe ) ); StateHasChanged(); })>Max</div>
</div>
@if ( _isCrafting )
{
<div class="craft-progress-wrap">
<div class="craft-progress-bar">
<div class="craft-progress-fill" style="width: @($"{(int)Math.Min(_craftProgress * 100f, 100f)}")%"></div>
</div>
<label class="craft-progress-label">@_selectedRecipe.RecipeName (@_craftQueueRemaining left)</label>
</div>
<div class="craft-btn craft-cancel-btn" onmousedown=@(() => CancelCraft())>Cancel</div>
}
else
{
var canStart = Inventory.CanCraft( _selectedRecipe, _craftQuantity );
<div class="craft-btn @(!canStart ? "craft-btn-disabled" : "")"
onmousedown=@(() => {
if ( !Inventory.CanCraft( _selectedRecipe, 1 ) ) return;
if ( Inventory.ConsumeCraftIngredients( _selectedRecipe ) )
{
_craftQueueRemaining = _craftQuantity;
_craftStartTime = RealTime.Now;
_craftProgress = 0f;
_isCrafting = true;
}
})>
Craft x@_craftQuantity
</div>
}
</div>
}
</div>
}
</div>
@* ── Tooltip ── *@
@if ( TooltipSlot != null && !TooltipSlot.IsEmpty )
{
var item = TooltipSlot.Item;
<div class="item-tooltip">
<label class="tooltip-name [email protected]().ToLower()">@item.ItemName</label>
@if ( Inventory?.EnableCategories == true )
{
<label class="tooltip-category">@item.Category</label>
}
@if ( Inventory?.EnableRarity == true && item.Rarity != ItemRarity.Common )
{
<label class="tooltip-rarity">@item.Rarity</label>
}
@if ( item.EquipSlot != EquipSlot.None )
{
<label class="tooltip-slot">@item.EquipSlot</label>
}
@if ( !string.IsNullOrEmpty( item.Description ) )
{
<label class="tooltip-desc">@item.Description</label>
}
@if ( item.Weight > 0f )
{
<label class="tooltip-weight">@item.Weight kg</label>
}
@if ( Inventory?.EnableDurability == true && TooltipSlot.Durability >= 0 && item.MaxDurability > 0 )
{
var durPct = Math.Clamp( TooltipSlot.Durability / item.MaxDurability * 100f, 0f, 100f );
<label class="tooltip-durability">Durability: @((int)TooltipSlot.Durability) / @item.MaxDurability (@($"{durPct:F0}%"))</label>
}
@if ( Inventory?.EnableEquipmentStats == true && item.StatModifiers != null && item.StatModifiers.Length > 0 )
{
var equippedItem = ( Inventory?.UseEquipment == true && item.EquipSlot != EquipSlot.None )
? Inventory.Equipment?.GetSlot( item.EquipSlot )?.Item
: null;
<div class="tooltip-stats">
@if ( equippedItem != null )
{
<label class="tooltip-compare-label">vs @equippedItem.ItemName</label>
}
@foreach ( var mod in item.StatModifiers )
{
var equippedVal = GetEquippedStat( equippedItem, mod.Type );
var delta = mod.Value - equippedVal;
var sign = mod.Value >= 0 ? "+" : "";
var deltaSign = delta >= 0 ? "+" : "";
<div class="tooltip-stat-row">
<label class="tooltip-stat @(mod.Value >= 0 ? "stat-positive" : "stat-negative")">@mod.Type: @($"{sign}{mod.Value:F0}")</label>
@if ( equippedItem != null )
{
<label class="tooltip-stat-delta @(delta > 0 ? "stat-positive" : delta < 0 ? "stat-negative" : "stat-neutral")">(@($"{deltaSign}{delta:F0}"))</label>
}
</div>
}
</div>
}
</div>
}
}
@* ── Context Menu ── *@
@if ( ContextMenuOpen && ContextMenuSlot != null && !ContextMenuSlot.IsEmpty )
{
<div class="context-menu" style="left: @(ContextMenuPos.x)px; top: @(ContextMenuPos.y)px;">
@if ( ContextMenuLootIndex >= 0 )
{
<div class="context-menu-item" onmousedown=@OnContextTake>Take</div>
}
else
{
@if ( ContextMenuBagIndex >= 0 && Inventory?.UseEquipment == true && ContextMenuSlot.Item.EquipSlot != EquipSlot.None )
{
<div class="context-menu-item" onmousedown=@OnContextEquip>Equip</div>
}
@if ( ContextMenuBagIndex >= 0 && ContextMenuSlot.Quantity > 1 )
{
<div class="context-menu-item" onmousedown=@OnContextSplit>Split Stack</div>
}
@if ( ContextMenuEquipSlot != EquipSlot.None && Inventory?.UseEquipment == true )
{
<div class="context-menu-item" onmousedown=@OnContextUnequip>Unequip</div>
}
@if ( ContextMenuSlot.Item.IsUsable )
{
<div class="context-menu-item" onmousedown=@OnContextUse>Use</div>
}
<div class="context-menu-item" onmousedown=@OnContextDrop>Drop</div>
<div class="context-menu-item context-menu-destroy" onmousedown=@OnContextDestroy>Destroy</div>
}
</div>
}
@* ── Destroy Confirmation ── *@
@if ( ShowDestroyConfirm )
{
<div class="confirm-overlay" onmousedown=@CancelDestroy>
<div class="confirm-dialog" onmousedown=@((e) => { })>
<label class="confirm-title">Destroy Item?</label>
<label class="confirm-item">@DestroyConfirmSlot?.Item?.ItemName x@DestroyConfirmSlot?.Quantity</label>
<div class="confirm-buttons">
<div class="confirm-btn confirm-yes" onmousedown=@ConfirmDestroy>Yes</div>
<div class="confirm-btn confirm-no" onmousedown=@CancelDestroy>No</div>
</div>
</div>
</div>
}
@* ── Hotbar (always visible, rendered AFTER overlay so it's on top) ── *@
@if ( Inventory?.UseHotbar == true )
{
<div class="hotbar-wrapper" @ref="HotbarWrapperPanel">
<div class="hotbar" @ref="HotbarPanel">
@for ( int i = 0; i < Inventory.BagWidth; i++ )
{
var slotIndex = Inventory.HotbarStartIndex + i;
var slot = Inventory.Bag.GetSlot( slotIndex );
var isActive = i == Inventory.HotbarActiveIndex;
var localI = i;
var hotbarBagIndex = Inventory.HotbarStartIndex + localI;
<div class="hotbar-slot @(isActive ? "active" : "") @(DragFromIndex == hotbarBagIndex ? "dragging" : "") @(HotbarHoverCol == i && IsDragging ? "drag-over" : "")"
@ref="@(e => HotbarSlotPanels[localI] = (Panel)e)"
onmousedown=@(e => OnBagSlotMouseDown( hotbarBagIndex, e ))
onmouseover=@(() => { TooltipSlot = slot; HoverEquipSlot = EquipSlot.None; HoverBagIndex = -1; })
onmouseout=@(() => { TooltipSlot = null; HotbarHoverCol = -1; })>
@if ( !slot.IsEmpty )
{
@if ( Inventory?.EnableRarity == true )
{
<div class="rarity-border [email protected]().ToLower()"></div>
}
<img [email protected]?.ResourcePath class="item-icon" />
@if ( slot.Quantity > 1 )
{
<label class="item-qty">@slot.Quantity</label>
}
@if ( Inventory?.EnableDurability == true && slot.Durability >= 0 && slot.Item.MaxDurability > 0 )
{
<div class="durability-bar">
<div class="durability-fill" style="width: @(Math.Clamp(slot.Durability / slot.Item.MaxDurability * 100f, 0f, 100f))%"></div>
</div>
}
}
<label class="hotbar-number">@(localI + 1)</label>
</div>
}
</div>
</div>
}
@* ── Crosshair ── *@
<div class="crosshair">
<div class="crosshair-line crosshair-top"></div>
<div class="crosshair-line crosshair-right"></div>
<div class="crosshair-line crosshair-bottom"></div>
<div class="crosshair-line crosshair-left"></div>
<div class="crosshair-dot"></div>
</div>
</root>
@code {
[Property] public InventoryComponent Inventory { get; set; }
[Property] public float ContainerCloseDistance { get; set; } = 200f;
public bool IsInventoryOpen { get; private set; } = false;
private string SearchText { get; set; } = "";
private ItemCategory? CategoryFilter { get; set; } = null;
private Panel HotbarWrapperPanel { get; set; }
private Panel RootPanel { get; set; }
private Panel HotbarPanel { get; set; }
private Panel[] HotbarSlotPanels { get; set; } = new Panel[9];
private bool ShiftHeld { get; set; }
private Vector2 CursorPos { get; set; }
private int DragFromIndex { get; set; } = -1;
private EquipSlot DragFromEquipSlot { get; set; } = EquipSlot.None;
private bool IsDragging { get; set; } = false;
private int HoverBagIndex { get; set; } = -1;
private int HotbarHoverCol { get; set; } = -1;
private EquipSlot HoverEquipSlot { get; set; } = EquipSlot.None;
private InventorySlot TooltipSlot { get; set; }
// ─── Crafting State ───────────────────────────────────────────
private CraftingStation _openStation;
private CraftingRecipe _selectedRecipe;
private int _craftQuantity = 1;
private bool _isCrafting = false;
private float _craftProgress = 0f;
private float _craftStartTime = 0f;
private int _craftQueueRemaining = 0;
private string _recipeSearch = "";
public void OpenStation( CraftingStation station )
{
_openStation = station;
_selectedRecipe = null;
_craftQuantity = 1;
IsInventoryOpen = true;
StateHasChanged();
}
public void CloseStation()
{
CancelCraft();
_openStation = null;
_selectedRecipe = null;
StateHasChanged();
}
private void CancelCraft()
{
_isCrafting = false;
_craftProgress = 0f;
_craftQueueRemaining = 0;
}
private System.Collections.Generic.IEnumerable<CraftingRecipe> GetVisibleRecipes()
{
if ( _openStation != null )
return _openStation.Recipes ?? new System.Collections.Generic.List<CraftingRecipe>();
return ResourceLibrary.GetAll<CraftingRecipe>()
.Where( r => !r.RequiresCraftingStation );
}
// ─── Loot State ───────────────────────────────────────────────
private StorageContainer _openContainer;
private int DragFromLootIndex { get; set; } = -1;
private int HoverLootIndex { get; set; } = -1;
/// <summary>Open a container's loot panel. Call from your interaction system.</summary>
public void OpenContainer( StorageContainer container )
{
if ( _openContainer != null )
_openContainer.OnContainerChanged -= StateHasChanged;
_openContainer = container;
if ( _openContainer != null )
_openContainer.OnContainerChanged += StateHasChanged;
IsInventoryOpen = true;
StateHasChanged();
}
/// <summary>Close the loot panel without closing the inventory.</summary>
public void CloseContainer()
{
if ( _openContainer != null )
_openContainer.OnContainerChanged -= StateHasChanged;
_openContainer = null;
StateHasChanged();
}
// ─── Context Menu State ────────────────────────────────────────
private bool ContextMenuOpen { get; set; } = false;
private Vector2 ContextMenuPos { get; set; }
private int ContextMenuBagIndex { get; set; } = -1;
private int ContextMenuLootIndex { get; set; } = -1;
private EquipSlot ContextMenuEquipSlot { get; set; } = EquipSlot.None;
private InventorySlot ContextMenuSlot { get; set; }
// ─── Destroy Confirmation State ────────────────────────────────
private bool ShowDestroyConfirm { get; set; } = false;
private int DestroyConfirmBagIndex { get; set; } = -1;
private EquipSlot DestroyConfirmEquipSlot { get; set; } = EquipSlot.None;
private InventorySlot DestroyConfirmSlot { get; set; }
// Must match SCSS values exactly
private const float SlotSize = 74f;
private const float HotbarBottom = 28f; // bottom offset on .hotbar-wrapper
// ─── Lifecycle ───────────────────────────────────────────────
protected override void OnStart()
{
if ( Inventory != null )
{
Inventory.OnInventoryChanged += StateHasChanged;
if ( Inventory.BagWidth > HotbarSlotPanels.Length )
HotbarSlotPanels = new Panel[Inventory.BagWidth];
}
StorageContainer.OnOpenRequested += OpenContainer;
CraftingStation.OnStationOpened += OpenStation;
}
protected override void OnDestroy()
{
if ( Inventory != null )
Inventory.OnInventoryChanged -= StateHasChanged;
if ( _openContainer != null )
_openContainer.OnContainerChanged -= StateHasChanged;
StorageContainer.OnOpenRequested -= OpenContainer;
CraftingStation.OnStationOpened -= OpenStation;
}
protected override void OnUpdate()
{
ShiftHeld = Input.Down( "run" );
if ( Input.Pressed( "inventory" ) )
{
if ( ContextMenuOpen )
{
CloseContextMenu();
StateHasChanged();
return;
}
IsInventoryOpen = !IsInventoryOpen;
if ( !IsInventoryOpen )
{
CancelDrag();
CloseContainer();
CloseStation();
}
StateHasChanged();
}
if ( _openContainer != null )
{
float dist = Vector3.DistanceBetween( Inventory.Transform.Position, _openContainer.Transform.Position );
if ( dist > ContainerCloseDistance )
{
CloseContainer();
StateHasChanged();
}
}
if ( _openStation != null )
{
float dist = Vector3.DistanceBetween( Inventory.Transform.Position, _openStation.Transform.Position );
if ( dist > _openStation.CloseDistance )
{
CloseStation();
StateHasChanged();
}
}
if ( _isCrafting && _selectedRecipe != null && Inventory != null )
{
float craftTime = Math.Max( _selectedRecipe.CraftTime, 0.05f );
_craftProgress = Math.Clamp( ( RealTime.Now - _craftStartTime ) / craftTime, 0f, 1f );
if ( _craftProgress >= 1f )
{
Inventory.GiveCraftResult( _selectedRecipe );
_craftQueueRemaining--;
if ( _craftQueueRemaining > 0 && Inventory.CanCraft( _selectedRecipe ) )
{
Inventory.ConsumeCraftIngredients( _selectedRecipe );
_craftStartTime = RealTime.Now;
_craftProgress = 0f;
}
else
{
CancelCraft();
}
}
StateHasChanged();
}
if ( IsInventoryOpen )
{
Input.AnalogLook = Angles.Zero;
if ( IsDragging )
{
var scale = RootPanel?.ScaleFromScreen ?? 1f;
var screenPos = Mouse.Position;
CursorPos = new Vector2( screenPos.x * scale, screenPos.y * scale );
UpdateHotbarHoverFromMouse();
StateHasChanged();
}
}
if ( Inventory?.UseHotbar == true && !IsInventoryOpen )
{
for ( int i = 0; i < Inventory.BagWidth && i < 9; i++ )
{
if ( Input.Pressed( $"slot{i + 1}" ) )
Inventory.SetHotbarActive( i );
}
}
}
private void UpdateHotbarHoverFromMouse()
{
if ( Inventory == null || HotbarPanel == null ) return;
var children = HotbarPanel?.Children?.ToList();
bool found = false;
if ( children != null )
{
for ( int i = 0; i < children.Count && i < Inventory.BagWidth; i++ )
{
var slot = children[i];
var sp = slot.MousePosition;
// Inside the slot panel: x in [0, 74), y in [0, 74)
if ( sp.x >= 0 && sp.x < SlotSize && sp.y >= 0 && sp.y < SlotSize )
{
found = true;
if ( HotbarHoverCol != i )
{
HotbarHoverCol = i;
HoverEquipSlot = EquipSlot.None;
HoverBagIndex = -1;
}
break;
}
}
}
if ( !found && HotbarHoverCol != -1 )
HotbarHoverCol = -1;
}
protected override int BuildHash()
{
return System.HashCode.Combine(
IsInventoryOpen,
Inventory?.BagDataHash,
Inventory?.EquipDataHash,
Inventory?.HotbarActiveIndex,
System.HashCode.Combine(
DragFromIndex, DragFromEquipSlot, HoverBagIndex,
DragFromLootIndex, HoverLootIndex
),
System.HashCode.Combine(
HotbarHoverCol, IsDragging,
ContextMenuOpen, ContextMenuBagIndex,
ContextMenuEquipSlot
),
System.HashCode.Combine(
SearchText, CategoryFilter,
_openContainer?.DataHash
),
System.HashCode.Combine(
_selectedRecipe?.ResourceName,
_craftQuantity, _isCrafting, _craftQueueRemaining
)
);
}
// ─── Mouse Down ──────────────────────────────────────────────
private void OnBagSlotMouseDown( int slotIndex, PanelEvent e )
{
if ( e is MousePanelEvent me && me.MouseButton == MouseButtons.Right )
{
CloseContextMenu();
ShowBagContextMenu( slotIndex );
return;
}
if ( Inventory == null ) return;
var slot = Inventory.Bag.GetSlot( slotIndex );
if ( slot.IsEmpty ) return;
if ( !IsInventoryOpen )
{
if ( Inventory.UseHotbar && slotIndex >= Inventory.HotbarStartIndex )
Inventory.SetHotbarActive( slotIndex - Inventory.HotbarStartIndex );
return;
}
// Shift-click: auto-move without dragging
if ( ShiftHeld )
{
if ( _openContainer != null )
_openContainer.TransferFromPlayer( slotIndex, Inventory );
else if ( Inventory.UseEquipment && slot.Item.EquipSlot != EquipSlot.None )
Inventory.EquipFromBag( slotIndex );
return;
}
DragFromIndex = slotIndex;
DragFromEquipSlot = EquipSlot.None;
IsDragging = true;
}
private void OnEquipSlotMouseDown( EquipSlot slotType, PanelEvent e )
{
if ( e is MousePanelEvent me && me.MouseButton == MouseButtons.Right )
{
CloseContextMenu();
ShowEquipContextMenu( slotType );
return;
}
if ( Inventory?.Equipment == null ) return;
var slot = Inventory.Equipment.GetSlot( slotType );
if ( slot == null || slot.IsEmpty ) return;
// Shift-click: unequip straight to bag
if ( ShiftHeld )
{
Inventory.UnequipToBag( slotType );
return;
}
DragFromEquipSlot = slotType;
DragFromIndex = -1;
IsDragging = true;
}
// ─── Context Menu ─────────────────────────────────────────────
private void ShowBagContextMenu( int slotIndex )
{
if ( Inventory == null ) return;
var slot = Inventory.Bag.GetSlot( slotIndex );
if ( slot.IsEmpty ) return;
var scale = RootPanel?.ScaleFromScreen ?? 1f;
ContextMenuPos = new Vector2( Mouse.Position.x * scale, Mouse.Position.y * scale );
ContextMenuBagIndex = slotIndex;
ContextMenuEquipSlot = EquipSlot.None;
ContextMenuSlot = slot;
ContextMenuOpen = true;
}
private void ShowEquipContextMenu( EquipSlot slotType )
{
if ( Inventory?.Equipment == null ) return;
var slot = Inventory.Equipment.GetSlot( slotType );
if ( slot == null || slot.IsEmpty ) return;
var scale = RootPanel?.ScaleFromScreen ?? 1f;
ContextMenuPos = new Vector2( Mouse.Position.x * scale, Mouse.Position.y * scale );
ContextMenuBagIndex = -1;
ContextMenuEquipSlot = slotType;
ContextMenuSlot = slot;
ContextMenuOpen = true;
}
private void CloseContextMenu()
{
ContextMenuOpen = false;
ContextMenuSlot = null;
ContextMenuBagIndex = -1;
ContextMenuLootIndex = -1;
ContextMenuEquipSlot = EquipSlot.None;
}
private void OnContextEquip()
{
if ( Inventory != null && ContextMenuBagIndex >= 0 )
Inventory.EquipFromBag( ContextMenuBagIndex );
CloseContextMenu();
}
private void OnContextUnequip()
{
if ( Inventory != null && ContextMenuEquipSlot != EquipSlot.None )
Inventory.UnequipToBag( ContextMenuEquipSlot );
CloseContextMenu();
}
private void OnContextSplit()
{
if ( Inventory == null || ContextMenuBagIndex < 0 ) { CloseContextMenu(); return; }
var fromSlot = Inventory.Bag.GetSlot( ContextMenuBagIndex );
if ( fromSlot.IsEmpty || fromSlot.Quantity < 2 ) { CloseContextMenu(); return; }
for ( int i = 0; i < Inventory.Bag.SlotCount; i++ )
{
if ( i == ContextMenuBagIndex ) continue;
var target = Inventory.Bag.GetSlot( i );
if ( target.IsEmpty )
{
Inventory.SplitStack( ContextMenuBagIndex, i );
break;
}
}
CloseContextMenu();
}
private void OnContextUse()
{
if ( Inventory == null ) { CloseContextMenu(); return; }
if ( ContextMenuBagIndex >= 0 )
Inventory.UseItem( ContextMenuBagIndex );
else if ( ContextMenuEquipSlot != EquipSlot.None )
Inventory.UseEquipItem( ContextMenuEquipSlot );
CloseContextMenu();
}
private void OnContextDrop()
{
if ( Inventory == null ) { CloseContextMenu(); return; }
if ( ContextMenuBagIndex >= 0 )
Inventory.DropItem( ContextMenuBagIndex, ContextMenuSlot.Quantity );
else if ( ContextMenuEquipSlot != EquipSlot.None )
Inventory.DropEquipItem( ContextMenuEquipSlot, 1 );
CloseContextMenu();
}
private void OnContextDestroy()
{
if ( Inventory == null ) { CloseContextMenu(); return; }
if ( Inventory.EnableDestroyConfirm )
{
DestroyConfirmBagIndex = ContextMenuBagIndex;
DestroyConfirmEquipSlot = ContextMenuEquipSlot;
DestroyConfirmSlot = ContextMenuSlot;
ShowDestroyConfirm = true;
CloseContextMenu();
return;
}
DoDestroy();
}
private void ConfirmDestroy()
{
ShowDestroyConfirm = false;
if ( Inventory == null ) return;
int savedBagIndex = DestroyConfirmBagIndex;
var savedEquipSlot = DestroyConfirmEquipSlot;
var savedSlot = DestroyConfirmSlot;
DestroyConfirmSlot = null;
if ( savedBagIndex >= 0 )
Inventory.DestroyItem( savedBagIndex, savedSlot?.Quantity ?? 1 );
else if ( savedEquipSlot != EquipSlot.None )
Inventory.DestroyEquipItem( savedEquipSlot, 1 );
}
// ─── Loot Handlers ───────────────────────────────────────────
private void OnLootSlotMouseDown( int slotIndex, PanelEvent e )
{
if ( _openContainer == null ) return;
if ( e is MousePanelEvent me && me.MouseButton == MouseButtons.Right )
{
CloseContextMenu();
var slot = _openContainer.Container.GetSlot( slotIndex );
if ( slot.IsEmpty ) return;
var scale = RootPanel?.ScaleFromScreen ?? 1f;
ContextMenuPos = new Vector2( Mouse.Position.x * scale, Mouse.Position.y * scale );
ContextMenuLootIndex = slotIndex;
ContextMenuBagIndex = -1;
ContextMenuEquipSlot = EquipSlot.None;
ContextMenuSlot = slot;
ContextMenuOpen = true;
return;
}
var lootSlot = _openContainer.Container.GetSlot( slotIndex );
if ( lootSlot.IsEmpty ) return;
// Shift-click: take straight to bag
if ( ShiftHeld )
{
_openContainer.TransferSlotToPlayer( slotIndex, Inventory );
return;
}
DragFromLootIndex = slotIndex;
DragFromIndex = -1;
DragFromEquipSlot = EquipSlot.None;
IsDragging = true;
}
private void OnContextTake()
{
if ( _openContainer != null && ContextMenuLootIndex >= 0 && Inventory != null )
_openContainer.TransferSlotToPlayer( ContextMenuLootIndex, Inventory );
CloseContextMenu();
}
private void OnTakeAll()
{
if ( _openContainer != null && Inventory != null )
_openContainer.TransferAllToPlayer( Inventory );
}
private void DoDestroy()
{
if ( Inventory == null ) return;
if ( ContextMenuBagIndex >= 0 )
Inventory.DestroyItem( ContextMenuBagIndex, ContextMenuSlot.Quantity );
else if ( ContextMenuEquipSlot != EquipSlot.None )
Inventory.DestroyEquipItem( ContextMenuEquipSlot, 1 );
}
private void CancelDestroy()
{
ShowDestroyConfirm = false;
DestroyConfirmSlot = null;
}
// ─── Root Mouse Up ───────────────────────────────────────────
private void OnRootMouseUp( PanelEvent e )
{
if ( ContextMenuOpen )
{
if ( e is MousePanelEvent me && me.MouseButton == MouseButtons.Right )
return;
CloseContextMenu();
return;
}
if ( !IsDragging )
{
CancelDrag();
return;
}
if ( DragFromLootIndex != -1 && _openContainer != null )
{
if ( HoverLootIndex != -1 && HoverLootIndex != DragFromLootIndex )
{
// Loot → loot: swap or merge within the container
var from = _openContainer.Container.GetSlot( DragFromLootIndex );
var to = _openContainer.Container.GetSlot( HoverLootIndex );
if ( !to.IsEmpty && to.Item == from.Item && to.Quantity < to.Item.MaxStack )
{
int space = to.Item.MaxStack - to.Quantity;
int move = Math.Min( space, from.Quantity );
to.Add( from.Item, move );
from.Remove( move );
_openContainer.ForcePush();
}
else
{
_openContainer.SwapSlots( DragFromLootIndex, HoverLootIndex );
}
}
else if ( HoverBagIndex != -1 || HotbarHoverCol != -1 )
{
// Loot → bag or hotbar
_openContainer.TransferSlotToPlayer( DragFromLootIndex, Inventory );
}
CancelDrag();
return;
}
if ( DragFromIndex != -1 )
{
// Bag → loot panel
if ( HoverLootIndex != -1 && _openContainer != null )
{
_openContainer.TransferFromPlayer( DragFromIndex, Inventory, HoverLootIndex );
CancelDrag();
return;
}
if ( HoverEquipSlot != EquipSlot.None )
{
Inventory?.EquipFromBag( DragFromIndex );
}
else if ( HotbarHoverCol != -1 )
{
int targetIndex = Inventory.HotbarStartIndex + HotbarHoverCol;
if ( targetIndex != DragFromIndex )
{
if ( IsValidSlot( targetIndex ) )
TryMergeOrSwap( DragFromIndex, targetIndex );
}
}
else if ( HoverBagIndex != -1 && HoverBagIndex != DragFromIndex )
{
if ( IsValidSlot( HoverBagIndex ) )
TryMergeOrSwap( DragFromIndex, HoverBagIndex );
}
}
else if ( DragFromEquipSlot != EquipSlot.None )
{
int targetIndex = -1;
if ( HotbarHoverCol != -1 )
targetIndex = Inventory.HotbarStartIndex + HotbarHoverCol;
else if ( HoverBagIndex != -1 )
targetIndex = HoverBagIndex;
if ( targetIndex >= 0 && IsValidSlot( targetIndex ) )
{
var targetSlot = Inventory.Bag.GetSlot( targetIndex );
var draggedItem = Inventory?.Equipment?.GetSlot( DragFromEquipSlot )?.Item;
if ( draggedItem != null )
{
if ( !targetSlot.IsEmpty && targetSlot.Item.EquipSlot == DragFromEquipSlot )
{
// Swap: unequip current, place dragged into that bag slot, equip what was in bag
Inventory.UnequipToBag( DragFromEquipSlot );
Inventory.EquipFromBag( targetIndex );
}
else
{
// Move to empty bag slot (or bag slot with same item to merge)
Inventory.UnequipToBag( DragFromEquipSlot );
// UnequipToBag puts item into first open slot; find it and swap to target
int landedAt = FindLastAdded( draggedItem );
if ( landedAt >= 0 && landedAt != targetIndex )
Inventory.SwapSlots( landedAt, targetIndex );
}
}
}
else
{
Inventory?.UnequipToBag( DragFromEquipSlot );
}
}
CancelDrag();
}
// ─── Helpers ─────────────────────────────────────────────────
private void TryMergeOrSwap( int fromIndex, int toIndex )
{
if ( Inventory == null ) return;
var from = Inventory.Bag.GetSlot( fromIndex );
var to = Inventory.Bag.GetSlot( toIndex );
if ( !to.IsEmpty && to.Item == from.Item && to.Quantity < to.Item.MaxStack )
Inventory.MergeStacks( fromIndex, toIndex );
else
Inventory.SwapSlots( fromIndex, toIndex );
}
private bool IsValidSlot( int index )
{
if ( Inventory?.Bag == null ) return false;
return index >= 0 && index < Inventory.Bag.SlotCount;
}
private bool ItemPassesFilter( InventorySlot slot )
{
if ( slot.IsEmpty ) return true;
if ( CategoryFilter.HasValue && slot.Item.Category != CategoryFilter.Value )
return false;
if ( !string.IsNullOrWhiteSpace( SearchText ) )
{
var q = SearchText.Trim().ToLower();
if ( !slot.Item.ItemName.ToLower().Contains( q )
&& !slot.Item.Description.ToLower().Contains( q ) )
return false;
}
return true;
}
private float GetEquippedStat( InventoryItem equipped, StatType type )
{
if ( equipped?.StatModifiers == null ) return 0f;
float total = 0f;
foreach ( var mod in equipped.StatModifiers )
if ( mod.Type == type ) total += mod.Value;
return total;
}
private int FindLastAdded( InventoryItem item )
{
if ( Inventory?.Bag == null || item == null ) return -1;
for ( int i = 0; i < Inventory.Bag.SlotCount; i++ )
{
var s = Inventory.Bag.GetSlot( i );
if ( !s.IsEmpty && s.Item == item ) return i;
}
return -1;
}
private void CancelDrag()
{
DragFromIndex = -1;
DragFromEquipSlot = EquipSlot.None;
DragFromLootIndex = -1;
IsDragging = false;
HoverBagIndex = -1;
HoverLootIndex = -1;
HotbarHoverCol = -1;
HoverEquipSlot = EquipSlot.None;
}
private void CloseInventory()
{
IsInventoryOpen = false;
CloseContainer();
CloseStation();
CancelDrag();
CloseContextMenu();
SearchText = "";
CategoryFilter = null;
StateHasChanged();
}
}