(Feat) Add Craft Systeme
This commit is contained in:
+59
-4
@@ -118,6 +118,8 @@ qu'une compilation propre l'est vraiment.
|
||||
| Inventaire | `InventoryComponent`, `InventoryWidget`, `InventorySlotWidget`, `HotbarWidget`, `InventoryDragDropOperation` |
|
||||
| Survie | `SurvivalStatsComponent`, `SurvivalStatsWidget` |
|
||||
| Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` |
|
||||
| Fabrication | `CraftingRecipeDataAsset`, `CraftingRecipeBook`, `CraftingComponent`, `CraftingWidget`, `CraftingRecipeSlotWidget`, `CraftingIngredientRowWidget` |
|
||||
| Écran à onglets (inventaire / craft) | `InventoryScreenWidget` |
|
||||
| Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` |
|
||||
| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` |
|
||||
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
|
||||
@@ -154,9 +156,55 @@ qu'une compilation propre l'est vraiment.
|
||||
`SetDialogZOrder()` : **le contrôleur reste seul maître de son empilement**, un widget
|
||||
réutilisable n'a pas à connaître la pile de son hôte. Une ligne = un index dans une liste de paliers, y compris les nombres :
|
||||
d'où un seul `WBP_SettingRow` pour tout le menu.
|
||||
- **Craft instantané**, jamais de barre de progression : c'est le choix de Valheim, Raft et
|
||||
Aloft. La fabrication qui prend du temps sera celle des postes qui travaillent **sans le
|
||||
joueur** (fourneau, séchoir) — un autre système, avec sa file d'attente, ses acteurs posés
|
||||
dans le monde et leur mesh qui change d'état. Ce ne sont donc **pas** des `ECraftingStation`.
|
||||
- **Une seule fenêtre de craft**, dont le contenu dépend d'où l'on se trouve. La recette déclare
|
||||
son `RequiredStation` ; `UCraftingComponent` tient la liste des postes **accessibles à
|
||||
l'instant t** (`{Hands}` seul, `{Hands, Workbench}` devant un établi). Par-dessus vient un
|
||||
filtre d'affichage **purement visuel** : les deux notions restent séparées, sinon un bouton
|
||||
d'interface finirait par rendre un craft impossible.
|
||||
- **`Craft()` consomme avant d'ajouter.** Refuser parce que l'inventaire est plein serait faux :
|
||||
consommer trois branches libère justement la case du résultat. Ce qui ne rentre malgré tout
|
||||
pas part par `OnCraftOverflow` et le **pawn** le pose au sol — le composant ne sait pas
|
||||
engendrer un acteur, et un établi déposerait son surplus sur sa propre table.
|
||||
- Le résultat passe par `AddItem()`, donc **exactement comme un ramassage** : piles entamées
|
||||
d'abord, puis barre rapide, puis grille.
|
||||
- **La barre rapide se masque sur l'onglet Fabrication.** `UInventoryScreenWidget` se contente
|
||||
de diffuser `OnTabChanged` ; c'est le contrôleur qui décide, via une unique
|
||||
`UpdateHotbarVisibility()`. Un seul point de décision, sinon fermer l'écran depuis l'onglet
|
||||
craft laisse la barre cachée pour le reste de la partie.
|
||||
|
||||
### Pièges UMG rencontrés sur l'écran de craft
|
||||
|
||||
- Un **Canvas Panel a une taille désirée de zéro** : imbriqué dans un conteneur en `Auto`, il
|
||||
disparaît. C'est ce qui arrive à `WBP_Inventory` devenu page d'un WidgetSwitcher.
|
||||
- Dans un Vertical/Horizontal Box, un enfant en **`Size = Fill` ne compte pas** dans la taille
|
||||
désirée du parent. Tous les enfants en Fill → le parent mesure zéro, et la Border qui
|
||||
l'entoure se réduit à son padding.
|
||||
- Un **Size Box impose une taille désirée, pas une taille allouée** : un parent qui l'aligne en
|
||||
`Fill` l'étire quand même. Il faut `Auto` + `Center`.
|
||||
- Une **Border ne peint rien avec `Draw As = Image` sans texture**. Pour un aplat de couleur,
|
||||
c'est **`Rounded Box`** (contour creux : Tint en alpha 0 + Outline Width).
|
||||
- **« Wrap With… » sur l'unique enfant d'une Border le fait sortir de la Border.** Vérifier
|
||||
l'indentation juste après, et re-glisser dans la hiérarchie.
|
||||
- Pour un cadre de taille fixe, le Size Box va **autour** de la Border. Dedans, c'est le contenu
|
||||
qui commande. Le plus externe gagne toujours.
|
||||
- `Scale Box` + `Stretch = Scale To Fit` est le seul « keep aspect ratio » d'UMG ; un Size Box
|
||||
avec `Min/Max Aspect Ratio` à 1.0 suffit pour des icônes carrées.
|
||||
- Le texte placeholder d'un Text Block (« Text Block ») fausse la mise en page dans la Designer.
|
||||
Y mettre une valeur représentative — le code l'écrase de toute façon à l'exécution.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Le code est en anglais, les commentaires en français.** Tout ce qui est du code s'écrit
|
||||
en anglais sans exception : noms de classes, de membres et de fonctions, valeurs d'enum,
|
||||
chaînes `Category = "..."`, `UMETA(DisplayName = ...)`, textes source des `FText` et
|
||||
`LOCTEXT`. Un `ECraftingStation::Hands`, pas `AlaMain`. Le français est réservé aux
|
||||
commentaires — un lecteur extérieur doit pouvoir lire le code sans traduire.
|
||||
*(Les fichiers antérieurs à cette règle ont encore des `Category` et des `FText` en
|
||||
français ; les mettre à jour au fur et à mesure qu'on les touche.)*
|
||||
- Commentaires **en français**. Ils expliquent le *pourquoi* et les pièges, jamais le *quoi*.
|
||||
- Nommage éditeur : `BP_` acteurs, `WBP_` widgets, `DA_` data assets, `IA_` input actions,
|
||||
`IMC_` mapping contexts, `GM_` game mode.
|
||||
@@ -167,9 +215,16 @@ qu'une compilation propre l'est vraiment.
|
||||
|
||||
## État actuel et suite
|
||||
|
||||
Boucle complète : récolter → ranger → consommer → survivre → mourir → réapparaître.
|
||||
Autour : menu principal, menu de pause, et un écran de réglages dont **seul l'onglet
|
||||
« Jeu & Contrôles » est rempli**.
|
||||
Boucle complète : récolter → **fabriquer** → ranger → consommer → survivre → mourir →
|
||||
réapparaître. Autour : menu principal, menu de pause, et un écran de réglages dont **seul
|
||||
l'onglet « Jeu & Contrôles » est rempli**.
|
||||
|
||||
Le craft est en place de bout en bout : `DA_RecipeBook`, onglets Inventaire / Fabrication,
|
||||
grille de recettes filtrée par poste accessible, panneau de détail avec compteurs colorés.
|
||||
Restent à faire quand le besoin viendra : l'**acteur établi** qui appellera
|
||||
`AddAvailableStation(Workbench)`, les **boutons de filtre** par poste (`SetStationFilter` existe
|
||||
déjà et `ECraftingStation::Count` sert de sentinelle « tout »), et les vraies recettes — celles
|
||||
en place sont deux `DA_Craft_Test_*` bâties sur `DA_Branch` et `DA_Rocher`.
|
||||
|
||||
Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout,
|
||||
prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix
|
||||
@@ -181,5 +236,5 @@ Prochaine étape gameplay recommandée : **spawner de ressources avec repousse**
|
||||
(`AResourceSpawner`, liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se
|
||||
vide en dix minutes.
|
||||
|
||||
Ensuite, par ordre de valeur : craft, objet visible en main, coffres, sauvegarde,
|
||||
Ensuite, par ordre de valeur : objet visible en main, établi, coffres, sauvegarde,
|
||||
cycle jour/nuit.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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