(Feat) Add Animation Fps Charater
This commit is contained in:
@@ -0,0 +1,900 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "CookingStation.h"
|
||||
|
||||
#include "Components/PointLightComponent.h"
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "ItemDataAsset.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "NiagaraComponent.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "CookingStation"
|
||||
|
||||
ACookingStation::ACookingStation()
|
||||
{
|
||||
// Tout passe par des timers : la station n'a rien a faire chaque frame.
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
// Acteur du monde. La cuisson et la combustion ne sont comptees QUE par le
|
||||
// serveur : les timers ne partent jamais sur le reseau, seuls leurs
|
||||
// resultats -- l'objet devenu cuit, la buche disparue -- sont repliques.
|
||||
// Quatre clients qui compteraient chacun leur cuisson divergeraient, et le
|
||||
// steak serait brule chez l'un et cru chez l'autre.
|
||||
bReplicates = true;
|
||||
SetReplicateMovement(false);
|
||||
|
||||
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
|
||||
SetRootComponent(BaseMesh);
|
||||
// Le foyer est un obstacle physique, contrairement a un ramassable : on
|
||||
// n'entre pas dans un feu de camp.
|
||||
BaseMesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
|
||||
InteractionSphere = CreateDefaultSubobject<USphereComponent>(TEXT("InteractionSphere"));
|
||||
InteractionSphere->SetupAttachment(BaseMesh);
|
||||
InteractionSphere->InitSphereRadius(90.f);
|
||||
InteractionSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
InteractionSphere->SetCollisionResponseToAllChannels(ECR_Ignore);
|
||||
InteractionSphere->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
|
||||
|
||||
FireEffect = CreateDefaultSubobject<UNiagaraComponent>(TEXT("FireEffect"));
|
||||
FireEffect->SetupAttachment(BaseMesh);
|
||||
// Surtout pas d'auto-activation : un feu neuf n'a pas de bois, il ne doit
|
||||
// pas flamber une frame avant que BeginPlay ne le coupe.
|
||||
FireEffect->SetAutoActivate(false);
|
||||
|
||||
FireLight = CreateDefaultSubobject<UPointLightComponent>(TEXT("FireLight"));
|
||||
FireLight->SetupAttachment(BaseMesh);
|
||||
FireLight->SetRelativeLocation(FVector(0.f, 0.f, 40.f));
|
||||
|
||||
// Movable, sinon rien ne fonctionne : une lumiere Static -- le defaut d'un
|
||||
// composant lumiere -- est cuite dans le lightmap, et l'allumer ou
|
||||
// l'eteindre a l'execution n'a strictement aucun effet a l'ecran.
|
||||
FireLight->SetMobility(EComponentMobility::Movable);
|
||||
|
||||
FireLight->SetIntensity(3000.f);
|
||||
FireLight->SetAttenuationRadius(600.f);
|
||||
FireLight->SetLightColor(FLinearColor(1.f, 0.55f, 0.2f));
|
||||
|
||||
// Une lumiere ponctuelle projette ses ombres sur SIX faces de cubemap, a
|
||||
// chaque frame et pour chaque objet a portee. Sur un feu de camp qu'on
|
||||
// posera par dizaines, c'est le poste le plus cher du systeme, et de loin.
|
||||
// Coche-la sur le feu principal du campement si tu la veux, pas partout.
|
||||
FireLight->CastShadows = false;
|
||||
|
||||
FireLight->SetVisibility(false);
|
||||
|
||||
SlotMeshes.Reserve(MaxCookingSlots);
|
||||
for (int32 Index = 0; Index < MaxCookingSlots; ++Index)
|
||||
{
|
||||
const FName ComponentName(*FString::Printf(TEXT("CookSlot_%d"), Index));
|
||||
|
||||
UStaticMeshComponent* SlotMesh = CreateDefaultSubobject<UStaticMeshComponent>(ComponentName);
|
||||
SlotMesh->SetupAttachment(BaseMesh);
|
||||
SlotMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
// Visible dans l'editeur pour que tu puisses positionner l'emplacement a
|
||||
// la souris avec un mesh de reperage ; le jeu le cache au demarrage tant
|
||||
// qu'il n'y a rien dessus.
|
||||
SlotMesh->SetHiddenInGame(false);
|
||||
|
||||
SlotMeshes.Add(SlotMesh);
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
DOREPLIFETIME(ACookingStation, Slots);
|
||||
DOREPLIFETIME(ACookingStation, Fuel);
|
||||
DOREPLIFETIME(ACookingStation, bIsLit);
|
||||
}
|
||||
|
||||
void ACookingStation::OnRep_Slots()
|
||||
{
|
||||
// Le mesh d'un emplacement suit son contenu : c'est TOUT l'affichage de la
|
||||
// cuisson, il n'y a aucune interface. Cru, cuit, brule -- le joueur ne lit
|
||||
// que ca.
|
||||
for (int32 Index = 0; Index < Slots.Num(); ++Index)
|
||||
{
|
||||
RefreshSlotMesh(Index);
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::OnRep_Fuel()
|
||||
{
|
||||
RefreshFuelIndicators();
|
||||
}
|
||||
|
||||
void ACookingStation::OnRep_IsLit()
|
||||
{
|
||||
ApplyFireVisuals(bIsLit);
|
||||
}
|
||||
|
||||
void ACookingStation::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Recensement des buches AVANT tout : il fixe la capacite, et il est purement
|
||||
// local -- ce sont des composants du Blueprint, identiques partout.
|
||||
CacheFuelIndicators();
|
||||
|
||||
// Le contenu initial est de l'etat de monde : seul le serveur le pose, les
|
||||
// clients le recevront par replication. Sans cette garde, chaque client se
|
||||
// donnerait ses propres buches de depart en plus de celles du serveur.
|
||||
if (HasAuthority())
|
||||
{
|
||||
Slots.SetNum(MaxCookingSlots);
|
||||
|
||||
if (StartingFuelItem && StartingFuelItem->bIsFuel)
|
||||
{
|
||||
const int32 Count = FMath::Min(StartingFuelCount, GetFuelCapacity());
|
||||
for (int32 Index = 0; Index < Count; ++Index)
|
||||
{
|
||||
FFuelSlotState& NewFuel = Fuel.AddDefaulted_GetRef();
|
||||
NewFuel.Item = StartingFuelItem;
|
||||
NewFuel.RemainingSeconds = FMath::Max(1.f, StartingFuelItem->FuelDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Les meshes de reperage poses dans le Blueprint n'ont plus rien a faire la :
|
||||
// RefreshSlotMesh remet chaque emplacement sur son contenu reel, donc rien.
|
||||
// Chez un client Slots est encore vide a cet instant ; OnRep_Slots repassera
|
||||
// des que le contenu reel arrivera.
|
||||
for (int32 Index = 0; Index < Slots.Num(); ++Index)
|
||||
{
|
||||
RefreshSlotMesh(Index);
|
||||
}
|
||||
|
||||
RefreshFuelIndicators();
|
||||
|
||||
// Le grill est vide au demarrage, donc le feu reste eteint quoi qu'il
|
||||
// arrive. On passe quand meme par le point de decision unique plutot que
|
||||
// d'ecrire un cas particulier de plus.
|
||||
ApplyFireVisuals(false);
|
||||
|
||||
if (HasAuthority())
|
||||
{
|
||||
UpdateFireState();
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
// Les timers portent un delegue vers this. Les laisser tourner apres la
|
||||
// destruction de l'acteur est le genre de detail qui ne se voit qu'au
|
||||
// changement de niveau, sous la forme d'un crash sans pile lisible.
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
FTimerManager& TimerManager = World->GetTimerManager();
|
||||
TimerManager.ClearTimer(FuelTimer);
|
||||
|
||||
for (FCookingSlotState& Slot : Slots)
|
||||
{
|
||||
TimerManager.ClearTimer(Slot.Timer);
|
||||
}
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Carburant et allumage
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
float ACookingStation::GetFuelRemaining() const
|
||||
{
|
||||
float Total = 0.f;
|
||||
|
||||
for (int32 Index = 0; Index < Fuel.Num(); ++Index)
|
||||
{
|
||||
// Feu allume, c'est le timer qui fait autorite pour la buche en cours :
|
||||
// son RemainingSeconds n'est rafraichi qu'a l'extinction.
|
||||
if (Index == 0 && bIsLit)
|
||||
{
|
||||
Total += FMath::Max(0.f, GetWorldTimerManager().GetTimerRemaining(FuelTimer));
|
||||
}
|
||||
else
|
||||
{
|
||||
Total += Fuel[Index].RemainingSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
return Total;
|
||||
}
|
||||
|
||||
void ACookingStation::SyncFuelFromTimer()
|
||||
{
|
||||
if (bIsLit && Fuel.Num() > 0)
|
||||
{
|
||||
Fuel[0].RemainingSeconds = FMath::Max(0.f, GetWorldTimerManager().GetTimerRemaining(FuelTimer));
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::StartFuelTimer()
|
||||
{
|
||||
GetWorldTimerManager().ClearTimer(FuelTimer);
|
||||
|
||||
if (!bIsLit || Fuel.Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float Duration = FMath::Max(0.05f, Fuel[0].RemainingSeconds);
|
||||
GetWorldTimerManager().SetTimer(FuelTimer, this, &ACookingStation::HandleFuelConsumed, Duration, false);
|
||||
}
|
||||
|
||||
bool ACookingStation::Light()
|
||||
{
|
||||
if (bIsLit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Fuel.Num() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bIsLit = true;
|
||||
StartFuelTimer();
|
||||
|
||||
// Reprise, pas redemarrage : chaque cuisson repart exactement la ou elle
|
||||
// s'etait figee quand le feu s'est eteint.
|
||||
for (const FCookingSlotState& Slot : Slots)
|
||||
{
|
||||
GetWorldTimerManager().UnPauseTimer(Slot.Timer);
|
||||
}
|
||||
|
||||
ApplyFireVisuals(true);
|
||||
UGameplayStatics::PlaySoundAtLocation(this, LightSound, GetActorLocation());
|
||||
|
||||
OnStationChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ACookingStation::Extinguish()
|
||||
{
|
||||
if (!bIsLit)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SyncFuelFromTimer();
|
||||
GetWorldTimerManager().ClearTimer(FuelTimer);
|
||||
bIsLit = false;
|
||||
|
||||
for (const FCookingSlotState& Slot : Slots)
|
||||
{
|
||||
GetWorldTimerManager().PauseTimer(Slot.Timer);
|
||||
}
|
||||
|
||||
ApplyFireVisuals(false);
|
||||
UGameplayStatics::PlaySoundAtLocation(this, ExtinguishSound, GetActorLocation());
|
||||
|
||||
OnStationChanged.Broadcast();
|
||||
}
|
||||
|
||||
bool ACookingStation::HasPendingWork() const
|
||||
{
|
||||
for (const FCookingSlotState& Slot : Slots)
|
||||
{
|
||||
float Unused = 0.f;
|
||||
if (!Slot.IsEmpty() && GetNextStage(Slot.Item, Unused))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ACookingStation::UpdateFireState()
|
||||
{
|
||||
// Point de decision unique du feu, donc point de garde unique : bIsLit et
|
||||
// les timers de combustion appartiennent au serveur. Chez un client c'est
|
||||
// OnRep_IsLit qui allume la flamme, sans jamais rien decider.
|
||||
if (!HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Un objet DEJA cuit qui peut encore bruler compte comme du travail : sans
|
||||
// ca le feu s'arreterait pile a la fin de la cuisson et la sur-cuisson
|
||||
// n'arriverait jamais. Oublier sa viande doit rester punitif.
|
||||
if (Fuel.Num() > 0 && HasPendingWork())
|
||||
{
|
||||
Light();
|
||||
}
|
||||
else
|
||||
{
|
||||
Extinguish();
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::HandleFuelConsumed()
|
||||
{
|
||||
if (Fuel.Num() > 0)
|
||||
{
|
||||
// RemoveAt et non un index de lecture qui avance : le foyer doit
|
||||
// toujours se lire "les N premieres buches sont pleines", c'est ce qui
|
||||
// rend l'affichage trivial et sans etat supplementaire.
|
||||
Fuel.RemoveAt(0);
|
||||
}
|
||||
|
||||
RefreshFuelIndicators();
|
||||
|
||||
if (Fuel.Num() == 0)
|
||||
{
|
||||
Extinguish();
|
||||
return;
|
||||
}
|
||||
|
||||
StartFuelTimer();
|
||||
OnStationChanged.Broadcast();
|
||||
}
|
||||
|
||||
int32 ACookingStation::FindFuelSource(const UInventoryComponent* Inventory, UItemDataAsset*& OutItem)
|
||||
{
|
||||
OutItem = nullptr;
|
||||
|
||||
if (!Inventory)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
// La main d'abord : c'est le seul moyen pour le joueur de designer ce qu'il
|
||||
// accepte de bruler. Sans cette priorite, remplir le feu piocherait la
|
||||
// premiere chose venue et ses planches partiraient avant ses branches.
|
||||
const FInventorySlot Selected = Inventory->GetSelectedSlot();
|
||||
if (Selected.Item && Selected.Item->bIsFuel)
|
||||
{
|
||||
OutItem = Selected.Item;
|
||||
return Inventory->GetSelectedHotbarIndex();
|
||||
}
|
||||
|
||||
const TArray<FInventorySlot>& InventorySlots = Inventory->GetSlots();
|
||||
for (int32 Index = 0; Index < InventorySlots.Num(); ++Index)
|
||||
{
|
||||
if (!InventorySlots[Index].IsEmpty() && InventorySlots[Index].Item->bIsFuel)
|
||||
{
|
||||
OutItem = InventorySlots[Index].Item;
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
bool ACookingStation::AddFuelFrom(int32 InventorySlotIndex, AActor* Interactor)
|
||||
{
|
||||
UInventoryComponent* Inventory = GetInventoryOf(Interactor);
|
||||
if (!Inventory || Fuel.Num() >= GetFuelCapacity())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Un seul exemplaire, jamais la pile : c'est le joueur qui decide combien
|
||||
// de bois il engage, buche par buche.
|
||||
FInventorySlot Taken;
|
||||
if (!Inventory->TakeFromSlot(InventorySlotIndex, 1, Taken) || Taken.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FFuelSlotState& NewFuel = Fuel.AddDefaulted_GetRef();
|
||||
NewFuel.Item = Taken.Item;
|
||||
NewFuel.RemainingSeconds = FMath::Max(1.f, Taken.Item->FuelDuration);
|
||||
|
||||
RefreshFuelIndicators();
|
||||
|
||||
// Du bois arrive alors qu'une cuisson attendait : c'est ce qui relance le
|
||||
// feu. Si rien n'attend, la buche patiente sans se consumer.
|
||||
UpdateFireState();
|
||||
|
||||
UGameplayStatics::PlaySoundAtLocation(this, PlaceItemSound, GetActorLocation());
|
||||
|
||||
OnStationChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ACookingStation::ApplyFireVisuals(bool bActive)
|
||||
{
|
||||
if (bActive)
|
||||
{
|
||||
FireEffect->Activate(/*bReset=*/true);
|
||||
}
|
||||
else
|
||||
{
|
||||
FireEffect->Deactivate();
|
||||
}
|
||||
|
||||
FireLight->SetVisibility(bActive);
|
||||
}
|
||||
|
||||
void ACookingStation::CacheFuelIndicators()
|
||||
{
|
||||
TArray<UStaticMeshComponent*> Found;
|
||||
GetComponents<UStaticMeshComponent>(Found);
|
||||
|
||||
FuelIndicators.Reset();
|
||||
for (UStaticMeshComponent* Component : Found)
|
||||
{
|
||||
if (Component->ComponentHasTag(FuelIndicatorTag))
|
||||
{
|
||||
FuelIndicators.Add(Component);
|
||||
}
|
||||
}
|
||||
|
||||
// GetComponents ne promet aucun ordre. Sans tri, la buche qui disparait en
|
||||
// premier changerait d'une compilation du Blueprint a l'autre -- un bug
|
||||
// qu'on ne reproduit jamais deux fois pareil.
|
||||
//
|
||||
// TArray::Sort DEREFERENCE les elements des qu'il s'agit de pointeurs,
|
||||
// TObjectPtr compris : le predicat recoit les composants, pas les pointeurs.
|
||||
//
|
||||
// Tri alphabetique, donc Log_10 passerait AVANT Log_2 : au-dela de neuf
|
||||
// buches, nomme-les Log_01, Log_02...
|
||||
FuelIndicators.Sort([](const UStaticMeshComponent& A, const UStaticMeshComponent& B)
|
||||
{
|
||||
return A.GetName().Compare(B.GetName()) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
void ACookingStation::RefreshFuelIndicators()
|
||||
{
|
||||
for (int32 Index = 0; Index < FuelIndicators.Num(); ++Index)
|
||||
{
|
||||
const bool bOccupied = Fuel.IsValidIndex(Index);
|
||||
FuelIndicators[Index]->SetVisibility(bOccupied);
|
||||
|
||||
if (bOccupied && bFuelIndicatorsUseItemMesh && Fuel[Index].Item)
|
||||
{
|
||||
Fuel[Index].Item->ApplyWorldVisualsTo(FuelIndicators[Index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Cuisson
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
UItemDataAsset* ACookingStation::GetNextStage(const UItemDataAsset* Item, float& OutDuration)
|
||||
{
|
||||
OutDuration = 0.f;
|
||||
|
||||
if (!Item)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Le test "!= Item" n'est pas de la paranoia : un resultat qui pointe sur
|
||||
// l'objet lui-meme fait tourner le timer indefiniment sans que rien ne
|
||||
// change a l'ecran. La viande "cuit" en boucle, reste crue, et le feu ne
|
||||
// s'arrete jamais puisqu'il croit avoir du travail. Vu en jeu, ca ressemble
|
||||
// a un timer casse, et on cherche du cote du code pendant une heure.
|
||||
//
|
||||
// La cuisson d'abord : un objet cru qui porterait aussi bCanBurn doit
|
||||
// devenir cuit avant de se gacher, jamais l'inverse.
|
||||
if (Item->bCanBeCooked && Item->CookedResult && Item->CookedResult != Item)
|
||||
{
|
||||
OutDuration = Item->CookDuration;
|
||||
return Item->CookedResult;
|
||||
}
|
||||
|
||||
if (Item->bCanBurn && Item->BurntResult && Item->BurntResult != Item)
|
||||
{
|
||||
OutDuration = Item->BurnDuration;
|
||||
return Item->BurntResult;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ACookingStation::StartSlotTimer(int32 SlotIndex)
|
||||
{
|
||||
FCookingSlotState& Slot = Slots[SlotIndex];
|
||||
|
||||
GetWorldTimerManager().ClearTimer(Slot.Timer);
|
||||
|
||||
if (Slot.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float Duration = 0.f;
|
||||
if (!GetNextStage(Slot.Item, Duration))
|
||||
{
|
||||
// Un objet qui se DIT transformable sans avoir d'etape suivante
|
||||
// exploitable n'est pas une fin de chaine, c'est une donnee cassee.
|
||||
// Sans ce message il resterait inerte sur le grill, et rien a l'ecran
|
||||
// ne dirait que le probleme est dans l'asset et pas dans le feu.
|
||||
if (Slot.Item->bCanBeCooked || Slot.Item->bCanBurn)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("ACookingStation : %s se declare transformable mais n'a aucune etape suivante valide (resultat vide, ou pointant sur lui-meme). Il restera inerte sur le feu."),
|
||||
*GetNameSafe(Slot.Item));
|
||||
}
|
||||
|
||||
// Maillon final -- un steak brule -- : il reste sur le grill sans plus
|
||||
// rien faire, et sans consommer un timer pour ne rien faire.
|
||||
return;
|
||||
}
|
||||
|
||||
Duration = FMath::Max(0.05f, Duration / CookSpeedMultiplier);
|
||||
|
||||
const FTimerDelegate Finished = FTimerDelegate::CreateUObject(this, &ACookingStation::HandleSlotFinished, SlotIndex);
|
||||
GetWorldTimerManager().SetTimer(Slot.Timer, Finished, Duration, false);
|
||||
|
||||
// Poser quelque chose sur un feu eteint est autorise : le compte a rebours
|
||||
// existe, il attend simplement qu'on allume.
|
||||
if (!bIsLit)
|
||||
{
|
||||
GetWorldTimerManager().PauseTimer(Slot.Timer);
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::HandleSlotFinished(int32 SlotIndex)
|
||||
{
|
||||
if (!Slots.IsValidIndex(SlotIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FCookingSlotState& Slot = Slots[SlotIndex];
|
||||
|
||||
float Unused = 0.f;
|
||||
UItemDataAsset* NextItem = GetNextStage(Slot.Item, Unused);
|
||||
if (!NextItem)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Slot.Item = NextItem;
|
||||
RefreshSlotMesh(SlotIndex);
|
||||
|
||||
// Enchainement automatique : le steak cuit qui porte bCanBurn se remet a
|
||||
// compter tout seul vers le brule. C'est ce qui fait que le joueur doit
|
||||
// revenir chercher sa viande au lieu de l'oublier sur le feu.
|
||||
StartSlotTimer(SlotIndex);
|
||||
|
||||
// Apres StartSlotTimer : si ce maillon etait le dernier, il n'y a plus rien
|
||||
// a transformer et le feu s'arrete de lui-meme.
|
||||
UpdateFireState();
|
||||
|
||||
OnStationChanged.Broadcast();
|
||||
}
|
||||
|
||||
float ACookingStation::GetSlotTimeRemaining(int32 SlotIndex) const
|
||||
{
|
||||
if (!Slots.IsValidIndex(SlotIndex))
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
// GetTimerRemaining renvoie -1 quand le handle ne designe aucun timer actif.
|
||||
return FMath::Max(0.f, GetWorldTimerManager().GetTimerRemaining(Slots[SlotIndex].Timer));
|
||||
}
|
||||
|
||||
bool ACookingStation::CanTakeFromSlot(int32 SlotIndex) const
|
||||
{
|
||||
if (!Slots.IsValidIndex(SlotIndex) || Slots[SlotIndex].IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bAllowTakingRawItems)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const UItemDataAsset* Item = Slots[SlotIndex].Item;
|
||||
|
||||
// Critere porte par l'objet lui-meme, sans etat a maintenir : tant qu'il
|
||||
// sait encore CUIRE, il est cru, donc engage. Des qu'il ne sait plus que
|
||||
// bruler, c'est qu'il a fini sa cuisson et qu'on peut le sauver.
|
||||
//
|
||||
// Consequence voulue : reposer une viande cuite sur le feu ne la reverrouille
|
||||
// pas, alors qu'un booleen "a deja transforme" l'aurait fait.
|
||||
if (!Item->bCanBeCooked)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Second filet : un objet dont la chaine est cassee ne cuira jamais. Le
|
||||
// laisser verrouille le sequestrerait pour toujours sur le grill, avec pour
|
||||
// seul recours de recharger la partie.
|
||||
float Unused = 0.f;
|
||||
return GetNextStage(Item, Unused) == nullptr;
|
||||
}
|
||||
|
||||
void ACookingStation::RefreshSlotMesh(int32 SlotIndex)
|
||||
{
|
||||
UStaticMeshComponent* SlotMesh = SlotMeshes[SlotIndex];
|
||||
const FCookingSlotState& Slot = Slots[SlotIndex];
|
||||
|
||||
if (Slot.IsEmpty() || !Slot.Item->WorldMesh)
|
||||
{
|
||||
SlotMesh->SetStaticMesh(nullptr);
|
||||
SlotMesh->SetVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mesh ET materiau : c'est souvent le materiau seul qui distingue le cru du
|
||||
// cuit quand les deux etapes partagent la meme geometrie.
|
||||
Slot.Item->ApplyWorldVisualsTo(SlotMesh);
|
||||
SlotMesh->SetRelativeScale3D(FVector(CookingMeshScale));
|
||||
SlotMesh->SetVisibility(true);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Visee et decision
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
int32 ACookingStation::ResolveAimedSlot(const AActor* Interactor) const
|
||||
{
|
||||
if (!Interactor)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
FVector ViewLocation;
|
||||
FRotator ViewRotation;
|
||||
// GetActorEyesViewPoint : sur un pawn possede, il renvoie le point de vue du
|
||||
// controller, donc exactement ce que le joueur voit. On evite ainsi de
|
||||
// dependre d'AFpsPlayer -- n'importe quel acteur pourra cuisiner.
|
||||
Interactor->GetActorEyesViewPoint(ViewLocation, ViewRotation);
|
||||
|
||||
const FVector ViewDirection = ViewRotation.Vector();
|
||||
const float MinDot = FMath::Cos(FMath::DegreesToRadians(SlotAimHalfAngle));
|
||||
|
||||
int32 BestIndex = INDEX_NONE;
|
||||
float BestDot = MinDot;
|
||||
|
||||
const int32 UsedSlots = FMath::Min(CookingSlotCount, MaxCookingSlots);
|
||||
for (int32 Index = 0; Index < UsedSlots; ++Index)
|
||||
{
|
||||
const FVector ToSlot = SlotMeshes[Index]->GetComponentLocation() - ViewLocation;
|
||||
if (ToSlot.IsNearlyZero())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Produit scalaire et non distance a l'ecran : c'est l'ecart ANGULAIRE
|
||||
// au centre du viseur qui compte, et il ne demande ni projection ni
|
||||
// acces au viewport. Sous le cone, aucun emplacement n'est vise et le
|
||||
// joueur s'adresse au foyer lui-meme.
|
||||
const float Dot = FVector::DotProduct(ToSlot.GetSafeNormal(), ViewDirection);
|
||||
if (Dot > BestDot)
|
||||
{
|
||||
BestDot = Dot;
|
||||
BestIndex = Index;
|
||||
}
|
||||
}
|
||||
|
||||
return BestIndex;
|
||||
}
|
||||
|
||||
UInventoryComponent* ACookingStation::GetInventoryOf(AActor* Interactor)
|
||||
{
|
||||
return Interactor ? Interactor->FindComponentByClass<UInventoryComponent>() : nullptr;
|
||||
}
|
||||
|
||||
FCookingInteraction ACookingStation::ResolveAction(AActor* Interactor) const
|
||||
{
|
||||
FCookingInteraction Result;
|
||||
Result.SlotIndex = ResolveAimedSlot(Interactor);
|
||||
|
||||
// Slots n'est dimensionne qu'au BeginPlay : un trace d'interaction qui
|
||||
// arriverait avant lirait hors du tableau.
|
||||
if (!Slots.IsValidIndex(Result.SlotIndex))
|
||||
{
|
||||
Result.SlotIndex = INDEX_NONE;
|
||||
}
|
||||
|
||||
const UInventoryComponent* Inventory = GetInventoryOf(Interactor);
|
||||
if (Inventory)
|
||||
{
|
||||
Result.HeldItem = Inventory->GetSelectedSlot().Item;
|
||||
Result.FuelSourceSlot = FindFuelSource(Inventory, Result.FuelItem);
|
||||
}
|
||||
|
||||
// 1. Un emplacement vise et occupe par quelque chose qu'on a le droit de
|
||||
// reprendre. Recuperer passe avant tout le reste, sinon on ne pourrait
|
||||
// jamais sortir sa viande en ayant du bois en main.
|
||||
if (CanTakeFromSlot(Result.SlotIndex))
|
||||
{
|
||||
Result.Action = ECookingAction::TakeItem;
|
||||
return Result;
|
||||
}
|
||||
|
||||
// 2. Un emplacement vise et libre, quelque chose de cuisinable en main.
|
||||
if (Result.SlotIndex != INDEX_NONE && Slots[Result.SlotIndex].IsEmpty()
|
||||
&& Result.HeldItem && Result.HeldItem->CanBePlacedOnFire())
|
||||
{
|
||||
Result.Action = ECookingAction::PlaceItem;
|
||||
return Result;
|
||||
}
|
||||
|
||||
// 3. Du bois quelque part dans l'inventaire et de la place dans le foyer.
|
||||
// Pas besoin de l'avoir en main : la main sert a CHOISIR le combustible,
|
||||
// pas a autoriser le geste.
|
||||
if (Result.FuelSourceSlot != INDEX_NONE && Fuel.Num() < GetFuelCapacity())
|
||||
{
|
||||
Result.Action = ECookingAction::AddFuel;
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Pas de quatrieme cas : allumer n'est plus un geste du joueur.
|
||||
return Result;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Actions
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
bool ACookingStation::PlaceItemFromHand(int32 SlotIndex, AActor* Interactor)
|
||||
{
|
||||
UInventoryComponent* Inventory = GetInventoryOf(Interactor);
|
||||
if (!Inventory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Un seul exemplaire, meme si la pile en contient vingt : un emplacement de
|
||||
// grill porte une piece, comme dans Raft.
|
||||
FInventorySlot Taken;
|
||||
if (!Inventory->TakeFromSlot(Inventory->GetSelectedHotbarIndex(), 1, Taken) || Taken.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FCookingSlotState& Slot = Slots[SlotIndex];
|
||||
Slot.Item = Taken.Item;
|
||||
Slot.RemainingUses = Taken.RemainingUses;
|
||||
|
||||
RefreshSlotMesh(SlotIndex);
|
||||
StartSlotTimer(SlotIndex);
|
||||
|
||||
// Poser quelque chose EST ce qui declenche le feu, s'il a du bois.
|
||||
UpdateFireState();
|
||||
|
||||
UGameplayStatics::PlaySoundAtLocation(this, PlaceItemSound, SlotMeshes[SlotIndex]->GetComponentLocation());
|
||||
|
||||
OnStationChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ACookingStation::TakeItemToInventory(int32 SlotIndex, AActor* Interactor)
|
||||
{
|
||||
UInventoryComponent* Inventory = GetInventoryOf(Interactor);
|
||||
if (!Inventory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FCookingSlotState& Slot = Slots[SlotIndex];
|
||||
|
||||
// On tente l'ajout AVANT de vider l'emplacement. Inventaire plein : la
|
||||
// nourriture reste sur le feu -- et continue donc de cuire, ce qui est la
|
||||
// bonne sanction plutot que de la faire disparaitre.
|
||||
const int32 Leftover = Inventory->AddItem(Slot.Item, 1, Slot.RemainingUses);
|
||||
if (Leftover > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GetWorldTimerManager().ClearTimer(Slot.Timer);
|
||||
Slot.Item = nullptr;
|
||||
Slot.RemainingUses = -1;
|
||||
|
||||
RefreshSlotMesh(SlotIndex);
|
||||
|
||||
// Le grill vide, plus rien ne justifie de bruler du bois.
|
||||
UpdateFireState();
|
||||
|
||||
OnStationChanged.Broadcast();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// IInteractable
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
bool ACookingStation::CanInteract_Implementation(AActor* Interactor) const
|
||||
{
|
||||
// Toujours vrai : meme sans rien a faire, le poste doit rester visable pour
|
||||
// que le prompt puisse annoncer son etat. C'est la seule interface qu'a le
|
||||
// joueur pour savoir ou en est sa cuisson et combien de bois il lui reste.
|
||||
// Le fait qu'une action soit possible ou non se dit par
|
||||
// IsInteractionAvailable, qui n'efface pas le texte.
|
||||
return true;
|
||||
}
|
||||
|
||||
FText ACookingStation::GetInteractionPrompt_Implementation(AActor* Interactor) const
|
||||
{
|
||||
const FCookingInteraction Interaction = ResolveAction(Interactor);
|
||||
|
||||
switch (Interaction.Action)
|
||||
{
|
||||
case ECookingAction::TakeItem:
|
||||
{
|
||||
const FText ItemName = Slots[Interaction.SlotIndex].Item->DisplayName;
|
||||
const float Remaining = GetSlotTimeRemaining(Interaction.SlotIndex);
|
||||
|
||||
// Exactement la formulation d'un objet au sol : une fois cuit, ce n'est
|
||||
// plus une manipulation de four, c'est un ramassage.
|
||||
if (!bShowRemainingTimeInPrompt || Remaining <= 0.f || !bIsLit)
|
||||
{
|
||||
return FText::Format(LOCTEXT("TakeDone", "Pick up {0}"), ItemName);
|
||||
}
|
||||
|
||||
return FText::Format(LOCTEXT("TakeDoneTimed", "Pick up {0} ({1}s)"),
|
||||
ItemName, FText::AsNumber(FMath::CeilToInt(Remaining)));
|
||||
}
|
||||
|
||||
case ECookingAction::PlaceItem:
|
||||
return FText::Format(LOCTEXT("PlaceOnFire", "Cook {0}"), Interaction.HeldItem->DisplayName);
|
||||
|
||||
case ECookingAction::AddFuel:
|
||||
return FText::Format(LOCTEXT("AddFuel", "Add {0} to the fire"), Interaction.FuelItem->DisplayName);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Aucune action possible : aucun texte. Tout ce qu'un commentaire d'etat
|
||||
// dirait -- ca cuit, il manque du bois, le foyer est plein -- se lit deja
|
||||
// sur les flammes, sur le nombre de buches et sur le mesh de la nourriture.
|
||||
// L'ecrire en plus, c'est reconstruire une interface par-dessus un monde
|
||||
// qui se suffit a lui-meme.
|
||||
return FText::GetEmpty();
|
||||
}
|
||||
|
||||
void ACookingStation::Interact_Implementation(AActor* Interactor)
|
||||
{
|
||||
// Meme fonction que le prompt : ce que le joueur vient de lire est
|
||||
// exactement ce qui va se produire.
|
||||
const FCookingInteraction Interaction = ResolveAction(Interactor);
|
||||
|
||||
switch (Interaction.Action)
|
||||
{
|
||||
case ECookingAction::TakeItem:
|
||||
TakeItemToInventory(Interaction.SlotIndex, Interactor);
|
||||
break;
|
||||
|
||||
case ECookingAction::PlaceItem:
|
||||
PlaceItemFromHand(Interaction.SlotIndex, Interactor);
|
||||
break;
|
||||
|
||||
case ECookingAction::AddFuel:
|
||||
AddFuelFrom(Interaction.FuelSourceSlot, Interactor);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::OnBeginFocus_Implementation()
|
||||
{
|
||||
if (bHighlightOnFocus)
|
||||
{
|
||||
BaseMesh->SetRenderCustomDepth(true);
|
||||
}
|
||||
}
|
||||
|
||||
void ACookingStation::OnEndFocus_Implementation()
|
||||
{
|
||||
if (bHighlightOnFocus)
|
||||
{
|
||||
BaseMesh->SetRenderCustomDepth(false);
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
Reference in New Issue
Block a user