359 lines
11 KiB
C++
359 lines
11 KiB
C++
// 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());
|
|
}
|