(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
@@ -123,7 +123,7 @@ int32 UInventoryComponent::AddItem(UItemDataAsset* Item, int32 Quantity, int32 R
if (Remaining < Quantity)
{
OnInventoryChanged.Broadcast();
BroadcastChanged();
}
return Remaining;
@@ -172,7 +172,7 @@ int32 UInventoryComponent::RemoveItem(UItemDataAsset* Item, int32 Quantity)
const int32 Removed = Quantity - Remaining;
if (Removed > 0)
{
OnInventoryChanged.Broadcast();
BroadcastChanged();
}
return Removed;
@@ -202,7 +202,7 @@ bool UInventoryComponent::ConsumeUse(int32 SlotIndex)
Slot.Clear();
}
OnInventoryChanged.Broadcast();
BroadcastChanged();
return true;
}
@@ -223,7 +223,7 @@ bool UInventoryComponent::ConsumeUse(int32 SlotIndex)
}
}
OnInventoryChanged.Broadcast();
BroadcastChanged();
return true;
}
@@ -327,86 +327,50 @@ bool UInventoryComponent::TakeFromSlot(int32 SlotIndex, int32 Quantity, FInvento
Slot.Clear();
}
OnInventoryChanged.Broadcast();
BroadcastChanged();
return true;
}
bool UInventoryComponent::MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity)
{
if (Quantity <= 0 || FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
{
return false;
}
FInventorySlot& From = Slots[FromIndex];
if (From.IsEmpty())
{
return false;
}
// Deplacement total : on retombe sur le comportement complet, echange compris.
if (Quantity >= From.Quantity)
{
return MoveItem(FromIndex, ToIndex);
}
FInventorySlot& To = Slots[ToIndex];
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
if (To.IsEmpty())
{
const int32 Transferred = FMath::Min(Quantity, MaxStack);
To.Item = From.Item;
To.Quantity = Transferred;
To.RemainingUses = From.RemainingUses;
From.Quantity -= Transferred;
OnInventoryChanged.Broadcast();
return true;
}
if (To.Item == From.Item)
{
const int32 Transferable = FMath::Min(Quantity, MaxStack - To.Quantity);
if (Transferable <= 0)
{
return false;
}
To.Quantity += Transferable;
From.Quantity -= Transferable;
OnInventoryChanged.Broadcast();
return true;
}
// Objet different et transfert partiel : aucune interpretation raisonnable.
// On refuse plutot que d'inventer un comportement que le joueur ne
// comprendrait pas.
return false;
return TransferSlot(this, FromIndex, this, ToIndex, Quantity);
}
bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
{
if (FromIndex == ToIndex || !Slots.IsValidIndex(FromIndex) || !Slots.IsValidIndex(ToIndex))
// MAX_int32 depasse forcement la pile : TransferSlot bascule alors sur le
// deplacement total, echange compris.
return TransferSlot(this, FromIndex, this, ToIndex, MAX_int32);
}
bool UInventoryComponent::ApplySlotMove(FInventorySlot& From, FInventorySlot& To, int32 Quantity)
{
if (Quantity <= 0 || From.IsEmpty())
{
return false;
}
FInventorySlot& From = Slots[FromIndex];
if (From.IsEmpty())
{
return false;
}
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
const bool bTotal = (Quantity >= From.Quantity);
FInventorySlot& To = Slots[ToIndex];
// Cible vide : la pile entiere part, quelle que soit sa taille.
if (To.IsEmpty())
{
To = From;
From.Clear();
OnInventoryChanged.Broadcast();
// Deplacement total : la pile entiere part, quelle que soit sa taille --
// on ne la tronque pas a MaxStack, elle etait deja legale a la source.
if (bTotal)
{
To = From;
From.Clear();
return true;
}
const int32 Transferred = FMath::Min(Quantity, MaxStack);
To.Item = From.Item;
To.Quantity = Transferred;
// L'usure suit l'objet : diviser une pile de canettes entamees donne
// deux piles au meme niveau, jamais une neuve.
To.RemainingUses = From.RemainingUses;
From.Quantity -= Transferred;
return true;
}
@@ -415,15 +379,21 @@ bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
// l'echange -- deux canettes entamees ne fusionnent jamais.
if (To.Item == From.Item)
{
const int32 MaxStack = From.Item->GetEffectiveMaxStack();
const int32 Transferable = FMath::Min(From.Quantity, MaxStack - To.Quantity);
const int32 Wanted = bTotal ? From.Quantity : Quantity;
const int32 Transferable = FMath::Min(Wanted, MaxStack - To.Quantity);
// Cible deja pleine : on echange plutot que de ne rien faire, sinon le
// geste du joueur resterait sans effet et passerait pour un bug.
if (Transferable <= 0)
{
// Cible pleine. Sur un deplacement total on echange plutot que de
// ne rien faire, sinon le geste du joueur passerait pour un bug ;
// sur un deplacement partiel on refuse, echanger la moitie d'une
// pile contre autre chose n'ayant aucun sens.
if (!bTotal)
{
return false;
}
Swap(From, To);
OnInventoryChanged.Broadcast();
return true;
}
@@ -434,16 +404,168 @@ bool UInventoryComponent::MoveItem(int32 FromIndex, int32 ToIndex)
From.Clear();
}
OnInventoryChanged.Broadcast();
return true;
}
// Objets differents : simple echange.
// Objets differents : echange, et seulement sur un deplacement total. Il
// n'existe aucune facon sensee d'echanger la moitie d'une pile contre autre
// chose -- on refuse plutot que d'inventer un comportement incomprehensible.
if (!bTotal)
{
return false;
}
Swap(From, To);
OnInventoryChanged.Broadcast();
return true;
}
bool UInventoryComponent::TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity)
{
if (!From || !To || Quantity <= 0)
{
return false;
}
// Meme inventaire et meme case : rien a faire. Le test doit porter sur le
// couple et non sur l'index seul, deux inventaires differents ayant tous
// les deux une case 3.
if (From == To && FromIndex == ToIndex)
{
return false;
}
if (!From->Slots.IsValidIndex(FromIndex) || !To->Slots.IsValidIndex(ToIndex))
{
return false;
}
// Deux references dans le meme tableau quand From == To : legal ici parce
// qu'aucun redimensionnement n'a lieu entre les deux acces.
if (!ApplySlotMove(From->Slots[FromIndex], To->Slots[ToIndex], Quantity))
{
return false;
}
From->BroadcastChanged();
if (To != From)
{
To->BroadcastChanged();
}
return true;
}
int32 UInventoryComponent::PushSlotInto(int32 SlotIndex, UInventoryComponent& To)
{
if (!Slots.IsValidIndex(SlotIndex))
{
return 0;
}
FInventorySlot& Slot = Slots[SlotIndex];
if (Slot.IsEmpty())
{
return 0;
}
const int32 Left = To.AddItem(Slot.Item, Slot.Quantity, Slot.RemainingUses);
const int32 Moved = Slot.Quantity - Left;
if (Moved <= 0)
{
return 0;
}
Slot.Quantity = Left;
if (Slot.Quantity <= 0)
{
Slot.Clear();
}
return Moved;
}
int32 UInventoryComponent::QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To)
{
if (!From || !To || From == To)
{
return 0;
}
const int32 Moved = From->PushSlotInto(FromIndex, *To);
if (Moved > 0)
{
From->BroadcastChanged();
}
return Moved;
}
int32 UInventoryComponent::TransferAllTo(UInventoryComponent* To, int32 FirstIndex)
{
if (!To || To == this)
{
return 0;
}
To->BeginBatch();
int32 Moved = 0;
for (int32 Index = FMath::Max(0, FirstIndex); Index < Slots.Num(); ++Index)
{
Moved += PushSlotInto(Index, *To);
}
To->EndBatch();
if (Moved > 0)
{
BroadcastChanged();
}
return Moved;
}
void UInventoryComponent::ConfigureAsContainer(int32 NewSlotCount)
{
// Aucune barre rapide : GetBackpackStartIndex() vaut alors 0, et la grille
// affiche la totalite du conteneur au lieu d'en masquer les dix premieres
// cases.
HotbarSlotCount = 0;
BonusSlotCount = 0;
BackpackSlotCount = FMath::Max(0, NewSlotCount);
SelectedHotbarIndex = 0;
EnsureSlotCount();
}
void UInventoryComponent::BroadcastChanged()
{
if (BatchDepth > 0)
{
bBatchDirty = true;
return;
}
OnInventoryChanged.Broadcast();
}
void UInventoryComponent::BeginBatch()
{
++BatchDepth;
}
void UInventoryComponent::EndBatch()
{
BatchDepth = FMath::Max(0, BatchDepth - 1);
if (BatchDepth > 0 || !bBatchDirty)
{
return;
}
bBatchDirty = false;
OnInventoryChanged.Broadcast();
}
bool UInventoryComponent::SetBonusSlotCount(int32 NewBonusSlots)
{
const int32 NewTotal = FMath::Max(0, HotbarSlotCount + BackpackSlotCount + FMath::Max(0, NewBonusSlots));
@@ -463,7 +585,7 @@ bool UInventoryComponent::SetBonusSlotCount(int32 NewBonusSlots)
CompactSlots();
Slots.SetNum(NewTotal);
OnInventoryChanged.Broadcast();
BroadcastChanged();
return true;
}