(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
+13 -3
View File
@@ -540,6 +540,16 @@ APickupItem* AFpsPlayer::SpawnPickup(const FInventorySlot& Slot, const FVector&
bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
{
return DropFromInventory(InventoryComponent, SlotIndex, Quantity);
}
bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity)
{
if (!Inventory)
{
return false;
}
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, impossible de jeter un objet."));
@@ -547,7 +557,7 @@ bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
}
FInventorySlot Taken;
if (!InventoryComponent->TakeFromSlot(SlotIndex, Quantity, Taken) || Taken.IsEmpty())
if (!Inventory->TakeFromSlot(SlotIndex, Quantity, Taken) || Taken.IsEmpty())
{
return false;
}
@@ -567,8 +577,8 @@ bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
if (!SpawnPickup(Taken, PlaceOnGround(DropLocation)))
{
// On ne perd pas l'objet : il retourne dans l'inventaire, usure comprise.
InventoryComponent->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses);
// On ne perd pas l'objet : il retourne d'ou il vient, usure comprise.
Inventory->AddItem(Taken.Item, Taken.Quantity, Taken.RemainingUses);
return false;
}
@@ -20,6 +20,7 @@
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
#include "SettingsMenuWidget.h"
#include "StorageContainer.h"
#include "SurvivalStatsWidget.h"
#define LOCTEXT_NAMESPACE "PauseMenu"
@@ -43,6 +44,13 @@ namespace
constexpr int32 ZOrderInventory = 20;
constexpr int32 ZOrderHotbar = 30;
/**
* Periode de surveillance de l'eloignement du coffre. Quatre fois par
* seconde suffit largement : c'est une comparaison de distance, pas une
* requete physique, et un quart de seconde de tolerance ne se voit pas.
*/
constexpr float StorageDistanceCheckPeriod = 0.25f;
// Le menu de pause passe devant tout le HUD, et la boite de confirmation
// devant lui. Meme echelle que dans le menu principal.
//
@@ -119,8 +127,9 @@ void AFpsPlayerController::OnPossess(APawn* InPawn)
void AFpsPlayerController::HandlePawnDied()
{
// L'inventaire ouvert au moment de la mort resterait affiche par-dessus
// l'ecran noir, curseur compris.
// L'ecran ouvert au moment de la mort resterait affiche par-dessus l'ecran
// noir, curseur compris. Sa fermeture rend le coffre au passage, couvercle
// compris.
if (bInventoryOpen)
{
ToggleInventory();
@@ -265,17 +274,23 @@ void AFpsPlayerController::UpdateHotbarVisibility()
Hotbar->SetVisibility(bHideHotbar ? ESlateVisibility::Collapsed : HotbarDefaultVisibility);
}
void AFpsPlayerController::HandleHotbarSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered)
{
if (InventoryScreen && bInventoryOpen)
{
InventoryScreen->NotifyExternalSlotHovered(Inventory, SlotIndex, bHovered);
}
}
void AFpsPlayerController::DropHoveredItem()
{
if (!bInventoryOpen || !InventoryScreen)
{
return;
}
UInventoryComponent* HoveredInventory = nullptr;
int32 SlotIndex = INDEX_NONE;
// Renvoie INDEX_NONE quand l'onglet Fabrication est affiche : la touche ne
// doit pas agir sur une case cachee derriere la grille de craft.
const int32 SlotIndex = InventoryScreen->GetHoveredSlotIndex();
if (SlotIndex == INDEX_NONE)
// L'ecran renvoie INDEX_NONE quand l'onglet Fabrication est affiche : la
// touche ne doit pas agir sur une case cachee derriere la grille de craft.
// Il consulte le panneau de coffre en premier quand il est ouvert.
if (!bInventoryOpen || !InventoryScreen || !InventoryScreen->GetHoveredSlot(HoveredInventory, SlotIndex))
{
return;
}
@@ -291,8 +306,10 @@ void AFpsPlayerController::DropHoveredItem()
// modal, pas une commande distincte.
const bool bSingle = IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);
// MAX_int32 pour "tout" : DropSlot borne de lui-meme a ce que contient le slot.
PlayerPawn->DropSlot(SlotIndex, bSingle ? 1 : MAX_int32);
// MAX_int32 pour "tout" : la fonction borne d'elle-meme a ce que contient
// le slot. On passe l'inventaire survole et non celui du pawn : viser une
// case du coffre doit sortir l'objet du coffre.
PlayerPawn->DropFromInventory(HoveredInventory, SlotIndex, bSingle ? 1 : MAX_int32);
}
void AFpsPlayerController::CreateHudWidgets()
@@ -351,6 +368,12 @@ void AFpsPlayerController::CreateHudWidgets()
// reviendra en quittant l'onglet Fabrication.
HotbarDefaultVisibility = Hotbar->GetVisibility();
Hotbar->AddToViewport(ZOrderHotbar);
// La barre est un widget de HUD, l'ecran d'inventaire un autre : ni
// l'un ni l'autre ne se connait. Le controller les possede tous les
// deux, c'est donc lui qui fait le pont -- exactement comme pour la
// visibilite de la barre sur l'onglet Fabrication.
Hotbar->OnSlotHovered.AddDynamic(this, &AFpsPlayerController::HandleHotbarSlotHovered);
}
}
@@ -418,6 +441,11 @@ void AFpsPlayerController::ToggleCrafting()
return;
}
// Le panneau de coffre est un FRERE du TabSwitcher : il resterait affiche a
// cote de la grille de recettes. On rend le coffre plutot que d'ignorer la
// touche, qui exprime une intention claire.
ReleaseActiveStorage();
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}
@@ -493,12 +521,13 @@ void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCrafting
bInventoryOpen = bOpen;
// Une fermeture, quelle qu'en soit la cause -- Tab, Echap, la mort --
// rend le poste. Le faire ici et nulle part ailleurs garantit qu'aucun
// chemin de sortie ne laisse l'etabli accessible depuis l'autre bout de
// la carte.
// rend le poste ET le coffre. Le faire ici et nulle part ailleurs garantit
// qu'aucun chemin de sortie ne laisse l'etabli accessible depuis l'autre
// bout de la carte, ni un couvercle leve derriere soi.
if (!bInventoryOpen)
{
ReleaseActiveStation();
ReleaseActiveStorage();
}
if (bInventoryOpen)
@@ -519,9 +548,14 @@ void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCrafting
// sur l'onglet Fabrication.
UpdateHotbarVisibility();
SetUiInputMode(bInventoryOpen);
}
void AFpsPlayerController::SetUiInputMode(bool bUiOpen)
{
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
if (bInventoryOpen)
if (bUiOpen)
{
// GameAndUI et non UIOnly : en UIOnly, plus aucune action Enhanced
// Input ne passerait et on ne pourrait plus refermer avec Tab.
@@ -564,6 +598,109 @@ void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCrafting
}
}
void AFpsPlayerController::OpenStorage(AStorageContainer* Container)
{
if (!Container || !IsLocalController() || !InventoryScreen)
{
return;
}
// Deja devant un autre coffre : on rend le precedent avant d'ouvrir le
// nouveau, sinon son couvercle resterait leve pour le reste de la partie.
ReleaseActiveStorage();
ActiveStorage = Container;
bStorageOpen = true;
InventoryScreen->ShowStorage(Container);
Container->SetOpen(true);
// La barre rapide reste visible et cliquable par-dessus l'ecran : on lui
// donne le coffre pour que le clic droit y marche aussi.
if (Hotbar)
{
Hotbar->SetQuickTransferTarget(Container->GetStorage());
}
// Le coffre n'est pas un ecran a part : c'est un panneau de plus sur
// l'ecran d'inventaire, sur l'onglet Inventaire. Consequence directe -- le
// sac du joueur ne bouge pas d'un pixel entre les deux situations.
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/false);
// La surveillance ne tourne QUE pendant l'ecran : un timer permanent pour
// une condition vraie deux minutes par partie serait du gaspillage.
GetWorldTimerManager().SetTimer(
StorageDistanceTimerHandle, this, &AFpsPlayerController::CheckStorageDistance,
StorageDistanceCheckPeriod, /*bLoop=*/true);
}
void AFpsPlayerController::CloseStorage()
{
if (!bStorageOpen)
{
return;
}
// Le panneau vit dans l'ecran d'inventaire : le refermer ferme l'ecran
// entier, qui rend le coffre au passage par ReleaseActiveStorage().
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
}
void AFpsPlayerController::ReleaseActiveStorage()
{
if (!bStorageOpen)
{
return;
}
bStorageOpen = false;
GetWorldTimerManager().ClearTimer(StorageDistanceTimerHandle);
if (AStorageContainer* Container = ActiveStorage.Get())
{
Container->SetOpen(false);
}
ActiveStorage.Reset();
if (InventoryScreen)
{
InventoryScreen->HideStorage();
}
if (Hotbar)
{
Hotbar->SetQuickTransferTarget(nullptr);
}
}
void AFpsPlayerController::CheckStorageDistance()
{
if (!bStorageOpen)
{
return;
}
const AStorageContainer* Container = ActiveStorage.Get();
const APawn* PlayerPawn = GetPawn();
// Coffre detruit ou joueur sans corps : dans les deux cas l'ecran pilote
// quelque chose qui n'existe plus.
if (!Container || !PlayerPawn)
{
CloseStorage();
return;
}
// DistSquared : on evite une racine carree quatre fois par seconde pour une
// comparaison qui n'a pas besoin de la distance reelle.
const float MaxDistSquared = FMath::Square(MaxStorageDistance);
if (FVector::DistSquared(Container->GetActorLocation(), PlayerPawn->GetActorLocation()) > MaxDistSquared)
{
CloseStorage();
}
}
void AFpsPlayerController::HandlePauseInput()
{
// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
@@ -582,8 +719,15 @@ void AFpsPlayerController::HandlePauseInput()
return;
}
// Echap sert d'abord a annuler ce qui est ouvert. Fermer l'inventaire est
// ce que le joueur attend en premier.
// Echap sert d'abord a annuler ce qui est ouvert. Le coffre passe avant
// l'inventaire : les deux ne sont jamais ouverts ensemble, mais l'ordre
// resterait juste le jour ou ca changerait.
if (bStorageOpen)
{
CloseStorage();
return;
}
if (bInventoryOpen)
{
ToggleInventory();
@@ -847,6 +991,10 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
InventoryScreen = nullptr;
}
// Le timer d'eloignement rappellerait une methode d'un controller en train
// de disparaitre. ClearTimer est sans effet s'il ne tourne pas.
GetWorldTimerManager().ClearTimer(StorageDistanceTimerHandle);
if (InteractionPrompt)
{
InteractionPrompt->RemoveFromParent();
@@ -80,6 +80,19 @@ void UHotbarWidget::HandleInventoryChanged()
Refresh();
}
void UHotbarWidget::SetQuickTransferTarget(UInventoryComponent* InTarget)
{
QuickTransferTarget = InTarget;
for (UInventorySlotWidget* SlotWidget : SlotWidgets)
{
if (SlotWidget)
{
SlotWidget->SetQuickTransferTarget(InTarget);
}
}
}
void UHotbarWidget::HandleSelectionChanged(int32 NewIndex)
{
// Seule la surbrillance bouge : inutile de repousser tout le contenu.
@@ -92,6 +105,11 @@ void UHotbarWidget::HandleSelectionChanged(int32 NewIndex)
}
}
void UHotbarWidget::HandleSlotHovered(int32 SlotIndex, bool bHovered)
{
OnSlotHovered.Broadcast(BoundInventory.Get(), SlotIndex, bHovered);
}
void UHotbarWidget::Refresh()
{
if (!SlotContainer)
@@ -161,7 +179,9 @@ void UHotbarWidget::RebuildSlots(int32 DesiredCount)
// Meme indexation que la grille : la case 0 de la barre EST le slot 0
// de l'inventaire. C'est ce qui rend le glisser-deposer entre les deux
// gratuit -- ils manipulent le meme tableau.
NewSlot->InitSlot(BoundInventory.Get(), Index, nullptr);
NewSlot->InitSlot(BoundInventory.Get(), Index);
NewSlot->OnHoverChanged.AddDynamic(this, &UHotbarWidget::HandleSlotHovered);
NewSlot->SetQuickTransferTarget(QuickTransferTarget.Get());
if (UHorizontalBoxSlot* BoxSlot = SlotContainer->AddChildToHorizontalBox(NewSlot))
{
@@ -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;
}
@@ -4,12 +4,15 @@
#include "InventoryScreenWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Components/WidgetSwitcher.h"
#include "CraftingWidget.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "InventoryComponent.h"
#include "InventoryDragDropOperation.h"
#include "InventoryWidget.h"
#include "StorageContainer.h"
void UInventoryScreenWidget::NativeConstruct()
{
@@ -25,6 +28,30 @@ void UInventoryScreenWidget::NativeConstruct()
CraftingTabButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleCraftingTabClicked);
}
if (StoreAllButton)
{
StoreAllButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleStoreAllClicked);
}
if (TakeAllButton)
{
TakeAllButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleTakeAllClicked);
}
// La grille de coffre n'affiche jamais de detail : c'est le meme
// WBP_Inventory que le sac, elle en porte donc un exemplaire qu'on coupe une
// fois pour toutes ici plutot que de compter sur une case a cocher.
if (StorageGrid)
{
StorageGrid->SetItemDetailsEnabled(false);
}
// Le panneau de coffre ne se montre qu'a l'ouverture d'un coffre. On le
// masque en C++ plutot que de dependre de ce qui a ete regle dans la
// Designer : un panneau laisse Visible par erreur afficherait une grille
// vide en permanence a cote du sac.
HideStorage();
ShowInventoryTab();
}
@@ -56,6 +83,152 @@ int32 UInventoryScreenWidget::GetHoveredSlotIndex() const
return InventoryPage->GetHoveredSlotIndex();
}
bool UInventoryScreenWidget::GetHoveredSlot(UInventoryComponent*& OutInventory, int32& OutIndex) const
{
// Le coffre en premier : c'est le panneau le plus a droite, donc celui que
// le curseur survole quand les deux sont affiches.
if (bStorageVisible && StorageGrid && StorageGrid->GetHoveredSlot(OutInventory, OutIndex))
{
return true;
}
if (InventoryPage && IsInventoryTabActive() && InventoryPage->GetHoveredSlot(OutInventory, OutIndex))
{
return true;
}
OutInventory = nullptr;
OutIndex = INDEX_NONE;
return false;
}
void UInventoryScreenWidget::NotifyExternalSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered)
{
// Rien a faire sous l'onglet Fabrication : le panneau est parti avec la
// page, et la barre rapide y est de toute facon masquee.
if (InventoryPage && IsInventoryTabActive())
{
InventoryPage->NotifyExternalSlotHovered(Inventory, SlotIndex, bHovered);
}
}
UInventoryComponent* UInventoryScreenWidget::GetPlayerInventory() const
{
const APlayerController* OwningController = GetOwningPlayer();
AFpsPlayer* PlayerPawn = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
return PlayerPawn ? PlayerPawn->GetInventoryComponent() : nullptr;
}
void UInventoryScreenWidget::ShowStorage(AStorageContainer* Container)
{
UInventoryComponent* Contents = Container ? Container->GetStorage() : nullptr;
if (!Contents)
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryScreenWidget::ShowStorage : coffre nul ou sans inventaire."));
return;
}
if (!StoragePanel || !StorageGrid)
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryScreenWidget : 'StoragePanel' ou 'StorageGrid' manque dans WBP_InventoryScreen, le coffre ne s'affichera pas."));
return;
}
BoundStorage = Contents;
bStorageVisible = true;
StorageGrid->SetColumnCount(Container->GetColumnCount());
StorageGrid->BindToInventory(Contents);
// Le clic droit envoie d'un cote a l'autre. Le pawn a pu changer depuis la
// derniere ouverture (respawn), d'ou la relecture plutot qu'une valeur
// gardee en cache.
StorageGrid->SetQuickTransferTarget(GetPlayerInventory());
if (InventoryPage)
{
InventoryPage->SetQuickTransferTarget(Contents);
// Le detail disparait en mode coffre : l'ecran passe deja a deux grilles
// cote a cote, une troisieme colonne le rendrait illisible.
InventoryPage->SetItemDetailsEnabled(false);
}
if (StorageTitle)
{
StorageTitle->SetText(Container->GetDisplayName());
}
StoragePanel->SetVisibility(ESlateVisibility::Visible);
// Un coffre ouvert sous l'onglet Fabrication n'aurait aucun sens : on force
// l'onglet Inventaire, et ShowPage grise le bouton Fabrication tant que
// bStorageVisible est vrai.
ShowInventoryTab();
}
void UInventoryScreenWidget::HideStorage()
{
BoundStorage.Reset();
bStorageVisible = false;
// On coupe les transferts rapides AVANT de debrancher : une case qui garde
// un coffre en memoire y enverrait encore des objets au prochain clic droit,
// panneau ferme.
if (InventoryPage)
{
InventoryPage->SetQuickTransferTarget(nullptr);
// Le sac retrouve son panneau. Repose a chaque fermeture, y compris a la
// construction : c'est l'etat par defaut de l'ecran, il ne doit dependre
// ni de la Designer ni de ce qu'a laisse la session precedente.
InventoryPage->SetItemDetailsEnabled(true);
}
if (StorageGrid)
{
StorageGrid->SetQuickTransferTarget(nullptr);
StorageGrid->BindToInventory(nullptr);
}
if (StoragePanel)
{
StoragePanel->SetVisibility(ESlateVisibility::Collapsed);
}
// Rend la main a l'onglet Fabrication, que ShowPage avait grise.
if (CraftingTabButton)
{
CraftingTabButton->SetIsEnabled(IsInventoryTabActive());
}
}
void UInventoryScreenWidget::HandleStoreAllClicked()
{
UInventoryComponent* PlayerInventory = GetPlayerInventory();
if (!PlayerInventory || !BoundStorage.IsValid())
{
return;
}
// A partir de la grille seulement : la barre rapide porte les outils qu'on
// vient d'utiliser, et les ranger a chaque retour d'expedition serait une
// punition deguisee en confort.
PlayerInventory->TransferAllTo(BoundStorage.Get(), PlayerInventory->GetBackpackStartIndex());
}
void UInventoryScreenWidget::HandleTakeAllClicked()
{
UInventoryComponent* PlayerInventory = GetPlayerInventory();
if (!PlayerInventory || !BoundStorage.IsValid())
{
return;
}
// Depuis l'index 0 : un coffre n'a pas de barre rapide. Ce qui ne rentre
// pas dans le sac reste dans le coffre.
BoundStorage->TransferAllTo(PlayerInventory, 0);
}
void UInventoryScreenWidget::ShowInventoryTab()
{
ShowPage(InventoryPage);
@@ -96,7 +269,10 @@ void UInventoryScreenWidget::ShowPage(UWidget* Page)
if (CraftingTabButton)
{
CraftingTabButton->SetIsEnabled(bInventoryActive);
// Grise aussi tant qu'un coffre est ouvert : ses ressources ne comptent
// pas encore dans les recettes, et un clic dont le resultat serait
// incomprehensible vaut mieux etre rendu impossible.
CraftingTabButton->SetIsEnabled(bInventoryActive && !bStorageVisible);
}
OnTabChanged.Broadcast(bInventoryActive);
@@ -114,11 +290,13 @@ bool UInventoryScreenWidget::NativeOnDrop(const FGeometry& InGeometry, const FDr
return true;
}
// Depuis l'inventaire SOURCE et non celui du joueur : sortir une pile du
// coffre en la lachant dans le vide doit la retirer du coffre.
if (const APlayerController* OwningController = GetOwningPlayer())
{
if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(OwningController->GetPawn()))
{
PlayerPawn->DropSlot(Payload->SourceSlotIndex, Payload->Quantity);
PlayerPawn->DropFromInventory(Payload->SourceInventory.Get(), Payload->SourceSlotIndex, Payload->Quantity);
}
}
@@ -8,7 +8,6 @@
#include "Components/TextBlock.h"
#include "Components/Widget.h"
#include "InventoryDragDropOperation.h"
#include "InventoryWidget.h"
#include "ItemDataAsset.h"
void UInventorySlotWidget::NativeConstruct()
@@ -53,31 +52,29 @@ void UInventorySlotWidget::ConfigureAsDragVisual()
SetVisibility(ESlateVisibility::HitTestInvisible);
}
void UInventorySlotWidget::InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex, UInventoryWidget* InOwningGrid)
void UInventorySlotWidget::InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex)
{
OwningInventory = InInventory;
SlotIndex = InSlotIndex;
OwningGrid = InOwningGrid;
}
void UInventorySlotWidget::SetQuickTransferTarget(UInventoryComponent* InTarget)
{
QuickTransferTarget = InTarget;
}
void UInventorySlotWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseEnter(InGeometry, InMouseEvent);
if (OwningGrid.IsValid())
{
OwningGrid->NotifySlotHovered(SlotIndex, true);
}
OnHoverChanged.Broadcast(SlotIndex, true);
}
void UInventorySlotWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
Super::NativeOnMouseLeave(InMouseEvent);
if (OwningGrid.IsValid())
{
OwningGrid->NotifySlotHovered(SlotIndex, false);
}
OnHoverChanged.Broadcast(SlotIndex, false);
}
void UInventorySlotWidget::SetSlot(const FInventorySlot& InSlot)
@@ -142,6 +139,18 @@ void UInventorySlotWidget::SetSlot(const FInventorySlot& InSlot)
FReply UInventorySlotWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
// Clic droit : transfert rapide vers l'inventaire d'en face, quand il y en
// a un. Traite avant le glisser pour que le bouton droit ne demarre jamais
// de DetectDrag -- ce qui laisserait une operation orpheline en cours.
if (!CachedSlot.IsEmpty() && InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton)
{
if (QuickTransferTarget.IsValid() && OwningInventory.IsValid() && SlotIndex != INDEX_NONE)
{
UInventoryComponent::QuickTransferSlot(OwningInventory.Get(), SlotIndex, QuickTransferTarget.Get());
return FReply::Handled();
}
}
if (CachedSlot.IsEmpty() || !InMouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton))
{
return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
@@ -225,15 +234,12 @@ bool UInventorySlotWidget::NativeOnDrop(const FGeometry& InGeometry, const FDrag
return Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
}
// On refuse les transferts entre deux inventaires differents : ce sera le
// jour ou tu ajouteras les coffres, avec ses propres regles.
if (!Payload->SourceInventory.IsValid() || Payload->SourceInventory != OwningInventory)
{
return false;
}
// MoveItemQuantity diffuse OnInventoryChanged, donc la grille se rafraichit
// seule. Une quantite egale a la pile entiere retombe sur le comportement
// complet, echange compris.
return OwningInventory->MoveItemQuantity(Payload->SourceSlotIndex, SlotIndex, Payload->Quantity);
// TransferSlot diffuse OnInventoryChanged des DEUX cotes, donc les grilles
// se rafraichissent seules. Elle gere indifferemment le meme inventaire et
// deux inventaires distincts : glisser vers un coffre, vers la barre rapide
// ou vers la case d'a cote emprunte exactement le meme chemin.
return UInventoryComponent::TransferSlot(
Payload->SourceInventory.Get(), Payload->SourceSlotIndex,
OwningInventory.Get(), SlotIndex,
Payload->Quantity);
}
@@ -10,48 +10,58 @@
#include "InventoryComponent.h"
#include "InventoryDragDropOperation.h"
#include "InventorySlotWidget.h"
#include "ItemDetailsWidget.h"
void UInventoryWidget::NativeConstruct()
{
Super::NativeConstruct();
BindToOwningPawn();
// Une grille de coffre n'a rien a chercher toute seule : son contenu lui
// est donne par l'ecran, apres coup.
if (bAutoBindToPawn)
{
BindToOwningPawn();
}
}
void UInventoryWidget::BindToOwningPawn()
{
UInventoryComponent* NewInventory = ResolveInventory();
if (BoundInventory.Get() == NewInventory)
if (!NewInventory)
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget : aucun UInventoryComponent trouve sur le pawn possede."));
}
BindToInventory(NewInventory);
}
void UInventoryWidget::BindToInventory(UInventoryComponent* InInventory)
{
if (BoundInventory.Get() == InInventory)
{
return;
}
// On annonce la sortie de survol AVANT de changer d'inventaire : le panneau
// de detail a besoin de savoir QUEL affichage devient perime, et une fois
// BoundInventory reaffecte cette information est perdue.
SetHoveredSlot(INDEX_NONE);
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UInventoryWidget::HandleInventoryChanged);
}
BoundInventory = NewInventory;
BoundInventory = InInventory;
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.AddDynamic(this, &UInventoryWidget::HandleInventoryChanged);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget : aucun UInventoryComponent trouve sur le pawn possede."));
}
// Les cases gardent l'ancien composant en memoire : on force leur
// recreation en vidant la grille.
for (UInventorySlotWidget* SlotWidget : SlotWidgets)
{
if (SlotWidget)
{
SlotWidget->RemoveFromParent();
}
}
SlotWidgets.Reset();
ClearSlotWidgets();
Refresh();
}
@@ -79,20 +89,138 @@ void UInventoryWidget::HandleInventoryChanged()
Refresh();
}
void UInventoryWidget::SetQuickTransferTarget(UInventoryComponent* InTarget)
{
QuickTransferTarget = InTarget;
// Les cases deja creees ne reliront rien d'elles-memes : on leur pousse.
for (UInventorySlotWidget* SlotWidget : SlotWidgets)
{
if (SlotWidget)
{
SlotWidget->SetQuickTransferTarget(InTarget);
}
}
}
void UInventoryWidget::SetColumnCount(int32 NewColumnCount)
{
const int32 Clamped = FMath::Max(1, NewColumnCount);
if (ColumnCount == Clamped)
{
return;
}
ColumnCount = Clamped;
// La position d'une case dans le UniformGrid est figee a son ajout : on
// repart des cases plutot que de recalculer chaque UUniformGridSlot. Ca
// n'arrive qu'a l'ouverture d'un coffre, pas a chaque rafraichissement.
ClearSlotWidgets();
Refresh();
}
void UInventoryWidget::NotifySlotHovered(int32 Index, bool bHovered)
{
if (bHovered)
{
HoveredSlotIndex = Index;
SetHoveredSlot(Index);
}
else if (HoveredSlotIndex == Index)
{
// Le test d'egalite evite d'effacer le survol quand les evenements
// arrivent dans l'ordre "entre dans B" puis "sort de A".
HoveredSlotIndex = INDEX_NONE;
SetHoveredSlot(INDEX_NONE);
}
}
void UInventoryWidget::SetHoveredSlot(int32 NewIndex)
{
if (HoveredSlotIndex == NewIndex)
{
return;
}
const int32 PreviousIndex = HoveredSlotIndex;
HoveredSlotIndex = NewIndex;
if (!ItemDetailsPanel || !bItemDetailsEnabled)
{
return;
}
if (HoveredSlotIndex != INDEX_NONE)
{
ItemDetailsPanel->ShowSlot(BoundInventory.Get(), HoveredSlotIndex);
}
else
{
// On efface la case qu'on QUITTE, pas le panneau : passer d'une case de
// la barre rapide a une case de la grille declenche "entre" puis "sort",
// et un effacement inconditionnel viderait ce qui vient d'etre affiche.
ItemDetailsPanel->ClearSlot(BoundInventory.Get(), PreviousIndex);
}
}
void UInventoryWidget::NotifyExternalSlotHovered(UInventoryComponent* InInventory, int32 InSlotIndex, bool bHovered)
{
if (!ItemDetailsPanel || !bItemDetailsEnabled || InSlotIndex == INDEX_NONE)
{
return;
}
if (bHovered)
{
ItemDetailsPanel->ShowSlot(InInventory, InSlotIndex);
}
else
{
ItemDetailsPanel->ClearSlot(InInventory, InSlotIndex);
}
}
void UInventoryWidget::UpdateItemDetails()
{
if (!ItemDetailsPanel)
{
return;
}
if (!bItemDetailsEnabled || HoveredSlotIndex == INDEX_NONE)
{
ItemDetailsPanel->Clear();
}
else
{
ItemDetailsPanel->ShowSlot(BoundInventory.Get(), HoveredSlotIndex);
}
}
void UInventoryWidget::SetItemDetailsEnabled(bool bEnabled)
{
bItemDetailsEnabled = bEnabled;
if (ItemDetailsPanel)
{
// On repose la visibilite a chaque appel, meme quand la valeur ne change
// pas : c'est ce qui evite de dependre de ce qui a ete regle dans la
// Designer, exactement comme le HideStorage() de l'ecran.
// Collapsed et non Hidden -- coupe, le panneau ne doit pas non plus
// reserver sa largeur, sinon la grille trainerait une colonne vide.
ItemDetailsPanel->SetVisibility(bEnabled ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed);
}
UpdateItemDetails();
}
bool UInventoryWidget::GetHoveredSlot(UInventoryComponent*& OutInventory, int32& OutIndex) const
{
OutInventory = BoundInventory.Get();
OutIndex = HoveredSlotIndex;
return OutInventory != nullptr && OutIndex != INDEX_NONE;
}
bool UInventoryWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
@@ -114,11 +242,13 @@ bool UInventoryWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDrop
}
// Lache hors de la grille : on jette au sol, comme dans Valheim ou Raft.
// Depuis l'inventaire SOURCE et non celui de cette grille : sortir une pile
// du coffre en la lachant dans le vide doit la retirer du coffre.
if (const APlayerController* OwningController = GetOwningPlayer())
{
if (AFpsPlayer* Player = Cast<AFpsPlayer>(OwningController->GetPawn()))
{
Player->DropSlot(Payload->SourceSlotIndex, Payload->Quantity);
Player->DropFromInventory(Payload->SourceInventory.Get(), Payload->SourceSlotIndex, Payload->Quantity);
}
}
@@ -133,16 +263,21 @@ void UInventoryWidget::Refresh()
return;
}
// Debranchee : la grille se vide au lieu de laisser a l'ecran le contenu
// d'un conteneur qu'on ne pilote plus. Pas un avertissement -- c'est l'etat
// normal d'une grille de coffre entre deux ouvertures.
if (!BoundInventory.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget::Refresh : aucun inventaire lie."));
ClearSlotWidgets();
return;
}
const TArray<FInventorySlot>& Slots = BoundInventory->GetSlots();
// La grille commence APRES la barre rapide : les deux conteneurs sont
// independants, un objet dans la barre n'apparait pas ici.
// independants, un objet dans la barre n'apparait pas ici. Sur un coffre,
// ConfigureAsContainer() met la barre a zero, donc l'offset vaut 0 et tout
// s'affiche.
const int32 Offset = BoundInventory->GetBackpackStartIndex();
const int32 GridCount = FMath::Max(0, Slots.Num() - Offset);
@@ -161,6 +296,25 @@ void UInventoryWidget::Refresh()
}
}
void UInventoryWidget::ClearSlotWidgets()
{
for (UInventorySlotWidget* SlotWidget : SlotWidgets)
{
if (SlotWidget)
{
SlotWidget->RemoveFromParent();
}
}
SlotWidgets.Reset();
// La case survolee vient de disparaitre : sans cette remise a zero, la
// touche "jeter" agirait sur un index qui ne designe plus rien -- et le
// panneau de detail continuerait de decrire un objet qui n'est plus la.
// NativeOnMouseLeave n'arrive JAMAIS quand le widget s'efface sous le
// curseur, ce qui est exactement ce qui se passe en fermant un coffre.
SetHoveredSlot(INDEX_NONE);
}
void UInventoryWidget::RebuildGrid(int32 DesiredCount)
{
if (SlotWidgets.Num() == DesiredCount)
@@ -185,6 +339,11 @@ void UInventoryWidget::RebuildGrid(int32 DesiredCount)
SlotWidgets.RemoveAt(Last);
}
if (!BoundInventory.IsValid())
{
return;
}
// Pas assez : on en cree, en les placant en ligne / colonne.
const int32 Columns = FMath::Max(1, ColumnCount);
while (SlotWidgets.Num() < DesiredCount)
@@ -205,7 +364,10 @@ void UInventoryWidget::RebuildGrid(int32 DesiredCount)
// grille. C'est ce qui rend le glisser entre la barre et la grille
// gratuit : les deux manipulent les memes index.
const int32 Absolute = BoundInventory->GetBackpackStartIndex() + Index;
NewSlot->InitSlot(BoundInventory.Get(), Absolute, this);
NewSlot->InitSlot(BoundInventory.Get(), Absolute);
NewSlot->OnHoverChanged.AddDynamic(this, &UInventoryWidget::NotifySlotHovered);
NewSlot->SetQuickTransferTarget(QuickTransferTarget.Get());
SlotGrid->AddChildToUniformGrid(NewSlot, Index / Columns, Index % Columns);
SlotWidgets.Add(NewSlot);
}
@@ -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);
}
}
@@ -0,0 +1,224 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "StorageContainer.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/CollisionProfile.h"
#include "Engine/StaticMesh.h"
#include "FpsPlayerController.h"
#include "GameFramework/Pawn.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Kismet/GameplayStatics.h"
#define LOCTEXT_NAMESPACE "StorageContainer"
AStorageContainer::AStorageContainer()
{
// Le tick n'existe que pour animer le couvercle : il demarre eteint et ne
// s'allume que le temps du mouvement.
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
SetRootComponent(BaseMesh);
// Les meshes ne portent AUCUNE collision : elle vit entierement dans la
// boite. Un mesh de pack sans collision simple rendrait le coffre muet sans
// qu'aucune erreur ne le signale, et celle du couvercle partirait avec lui
// des l'ouverture.
BaseMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
InteractionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("InteractionBox"));
InteractionBox->SetupAttachment(BaseMesh);
// Explicite plutot que de compter sur le profil par defaut : ce composant a
// deux roles indissociables, empecher le joueur de traverser le coffre et
// bloquer le canal Visibility pour que le trace d'interaction le trouve.
// Un profil qui laisserait passer Visibility rendrait le coffre inutilisable
// sans qu'aucune erreur ne le signale.
InteractionBox->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
// Valeur de depart pour un coffre qui n'aurait pas encore de mesh :
// FitInteractionBox() l'ecrase des qu'il y en a un.
InteractionBox->SetBoxExtent(FVector(40.f, 30.f, 30.f));
LidPivot = CreateDefaultSubobject<USceneComponent>(TEXT("LidPivot"));
LidPivot->SetupAttachment(BaseMesh);
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LidMesh"));
LidMesh->SetupAttachment(LidPivot);
LidMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));
DisplayName = LOCTEXT("ChestName", "Chest");
InteractionPrompt = LOCTEXT("OpenChest", "Ouvrir le coffre");
}
void AStorageContainer::PreInitializeComponents()
{
// Avant InitializeComponents(), donc avant que le composant fige sa taille
// par defaut de 30 slots avec une barre rapide.
if (Storage)
{
Storage->ConfigureAsContainer(SlotCount);
}
Super::PreInitializeComponents();
}
void AStorageContainer::BeginPlay()
{
Super::BeginPlay();
// Le couvercle repose a zero, quelle que soit la rotation laissee dans le
// Blueprint : le repos EST la reference de l'animation.
LidAlpha = 0.f;
ApplyLidRotation();
if (!Storage)
{
return;
}
for (const FStorageStartingItem& Entry : StartingItems)
{
if (!Entry.Item || Entry.Quantity <= 0)
{
continue;
}
const int32 Left = Storage->AddItem(Entry.Item, Entry.Quantity);
if (Left > 0)
{
UE_LOG(LogTemp, Warning, TEXT("%s : %d x %s n'ont pas tenu dans le coffre (%d cases)."),
*GetName(), Left, *GetNameSafe(Entry.Item), SlotCount);
}
}
}
void AStorageContainer::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
// Tourne aussi dans l'editeur : la boite se recale a l'instant ou tu
// changes de mesh, sans avoir a lancer le jeu pour t'en assurer.
if (bAutoFitInteractionBox)
{
FitInteractionBox();
}
}
void AStorageContainer::FitInteractionBox()
{
if (!InteractionBox || !BaseMesh)
{
return;
}
// Volume combine des deux meshes, exprime dans le repere de la caisse --
// celui de la boite, qui en est fille.
FBox Combined(ForceInit);
const UStaticMeshComponent* Meshes[] = { BaseMesh, LidMesh };
for (const UStaticMeshComponent* Component : Meshes)
{
const UStaticMesh* Mesh = Component ? Component->GetStaticMesh() : nullptr;
if (!Mesh)
{
continue;
}
// Transform RELATIVE a la racine et non absolue : la boite doit rester
// juste quel que soit l'endroit ou l'acteur est pose dans le niveau.
const FTransform Relative = Component->GetComponentTransform().GetRelativeTransform(BaseMesh->GetComponentTransform());
Combined += Mesh->GetBoundingBox().TransformBy(Relative);
}
if (!Combined.IsValid)
{
return;
}
InteractionBox->SetRelativeLocation(Combined.GetCenter());
InteractionBox->SetBoxExtent(Combined.GetExtent() + FVector(InteractionBoxPadding));
}
FText AStorageContainer::GetInteractionPrompt_Implementation(AActor* Interactor) const
{
return InteractionPrompt;
}
void AStorageContainer::Interact_Implementation(AActor* Interactor)
{
// L'ecran vit sur le controller, pas sur le pawn : c'est lui qu'on cherche.
const APawn* InteractingPawn = Cast<APawn>(Interactor);
AFpsPlayerController* PlayerController = InteractingPawn ? Cast<AFpsPlayerController>(InteractingPawn->GetController()) : nullptr;
if (!PlayerController)
{
return;
}
PlayerController->OpenStorage(this);
}
void AStorageContainer::SetOpen(bool bInOpen)
{
if (bOpen == bInOpen)
{
return;
}
bOpen = bInOpen;
if (USoundBase* Sound = bOpen ? OpenSound : CloseSound)
{
UGameplayStatics::PlaySoundAtLocation(this, Sound, GetActorLocation());
}
// Le tick s'eteint tout seul en fin de course. Le rallumer ici suffit, y
// compris si l'animation precedente n'etait pas terminee : l'alpha repart
// de sa valeur courante, donc refermer un coffre a moitie ouvert ne saute
// pas.
SetActorTickEnabled(true);
}
void AStorageContainer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
const float Target = bOpen ? 1.f : 0.f;
const float Speed = 1.f / FMath::Max(0.05f, LidDuration);
LidAlpha = FMath::FInterpConstantTo(LidAlpha, Target, DeltaTime, Speed);
ApplyLidRotation();
if (FMath::IsNearlyEqual(LidAlpha, Target))
{
// On force la valeur exacte avant de se rendormir : sans ca le couvercle
// resterait a un poil de sa position finale pour le reste de la partie.
LidAlpha = Target;
ApplyLidRotation();
SetActorTickEnabled(false);
}
}
void AStorageContainer::ApplyLidRotation()
{
if (!LidPivot)
{
return;
}
// Lissage aux extremites : un couvercle a vitesse constante demarre et
// s'arrete d'un coup, ce qui se lit comme une saccade sur un objet lourd.
const float Eased = FMath::SmoothStep(0.f, 1.f, LidAlpha);
LidPivot->SetRelativeRotation(OpenRotation * Eased);
}
#undef LOCTEXT_NAMESPACE
@@ -310,7 +310,6 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bShowRemainingTimeInPrompt = false;
/** Active le Custom Depth quand le poste est vise, comme les ramassables. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bHighlightOnFocus = true;
+13
View File
@@ -121,6 +121,19 @@ public:
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool DropSlot(int32 SlotIndex, int32 Quantity);
/**
* Meme chose, depuis un inventaire quelconque : celui d'un coffre ouvert,
* typiquement.
*
* C'est le PAWN qui pose au sol et pas le composant, pour la meme raison
* que le surplus de fabrication : un UInventoryComponent ne sait pas
* engendrer un acteur, et un coffre qui deposerait lui-meme laisserait le
* butin dans sa propre caisse. L'objet part donc toujours devant les yeux
* du joueur, quel que soit le contenant d'ou il sort.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
bool DropFromInventory(UInventoryComponent* Inventory, int32 SlotIndex, int32 Quantity);
/** Vide l'inventaire entier en un tas autour du personnage. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void DropAllItems();
@@ -11,6 +11,7 @@
#include "GameFramework/PlayerController.h"
#include "FpsPlayerController.generated.h"
class AStorageContainer;
class UConfirmDialogWidget;
class UCraftingComponent;
class UHotbarWidget;
@@ -66,6 +67,27 @@ public:
UFUNCTION(BlueprintPure, Category = "UI")
bool IsInventoryOpen() const { return bInventoryOpen; }
/**
* Ouvre l'ecran d'inventaire avec le panneau du coffre a cote : le sac a
* gauche, son contenu a droite.
*
* Ce n'est PAS un ecran a part. Le sac du joueur reste exactement au meme
* endroit qu'à l'ouverture par Tab -- c'est tout l'interet, un ecran
* distinct le ferait sauter a chaque fois qu'on ouvre un coffre.
*
* Comme pour l'etabli, c'est le controller qui pilote et non le coffre : lui
* seul sait quand l'ecran se referme, et donc quand refermer le couvercle.
*/
UFUNCTION(BlueprintCallable, Category = "UI|Storage")
void OpenStorage(AStorageContainer* Container);
/** Referme l'ecran de coffre. Sans effet s'il n'y en a pas d'ouvert. */
UFUNCTION(BlueprintCallable, Category = "UI|Storage")
void CloseStorage();
UFUNCTION(BlueprintPure, Category = "UI|Storage")
bool IsStorageOpen() const { return bStorageOpen; }
/** Ferme le menu de pause et relance le monde. */
UFUNCTION(BlueprintCallable, Category = "UI|Pause")
void ResumeGame();
@@ -189,6 +211,15 @@ protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UInventoryScreenWidget> InventoryScreenClass;
/**
* Distance au-dela de laquelle l'ecran de coffre se referme tout seul.
*
* Sans elle, on viderait un coffre depuis l'autre bout de la carte : rien
* n'oblige le joueur a rester devant une fois l'ecran ouvert.
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Storage", meta = (ClampMin = "50", Units = "cm"))
float MaxStorageDistance = 400.f;
/** Ton WBP_Hotbar, affiche en permanence. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
TSubclassOf<UHotbarWidget> HotbarClass;
@@ -297,6 +328,29 @@ private:
/** Rend le poste accorde par le dernier OpenCraftingAtStation. Sans effet sinon. */
void ReleaseActiveStation();
/**
* Rend le coffre ouvert : couvercle referme, panneau masque, surveillance
* arretee. Sans effet s'il n'y en a pas.
*
* Separee de CloseStorage() pour la meme raison que ReleaseActiveStation :
* c'est la fermeture de l'ECRAN qui l'appelle, quelle qu'en soit la cause --
* Tab, Echap, la mort, un autre coffre. Un seul point de sortie, donc aucun
* chemin qui laisse un couvercle leve derriere soi.
*/
void ReleaseActiveStorage();
/**
* Mode d'input et curseur pour un ecran plein cadre.
*
* Partage par l'inventaire et le coffre : deux copies de ces huit lignes
* finiraient par diverger, et le symptome -- une camera qui ne repond plus,
* un sprint bloque -- se cherche tres longtemps.
*/
void SetUiInputMode(bool bUiOpen);
/** Le joueur s'est-il trop eloigne du coffre ouvert. Appele par un timer. */
void CheckStorageDistance();
/** Raccourci vers le composant de fabrication du pawn possede. */
UCraftingComponent* GetPawnCrafting() const;
@@ -311,6 +365,17 @@ private:
*/
void UpdateHotbarVisibility();
/**
* Relaie le survol d'une case de la barre rapide vers le panneau de detail
* de l'ecran d'inventaire.
*
* Ignore ecran ferme : sans curseur il n'y a pas de survol, mais un widget
* masque sous la souris peut encore emettre sa sortie -- et decrire un objet
* dans un panneau invisible n'aurait aucun sens.
*/
UFUNCTION()
void HandleHotbarSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered);
/** Jette la pile survolee dans la grille, si l'inventaire est ouvert. */
void DropHoveredItem();
@@ -329,6 +394,14 @@ private:
FTimerHandle RespawnTimerHandle;
/** Surveillance de l'eloignement, actif seulement pendant qu'un coffre est ouvert. */
FTimerHandle StorageDistanceTimerHandle;
/** Le coffre actuellement ouvert, pour lui refermer son couvercle. */
TWeakObjectPtr<AStorageContainer> ActiveStorage;
bool bStorageOpen = false;
bool bInventoryOpen = false;
/** Onglet Fabrication affiche. N'a de sens que si bInventoryOpen est vrai. */
@@ -10,6 +10,15 @@ class UHorizontalBox;
class UInventoryComponent;
class UInventorySlotWidget;
/**
* Le curseur entre ou sort d'une case de la barre rapide.
*
* La barre n'affiche rien elle-meme : le panneau de detail vit dans l'ecran
* d'inventaire, qu'elle ne connait pas. C'est le PlayerController qui tient les
* deux widgets et qui fait le pont -- il est deja le proprietaire du HUD.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnHotbarSlotHovered, UInventoryComponent*, Inventory, int32, SlotIndex, bool, bHovered);
/**
* Barre rapide en bas de l'ecran, affichee en permanence.
*
@@ -34,6 +43,19 @@ public:
/** (Re)branche sur l'inventaire du pawn possede. Voir UInventoryWidget. */
void BindToOwningPawn();
/**
* Destination du clic droit pour les cases de la barre.
*
* La barre reste affichee et cliquable par-dessus l'ecran de coffre : sans
* ca, le clic droit marcherait dans la grille et pas dans la barre, ce qui
* passerait pour un bug. Le controller lui pousse le coffre a l'ouverture
* et nullptr a la fermeture.
*/
void SetQuickTransferTarget(UInventoryComponent* InTarget);
UPROPERTY(BlueprintAssignable, Category = "Inventory")
FOnHotbarSlotHovered OnSlotHovered;
protected:
/** A nommer exactement "SlotContainer" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
@@ -54,12 +76,19 @@ private:
UFUNCTION()
void HandleSelectionChanged(int32 NewIndex);
/** Abonne aux cases : relaie le survol vers l'exterieur. */
UFUNCTION()
void HandleSlotHovered(int32 SlotIndex, bool bHovered);
void RebuildSlots(int32 DesiredCount);
UInventoryComponent* ResolveInventory() const;
TWeakObjectPtr<UInventoryComponent> BoundInventory;
/** Destination du clic droit, recopiee sur chaque case creee. */
TWeakObjectPtr<UInventoryComponent> QuickTransferTarget;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UInventorySlotWidget>> SlotWidgets;
@@ -143,6 +143,65 @@ public:
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;
@@ -226,5 +285,33 @@ private:
/** 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;
};
@@ -6,9 +6,12 @@
#include "Blueprint/UserWidget.h"
#include "InventoryScreenWidget.generated.h"
class AStorageContainer;
class UButton;
class UCraftingWidget;
class UInventoryComponent;
class UInventoryWidget;
class UTextBlock;
class UWidgetSwitcher;
/**
@@ -51,6 +54,19 @@ public:
*/
int32 GetHoveredSlotIndex() const;
/**
* Meme chose, avec l'inventaire auquel la case appartient. Depuis les
* coffres, l'index seul ne designe plus rien de facon unique.
*/
bool GetHoveredSlot(UInventoryComponent*& OutInventory, int32& OutIndex) const;
/**
* Survol d'une case exterieure a l'ecran -- la barre rapide, qui reste
* affichee par-dessus lui. Alimente le panneau de detail de la page
* d'inventaire, seul panneau existant.
*/
void NotifyExternalSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered);
/** Revient sur l'onglet Inventaire. Appele a chaque ouverture par Tab. */
UFUNCTION(BlueprintCallable, Category = "UI")
void ShowInventoryTab();
@@ -62,6 +78,29 @@ public:
UPROPERTY(BlueprintAssignable, Category = "UI")
FOnInventoryScreenTabChanged OnTabChanged;
// ------------------------------------------------------------------
// Panneau de coffre
//
// Le contenu d'un coffre s'affiche A COTE de la grille du joueur, dans ce
// meme ecran, et non dans un ecran a part. La raison est du ressenti pur :
// le sac ne bouge pas d'un pixel entre "j'ouvre mon sac" et "j'ouvre un
// coffre". Deux ecrans distincts le feraient sauter a chaque fois.
//
// Techniquement c'est une deuxieme instance du MEME WBP_Inventory que
// InventoryPage. Elle ne peut pas vivre a l'interieur de WBP_Inventory :
// un Widget Blueprint qui se contient lui-meme est une reference
// circulaire, et UMG refuse de le compiler.
// ------------------------------------------------------------------
/** Branche et affiche le panneau de droite. Force l'onglet Inventaire. */
void ShowStorage(AStorageContainer* Container);
/** Masque et debranche le panneau. Sans effet s'il est deja cache. */
void HideStorage();
UFUNCTION(BlueprintPure, Category = "UI|Storage")
bool IsStorageVisible() const { return bStorageVisible; }
protected:
/**
* Absorbe ou traite les depots qui ne visent aucune page.
@@ -91,7 +130,45 @@ protected:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCraftingWidget> CraftingPage;
/**
* Tout le bloc de droite -- cadre, titre, grille, boutons -- qu'on montre et
* qu'on cache d'un bloc.
*
* BindWidgetOptional et non BindWidget, comme tous les elements de coffre
* qui suivent : l'ecran doit rester valide dans un projet ou personne n'a
* encore construit ce panneau. Sans ca, ajouter les coffres casserait la
* compilation de WBP_InventoryScreen tant que la mise en page n'est pas
* faite -- et donc l'inventaire entier.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> StoragePanel;
/** Un deuxieme WBP_Inventory, renomme "StorageGrid". DECOCHE-lui Auto Bind To Pawn. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UInventoryWidget> StorageGrid;
/** Nom du coffre ouvert. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> StorageTitle;
/** Deverse la grille du sac dans le coffre. La barre rapide n'y passe pas. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> StoreAllButton;
/** Vide le coffre dans le sac, dans la limite de la place disponible. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UButton> TakeAllButton;
private:
UFUNCTION()
void HandleStoreAllClicked();
UFUNCTION()
void HandleTakeAllClicked();
/** Inventaire du pawn possede, ou nullptr. */
UInventoryComponent* GetPlayerInventory() const;
UFUNCTION()
void HandleInventoryTabClicked();
@@ -108,4 +185,16 @@ private:
void ShowPage(UWidget* Page);
bool IsInventoryTabActive() const;
/**
* Le panneau de coffre est affiche.
*
* Tant qu'il l'est, l'onglet Fabrication est grise : fabriquer avec un
* coffre ouvert a cote n'a pas de sens tant que ses ressources ne comptent
* pas dans les recettes, et laisser le bouton actif inviterait a un clic
* dont le resultat serait incomprehensible.
*/
bool bStorageVisible = false;
TWeakObjectPtr<UInventoryComponent> BoundStorage;
};
@@ -8,10 +8,18 @@
#include "InventorySlotWidget.generated.h"
class UImage;
class UInventoryWidget;
class UProgressBar;
class UTextBlock;
/**
* Le curseur est entre sur cette case, ou en est sorti.
*
* Un delegue et non un pointeur vers la grille : la barre rapide pose les
* memes cases sans etre un UInventoryWidget, et elle a autant besoin du survol
* depuis que l'inventaire affiche un panneau de detail.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInventorySlotHoverChanged, int32, SlotIndex, bool, bHovered);
/**
* Une case de la grille d'inventaire : une icone et une quantite.
*
@@ -29,7 +37,11 @@ public:
virtual void NativeConstruct() override;
/** Appele une fois a la creation : dit a la case qui elle represente. */
void InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex, UInventoryWidget* InOwningGrid);
void InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex);
/** Le conteneur qui a cree cette case s'y abonne pour suivre le survol. */
UPROPERTY()
FOnInventorySlotHoverChanged OnHoverChanged;
/** Affiche le contenu d'un slot, ou se vide si le slot est vide. */
UFUNCTION(BlueprintCallable, Category = "Inventaire")
@@ -45,6 +57,15 @@ public:
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetSelected(bool bSelected);
/**
* Inventaire vers lequel le CLIC DROIT envoie le contenu de cette case.
*
* Nul en temps normal : hors de l'ecran de coffre il n'existe aucun "autre
* cote" evident, et deviner une destination reviendrait a faire disparaitre
* des objets sur un clic malheureux. C'est l'ecran qui sait, et qui pousse.
*/
void SetQuickTransferTarget(UInventoryComponent* InTarget);
protected:
// --- Glisser-deposer ---
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
@@ -101,8 +122,8 @@ private:
TWeakObjectPtr<UInventoryComponent> OwningInventory;
/** La grille parente, pour lui signaler le survol. */
TWeakObjectPtr<UInventoryWidget> OwningGrid;
/** Destination du clic droit. Voir SetQuickTransferTarget(). */
TWeakObjectPtr<UInventoryComponent> QuickTransferTarget;
int32 SlotIndex = INDEX_NONE;
+106 -4
View File
@@ -8,13 +8,19 @@
class UInventoryComponent;
class UInventorySlotWidget;
class UItemDetailsWidget;
class UUniformGridPanel;
/**
* Grille d'inventaire. Cree ses cases dynamiquement a partir du nombre de
* slots du composant, et se remet a jour uniquement quand l'inventaire change.
* Grille de cases. Cree ses cases dynamiquement a partir du nombre de slots du
* composant, et se remet a jour uniquement quand l'inventaire change.
*
* Le nombre de cases n'est PAS fige : quand un sac ajoutera des slots, la
* Elle ne suppose PLUS qu'elle affiche le sac du joueur : le meme widget sert
* de grille de coffre en lui donnant simplement un autre UInventoryComponent.
* C'est ce qui permet d'avoir un seul WBP_Inventory pose des deux cotes de
* l'ecran de coffre -- et donc la meme apparence, sans duplication.
*
* Le nombre de cases n'est pas fige : quand un sac ajoutera des slots, la
* grille s'agrandira toute seule au prochain rafraichissement.
*/
UCLASS(Abstract)
@@ -34,9 +40,41 @@ public:
UFUNCTION(BlueprintPure, Category = "Inventaire")
int32 GetHoveredSlotIndex() const { return HoveredSlotIndex; }
/** Appele par les cases quand le curseur entre ou sort. */
/**
* Case survolee ET inventaire auquel elle appartient.
*
* L'index seul ne suffit plus : la touche "jeter" doit savoir de quel
* conteneur sortir l'objet, et la case 3 existe aussi bien dans le sac que
* dans le coffre.
*/
bool GetHoveredSlot(UInventoryComponent*& OutInventory, int32& OutIndex) const;
/** Abonne aux cases : le curseur entre ou sort de l'une d'elles. */
UFUNCTION()
void NotifySlotHovered(int32 Index, bool bHovered);
/**
* Survol d'une case qui n'appartient PAS a cette grille -- la barre rapide.
*
* Ne touche pas a HoveredSlotIndex : celui-ci sert a la touche "jeter", qui
* doit continuer de viser la grille et elle seule. Seul le panneau de detail
* repond, parce qu'il n'y en a qu'un a l'ecran et qu'il appartient a la
* grille du sac.
*/
void NotifyExternalSlotHovered(UInventoryComponent* InInventory, int32 InSlotIndex, bool bHovered);
/**
* Autorise ou coupe le panneau de detail de cette grille.
*
* C'est l'ECRAN qui decide, jamais une case a cocher dans la Designer : le
* WBP_Inventory est instancie deux fois sur l'ecran de coffre, et un reglage
* qu'on peut oublier de decocher sur l'une des deux instances est un bug qui
* attend. Coupe : la grille de coffre n'en a jamais, et le sac perd le sien
* tant qu'un coffre est ouvert -- l'ecran est deja assez large a trois
* colonnes.
*/
void SetItemDetailsEnabled(bool bEnabled);
/**
* (Re)branche le widget sur l'inventaire du pawn actuellement possede.
* Indispensable apres un respawn : le nouveau pawn a un nouveau composant,
@@ -44,6 +82,29 @@ public:
*/
void BindToOwningPawn();
/**
* (Re)branche le widget sur un inventaire quelconque -- celui d'un coffre,
* typiquement. nullptr vide la grille : une grille de coffre referme ne doit
* pas continuer d'afficher un contenu qu'on ne pilote plus.
*/
void BindToInventory(UInventoryComponent* InInventory);
UFUNCTION(BlueprintPure, Category = "Inventaire")
UInventoryComponent* GetBoundInventory() const { return BoundInventory.Get(); }
/** Destination du clic droit pour toutes les cases de cette grille. */
void SetQuickTransferTarget(UInventoryComponent* InTarget);
/**
* Change le nombre de colonnes et redispose les cases.
*
* Sert a la grille de coffre : c'est le coffre pose dans le niveau qui
* decide de sa forme, pas le Widget Blueprint -- un coffre de 12 cases et
* un de 30 ne se lisent pas bien avec le meme nombre de colonnes.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void SetColumnCount(int32 NewColumnCount);
protected:
/** A nommer exactement "SlotGrid" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
@@ -57,6 +118,29 @@ protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire", meta = (ClampMin = "1"))
int32 ColumnCount = 6;
/**
* La grille va chercher toute seule l'inventaire du pawn a sa construction.
*
* DECOCHE-LE sur la grille de coffre. Sans ca elle s'accrocherait au pawn
* dans son NativeConstruct -- qui s'execute avant que l'ecran ait eu le
* temps de lui donner le conteneur -- et afficherait le sac du joueur des
* deux cotes de l'ecran.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inventaire")
bool bAutoBindToPawn = true;
/**
* Le panneau de detail pose a cote de la grille, dans le meme Widget
* Blueprint. A nommer "ItemDetailsPanel".
*
* Optionnel : la grille doit rester valide dans un WBP_Inventory ou ce
* panneau n'existe pas encore, sinon ajouter cette fonctionnalite casserait
* la compilation de l'inventaire entier tant que la mise en page n'est pas
* faite.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UItemDetailsWidget> ItemDetailsPanel;
protected:
/**
* Absorbe les depots qui ne visent aucune case. Sans ca, lacher un objet
@@ -72,13 +156,31 @@ private:
/** Cree ou supprime des cases pour coller au nombre de slots. */
void RebuildGrid(int32 DesiredCount);
/** Detruit toutes les cases : leur inventaire a change, elles sont perimees. */
void ClearSlotWidgets();
/** Retrouve l'inventaire du pawn possede. */
UInventoryComponent* ResolveInventory() const;
/**
* Point de passage UNIQUE du survol : met a jour l'index et le panneau de
* detail. Ne fait rien si l'index ne change pas.
*/
void SetHoveredSlot(int32 NewIndex);
/** Pousse la case survolee dans le panneau, ou l'efface. */
void UpdateItemDetails();
TWeakObjectPtr<UInventoryComponent> BoundInventory;
/** Destination du clic droit, recopiee sur chaque case creee. */
TWeakObjectPtr<UInventoryComponent> QuickTransferTarget;
int32 HoveredSlotIndex = INDEX_NONE;
/** Voir SetItemDetailsEnabled(). */
bool bItemDetailsEnabled = true;
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
UPROPERTY(Transient)
TArray<TObjectPtr<UInventorySlotWidget>> SlotWidgets;
@@ -0,0 +1,114 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ItemDetailsWidget.generated.h"
class UImage;
class UInventoryComponent;
class UProgressBar;
class UTextBlock;
/**
* Le panneau qui decrit l'objet survole : icone, nom, description, usure.
* Rien d'autre -- volontairement.
*
* Il ne connait ni la grille ni l'ecran. On lui pousse un couple
* (inventaire, index) et il lit -- et NON une copie du FInventorySlot :
* le contenu peut changer pendant le survol (une charge consommee, un craft
* qui vide la pile), donc le panneau s'abonne a OnInventoryChanged de
* l'inventaire affiche et se relit tout seul. Jamais de Tick.
*
* Aucun texte en dur ici en dehors du format numerique de l'usure : la mise en
* page et les libelles appartiennent au Widget Blueprint.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UItemDetailsWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Affiche la case InSlotIndex de InInventory. Index invalide = etat vide. */
void ShowSlot(UInventoryComponent* InInventory, int32 InSlotIndex);
/** Repasse en etat vide et coupe l'abonnement. */
void Clear();
/**
* Efface, mais seulement si c'est bien CETTE case qui est affichee.
*
* Le panneau est unique et trois conteneurs le nourrissent -- grille, barre
* rapide, et la grille elle-meme quand elle se rebranche. Passer de l'un a
* l'autre emet "entre dans B" PUIS "sort de A" : un effacement
* inconditionnel viderait l'affichage qui vient d'arriver.
*/
void ClearSlot(const UInventoryComponent* InInventory, int32 InSlotIndex);
protected:
/** A nommer exactement "ItemIcon" dans le Widget Blueprint. */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> ItemIcon;
/** A nommer exactement "ItemNameText". */
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> ItemNameText;
/** Pense a cocher "Auto Wrap Text" dessus, sinon le panneau s'elargit. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> ItemDescriptionText;
/**
* Bloc d'usure entier -- barre et texte ensemble -- replie sur tout objet
* qui n'a pas de charges multiples. Sans lui, la barre et son texte
* devraient etre replies separement et finiraient un jour desynchronises.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> UsesPanel;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> UsesBar;
/** "3 / 5", pose par-dessus la barre. */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> UsesText;
/**
* Le conteneur qui empile tout le panneau -- a renommer "ContentRoot", ce
* n'est pas un widget de plus.
*
* Il est masque en bloc quand rien n'est survole. Sans lui, la barre de
* separation resterait seule a l'ecran : c'est une decoration du Blueprint,
* le C++ n'a aucune raison de la connaitre individuellement.
*/
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UWidget> ContentRoot;
private:
UFUNCTION()
void HandleInventoryChanged();
/** Relit la case et repeint tout le panneau. */
void Refresh();
/**
* Etat vide : plus rien a l'ecran.
*
* Le panneau n'a pas de fond, donc vide il est simplement invisible -- et sa
* largeur est tenue par le Size Box du Blueprint, pas par son contenu. C'est
* ce qui empeche la grille de sauter lateralement quand le curseur quitte
* une case.
*/
void ClearDisplay();
/** (Re)branche OnInventoryChanged. Un seul abonnement a la fois. */
void BindInventory(UInventoryComponent* InInventory);
TWeakObjectPtr<UInventoryComponent> BoundInventory;
int32 SlotIndex = INDEX_NONE;
};
@@ -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;
};