(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:
@@ -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
|
||||
Reference in New Issue
Block a user