// 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 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 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 EMBERWILD_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& 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 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 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> SlotMeshes; /** Flamme. Active tant que le feu brule, coupee des qu'il s'eteint. */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking") TObjectPtr 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 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 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 LightSound; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound") TObjectPtr ExtinguishSound; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound") TObjectPtr 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. */ virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; 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. * * Replique : c'est ce qui fait changer le mesh de la nourriture sous les * yeux de tous. Le FTimerHandle que porte chaque slot n'est pas une * UPROPERTY et ne part donc pas sur le reseau -- c'est voulu, la cuisson se * compte uniquement sur le serveur et les clients n'en voient que le * resultat. */ UPROPERTY(Transient, ReplicatedUsing = OnRep_Slots) TArray 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. */ /** * Replique : le nombre de buches visibles EST la jauge de carburant, c'est * la seule facon pour un joueur de savoir combien il reste de feu. Les * secondes restantes partent avec, mais seul le serveur les decompte. */ UPROPERTY(Transient, ReplicatedUsing = OnRep_Fuel) TArray 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> FuelIndicators; UPROPERTY(ReplicatedUsing = OnRep_IsLit) bool bIsLit = false; /** Le contenu du grill a change cote serveur : on repose tous les meshes. */ UFUNCTION() void OnRep_Slots(); UFUNCTION() void OnRep_Fuel(); UFUNCTION() void OnRep_IsLit(); };