(Feat) Add Harvetable

This commit is contained in:
2026-08-16 19:35:27 +02:00
parent d6f46f32af
commit 2c272c11b9
41 changed files with 1759 additions and 71 deletions
+68 -2
View File
@@ -3,6 +3,12 @@
#pragma once
#include "CoreMinimal.h"
// FVector_NetQuantize / FVector_NetQuantizeNormal, dans la signature de
// Server_UseItem : un point d'impact au demi-centimetre pres et une direction
// deja normalisee tiennent en quelques octets la ou deux FVector pleins en
// coutent vingt-quatre, pour une precision dont personne ne verrait la difference
// sur un jet de copeaux.
#include "Engine/NetSerialization.h"
#include "GameFramework/Character.h"
#include "FpsPlayer.generated.h"
@@ -12,6 +18,7 @@ class UCameraComponent;
class UCharacterAppearanceComponent;
class UCraftingComponent;
class UFootstepComponent;
class UHarvestingComponent;
class UHeadBobComponent;
class UHeldItemComponent;
class UInputAction;
@@ -167,6 +174,9 @@ public:
UFUNCTION(BlueprintPure, Category = "Audio")
UFootstepComponent* GetFootstepComponent() const { return FootstepComponent; }
UFUNCTION(BlueprintPure, Category = "Harvesting")
UHarvestingComponent* GetHarvestingComponent() const { return HarvestingComponent; }
// ------------------------------------------------------------------
// Commandes console d'apparence
//
@@ -245,6 +255,23 @@ public:
UFUNCTION(BlueprintCallable, Category = "Inventaire")
void DropAllItems();
/**
* Donne un objet au joueur, et pose au sol ce qui ne rentre pas.
*
* Le chemin normal de tout ce que le monde produit : récolte, surplus de
* fabrication, futur butin. Refuser parce que l'inventaire est plein ferait
* disparaître ce que le joueur vient d'obtenir sous ses yeux — c'est le
* genre de perte silencieuse qui coûte une session de jeu.
*
* Passe par `AddItem`, donc exactement comme un ramassage : piles entamées
* d'abord, puis barre rapide, puis grille.
*
* @return le nombre d'exemplaires réellement entrés dans l'inventaire ; le
* reste est au sol.
*/
UFUNCTION(BlueprintCallable, Category = "Inventaire")
int32 GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses = -1);
UFUNCTION(BlueprintPure, Category = "Survie")
bool IsDead() const { return bIsDead; }
@@ -472,6 +499,16 @@ protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Audio")
TObjectPtr<UFootstepComponent> FootstepComponent;
/**
* Le coup d'outil : trace du geste et recolte.
*
* Sur le pawn parce qu'il frappe avec CE corps, depuis CETTE camera, et qu'un
* cadavre ne recolte plus. Pendant exact d'UInteractionComponent, dont il
* partage le schema reseau -- voir son commentaire de classe.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Harvesting")
TObjectPtr<UHarvestingComponent> HarvestingComponent;
// ------------------------------------------------------------------
// Enhanced Input : assets à assigner dans le Blueprint
// ------------------------------------------------------------------
@@ -786,12 +823,34 @@ private:
* Le slot est passé explicitement : SelectedHotbarIndex n'est PAS répliqué,
* c'est une intention purement locale. Le serveur ne peut donc pas deviner
* ce que le joueur avait en main.
*
* La CIBLE aussi voyage, pour la même raison de fond : elle a été tracée
* depuis le point de vue réel du joueur à l'instant du geste. La retracer
* côté serveur la retracerait depuis une position qui a un demi-aller-retour
* de retard — un joueur en mouvement raterait des arbres qu'il vise pourtant
* en plein. Le serveur revalide la distance, il ne refait pas la visée.
*/
UFUNCTION(Server, Reliable)
void Server_UseItem(int32 SlotIndex);
void Server_UseItem(int32 SlotIndex, AActor* SwingTarget, FVector_NetQuantize ImpactPoint,
FVector_NetQuantizeNormal HitDirection);
/** L'utilisation réelle, côté autorité. */
void PerformUseItem(int32 SlotIndex);
void PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint,
const FVector& HitDirection);
/**
* Le coup d'outil côté autorité, une fois la cible validée.
*
* Séparée de `PerformUseItem` parce qu'elle porte une décision qui lui est
* propre : l'usure. Un coup qui ne porte pas — mauvais outil, souche, vide —
* ne consomme aucune charge, sinon taper un rocher à la hache l'abîmerait
* sans rien rendre.
*
* @param SlotIndex la case de l'outil, pour y prélever la charge. INDEX_NONE
* à mains nues.
*/
void PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget,
const FVector& ImpactPoint, const FVector& HitDirection);
/**
* Point d'entrée unique du sprint : applique en local ET prévient le serveur.
@@ -839,6 +898,13 @@ private:
UFUNCTION()
void HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses);
/**
* Pose une pile aux pieds du joueur. Le fond commun de `HandleCraftOverflow`
* et de `GiveItemOrDrop` : deux copies finiraient par diverger, et l'une des
* deux oublierait le trace vers le sol.
*/
void DropAtFeet(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);
@@ -0,0 +1,180 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ItemDataAsset.h"
#include "HarvestableDataAsset.generated.h"
class UItemDataAsset;
class UNiagaraSystem;
class USoundBase;
/**
* Une ligne de butin : « 1 a 3 Bois, 70 % du temps ».
*
* Le tirage n'a lieu QUE sur le serveur (AHarvestableResource::TryHarvest), donc
* l'aleatoire ne peut pas diverger d'une machine a l'autre -- les clients ne
* recoivent jamais le jet, seulement l'objet qui atterrit dans l'inventaire.
*/
USTRUCT(BlueprintType)
struct FHarvestDrop
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop")
TObjectPtr<UItemDataAsset> Item;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "1"))
int32 MinQuantity = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "1"))
int32 MaxQuantity = 1;
/** Probabilite que cette ligne tombe. 1 = toujours. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "0", ClampMax = "1"))
float Chance = 1.f;
/** Tire une quantite pour ce coup. Renvoie 0 quand le jet echoue. */
int32 Roll() const;
};
/**
* Les REGLES de recolte d'une ressource : ce qu'il faut pour l'abattre, ce
* qu'elle rend, en combien de temps elle repousse.
*
* Un asset par ESPECE, pas par instance ni par mesh : les quinze variantes de
* sapin du pack partagent toutes DA_Harvest_Tree, chacune posant son propre mesh
* sur son acteur. C'est la meme separation que UItemDataAsset et APickupItem --
* la donnee decrit une categorie, l'acteur porte le visuel et l'etat.
*
* Ce qui ne doit JAMAIS venir ici : les points de vie restants, le fait qu'un
* arbre soit deja abattu, l'heure de sa repousse. C'est de l'etat, il vit sur
* AHarvestableResource -- sinon abattre un arbre abattrait toute la foret.
*/
UCLASS(BlueprintType)
class EMBERWILD_API UHarvestableDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/** Identifiant stable, pour la sauvegarde a venir. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
FName HarvestableId;
/** Nom lisible, pour les logs et un futur message « Il vous faut une hache ». */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
FText DisplayName;
/**
* Outil capable d'entamer cette ressource.
*
* `None` ne veut pas dire « n'importe quoi » mais « a mains nues » : c'est ce
* qui rend un buisson de baies cueillable au poing, sans code particulier.
* Un arbre demande Axe, un filon Pickaxe.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
EToolType RequiredTool = EToolType::Axe;
/**
* Points a retirer avant epuisement.
*
* En points et pas en nombre de coups, pour que la hache en fer abatte en
* quatre coups l'arbre qui en demande dix a la hache de pierre : c'est
* `HarvestPower` de l'outil qui fait la difference. Un compte de coups en dur
* ici obligerait a dupliquer chaque ressource par palier d'outil.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable", meta = (ClampMin = "1"))
float Health = 100.f;
/**
* Ce que rend CHAQUE coup, meme celui qui n'abat pas.
*
* C'est la moitie qui rend le geste satisfaisant : un arbre qui ne donne rien
* pendant neuf coups laisse croire qu'on tape dans le vide. Garde les
* quantites basses -- c'est le lot final qui doit peser.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
TArray<FHarvestDrop> PerHitDrops;
/** Bonus du coup qui epuise la ressource. C'est la recompense de l'abattage. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable")
TArray<FHarvestDrop> DepletionDrops;
/**
* Secondes avant que la ressource redevienne recoltable. 0 = jamais.
*
* Une souche qui repousse dispense d'un systeme de spawn tant que la carte
* est peuplee a la main : rien ne disparait, donc rien n'a a etre recree.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable", meta = (ClampMin = "0", Units = "s"))
float RegrowDelay = 180.f;
// ------------------------------------------------------------------
// Retour au joueur
//
// Sur la DONNEE et non sur l'acteur : c'est l'espece qui decide qu'un coup
// de hache dans du bois claque et qu'un coup de pioche dans la pierre
// resonne. Regler ca instance par instance sur quatre cents arbres poses
// dans le niveau serait ingerable.
// ------------------------------------------------------------------
/** Joue a chaque coup qui entame reellement la ressource. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
TObjectPtr<USoundBase> HitSound;
/** Copeaux, eclats de pierre. Joue au point d'impact, oriente par la normale. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
TObjectPtr<UNiagaraSystem> HitEffect;
/**
* Le coup n'a rien fait : mauvais outil, ou ressource deja epuisee.
*
* Un seul son pour les deux cas, et c'est volontaire : dans les deux le
* joueur doit entendre que son geste ne sert a rien. Le laisser vide donne un
* coup parfaitement muet, ce que l'oreille lit comme un bug.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
TObjectPtr<USoundBase> IneffectiveHitSound;
/** Craquement de chute, eboulement. Joue sur le coup qui epuise. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
TObjectPtr<USoundBase> DepletionSound;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback")
TObjectPtr<UNiagaraSystem> DepletionEffect;
/**
* Angle maximal du balancement encaisse a chaque coup, en degres.
*
* 0 pour un rocher : la pierre ne plie pas, et la voir osciller casse
* immediatement l'illusion de masse. Quelques degres suffisent sur un arbre --
* au-dela le tronc a l'air en caoutchouc.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback",
meta = (ClampMin = "0", ClampMax = "45", Units = "deg"))
float HitShakeAngle = 3.f;
/** Duree du balancement amorti, en secondes. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback",
meta = (ClampMin = "0", Units = "s"))
float HitShakeDuration = 0.45f;
/** Oscillations par seconde. Plus haut = plus nerveux, plus leger. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback", meta = (ClampMin = "0.1"))
float HitShakeFrequency = 7.f;
/** Vrai si cette ressource se recolte a mains nues. */
UFUNCTION(BlueprintPure, Category = "Harvestable")
bool IsHarvestableByHand() const { return RequiredTool == EToolType::None; }
#if WITH_EDITOR
/**
* Une table de butin dont une ligne pointe dans le vide ne planterait pas :
* l'arbre donnerait simplement moins, sans rien dire. C'est 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
};
@@ -0,0 +1,242 @@
// 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;
};
@@ -0,0 +1,126 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Engine/EngineTypes.h"
#include "HarvestableResource.h"
#include "HarvestingComponent.generated.h"
class UItemDataAsset;
/**
* Le COUP : ce qui part quand la hache touche.
*
* A poser sur le pawn du joueur, a cote d'UInteractionComponent -- dont il est le
* pendant exact, et la comparaison est la meilleure facon de comprendre les deux :
*
* UInteractionComponent trace en continu, affiche un prompt, agit sur E.
* UHarvestingComponent ne trace QU'AU moment du coup, n'affiche rien.
*
* On ne « vise » pas un arbre, on le frappe : il n'y a donc ni cible memorisee,
* ni surbrillance, ni texte a l'ecran. Le trace part une fois par coup, depuis
* l'AnimNotify pose dans le montage de l'outil -- jamais a l'appui du clic, sans
* quoi la hache abattrait l'arbre avant de l'avoir touche.
*
* ------------------------------------------------------------------
* Reseau : le client vise, le serveur tranche
* ------------------------------------------------------------------
*
* C'est le schema d'UInteractionComponent, et pour la meme raison : le trace doit
* partir du point de vue REEL du joueur au moment du geste. Le refaire cote
* serveur le referait depuis une position qui a un demi-aller-retour de retard,
* et un joueur en mouvement raterait des arbres qu'il vise pourtant en plein.
*
* La cible part donc dans la RPC (AFpsPlayer::Server_UseItem) et le serveur
* revalide la distance avec une tolerance. Ce n'est PAS de l'anti-triche -- coop
* entre amis -- mais un garde-fou contre un client desynchronise qui abattrait
* un arbre a l'autre bout de la carte.
*
* Aucune RPC ne vit sur ce composant : elles sont toutes sur le pawn, qui portait
* deja Server_UseItem. Un composant qui en porterait aurait besoin de
* SetIsReplicatedByDefault(true), sans quoi le moteur les jetterait en silence.
*/
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
class EMBERWILD_API UHarvestingComponent : public UActorComponent
{
GENERATED_BODY()
public:
UHarvestingComponent();
/**
* Trace le coup depuis le point de vue du joueur. A appeler EN LOCAL.
*
* Renvoie n'importe quel acteur touche, pas seulement une ressource : c'est le
* serveur qui decide de ce qu'on peut recolter, et lui renvoyer un nullptr
* parce qu'on a mal identifie la cible chez nous serait lui retirer ce mot.
*
* @param OutImpactPoint point touche, pour les copeaux et la validation.
* @param OutDirection direction du coup, normalisee.
* @return l'acteur touche, ou nullptr si le coup part dans le vide.
*/
UFUNCTION(BlueprintCallable, Category = "Harvesting")
AActor* TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const;
/**
* Applique le coup a la cible. A n'appeler que sur le SERVEUR.
*
* @param Tool l'objet en main, nul si les mains sont vides.
* @return ce qui s'est produit ; l'appelant s'en sert pour decider de l'usure.
*/
EHarvestResult ApplySwing(AActor* Target, const UItemDataAsset* Tool,
const FVector& ImpactPoint, const FVector& HitDirection);
/** Points retires par un coup avec cet outil, mains vides comprises. */
UFUNCTION(BlueprintPure, Category = "Harvesting")
float GetSwingPower(const UItemDataAsset* Tool) const;
protected:
/**
* Portee du coup en cm.
*
* Plus courte que celle de l'interaction, et c'est voulu : ramasser une branche
* a bout de bras est un confort, abattre un arbre a la meme distance donnerait
* une hache qui frappe le vide. On doit etre AU pied de l'arbre.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "1", Units = "cm"))
float SwingRange = 200.f;
/**
* Rayon de la sphere balayee.
*
* Nettement plus large que celui de l'interaction : on ne vise pas un tronc au
* pixel pres, on donne un coup de hache. Un rayon serre rend la recolte penible
* sans rendre le jeu plus exigeant.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "0", Units = "cm"))
float SwingRadius = 25.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting")
TEnumAsByte<ECollisionChannel> SwingChannel = ECC_Visibility;
/**
* Puissance d'un coup a mains nues.
*
* Sur le composant et pas sur un DataAsset, pour la raison evidente qu'il n'y a
* pas d'objet a qui la demander -- exactement comme UnarmedUseMontage sur
* UHeldItemComponent. Elle n'a d'effet que sur les ressources dont
* RequiredTool vaut None : cueillir des baies, arracher des herbes.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "0"))
float UnarmedHarvestPower = 10.f;
/** Marge de distance accordee par le serveur. Voir le commentaire de classe. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting|Network", meta = (ClampMin = "1.0"))
float ServerRangeTolerance = 2.f;
/** Affiche le trace du coup. Le premier reflexe quand la hache ne touche rien. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting")
bool bDrawDebug = false;
private:
/** Point de vue reel du joueur, camera comprise. */
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
};
+22 -7
View File
@@ -240,20 +240,35 @@ public:
// ------------------------------------------------------------------
// Outil
//
// Volontairement reduit au TYPE pour l'instant : la puissance de recolte et
// le cout en durabilite d'un coup n'ont pas encore de lecteur, et inventer
// leur forme avant d'ecrire les ressources recoltables serait deviner.
// L'usure passe par MaxUses / RemainingUses, comme une canette : un outil neuf
// part a MaxUses charges, l'inventaire les suit deja au sol comme dans un
// coffre, et WBP_ItemDetails affiche deja la barre correspondante. Un coup qui
// PORTE consomme une charge -- un coup dans le vide ou contre la mauvaise
// matiere n'use rien, voir UHarvestingComponent.
//
// L'usure, elle, est deja tranchee : ce sera MaxUses / RemainingUses, comme
// une canette. Un outil neuf part donc a MaxUses charges, l'inventaire les
// suit deja au sol comme dans un coffre, et WBP_ItemDetails affiche deja la
// barre correspondante -- rien a ajouter le jour venu.
// Corollaire a connaitre : un outil laisse a MaxUses = 1 casserait au premier
// coup. AFpsPlayer::PerformUseItem s'y refuse et le dit dans le log, plutot que
// de faire disparaitre la hache que le joueur vient de fabriquer.
// ------------------------------------------------------------------
/** Ce que cet objet sait recolter. None = ce n'est pas un outil. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool")
EToolType ToolType = EToolType::None;
/**
* Points retires a une ressource par coup.
*
* A comparer au Health de UHarvestableDataAsset : 25 contre un arbre a 100
* donne quatre coups. C'est ce champ, et lui seul, qui fait la difference entre
* la hache de pierre et celle en fer -- la ressource, elle, ne change pas.
*
* Sans effet sur une ressource que ToolType n'autorise pas : une pioche a 100
* de puissance ne coupe toujours pas un arbre.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool",
meta = (EditCondition = "ToolType != EToolType::None", ClampMin = "0"))
float HarvestPower = 25.f;
UFUNCTION(BlueprintPure, Category = "Item|Tool")
bool IsTool() const { return ToolType != EToolType::None; }