(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,239 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "StorageContainer.generated.h"
class UBoxComponent;
class UInventoryComponent;
class UItemDataAsset;
class USoundBase;
class UStaticMeshComponent;
/**
* Contenu initial d'un coffre pose dans le niveau.
*
* De la donnee de level design, pas du gameplay : un coffre de depart, une
* cache a trouver. Verse une seule fois au BeginPlay, jamais relu ensuite.
*/
USTRUCT(BlueprintType)
struct FStorageStartingItem
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UItemDataAsset> Item = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage", meta = (ClampMin = "1"))
int32 Quantity = 1;
};
/**
* Coffre pose dans le monde.
*
* Il ne contient AUCUN modele de donnees a lui : son contenu est un
* UInventoryComponent, exactement le meme que celui du joueur, simplement
* configure sans barre rapide. C'est ce qui rend le glisser-deposer entre le
* sac et le coffre gratuit -- ce sont les memes cases, le meme FInventorySlot,
* et donc la durabilite d'une hache traverse le coffre sans une ligne de code.
*
* Toute la collision tient dans UNE boite, et les deux meshes n'en portent
* aucune. C'est la difference avec ACraftingStation, qui se contente de son
* mesh : un coffre est en deux morceaux dont l'un PIVOTE. Se reposer sur leurs
* collisions imposerait que le pack en fournisse deux correctes, et laisserait
* surtout un trou dans la surface visable des que le couvercle se leve --
* viser le haut du coffre ouvert ne toucherait plus rien.
*
* La boite est fille de la caisse et ne bouge donc jamais : le coffre se vise
* exactement pareil ouvert ou ferme. Elle bloque aussi le joueur physiquement,
* ce qui evite d'avoir a se demander si le mesh du pack sait le faire.
*
* Aucun Tick au repos : il ne s'active que le temps d'animer le couvercle, et
* se rendort des que l'angle est atteint.
*/
UCLASS()
class SURVIVAL_PROJET_API AStorageContainer : public AActor, public IInteractable
{
GENERATED_BODY()
public:
AStorageContainer();
virtual void Tick(float DeltaTime) override;
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
UFUNCTION(BlueprintPure, Category = "Storage")
UInventoryComponent* GetStorage() const { return Storage; }
/** Nom affiche en titre de l'ecran de coffre. */
UFUNCTION(BlueprintPure, Category = "Storage")
FText GetDisplayName() const { return DisplayName; }
/** Cases par ligne souhaitees pour la grille de l'ecran. */
UFUNCTION(BlueprintPure, Category = "Storage")
int32 GetColumnCount() const { return ColumnCount; }
/**
* Ouvre ou ferme le couvercle.
*
* Appele par le controller, pas par le coffre lui-meme : c'est lui qui sait
* quand l'ecran se referme, que ce soit par Echap, par la mort du joueur ou
* parce qu'il s'est trop eloigne. Un coffre qui deciderait seul finirait
* ouvert dans la moitie de ces cas.
*/
UFUNCTION(BlueprintCallable, Category = "Storage")
void SetOpen(bool bInOpen);
UFUNCTION(BlueprintPure, Category = "Storage")
bool IsOpen() const { return bOpen; }
protected:
/**
* Dimensionne le composant AVANT que son InitializeComponent() fige la
* taille par defaut. PostInitializeComponents serait deja trop tard, et le
* constructeur trop tot -- il ne connait pas les valeurs reglees sur
* l'instance posee dans le niveau.
*/
virtual void PreInitializeComponents() override;
virtual void BeginPlay() override;
/** Redimensionne la boite quand un mesh change dans le Blueprint. */
virtual void OnConstruction(const FTransform& Transform) override;
// ------------------------------------------------------------------
// Composants
// ------------------------------------------------------------------
/** La caisse : SM_Chest_Bottom. Racine, purement visuelle. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UStaticMeshComponent> BaseMesh;
/**
* L'unique collision du coffre : ce que le trace d'interaction vient
* toucher, et ce qui empeche le joueur de traverser.
*
* Fille de la caisse et non du pivot : elle ne suit surtout pas le
* couvercle. Un coffre ouvert doit se viser au meme endroit qu'un coffre
* ferme, sinon le prompt disparait au moment precis ou l'on s'en sert.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UBoxComponent> InteractionBox;
/**
* La charniere. Place-la a la souris sur l'arete arriere du coffre.
*
* Ce composant vide n'est pas un luxe : le pivot de SM_Chest_Top est celui
* que le pack lui a donne, presque jamais la charniere. Faire tourner le
* mesh directement le ferait pivoter autour de son centre et traverser la
* caisse. Ici on tourne le pivot, et le mesh suit avec son decalage.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<USceneComponent> LidPivot;
/** Le couvercle : SM_Chest_Top, enfant du pivot. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UStaticMeshComponent> LidMesh;
/** Le contenu. Un inventaire ordinaire, sans barre rapide. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UInventoryComponent> Storage;
// ------------------------------------------------------------------
// Reglages
// ------------------------------------------------------------------
/**
* Nombre de cases. Reglable PAR INSTANCE : le meme Blueprint donne un petit
* coffre de 12 et un grand de 30 selon celui qu'on pose.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage", meta = (ClampMin = "1", ClampMax = "60"))
int32 SlotCount = 20;
/** Cases par ligne dans l'ecran. Purement visuel. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage", meta = (ClampMin = "1"))
int32 ColumnCount = 6;
/** Nom affiche en titre de l'ecran. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
FText DisplayName;
/** Texte affiche quand le joueur vise le coffre. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
FText InteractionPrompt;
/** Verse dans le coffre au demarrage. Laisse vide pour un coffre a remplir. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage")
TArray<FStorageStartingItem> StartingItems;
// ------------------------------------------------------------------
// Collision
// ------------------------------------------------------------------
/**
* La boite epouse toute seule les deux meshes, couvercle ferme.
*
* Coche par defaut : changer SM_Chest_v1 pour le v3 doit suffire, sans
* avoir a retailler une boite a la main derriere. Decoche-le seulement si
* tu veux une zone volontairement differente du volume du coffre.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Collision")
bool bAutoFitInteractionBox = true;
/**
* Marge ajoutee autour des meshes par l'ajustement automatique.
*
* Garde-la petite. Le trace d'interaction est deja un balayage spherique
* tolerant : gonfler la boite ne rendrait pas le coffre plus facile a viser,
* ca l'empecherait juste de se coller a un mur.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Collision", meta = (ClampMin = "0", Units = "cm"))
float InteractionBoxPadding = 0.f;
// ------------------------------------------------------------------
// Couvercle
// ------------------------------------------------------------------
/**
* Rotation RELATIVE du pivot couvercle ouvert. Le repos vaut toujours zero.
*
* Un FRotator complet et pas un simple angle : selon l'orientation que tu
* donnes au pivot, la charniere peut tomber sur le pitch, le yaw ou le roll.
* Regle les trois a l'oeil dans le viewport plutot que d'aller comprendre
* quel axe local du mesh pointe ou.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Lid")
FRotator OpenRotation = FRotator(-100.f, 0.f, 0.f);
/** Duree de l'ouverture, en secondes. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Lid", meta = (ClampMin = "0.05", Units = "s"))
float LidDuration = 0.35f;
// ------------------------------------------------------------------
// Sons. Facultatifs : un champ vide ne joue rien.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Sound")
TObjectPtr<USoundBase> OpenSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Storage|Sound")
TObjectPtr<USoundBase> CloseSound;
private:
/** Recopie LidAlpha dans la rotation du pivot. */
void ApplyLidRotation();
/** Cale la boite sur le volume combine des deux meshes, couvercle ferme. */
void FitInteractionBox();
bool bOpen = false;
/** 0 = ferme, 1 = ouvert. Interpole, jamais pose directement. */
float LidAlpha = 0.f;
};