(Feat) Add Harvetable
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
#include "CraftingComponent.h"
|
||||
#include "FootstepComponent.h"
|
||||
#include "FpsPlayerController.h"
|
||||
#include "HarvestingComponent.h"
|
||||
#include "HeadBobComponent.h"
|
||||
#include "HeldItemComponent.h"
|
||||
#include "InteractionComponent.h"
|
||||
@@ -115,6 +116,7 @@ AFpsPlayer::AFpsPlayer()
|
||||
AppearanceComponent = CreateDefaultSubobject<UCharacterAppearanceComponent>(TEXT("AppearanceComponent"));
|
||||
HeldItemComponent = CreateDefaultSubobject<UHeldItemComponent>(TEXT("HeldItemComponent"));
|
||||
FootstepComponent = CreateDefaultSubobject<UFootstepComponent>(TEXT("FootstepComponent"));
|
||||
HarvestingComponent = CreateDefaultSubobject<UHarvestingComponent>(TEXT("HarvestingComponent"));
|
||||
|
||||
// En vue FPS on ne veut pas voir son propre corps (mesh 3e personne),
|
||||
// mais on garde son ombre.
|
||||
@@ -786,52 +788,113 @@ void AFpsPlayer::TriggerHeldItemAction()
|
||||
return;
|
||||
}
|
||||
|
||||
// Le trace part d'ICI, chez celui qui frappe, et sans condition sur ce qu'il
|
||||
// tient. Deux raisons de ne pas le reserver aux outils : une ressource dont
|
||||
// RequiredTool vaut None se cueille a mains nues, et lire l'inventaire local
|
||||
// pour trier ferait dependre le trace d'un etat qui peut avoir une frame de
|
||||
// retard. Le cout est un unique balayage spherique par CLIC -- pas par frame.
|
||||
FVector ImpactPoint;
|
||||
FVector HitDirection;
|
||||
AActor* SwingTarget = HarvestingComponent
|
||||
? HarvestingComponent->TraceSwingTarget(ImpactPoint, HitDirection)
|
||||
: nullptr;
|
||||
|
||||
// L'hote agit tout de suite, le client demande. On envoie meme si le slot
|
||||
// parait vide chez nous : c'est le serveur qui fait foi, et son inventaire
|
||||
// peut avoir une frame d'avance sur le notre.
|
||||
if (!HasAuthority())
|
||||
{
|
||||
Server_UseItem(PendingUseSlotIndex);
|
||||
Server_UseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||
return;
|
||||
}
|
||||
|
||||
PerformUseItem(PendingUseSlotIndex);
|
||||
PerformUseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||
}
|
||||
|
||||
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex)
|
||||
void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex, AActor* SwingTarget,
|
||||
FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection)
|
||||
{
|
||||
PerformUseItem(SlotIndex);
|
||||
PerformUseItem(SlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||
}
|
||||
|
||||
void AFpsPlayer::PerformUseItem(int32 SlotIndex)
|
||||
void AFpsPlayer::PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint,
|
||||
const FVector& HitDirection)
|
||||
{
|
||||
if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex))
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex];
|
||||
if (Selected.IsEmpty())
|
||||
// Un slot invalide ou vide n'est PAS une sortie : c'est le cas des mains nues,
|
||||
// qui frappent quand meme -- un buisson de baies se cueille au poing. La
|
||||
// version precedente sortait ici, quand seuls les consommables agissaient.
|
||||
const FInventorySlot* Selected = InventoryComponent->GetSlots().IsValidIndex(SlotIndex)
|
||||
? &InventoryComponent->GetSlots()[SlotIndex]
|
||||
: nullptr;
|
||||
|
||||
const UItemDataAsset* Item = (Selected && !Selected->IsEmpty()) ? Selected->Item.Get() : nullptr;
|
||||
|
||||
if (!Item)
|
||||
{
|
||||
PerformToolSwing(nullptr, INDEX_NONE, SwingTarget, ImpactPoint, HitDirection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Item->bIsConsumable)
|
||||
{
|
||||
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
|
||||
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
|
||||
|
||||
// ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et
|
||||
// usages multiples (une charge en moins, l'objet reste tant qu'il en a).
|
||||
// On vise le slot actif precisement, et pas RemoveItem qui piocherait
|
||||
// n'importe quelle pile du meme objet, y compris dans le sac.
|
||||
InventoryComponent->ConsumeUse(SlotIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tout le reste frappe : les outils avec leur puissance, les autres objets
|
||||
// comme un poing. Un objet qui n'est ni consommable ni outil ne peut donc rien
|
||||
// contre un arbre -- AHarvestableResource::AcceptsTool l'y refuse -- mais
|
||||
// cueille les ressources a mains nues, ce qui est exactement ce qu'on veut :
|
||||
// tenir une pomme n'empeche pas de ramasser des baies.
|
||||
PerformToolSwing(Item, SlotIndex, SwingTarget, ImpactPoint, HitDirection);
|
||||
}
|
||||
|
||||
void AFpsPlayer::PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget,
|
||||
const FVector& ImpactPoint, const FVector& HitDirection)
|
||||
{
|
||||
if (!HarvestingComponent || !SwingTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const UItemDataAsset* Item = Selected.Item;
|
||||
const EHarvestResult Result = HarvestingComponent->ApplySwing(SwingTarget, Tool, ImpactPoint, HitDirection);
|
||||
|
||||
// Objet non consommable : on ne fait rien pour l'instant. C'est ici que
|
||||
// viendront les outils (frapper), les armes (tirer), les placeables.
|
||||
if (!Item->bIsConsumable)
|
||||
// SEULS les deux resultats qui portent usent l'outil. Un mauvais outil, une
|
||||
// souche ou un coup dans le vide n'en consomment aucune charge : abimer une
|
||||
// hache en tapant un rocher serait une punition que rien a l'ecran n'explique.
|
||||
if (Result != EHarvestResult::Harvested && Result != EHarvestResult::Depleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Les valeurs de restauration sont appliquees a CHAQUE utilisation.
|
||||
SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore);
|
||||
if (!Tool || !Tool->IsTool() || SlotIndex == INDEX_NONE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Un outil laisse a MaxUses = 1 disparaitrait au PREMIER coup : ConsumeUse
|
||||
// retire alors l'exemplaire entier. On refuse plutot que de faire s'evaporer
|
||||
// la hache que le joueur vient de fabriquer, et on le dit -- le symptome, lui,
|
||||
// se lit comme « ma hache a disparu » et se cherche tres longtemps.
|
||||
if (Tool->MaxUses <= 1)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : l'outil %s a MaxUses = 1, il casserait au premier coup. Usure ignoree."),
|
||||
*GetNameSafe(Tool));
|
||||
return;
|
||||
}
|
||||
|
||||
// ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et
|
||||
// usages multiples (une charge en moins, l'objet reste tant qu'il en a).
|
||||
// On vise le slot actif precisement, et pas RemoveItem qui piocherait
|
||||
// n'importe quelle pile du meme objet, y compris dans le sac.
|
||||
InventoryComponent->ConsumeUse(SlotIndex);
|
||||
}
|
||||
|
||||
@@ -940,6 +1003,11 @@ bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotInd
|
||||
}
|
||||
|
||||
void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||
{
|
||||
DropAtFeet(Item, Quantity, RemainingUses);
|
||||
}
|
||||
|
||||
void AFpsPlayer::DropAtFeet(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||
{
|
||||
if (!Item || Quantity <= 0)
|
||||
{
|
||||
@@ -948,7 +1016,7 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32
|
||||
|
||||
if (!DroppedPickupClass)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."),
|
||||
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sont perdus."),
|
||||
Quantity, *GetNameSafe(Item));
|
||||
return;
|
||||
}
|
||||
@@ -960,16 +1028,43 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32
|
||||
|
||||
// Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le
|
||||
// nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser
|
||||
// une cloison qu'on ne regarde meme pas.
|
||||
// une cloison qu'on ne regarde meme pas. Le meme raisonnement vaut pour le
|
||||
// surplus de recolte -- le joueur regarde l'arbre, pas le sol.
|
||||
const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f;
|
||||
|
||||
if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation)))
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."),
|
||||
UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s."),
|
||||
Quantity, *GetNameSafe(Item));
|
||||
}
|
||||
}
|
||||
|
||||
int32 AFpsPlayer::GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses)
|
||||
{
|
||||
if (!Item || Quantity <= 0 || !InventoryComponent)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Garde-fou et pas simple precaution : engendrer un pickup chez un client
|
||||
// donnerait un objet visible de lui seul, efface au premier nettoyage reseau.
|
||||
// L'appelant normal -- AHarvestableResource -- est deja cote serveur.
|
||||
if (!HasAuthority())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer::GiveItemOrDrop appelee sans autorite pour %s, ignoree."),
|
||||
*GetNameSafe(Item));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int32 Rejected = InventoryComponent->AddItem(Item, Quantity, RemainingUses);
|
||||
if (Rejected > 0)
|
||||
{
|
||||
DropAtFeet(Item, Rejected, RemainingUses);
|
||||
}
|
||||
|
||||
return Quantity - Rejected;
|
||||
}
|
||||
|
||||
void AFpsPlayer::DropAllItems()
|
||||
{
|
||||
// Declenche par la mort, qui sera arbitree par le serveur. La garde est la
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "HarvestableDataAsset.h"
|
||||
|
||||
#include "ItemDataAsset.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Misc/DataValidation.h"
|
||||
#endif
|
||||
|
||||
#define LOCTEXT_NAMESPACE "HarvestableData"
|
||||
|
||||
int32 FHarvestDrop::Roll() const
|
||||
{
|
||||
if (!Item)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Le jet de chance d'abord : une ligne a 30 % qui echoue ne doit pas consommer
|
||||
// de tirage de quantite, sinon deux tables identiques a la chance pres ne
|
||||
// donneraient pas la meme suite -- sans consequence en jeu, mais deroutant a
|
||||
// deboguer avec une graine fixee.
|
||||
if (Chance < 1.f && FMath::FRand() > Chance)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Max borne par Min plutot que l'inverse : un designer qui saisit 3 puis 1
|
||||
// obtient 3, jamais un intervalle vide qui rendrait la ligne muette sans rien
|
||||
// dire. RandRange est inclusif des deux cotes.
|
||||
return FMath::RandRange(MinQuantity, FMath::Max(MinQuantity, MaxQuantity));
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
EDataValidationResult UHarvestableDataAsset::IsDataValid(FDataValidationContext& Context) const
|
||||
{
|
||||
EDataValidationResult Result = Super::IsDataValid(Context);
|
||||
|
||||
// Le symptome d'une ligne vide est fourbe : l'arbre se recolte, s'abat, et
|
||||
// rend simplement moins que prevu. Rien dans le log, rien a l'ecran.
|
||||
auto ValidateTable = [&Context, &Result](const TArray<FHarvestDrop>& Table, const FText& TableName)
|
||||
{
|
||||
for (int32 Index = 0; Index < Table.Num(); ++Index)
|
||||
{
|
||||
if (!Table[Index].Item)
|
||||
{
|
||||
Context.AddError(FText::Format(
|
||||
LOCTEXT("DropNoItem", "{0}[{1}] has no 'Item' assigned: this line would silently yield nothing."),
|
||||
TableName, FText::AsNumber(Index)));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ValidateTable(PerHitDrops, LOCTEXT("PerHitTable", "Per Hit Drops"));
|
||||
ValidateTable(DepletionDrops, LOCTEXT("DepletionTable", "Depletion Drops"));
|
||||
|
||||
// Une ressource sans aucun butin est frappable, s'epuise, et ne donne rien.
|
||||
// C'est legitime pour un decor destructible ; ca ne l'est presque jamais ici.
|
||||
if (PerHitDrops.IsEmpty() && DepletionDrops.IsEmpty())
|
||||
{
|
||||
Context.AddWarning(LOCTEXT("NoDropsAtAll", "This harvestable has no drops at all: harvesting it will consume tool durability for nothing."));
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
#endif
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,358 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "HarvestableResource.h"
|
||||
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "FpsPlayer.h"
|
||||
#include "HarvestableDataAsset.h"
|
||||
#include "ItemDataAsset.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NiagaraFunctionLibrary.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
AHarvestableResource::AHarvestableResource()
|
||||
{
|
||||
// Le Tick ne sert QU'AU balancement, et il s'allume tout seul le temps de
|
||||
// l'amortissement. Une foret de quatre cents arbres qui ticke en permanence
|
||||
// pour ne rien faire est exactement ce qu'on evite ici.
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
PrimaryActorTick.bStartWithTickEnabled = false;
|
||||
|
||||
// Acteur du monde : le compte des coups appartient au serveur. Quatre clients
|
||||
// qui compteraient chacun leurs points de vie divergeraient, et l'arbre
|
||||
// tomberait chez l'un alors qu'il tient encore chez l'autre.
|
||||
bReplicates = true;
|
||||
SetReplicateMovement(false);
|
||||
|
||||
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
SetRootComponent(Mesh);
|
||||
|
||||
// QueryAndPhysics : l'arbre barre le passage en plus d'etre visable. Le trace
|
||||
// du coup passe par le canal Visibility, que la collision par defaut bloque.
|
||||
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
|
||||
// Le balancement bouge le mesh a l'execution : Static le cuirait sur place et
|
||||
// la rotation n'aurait aucun effet a l'ecran, sans le moindre avertissement.
|
||||
Mesh->SetMobility(EComponentMobility::Movable);
|
||||
}
|
||||
|
||||
void AHarvestableResource::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(AHarvestableResource, bDepleted);
|
||||
}
|
||||
|
||||
void AHarvestableResource::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Retenue ici et pas au constructeur : la rotation relative du mesh peut avoir
|
||||
// ete reglee dans le Blueprint ou sur l'instance posee dans le niveau, et c'est
|
||||
// a CETTE valeur que le balancement doit revenir.
|
||||
BaseMeshRotation = Mesh->GetRelativeRotation();
|
||||
|
||||
// AVANT le premier ApplyDepletionVisuals, qui peut deja poser la souche chez un
|
||||
// client arrivant sur un arbre abattu : on capturerait alors la souche comme
|
||||
// etat intact, et l'arbre ne repousserait jamais.
|
||||
IntactMesh = Mesh->GetStaticMesh();
|
||||
|
||||
if (HasAuthority() && HarvestData)
|
||||
{
|
||||
CurrentHealth = HarvestData->Health;
|
||||
}
|
||||
|
||||
// Sur toutes les machines : chez l'hote parce qu'aucun OnRep ne se declenche
|
||||
// pour lui, chez un client parce que l'ordre d'arrivee entre la construction de
|
||||
// l'acteur et ses proprietes repliquees n'est garanti par rien.
|
||||
ApplyDepletionVisuals();
|
||||
|
||||
if (!HarvestData)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("AHarvestableResource : %s n'a pas de HarvestData, il ne se recoltera jamais."),
|
||||
*GetName());
|
||||
}
|
||||
}
|
||||
|
||||
bool AHarvestableResource::AcceptsTool(const UItemDataAsset* Tool) const
|
||||
{
|
||||
if (!HarvestData)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mains vides = EToolType::None, exactement comme un objet qui n'est pas un
|
||||
// outil : tenir une pomme ne doit pas abattre plus qu'un poing.
|
||||
const EToolType HeldType = (Tool && Tool->IsTool()) ? Tool->ToolType : EToolType::None;
|
||||
|
||||
return HeldType == HarvestData->RequiredTool;
|
||||
}
|
||||
|
||||
EHarvestResult AHarvestableResource::TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power,
|
||||
const FVector& ImpactPoint, const FVector& HitDirection)
|
||||
{
|
||||
// La garde est ici et pas seulement chez l'appelant : c'est le point d'entree
|
||||
// unique du systeme, et rien ne garantit qu'un futur appelant y pense.
|
||||
if (!HasAuthority() || !HarvestData || !Harvester || Power <= 0.f)
|
||||
{
|
||||
return EHarvestResult::Invalid;
|
||||
}
|
||||
|
||||
if (bDepleted)
|
||||
{
|
||||
// Un « toc » et rien d'autre : le joueur doit entendre que sa souche ne
|
||||
// donne plus rien, sans y perdre de durabilite.
|
||||
Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection);
|
||||
return EHarvestResult::AlreadyDepleted;
|
||||
}
|
||||
|
||||
if (!AcceptsTool(Tool))
|
||||
{
|
||||
Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection);
|
||||
return EHarvestResult::WrongTool;
|
||||
}
|
||||
|
||||
CurrentHealth -= Power;
|
||||
|
||||
// Le butin par coup part AVANT le test d'epuisement : le coup qui abat rend
|
||||
// donc les deux tables. C'est ce que le joueur attend -- son dernier coup a
|
||||
// bien entaille le tronc en plus de le faire tomber.
|
||||
GrantDrops(HarvestData->PerHitDrops, Harvester);
|
||||
|
||||
if (CurrentHealth > 0.f)
|
||||
{
|
||||
Multicast_PlayHarvestFeedback(EHarvestFeedback::Hit, ImpactPoint, HitDirection);
|
||||
return EHarvestResult::Harvested;
|
||||
}
|
||||
|
||||
GrantDrops(HarvestData->DepletionDrops, Harvester);
|
||||
Multicast_PlayHarvestFeedback(EHarvestFeedback::FinalBlow, ImpactPoint, HitDirection);
|
||||
Deplete();
|
||||
|
||||
return EHarvestResult::Depleted;
|
||||
}
|
||||
|
||||
int32 AHarvestableResource::GrantDrops(const TArray<FHarvestDrop>& Table, AFpsPlayer* Harvester)
|
||||
{
|
||||
int32 Granted = 0;
|
||||
|
||||
for (const FHarvestDrop& Drop : Table)
|
||||
{
|
||||
const int32 Quantity = Drop.Roll();
|
||||
if (Quantity <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Passe par le pawn et pas par son inventaire : ce qui ne rentre pas doit
|
||||
// tomber au sol, et un UInventoryComponent ne sait pas engendrer un acteur.
|
||||
Harvester->GiveItemOrDrop(Drop.Item, Quantity);
|
||||
++Granted;
|
||||
}
|
||||
|
||||
return Granted;
|
||||
}
|
||||
|
||||
void AHarvestableResource::Deplete()
|
||||
{
|
||||
bDepleted = true;
|
||||
|
||||
// Chez l'hote aucun OnRep ne se declenche : on applique nous-memes juste apres
|
||||
// avoir ecrit la propriete. Meme schema que SetCharacterAppearance.
|
||||
ApplyDepletionVisuals();
|
||||
|
||||
const float Delay = HarvestData ? HarvestData->RegrowDelay : 0.f;
|
||||
if (Delay <= 0.f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GetWorldTimerManager().SetTimer(RegrowTimer, this, &AHarvestableResource::Regrow, Delay, /*bLoop=*/false);
|
||||
}
|
||||
|
||||
void AHarvestableResource::Regrow()
|
||||
{
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentHealth = HarvestData ? HarvestData->Health : 0.f;
|
||||
bDepleted = false;
|
||||
ApplyDepletionVisuals();
|
||||
}
|
||||
|
||||
void AHarvestableResource::OnRep_Depleted()
|
||||
{
|
||||
ApplyDepletionVisuals();
|
||||
}
|
||||
|
||||
void AHarvestableResource::ApplyDepletionVisuals()
|
||||
{
|
||||
if (!bDepleted)
|
||||
{
|
||||
SetActorHiddenInGame(false);
|
||||
SetActorEnableCollision(true);
|
||||
|
||||
// IntactMesh peut etre nul le tout premier coup -- BeginPlay ne l'a pas
|
||||
// encore capture -- auquel cas il n'y a de toute facon rien a reposer.
|
||||
if (IntactMesh && Mesh->GetStaticMesh() != IntactMesh)
|
||||
{
|
||||
Mesh->SetStaticMesh(IntactMesh);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (DepletedMesh)
|
||||
{
|
||||
Mesh->SetStaticMesh(DepletedMesh);
|
||||
SetActorHiddenInGame(false);
|
||||
|
||||
// Une souche reste solide : on la garde visable, ce qui permet au coup
|
||||
// suivant de rencontrer quelque chose et de sonner « inefficace » plutot
|
||||
// que de traverser dans le vide.
|
||||
SetActorEnableCollision(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sans mesh de remplacement, la ressource disparait entierement. La collision
|
||||
// PART avec elle : la laisser donnerait un mur invisible en pleine foret.
|
||||
SetActorHiddenInGame(true);
|
||||
SetActorEnableCollision(false);
|
||||
}
|
||||
|
||||
void AHarvestableResource::Multicast_PlayHarvestFeedback_Implementation(EHarvestFeedback Feedback,
|
||||
FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection)
|
||||
{
|
||||
PlayHarvestFeedback(Feedback, ImpactPoint, HitDirection);
|
||||
}
|
||||
|
||||
void AHarvestableResource::PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint,
|
||||
const FVector& HitDirection)
|
||||
{
|
||||
if (!HarvestData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Un serveur dedie n'a ni haut-parleur ni ecran : les FX y seraient du travail
|
||||
// pur. En serveur d'ecoute la fenetre de l'hote est un client comme un autre,
|
||||
// donc elle passe cette garde.
|
||||
if (IsNetMode(NM_DedicatedServer))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
USoundBase* Sound = nullptr;
|
||||
UNiagaraSystem* Effect = nullptr;
|
||||
|
||||
switch (Feedback)
|
||||
{
|
||||
case EHarvestFeedback::Hit:
|
||||
Sound = HarvestData->HitSound;
|
||||
Effect = HarvestData->HitEffect;
|
||||
break;
|
||||
|
||||
case EHarvestFeedback::FinalBlow:
|
||||
// Le son de chute REMPLACE celui du coup, il ne s'y ajoute pas : deux
|
||||
// impacts sur la meme frame s'entendent comme un doublon, pas comme un
|
||||
// evenement plus fort.
|
||||
Sound = HarvestData->DepletionSound ? HarvestData->DepletionSound.Get() : HarvestData->HitSound.Get();
|
||||
Effect = HarvestData->DepletionEffect ? HarvestData->DepletionEffect.Get() : HarvestData->HitEffect.Get();
|
||||
break;
|
||||
|
||||
case EHarvestFeedback::Ineffective:
|
||||
Sound = HarvestData->IneffectiveHitSound;
|
||||
break;
|
||||
}
|
||||
|
||||
if (Sound)
|
||||
{
|
||||
UGameplayStatics::PlaySoundAtLocation(this, Sound, ImpactPoint);
|
||||
}
|
||||
|
||||
if (Effect)
|
||||
{
|
||||
// Les copeaux repartent VERS le joueur : -HitDirection. Oriente dans le sens
|
||||
// du coup, ils gicleraient dans le tronc et on ne verrait rien.
|
||||
UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, Effect, ImpactPoint,
|
||||
(-HitDirection).Rotation());
|
||||
}
|
||||
|
||||
// Pas de balancement sur un coup qui n'a rien fait : une souche qui oscille
|
||||
// sous un coup inefficace annonce exactement le contraire de ce qui se passe.
|
||||
if (Feedback != EHarvestFeedback::Ineffective)
|
||||
{
|
||||
StartShake(HitDirection);
|
||||
}
|
||||
}
|
||||
|
||||
void AHarvestableResource::StartShake(const FVector& HitDirection)
|
||||
{
|
||||
if (!HarvestData || HarvestData->HitShakeAngle <= 0.f || HarvestData->HitShakeDuration <= 0.f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// L'axe de rotation est perpendiculaire au coup ET a la verticale : l'arbre
|
||||
// penche DANS le sens ou on l'a frappe. Le produit vectoriel degenere quand on
|
||||
// frappe a la verticale -- de haut en bas sur un rocher -- d'ou le repli.
|
||||
FVector WorldAxis = FVector::CrossProduct(FVector::UpVector, HitDirection);
|
||||
if (!WorldAxis.Normalize())
|
||||
{
|
||||
WorldAxis = GetActorRightVector();
|
||||
}
|
||||
|
||||
// En espace local : le mesh tourne dans le repere de son parent, et un arbre
|
||||
// pose avec un yaw quelconque pencherait de travers si l'axe restait en monde.
|
||||
ShakeAxis = GetActorTransform().InverseTransformVectorNoScale(WorldAxis).GetSafeNormal();
|
||||
|
||||
// Reparti de zero meme si un balancement tournait deja : marteler l'arbre doit
|
||||
// relancer l'oscillation a pleine amplitude, pas l'accumuler.
|
||||
ShakeElapsed = 0.f;
|
||||
SetActorTickEnabled(true);
|
||||
}
|
||||
|
||||
void AHarvestableResource::Tick(float DeltaTime)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
|
||||
if (ShakeElapsed < 0.f || !HarvestData)
|
||||
{
|
||||
SetActorTickEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ShakeElapsed += DeltaTime;
|
||||
|
||||
const float Duration = HarvestData->HitShakeDuration;
|
||||
if (ShakeElapsed >= Duration)
|
||||
{
|
||||
// On repose la rotation d'origine EXACTEMENT plutot que de laisser la
|
||||
// derniere valeur calculee : a un cheveu pres, l'arbre resterait penche, et
|
||||
// vingt coups plus tard il pousserait franchement de travers.
|
||||
Mesh->SetRelativeRotation(BaseMeshRotation);
|
||||
ShakeElapsed = -1.f;
|
||||
SetActorTickEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const float Alpha = ShakeElapsed / Duration;
|
||||
|
||||
// Amortissement au carre plutot que lineaire : l'oscillation meurt vite, ce qui
|
||||
// donne le poids d'un tronc. Lineaire, elle traine et l'arbre parait mou.
|
||||
const float Damping = FMath::Square(1.f - Alpha);
|
||||
|
||||
const float Angle = HarvestData->HitShakeAngle * Damping
|
||||
* FMath::Sin(2.f * PI * HarvestData->HitShakeFrequency * ShakeElapsed);
|
||||
|
||||
// Composition par quaternion et pas addition de FRotator : additionner deux
|
||||
// rotations d'Euler n'est une composition correcte que par chance, et l'arbre
|
||||
// pose avec un pitch non nul partirait de travers.
|
||||
const FQuat Offset(ShakeAxis, FMath::DegreesToRadians(Angle));
|
||||
Mesh->SetRelativeRotation(Offset * BaseMeshRotation.Quaternion());
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "HarvestingComponent.h"
|
||||
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Engine/World.h"
|
||||
#include "FpsPlayer.h"
|
||||
#include "GameFramework/Controller.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
#include "HarvestableResource.h"
|
||||
#include "ItemDataAsset.h"
|
||||
|
||||
UHarvestingComponent::UHarvestingComponent()
|
||||
{
|
||||
// Aucun Tick : ce composant ne travaille qu'au moment du coup, une poignee de
|
||||
// fois par minute. C'est toute la difference avec UInteractionComponent, qui
|
||||
// doit entretenir un prompt a l'ecran.
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
|
||||
// Pas de SetIsReplicatedByDefault : aucune RPC ne vit ici, elles sont sur le
|
||||
// pawn. Voir le commentaire de classe.
|
||||
}
|
||||
|
||||
bool UHarvestingComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const
|
||||
{
|
||||
const APawn* OwnerPawn = Cast<APawn>(GetOwner());
|
||||
if (!OwnerPawn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Par le controller et pas par la camera : GetPlayerViewPoint renvoie
|
||||
// exactement ce que le joueur a a l'ecran, secousses et balancement de tete
|
||||
// compris. Le coup part donc pile du centre du viseur.
|
||||
if (const AController* OwnerController = OwnerPawn->GetController())
|
||||
{
|
||||
OwnerController->GetPlayerViewPoint(OutLocation, OutRotation);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
AActor* UHarvestingComponent::TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const
|
||||
{
|
||||
OutImpactPoint = FVector::ZeroVector;
|
||||
OutDirection = FVector::ForwardVector;
|
||||
|
||||
FVector ViewLocation;
|
||||
FRotator ViewRotation;
|
||||
if (!GetViewPoint(ViewLocation, ViewRotation))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
OutDirection = ViewRotation.Vector();
|
||||
const FVector TraceEnd = ViewLocation + OutDirection * SwingRange;
|
||||
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(HarvestSwingTrace), /*bTraceComplex=*/false, GetOwner());
|
||||
|
||||
FHitResult Hit;
|
||||
const bool bHit = GetWorld()->SweepSingleByChannel(
|
||||
Hit,
|
||||
ViewLocation,
|
||||
TraceEnd,
|
||||
FQuat::Identity,
|
||||
SwingChannel,
|
||||
FCollisionShape::MakeSphere(SwingRadius),
|
||||
Params);
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDrawDebug)
|
||||
{
|
||||
const FColor Color = bHit ? FColor::Green : FColor::Red;
|
||||
DrawDebugLine(GetWorld(), ViewLocation, TraceEnd, Color, false, 1.5f, 0, 1.f);
|
||||
if (bHit)
|
||||
{
|
||||
DrawDebugSphere(GetWorld(), Hit.ImpactPoint, SwingRadius, 12, Color, false, 1.5f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!bHit)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ImpactPoint et pas Location : Location est le centre de la sphere balayee a
|
||||
// l'arret, donc a SwingRadius de la surface. Les copeaux en jailliraient a
|
||||
// vingt-cinq centimetres du tronc, en plein air.
|
||||
OutImpactPoint = Hit.ImpactPoint;
|
||||
|
||||
return Hit.GetActor();
|
||||
}
|
||||
|
||||
float UHarvestingComponent::GetSwingPower(const UItemDataAsset* Tool) const
|
||||
{
|
||||
// Un objet qui n'est pas un outil frappe comme un poing : tenir une pomme ne
|
||||
// doit ni empecher de cueillir une baie, ni aider a abattre un arbre.
|
||||
return (Tool && Tool->IsTool()) ? Tool->HarvestPower : UnarmedHarvestPower;
|
||||
}
|
||||
|
||||
EHarvestResult UHarvestingComponent::ApplySwing(AActor* Target, const UItemDataAsset* Tool,
|
||||
const FVector& ImpactPoint, const FVector& HitDirection)
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner || !Owner->HasAuthority())
|
||||
{
|
||||
return EHarvestResult::Invalid;
|
||||
}
|
||||
|
||||
AHarvestableResource* Resource = Cast<AHarvestableResource>(Target);
|
||||
if (!IsValid(Resource))
|
||||
{
|
||||
// Le cas normal, et de loin : la plupart des coups tombent sur le sol, un
|
||||
// mur, ou rien du tout. Ce n'est pas une erreur, juste un coup dans le vide.
|
||||
return EHarvestResult::Invalid;
|
||||
}
|
||||
|
||||
// Le client a vise il y a un aller-retour : entre-temps il a bouge, et la
|
||||
// position que le serveur connait de lui est en retard. On revalide donc, avec
|
||||
// la meme portee elargie par ServerRangeTolerance.
|
||||
//
|
||||
// La distance se mesure a la BOITE ENGLOBANTE de la cible et pas a son origine :
|
||||
// le pivot d'un arbre de pack est au SOL, donc frapper le tronc a deux metres de
|
||||
// haut mesurerait une distance bien plus grande que la portee du coup. Le
|
||||
// symptome serait un grand arbre impossible a abattre pour un client, et
|
||||
// parfaitement normal chez l'hote.
|
||||
const FBox TargetBounds = Resource->GetComponentsBoundingBox(/*bNonColliding=*/true);
|
||||
const float MaxDistanceSq = FMath::Square(SwingRange * ServerRangeTolerance);
|
||||
|
||||
if (TargetBounds.ComputeSquaredDistanceToPoint(Owner->GetActorLocation()) > MaxDistanceSq)
|
||||
{
|
||||
UE_LOG(LogTemp, Verbose, TEXT("UHarvestingComponent : %s a frappe %s, hors de portee cote serveur."),
|
||||
*GetNameSafe(Owner), *GetNameSafe(Resource));
|
||||
return EHarvestResult::Invalid;
|
||||
}
|
||||
|
||||
AFpsPlayer* Harvester = Cast<AFpsPlayer>(Owner);
|
||||
if (!Harvester)
|
||||
{
|
||||
return EHarvestResult::Invalid;
|
||||
}
|
||||
|
||||
return Resource->TryHarvest(Harvester, Tool, GetSwingPower(Tool), ImpactPoint, HitDirection);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user