(Feat) Add Craft Systeme

This commit is contained in:
2026-07-30 23:08:33 +02:00
parent 9b5ad7c8a4
commit 3f3ae1f5fa
57 changed files with 2276 additions and 127 deletions
@@ -0,0 +1,226 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CraftingComponent.h"
#include "CraftingRecipeBook.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
UCraftingComponent::UCraftingComponent()
{
PrimaryComponentTick.bCanEverTick = false;
// Necessaire pour que InitializeComponent() soit appele.
bWantsInitializeComponent = true;
}
void UCraftingComponent::InitializeComponent()
{
Super::InitializeComponent();
// Les mains sont toujours disponibles. Les postes s'ajoutent par-dessus.
AvailableStations.Add(ECraftingStation::Hands);
if (const AActor* Owner = GetOwner())
{
Inventory = Owner->FindComponentByClass<UInventoryComponent>();
}
if (!Inventory)
{
UE_LOG(LogTemp, Warning, TEXT("UCraftingComponent : aucun UInventoryComponent sur %s, aucune fabrication ne sera possible."),
*GetNameSafe(GetOwner()));
}
if (!RecipeBook)
{
UE_LOG(LogTemp, Warning, TEXT("UCraftingComponent : RecipeBook n'est pas assigne sur %s, la grille de craft restera vide."),
*GetNameSafe(GetOwner()));
}
}
void UCraftingComponent::AddAvailableStation(ECraftingStation Station)
{
if (Station == ECraftingStation::Count)
{
return;
}
bool bAlreadyThere = false;
AvailableStations.Add(Station, &bAlreadyThere);
// On ne diffuse que sur un vrai changement : deux volumes d'etabli qui se
// chevauchent rafraichiraient l'interface deux fois pour rien.
if (!bAlreadyThere)
{
OnAvailableStationsChanged.Broadcast();
}
}
void UCraftingComponent::RemoveAvailableStation(ECraftingStation Station)
{
// Retirer les mains laisserait le joueur incapable de fabriquer quoi que ce
// soit, sans qu'aucun message ne l'explique.
if (Station == ECraftingStation::Hands)
{
return;
}
if (AvailableStations.Remove(Station) > 0)
{
OnAvailableStationsChanged.Broadcast();
}
}
void UCraftingComponent::GatherRecipes(TArray<UCraftingRecipeDataAsset*>& OutRecipes, ECraftingStation StationFilter) const
{
OutRecipes.Reset();
if (!RecipeBook)
{
return;
}
const TArray<TObjectPtr<UCraftingRecipeDataAsset>>& AllRecipes = RecipeBook->GetRecipes();
OutRecipes.Reserve(AllRecipes.Num());
for (UCraftingRecipeDataAsset* Recipe : AllRecipes)
{
// Une entree vide dans le livre est signalee par sa validation d'asset.
// Ici on se contente de la sauter.
if (!Recipe)
{
continue;
}
if (!AvailableStations.Contains(Recipe->RequiredStation))
{
continue;
}
if (StationFilter != ECraftingStation::Count && Recipe->RequiredStation != StationFilter)
{
continue;
}
OutRecipes.Add(Recipe);
}
}
bool UCraftingComponent::CanCraft(const UCraftingRecipeDataAsset* Recipe, ECraftFailureReason& OutReason) const
{
OutReason = ECraftFailureReason::None;
if (!RecipeBook)
{
OutReason = ECraftFailureReason::NoRecipeBook;
return false;
}
if (!Recipe || !Recipe->ResultItem)
{
OutReason = ECraftFailureReason::InvalidRecipe;
return false;
}
if (!Inventory)
{
OutReason = ECraftFailureReason::InvalidRecipe;
return false;
}
if (!AvailableStations.Contains(Recipe->RequiredStation))
{
OutReason = ECraftFailureReason::StationUnavailable;
return false;
}
for (const FCraftIngredient& Ingredient : Recipe->Ingredients)
{
// Un ingredient nul est refuse par la validation de l'asset. S'il passe
// quand meme, on ne fabrique pas : mieux vaut un bouton grise qu'une
// recette qui coute moins cher que ce qu'elle affiche.
if (!Ingredient.Item)
{
OutReason = ECraftFailureReason::InvalidRecipe;
return false;
}
if (Inventory->GetItemCount(Ingredient.Item) < Ingredient.Quantity)
{
OutReason = ECraftFailureReason::MissingIngredients;
return false;
}
}
return true;
}
void UCraftingComponent::GatherIngredientStatus(const UCraftingRecipeDataAsset* Recipe, TArray<FCraftIngredientStatus>& OutStatus) const
{
OutStatus.Reset();
if (!Recipe)
{
return;
}
OutStatus.Reserve(Recipe->Ingredients.Num());
for (const FCraftIngredient& Ingredient : Recipe->Ingredients)
{
if (!Ingredient.Item)
{
continue;
}
FCraftIngredientStatus& Status = OutStatus.AddDefaulted_GetRef();
Status.Item = Ingredient.Item;
Status.RequiredQuantity = Ingredient.Quantity;
Status.OwnedQuantity = Inventory ? Inventory->GetItemCount(Ingredient.Item) : 0;
}
}
bool UCraftingComponent::Craft(UCraftingRecipeDataAsset* Recipe)
{
ECraftFailureReason Reason = ECraftFailureReason::None;
if (!CanCraft(Recipe, Reason))
{
return false;
}
// CanCraft a deja verifie que tout est present : aucun RemoveItem ne peut
// echouer a partir d'ici, donc pas de consommation a moitie faite.
//
// Chaque retrait diffuse OnInventoryChanged, donc l'interface se rafraichit
// une fois par ingredient. C'est du gaspillage assume : ca reste quelques
// dizaines de SetSlot sur un clic, et grouper les diffusions demanderait
// une API de lot dans l'inventaire pour un gain invisible.
for (const FCraftIngredient& Ingredient : Recipe->Ingredients)
{
Inventory->RemoveItem(Ingredient.Item, Ingredient.Quantity);
}
// -1 : l'objet sort neuf de la fabrication, avec toutes ses charges.
const int32 Leftover = Inventory->AddItem(Recipe->ResultItem, Recipe->ResultQuantity, -1);
if (Leftover > 0)
{
if (OnCraftOverflow.IsBound())
{
OnCraftOverflow.Broadcast(Recipe->ResultItem, Leftover, -1);
}
else
{
// Personne pour poser l'objet au sol : il est perdu. C'est une erreur
// de branchement, pas une situation de jeu -- d'ou le niveau Error.
UE_LOG(LogTemp, Error,
TEXT("UCraftingComponent : %d x %s n'ont pas pu entrer dans l'inventaire et personne n'ecoute OnCraftOverflow. Objets perdus."),
Leftover, *GetNameSafe(Recipe->ResultItem));
}
}
OnCraftSucceeded.Broadcast(Recipe);
return true;
}
@@ -0,0 +1,41 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CraftingIngredientRowWidget.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
#include "ItemDataAsset.h"
void UCraftingIngredientRowWidget::SetIngredient(const FCraftIngredientStatus& Status)
{
if (IngredientIcon)
{
UTexture2D* Icon = Status.Item ? Status.Item->Icon : nullptr;
if (Icon)
{
IngredientIcon->SetBrushFromTexture(Icon);
IngredientIcon->SetVisibility(ESlateVisibility::HitTestInvisible);
}
else
{
// Sans ca, un objet sans icone afficherait le carre blanc du brush
// par defaut au milieu de la liste.
IngredientIcon->SetVisibility(ESlateVisibility::Hidden);
}
}
if (IngredientNameText)
{
IngredientNameText->SetText(Status.Item ? Status.Item->DisplayName : FText::GetEmpty());
}
if (IngredientCountText)
{
IngredientCountText->SetText(FText::Format(CountFormat,
FText::AsNumber(Status.OwnedQuantity),
FText::AsNumber(Status.RequiredQuantity)));
IngredientCountText->SetColorAndOpacity(FSlateColor(Status.IsSatisfied() ? EnoughColor : MissingColor));
}
}
@@ -0,0 +1,50 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CraftingRecipeBook.h"
#include "CraftingRecipeDataAsset.h"
#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif
#define LOCTEXT_NAMESPACE "CraftingBook"
#if WITH_EDITOR
EDataValidationResult UCraftingRecipeBook::IsDataValid(FDataValidationContext& Context) const
{
EDataValidationResult Result = Super::IsDataValid(Context);
// Une case vide dans le tableau ne casse rien au runtime, on la saute. Mais
// c'est le symptome d'une recette supprimee ou d'un ajout laisse en plan, et
// la grille de craft aurait un trou sans qu'on sache pourquoi.
TSet<const UCraftingRecipeDataAsset*> SeenRecipes;
SeenRecipes.Reserve(Recipes.Num());
for (int32 Index = 0; Index < Recipes.Num(); ++Index)
{
const UCraftingRecipeDataAsset* Recipe = Recipes[Index];
if (!Recipe)
{
Context.AddWarning(FText::Format(
LOCTEXT("BookNullRecipe", "Book entry {0} is empty."), Index));
continue;
}
bool bAlreadySeen = false;
SeenRecipes.Add(Recipe, &bAlreadySeen);
if (bAlreadySeen)
{
Context.AddWarning(FText::Format(
LOCTEXT("BookDuplicateRecipe", "Recipe {0} is listed twice: it will show up twice in the grid."),
FText::FromString(Recipe->GetName())));
}
}
return Result;
}
#endif
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,83 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CraftingRecipeDataAsset.h"
#include "ItemDataAsset.h"
#if WITH_EDITOR
#include "Misc/DataValidation.h"
#endif
#define LOCTEXT_NAMESPACE "Crafting"
FText UCraftingRecipeDataAsset::GetDisplayName() const
{
if (!DisplayNameOverride.IsEmpty())
{
return DisplayNameOverride;
}
return ResultItem ? ResultItem->DisplayName : FText::GetEmpty();
}
UTexture2D* UCraftingRecipeDataAsset::GetIcon() const
{
if (IconOverride)
{
return IconOverride;
}
return ResultItem ? ResultItem->Icon : nullptr;
}
#if WITH_EDITOR
EDataValidationResult UCraftingRecipeDataAsset::IsDataValid(FDataValidationContext& Context) const
{
EDataValidationResult Result = Super::IsDataValid(Context);
if (!ResultItem)
{
Context.AddError(LOCTEXT("RecipeNoResult", "This recipe has no result item: it would consume the ingredients and give nothing back."));
Result = EDataValidationResult::Invalid;
}
if (RecipeId.IsNone())
{
Context.AddWarning(LOCTEXT("RecipeNoId", "RecipeId is empty. Saved recipe unlocks will not be able to identify this recipe."));
}
// Deux lignes portant le meme objet donneraient un cout affiche en double
// dans le panneau de detail, alors que la verification, elle, les cumule.
// L'ecart entre les deux est indetectable a la lecture de l'asset.
TSet<const UItemDataAsset*> SeenItems;
SeenItems.Reserve(Ingredients.Num());
for (int32 Index = 0; Index < Ingredients.Num(); ++Index)
{
const FCraftIngredient& Ingredient = Ingredients[Index];
if (!Ingredient.Item)
{
Context.AddError(FText::Format(
LOCTEXT("RecipeNullIngredient", "Ingredient {0} has no item assigned."), Index));
Result = EDataValidationResult::Invalid;
continue;
}
bool bAlreadySeen = false;
SeenItems.Add(Ingredient.Item, &bAlreadySeen);
if (bAlreadySeen)
{
Context.AddError(FText::Format(
LOCTEXT("RecipeDuplicateIngredient", "Item {0} appears twice in the ingredients. Merge them into a single line."),
FText::FromString(Ingredient.Item->GetName())));
Result = EDataValidationResult::Invalid;
}
}
return Result;
}
#endif
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,66 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CraftingRecipeSlotWidget.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
#include "CraftingRecipeDataAsset.h"
#include "CraftingWidget.h"
void UCraftingRecipeSlotWidget::InitSlot(UCraftingRecipeDataAsset* InRecipe, UCraftingWidget* InOwningGrid)
{
Recipe = InRecipe;
OwningGrid = InOwningGrid;
if (RecipeIcon)
{
UTexture2D* Icon = Recipe ? Recipe->GetIcon() : nullptr;
if (Icon)
{
RecipeIcon->SetBrushFromTexture(Icon);
RecipeIcon->SetVisibility(ESlateVisibility::HitTestInvisible);
}
else
{
// Pas d'icone : on masque plutot que de laisser le brush par defaut,
// qui afficherait un carre blanc franchement laid dans la grille.
RecipeIcon->SetVisibility(ESlateVisibility::Hidden);
}
}
if (RecipeNameText)
{
RecipeNameText->SetText(Recipe ? Recipe->GetDisplayName() : FText::GetEmpty());
}
SetSelected(false);
SetCraftable(true);
}
void UCraftingRecipeSlotWidget::SetSelected(bool bSelected)
{
if (SelectionBorder)
{
SelectionBorder->SetVisibility(bSelected ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden);
}
}
void UCraftingRecipeSlotWidget::SetCraftable(bool bCraftable)
{
if (RecipeIcon)
{
RecipeIcon->SetColorAndOpacity(bCraftable ? CraftableTint : UncraftableTint);
}
}
FReply UCraftingRecipeSlotWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && OwningGrid.IsValid())
{
OwningGrid->NotifyRecipeClicked(Recipe);
return FReply::Handled();
}
return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
}
@@ -0,0 +1,397 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "CraftingWidget.h"
#include "Components/Button.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
#include "Components/UniformGridPanel.h"
#include "CraftingComponent.h"
#include "CraftingIngredientRowWidget.h"
#include "CraftingRecipeSlotWidget.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
void UCraftingWidget::NativeConstruct()
{
Super::NativeConstruct();
if (CraftButton)
{
CraftButton->OnClicked.AddDynamic(this, &UCraftingWidget::HandleCraftClicked);
}
BindToOwningPawn();
}
void UCraftingWidget::NativeDestruct()
{
UnbindDelegates();
Super::NativeDestruct();
}
UCraftingComponent* UCraftingWidget::ResolveCrafting() const
{
const APlayerController* OwningController = GetOwningPlayer();
AFpsPlayer* PlayerPawn = OwningController ? Cast<AFpsPlayer>(OwningController->GetPawn()) : nullptr;
return PlayerPawn ? PlayerPawn->GetCraftingComponent() : nullptr;
}
void UCraftingWidget::UnbindDelegates()
{
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UCraftingWidget::HandleInventoryChanged);
}
BoundInventory.Reset();
if (BoundCrafting.IsValid())
{
BoundCrafting->OnAvailableStationsChanged.RemoveDynamic(this, &UCraftingWidget::HandleStationsChanged);
}
BoundCrafting.Reset();
}
void UCraftingWidget::BindToOwningPawn()
{
UCraftingComponent* NewCrafting = ResolveCrafting();
if (BoundCrafting.Get() == NewCrafting)
{
return;
}
UnbindDelegates();
BoundCrafting = NewCrafting;
if (BoundCrafting.IsValid())
{
BoundCrafting->OnAvailableStationsChanged.AddDynamic(this, &UCraftingWidget::HandleStationsChanged);
// L'inventaire est la vraie source des couleurs et de l'etat du bouton :
// c'est lui qui change quand le joueur ramasse ou consomme.
if (const AActor* Owner = BoundCrafting->GetOwner())
{
BoundInventory = Owner->FindComponentByClass<UInventoryComponent>();
if (BoundInventory.IsValid())
{
BoundInventory->OnInventoryChanged.AddDynamic(this, &UCraftingWidget::HandleInventoryChanged);
}
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget : aucun UCraftingComponent trouve sur le pawn possede."));
}
// Les cases gardent l'ancienne recette et l'ancien parent en memoire : on
// force leur recreation en vidant la grille.
for (UCraftingRecipeSlotWidget* SlotWidget : RecipeSlots)
{
if (SlotWidget)
{
SlotWidget->RemoveFromParent();
}
}
RecipeSlots.Reset();
// Les lignes de cout aussi : elles affichent l'inventaire du pawn precedent.
for (UCraftingIngredientRowWidget* Row : IngredientRows)
{
if (Row)
{
Row->RemoveFromParent();
}
}
IngredientRows.Reset();
SelectedRecipe = nullptr;
Refresh();
}
void UCraftingWidget::HandleInventoryChanged()
{
// Seules les couleurs et l'etat du bouton bougent : la liste des recettes,
// elle, ne depend que des postes accessibles.
RefreshStates();
}
void UCraftingWidget::HandleStationsChanged()
{
Refresh();
}
void UCraftingWidget::SetStationFilter(ECraftingStation InFilter)
{
if (StationFilter == InFilter)
{
return;
}
StationFilter = InFilter;
Refresh();
}
void UCraftingWidget::Refresh()
{
if (!RecipeGrid)
{
UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget::Refresh : le widget nomme 'RecipeGrid' manque dans le Blueprint."));
return;
}
TArray<UCraftingRecipeDataAsset*> Recipes;
if (BoundCrafting.IsValid())
{
BoundCrafting->GatherRecipes(Recipes, StationFilter);
}
RebuildGrid(Recipes);
if (EmptyGridMessage)
{
EmptyGridMessage->SetVisibility(Recipes.Num() > 0 ? ESlateVisibility::Collapsed : ESlateVisibility::Visible);
}
// La recette choisie a pu disparaitre de la liste : on s'est eloigne de
// l'etabli, ou le filtre a change.
if (SelectedRecipe && !Recipes.Contains(SelectedRecipe))
{
SelectedRecipe = nullptr;
}
RefreshStates();
}
void UCraftingWidget::RebuildGrid(const TArray<UCraftingRecipeDataAsset*>& NewRecipes)
{
// La liste ne change qu'en s'approchant d'un poste ou en changeant de
// filtre. Recreer les cases a chaque ramassage serait du gaspillage pur,
// d'ou cette comparaison prealable.
bool bSameList = RecipeSlots.Num() == NewRecipes.Num();
if (bSameList)
{
for (int32 Index = 0; Index < RecipeSlots.Num(); ++Index)
{
if (!RecipeSlots[Index] || RecipeSlots[Index]->GetRecipe() != NewRecipes[Index])
{
bSameList = false;
break;
}
}
}
if (bSameList)
{
return;
}
for (UCraftingRecipeSlotWidget* SlotWidget : RecipeSlots)
{
if (SlotWidget)
{
SlotWidget->RemoveFromParent();
}
}
RecipeSlots.Reset();
if (NewRecipes.Num() == 0)
{
return;
}
if (!RecipeSlotClass)
{
UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget : RecipeSlotClass n'est pas assignee, la grille restera vide."));
return;
}
const int32 Columns = FMath::Max(1, ColumnCount);
RecipeSlots.Reserve(NewRecipes.Num());
for (int32 Index = 0; Index < NewRecipes.Num(); ++Index)
{
UCraftingRecipeSlotWidget* NewSlot = CreateWidget<UCraftingRecipeSlotWidget>(this, RecipeSlotClass);
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("UCraftingWidget : impossible de creer une case a partir de %s. Recompile ce Widget Blueprint."),
*GetNameSafe(RecipeSlotClass));
break;
}
NewSlot->InitSlot(NewRecipes[Index], this);
RecipeGrid->AddChildToUniformGrid(NewSlot, Index / Columns, Index % Columns);
RecipeSlots.Add(NewSlot);
}
}
void UCraftingWidget::RefreshStates()
{
UCraftingComponent* Crafting = BoundCrafting.Get();
for (UCraftingRecipeSlotWidget* SlotWidget : RecipeSlots)
{
if (!SlotWidget)
{
continue;
}
ECraftFailureReason Reason = ECraftFailureReason::None;
const bool bCraftable = Crafting && Crafting->CanCraft(SlotWidget->GetRecipe(), Reason);
SlotWidget->SetCraftable(bCraftable);
SlotWidget->SetSelected(SlotWidget->GetRecipe() == SelectedRecipe);
}
RefreshDetails();
}
void UCraftingWidget::RefreshDetails()
{
UCraftingComponent* Crafting = BoundCrafting.Get();
if (DetailsPanel)
{
DetailsPanel->SetVisibility(SelectedRecipe ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
}
if (!SelectedRecipe)
{
if (CraftButton)
{
CraftButton->SetIsEnabled(false);
}
// Le panneau est masque, mais on vide quand meme : le contenu de
// l'ancienne recette reapparaitrait un instant a la selection suivante.
RefreshIngredients();
return;
}
if (ResultIcon)
{
if (UTexture2D* Icon = SelectedRecipe->GetIcon())
{
ResultIcon->SetBrushFromTexture(Icon);
ResultIcon->SetVisibility(ESlateVisibility::HitTestInvisible);
}
else
{
ResultIcon->SetVisibility(ESlateVisibility::Hidden);
}
}
if (ResultNameText)
{
ResultNameText->SetText(SelectedRecipe->GetDisplayName());
}
if (ResultQuantityText)
{
const bool bMultiple = SelectedRecipe->ResultQuantity > 1;
ResultQuantityText->SetVisibility(bMultiple ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed);
if (bMultiple)
{
ResultQuantityText->SetText(FText::AsNumber(SelectedRecipe->ResultQuantity));
}
}
if (ResultDescriptionText)
{
ResultDescriptionText->SetText(SelectedRecipe->ResultItem ? SelectedRecipe->ResultItem->Description : FText::GetEmpty());
}
RefreshIngredients();
if (CraftButton)
{
ECraftFailureReason Reason = ECraftFailureReason::None;
CraftButton->SetIsEnabled(Crafting && Crafting->CanCraft(SelectedRecipe, Reason));
}
}
void UCraftingWidget::RefreshIngredients()
{
if (!IngredientList)
{
return;
}
TArray<FCraftIngredientStatus> Statuses;
if (SelectedRecipe && BoundCrafting.IsValid())
{
BoundCrafting->GatherIngredientStatus(SelectedRecipe, Statuses);
}
// Trop de lignes : on retire le surplus par la fin.
while (IngredientRows.Num() > Statuses.Num())
{
const int32 Last = IngredientRows.Num() - 1;
if (IngredientRows[Last])
{
IngredientRows[Last]->RemoveFromParent();
}
IngredientRows.RemoveAt(Last);
}
if (IngredientRows.Num() < Statuses.Num() && !IngredientRowClass)
{
UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget : IngredientRowClass n'est pas assignee, la liste de cout restera vide."));
}
// Pas assez : on en cree.
while (IngredientRows.Num() < Statuses.Num() && IngredientRowClass)
{
UCraftingIngredientRowWidget* NewRow = CreateWidget<UCraftingIngredientRowWidget>(this, IngredientRowClass);
if (!NewRow)
{
UE_LOG(LogTemp, Error, TEXT("UCraftingWidget : impossible de creer une ligne a partir de %s. Recompile ce Widget Blueprint."),
*GetNameSafe(IngredientRowClass));
break;
}
IngredientList->AddChild(NewRow);
IngredientRows.Add(NewRow);
}
for (int32 Index = 0; Index < IngredientRows.Num(); ++Index)
{
if (IngredientRows[Index] && Statuses.IsValidIndex(Index))
{
IngredientRows[Index]->SetIngredient(Statuses[Index]);
}
}
}
void UCraftingWidget::NotifyRecipeClicked(UCraftingRecipeDataAsset* InRecipe)
{
if (SelectedRecipe == InRecipe)
{
return;
}
SelectedRecipe = InRecipe;
RefreshStates();
}
void UCraftingWidget::HandleCraftClicked()
{
if (!SelectedRecipe || !BoundCrafting.IsValid())
{
return;
}
// Pas de re-verification ici : Craft() refait CanCraft de toute facon, et
// dupliquer la condition finirait par la faire diverger.
BoundCrafting->Craft(SelectedRecipe);
// L'inventaire diffuse son changement, donc RefreshStates est deja passe.
// Rien a rappeler.
}
+116 -35
View File
@@ -4,6 +4,7 @@
#include "FpsPlayer.h"
#include "Camera/CameraComponent.h"
#include "Camera/CameraTypes.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
@@ -12,6 +13,7 @@
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "InputActionValue.h"
#include "CraftingComponent.h"
#include "InteractionComponent.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
@@ -19,6 +21,32 @@
#include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"
float FCameraSpring::Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset)
{
// L'impulsion n'est pas versee d'un coup : la vitesse passerait de 0 a
// plusieurs centaines d'unites par seconde en une frame, et cette
// discontinuite se lit comme un a-coup. On l'etale sur RampTime.
if (!FMath::IsNearlyZero(PendingImpulse))
{
const float ReleaseRatio = (RampTime > KINDA_SMALL_NUMBER)
? FMath::Clamp(Step / RampTime, 0.f, 1.f)
: 1.f;
const float Released = PendingImpulse * ReleaseRatio;
Velocity += Released;
PendingImpulse -= Released;
}
// Ressort amorti classique : acceleration = -k*x - c*v.
// On exprime k et c a partir d'une frequence et d'un taux d'amortissement,
// beaucoup plus parlants a regler qu'une raideur brute.
const float Omega = 2.f * PI * Frequency;
const float Acceleration = -(Omega * Omega * Offset) - (2.f * Damping * Omega * Velocity);
Velocity += Acceleration * Step;
Offset = FMath::Clamp(Offset + Velocity * Step, -MaxOffset, MaxOffset);
return Offset;
}
// Sets default values
AFpsPlayer::AFpsPlayer()
{
@@ -47,6 +75,7 @@ AFpsPlayer::AFpsPlayer()
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
SurvivalStats = CreateDefaultSubobject<USurvivalStatsComponent>(TEXT("SurvivalStats"));
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
@@ -73,6 +102,10 @@ void AFpsPlayer::BeginPlay()
SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath);
// Sans cet abonnement, un objet fabrique alors que l'inventaire est plein
// serait purement et simplement perdu.
CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow);
// Les reglages d'abord : le pawn est recree a chaque respawn et repart des
// valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre
// ensuite les changements faits en cours de partie depuis le menu de pause.
@@ -184,28 +217,14 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
}
// ==================================================================
// 2. Ressort d'impact (saut, atterrissage, et tout AddCameraPunch)
// 2. Ressorts : impact (saut, atterrissage) et bascule de tete
// ==================================================================
// L'impulsion n'est pas versee d'un coup : la vitesse de la camera passerait
// de 0 a plusieurs centaines de cm/s en une frame, et cette discontinuite se
// lit comme un a-coup. On l'etale sur PunchImpulseRampTime.
if (!FMath::IsNearlyZero(PendingPunch))
{
const float ReleaseRatio = (PunchImpulseRampTime > KINDA_SMALL_NUMBER)
? FMath::Clamp(Step / PunchImpulseRampTime, 0.f, 1.f)
: 1.f;
const float Released = PendingPunch * ReleaseRatio;
PunchVelocity += Released;
PendingPunch -= Released;
}
PunchSpring.Update(Step, PunchFrequency, PunchDamping, PunchImpulseRampTime, PunchMaxOffset);
// Ressort amorti classique : acceleration = -k*x - c*v.
// On exprime k et c a partir d'une frequence et d'un taux d'amortissement,
// beaucoup plus parlants a regler qu'une raideur brute.
const float Omega = 2.f * PI * PunchFrequency;
const float Acceleration = -(Omega * Omega * PunchOffset) - (2.f * PunchDamping * Omega * PunchVelocity);
PunchVelocity += Acceleration * Step;
PunchOffset = FMath::Clamp(PunchOffset + PunchVelocity * Step, -PunchMaxOffset, PunchMaxOffset);
// Le garde-fou est en unites normalisees, ou 1 = le pic d'une bascule
// complete. A 1.5 on laisse passer un leger cumul quand deux impulsions se
// suivent de tres pres, sans jamais laisser la tete partir.
CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f);
// ==================================================================
// 3. FOV de sprint
@@ -216,8 +235,15 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
const float TargetFOV = BaseFOV + ((bIsSprinting && bMovingFastEnough) ? SprintFOVOffset : 0.f);
FirstPersonCamera->SetFieldOfView(FMath::FInterpTo(FirstPersonCamera->FieldOfView, TargetFOV, Step, FOVInterpSpeed));
// L'application est laissee a ApplyCameraTransform : CalcCamera la rappelle
// juste avant que la vue soit lue, avec l'etat le plus frais de la capsule.
ApplyCameraTransform();
}
void AFpsPlayer::ApplyCameraTransform()
{
// ==================================================================
// 4. Application : position
// Position
// ==================================================================
// SmoothStep donne une courbe en S : demarrage doux, acceleration, arrivee douce.
const float Smoothed = FMath::SmoothStep(0.f, 1.f, CrouchAlpha);
@@ -227,22 +253,25 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
// d'un coup pour garder les pieds au sol. La camera, attachee a la capsule,
// suivait ce saut instantane -- c'est ce qui rendait le crouch "snap".
// En raisonnant en hauteur d'oeil AU-DESSUS DES PIEDS et en soustrayant la
// demi-hauteur courante de la capsule, ce saut est annule exactement.
// demi-hauteur courante de la capsule, ce saut est annule exactement --
// a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera.
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchOffset));
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset));
// ==================================================================
// 5. Application : rotation
// Rotation
// ==================================================================
// sin(alpha * PI) vaut 0 aux deux extremites et 1 au milieu : le tilt monte
// puis redescend tout seul pendant la transition, et est nul au repos.
// CrouchTiltDirection inverse le sens selon qu'on descend ou qu'on remonte.
const float CrouchTiltShape = FMath::Sin(CrouchAlpha * PI) * CrouchTiltDirection;
// La bascule ne depend plus de l'avancement de la transition : c'est l'etat
// d'un ressort, donc elle reste continue meme si le joueur change d'avis en
// plein mouvement. L'ancienne forme sin(alpha*PI) multipliee par un signe
// sautait de +CrouchTiltPitch a -CrouchTiltPitch en une frame des qu'on
// s'accroupissait et se relevait coup sur coup.
const float CrouchTilt = CrouchTiltSpring.Offset;
FRotator ViewOffset(-CrouchTiltPitch * CrouchTiltShape, 0.f, CrouchTiltRoll * CrouchTiltShape);
FRotator ViewOffset(-CrouchTiltPitch * CrouchTilt, 0.f, CrouchTiltRoll * CrouchTilt);
// Le regard suit le deplacement vertical de la camera : elle descend, on
// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
ViewOffset.Pitch += PunchOffset * PunchPitchPerCm;
ViewOffset.Pitch += PunchSpring.Offset * PunchPitchPerCm;
if (const AController* OwningController = GetController())
{
@@ -250,6 +279,23 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
}
}
void AFpsPlayer::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult)
{
ApplyCameraTransform();
Super::CalcCamera(DeltaTime, OutResult);
}
void AFpsPlayer::AddCrouchTilt(float Direction)
{
// Impulsion normalisee pour qu'un ressort critique culmine exactement a 1 :
// x(t) = v0 * t * e^(-w*t) atteint son maximum v0/(e*w) en t = 1/w, donc
// v0 = e*w. CrouchTiltPitch et CrouchTiltRoll restent ainsi des degres au
// pic, et non un reglage a l'aveugle.
const float Omega = 2.f * PI * CrouchTiltFrequency;
CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega);
}
void AFpsPlayer::AddCameraPunch(float Strength)
{
// Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui
@@ -257,7 +303,7 @@ void AFpsPlayer::AddCameraPunch(float Strength)
// reculs d'arme a venir. A 0, la camera ne bouge plus du tout.
// Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects
// qui l'injecte progressivement dans le ressort.
PendingPunch += Strength * CameraShakeMultiplier;
PunchSpring.AddImpulse(Strength * CameraShakeMultiplier);
}
void AFpsPlayer::Landed(const FHitResult& Hit)
@@ -529,6 +575,37 @@ bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity)
return true;
}
void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
{
if (!Item || Quantity <= 0)
{
return;
}
if (!DroppedPickupClass)
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."),
Quantity, *GetNameSafe(Item));
return;
}
FInventorySlot Overflow;
Overflow.Item = Item;
Overflow.Quantity = Quantity;
Overflow.RemainingUses = RemainingUses;
// Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le
// nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser
// une cloison qu'on ne regarde meme pas.
const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f;
if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation)))
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."),
Quantity, *GetNameSafe(Item));
}
}
void AFpsPlayer::DropAllItems()
{
if (!DroppedPickupClass)
@@ -597,8 +674,12 @@ void AFpsPlayer::ClearTransientInputStates()
void AFpsPlayer::StartCrouch()
{
// En mode bascule, la meme touche releve le personnage.
if (bCrouchToggleMode && bIsCrouched)
// En mode bascule, la meme touche releve le personnage. On teste l'INTENTION
// (bWantsToCrouch) et non l'etat applique (bIsCrouched) : le CharacterMovement
// ne redimensionne la capsule qu'a son propre tick, donc deux appuis dans la
// meme frame verraient tous les deux bIsCrouched a false et le personnage
// resterait accroupi. Symptome typique quand on martele la touche.
if (bCrouchToggleMode && GetCharacterMovement()->bWantsToCrouch)
{
StopCrouch();
return;
@@ -635,7 +716,7 @@ void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdj
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
CrouchTargetAlpha = 1.f;
CrouchTiltDirection = 1.f;
AddCrouchTilt(1.f);
}
void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
@@ -643,5 +724,5 @@ void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjus
Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
CrouchTargetAlpha = 0.f;
CrouchTiltDirection = -1.f;
AddCrouchTilt(-1.f);
}
@@ -14,7 +14,7 @@
#include "InputCoreTypes.h"
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "InventoryWidget.h"
#include "InventoryScreenWidget.h"
#include "Kismet/GameplayStatics.h"
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
@@ -104,7 +104,7 @@ void AFpsPlayerController::OnPossess(APawn* InPawn)
if (Crosshair) { /* pas d'abonnement */ }
if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); }
if (Hotbar) { Hotbar->BindToOwningPawn(); }
if (InventoryWidget) { InventoryWidget->BindToOwningPawn(); }
if (InventoryScreen) { InventoryScreen->BindToOwningPawn(); }
if (SurvivalStatsWidget) { SurvivalStatsWidget->BindToOwningPawn(); }
if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(InPawn))
@@ -173,6 +173,11 @@ void AFpsPlayerController::SetupInputComponent()
Input->BindAction(ToggleInventoryAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleInventory);
}
if (ToggleCraftingAction)
{
Input->BindAction(ToggleCraftingAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleCrafting);
}
if (DropItemAction)
{
Input->BindAction(DropItemAction, ETriggerEvent::Started, this, &AFpsPlayerController::DropHoveredItem);
@@ -236,14 +241,39 @@ void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value)
}
}
void AFpsPlayerController::DropHoveredItem()
void AFpsPlayerController::HandleInventoryTabChanged(bool bInventoryTabActive)
{
if (!bInventoryOpen || !InventoryWidget)
bCraftingTabActive = !bInventoryTabActive;
UpdateHotbarVisibility();
}
void AFpsPlayerController::UpdateHotbarVisibility()
{
if (!Hotbar)
{
return;
}
const int32 SlotIndex = InventoryWidget->GetHoveredSlotIndex();
// La barre rapide reste affichee sur l'onglet Inventaire : elle en fait
// partie, on y glisse des objets. Elle n'a en revanche aucun sens sous la
// page de craft, ou elle ne ferait qu'encombrer.
// Surtout pas "bHidden" : AActor en a deja un, le masquer donne une erreur
// de compilation (C4458) plutot qu'un simple avertissement.
const bool bHideHotbar = bInventoryOpen && bCraftingTabActive;
Hotbar->SetVisibility(bHideHotbar ? ESlateVisibility::Collapsed : HotbarDefaultVisibility);
}
void AFpsPlayerController::DropHoveredItem()
{
if (!bInventoryOpen || !InventoryScreen)
{
return;
}
// 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)
{
return;
@@ -316,27 +346,35 @@ void AFpsPlayerController::CreateHudWidgets()
Hotbar = CreateWidget<UHotbarWidget>(this, HotbarClass);
if (Hotbar)
{
// Relevee avant tout masquage : c'est la valeur a laquelle on
// reviendra en quittant l'onglet Fabrication.
HotbarDefaultVisibility = Hotbar->GetVisibility();
Hotbar->AddToViewport(ZOrderHotbar);
}
}
if (!InventoryWidget)
if (!InventoryScreen)
{
if (InventoryClass)
if (InventoryScreenClass)
{
InventoryWidget = CreateWidget<UInventoryWidget>(this, InventoryClass);
if (InventoryWidget)
InventoryScreen = CreateWidget<UInventoryScreenWidget>(this, InventoryScreenClass);
if (InventoryScreen)
{
InventoryWidget->AddToViewport(ZOrderInventory);
// Cree des le depart pour que son abonnement au delegue soit
// actif, mais cache : ainsi il reste a jour meme ferme, et
// l'ouverture est instantanee.
InventoryWidget->SetVisibility(ESlateVisibility::Collapsed);
// Abonnement AVANT l'ajout au viewport : c'est lui qui declenche
// NativeConstruct, donc la premiere diffusion d'onglet. S'abonner
// apres, on la manquerait.
InventoryScreen->OnTabChanged.AddDynamic(this, &AFpsPlayerController::HandleInventoryTabChanged);
InventoryScreen->AddToViewport(ZOrderInventory);
// Cree des le depart pour que les abonnements de ses deux pages
// soient actifs, mais cache : ainsi l'ecran reste a jour meme
// ferme, et l'ouverture est instantanee.
InventoryScreen->SetVisibility(ESlateVisibility::Collapsed);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryClass n'est pas assignee, l'inventaire ne s'ouvrira pas."));
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryScreenClass n'est pas assignee, l'inventaire ne s'ouvrira pas."));
}
}
@@ -362,14 +400,73 @@ void AFpsPlayerController::CreateHudWidgets()
void AFpsPlayerController::ToggleInventory()
{
if (!InventoryWidget)
// Tab veut dire "ouvre mon sac". Rouvrir sur l'onglet Fabrication parce
// qu'on l'avait laisse la serait une surprise a chaque fois.
SetInventoryScreenOpen(!bInventoryOpen, /*bShowCraftingTab=*/false);
}
void AFpsPlayerController::ToggleCrafting()
{
// Deja sur l'onglet Fabrication : la meme touche referme, comme Tab le fait
// pour l'inventaire. Depuis l'onglet Inventaire, elle bascule sans fermer --
// c'est le geste attendu quand on cherche quoi construire avec ce qu'on vient
// de ramasser.
if (bInventoryOpen && bCraftingTabActive)
{
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
return;
}
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}
void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab)
{
if (!InventoryScreen)
{
return;
}
bInventoryOpen = !bInventoryOpen;
// Ecran deja ouvert : on ne fait que changer d'onglet. Repasser par le mode
// d'input serait au mieux inutile, au pire nuisible -- SetIgnoreLookInput
// gere un compteur qu'un appel en trop desequilibrerait durablement.
if (bOpen && bInventoryOpen)
{
if (bShowCraftingTab)
{
InventoryScreen->ShowCraftingTab();
}
else
{
InventoryScreen->ShowInventoryTab();
}
return;
}
InventoryWidget->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
if (bOpen == bInventoryOpen)
{
return;
}
bInventoryOpen = bOpen;
if (bInventoryOpen)
{
if (bShowCraftingTab)
{
InventoryScreen->ShowCraftingTab();
}
else
{
InventoryScreen->ShowInventoryTab();
}
}
InventoryScreen->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
// Fermer l'ecran doit toujours rendre la barre, meme si on l'avait laissee
// sur l'onglet Fabrication.
UpdateHotbarVisibility();
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
@@ -693,10 +790,10 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
PauseMenu = nullptr;
}
if (InventoryWidget)
if (InventoryScreen)
{
InventoryWidget->RemoveFromParent();
InventoryWidget = nullptr;
InventoryScreen->RemoveFromParent();
InventoryScreen = nullptr;
}
if (InteractionPrompt)
@@ -0,0 +1,126 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "InventoryScreenWidget.h"
#include "Components/Button.h"
#include "Components/WidgetSwitcher.h"
#include "CraftingWidget.h"
#include "FpsPlayer.h"
#include "GameFramework/PlayerController.h"
#include "InventoryDragDropOperation.h"
#include "InventoryWidget.h"
void UInventoryScreenWidget::NativeConstruct()
{
Super::NativeConstruct();
if (InventoryTabButton)
{
InventoryTabButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleInventoryTabClicked);
}
if (CraftingTabButton)
{
CraftingTabButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleCraftingTabClicked);
}
ShowInventoryTab();
}
void UInventoryScreenWidget::BindToOwningPawn()
{
if (InventoryPage)
{
InventoryPage->BindToOwningPawn();
}
if (CraftingPage)
{
CraftingPage->BindToOwningPawn();
}
}
bool UInventoryScreenWidget::IsInventoryTabActive() const
{
return TabSwitcher && InventoryPage && TabSwitcher->GetActiveWidget() == InventoryPage;
}
int32 UInventoryScreenWidget::GetHoveredSlotIndex() const
{
if (!InventoryPage || !IsInventoryTabActive())
{
return INDEX_NONE;
}
return InventoryPage->GetHoveredSlotIndex();
}
void UInventoryScreenWidget::ShowInventoryTab()
{
ShowPage(InventoryPage);
}
void UInventoryScreenWidget::ShowCraftingTab()
{
ShowPage(CraftingPage);
}
void UInventoryScreenWidget::HandleInventoryTabClicked()
{
ShowPage(InventoryPage);
}
void UInventoryScreenWidget::HandleCraftingTabClicked()
{
ShowPage(CraftingPage);
}
void UInventoryScreenWidget::ShowPage(UWidget* Page)
{
if (!TabSwitcher || !Page)
{
return;
}
TabSwitcher->SetActiveWidget(Page);
// Le bouton de l'onglet courant est desactive : il sert d'indicateur de
// position autant que de garde-fou contre un clic sans effet.
const bool bInventoryActive = (Page == InventoryPage);
if (InventoryTabButton)
{
InventoryTabButton->SetIsEnabled(!bInventoryActive);
}
if (CraftingTabButton)
{
CraftingTabButton->SetIsEnabled(bInventoryActive);
}
OnTabChanged.Broadcast(bInventoryActive);
}
bool UInventoryScreenWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation);
// On n'arrive ici que si ni une case ni la page d'inventaire n'ont traite le
// depot : les enfants sont consultes en premier et arretent la propagation.
const UInventoryDragDropOperation* Payload = Cast<UInventoryDragDropOperation>(InOperation);
if (!Payload || Payload->SourceSlotIndex == INDEX_NONE)
{
return true;
}
if (const APlayerController* OwningController = GetOwningPlayer())
{
if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(OwningController->GetPawn()))
{
PlayerPawn->DropSlot(Payload->SourceSlotIndex, Payload->Quantity);
}
}
return true;
}
@@ -45,6 +45,7 @@ void USettingsControlsWidget::NativeConstruct()
SetupRow(InteractRow, TEXT("Interact"), LOCTEXT("Interagir", "Interagir"));
SetupRow(UseItemRow, TEXT("UseItem"), LOCTEXT("UtiliserObjet", "Utiliser l'objet"));
SetupRow(ToggleInventoryRow, TEXT("ToggleInventory"), LOCTEXT("Inventaire", "Inventaire"));
SetupRow(ToggleCraftingRow, TEXT("ToggleCrafting"), LOCTEXT("Fabrication", "Fabrication"));
SetupRow(DropItemRow, TEXT("DropItem"), LOCTEXT("Jeter", "Jeter"));
RefreshFromSettings();
@@ -84,11 +85,11 @@ TArray<USettingKeyRowWidget*> USettingsControlsWidget::GetAllRows() const
{
// L'ordre est celui de l'affichage : il sert aussi a la reinitialisation.
TArray<USettingKeyRowWidget*> Rows;
Rows.Reserve(11);
Rows.Reserve(12);
for (USettingKeyRowWidget* Row : { MoveForwardRow.Get(), MoveBackwardRow.Get(), MoveLeftRow.Get(),
MoveRightRow.Get(), JumpRow.Get(), SprintRow.Get(), CrouchRow.Get(), InteractRow.Get(),
UseItemRow.Get(), ToggleInventoryRow.Get(), DropItemRow.Get() })
UseItemRow.Get(), ToggleInventoryRow.Get(), ToggleCraftingRow.Get(), DropItemRow.Get() })
{
if (Row)
{