(Feat) Add Craft Systeme
This commit is contained in:
@@ -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.
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "CraftingRecipeDataAsset.h"
|
||||
#include "CraftingComponent.generated.h"
|
||||
|
||||
class UCraftingRecipeBook;
|
||||
class UInventoryComponent;
|
||||
class UItemDataAsset;
|
||||
|
||||
/**
|
||||
* Pourquoi une fabrication a echoue. Sert a l'interface, pour dire au joueur
|
||||
* ce qui manque au lieu de laisser un bouton grise sans explication.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class ECraftFailureReason : uint8
|
||||
{
|
||||
None,
|
||||
InvalidRecipe,
|
||||
NoRecipeBook,
|
||||
StationUnavailable,
|
||||
MissingIngredients
|
||||
};
|
||||
|
||||
/**
|
||||
* Etat d'un ingredient pour une recette donnee : ce qu'il faut, ce qu'on a.
|
||||
*
|
||||
* C'est le composant qui fait le rapprochement, pas le widget. Une ligne
|
||||
* d'ingredient n'a plus qu'a afficher deux nombres et choisir une couleur --
|
||||
* elle n'interroge jamais l'inventaire elle-meme.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FCraftIngredientStatus
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Crafting")
|
||||
TObjectPtr<UItemDataAsset> Item = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Crafting")
|
||||
int32 RequiredQuantity = 0;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Crafting")
|
||||
int32 OwnedQuantity = 0;
|
||||
|
||||
bool IsSatisfied() const { return OwnedQuantity >= RequiredQuantity; }
|
||||
};
|
||||
|
||||
/** Une fabrication vient d'aboutir. Pour le son, l'animation, une notification. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCraftSucceeded, UCraftingRecipeDataAsset*, Recipe);
|
||||
|
||||
/** Les postes accessibles ont change : l'interface doit revoir sa liste. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAvailableStationsChanged);
|
||||
|
||||
/**
|
||||
* Le resultat n'est pas entre dans l'inventaire. A quelqu'un de le poser
|
||||
* dans le monde -- voir le commentaire de Craft().
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCraftOverflow, UItemDataAsset*, Item, int32, Quantity, int32, RemainingUses);
|
||||
|
||||
/**
|
||||
* Fabrication d'objets a partir d'un livre de recettes et de l'inventaire du
|
||||
* meme acteur.
|
||||
*
|
||||
* Sur le pawn : fabriquer est une capacite du corps, au meme titre que porter
|
||||
* un sac. Le composant ne connait ni l'interface ni la classe du pawn -- un
|
||||
* futur etabli pourra porter le meme composant avec son propre livre.
|
||||
*/
|
||||
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||
class SURVIVAL_PROJET_API UCraftingComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UCraftingComponent();
|
||||
|
||||
virtual void InitializeComponent() override;
|
||||
|
||||
/**
|
||||
* Recettes a afficher, dans l'ordre du livre.
|
||||
*
|
||||
* Ne renvoie que celles dont le poste est ACCESSIBLE : les mains vides, on
|
||||
* ne voit pas les recettes d'etabli. C'est le modele choisi -- une seule
|
||||
* fenetre de craft dont le contenu depend d'ou l'on se trouve.
|
||||
*
|
||||
* @param StationFilter filtre d'affichage supplementaire, purement visuel.
|
||||
* Passe Count pour tout voir. Un poste demande ici mais non
|
||||
* accessible ne fait rien apparaitre : le filtre restreint, il
|
||||
* n'autorise jamais.
|
||||
*
|
||||
* Pas de valeur par defaut sur StationFilter : UHT refuse une entree
|
||||
* UMETA(Hidden) comme defaut, et dé-cacher Count le rendrait selectionnable
|
||||
* dans le champ Required Station d'une recette. L'appelant l'ecrit.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void GatherRecipes(TArray<UCraftingRecipeDataAsset*>& OutRecipes, ECraftingStation StationFilter) const;
|
||||
|
||||
/** Peut-on fabriquer cette recette maintenant, et sinon pourquoi. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
bool CanCraft(const UCraftingRecipeDataAsset* Recipe, ECraftFailureReason& OutReason) const;
|
||||
|
||||
/** Le detail requis / possede de chaque ingredient, dans l'ordre de la recette. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void GatherIngredientStatus(const UCraftingRecipeDataAsset* Recipe, TArray<FCraftIngredientStatus>& OutStatus) const;
|
||||
|
||||
/**
|
||||
* Consomme les ingredients et ajoute le resultat a l'inventaire.
|
||||
*
|
||||
* L'ordre compte : on consomme AVANT d'ajouter. Refuser une fabrication
|
||||
* parce que l'inventaire est plein serait faux -- consommer trois branches
|
||||
* libere justement la case ou poser le resultat. Et si malgre tout il ne
|
||||
* rentre pas, l'objet part dans OnCraftOverflow pour etre pose au sol :
|
||||
* aucune ressource ne disparait jamais, ni a l'entree ni a la sortie.
|
||||
*
|
||||
* @return true si la fabrication a eu lieu.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
bool Craft(UCraftingRecipeDataAsset* Recipe);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Postes accessibles
|
||||
//
|
||||
// Etat courant, pas de la configuration : {Hands} en permanence, plus
|
||||
// Workbench tant qu'on est devant un etabli. C'est une CONDITION de
|
||||
// fabrication, a ne pas confondre avec le filtre d'affichage de
|
||||
// GatherRecipes -- melanger les deux rendrait un jour un craft impossible
|
||||
// a cause d'un bouton d'interface.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Crafting|Stations")
|
||||
bool HasStation(ECraftingStation Station) const { return AvailableStations.Contains(Station); }
|
||||
|
||||
/** A appeler quand le joueur entre dans le rayon d'un poste. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting|Stations")
|
||||
void AddAvailableStation(ECraftingStation Station);
|
||||
|
||||
/** A appeler quand il s'en eloigne. Hands ne peut pas etre retire. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting|Stations")
|
||||
void RemoveAvailableStation(ECraftingStation Station);
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Crafting")
|
||||
FOnCraftSucceeded OnCraftSucceeded;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Crafting")
|
||||
FOnAvailableStationsChanged OnAvailableStationsChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Crafting")
|
||||
FOnCraftOverflow OnCraftOverflow;
|
||||
|
||||
protected:
|
||||
/** Ton DA_RecipeBook. Sans lui le composant ne connait aucune recette. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TObjectPtr<UCraftingRecipeBook> RecipeBook;
|
||||
|
||||
/** Resolu une fois a l'initialisation : chercher le composant a chaque clic serait absurde. */
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "Crafting")
|
||||
TObjectPtr<UInventoryComponent> Inventory;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Pas une UPROPERTY : un TSet de valeurs d'enum ne contient aucune
|
||||
* reference a collecter, et cet etat n'a rien a regler dans l'editeur.
|
||||
*/
|
||||
TSet<ECraftingStation> AvailableStations;
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CraftingComponent.h"
|
||||
#include "CraftingIngredientRowWidget.generated.h"
|
||||
|
||||
class UImage;
|
||||
class UTextBlock;
|
||||
|
||||
/**
|
||||
* Une ligne de cout dans le panneau de detail : l'icone d'un ingredient, son
|
||||
* nom, et "2/3" colore selon qu'on l'a ou non.
|
||||
*
|
||||
* Elle ne consulte jamais l'inventaire : UCraftingComponent lui remet un
|
||||
* FCraftIngredientStatus deja calcule. Une ligne qui irait compter elle-meme
|
||||
* referait le travail autant de fois qu'il y a d'ingredients, et pourrait
|
||||
* afficher autre chose que ce sur quoi le bouton Fabriquer s'appuie.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UCraftingIngredientRowWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Affiche un ingredient et applique la couleur qui va avec. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void SetIngredient(const FCraftIngredientStatus& Status);
|
||||
|
||||
protected:
|
||||
/** Facultatif : l'icone de la ressource demandee. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UImage> IngredientIcon;
|
||||
|
||||
/** Facultatif : le nom de la ressource. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UTextBlock> IngredientNameText;
|
||||
|
||||
/** Obligatoire : c'est lui qui porte le "2/3". A nommer "IngredientCountText". */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> IngredientCountText;
|
||||
|
||||
/** Couleur du compteur quand la ressource est en quantite suffisante. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
|
||||
FLinearColor EnoughColor = FLinearColor(0.35f, 0.85f, 0.35f, 1.f);
|
||||
|
||||
/** Couleur du compteur quand il en manque. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
|
||||
FLinearColor MissingColor = FLinearColor(0.9f, 0.3f, 0.3f, 1.f);
|
||||
|
||||
/**
|
||||
* Mise en forme du compteur. {0} = possede, {1} = requis.
|
||||
* Exposee pour pouvoir passer a "2 / 3" ou "2 sur 3" sans recompiler.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
|
||||
FText CountFormat = NSLOCTEXT("Crafting", "IngredientCountFormat", "{0}/{1}");
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "CraftingRecipeBook.generated.h"
|
||||
|
||||
class UCraftingRecipeDataAsset;
|
||||
|
||||
/**
|
||||
* La liste ordonnee des recettes du jeu.
|
||||
*
|
||||
* Un catalogue plutot qu'un scan de l'AssetManager : le contenu est
|
||||
* deterministe, rien ne se charge par surprise au runtime, et surtout l'ORDRE
|
||||
* d'affichage de la grille devient une donnee de design qu'on regle a la
|
||||
* souris. Un scan d'assets rendrait cet ordre dependant du nom des fichiers.
|
||||
*
|
||||
* Un catalogue plutot qu'une liste posee sur le composant, aussi : un etabli
|
||||
* devra pointer son propre livre, et le pawn n'a pas a etre l'endroit ou vit
|
||||
* la table de craft du jeu.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SURVIVAL_PROJET_API UCraftingRecipeBook : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** L'ordre de ce tableau est l'ordre d'affichage dans la grille de craft. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TArray<TObjectPtr<UCraftingRecipeDataAsset>> Recipes;
|
||||
|
||||
// Pas de UFUNCTION ici : UHT refuse un TObjectPtr en valeur de retour. Le
|
||||
// Blueprint lit de toute facon Recipes directement, c'est une UPROPERTY.
|
||||
const TArray<TObjectPtr<UCraftingRecipeDataAsset>>& GetRecipes() const { return Recipes; }
|
||||
|
||||
#if WITH_EDITOR
|
||||
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "CraftingRecipeDataAsset.generated.h"
|
||||
|
||||
class UItemDataAsset;
|
||||
class UTexture2D;
|
||||
|
||||
/**
|
||||
* Poste de travail necessaire pour realiser une recette.
|
||||
*
|
||||
* Ce n'est PAS un filtre d'affichage : c'est une condition de realisation.
|
||||
* Le UCraftingComponent tient la liste des stations dont le joueur dispose a
|
||||
* l'instant t -- {Hands} tout seul, {Hands, Workbench} devant un etabli -- et
|
||||
* une recette n'est realisable que si la sienne s'y trouve. Le filtre de
|
||||
* l'interface est une notion separee, purement visuelle : melanger les deux
|
||||
* finirait par rendre un craft impossible parce qu'un bouton d'affichage est
|
||||
* mal positionne, et ce genre de bug ne se comprend jamais sur le moment.
|
||||
*
|
||||
* Fourneau et feu de cuisson n'ont volontairement PAS leur entree ici : ils ne
|
||||
* passent par aucun menu. Ce sont des acteurs poses dans le monde, facon Raft,
|
||||
* ou l'on depose une ressource et ou l'on voit le mesh changer d'etat. Leur
|
||||
* donnee et leur file d'attente leur appartiendront, une recette de menu n'a
|
||||
* rien a y faire.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class ECraftingStation : uint8
|
||||
{
|
||||
Hands,
|
||||
Workbench,
|
||||
|
||||
// Sentinelle, jamais assignee a une recette. Elle sert a parcourir l'enum
|
||||
// pour construire les boutons de filtre sans avoir a les enumerer a la main.
|
||||
Count UMETA(Hidden)
|
||||
};
|
||||
|
||||
/** Une ligne de cout : tant d'exemplaires de tel objet. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FCraftIngredient
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
|
||||
TObjectPtr<UItemDataAsset> Item = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe", meta = (ClampMin = "1"))
|
||||
int32 Quantity = 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Une recette : ce qu'il faut, ce que ca donne, ou ca se fabrique.
|
||||
*
|
||||
* Un asset par recette, comme un DataAsset par objet. Trois comportements de
|
||||
* craft n'existeront jamais : la recette est de la donnee pure, c'est le
|
||||
* composant qui l'execute.
|
||||
*
|
||||
* Elle ne contient aucun etat -- ni "deja decouverte", ni "en cours de
|
||||
* fabrication". Le jour ou les recettes se debloqueront en jeu, la liste des
|
||||
* RecipeId connus vivra dans la sauvegarde, pas ici : sinon decouvrir une
|
||||
* recette la debloquerait pour toutes les parties a la fois.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SURVIVAL_PROJET_API UCraftingRecipeDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Identifiant stable, independant du nom de l'asset.
|
||||
* Servira a la sauvegarde des recettes decouvertes : renommer un asset ne
|
||||
* doit pas faire oublier au joueur ce qu'il avait debloque.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
|
||||
FName RecipeId;
|
||||
|
||||
/** Ce que la fabrication consomme. Une recette sans cout est acceptee (objet gratuit). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
|
||||
TArray<FCraftIngredient> Ingredients;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
|
||||
TObjectPtr<UItemDataAsset> ResultItem;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe", meta = (ClampMin = "1"))
|
||||
int32 ResultQuantity = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe")
|
||||
ECraftingStation RequiredStation = ECraftingStation::Hands;
|
||||
|
||||
// Pas de duree de fabrication ici : le craft de menu est instantane, comme
|
||||
// dans Valheim ou Raft. La fabrication qui prend du temps est celle des
|
||||
// postes qui travaillent SANS le joueur, et ceux-la ne sont pas des recettes.
|
||||
|
||||
/**
|
||||
* Nom affiche, quand celui du resultat ne suffit pas.
|
||||
* Laisse vide dans l'immense majorite des cas : dupliquer le nom de l'objet
|
||||
* dans la recette, c'est se garantir deux textes qui divergeront un jour.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe|Display")
|
||||
FText DisplayNameOverride;
|
||||
|
||||
/** Icone affichee dans la grille de craft. Vide = celle du resultat. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe|Display")
|
||||
TObjectPtr<UTexture2D> IconOverride;
|
||||
|
||||
/** Le nom a afficher, override compris. Passe toujours par ici, jamais par ResultItem directement. */
|
||||
UFUNCTION(BlueprintPure, Category = "Recipe")
|
||||
FText GetDisplayName() const;
|
||||
|
||||
/** L'icone a afficher, override compris. */
|
||||
UFUNCTION(BlueprintPure, Category = "Recipe")
|
||||
UTexture2D* GetIcon() const;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Une recette sans resultat ne planterait pas : elle ne ferait simplement
|
||||
* RIEN, en consommant les ingredients. C'est exactement le genre de bug qu'on
|
||||
* cherche pendant une heure -- autant que l'editeur le refuse a la sauvegarde.
|
||||
*/
|
||||
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CraftingRecipeSlotWidget.generated.h"
|
||||
|
||||
class UCraftingRecipeDataAsset;
|
||||
class UCraftingWidget;
|
||||
class UImage;
|
||||
class UTextBlock;
|
||||
|
||||
/**
|
||||
* Une case de la grille de craft : l'icone d'une recette, cliquable.
|
||||
*
|
||||
* Passive comme la case d'inventaire : elle n'interroge jamais le composant de
|
||||
* fabrication, c'est la page parente qui lui pousse son etat. Elle ne gere que
|
||||
* son propre clic, parce que c'est elle qui recoit l'evenement souris.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UCraftingRecipeSlotWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Appele une fois a la creation : dit a la case quelle recette elle represente. */
|
||||
void InitSlot(UCraftingRecipeDataAsset* InRecipe, UCraftingWidget* InOwningGrid);
|
||||
|
||||
/** Affiche ou masque le cadre de selection. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void SetSelected(bool bSelected);
|
||||
|
||||
/**
|
||||
* Assombrit la case quand les ressources manquent.
|
||||
* La recette reste visible et cliquable : le joueur doit pouvoir consulter
|
||||
* ce qui lui manque, sinon il ne sait pas quoi aller chercher.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void SetCraftable(bool bCraftable);
|
||||
|
||||
UCraftingRecipeDataAsset* GetRecipe() const { return Recipe; }
|
||||
|
||||
protected:
|
||||
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
|
||||
|
||||
/** A nommer exactement "RecipeIcon" dans le Widget Blueprint. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UImage> RecipeIcon;
|
||||
|
||||
/** Facultatif : le nom sous l'icone, si tu veux une grille legendee. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UTextBlock> RecipeNameText;
|
||||
|
||||
/** Facultatif : cadre affiche quand la case est selectionnee. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UWidget> SelectionBorder;
|
||||
|
||||
/** Teinte de l'icone quand la recette est realisable. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
|
||||
FLinearColor CraftableTint = FLinearColor::White;
|
||||
|
||||
/** Teinte quand il manque des ressources. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting")
|
||||
FLinearColor UncraftableTint = FLinearColor(0.3f, 0.3f, 0.3f, 1.f);
|
||||
|
||||
private:
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UCraftingRecipeDataAsset> Recipe;
|
||||
|
||||
/** La page parente, a prevenir du clic. */
|
||||
TWeakObjectPtr<UCraftingWidget> OwningGrid;
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "CraftingRecipeDataAsset.h"
|
||||
#include "CraftingWidget.generated.h"
|
||||
|
||||
class UButton;
|
||||
class UCraftingComponent;
|
||||
class UCraftingIngredientRowWidget;
|
||||
class UCraftingRecipeSlotWidget;
|
||||
class UImage;
|
||||
class UInventoryComponent;
|
||||
class UPanelWidget;
|
||||
class UTextBlock;
|
||||
class UUniformGridPanel;
|
||||
|
||||
/**
|
||||
* La page de fabrication : la grille de recettes a gauche, le detail de la
|
||||
* recette choisie a droite.
|
||||
*
|
||||
* Elle ne calcule rien. UCraftingComponent lui dit quelles recettes afficher,
|
||||
* lesquelles sont realisables, et fabrique quand on clique. Elle s'abonne a
|
||||
* l'inventaire et aux postes accessibles pour se rafraichir -- jamais de Tick
|
||||
* pour relire un etat.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UCraftingWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
|
||||
/**
|
||||
* (Re)branche la page sur le pawn actuellement possede.
|
||||
* Indispensable apres un respawn : le nouveau pawn a de nouveaux composants,
|
||||
* et les anciens abonnements pointeraient vers des objets detruits.
|
||||
*/
|
||||
void BindToOwningPawn();
|
||||
|
||||
/** Relit la liste, l'etat de chaque case et le panneau de detail. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void Refresh();
|
||||
|
||||
/** Appele par une case quand on clique dessus. */
|
||||
void NotifyRecipeClicked(UCraftingRecipeDataAsset* InRecipe);
|
||||
|
||||
/**
|
||||
* Restreint l'affichage a un seul poste. Count = tout ce qui est accessible.
|
||||
* Purement visuel : ce filtre ne peut jamais rendre une recette realisable.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting")
|
||||
void SetStationFilter(ECraftingStation InFilter);
|
||||
|
||||
protected:
|
||||
/** A nommer exactement "RecipeGrid" dans le Widget Blueprint. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UUniformGridPanel> RecipeGrid;
|
||||
|
||||
/** L'icone du resultat, dans le panneau de droite. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UImage> ResultIcon;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> ResultNameText;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> CraftButton;
|
||||
|
||||
/** Facultatif : la description de l'objet produit. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UTextBlock> ResultDescriptionText;
|
||||
|
||||
/** Facultatif : "x2" quand la recette produit plusieurs exemplaires. Masque sinon. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UTextBlock> ResultQuantityText;
|
||||
|
||||
/**
|
||||
* Facultatif : le panneau de droite dans son ensemble. Masque tant qu'aucune
|
||||
* recette n'est choisie, pour ne pas afficher un cadre vide au premier
|
||||
* affichage.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UWidget> DetailsPanel;
|
||||
|
||||
/** Facultatif : message affiche quand aucune recette n'est disponible ici. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UWidget> EmptyGridMessage;
|
||||
|
||||
/**
|
||||
* Le conteneur des lignes de cout. Declare en UPanelWidget et non en
|
||||
* UVerticalBox : ca laisse le choix d'un Wrap Box ou d'un Grid Panel dans le
|
||||
* Blueprint sans toucher au C++, puisqu'on ne fait qu'y ajouter des enfants.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UPanelWidget> IngredientList;
|
||||
|
||||
/** Ton WBP_RecipeSlot. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TSubclassOf<UCraftingRecipeSlotWidget> RecipeSlotClass;
|
||||
|
||||
/** Ton WBP_IngredientRow. Sans elle, la liste de cout reste vide. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TSubclassOf<UCraftingIngredientRowWidget> IngredientRowClass;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting", meta = (ClampMin = "1"))
|
||||
int32 ColumnCount = 5;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleInventoryChanged();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleStationsChanged();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCraftClicked();
|
||||
|
||||
/** Recree les cases uniquement si la liste affichee a change. */
|
||||
void RebuildGrid(const TArray<UCraftingRecipeDataAsset*>& NewRecipes);
|
||||
|
||||
/** Pousse l'etat realisable/non a chaque case et met a jour le panneau. */
|
||||
void RefreshStates();
|
||||
|
||||
void RefreshDetails();
|
||||
|
||||
/**
|
||||
* Remplit la liste de cout de la recette choisie.
|
||||
*
|
||||
* Les lignes sont RECYCLEES et non recreees : cette fonction repasse a
|
||||
* chaque changement d'inventaire, donc a chaque ramassage. Detruire puis
|
||||
* reconstruire trois widgets par branche ramassee serait du gaspillage.
|
||||
*/
|
||||
void RefreshIngredients();
|
||||
|
||||
UCraftingComponent* ResolveCrafting() const;
|
||||
|
||||
/** Detache les deux abonnements. Appele avant de rebrancher, et a la destruction. */
|
||||
void UnbindDelegates();
|
||||
|
||||
TWeakObjectPtr<UCraftingComponent> BoundCrafting;
|
||||
|
||||
TWeakObjectPtr<UInventoryComponent> BoundInventory;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UCraftingRecipeDataAsset> SelectedRecipe;
|
||||
|
||||
/** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UCraftingRecipeSlotWidget>> RecipeSlots;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UCraftingIngredientRowWidget>> IngredientRows;
|
||||
|
||||
ECraftingStation StationFilter = ECraftingStation::Count;
|
||||
};
|
||||
@@ -8,13 +8,57 @@
|
||||
|
||||
class APickupItem;
|
||||
class UCameraComponent;
|
||||
class UCraftingComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
class UInteractionComponent;
|
||||
class UInventoryComponent;
|
||||
class UItemDataAsset;
|
||||
class USurvivalStatsComponent;
|
||||
struct FInputActionValue;
|
||||
|
||||
/**
|
||||
* Ressort amorti à une dimension, partagé par les effets de caméra.
|
||||
*
|
||||
* Pourquoi un ressort plutôt qu'une courbe paramétrée par le temps : un ressort
|
||||
* n'a pas d'animation « en cours » qu'il faudrait annuler pour en jouer une
|
||||
* autre. Une nouvelle impulsion s'ajoute simplement à l'état courant, position
|
||||
* et vitesse restent continues, donc le mouvement est interruptible à n'importe
|
||||
* quel instant sans jamais produire de saut. C'est exactement ce que le crouch
|
||||
* demande quand le joueur martèle la touche.
|
||||
*/
|
||||
struct FCameraSpring
|
||||
{
|
||||
/** Déplacement courant, dans l'unité de ce que le ressort pilote. */
|
||||
float Offset = 0.f;
|
||||
|
||||
/** Vitesse courante, par seconde. */
|
||||
float Velocity = 0.f;
|
||||
|
||||
/** Impulsion pas encore versée, étalée sur RampTime pour éviter l'à-coup. */
|
||||
float PendingImpulse = 0.f;
|
||||
|
||||
void AddImpulse(float Strength) { PendingImpulse += Strength; }
|
||||
|
||||
/** Remet le ressort au repos, sans transition (respawn, téléportation). */
|
||||
void Reset()
|
||||
{
|
||||
Offset = 0.f;
|
||||
Velocity = 0.f;
|
||||
PendingImpulse = 0.f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intègre le ressort d'un pas de temps et renvoie le nouvel Offset.
|
||||
*
|
||||
* @param Frequency fréquence propre en Hz : vitesse de retour au repos.
|
||||
* @param Damping 1 = retour net sans rebond, en dessous ça oscille.
|
||||
* @param RampTime durée sur laquelle une impulsion est versée (0 = d'un coup).
|
||||
* @param MaxOffset garde-fou d'amplitude.
|
||||
*/
|
||||
float Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Personnage joueur en vue première personne.
|
||||
* Les assets d'input (Input Mapping Context + Input Actions) se branchent
|
||||
@@ -48,6 +92,9 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Inventaire")
|
||||
UInventoryComponent* GetInventoryComponent() const { return InventoryComponent; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Crafting")
|
||||
UCraftingComponent* GetCraftingComponent() const { return CraftingComponent; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Survie")
|
||||
USurvivalStatsComponent* GetSurvivalStats() const { return SurvivalStats; }
|
||||
|
||||
@@ -100,6 +147,20 @@ public:
|
||||
UFUNCTION()
|
||||
void ApplyUserSettings();
|
||||
|
||||
/**
|
||||
* Pose la transform de la camera juste avant que la vue soit lue.
|
||||
*
|
||||
* Le gestionnaire de camera passe par ici pour construire le point de vue :
|
||||
* c'est le seul endroit ou l'on est certain de lire la position de l'acteur
|
||||
* et la demi-hauteur de la capsule au MEME instant. Le Tick du pawn, lui,
|
||||
* n'a aucune dependance de tick vis-a-vis du CharacterMovement (verifie dans
|
||||
* Character.cpp : seul le Mesh en recoit une), donc la frame ou la capsule
|
||||
* change de taille pouvait etre calculee avec l'ancienne demi-hauteur --
|
||||
* soit un decalage de 28 cm sur une frame, a chaque accroupissement.
|
||||
* Bonus : le regard n'a plus une frame de retard sur la souris.
|
||||
*/
|
||||
virtual void CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult) override;
|
||||
|
||||
protected:
|
||||
// Called when the game starts or when spawned
|
||||
virtual void BeginPlay() override;
|
||||
@@ -133,6 +194,13 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire")
|
||||
TObjectPtr<UInventoryComponent> InventoryComponent;
|
||||
|
||||
/**
|
||||
* Fabrication. Sur le pawn comme l'inventaire : c'est ce corps qui fabrique,
|
||||
* et il lit l'inventaire du meme acteur.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TObjectPtr<UCraftingComponent> CraftingComponent;
|
||||
|
||||
/** Vie, faim, soif. Sur le pawn : c'est ce corps qui a faim. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Survie")
|
||||
TObjectPtr<USurvivalStatsComponent> SurvivalStats;
|
||||
@@ -259,17 +327,44 @@ protected:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.01"))
|
||||
float CrouchTransitionDuration = 0.4f;
|
||||
|
||||
/** Tangage (degrés) au pic de la transition. Positif = le regard plonge. */
|
||||
/**
|
||||
* Tangage (degrés) au pic de la bascule. Positif = le regard plonge.
|
||||
*
|
||||
* Le tilt n'est plus une courbe indexée sur l'avancement de la transition
|
||||
* mais un ressort relancé à chaque changement d'état : c'est ce qui permet
|
||||
* de s'accroupir et de se relever coup sur coup sans que la caméra saute.
|
||||
* L'impulsion est normalisée pour que le pic tombe bien sur cette valeur.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi")
|
||||
float CrouchTiltPitch = 3.f;
|
||||
|
||||
/**
|
||||
* Roulis (degrés) au pic de la transition : bascule sur le côté.
|
||||
* Roulis (degrés) au pic de la bascule : bascule sur le côté.
|
||||
* A 0 par defaut, le mouvement reste purement vertical.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi")
|
||||
float CrouchTiltRoll = 0.f;
|
||||
|
||||
/**
|
||||
* Fréquence du ressort de bascule (Hz). Le pic est atteint après environ
|
||||
* 1/(2*PI*f) secondes : 0.9 Hz place le sommet vers 0.18 s, soit à peu près
|
||||
* au milieu d'une transition de 0.4 s. Plus haut = plus sec et plus court.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.1"))
|
||||
float CrouchTiltFrequency = 0.9f;
|
||||
|
||||
/** Amortissement de la bascule. 1 = aucun rebond ; en dessous, la tête oscille. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.05", ClampMax = "2.0"))
|
||||
float CrouchTiltDamping = 1.f;
|
||||
|
||||
/**
|
||||
* Durée (s) sur laquelle l'impulsion de bascule est versée dans le ressort.
|
||||
* A 0 la rotation démarre à pleine vitesse angulaire en une frame, ce qui
|
||||
* se lit comme un claquement ; quelques centièmes suffisent à l'adoucir.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0"))
|
||||
float CrouchTiltRampTime = 0.05f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Réglages : impacts caméra (saut / atterrissage)
|
||||
// ------------------------------------------------------------------
|
||||
@@ -379,13 +474,34 @@ protected:
|
||||
void UseSelectedItem();
|
||||
|
||||
private:
|
||||
/** Fait avancer toutes les animations de caméra et les applique. */
|
||||
/** Fait avancer toutes les animations de caméra (intégration uniquement). */
|
||||
void UpdateCameraEffects(float DeltaTime);
|
||||
|
||||
/**
|
||||
* Recopie l'état des animations dans la transform de la caméra.
|
||||
* Séparée de l'intégration : appelée à la fois en fin de Tick et depuis
|
||||
* CalcCamera, elle doit rester idempotente et sans notion de temps.
|
||||
*/
|
||||
void ApplyCameraTransform();
|
||||
|
||||
/** Relance la bascule de tête. Direction : +1 on s'accroupit, -1 on se relève. */
|
||||
void AddCrouchTilt(float Direction);
|
||||
|
||||
/** Branche sur OnDied du composant de survie. */
|
||||
UFUNCTION()
|
||||
void HandleDeath();
|
||||
|
||||
/**
|
||||
* Le resultat d'une fabrication n'est pas entre dans l'inventaire : on le
|
||||
* pose aux pieds du joueur plutot que de le laisser disparaitre.
|
||||
*
|
||||
* C'est le pawn qui fait ca et non UCraftingComponent : le composant n'a pas
|
||||
* a savoir comment on engendre un objet dans le monde, et un etabli qui
|
||||
* porterait le meme composant deposerait son surplus sur sa propre table.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses);
|
||||
|
||||
/** Fait apparaitre un pickup deja configure a un endroit donne. */
|
||||
APickupItem* SpawnPickup(const struct FInventorySlot& Slot, const FVector& Location);
|
||||
|
||||
@@ -400,17 +516,15 @@ private:
|
||||
/** Valeur visée par CrouchAlpha. */
|
||||
float CrouchTargetAlpha = 0.f;
|
||||
|
||||
/** +1 quand on s'accroupit, -1 quand on se relève : inverse le sens du tilt. */
|
||||
float CrouchTiltDirection = 1.f;
|
||||
/** Déplacement vertical de la caméra (cm) : sauts, atterrissages, explosions. */
|
||||
FCameraSpring PunchSpring;
|
||||
|
||||
/** Décalage vertical courant de la caméra (cm), état du ressort d'impact. */
|
||||
float PunchOffset = 0.f;
|
||||
|
||||
/** Vitesse du ressort d'impact (cm/s). */
|
||||
float PunchVelocity = 0.f;
|
||||
|
||||
/** Impulsion restant à injecter dans le ressort, étalée sur PunchImpulseRampTime. */
|
||||
float PendingPunch = 0.f;
|
||||
/**
|
||||
* Bascule de tête pendant l'accroupissement, en unités normalisées :
|
||||
* son pic vaut 1, ce sont CrouchTiltPitch et CrouchTiltRoll qui le
|
||||
* convertissent en degrés.
|
||||
*/
|
||||
FCameraSpring CrouchTiltSpring;
|
||||
|
||||
/** Vitesse verticale mémorisée pendant la chute, pour calibrer l'atterrissage. */
|
||||
float LastFallSpeed = 0.f;
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
// ESlateVisibility n'est que declare en avant par le moteur : il faut le type
|
||||
// complet pour en stocker une valeur par defaut.
|
||||
#include "Components/SlateWrapperTypes.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "FpsPlayerController.generated.h"
|
||||
|
||||
@@ -11,7 +14,7 @@ class UHotbarWidget;
|
||||
class UInputAction;
|
||||
class UInventoryComponent;
|
||||
class UInteractionPromptWidget;
|
||||
class UInventoryWidget;
|
||||
class UInventoryScreenWidget;
|
||||
class UPauseMenuWidget;
|
||||
class UScreenFadeComponent;
|
||||
class USettingsMenuWidget;
|
||||
@@ -32,10 +35,17 @@ class SURVIVAL_PROJET_API AFpsPlayerController : public APlayerController
|
||||
public:
|
||||
AFpsPlayerController();
|
||||
|
||||
/** Ouvre ou ferme l'inventaire. */
|
||||
/** Ouvre ou ferme l'ecran, toujours sur l'onglet Inventaire. */
|
||||
UFUNCTION(BlueprintCallable, Category = "UI")
|
||||
void ToggleInventory();
|
||||
|
||||
/**
|
||||
* Ouvre l'ecran directement sur l'onglet Fabrication.
|
||||
* Depuis l'onglet Inventaire elle bascule, depuis l'onglet Fabrication elle referme.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "UI")
|
||||
void ToggleCrafting();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "UI")
|
||||
bool IsInventoryOpen() const { return bInventoryOpen; }
|
||||
|
||||
@@ -99,6 +109,14 @@ protected:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> ToggleInventoryAction;
|
||||
|
||||
/**
|
||||
* Ouverture directe sur l'onglet Fabrication, valeur Digital (bool).
|
||||
* Sur le controller comme ToggleInventoryAction : elle doit repondre alors
|
||||
* meme que l'input du pawn est coupe, ecran ouvert.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input")
|
||||
TObjectPtr<UInputAction> ToggleCraftingAction;
|
||||
|
||||
/**
|
||||
* Jette l'objet survole, valeur Digital (bool). Ctrl enfonce = un seul
|
||||
* exemplaire au lieu de la pile entiere.
|
||||
@@ -146,9 +164,13 @@ protected:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
|
||||
TSubclassOf<UInteractionPromptWidget> InteractionPromptClass;
|
||||
|
||||
/** Ton WBP_Inventory. */
|
||||
/**
|
||||
* Ton WBP_InventoryScreen : l'ecran a onglets qui contient la grille
|
||||
* d'inventaire ET la page de fabrication. Ce n'est plus WBP_Inventory
|
||||
* directement, celui-ci est devenu une page a l'interieur.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
|
||||
TSubclassOf<UInventoryWidget> InventoryClass;
|
||||
TSubclassOf<UInventoryScreenWidget> InventoryScreenClass;
|
||||
|
||||
/** Ton WBP_Hotbar, affiche en permanence. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
|
||||
@@ -201,7 +223,7 @@ protected:
|
||||
TObjectPtr<UInteractionPromptWidget> InteractionPrompt;
|
||||
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
|
||||
TObjectPtr<UInventoryWidget> InventoryWidget;
|
||||
TObjectPtr<UInventoryScreenWidget> InventoryScreen;
|
||||
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "UI")
|
||||
TObjectPtr<UHotbarWidget> Hotbar;
|
||||
@@ -246,6 +268,26 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleSettingsCloseRequested();
|
||||
|
||||
/**
|
||||
* Point d'entree unique de l'ouverture et de la fermeture de l'ecran.
|
||||
*
|
||||
* Tab et la touche de fabrication passent tous les deux par ici : le mode
|
||||
* d'input, la souris et la barre rapide sont ainsi decides a un seul endroit.
|
||||
* Deux chemins separes finiraient par diverger.
|
||||
*/
|
||||
void SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab);
|
||||
|
||||
/** L'ecran a change d'onglet : la barre rapide n'a rien a faire sous la page de craft. */
|
||||
UFUNCTION()
|
||||
void HandleInventoryTabChanged(bool bInventoryTabActive);
|
||||
|
||||
/**
|
||||
* Etat unique de la barre rapide : visible partout, sauf par-dessus l'onglet
|
||||
* Fabrication. Passer par une seule fonction evite les cas ou la fermeture
|
||||
* de l'ecran laisserait la barre cachee.
|
||||
*/
|
||||
void UpdateHotbarVisibility();
|
||||
|
||||
/** Jette la pile survolee dans la grille, si l'inventaire est ouvert. */
|
||||
void DropHoveredItem();
|
||||
|
||||
@@ -266,6 +308,17 @@ private:
|
||||
|
||||
bool bInventoryOpen = false;
|
||||
|
||||
/** Onglet Fabrication affiche. N'a de sens que si bInventoryOpen est vrai. */
|
||||
bool bCraftingTabActive = false;
|
||||
|
||||
/**
|
||||
* Visibilite d'origine de la barre rapide, relevee a sa creation.
|
||||
* On la restaure telle quelle plutot que de forcer Visible : le Blueprint a
|
||||
* peut-etre choisi SelfHitTestInvisible, et l'ecraser changerait
|
||||
* silencieusement son comportement au clic.
|
||||
*/
|
||||
ESlateVisibility HotbarDefaultVisibility = ESlateVisibility::Visible;
|
||||
|
||||
bool bPauseMenuOpen = false;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "InventoryScreenWidget.generated.h"
|
||||
|
||||
class UButton;
|
||||
class UCraftingWidget;
|
||||
class UInventoryWidget;
|
||||
class UWidgetSwitcher;
|
||||
|
||||
/**
|
||||
* L'onglet visible a change. true = Inventaire, false = Fabrication.
|
||||
*
|
||||
* L'ecran se contente d'annoncer. Ce que ca implique pour le reste du HUD --
|
||||
* masquer la barre rapide, par exemple -- ne le regarde pas : il serait alors
|
||||
* un widget reutilisable qui connait la pile de son hote, exactement ce qu'on
|
||||
* evite partout ailleurs.
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryScreenTabChanged, bool, bInventoryTabActive);
|
||||
|
||||
/**
|
||||
* L'ecran que Tab ouvre : une barre d'onglets et deux pages, Inventaire et
|
||||
* Fabrication.
|
||||
*
|
||||
* Il ne contient aucune logique de l'une ni de l'autre. Les pages restent des
|
||||
* widgets autonomes -- la grille d'inventaire n'a pas change d'une ligne en
|
||||
* devenant une page -- et l'ecran ne fait que les montrer, les rebrancher apres
|
||||
* un respawn, et relayer au controller ce dont il a besoin.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_InventoryScreen qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UInventoryScreenWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
/** Rebranche les deux pages sur le pawn possede. A appeler depuis OnPossess. */
|
||||
void BindToOwningPawn();
|
||||
|
||||
/**
|
||||
* Index de la case d'inventaire survolee, ou INDEX_NONE.
|
||||
*
|
||||
* Renvoie INDEX_NONE des que l'onglet Fabrication est affiche : sans ca, la
|
||||
* touche "jeter" agirait sur une case invisible, sous le panneau de craft.
|
||||
*/
|
||||
int32 GetHoveredSlotIndex() const;
|
||||
|
||||
/** Revient sur l'onglet Inventaire. Appele a chaque ouverture par Tab. */
|
||||
UFUNCTION(BlueprintCallable, Category = "UI")
|
||||
void ShowInventoryTab();
|
||||
|
||||
/** Bascule sur l'onglet Fabrication. Appele par la touche dediee. */
|
||||
UFUNCTION(BlueprintCallable, Category = "UI")
|
||||
void ShowCraftingTab();
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "UI")
|
||||
FOnInventoryScreenTabChanged OnTabChanged;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Absorbe ou traite les depots qui ne visent aucune page.
|
||||
* Meme regle que dans la grille : lache hors des cases, l'objet est jete.
|
||||
*/
|
||||
virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UWidgetSwitcher> TabSwitcher;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> InventoryTabButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> CraftingTabButton;
|
||||
|
||||
/**
|
||||
* Ton WBP_Inventory, pose comme enfant du WidgetSwitcher et renomme
|
||||
* "InventoryPage". Les deux pages sont obligatoires : un ecran a onglets
|
||||
* auquel il manque une page n'a aucun sens, autant que le Blueprint refuse
|
||||
* de compiler.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UInventoryWidget> InventoryPage;
|
||||
|
||||
/** Ton WBP_Crafting, renomme "CraftingPage". */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UCraftingWidget> CraftingPage;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleInventoryTabClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCraftingTabClicked();
|
||||
|
||||
/**
|
||||
* Affiche une page et grise son bouton d'onglet.
|
||||
*
|
||||
* On passe le widget et non un index en dur : l'ordre des enfants du
|
||||
* WidgetSwitcher se change d'un glisser dans la Designer, et un 0/1 code ici
|
||||
* inverserait silencieusement les onglets le jour ou ca arrive.
|
||||
*/
|
||||
void ShowPage(UWidget* Page);
|
||||
|
||||
bool IsInventoryTabActive() const;
|
||||
};
|
||||
@@ -68,6 +68,9 @@ protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> ToggleInventoryRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> ToggleCraftingRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> DropItemRow;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user