// 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 "InventoryWidget.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, UInventoryWidget* InOwningGrid) { OwningInventory = InInventory; SlotIndex = InSlotIndex; OwningGrid = InOwningGrid; } void UInventorySlotWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { Super::NativeOnMouseEnter(InGeometry, InMouseEvent); if (OwningGrid.IsValid()) { OwningGrid->NotifySlotHovered(SlotIndex, true); } } void UInventorySlotWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent) { Super::NativeOnMouseLeave(InMouseEvent); if (OwningGrid.IsValid()) { OwningGrid->NotifySlotHovered(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(InSlot.RemainingUses) / static_cast(MaxUses), 0.f, 1.f)); UsesBar->SetVisibility(ESlateVisibility::HitTestInvisible); } } } FReply UInventorySlotWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { 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(); 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(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(InOperation); if (!Payload || SlotIndex == INDEX_NONE) { 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); }