Files
Mathew 41d6e6a7f3 (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>
2026-08-01 22:51:24 +02:00

318 lines
12 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"
class UItemDataAsset;
/**
* Contenu d'un emplacement. Item nul = slot vide.
* Le tableau de slots a toujours exactement la taille de l'inventaire :
* l'index dans le tableau EST la position dans la grille, ce qui rend
* l'affichage et le futur drag & drop triviaux.
*/
USTRUCT(BlueprintType)
struct FInventorySlot
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
int32 Quantity = 0;
/**
* Charges restantes de l'exemplaire courant, pour les objets a usages
* multiples. Etat par instance, d'ou sa place ici et pas sur le DataAsset :
* une canette entamee ne doit pas entamer toutes les canettes du jeu.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventaire")
int32 RemainingUses = 0;
bool IsEmpty() const { return Item == nullptr || Quantity <= 0; }
void Clear()
{
Item = nullptr;
Quantity = 0;
RemainingUses = 0;
}
};
/** Diffuse a chaque modification du contenu ou de la taille. L'UI s'y abonne. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChanged);
/** Diffuse quand le joueur change de slot actif dans la barre rapide. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSelectedHotbarSlotChanged, int32, NewIndex);
/**
* Inventaire en grille a slots, facon Raft / Valheim.
*
* Le nombre de slots est dynamique : BaseSlotCount fixe le minimum, et
* BonusSlotCount permet a un sac equipe de l'agrandir. Retirer le sac
* refuse si les objets ne rentrent plus, plutot que de les faire
* disparaitre en silence.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class SURVIVAL_PROJET_API UInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
UInventoryComponent();
virtual void InitializeComponent() override;
/**
* Ajoute des objets, en remplissant d'abord les piles entamees.
* @param RemainingUses charges de l'exemplaire ajoute ; -1 signifie "plein".
* Sert a rendre son etat a un objet ramasse au sol.
* @return le nombre d'exemplaires qui N'ONT PAS pu rentrer (0 = tout est passe).
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 AddItem(UItemDataAsset* Item, int32 Quantity = 1, int32 RemainingUses = -1);
/**
* Consomme une utilisation du slot.
* Retire un exemplaire quand il ne reste plus de charge.
* @return true si quelque chose a ete consomme.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool ConsumeUse(int32 SlotIndex);
/**
* Retire des objets, en vidant d'abord les piles les plus petites.
* @return le nombre d'exemplaires reellement retires.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 RemoveItem(UItemDataAsset* Item, int32 Quantity = 1);
/** Combien d'exemplaires de cet objet, toutes piles confondues. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetItemCount(const UItemDataAsset* Item) const;
/** Combien d'exemplaires pourraient encore rentrer, sans rien modifier. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetRoomFor(const UItemDataAsset* Item) const;
/**
* Un seul tableau, deux conteneurs distincts :
* index 0 .. HotbarSlotCount-1 -> barre rapide
* index HotbarSlotCount .. fin -> grille d'inventaire (+ bonus de sac)
*
* Un objet range dans la barre n'apparait PAS dans la grille, et
* inversement. Ils partagent le meme tableau uniquement pour que le
* glisser-deposer entre les deux ne demande aucune conversion d'index.
*/
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetTotalSlotCount() const { return FMath::Max(0, HotbarSlotCount + BackpackSlotCount + BonusSlotCount); }
/** Premier index affiche par la grille, juste apres la barre rapide. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetBackpackStartIndex() const { return GetHotbarSlotCount(); }
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetUsedSlotCount() const;
UFUNCTION(BlueprintPure, Category = "Inventaire")
const TArray<FInventorySlot>& GetSlots() const { return Slots; }
/**
* Deplace la pile entiere d'un slot vers un autre.
* - cible vide -> la pile entiere part
* - meme objet -> fusion jusqu'a MaxStackSize, le reste demeure a la source
* - objet different -> echange des deux slots
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool MoveItem(int32 FromIndex, int32 ToIndex);
/**
* Deplace seulement une partie d'une pile.
*
* Un transfert partiel vers un slot occupe par un objet DIFFERENT est
* refuse : il n'existe aucune facon sensee d'echanger la moitie d'une pile
* contre autre chose. Seul un deplacement total peut provoquer un echange.
*
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool MoveItemQuantity(int32 FromIndex, int32 ToIndex, int32 Quantity);
// ------------------------------------------------------------------
// Transferts entre deux inventaires (coffres)
// ------------------------------------------------------------------
/**
* Deplace le contenu d'un slot vers un autre, dans le MEME inventaire ou
* dans un autre. C'est desormais l'unique implementation du deplacement :
* MoveItem() et MoveItemQuantity() ne sont que des appels avec From == To.
*
* Statique parce qu'elle n'appartient ni a la source ni a la destination --
* elle arbitre entre les deux, et doit diffuser OnInventoryChanged sur les
* DEUX. Oublier la seconde diffusion laisse la grille d'en face afficher un
* etat perime jusqu'au prochain evenement, ce qui se lit comme un objet
* qui disparait.
*
* @param Quantity nombre d'exemplaires ; MAX_int32 pour la pile entiere.
* @return true si quelque chose a change.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
static bool TransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To, int32 ToIndex, int32 Quantity);
/**
* Envoie le contenu d'un slot vers un autre inventaire sans designer de
* case d'arrivee : c'est le clic droit de l'ecran de coffre.
*
* Passe par AddItem(), donc exactement comme un ramassage -- piles entamees
* d'abord, puis barre rapide, puis grille. Ce qui ne rentre pas reste sur
* place : un transfert rapide ne perd jamais rien.
*
* @return le nombre d'exemplaires reellement partis.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
static int32 QuickTransferSlot(UInventoryComponent* From, int32 FromIndex, UInventoryComponent* To);
/**
* Deverse tout ce qui peut entrer vers un autre inventaire : les boutons
* "Tout ranger" et "Tout prendre".
*
* @param FirstIndex premier slot considere. Le joueur passe
* GetBackpackStartIndex() pour que "Tout ranger" laisse sa barre
* rapide tranquille -- ranger ses outils dans le coffre a chaque
* retour d'expedition serait une punition, pas un confort.
* @return le nombre d'exemplaires reellement partis.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 TransferAllTo(UInventoryComponent* To, int32 FirstIndex = 0);
/**
* Transforme ce composant en contenant du monde : NewSlotCount cases de
* grille et AUCUNE barre rapide, pour que GetBackpackStartIndex() vaille 0
* et que la grille affiche la totalite du coffre.
*
* A appeler avant que le composant contienne quoi que ce soit -- typiquement
* depuis PreInitializeComponents() de l'acteur porteur. Reduire la taille
* d'un conteneur deja rempli fait disparaitre ce qui depasse.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void ConfigureAsContainer(int32 NewSlotCount);
/** Index du premier slot libre, ou INDEX_NONE si l'inventaire est plein. */
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 FindFirstEmptySlot() const;
/**
* Retire jusqu'a Quantity exemplaires d'un slot precis et renvoie ce qui a
* ete retire. Sert au drop au sol et, plus tard, au transfert vers un coffre.
*
* Different de RemoveItem() qui pioche par type sans se soucier du slot :
* ici le joueur a designe une case, on ne touche qu'a celle-la.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool TakeFromSlot(int32 SlotIndex, int32 Quantity, FInventorySlot& OutTaken);
/**
* Change le bonus de slots apporte par l'equipement (sac).
* @return false si reduire la taille laisserait des objets dehors ;
* dans ce cas rien n'est modifie et le sac ne doit pas etre retire.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool SetBonusSlotCount(int32 NewBonusSlots);
UPROPERTY(BlueprintAssignable, Category = "Inventaire")
FOnInventoryChanged OnInventoryChanged;
// ------------------------------------------------------------------
// Barre rapide
// ------------------------------------------------------------------
//
// La barre rapide n'est PAS un jeu de slots separe : ce sont les
// HotbarSlotCount premiers slots de l'inventaire, simplement affiches
// deux fois. Consequence directe : le glisser-deposer entre la barre et
// la grille fonctionne sans une ligne de code de plus, puisque ce sont
// les memes index dans le meme tableau.
/** Change le slot actif. Un index hors de la barre est ignore. */
UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
void SelectHotbarSlot(int32 Index);
/** Avance ou recule dans la barre, en boucle. */
UFUNCTION(BlueprintCallable, Category = "Inventaire|Barre rapide")
void CycleHotbarSelection(int32 Delta);
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
int32 GetSelectedHotbarIndex() const { return SelectedHotbarIndex; }
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
int32 GetHotbarSlotCount() const { return FMath::Min(HotbarSlotCount, Slots.Num()); }
/** Contenu du slot actif. C'est ce que le joueur "a en main". */
UFUNCTION(BlueprintPure, Category = "Inventaire|Barre rapide")
FInventorySlot GetSelectedSlot() const;
UPROPERTY(BlueprintAssignable, Category = "Inventaire|Barre rapide")
FOnSelectedHotbarSlotChanged OnSelectedHotbarSlotChanged;
protected:
/** Cases affichees dans la grille d'inventaire. N'inclut PAS la barre rapide. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "0"))
int32 BackpackSlotCount = 20;
/** Slots supplementaires apportes par l'equipement (sac). Passe par SetBonusSlotCount(). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
int32 BonusSlotCount = 0;
/**
* Cases de la barre rapide, independantes de celles de la grille.
* Elles occupent les premiers index du tableau.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire|Barre rapide", meta = (ClampMin = "0"))
int32 HotbarSlotCount = 10;
/** Toujours dimensionne a GetTotalSlotCount(). */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
TArray<FInventorySlot> Slots;
private:
/** Redimensionne le tableau si la taille cible a change. */
void EnsureSlotCount();
/** Regroupe les slots occupes au debut du tableau. */
void CompactSlots();
/**
* Regles de deplacement entre deux cases, sans savoir a quel inventaire
* chacune appartient. Ne diffuse rien : c'est l'appelant qui sait combien
* de composants ont bouge.
*/
static bool ApplySlotMove(FInventorySlot& From, FInventorySlot& To, int32 Quantity);
/**
* Verse un slot dans un autre inventaire via AddItem(). Ne diffuse que du
* cote destination (AddItem s'en charge) : la source reste a la charge de
* l'appelant, qui peut en enchainer vingt.
* @return exemplaires reellement partis.
*/
int32 PushSlotInto(int32 SlotIndex, UInventoryComponent& To);
/**
* Diffusion differee. Un "Tout ranger" deplace jusqu'a vingt piles : sans
* regroupement, l'UI se reconstruirait vingt fois pour un seul clic.
* Toutes les modifications passent par BroadcastChanged() plutot que par
* OnInventoryChanged.Broadcast() directement, sinon le lot fuit.
*/
void BroadcastChanged();
void BeginBatch();
void EndBatch();
int32 SelectedHotbarIndex = 0;
int32 BatchDepth = 0;
bool bBatchDirty = false;
};