// Fill out your copyright notice in the Description page of Project Settings. #include "InventoryWidget.h" #include "Components/UniformGridPanel.h" #include "Components/UniformGridSlot.h" #include "FpsPlayer.h" #include "GameFramework/PlayerController.h" #include "InventoryComponent.h" #include "InventoryDragDropOperation.h" #include "InventorySlotWidget.h" #include "ItemDetailsWidget.h" void UInventoryWidget::NativeConstruct() { Super::NativeConstruct(); // 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 (!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 = InInventory; if (BoundInventory.IsValid()) { BoundInventory->OnInventoryChanged.AddDynamic(this, &UInventoryWidget::HandleInventoryChanged); } // Les cases gardent l'ancien composant en memoire : on force leur // recreation en vidant la grille. ClearSlotWidgets(); Refresh(); } void UInventoryWidget::NativeDestruct() { if (BoundInventory.IsValid()) { BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UInventoryWidget::HandleInventoryChanged); } BoundInventory.Reset(); Super::NativeDestruct(); } UInventoryComponent* UInventoryWidget::ResolveInventory() const { const APlayerController* OwningController = GetOwningPlayer(); AFpsPlayer* Player = OwningController ? Cast(OwningController->GetPawn()) : nullptr; return Player ? Player->GetInventoryComponent() : nullptr; } 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) { 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". 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); // On n'arrive ici que si AUCUNE case n'a traite le depot : les widgets // enfants sont consultes en premier et arretent la propagation. const UInventoryDragDropOperation* Payload = Cast(InOperation); if (!Payload || Payload->SourceSlotIndex == INDEX_NONE) { return true; } // Lache DANS la grille mais entre deux cases (l'espacement) : on absorbe. // Jeter un objet au sol parce que le joueur a rate une case de trois // pixels serait impardonnable dans un jeu de survie. if (SlotGrid && SlotGrid->GetCachedGeometry().IsUnderLocation(InDragDropEvent.GetScreenSpacePosition())) { return true; } // 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(OwningController->GetPawn())) { Player->DropFromInventory(Payload->SourceInventory.Get(), Payload->SourceSlotIndex, Payload->Quantity); } } return true; } void UInventoryWidget::Refresh() { if (!SlotGrid) { UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget::Refresh : SlotGrid est nul. Le widget nomme 'SlotGrid' manque dans le Blueprint.")); 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()) { ClearSlotWidgets(); return; } const TArray& Slots = BoundInventory->GetSlots(); // La grille commence APRES la barre rapide : les deux conteneurs sont // 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); // Le nombre de slots peut changer en cours de partie (sac equipe). // On ne reconstruit que si la taille a bouge : recreer toutes les cases // a chaque ramassage serait du gaspillage pur. RebuildGrid(GridCount); for (int32 Index = 0; Index < SlotWidgets.Num(); ++Index) { const int32 Absolute = Offset + Index; if (SlotWidgets[Index] && Slots.IsValidIndex(Absolute)) { SlotWidgets[Index]->SetSlot(Slots[Absolute]); } } } 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) { return; } if (!SlotWidgetClass) { UE_LOG(LogTemp, Warning, TEXT("UInventoryWidget : SlotWidgetClass n'est pas assignee, la grille restera vide.")); return; } // Trop de cases : on retire le surplus par la fin. while (SlotWidgets.Num() > DesiredCount) { const int32 Last = SlotWidgets.Num() - 1; if (SlotWidgets[Last]) { SlotWidgets[Last]->RemoveFromParent(); } 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) { UInventorySlotWidget* NewSlot = CreateWidget(this, SlotWidgetClass); if (!NewSlot) { // Arrive typiquement quand le Widget Blueprint de case ne compile // plus, apres un changement de BindWidget cote C++ par exemple. UE_LOG(LogTemp, Error, TEXT("UInventoryWidget : impossible de creer une case a partir de %s. Recompile ce Widget Blueprint."), *GetNameSafe(SlotWidgetClass)); break; } const int32 Index = SlotWidgets.Num(); // On donne l'index ABSOLU dans le tableau, pas la position dans la // 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); NewSlot->OnHoverChanged.AddDynamic(this, &UInventoryWidget::NotifySlotHovered); NewSlot->SetQuickTransferTarget(QuickTransferTarget.Get()); SlotGrid->AddChildToUniformGrid(NewSlot, Index / Columns, Index % Columns); SlotWidgets.Add(NewSlot); } }