41d6e6a7f3
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>
246 lines
8.0 KiB
C++
246 lines
8.0 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "InventorySlotWidget.h"
|
|
|
|
#include "Components/Image.h"
|
|
#include "Components/ProgressBar.h"
|
|
#include "Components/TextBlock.h"
|
|
#include "Components/Widget.h"
|
|
#include "InventoryDragDropOperation.h"
|
|
#include "ItemDataAsset.h"
|
|
|
|
void UInventorySlotWidget::NativeConstruct()
|
|
{
|
|
Super::NativeConstruct();
|
|
|
|
// Force en C++ plutot que de dependre d'un reglage du Blueprint : une case
|
|
// non testable a la souris ne recevrait jamais NativeOnMouseButtonDown, et
|
|
// le glisser-deposer serait mysterieusement inerte.
|
|
// NativeConstruct s'execute APRES ConfigureAsDragVisual() pour un fantome
|
|
// (le widget n'est construit qu'une fois ajoute a la couche de drag),
|
|
// d'ou le test : sans lui, on remettrait le fantome en Visible et il
|
|
// intercepterait le depot destine a la case du dessous.
|
|
SetVisibility(bIsDragVisual ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Visible);
|
|
}
|
|
|
|
void UInventorySlotWidget::SetSelected(bool bSelected)
|
|
{
|
|
if (SelectionBorder)
|
|
{
|
|
SelectionBorder->SetVisibility(bSelected ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed);
|
|
}
|
|
}
|
|
|
|
void UInventorySlotWidget::ConfigureAsDragVisual()
|
|
{
|
|
bIsDragVisual = true;
|
|
|
|
// Le cadre de selection appartient a la barre, pas a l'objet.
|
|
if (SelectionBorder)
|
|
{
|
|
SelectionBorder->SetVisibility(ESlateVisibility::Collapsed);
|
|
}
|
|
|
|
// Le fond de case appartient a la grille, pas a l'objet. Le trainer sous
|
|
// le curseur donne l'impression de deplacer la case elle-meme.
|
|
if (SlotBackground)
|
|
{
|
|
SlotBackground->SetVisibility(ESlateVisibility::Collapsed);
|
|
}
|
|
|
|
SetVisibility(ESlateVisibility::HitTestInvisible);
|
|
}
|
|
|
|
void UInventorySlotWidget::InitSlot(UInventoryComponent* InInventory, int32 InSlotIndex)
|
|
{
|
|
OwningInventory = InInventory;
|
|
SlotIndex = InSlotIndex;
|
|
}
|
|
|
|
void UInventorySlotWidget::SetQuickTransferTarget(UInventoryComponent* InTarget)
|
|
{
|
|
QuickTransferTarget = InTarget;
|
|
}
|
|
|
|
void UInventorySlotWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
|
|
{
|
|
Super::NativeOnMouseEnter(InGeometry, InMouseEvent);
|
|
|
|
OnHoverChanged.Broadcast(SlotIndex, true);
|
|
}
|
|
|
|
void UInventorySlotWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
|
|
{
|
|
Super::NativeOnMouseLeave(InMouseEvent);
|
|
|
|
OnHoverChanged.Broadcast(SlotIndex, false);
|
|
}
|
|
|
|
void UInventorySlotWidget::SetSlot(const FInventorySlot& InSlot)
|
|
{
|
|
CachedSlot = InSlot;
|
|
|
|
// Filet de securite : toute mise a jour de contenu remet la case en pleine
|
|
// opacite, meme si un glisser s'est termine par un chemin imprevu.
|
|
if (!bIsDragVisual)
|
|
{
|
|
SetRenderOpacity(1.f);
|
|
}
|
|
|
|
const bool bEmpty = InSlot.IsEmpty();
|
|
|
|
if (ItemIcon)
|
|
{
|
|
// Hidden et non Collapsed : on veut que la case garde sa taille dans
|
|
// la grille meme vide, sinon la mise en page sauterait a chaque
|
|
// ramassage.
|
|
if (bEmpty || !InSlot.Item->Icon)
|
|
{
|
|
ItemIcon->SetVisibility(ESlateVisibility::Hidden);
|
|
}
|
|
else
|
|
{
|
|
ItemIcon->SetBrushFromTexture(InSlot.Item->Icon, /*bMatchSize=*/false);
|
|
ItemIcon->SetVisibility(ESlateVisibility::HitTestInvisible);
|
|
}
|
|
}
|
|
|
|
if (QuantityText)
|
|
{
|
|
// On n'affiche pas "x1" : ca alourdit la grille pour rien.
|
|
if (bEmpty || InSlot.Quantity <= 1)
|
|
{
|
|
QuantityText->SetVisibility(ESlateVisibility::Hidden);
|
|
}
|
|
else
|
|
{
|
|
QuantityText->SetText(FText::AsNumber(InSlot.Quantity));
|
|
QuantityText->SetVisibility(ESlateVisibility::HitTestInvisible);
|
|
}
|
|
}
|
|
|
|
if (UsesBar)
|
|
{
|
|
// Collapsed et non Hidden : une barre d'usure sur un objet qui n'en a
|
|
// pas ne doit meme pas reserver sa place au bas de la case.
|
|
const int32 MaxUses = bEmpty ? 1 : FMath::Max(1, InSlot.Item->MaxUses);
|
|
if (MaxUses <= 1)
|
|
{
|
|
UsesBar->SetVisibility(ESlateVisibility::Collapsed);
|
|
}
|
|
else
|
|
{
|
|
UsesBar->SetPercent(FMath::Clamp(static_cast<float>(InSlot.RemainingUses) / static_cast<float>(MaxUses), 0.f, 1.f));
|
|
UsesBar->SetVisibility(ESlateVisibility::HitTestInvisible);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// On ne demarre pas le glisser tout de suite : DetectDrag attend que la
|
|
// souris bouge reellement. Sans ca, un simple clic serait interprete comme
|
|
// un debut de glisser et on ne pourrait jamais faire autre chose.
|
|
return FReply::Handled().DetectDrag(TakeWidget(), EKeys::LeftMouseButton);
|
|
}
|
|
|
|
void UInventorySlotWidget::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation)
|
|
{
|
|
Super::NativeOnDragDetected(InGeometry, InMouseEvent, OutOperation);
|
|
|
|
if (CachedSlot.IsEmpty() || !OwningInventory.IsValid() || SlotIndex == INDEX_NONE)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Modificateurs au moment de la saisie : Maj pour la moitie, Ctrl pour un
|
|
// seul. Conventions etablies du genre, le joueur les connait deja.
|
|
int32 DraggedQuantity = CachedSlot.Quantity;
|
|
if (InMouseEvent.IsControlDown())
|
|
{
|
|
DraggedQuantity = 1;
|
|
}
|
|
else if (InMouseEvent.IsShiftDown())
|
|
{
|
|
// Moitie arrondie au superieur : sur une pile de 7 on emporte 4.
|
|
// C'est la convention de Minecraft et consorts.
|
|
DraggedQuantity = (CachedSlot.Quantity + 1) / 2;
|
|
}
|
|
|
|
UInventoryDragDropOperation* Operation = NewObject<UInventoryDragDropOperation>();
|
|
Operation->SourceSlotIndex = SlotIndex;
|
|
Operation->Quantity = DraggedQuantity;
|
|
Operation->SourceInventory = OwningInventory;
|
|
Operation->Pivot = EDragPivot::CenterCenter;
|
|
|
|
// Le fantome sous le curseur est une copie de cette meme case : aucun
|
|
// widget supplementaire a maintenir, et l'apparence reste coherente.
|
|
// Il affiche la quantite EMPORTEE, pas la pile complete.
|
|
if (UInventorySlotWidget* Visual = CreateWidget<UInventorySlotWidget>(GetOwningPlayer(), GetClass()))
|
|
{
|
|
FInventorySlot VisualSlot = CachedSlot;
|
|
VisualSlot.Quantity = DraggedQuantity;
|
|
|
|
Visual->SetSlot(VisualSlot);
|
|
Visual->ConfigureAsDragVisual();
|
|
Visual->SetRenderOpacity(DragVisualOpacity);
|
|
Operation->DefaultDragVisual = Visual;
|
|
}
|
|
|
|
// On n'estompe la case source que si la pile part en entier. Lors d'une
|
|
// division une partie reste sur place, et la faire palir serait mentir
|
|
// sur ce qu'elle contient encore.
|
|
if (DraggedQuantity >= CachedSlot.Quantity)
|
|
{
|
|
SetRenderOpacity(DraggedSlotOpacity);
|
|
}
|
|
|
|
// Les deux issues possibles remettent l'opacite : depot reussi, ou
|
|
// abandon hors de la grille.
|
|
Operation->OnDrop.AddDynamic(this, &UInventorySlotWidget::HandleDragFinished);
|
|
Operation->OnDragCancelled.AddDynamic(this, &UInventorySlotWidget::HandleDragFinished);
|
|
|
|
OutOperation = Operation;
|
|
}
|
|
|
|
void UInventorySlotWidget::HandleDragFinished(UDragDropOperation* Operation)
|
|
{
|
|
SetRenderOpacity(1.f);
|
|
}
|
|
|
|
bool UInventorySlotWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
|
|
{
|
|
const UInventoryDragDropOperation* Payload = Cast<UInventoryDragDropOperation>(InOperation);
|
|
if (!Payload || SlotIndex == INDEX_NONE)
|
|
{
|
|
return Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
|
|
}
|
|
|
|
// 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);
|
|
}
|