(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
+116 -21
View File
@@ -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