Files
Unreal_EmberWild/Source/EmberWild/Public/HarvestableResource.h
T
2026-08-16 19:35:27 +02:00

243 lines
8.8 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/NetSerialization.h"
#include "GameFramework/Actor.h"
#include "HarvestableResource.generated.h"
class AFpsPlayer;
class UHarvestableDataAsset;
class UItemDataAsset;
class UStaticMesh;
class UStaticMeshComponent;
/**
* Ce qu'a produit un coup, du point de vue de celui qui frappe.
*
* Un enum et pas un booleen, parce que l'appelant en tire trois conduites
* distinctes : un coup qui porte use l'outil et rend du butin, un mauvais outil
* ne doit RIEN user -- sinon taper un rocher a la hache la deteriore sans rien
* donner, ce que personne ne peut comprendre -- et une ressource deja epuisee
* n'est pas une erreur du joueur, juste un geste sans effet.
*/
UENUM(BlueprintType)
enum class EHarvestResult : uint8
{
/** Le coup a porte : du butin est parti, l'outil s'use. */
Harvested,
/** Le coup a epuise la ressource : butin final compris. */
Depleted,
/** L'outil en main ne peut rien contre cette ressource. */
WrongTool,
/** Deja epuisee ; elle repoussera peut-etre. */
AlreadyDepleted,
/** Cible invalide, donnee manquante : rien ne s'est produit. */
Invalid
};
/** Ce que le multicast de retour doit jouer. Un seul RPC pour les trois cas. */
UENUM()
enum class EHarvestFeedback : uint8
{
Hit,
FinalBlow,
Ineffective
};
/**
* Un arbre, un rocher, un buisson : quelque chose qu'on frappe pour en tirer des
* ressources.
*
* Ce n'est PAS un IInteractable, et c'est la decision structurante : on ne
* « valide » pas un arbre, on le frappe. Le coup part du marqueur
* UAnimNotify_ItemAction pose dans le montage de l'outil, donc au moment ou la
* hache touche -- jamais a l'appui. L'acteur n'a donc ni prompt, ni volume de
* visee dedie : le trace du coup vient toucher son MESH, comme un vrai coup.
*
* ------------------------------------------------------------------
* Ou vit quoi
* ------------------------------------------------------------------
*
* Les REGLES (outil requis, points de vie, butin, repousse) sont dans le
* UHarvestableDataAsset, partage par toute l'espece. Les MESHES sont ici, sur
* l'acteur : les quinze sapins du pack ont quinze visuels et une seule regle.
* Mettre le mesh sur la donnee obligerait a un DataAsset par variante, mettre la
* regle sur l'acteur obligerait a re-regler le butin sur quatre cents instances.
*
* ------------------------------------------------------------------
* Reseau : le serveur compte, les clients regardent
* ------------------------------------------------------------------
*
* CurrentHealth n'est PAS repliquee et ne doit jamais l'etre : c'est un compteur
* qui change a chaque coup pour un resultat que le joueur lit deja dans le
* balancement du tronc et dans les objets qui arrivent dans son sac. Seul
* bDepleted part sur le reseau -- une transition, pas un flux.
*/
UCLASS()
class EMBERWILD_API AHarvestableResource : public AActor
{
GENERATED_BODY()
public:
AHarvestableResource();
virtual void Tick(float DeltaTime) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/**
* Encaisse un coup. A n'appeler que sur le SERVEUR.
*
* @param Harvester celui qui frappe ; c'est son inventaire qui recoit.
* @param Tool l'objet en main, nul si les mains sont vides.
* @param Power points retires par ce coup.
* @param ImpactPoint point touche par le trace, pour les copeaux.
* @param HitDirection direction du coup, normalisee, pour le sens du balancement.
*/
EHarvestResult TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power,
const FVector& ImpactPoint, const FVector& HitDirection);
/** Vrai si l'outil fourni (nul = mains nues) peut entamer cette ressource. */
UFUNCTION(BlueprintPure, Category = "Harvestable")
bool AcceptsTool(const UItemDataAsset* Tool) const;
UFUNCTION(BlueprintPure, Category = "Harvestable")
bool IsDepleted() const { return bDepleted; }
UFUNCTION(BlueprintPure, Category = "Harvestable")
UHarvestableDataAsset* GetHarvestData() const { return HarvestData; }
protected:
virtual void BeginPlay() override;
/**
* Le visuel ET la surface qu'on frappe.
*
* Le mesh porte lui-meme la collision, comme ACraftingStation et contrairement
* a APickupItem : un arbre est un obstacle, il a forcement une collision, et un
* volume ajoute par-dessus laisserait frapper le vide autour du tronc.
* Corollaire a verifier sur chaque mesh de pack : sans collision simple, le
* trace du coup ne rencontre rien et l'arbre est intouchable.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Harvestable")
TObjectPtr<UStaticMeshComponent> Mesh;
/** Les regles de recolte. Sans elle l'acteur est un simple decor. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
TObjectPtr<UHarvestableDataAsset> HarvestData;
/**
* Ce qu'on voit une fois la ressource epuisee : la souche, le filon vide.
*
* Vide = l'acteur se cache entierement, collision comprise. C'est un repli
* acceptable pour un buisson de baies -- personne ne remarque un buisson
* absent -- mais un arbre qui s'evapore se voit immediatement : donne-lui une
* souche.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
TObjectPtr<UStaticMesh> DepletedMesh;
/**
* Epuisee, en attente de repousse.
*
* ReplicatedUsing et pas simplement Replicated : c'est le meme piege que
* APickupItem::ItemData. Un client qui rejoint une partie en cours construit
* l'acteur AVANT que la propriete arrive -- il verrait l'arbre intact et
* pourrait le frapper, alors que le serveur le sait deja abattu.
*/
UPROPERTY(ReplicatedUsing = OnRep_Depleted, BlueprintReadOnly, Category = "Harvestable")
bool bDepleted = false;
UFUNCTION()
void OnRep_Depleted();
private:
/**
* Points restants. SERVEUR UNIQUEMENT -- voir le commentaire de classe.
* Initialisee au BeginPlay depuis la donnee, et remise a plein a la repousse.
*/
float CurrentHealth = 0.f;
/**
* Le mesh d'origine, retenu au BeginPlay pour le reposer a la repousse.
*
* Retenu et surtout pas relu depuis le CDO de la classe : une instance posee
* dans le niveau peut avoir son propre mesh -- c'est meme le cas normal, un
* seul BP_Harvestable servant les quinze variantes d'arbre du pack. Le CDO
* rendrait alors le mesh du Blueprint, et l'arbre repousserait en sapin
* generique. Le symptome n'apparait qu'apres RegrowDelay secondes.
*/
UPROPERTY(Transient)
TObjectPtr<UStaticMesh> IntactMesh;
/**
* Verse le contenu d'une table dans l'inventaire du recolteur.
* @return le nombre de lignes qui ont reellement donne quelque chose.
*/
int32 GrantDrops(const TArray<struct FHarvestDrop>& Table, AFpsPlayer* Harvester);
/** Passe la ressource a l'etat epuise et arme la repousse. Serveur. */
void Deplete();
/** Remet la ressource d'aplomb. Appelee par le timer, donc serveur. */
void Regrow();
/**
* Pose le mesh correspondant a bDepleted. Tourne sur TOUTES les machines.
*
* Partagee par BeginPlay et OnRep : deux copies finiraient par diverger, et le
* symptome serait une souche correcte chez l'hote et un arbre intact chez le
* client qui vient de le voir tomber.
*/
void ApplyDepletionVisuals();
/**
* Retour audiovisuel du coup, joue chez tout le monde.
*
* Unreliable, et c'est un choix : un paquet perdu coute un bruit de hache, pas
* un etat de jeu. Le fiabiliser ferait payer un accuse de reception a chacun
* des trois autres joueurs pour chaque coup porte dans la partie.
*/
UFUNCTION(NetMulticast, Unreliable)
void Multicast_PlayHarvestFeedback(EHarvestFeedback Feedback, FVector_NetQuantize ImpactPoint,
FVector_NetQuantizeNormal HitDirection);
/** Joue son, particules et balancement. Locale a chaque machine. */
void PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint, const FVector& HitDirection);
/** Lance le balancement. Axe deduit de la direction du coup. */
void StartShake(const FVector& HitDirection);
/**
* Temps ecoule dans le balancement en cours, en secondes.
* Negatif = aucun balancement, donc le Tick est coupe.
*/
float ShakeElapsed = -1.f;
/**
* Axe autour duquel le mesh oscille, en espace LOCAL.
*
* En local et pas en monde : le mesh tourne dans le repere de son parent, et
* un arbre pose avec un yaw quelconque ferait pencher le tronc de travers si
* l'axe restait exprime en monde.
*/
FVector ShakeAxis = FVector::RightVector;
/**
* Rotation d'origine du mesh, pour y revenir exactement.
*
* Retenue et jamais recalculee : accumuler des AddRelativeRotation laisse
* derriver l'arbre a chaque coup, et au bout de vingt coups il pousse
* franchement de travers. Le symptome ne sort qu'apres une longue session.
*/
FRotator BaseMeshRotation = FRotator::ZeroRotator;
/** Repousse, cote serveur uniquement. */
FTimerHandle RegrowTimer;
};