(Feat) Add Cooking and Workbench

This commit is contained in:
2026-08-01 00:03:53 +02:00
parent 3f3ae1f5fa
commit d50ef564ba
360 changed files with 5316 additions and 67 deletions
@@ -0,0 +1,482 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interactable.h"
#include "CookingStation.generated.h"
class UInventoryComponent;
class UItemDataAsset;
class UNiagaraComponent;
class UPointLightComponent;
class USoundBase;
class USphereComponent;
class UStaticMeshComponent;
/**
* Ce qu'un emplacement du grill porte a l'instant t.
*
* Uniquement de l'etat : le DataAsset dit ce que l'objet EST, cette struct dit
* ou il en est. C'est la meme separation que FInventorySlot, pour la meme raison.
*/
USTRUCT(BlueprintType)
struct FCookingSlotState
{
GENERATED_BODY()
/** L'objet pose, ou nullptr. Un seul exemplaire par emplacement, comme dans Raft. */
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UItemDataAsset> Item = nullptr;
/**
* Charges restantes de l'exemplaire pose. -1 = plein.
* L'usure traverse la cuisson comme elle traverse le sol : ce qu'on
* recupere est le meme exemplaire, pas un neuf.
*/
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
int32 RemainingUses = -1;
/**
* Compte a rebours de l'etape en cours. Pas une UPROPERTY : un FTimerHandle
* n'est qu'un index dans le gestionnaire de timers, il n'y a rien a
* collecter ni a serialiser.
*/
FTimerHandle Timer;
bool IsEmpty() const { return Item == nullptr; }
};
/**
* Une buche dans le foyer : QUEL objet a ete mis, et combien de temps il tient
* encore.
*
* C'est le coeur du modele : le carburant n'est pas une jauge de secondes mais
* une pile d'objets. Cinq buches, cinq objets -- une branche qui tient dix
* secondes occupe exactement la meme place qu'une planche qui en tient cent
* quatre-vingts. Le joueur voit donc ce qu'il a donne au feu, pas un
* pourcentage abstrait, et melanger les combustibles reste lisible.
*/
USTRUCT(BlueprintType)
struct FFuelSlotState
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UItemDataAsset> Item = nullptr;
/** Secondes de combustion restantes pour CE morceau. */
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
float RemainingSeconds = 0.f;
};
/** L'etat de la station a change : allumage, depot, transformation, retrait. */
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCookingStationChanged);
/**
* Ce que l'appui sur Interagir va declencher, compte tenu de ce que le joueur
* vise et de ce qu'il tient.
*
* Pas un UENUM : c'est un detail d'implementation, rien a l'exterieur n'a
* besoin de le nommer.
*/
enum class ECookingAction : uint8
{
None,
TakeItem,
PlaceItem,
AddFuel
};
/**
* Resultat complet de la decision. Calcule UNE fois, par ResolveAction(), et
* servi a la fois au texte du prompt et a l'execution : deux cascades de `if`
* separees finiraient par diverger, et le joueur lirait "Light the fire" en
* obtenant autre chose.
*
* Struct de pile a duree de vie tres courte, d'ou les pointeurs bruts : rien
* ici ne peut etre collecte entre le calcul et son usage.
*/
struct FCookingInteraction
{
ECookingAction Action = ECookingAction::None;
/** Emplacement de cuisson vise, ou INDEX_NONE si le joueur regarde le foyer. */
int32 SlotIndex = INDEX_NONE;
/** Objet actif de la barre rapide. */
UItemDataAsset* HeldItem = nullptr;
/** Slot d'inventaire d'ou sortira le bois, et l'objet correspondant. */
int32 FuelSourceSlot = INDEX_NONE;
UItemDataAsset* FuelItem = nullptr;
};
/**
* Poste de cuisson pose dans le monde : feu de camp, et plus tard fourneau ou
* sechoir en changeant simplement ses reglages.
*
* Modele Raft assume : AUCUNE interface. Le joueur tient sa viande crue dans la
* barre rapide, vise un emplacement du grill, appuie sur Interagir, et regarde
* le mesh changer.
*
* Le poste ne commente jamais son propre etat : le prompt n'affiche QUE des
* actions realisables, exactement comme un objet a ramasser. Ce qui cuit, ce
* qui manque et ce qui brule se lit sur les flammes, sur le nombre de buches
* et sur le mesh de la nourriture -- l'ecrire en plus reconstruirait une
* interface par-dessus un monde qui se suffit a lui-meme.
*
* Aucun Tick. Un timer par emplacement de cuisson, un seul pour le carburant :
* celui de la buche en train de bruler. Eteindre le feu met ces timers en
* pause, et la cuisson reprend exactement ou elle s'etait arretee sans qu'une
* ligne ne compte le temps a la main.
*
* Le feu ne s'allume PAS a la main : il part des qu'il a du bois et quelque
* chose a transformer, et s'arrete des qu'il n'a plus rien a faire. Le bois ne
* part donc jamais en fumee pour rien -- ce qui evite au joueur d'avoir a
* penser a eteindre derriere lui, une corvee qui n'apporte aucune decision
* interessante.
*/
UCLASS()
class SURVIVAL_PROJET_API ACookingStation : public AActor, public IInteractable
{
GENERATED_BODY()
public:
ACookingStation();
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
virtual void OnBeginFocus_Implementation() override;
virtual void OnEndFocus_Implementation() override;
/**
* Allume le feu s'il reste du bois dans le foyer.
*
* En temps normal, personne n'appelle ceci : UpdateFireState() decide seul.
* Reste exposee pour un futur allume-feu ou un evenement scenarise.
* @return false si le foyer est vide.
*/
UFUNCTION(BlueprintCallable, Category = "Cooking")
bool Light();
/** Eteint le feu. Les cuissons en cours se figent, elles ne repartent pas de zero. */
UFUNCTION(BlueprintCallable, Category = "Cooking")
void Extinguish();
/** Vrai si au moins un emplacement porte quelque chose qui a encore une etape a franchir. */
UFUNCTION(BlueprintPure, Category = "Cooking")
bool HasPendingWork() const;
UFUNCTION(BlueprintPure, Category = "Cooking")
bool IsLit() const { return bIsLit; }
/**
* Combien de morceaux de bois le foyer peut contenir.
*
* C'est le nombre de buches taguees dans le Blueprint : la capacite est une
* decision de mise en scene, et la regler a deux endroits garantirait qu'ils
* finissent par se contredire. FallbackFuelCapacity ne sert qu'aux postes
* sans buche visible, un four ferme par exemple.
*/
UFUNCTION(BlueprintPure, Category = "Cooking")
int32 GetFuelCapacity() const { return FuelIndicators.Num() > 0 ? FuelIndicators.Num() : FallbackFuelCapacity; }
/** Morceaux de bois actuellement dans le foyer. */
UFUNCTION(BlueprintPure, Category = "Cooking")
int32 GetFuelCount() const { return Fuel.Num(); }
/** Secondes de combustion cumulees sur tout ce qui est dans le foyer. */
UFUNCTION(BlueprintPure, Category = "Cooking")
float GetFuelRemaining() const;
/** Secondes avant la prochaine transformation, ou 0 si l'emplacement n'attend rien. */
UFUNCTION(BlueprintPure, Category = "Cooking")
float GetSlotTimeRemaining(int32 SlotIndex) const;
UFUNCTION(BlueprintPure, Category = "Cooking")
const TArray<FCookingSlotState>& GetSlots() const { return Slots; }
UPROPERTY(BlueprintAssignable, Category = "Cooking")
FOnCookingStationChanged OnStationChanged;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
/**
* Nombre d'emplacements de cuisson existant dans la classe. Les composants
* sont crees au constructeur, donc ce nombre est fige a la compilation --
* c'est le prix a payer pour pouvoir les positionner A LA SOURIS dans le
* viewport du Blueprint. Des composants engendres a l'execution ne seraient
* reglables qu'en tapant des coordonnees a l'aveugle.
*/
static constexpr int32 MaxCookingSlots = 4;
// ------------------------------------------------------------------
// Composants
// ------------------------------------------------------------------
/** Le foyer lui-meme : bois, pierres, structure. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UStaticMeshComponent> BaseMesh;
/**
* Ce que le trace d'interaction vient toucher. Sphere dediee et non le mesh,
* pour la meme raison que sur APickupItem : rien ne garantit qu'un mesh
* d'asset pack ait une collision.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<USphereComponent> InteractionSphere;
/**
* Un mesh par emplacement de cuisson, qui porte la nourriture posee. Sa
* transform sert a la fois a l'afficher et a savoir ce que le joueur vise :
* pas besoin d'une collision par emplacement, donc pas une requete physique
* de plus.
*
* Positionne-les dans le viewport du Blueprint. Ils sont caches au demarrage
* tant qu'ils sont vides, mais restent visibles dans l'editeur pour que tu
* puisses les placer.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TArray<TObjectPtr<UStaticMeshComponent>> SlotMeshes;
/** Flamme. Active tant que le feu brule, coupee des qu'il s'eteint. */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UNiagaraComponent> FireEffect;
/**
* Halo du foyer, allume en meme temps que la flamme.
*
* Regle intensite, couleur et rayon directement sur le composant dans le
* Blueprint : ce sont des proprietes natives, les redeclarer ici en
* doublon garantirait qu'un jour les deux se contredisent.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
TObjectPtr<UPointLightComponent> FireLight;
// ------------------------------------------------------------------
// Reglages
// ------------------------------------------------------------------
/** Emplacements de cuisson reellement utilises. Les autres restent caches. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "1", ClampMax = "4"))
int32 CookingSlotCount = 2;
/**
* Echelle appliquee au mesh de la nourriture posee.
* Les meshes d'objets sont dimensionnes pour etre poses au sol ; sur un
* grill ils sont souvent trop gros.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "0.01"))
float CookingMeshScale = 1.f;
/**
* Demi-angle du cone de visee autour de chaque emplacement, en degres.
*
* Au-dela, on considere que le joueur vise le foyer et pas un emplacement :
* c'est ce qui lui permet d'ajouter du bois ou d'allumer le feu sans devoir
* eviter le grill du regard.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "1", ClampMax = "89"))
float SlotAimHalfAngle = 25.f;
/** Multiplie la vitesse de cuisson. Un fourneau va plus vite que des braises. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "0.01"))
float CookSpeedMultiplier = 1.f;
/**
* Autorise a reprendre un objet qui n'a pas fini de cuire.
*
* Decoche par defaut : ce qu'on a mis a cuire est engage, on attend. Un
* objet qui a fini sa cuisson reste toujours recuperable, y compris pendant
* qu'il se dirige vers le brule -- sans quoi il serait impossible de sauver
* sa viande a temps, ce qui viderait la sur-cuisson de tout son sens.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bAllowTakingRawItems = false;
/**
* Ajoute les secondes restantes aux textes du prompt.
*
* Decoche en jeu : un chiffre qui s'egrene est de l'interface deguisee, et
* le joueur doit lire l'avancement sur le mesh, pas sur un compteur. Coche
* le temps d'un reglage pour verifier tes durees.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bShowRemainingTimeInPrompt = false;
/** Active le Custom Depth quand le poste est vise, comme les ramassables. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
bool bHighlightOnFocus = true;
// ------------------------------------------------------------------
// Reglages : carburant
// ------------------------------------------------------------------
/**
* Tag a poser sur les meshes de buches qui figurent le contenu du foyer.
* Leur NOMBRE fait la capacite du feu, et leur ordre alphabetique l'ordre
* d'apparition : la derniere posee est la premiere a bruler.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
FName FuelIndicatorTag = TEXT("FuelLog");
/** Capacite des postes qui n'ont aucune buche visible. Ignore des qu'il y en a une. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel", meta = (ClampMin = "1"))
int32 FallbackFuelCapacity = 5;
/**
* Chaque buche prend le mesh de l'objet qu'elle represente.
*
* Decoche par defaut : les buches d'un feu de camp sont posees a la main
* dans le Blueprint, avec leur orientation et leur imbrication soignees, et
* y substituer un mesh d'objet gacherait ce travail. Coche-le pour un
* support ou l'on veut distinguer d'un coup d'oeil branches et planches.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
bool bFuelIndicatorsUseItemMesh = false;
/** Bois deja dans le foyer au demarrage. Laisse vide pour un feu a alimenter. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
TObjectPtr<UItemDataAsset> StartingFuelItem;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel", meta = (ClampMin = "0"))
int32 StartingFuelCount = 0;
// ------------------------------------------------------------------
// Sons. Tous facultatifs : un champ vide ne joue rien.
// ------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
TObjectPtr<USoundBase> LightSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
TObjectPtr<USoundBase> ExtinguishSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
TObjectPtr<USoundBase> PlaceItemSound;
private:
/**
* Quel emplacement de cuisson le joueur vise, ou INDEX_NONE s'il regarde le
* foyer. Se base sur le point de vue reel du pawn, donc sur ce qu'il voit a
* l'ecran, et non sur la position de son corps.
*/
int32 ResolveAimedSlot(const AActor* Interactor) const;
/** Decide ce que ferait un appui sur Interagir, sans rien modifier. */
FCookingInteraction ResolveAction(AActor* Interactor) const;
/** Depose l'objet actif de la barre rapide sur l'emplacement de cuisson. */
bool PlaceItemFromHand(int32 SlotIndex, AActor* Interactor);
/** Rend l'objet a l'inventaire. Refuse et laisse tout en place si c'est plein. */
bool TakeItemToInventory(int32 SlotIndex, AActor* Interactor);
/**
* Met UN morceau de bois dans le foyer, pris dans un slot d'inventaire.
*
* Un exemplaire par appui, jamais la pile entiere : donner du bois est un
* geste irreversible, et vider vingt branches d'un coup sur un feu qui n'en
* demandait qu'une ne se rattrape pas. Le joueur dose lui-meme.
*/
bool AddFuelFrom(int32 InventorySlotIndex, AActor* Interactor);
/**
* Ou prendre du bois : la main d'abord, sinon le premier slot qui en
* contient. La main est prioritaire parce que c'est le seul moyen pour le
* joueur de CHOISIR ce qu'il brule.
*/
static int32 FindFuelSource(const UInventoryComponent* Inventory, UItemDataAsset*& OutItem);
/** Peut-on retirer ce qui se trouve sur cet emplacement de cuisson. */
bool CanTakeFromSlot(int32 SlotIndex) const;
/** Arme le compte a rebours de l'etape courante de l'emplacement. */
void StartSlotTimer(int32 SlotIndex);
/** Fin d'etape : l'objet devient son resultat, et on enchaine s'il y a une suite. */
void HandleSlotFinished(int32 SlotIndex);
/** Recopie le WorldMesh de l'objet pose, ou cache le composant si l'emplacement est vide. */
void RefreshSlotMesh(int32 SlotIndex);
/**
* Allume ou eteint selon qu'il y ait, ou non, du bois ET du travail.
*
* Point de decision UNIQUE : toutes les actions qui changent le contenu du
* foyer ou du grill finissent ici. Laisser chaque action decider elle-meme
* d'allumer aurait fini par oublier un cas -- typiquement le dernier objet
* retire, qui laisserait le feu bruler dans le vide.
*/
void UpdateFireState();
/** La buche du dessus a fini de bruler : on passe a la suivante. */
void HandleFuelConsumed();
/** Arme le timer sur la buche en cours de combustion. */
void StartFuelTimer();
/** Recopie le restant du timer dans la buche courante avant de le modifier. */
void SyncFuelFromTimer();
/**
* Allume ou coupe tout ce qui se voit quand le feu brule.
*
* Un seul endroit pour la flamme et la lumiere : le jour ou s'ajoute une
* boucle sonore ou un panache de fumee, il n'y aura toujours qu'un point a
* modifier au lieu de deux appels a retrouver dans Light() et Extinguish().
*/
void ApplyFireVisuals(bool bActive);
/** Retrouve les buches par leur tag, une fois pour toutes, au demarrage. */
void CacheFuelIndicators();
/**
* Montre exactement autant de buches qu'il y a de morceaux dans le foyer.
*
* Plus aucun calcul de palier ni de timer d'affichage depuis que le
* carburant est une pile d'objets : une buche visible EST un objet stocke.
*/
void RefreshFuelIndicators();
/**
* Ce que cet objet va devenir sur le feu, et en combien de temps.
* La cuisson passe avant la combustion : voir le commentaire de bCanBurn
* sur UItemDataAsset.
*/
static UItemDataAsset* GetNextStage(const UItemDataAsset* Item, float& OutDuration);
/** Inventaire du pawn qui interagit, ou nullptr. */
static UInventoryComponent* GetInventoryOf(AActor* Interactor);
/** Etat courant de chaque emplacement de cuisson. Toujours dimensionne a MaxCookingSlots. */
UPROPERTY(Transient)
TArray<FCookingSlotState> Slots;
/**
* Le contenu du foyer, dans l'ordre ou il a ete donne. L'index 0 est celui
* qui brule ; quand il est consomme, tout le monde avance d'un cran et la
* derniere buche disparait du foyer.
*/
UPROPERTY(Transient)
TArray<FFuelSlotState> Fuel;
FTimerHandle FuelTimer;
/**
* Buches de mise en scene, resolues au BeginPlay et triees par nom.
* Transient : ces pointeurs designent des composants du Blueprint, il n'y a
* rien a sauvegarder, seulement a garder du GC le temps de la partie.
*/
UPROPERTY(Transient)
TArray<TObjectPtr<UStaticMeshComponent>> FuelIndicators;
bool bIsLit = false;
};
@@ -131,9 +131,16 @@ public:
// ------------------------------------------------------------------
UFUNCTION(BlueprintPure, Category = "Crafting|Stations")
bool HasStation(ECraftingStation Station) const { return AvailableStations.Contains(Station); }
bool HasStation(ECraftingStation Station) const;
/** A appeler quand le joueur entre dans le rayon d'un poste. */
/**
* A appeler quand le joueur entre dans le rayon d'un poste.
*
* Les appels se COMPTENT : deux etablis dont les portees se chevauchent, ce
* qui arrive des qu'on construit une base un peu dense, s'ajoutent tous les
* deux. Sortir du premier ne doit pas retirer l'acces alors qu'on est encore
* devant le second.
*/
UFUNCTION(BlueprintCallable, Category = "Crafting|Stations")
void AddAvailableStation(ECraftingStation Station);
@@ -161,8 +168,11 @@ protected:
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.
* Combien de postes de chaque type sont a portee, et non un simple
* "present / absent" -- voir AddAvailableStation.
*
* Pas une UPROPERTY : ni la cle ni la valeur ne sont des references a
* collecter, et cet etat n'a rien a regler dans l'editeur.
*/
TSet<ECraftingStation> AvailableStations;
TMap<ECraftingStation, int32> StationRefCounts;
};
@@ -0,0 +1,65 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CraftingRecipeDataAsset.h"
#include "Interactable.h"
#include "CraftingStation.generated.h"
class UStaticMeshComponent;
/**
* Un poste de travail pose dans le monde : etabli, et ce qui suivra.
*
* Il ne fabrique rien lui-meme et ne contient aucune recette. Tout ce qu'il
* fait, c'est ouvrir l'ecran de fabrication en annoncant SON type de poste --
* le reste appartient au UCraftingComponent du joueur, qui filtrera les
* recettes en consequence.
*
* L'acces au poste ne dure que le temps de l'ecran : c'est le controller qui
* l'accorde a l'ouverture et le reprend a la fermeture. Un etabli qui donnerait
* son acces "par proximite" obligerait a surveiller la distance en permanence,
* pour un resultat que le joueur comprendrait moins bien -- ici on fabrique a
* l'etabli parce qu'on est en train de s'en servir.
*
* Un seul BP_CraftingStation suffit pour tous les postes : c'est StationType
* qui change par instance, comme ItemData sur BP_Pickup.
*/
UCLASS()
class SURVIVAL_PROJET_API ACraftingStation : public AActor, public IInteractable
{
GENERATED_BODY()
public:
ACraftingStation();
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
protected:
/**
* Le visuel ET la surface d'interaction.
*
* Pas de sphere dediee ici, contrairement a APickupItem : un poste est un
* meuble construit, dont le mesh a une vraie collision simple. Une sphere
* autour d'un volume pareil serait touchee AVANT lui -- le trace s'arrete au
* premier bloqueur -- et le prompt apparaitrait en visant le vide autour, ou
* masquerait un objet pose au pied du meuble.
*
* Corollaire : le mesh assigne doit avoir une collision simple. Sans elle,
* le trace le traverse, puisqu'il est lance en bTraceComplex = false.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Crafting")
TObjectPtr<UStaticMeshComponent> Mesh;
/** Le poste que cet acteur rend accessible pendant qu'on s'en sert. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
ECraftingStation StationType = ECraftingStation::Workbench;
/** Texte affiche quand le joueur vise le poste. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
FText InteractionPrompt;
};
@@ -4,12 +4,15 @@
#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.
// complet pour en stocker une valeur par defaut. Meme raison pour
// ECraftingStation, stocke en membre.
#include "Components/SlateWrapperTypes.h"
#include "CraftingRecipeDataAsset.h"
#include "GameFramework/PlayerController.h"
#include "FpsPlayerController.generated.h"
class UConfirmDialogWidget;
class UCraftingComponent;
class UHotbarWidget;
class UInputAction;
class UInventoryComponent;
@@ -46,6 +49,20 @@ public:
UFUNCTION(BlueprintCallable, Category = "UI")
void ToggleCrafting();
/**
* Ouvre l'ecran de fabrication depuis un poste de travail.
*
* L'acces au poste ne dure QUE le temps de l'ecran : il est accorde ici et
* repris a la fermeture. C'est le contrat de l'interaction -- on fabrique a
* l'etabli parce qu'on est en train de s'en servir, pas parce qu'on en a
* croise un un jour.
*
* Le contrôleur s'en charge et non l'etabli : c'est lui qui sait quand
* l'ecran se referme, et lui seul peut donc rendre le poste au bon moment.
*/
UFUNCTION(BlueprintCallable, Category = "UI")
void OpenCraftingAtStation(ECraftingStation Station);
UFUNCTION(BlueprintPure, Category = "UI")
bool IsInventoryOpen() const { return bInventoryOpen; }
@@ -277,6 +294,12 @@ private:
*/
void SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab);
/** Rend le poste accorde par le dernier OpenCraftingAtStation. Sans effet sinon. */
void ReleaseActiveStation();
/** Raccourci vers le composant de fabrication du pawn possede. */
UCraftingComponent* GetPawnCrafting() const;
/** L'ecran a change d'onglet : la barre rapide n'a rien a faire sous la page de craft. */
UFUNCTION()
void HandleInventoryTabChanged(bool bInventoryTabActive);
@@ -311,6 +334,12 @@ private:
/** Onglet Fabrication affiche. N'a de sens que si bInventoryOpen est vrai. */
bool bCraftingTabActive = false;
/**
* Poste accorde par l'ecran actuellement ouvert, a rendre a la fermeture.
* Count = aucun, l'ecran a ete ouvert au clavier et pas depuis un etabli.
*/
ECraftingStation ActiveStation = ECraftingStation::Count;
/**
* Visibilite d'origine de la barre rapide, relevee a sa creation.
* On la restaure telle quelle plutot que de forcer Visible : le Blueprint a
+8 -2
View File
@@ -40,10 +40,16 @@ public:
* Texte affiche au joueur quand il vise l'objet ("Ramasser", "Ouvrir"...).
* Renvoie du FText et non du FString : c'est le type localisable d'Unreal,
* indispensable si tu traduis le jeu un jour.
*
* Interactor est fourni parce que le texte depend souvent de CELUI qui vise
* et d'ou il vise : un feu de camp nomme l'emplacement precis qu'on regarde,
* un coffre verrouille annonce quelle cle il attend. Sans lui, l'objet
* devrait aller chercher le joueur local par un chemin detourne -- ce qui
* marche exactement jusqu'au jour ou il y a deux joueurs.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
FText GetInteractionPrompt() const;
virtual FText GetInteractionPrompt_Implementation() const { return FText::GetEmpty(); }
FText GetInteractionPrompt(AActor* Interactor) const;
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const { return FText::GetEmpty(); }
/**
* Permet de refuser l'interaction selon le contexte : porte verrouillee,
@@ -13,6 +13,14 @@
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInteractionFocusChanged, AActor*, NewFocus, AActor*, OldFocus);
/**
* Diffuse quand le TEXTE du prompt change, ce qui n'est pas la meme chose qu'un
* changement de cible : un feu de camp affiche un compte a rebours, et le fait
* de balayer d'un emplacement a l'autre ne change pas l'acteur vise. S'abonner
* uniquement a OnFocusChanged laisserait ces deux cas figes a l'ecran.
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractionPromptChanged, const FText&, NewPrompt);
/**
* A poser sur le pawn du joueur. Lance regulierement un trace depuis le point
* de vue de la camera, garde en memoire l'objet IInteractable vise, et declenche
@@ -40,14 +48,24 @@ public:
UFUNCTION(BlueprintPure, Category = "Interaction")
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
/** Texte a afficher a l'ecran, vide si rien n'est vise. */
/**
* Texte a afficher a l'ecran, vide si rien n'est vise.
*
* Renvoie une valeur DEJA calculee : le prompt est reconstruit une fois par
* trace, pas une fois par lecture. Un widget peut donc l'interroger sans
* jamais declencher le moindre travail.
*/
UFUNCTION(BlueprintPure, Category = "Interaction")
FText GetFocusedPrompt() const;
const FText& GetFocusedPrompt() const { return CachedPrompt; }
/** Branche ton widget la-dessus pour afficher / cacher le prompt. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnInteractionFocusChanged OnFocusChanged;
/** Le texte affiche a change. C'est a ca qu'un widget de prompt s'abonne. */
UPROPERTY(BlueprintAssignable, Category = "Interaction")
FOnInteractionPromptChanged OnPromptChanged;
protected:
/** Portee du trace en cm. 250 = un peu plus d'un bras et demi. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "1"))
@@ -88,6 +106,9 @@ private:
/** Point de vue reel du joueur, camera comprise. */
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
/** Reconstruit CachedPrompt et diffuse uniquement si le texte a reellement change. */
void RefreshPrompt();
/** Cible courante. TWeakObjectPtr : si l'objet est detruit, on ne garde pas un pointeur mort. */
TWeakObjectPtr<AActor> FocusedActor;
@@ -99,5 +120,11 @@ private:
*/
bool bHasFocusedActor = false;
/**
* Dernier texte diffuse. Le garder permet deux choses : ne reveiller l'UI
* que sur un vrai changement, et servir GetFocusedPrompt() sans recalculer.
*/
FText CachedPrompt;
float TimeSinceLastTrace = 0.f;
};
@@ -19,8 +19,12 @@ class UTextBlock;
* en UMG, l'equivalent d'un prefab d'UI avec son script en Unity.
*
* Le widget ne fait AUCUN travail par frame : il s'abonne au delegue
* OnFocusChanged du composant d'interaction et ne se met a jour que lorsque
* la cible change reellement.
* OnPromptChanged du composant d'interaction et ne se met a jour que lorsque
* le texte change reellement.
*
* OnPromptChanged et pas OnFocusChanged : le texte peut bouger sans que la
* cible change -- un feu de camp qui egrene son compte a rebours, ou le regard
* qui glisse d'un emplacement du grill a l'autre sur le meme acteur.
*/
UCLASS(Abstract)
class SURVIVAL_PROJET_API UInteractionPromptWidget : public UUserWidget
@@ -54,9 +58,9 @@ protected:
UInteractionPromptWidget(const FObjectInitializer& ObjectInitializer);
private:
/** Appele par le composant d'interaction a chaque changement de cible. */
/** Appele par le composant d'interaction a chaque changement du texte affiche. */
UFUNCTION()
void HandleFocusChanged(AActor* NewFocus, AActor* OldFocus);
void HandlePromptChanged(const FText& NewPrompt);
/** Recalcule le texte et la visibilite. */
void Refresh();
+112 -5
View File
@@ -6,6 +6,7 @@
#include "Engine/DataAsset.h"
#include "ItemDataAsset.generated.h"
class UMaterialInterface;
class UStaticMesh;
class UTexture2D;
@@ -47,6 +48,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UStaticMesh> WorldMesh;
/**
* Materiau force sur le WorldMesh. Vide = celui du mesh.
*
* Indispensable des qu'un pack partage une geometrie entre plusieurs
* aliments : une chaine de cuisson demande un visuel par etape, et sans ca
* il faudrait trois meshes distincts la ou un seul suffit avec trois
* teintes. N'ecrase que le PREMIER slot de materiau -- un mesh qui en
* expose plusieurs garde les autres tels quels.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
TObjectPtr<UMaterialInterface> WorldMeshMaterial;
/**
* Nombre maximum d'exemplaires par slot.
* 1 = ne s'empile pas (outils, armes), 20 = ressource classique.
@@ -75,11 +88,22 @@ public:
UFUNCTION(BlueprintPure, Category = "Item")
int32 GetEffectiveMaxStack() const { return MaxUses > 1 ? 1 : FMath::Max(1, MaxStackSize); }
/**
* Pose le mesh et le materiau de l'objet sur un composant.
*
* Un DataAsset ne devrait pas piloter un composant, mais la regle "mesh plus
* materiau optionnel" a deja deux appelants -- l'objet au sol et l'objet sur
* le feu -- et en aura d'autres, l'objet en main le premier. La dupliquer
* garantit qu'un jour l'un des deux oubliera le materiau.
*/
UFUNCTION(BlueprintCallable, Category = "Item")
void ApplyWorldVisualsTo(class UStaticMeshComponent* Component) const;
/**
* Slots ajoutes a l'inventaire quand cet objet est equipe comme sac.
* Laisse a 0 pour tout ce qui n'est pas un contenant.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Sac", meta = (ClampMin = "0"))
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Backpack", meta = (ClampMin = "0"))
int32 BonusInventorySlots = 0;
// ------------------------------------------------------------------
@@ -87,18 +111,101 @@ public:
// ------------------------------------------------------------------
/** Coche pour qu'un clic gauche consomme l'objet et applique ses effets. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable")
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable")
bool bIsConsumable = false;
/** Faim rendue a la consommation. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
float HungerRestore = 0.f;
/** Soif rendue. Une valeur negative deshydrate -- utile pour l'eau salee. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
float ThirstRestore = 0.f;
/** Vie rendue. Negative pour un aliment avarie ou toxique. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
float HealthRestore = 0.f;
// ------------------------------------------------------------------
// Cuisson
//
// Modele en CHAINE D'OBJETS, sans recette : cuire n'est pas un choix que le
// joueur fait dans un menu, c'est une propriete de l'objet. "Steak cru"
// pointe vers "Steak cuit", qui pointe vers "Steak brule". Chaque maillon
// est un asset complet, donc le changement de mesh sur le feu ne coute pas
// une ligne : c'est le WorldMesh du maillon suivant.
//
// Ce qui n'est PAS ici, et ne doit jamais y venir : le temps deja passe sur
// le feu. C'est de l'etat, il vit sur ACookingStation. Le mettre dans le
// DataAsset ferait cuire tous les steaks du monde en meme temps.
// ------------------------------------------------------------------
/** Cet objet se transforme quand on le pose sur un feu allume. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking")
bool bCanBeCooked = false;
/** Secondes de feu ACTIF necessaires. Un feu eteint met le compteur en pause. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBeCooked", ClampMin = "0.1", Units = "s"))
float CookDuration = 20.f;
/** Ce que l'objet devient une fois cuit. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBeCooked"))
TObjectPtr<UItemDataAsset> CookedResult;
/**
* L'objet se degrade s'il reste sur le feu.
*
* Volontairement independant de bCanBeCooked : un objet peut bruler sans
* jamais avoir ete cuisinable (une planche jetee dans les flammes), et un
* plat peut rester indefiniment au chaud sans jamais se gacher. Une seule
* chaine commune imposerait l'un ou l'autre a tout le monde.
*
* Si les deux sont coches, la CUISSON passe en premier : c'est le maillon
* cuit qui portera bCanBurn pour continuer vers le brule.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking")
bool bCanBurn = false;
/** Secondes de feu actif avant que l'objet se gache. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBurn", ClampMin = "0.1", Units = "s"))
float BurnDuration = 40.f;
/** Ce que l'objet devient une fois brule. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBurn"))
TObjectPtr<UItemDataAsset> BurntResult;
/**
* Le feu accepte-t-il cet objet sur un emplacement de cuisson.
*
* Un maillon final -- le steak brule -- repond non : on ne peut plus le
* poser, et il ne fait plus rien s'il est deja la. La regle est centralisee
* ici pour que la station n'ait pas a la reconstruire a chaque appel.
*/
UFUNCTION(BlueprintPure, Category = "Item|Cooking")
bool CanBePlacedOnFire() const
{
return (bCanBeCooked && CookedResult != nullptr) || (bCanBurn && BurntResult != nullptr);
}
// ------------------------------------------------------------------
// Carburant
// ------------------------------------------------------------------
/** Cet objet peut alimenter un feu. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Fuel")
bool bIsFuel = false;
/** Secondes de combustion apportees par exemplaire. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Fuel", meta = (EditCondition = "bIsFuel", ClampMin = "1", Units = "s"))
float FuelDuration = 60.f;
#if WITH_EDITOR
/**
* Une chaine de cuisson mal branchee ne planterait pas : la viande resterait
* simplement crue sur le feu, sans rien dire. C'est exactement le genre de
* bug qu'on cherche une heure -- autant que l'editeur le refuse a la
* sauvegarde.
*/
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
};
+1 -1
View File
@@ -36,7 +36,7 @@ public:
void InitPickup(UItemDataAsset* InItemData, int32 InQuantity, int32 InRemainingUses = -1);
// --- IInteractable ---
virtual FText GetInteractionPrompt_Implementation() const override;
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
virtual void Interact_Implementation(AActor* Interactor) override;
virtual void OnBeginFocus_Implementation() override;