// 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& 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 Mesh; /** Les regles de recolte. Sans elle l'acteur est un simple decor. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") TObjectPtr 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 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 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& 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; };