(Feat) Add Storage Chests + Item Details Panel

Coffres : AStorageContainer, un UInventoryComponent configure en conteneur,
affiche comme panneau de WBP_InventoryScreen plutot que dans un ecran a lui.
Deplacement unifie par UInventoryComponent::TransferSlot / TransferAllTo.

Panneau de detail : UItemDetailsWidget, pose DANS WBP_Inventory a droite de la
grille. Icone, nom, separation, description, barre d'usure. Il recoit un couple
(inventaire, index) et s'abonne a OnInventoryChanged, donc une charge consommee
sous le curseur se voit.

Une case ne connait plus sa grille : elle diffuse OnHoverChanged. La barre
rapide s'y abonne aussi et relaie par le PlayerController, seul a posseder a la
fois le HUD et l'ecran. Le panneau efface la case qu'on QUITTE et non lui-meme,
sinon passer de la barre a la grille viderait ce qui vient d'etre affiche.

L'ecran allume et eteint le panneau (SetItemDetailsEnabled) : WBP_Inventory
etant instancie deux fois en mode coffre, une case a cocher par instance serait
un bug qui attend qu'on oublie de la decocher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 22:51:24 +02:00
parent d50ef564ba
commit 41d6e6a7f3
50 changed files with 2947 additions and 167 deletions
@@ -0,0 +1,181 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "ItemDetailsWidget.h"
#include "Components/Image.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "Components/Widget.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
void UItemDetailsWidget::NativeConstruct()
{
Super::NativeConstruct();
// Etat vide des la construction : sans ca le panneau afficherait les valeurs
// placeholder saisies dans la Designer jusqu'au premier survol.
Clear();
}
void UItemDetailsWidget::NativeDestruct()
{
BindInventory(nullptr);
Super::NativeDestruct();
}
void UItemDetailsWidget::BindInventory(UInventoryComponent* InInventory)
{
if (BoundInventory.Get() == InInventory)
{
return;
}
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UItemDetailsWidget::HandleInventoryChanged);
}
BoundInventory = InInventory;
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.AddDynamic(this, &UItemDetailsWidget::HandleInventoryChanged);
}
}
void UItemDetailsWidget::ShowSlot(UInventoryComponent* InInventory, int32 InSlotIndex)
{
BindInventory(InInventory);
SlotIndex = InSlotIndex;
Refresh();
}
void UItemDetailsWidget::Clear()
{
BindInventory(nullptr);
SlotIndex = INDEX_NONE;
Refresh();
}
void UItemDetailsWidget::ClearSlot(const UInventoryComponent* InInventory, int32 InSlotIndex)
{
if (BoundInventory.Get() == InInventory && SlotIndex == InSlotIndex)
{
Clear();
}
}
void UItemDetailsWidget::HandleInventoryChanged()
{
// La case survolee a change sous le curseur : une charge consommee, une pile
// deplacee. On relit plutot que de garder une copie perimee a l'ecran.
Refresh();
}
void UItemDetailsWidget::Refresh()
{
// Surtout pas "Slot" : UWidget a deja un membre de ce nom, et le masquer est
// une erreur de compilation (C4458) traitee comme telle par le moteur.
const FInventorySlot* HoveredSlot = nullptr;
if (BoundInventory.IsValid())
{
const TArray<FInventorySlot>& Slots = BoundInventory->GetSlots();
if (Slots.IsValidIndex(SlotIndex) && !Slots[SlotIndex].IsEmpty())
{
HoveredSlot = &Slots[SlotIndex];
}
}
if (ContentRoot)
{
// Hidden et non Collapsed : le panneau garde sa taille, donc rien ne
// bouge dans la mise en page quand le curseur quitte une case.
ContentRoot->SetVisibility(HoveredSlot ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Hidden);
}
if (!HoveredSlot)
{
ClearDisplay();
return;
}
const UItemDataAsset* Item = HoveredSlot->Item;
if (ItemIcon)
{
if (Item->Icon)
{
ItemIcon->SetBrushFromTexture(Item->Icon, /*bMatchSize=*/false);
ItemIcon->SetVisibility(ESlateVisibility::HitTestInvisible);
}
else
{
ItemIcon->SetVisibility(ESlateVisibility::Hidden);
}
}
if (ItemNameText)
{
ItemNameText->SetText(Item->DisplayName);
}
if (ItemDescriptionText)
{
ItemDescriptionText->SetText(Item->Description);
}
// --- Usure ---
// Meme regle que sur la case de la grille : rien a montrer sur un objet a
// usage unique, et le bloc se replie plutot que de laisser un vide.
const int32 MaxUses = FMath::Max(1, Item->MaxUses);
const bool bHasUses = MaxUses > 1;
if (UsesPanel)
{
UsesPanel->SetVisibility(bHasUses ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed);
}
if (bHasUses)
{
if (UsesBar)
{
UsesBar->SetPercent(FMath::Clamp(static_cast<float>(HoveredSlot->RemainingUses) / static_cast<float>(MaxUses), 0.f, 1.f));
}
if (UsesText)
{
UsesText->SetText(FText::Format(INVTEXT("{0} / {1}"),
FText::AsNumber(HoveredSlot->RemainingUses), FText::AsNumber(MaxUses)));
}
}
}
void UItemDetailsWidget::ClearDisplay()
{
if (ItemIcon)
{
// Hidden et non Collapsed : l'icone garde sa place, donc le nom et la
// description ne remontent pas d'un cran au moment de disparaitre.
ItemIcon->SetVisibility(ESlateVisibility::Hidden);
}
if (ItemNameText)
{
ItemNameText->SetText(FText::GetEmpty());
}
if (ItemDescriptionText)
{
ItemDescriptionText->SetText(FText::GetEmpty());
}
if (UsesPanel)
{
UsesPanel->SetVisibility(ESlateVisibility::Collapsed);
}
}