(Feat) Add Cooking and Workbench
This commit is contained in:
@@ -0,0 +1,840 @@
|
||||
// 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 "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;
|
||||
|
||||
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::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
Slots.SetNum(MaxCookingSlots);
|
||||
|
||||
// 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.
|
||||
for (int32 Index = 0; Index < MaxCookingSlots; ++Index)
|
||||
{
|
||||
RefreshSlotMesh(Index);
|
||||
}
|
||||
|
||||
// Avant tout usage du carburant : c'est ce recensement qui fixe la capacite.
|
||||
CacheFuelIndicators();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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()
|
||||
{
|
||||
// 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
|
||||
@@ -19,8 +19,8 @@ void UCraftingComponent::InitializeComponent()
|
||||
{
|
||||
Super::InitializeComponent();
|
||||
|
||||
// Les mains sont toujours disponibles. Les postes s'ajoutent par-dessus.
|
||||
AvailableStations.Add(ECraftingStation::Hands);
|
||||
// Les mains sont toujours disponibles, et rien ne peut les retirer.
|
||||
StationRefCounts.Add(ECraftingStation::Hands, 1);
|
||||
|
||||
if (const AActor* Owner = GetOwner())
|
||||
{
|
||||
@@ -40,6 +40,12 @@ void UCraftingComponent::InitializeComponent()
|
||||
}
|
||||
}
|
||||
|
||||
bool UCraftingComponent::HasStation(ECraftingStation Station) const
|
||||
{
|
||||
const int32* Count = StationRefCounts.Find(Station);
|
||||
return Count && *Count > 0;
|
||||
}
|
||||
|
||||
void UCraftingComponent::AddAvailableStation(ECraftingStation Station)
|
||||
{
|
||||
if (Station == ECraftingStation::Count)
|
||||
@@ -47,12 +53,13 @@ void UCraftingComponent::AddAvailableStation(ECraftingStation Station)
|
||||
return;
|
||||
}
|
||||
|
||||
bool bAlreadyThere = false;
|
||||
AvailableStations.Add(Station, &bAlreadyThere);
|
||||
int32& Count = StationRefCounts.FindOrAdd(Station);
|
||||
++Count;
|
||||
|
||||
// On ne diffuse que sur un vrai changement : deux volumes d'etabli qui se
|
||||
// chevauchent rafraichiraient l'interface deux fois pour rien.
|
||||
if (!bAlreadyThere)
|
||||
// On ne diffuse que sur la transition 0 -> 1. Entrer dans le rayon d'un
|
||||
// deuxieme etabli ne change rien de visible, inutile de reconstruire la
|
||||
// grille de recettes pour ca.
|
||||
if (Count == 1)
|
||||
{
|
||||
OnAvailableStationsChanged.Broadcast();
|
||||
}
|
||||
@@ -67,8 +74,17 @@ void UCraftingComponent::RemoveAvailableStation(ECraftingStation Station)
|
||||
return;
|
||||
}
|
||||
|
||||
if (AvailableStations.Remove(Station) > 0)
|
||||
int32* Count = StationRefCounts.Find(Station);
|
||||
if (!Count || *Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
--(*Count);
|
||||
|
||||
if (*Count <= 0)
|
||||
{
|
||||
StationRefCounts.Remove(Station);
|
||||
OnAvailableStationsChanged.Broadcast();
|
||||
}
|
||||
}
|
||||
@@ -94,7 +110,7 @@ void UCraftingComponent::GatherRecipes(TArray<UCraftingRecipeDataAsset*>& OutRec
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!AvailableStations.Contains(Recipe->RequiredStation))
|
||||
if (!HasStation(Recipe->RequiredStation))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -130,7 +146,7 @@ bool UCraftingComponent::CanCraft(const UCraftingRecipeDataAsset* Recipe, ECraft
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AvailableStations.Contains(Recipe->RequiredStation))
|
||||
if (!HasStation(Recipe->RequiredStation))
|
||||
{
|
||||
OutReason = ECraftFailureReason::StationUnavailable;
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "CraftingStation.h"
|
||||
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/CollisionProfile.h"
|
||||
#include "FpsPlayerController.h"
|
||||
#include "GameFramework/Pawn.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "CraftingStation"
|
||||
|
||||
ACraftingStation::ACraftingStation()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
|
||||
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
||||
SetRootComponent(Mesh);
|
||||
|
||||
// Explicite plutot que de compter sur le profil par defaut : ce composant a
|
||||
// deux roles indissociables, empecher le joueur de traverser le meuble et
|
||||
// bloquer le canal Visibility pour que le trace d'interaction le trouve.
|
||||
// Un profil qui laisserait passer Visibility rendrait le poste inutilisable
|
||||
// sans qu'aucune erreur ne le signale.
|
||||
Mesh->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
|
||||
|
||||
InteractionPrompt = LOCTEXT("UseStation", "Utiliser l'établi");
|
||||
}
|
||||
|
||||
FText ACraftingStation::GetInteractionPrompt_Implementation(AActor* Interactor) const
|
||||
{
|
||||
return InteractionPrompt;
|
||||
}
|
||||
|
||||
void ACraftingStation::Interact_Implementation(AActor* Interactor)
|
||||
{
|
||||
// L'ecran vit sur le controller, pas sur le pawn : c'est lui qu'on cherche.
|
||||
const APawn* InteractingPawn = Cast<APawn>(Interactor);
|
||||
AFpsPlayerController* PlayerController = InteractingPawn ? Cast<AFpsPlayerController>(InteractingPawn->GetController()) : nullptr;
|
||||
|
||||
if (!PlayerController)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerController->OpenCraftingAtStation(StationType);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "InputCoreTypes.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "InteractionPromptWidget.h"
|
||||
#include "CraftingComponent.h"
|
||||
#include "InventoryScreenWidget.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "PauseMenuWidget.h"
|
||||
@@ -420,6 +421,47 @@ void AFpsPlayerController::ToggleCrafting()
|
||||
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::OpenCraftingAtStation(ECraftingStation Station)
|
||||
{
|
||||
if (Station == ECraftingStation::Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Deja ouvert sur un autre poste : on rend le precedent avant d'accorder le
|
||||
// nouveau, sinon le compteur du composant ne redescendrait jamais.
|
||||
ReleaseActiveStation();
|
||||
|
||||
if (UCraftingComponent* Crafting = GetPawnCrafting())
|
||||
{
|
||||
Crafting->AddAvailableStation(Station);
|
||||
ActiveStation = Station;
|
||||
}
|
||||
|
||||
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::ReleaseActiveStation()
|
||||
{
|
||||
if (ActiveStation == ECraftingStation::Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (UCraftingComponent* Crafting = GetPawnCrafting())
|
||||
{
|
||||
Crafting->RemoveAvailableStation(ActiveStation);
|
||||
}
|
||||
|
||||
ActiveStation = ECraftingStation::Count;
|
||||
}
|
||||
|
||||
UCraftingComponent* AFpsPlayerController::GetPawnCrafting() const
|
||||
{
|
||||
const AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
|
||||
return PlayerPawn ? PlayerPawn->GetCraftingComponent() : nullptr;
|
||||
}
|
||||
|
||||
void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab)
|
||||
{
|
||||
if (!InventoryScreen)
|
||||
@@ -450,6 +492,15 @@ void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCrafting
|
||||
|
||||
bInventoryOpen = bOpen;
|
||||
|
||||
// Une fermeture, quelle qu'en soit la cause -- Tab, Echap, la mort --
|
||||
// rend le poste. Le faire ici et nulle part ailleurs garantit qu'aucun
|
||||
// chemin de sortie ne laisse l'etabli accessible depuis l'autre bout de
|
||||
// la carte.
|
||||
if (!bInventoryOpen)
|
||||
{
|
||||
ReleaseActiveStation();
|
||||
}
|
||||
|
||||
if (bInventoryOpen)
|
||||
{
|
||||
if (bShowCraftingTab)
|
||||
|
||||
@@ -57,6 +57,7 @@ void UInteractionComponent::UpdateFocus()
|
||||
if (!GetViewPoint(ViewLocation, ViewRotation))
|
||||
{
|
||||
SetFocusedActor(nullptr);
|
||||
RefreshPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,6 +85,10 @@ void UInteractionComponent::UpdateFocus()
|
||||
|
||||
SetFocusedActor(Candidate);
|
||||
|
||||
// Apres SetFocusedActor, jamais avant : l'ordre garantit que l'UI recoit
|
||||
// d'abord "la cible a change", puis le texte correspondant.
|
||||
RefreshPrompt();
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
if (bDrawDebug)
|
||||
{
|
||||
@@ -161,13 +166,26 @@ void UInteractionComponent::TryInteract()
|
||||
UpdateFocus();
|
||||
}
|
||||
|
||||
FText UInteractionComponent::GetFocusedPrompt() const
|
||||
void UInteractionComponent::RefreshPrompt()
|
||||
{
|
||||
AActor* Target = FocusedActor.Get();
|
||||
if (!IsValid(Target))
|
||||
|
||||
// Un seul FText::Format par trace, soit 20 fois par seconde et seulement
|
||||
// quand on vise quelque chose. Recalculer ici plutot que dans le getter
|
||||
// evite qu'un widget qui interroge le prompt plusieurs fois par frame
|
||||
// paie le format autant de fois.
|
||||
const FText NewPrompt = IsValid(Target)
|
||||
? IInteractable::Execute_GetInteractionPrompt(Target, GetOwner())
|
||||
: FText::GetEmpty();
|
||||
|
||||
// EqualTo et non IdenticalTo : on compare ce qui s'affiche, pas l'instance.
|
||||
// Deux FText::Format successifs produisent toujours deux instances
|
||||
// differentes, IdenticalTo redeclencherait donc l'UI a chaque trace.
|
||||
if (CachedPrompt.EqualTo(NewPrompt))
|
||||
{
|
||||
return FText::GetEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
return IInteractable::Execute_GetInteractionPrompt(Target);
|
||||
CachedPrompt = NewPrompt;
|
||||
OnPromptChanged.Broadcast(CachedPrompt);
|
||||
}
|
||||
|
||||
@@ -40,16 +40,16 @@ void UInteractionPromptWidget::BindToOwningPawn()
|
||||
|
||||
if (BoundComponent.IsValid())
|
||||
{
|
||||
BoundComponent->OnFocusChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleFocusChanged);
|
||||
BoundComponent->OnPromptChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandlePromptChanged);
|
||||
}
|
||||
|
||||
BoundComponent = NewComponent;
|
||||
|
||||
if (BoundComponent.IsValid())
|
||||
{
|
||||
// Abonnement au delegue : le widget ne se reveille que quand la cible
|
||||
// Abonnement au delegue : le widget ne se reveille que quand le texte
|
||||
// change vraiment. Pas de Tick, pas de polling.
|
||||
BoundComponent->OnFocusChanged.AddDynamic(this, &UInteractionPromptWidget::HandleFocusChanged);
|
||||
BoundComponent->OnPromptChanged.AddDynamic(this, &UInteractionPromptWidget::HandlePromptChanged);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -65,14 +65,14 @@ void UInteractionPromptWidget::NativeDestruct()
|
||||
// vers un widget detruit -- typiquement au changement de niveau.
|
||||
if (BoundComponent.IsValid())
|
||||
{
|
||||
BoundComponent->OnFocusChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandleFocusChanged);
|
||||
BoundComponent->OnPromptChanged.RemoveDynamic(this, &UInteractionPromptWidget::HandlePromptChanged);
|
||||
}
|
||||
BoundComponent.Reset();
|
||||
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
void UInteractionPromptWidget::HandleFocusChanged(AActor* NewFocus, AActor* OldFocus)
|
||||
void UInteractionPromptWidget::HandlePromptChanged(const FText& NewPrompt)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
@@ -91,6 +91,9 @@ void UInteractionPromptWidget::Refresh()
|
||||
|
||||
if (PromptText)
|
||||
{
|
||||
// Un prompt non vide est toujours une action realisable : les cibles ne
|
||||
// renvoient jamais de texte purement informatif, donc la touche a
|
||||
// toujours sa place devant.
|
||||
PromptText->SetText(FText::Format(PromptFormat, GetInteractKeyDisplayName(), Prompt));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "ItemDataAsset.h"
|
||||
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "Misc/DataValidation.h"
|
||||
#endif
|
||||
|
||||
#define LOCTEXT_NAMESPACE "ItemData"
|
||||
|
||||
void UItemDataAsset::ApplyWorldVisualsTo(UStaticMeshComponent* Component) const
|
||||
{
|
||||
if (!Component)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Component->SetStaticMesh(WorldMesh);
|
||||
|
||||
// SetMaterial(0, nullptr) ne "retire" pas le materiau, il pose un slot vide
|
||||
// et le mesh devient gris damier. Sans override, on ne touche a rien et le
|
||||
// mesh garde le sien.
|
||||
if (WorldMeshMaterial)
|
||||
{
|
||||
Component->SetMaterial(0, WorldMeshMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
EDataValidationResult UItemDataAsset::IsDataValid(FDataValidationContext& Context) const
|
||||
{
|
||||
EDataValidationResult Result = Super::IsDataValid(Context);
|
||||
|
||||
if (bCanBeCooked && !CookedResult)
|
||||
{
|
||||
Context.AddError(LOCTEXT("CookNoResult", "'Can Be Cooked' is checked but no 'Cooked Result' is assigned: this item would sit on the fire forever without ever changing."));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
if (bCanBurn && !BurntResult)
|
||||
{
|
||||
Context.AddError(LOCTEXT("BurnNoResult", "'Can Burn' is checked but no 'Burnt Result' is assigned."));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
// Une chaine qui reboucle sur elle-meme ne plante pas, elle tourne : la
|
||||
// station re-arme un timer identique a chaque etape et le joueur voit un
|
||||
// objet qui ne finit jamais de cuire. Impossible a diagnostiquer en jeu.
|
||||
if (CookedResult == this)
|
||||
{
|
||||
Context.AddError(LOCTEXT("CookSelfResult", "'Cooked Result' points back to this asset: the item would cook into itself, endlessly."));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
if (BurntResult == this)
|
||||
{
|
||||
Context.AddError(LOCTEXT("BurnSelfResult", "'Burnt Result' points back to this asset: the item would burn into itself, endlessly."));
|
||||
Result = EDataValidationResult::Invalid;
|
||||
}
|
||||
|
||||
// Un objet a charges ne s'empile pas (voir GetEffectiveMaxStack). Le poser
|
||||
// sur le feu reste possible, mais la charge entamee suit l'exemplaire et le
|
||||
// resultat en herite : autant que ce soit un choix, pas une surprise.
|
||||
if (MaxUses > 1 && bCanBeCooked)
|
||||
{
|
||||
Context.AddWarning(LOCTEXT("CookableWithCharges", "This item has multiple uses AND can be cooked. The remaining charges follow the item through cooking, which is rarely what you want for food."));
|
||||
}
|
||||
|
||||
return Result;
|
||||
}
|
||||
#endif
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -45,24 +45,25 @@ void APickupItem::OnConstruction(const FTransform& Transform)
|
||||
// pour les objets jetes, puisque OnConstruction s'execute a FinishSpawning.
|
||||
if (bUseMeshFromItemData && ItemData && ItemData->WorldMesh)
|
||||
{
|
||||
Mesh->SetStaticMesh(ItemData->WorldMesh);
|
||||
ItemData->ApplyWorldVisualsTo(Mesh);
|
||||
}
|
||||
}
|
||||
|
||||
FText APickupItem::GetInteractionPrompt_Implementation() const
|
||||
FText APickupItem::GetInteractionPrompt_Implementation(AActor* Interactor) const
|
||||
{
|
||||
// Interactor ne sert pas ici : ramasser dit la meme chose a tout le monde.
|
||||
if (!ItemData)
|
||||
{
|
||||
return LOCTEXT("PickupUnconfigured", "Objet non configure");
|
||||
return LOCTEXT("PickupUnconfigured", "Unconfigured item");
|
||||
}
|
||||
|
||||
if (Quantity > 1)
|
||||
{
|
||||
return FText::Format(LOCTEXT("PickupPromptStack", "Ramasser {0} x{1}"),
|
||||
return FText::Format(LOCTEXT("PickupPromptStack", "Pick up {0} x{1}"),
|
||||
ItemData->DisplayName, FText::AsNumber(Quantity));
|
||||
}
|
||||
|
||||
return FText::Format(LOCTEXT("PickupPrompt", "Ramasser {0}"), ItemData->DisplayName);
|
||||
return FText::Format(LOCTEXT("PickupPrompt", "Pick up {0}"), ItemData->DisplayName);
|
||||
}
|
||||
|
||||
bool APickupItem::CanInteract_Implementation(AActor* Interactor) const
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Interactable.h"
|
||||
#include "CookingStation.generated.h"
|
||||
|
||||
class UInventoryComponent;
|
||||
class UItemDataAsset;
|
||||
class UNiagaraComponent;
|
||||
class UPointLightComponent;
|
||||
class USoundBase;
|
||||
class USphereComponent;
|
||||
class UStaticMeshComponent;
|
||||
|
||||
/**
|
||||
* Ce qu'un emplacement du grill porte a l'instant t.
|
||||
*
|
||||
* Uniquement de l'etat : le DataAsset dit ce que l'objet EST, cette struct dit
|
||||
* ou il en est. C'est la meme separation que FInventorySlot, pour la meme raison.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FCookingSlotState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** L'objet pose, ou nullptr. Un seul exemplaire par emplacement, comme dans Raft. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
|
||||
TObjectPtr<UItemDataAsset> Item = nullptr;
|
||||
|
||||
/**
|
||||
* Charges restantes de l'exemplaire pose. -1 = plein.
|
||||
* L'usure traverse la cuisson comme elle traverse le sol : ce qu'on
|
||||
* recupere est le meme exemplaire, pas un neuf.
|
||||
*/
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
|
||||
int32 RemainingUses = -1;
|
||||
|
||||
/**
|
||||
* Compte a rebours de l'etape en cours. Pas une UPROPERTY : un FTimerHandle
|
||||
* n'est qu'un index dans le gestionnaire de timers, il n'y a rien a
|
||||
* collecter ni a serialiser.
|
||||
*/
|
||||
FTimerHandle Timer;
|
||||
|
||||
bool IsEmpty() const { return Item == nullptr; }
|
||||
};
|
||||
|
||||
/**
|
||||
* Une buche dans le foyer : QUEL objet a ete mis, et combien de temps il tient
|
||||
* encore.
|
||||
*
|
||||
* C'est le coeur du modele : le carburant n'est pas une jauge de secondes mais
|
||||
* une pile d'objets. Cinq buches, cinq objets -- une branche qui tient dix
|
||||
* secondes occupe exactement la meme place qu'une planche qui en tient cent
|
||||
* quatre-vingts. Le joueur voit donc ce qu'il a donne au feu, pas un
|
||||
* pourcentage abstrait, et melanger les combustibles reste lisible.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct FFuelSlotState
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
|
||||
TObjectPtr<UItemDataAsset> Item = nullptr;
|
||||
|
||||
/** Secondes de combustion restantes pour CE morceau. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Cooking")
|
||||
float RemainingSeconds = 0.f;
|
||||
};
|
||||
|
||||
/** L'etat de la station a change : allumage, depot, transformation, retrait. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCookingStationChanged);
|
||||
|
||||
/**
|
||||
* Ce que l'appui sur Interagir va declencher, compte tenu de ce que le joueur
|
||||
* vise et de ce qu'il tient.
|
||||
*
|
||||
* Pas un UENUM : c'est un detail d'implementation, rien a l'exterieur n'a
|
||||
* besoin de le nommer.
|
||||
*/
|
||||
enum class ECookingAction : uint8
|
||||
{
|
||||
None,
|
||||
TakeItem,
|
||||
PlaceItem,
|
||||
AddFuel
|
||||
};
|
||||
|
||||
/**
|
||||
* Resultat complet de la decision. Calcule UNE fois, par ResolveAction(), et
|
||||
* servi a la fois au texte du prompt et a l'execution : deux cascades de `if`
|
||||
* separees finiraient par diverger, et le joueur lirait "Light the fire" en
|
||||
* obtenant autre chose.
|
||||
*
|
||||
* Struct de pile a duree de vie tres courte, d'ou les pointeurs bruts : rien
|
||||
* ici ne peut etre collecte entre le calcul et son usage.
|
||||
*/
|
||||
struct FCookingInteraction
|
||||
{
|
||||
ECookingAction Action = ECookingAction::None;
|
||||
|
||||
/** Emplacement de cuisson vise, ou INDEX_NONE si le joueur regarde le foyer. */
|
||||
int32 SlotIndex = INDEX_NONE;
|
||||
|
||||
/** Objet actif de la barre rapide. */
|
||||
UItemDataAsset* HeldItem = nullptr;
|
||||
|
||||
/** Slot d'inventaire d'ou sortira le bois, et l'objet correspondant. */
|
||||
int32 FuelSourceSlot = INDEX_NONE;
|
||||
UItemDataAsset* FuelItem = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Poste de cuisson pose dans le monde : feu de camp, et plus tard fourneau ou
|
||||
* sechoir en changeant simplement ses reglages.
|
||||
*
|
||||
* Modele Raft assume : AUCUNE interface. Le joueur tient sa viande crue dans la
|
||||
* barre rapide, vise un emplacement du grill, appuie sur Interagir, et regarde
|
||||
* le mesh changer.
|
||||
*
|
||||
* Le poste ne commente jamais son propre etat : le prompt n'affiche QUE des
|
||||
* actions realisables, exactement comme un objet a ramasser. Ce qui cuit, ce
|
||||
* qui manque et ce qui brule se lit sur les flammes, sur le nombre de buches
|
||||
* et sur le mesh de la nourriture -- l'ecrire en plus reconstruirait une
|
||||
* interface par-dessus un monde qui se suffit a lui-meme.
|
||||
*
|
||||
* Aucun Tick. Un timer par emplacement de cuisson, un seul pour le carburant :
|
||||
* celui de la buche en train de bruler. Eteindre le feu met ces timers en
|
||||
* pause, et la cuisson reprend exactement ou elle s'etait arretee sans qu'une
|
||||
* ligne ne compte le temps a la main.
|
||||
*
|
||||
* Le feu ne s'allume PAS a la main : il part des qu'il a du bois et quelque
|
||||
* chose a transformer, et s'arrete des qu'il n'a plus rien a faire. Le bois ne
|
||||
* part donc jamais en fumee pour rien -- ce qui evite au joueur d'avoir a
|
||||
* penser a eteindre derriere lui, une corvee qui n'apporte aucune decision
|
||||
* interessante.
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API ACookingStation : public AActor, public IInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ACookingStation();
|
||||
|
||||
// --- IInteractable ---
|
||||
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
|
||||
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
|
||||
virtual void Interact_Implementation(AActor* Interactor) override;
|
||||
virtual void OnBeginFocus_Implementation() override;
|
||||
virtual void OnEndFocus_Implementation() override;
|
||||
|
||||
/**
|
||||
* Allume le feu s'il reste du bois dans le foyer.
|
||||
*
|
||||
* En temps normal, personne n'appelle ceci : UpdateFireState() decide seul.
|
||||
* Reste exposee pour un futur allume-feu ou un evenement scenarise.
|
||||
* @return false si le foyer est vide.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Cooking")
|
||||
bool Light();
|
||||
|
||||
/** Eteint le feu. Les cuissons en cours se figent, elles ne repartent pas de zero. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Cooking")
|
||||
void Extinguish();
|
||||
|
||||
/** Vrai si au moins un emplacement porte quelque chose qui a encore une etape a franchir. */
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
bool HasPendingWork() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
bool IsLit() const { return bIsLit; }
|
||||
|
||||
/**
|
||||
* Combien de morceaux de bois le foyer peut contenir.
|
||||
*
|
||||
* C'est le nombre de buches taguees dans le Blueprint : la capacite est une
|
||||
* decision de mise en scene, et la regler a deux endroits garantirait qu'ils
|
||||
* finissent par se contredire. FallbackFuelCapacity ne sert qu'aux postes
|
||||
* sans buche visible, un four ferme par exemple.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
int32 GetFuelCapacity() const { return FuelIndicators.Num() > 0 ? FuelIndicators.Num() : FallbackFuelCapacity; }
|
||||
|
||||
/** Morceaux de bois actuellement dans le foyer. */
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
int32 GetFuelCount() const { return Fuel.Num(); }
|
||||
|
||||
/** Secondes de combustion cumulees sur tout ce qui est dans le foyer. */
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
float GetFuelRemaining() const;
|
||||
|
||||
/** Secondes avant la prochaine transformation, ou 0 si l'emplacement n'attend rien. */
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
float GetSlotTimeRemaining(int32 SlotIndex) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Cooking")
|
||||
const TArray<FCookingSlotState>& GetSlots() const { return Slots; }
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Cooking")
|
||||
FOnCookingStationChanged OnStationChanged;
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
/**
|
||||
* Nombre d'emplacements de cuisson existant dans la classe. Les composants
|
||||
* sont crees au constructeur, donc ce nombre est fige a la compilation --
|
||||
* c'est le prix a payer pour pouvoir les positionner A LA SOURIS dans le
|
||||
* viewport du Blueprint. Des composants engendres a l'execution ne seraient
|
||||
* reglables qu'en tapant des coordonnees a l'aveugle.
|
||||
*/
|
||||
static constexpr int32 MaxCookingSlots = 4;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Composants
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Le foyer lui-meme : bois, pierres, structure. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
TObjectPtr<UStaticMeshComponent> BaseMesh;
|
||||
|
||||
/**
|
||||
* Ce que le trace d'interaction vient toucher. Sphere dediee et non le mesh,
|
||||
* pour la meme raison que sur APickupItem : rien ne garantit qu'un mesh
|
||||
* d'asset pack ait une collision.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
TObjectPtr<USphereComponent> InteractionSphere;
|
||||
|
||||
/**
|
||||
* Un mesh par emplacement de cuisson, qui porte la nourriture posee. Sa
|
||||
* transform sert a la fois a l'afficher et a savoir ce que le joueur vise :
|
||||
* pas besoin d'une collision par emplacement, donc pas une requete physique
|
||||
* de plus.
|
||||
*
|
||||
* Positionne-les dans le viewport du Blueprint. Ils sont caches au demarrage
|
||||
* tant qu'ils sont vides, mais restent visibles dans l'editeur pour que tu
|
||||
* puisses les placer.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
TArray<TObjectPtr<UStaticMeshComponent>> SlotMeshes;
|
||||
|
||||
/** Flamme. Active tant que le feu brule, coupee des qu'il s'eteint. */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
TObjectPtr<UNiagaraComponent> FireEffect;
|
||||
|
||||
/**
|
||||
* Halo du foyer, allume en meme temps que la flamme.
|
||||
*
|
||||
* Regle intensite, couleur et rayon directement sur le composant dans le
|
||||
* Blueprint : ce sont des proprietes natives, les redeclarer ici en
|
||||
* doublon garantirait qu'un jour les deux se contredisent.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
TObjectPtr<UPointLightComponent> FireLight;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reglages
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Emplacements de cuisson reellement utilises. Les autres restent caches. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "1", ClampMax = "4"))
|
||||
int32 CookingSlotCount = 2;
|
||||
|
||||
/**
|
||||
* Echelle appliquee au mesh de la nourriture posee.
|
||||
* Les meshes d'objets sont dimensionnes pour etre poses au sol ; sur un
|
||||
* grill ils sont souvent trop gros.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "0.01"))
|
||||
float CookingMeshScale = 1.f;
|
||||
|
||||
/**
|
||||
* Demi-angle du cone de visee autour de chaque emplacement, en degres.
|
||||
*
|
||||
* Au-dela, on considere que le joueur vise le foyer et pas un emplacement :
|
||||
* c'est ce qui lui permet d'ajouter du bois ou d'allumer le feu sans devoir
|
||||
* eviter le grill du regard.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "1", ClampMax = "89"))
|
||||
float SlotAimHalfAngle = 25.f;
|
||||
|
||||
/** Multiplie la vitesse de cuisson. Un fourneau va plus vite que des braises. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking", meta = (ClampMin = "0.01"))
|
||||
float CookSpeedMultiplier = 1.f;
|
||||
|
||||
/**
|
||||
* Autorise a reprendre un objet qui n'a pas fini de cuire.
|
||||
*
|
||||
* Decoche par defaut : ce qu'on a mis a cuire est engage, on attend. Un
|
||||
* objet qui a fini sa cuisson reste toujours recuperable, y compris pendant
|
||||
* qu'il se dirige vers le brule -- sans quoi il serait impossible de sauver
|
||||
* sa viande a temps, ce qui viderait la sur-cuisson de tout son sens.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
bool bAllowTakingRawItems = false;
|
||||
|
||||
/**
|
||||
* Ajoute les secondes restantes aux textes du prompt.
|
||||
*
|
||||
* Decoche en jeu : un chiffre qui s'egrene est de l'interface deguisee, et
|
||||
* le joueur doit lire l'avancement sur le mesh, pas sur un compteur. Coche
|
||||
* le temps d'un reglage pour verifier tes durees.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
bool bShowRemainingTimeInPrompt = false;
|
||||
|
||||
|
||||
/** Active le Custom Depth quand le poste est vise, comme les ramassables. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking")
|
||||
bool bHighlightOnFocus = true;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Reglages : carburant
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Tag a poser sur les meshes de buches qui figurent le contenu du foyer.
|
||||
* Leur NOMBRE fait la capacite du feu, et leur ordre alphabetique l'ordre
|
||||
* d'apparition : la derniere posee est la premiere a bruler.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
|
||||
FName FuelIndicatorTag = TEXT("FuelLog");
|
||||
|
||||
/** Capacite des postes qui n'ont aucune buche visible. Ignore des qu'il y en a une. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel", meta = (ClampMin = "1"))
|
||||
int32 FallbackFuelCapacity = 5;
|
||||
|
||||
/**
|
||||
* Chaque buche prend le mesh de l'objet qu'elle represente.
|
||||
*
|
||||
* Decoche par defaut : les buches d'un feu de camp sont posees a la main
|
||||
* dans le Blueprint, avec leur orientation et leur imbrication soignees, et
|
||||
* y substituer un mesh d'objet gacherait ce travail. Coche-le pour un
|
||||
* support ou l'on veut distinguer d'un coup d'oeil branches et planches.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
|
||||
bool bFuelIndicatorsUseItemMesh = false;
|
||||
|
||||
/** Bois deja dans le foyer au demarrage. Laisse vide pour un feu a alimenter. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel")
|
||||
TObjectPtr<UItemDataAsset> StartingFuelItem;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Fuel", meta = (ClampMin = "0"))
|
||||
int32 StartingFuelCount = 0;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Sons. Tous facultatifs : un champ vide ne joue rien.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
|
||||
TObjectPtr<USoundBase> LightSound;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
|
||||
TObjectPtr<USoundBase> ExtinguishSound;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cooking|Sound")
|
||||
TObjectPtr<USoundBase> PlaceItemSound;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Quel emplacement de cuisson le joueur vise, ou INDEX_NONE s'il regarde le
|
||||
* foyer. Se base sur le point de vue reel du pawn, donc sur ce qu'il voit a
|
||||
* l'ecran, et non sur la position de son corps.
|
||||
*/
|
||||
int32 ResolveAimedSlot(const AActor* Interactor) const;
|
||||
|
||||
/** Decide ce que ferait un appui sur Interagir, sans rien modifier. */
|
||||
FCookingInteraction ResolveAction(AActor* Interactor) const;
|
||||
|
||||
/** Depose l'objet actif de la barre rapide sur l'emplacement de cuisson. */
|
||||
bool PlaceItemFromHand(int32 SlotIndex, AActor* Interactor);
|
||||
|
||||
/** Rend l'objet a l'inventaire. Refuse et laisse tout en place si c'est plein. */
|
||||
bool TakeItemToInventory(int32 SlotIndex, AActor* Interactor);
|
||||
|
||||
/**
|
||||
* Met UN morceau de bois dans le foyer, pris dans un slot d'inventaire.
|
||||
*
|
||||
* Un exemplaire par appui, jamais la pile entiere : donner du bois est un
|
||||
* geste irreversible, et vider vingt branches d'un coup sur un feu qui n'en
|
||||
* demandait qu'une ne se rattrape pas. Le joueur dose lui-meme.
|
||||
*/
|
||||
bool AddFuelFrom(int32 InventorySlotIndex, AActor* Interactor);
|
||||
|
||||
/**
|
||||
* Ou prendre du bois : la main d'abord, sinon le premier slot qui en
|
||||
* contient. La main est prioritaire parce que c'est le seul moyen pour le
|
||||
* joueur de CHOISIR ce qu'il brule.
|
||||
*/
|
||||
static int32 FindFuelSource(const UInventoryComponent* Inventory, UItemDataAsset*& OutItem);
|
||||
|
||||
/** Peut-on retirer ce qui se trouve sur cet emplacement de cuisson. */
|
||||
bool CanTakeFromSlot(int32 SlotIndex) const;
|
||||
|
||||
/** Arme le compte a rebours de l'etape courante de l'emplacement. */
|
||||
void StartSlotTimer(int32 SlotIndex);
|
||||
|
||||
/** Fin d'etape : l'objet devient son resultat, et on enchaine s'il y a une suite. */
|
||||
void HandleSlotFinished(int32 SlotIndex);
|
||||
|
||||
/** Recopie le WorldMesh de l'objet pose, ou cache le composant si l'emplacement est vide. */
|
||||
void RefreshSlotMesh(int32 SlotIndex);
|
||||
|
||||
/**
|
||||
* Allume ou eteint selon qu'il y ait, ou non, du bois ET du travail.
|
||||
*
|
||||
* Point de decision UNIQUE : toutes les actions qui changent le contenu du
|
||||
* foyer ou du grill finissent ici. Laisser chaque action decider elle-meme
|
||||
* d'allumer aurait fini par oublier un cas -- typiquement le dernier objet
|
||||
* retire, qui laisserait le feu bruler dans le vide.
|
||||
*/
|
||||
void UpdateFireState();
|
||||
|
||||
/** La buche du dessus a fini de bruler : on passe a la suivante. */
|
||||
void HandleFuelConsumed();
|
||||
|
||||
/** Arme le timer sur la buche en cours de combustion. */
|
||||
void StartFuelTimer();
|
||||
|
||||
/** Recopie le restant du timer dans la buche courante avant de le modifier. */
|
||||
void SyncFuelFromTimer();
|
||||
|
||||
/**
|
||||
* Allume ou coupe tout ce qui se voit quand le feu brule.
|
||||
*
|
||||
* Un seul endroit pour la flamme et la lumiere : le jour ou s'ajoute une
|
||||
* boucle sonore ou un panache de fumee, il n'y aura toujours qu'un point a
|
||||
* modifier au lieu de deux appels a retrouver dans Light() et Extinguish().
|
||||
*/
|
||||
void ApplyFireVisuals(bool bActive);
|
||||
|
||||
/** Retrouve les buches par leur tag, une fois pour toutes, au demarrage. */
|
||||
void CacheFuelIndicators();
|
||||
|
||||
/**
|
||||
* Montre exactement autant de buches qu'il y a de morceaux dans le foyer.
|
||||
*
|
||||
* Plus aucun calcul de palier ni de timer d'affichage depuis que le
|
||||
* carburant est une pile d'objets : une buche visible EST un objet stocke.
|
||||
*/
|
||||
void RefreshFuelIndicators();
|
||||
|
||||
/**
|
||||
* Ce que cet objet va devenir sur le feu, et en combien de temps.
|
||||
* La cuisson passe avant la combustion : voir le commentaire de bCanBurn
|
||||
* sur UItemDataAsset.
|
||||
*/
|
||||
static UItemDataAsset* GetNextStage(const UItemDataAsset* Item, float& OutDuration);
|
||||
|
||||
/** Inventaire du pawn qui interagit, ou nullptr. */
|
||||
static UInventoryComponent* GetInventoryOf(AActor* Interactor);
|
||||
|
||||
/** Etat courant de chaque emplacement de cuisson. Toujours dimensionne a MaxCookingSlots. */
|
||||
UPROPERTY(Transient)
|
||||
TArray<FCookingSlotState> Slots;
|
||||
|
||||
/**
|
||||
* Le contenu du foyer, dans l'ordre ou il a ete donne. L'index 0 est celui
|
||||
* qui brule ; quand il est consomme, tout le monde avance d'un cran et la
|
||||
* derniere buche disparait du foyer.
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TArray<FFuelSlotState> Fuel;
|
||||
|
||||
FTimerHandle FuelTimer;
|
||||
|
||||
/**
|
||||
* Buches de mise en scene, resolues au BeginPlay et triees par nom.
|
||||
* Transient : ces pointeurs designent des composants du Blueprint, il n'y a
|
||||
* rien a sauvegarder, seulement a garder du GC le temps de la partie.
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
TArray<TObjectPtr<UStaticMeshComponent>> FuelIndicators;
|
||||
|
||||
bool bIsLit = false;
|
||||
};
|
||||
@@ -131,9 +131,16 @@ public:
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Crafting|Stations")
|
||||
bool HasStation(ECraftingStation Station) const { return AvailableStations.Contains(Station); }
|
||||
bool HasStation(ECraftingStation Station) const;
|
||||
|
||||
/** A appeler quand le joueur entre dans le rayon d'un poste. */
|
||||
/**
|
||||
* A appeler quand le joueur entre dans le rayon d'un poste.
|
||||
*
|
||||
* Les appels se COMPTENT : deux etablis dont les portees se chevauchent, ce
|
||||
* qui arrive des qu'on construit une base un peu dense, s'ajoutent tous les
|
||||
* deux. Sortir du premier ne doit pas retirer l'acces alors qu'on est encore
|
||||
* devant le second.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Crafting|Stations")
|
||||
void AddAvailableStation(ECraftingStation Station);
|
||||
|
||||
@@ -161,8 +168,11 @@ protected:
|
||||
|
||||
private:
|
||||
/**
|
||||
* Pas une UPROPERTY : un TSet de valeurs d'enum ne contient aucune
|
||||
* reference a collecter, et cet etat n'a rien a regler dans l'editeur.
|
||||
* Combien de postes de chaque type sont a portee, et non un simple
|
||||
* "present / absent" -- voir AddAvailableStation.
|
||||
*
|
||||
* Pas une UPROPERTY : ni la cle ni la valeur ne sont des references a
|
||||
* collecter, et cet etat n'a rien a regler dans l'editeur.
|
||||
*/
|
||||
TSet<ECraftingStation> AvailableStations;
|
||||
TMap<ECraftingStation, int32> StationRefCounts;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CraftingRecipeDataAsset.h"
|
||||
#include "Interactable.h"
|
||||
#include "CraftingStation.generated.h"
|
||||
|
||||
class UStaticMeshComponent;
|
||||
|
||||
/**
|
||||
* Un poste de travail pose dans le monde : etabli, et ce qui suivra.
|
||||
*
|
||||
* Il ne fabrique rien lui-meme et ne contient aucune recette. Tout ce qu'il
|
||||
* fait, c'est ouvrir l'ecran de fabrication en annoncant SON type de poste --
|
||||
* le reste appartient au UCraftingComponent du joueur, qui filtrera les
|
||||
* recettes en consequence.
|
||||
*
|
||||
* L'acces au poste ne dure que le temps de l'ecran : c'est le controller qui
|
||||
* l'accorde a l'ouverture et le reprend a la fermeture. Un etabli qui donnerait
|
||||
* son acces "par proximite" obligerait a surveiller la distance en permanence,
|
||||
* pour un resultat que le joueur comprendrait moins bien -- ici on fabrique a
|
||||
* l'etabli parce qu'on est en train de s'en servir.
|
||||
*
|
||||
* Un seul BP_CraftingStation suffit pour tous les postes : c'est StationType
|
||||
* qui change par instance, comme ItemData sur BP_Pickup.
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API ACraftingStation : public AActor, public IInteractable
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
ACraftingStation();
|
||||
|
||||
// --- IInteractable ---
|
||||
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
|
||||
virtual void Interact_Implementation(AActor* Interactor) override;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Le visuel ET la surface d'interaction.
|
||||
*
|
||||
* Pas de sphere dediee ici, contrairement a APickupItem : un poste est un
|
||||
* meuble construit, dont le mesh a une vraie collision simple. Une sphere
|
||||
* autour d'un volume pareil serait touchee AVANT lui -- le trace s'arrete au
|
||||
* premier bloqueur -- et le prompt apparaitrait en visant le vide autour, ou
|
||||
* masquerait un objet pose au pied du meuble.
|
||||
*
|
||||
* Corollaire : le mesh assigne doit avoir une collision simple. Sans elle,
|
||||
* le trace le traverse, puisqu'il est lance en bTraceComplex = false.
|
||||
*/
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TObjectPtr<UStaticMeshComponent> Mesh;
|
||||
|
||||
/** Le poste que cet acteur rend accessible pendant qu'on s'en sert. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
ECraftingStation StationType = ECraftingStation::Workbench;
|
||||
|
||||
/** Texte affiche quand le joueur vise le poste. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
FText InteractionPrompt;
|
||||
};
|
||||
@@ -4,12 +4,15 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
// ESlateVisibility n'est que declare en avant par le moteur : il faut le type
|
||||
// complet pour en stocker une valeur par defaut.
|
||||
// complet pour en stocker une valeur par defaut. Meme raison pour
|
||||
// ECraftingStation, stocke en membre.
|
||||
#include "Components/SlateWrapperTypes.h"
|
||||
#include "CraftingRecipeDataAsset.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "FpsPlayerController.generated.h"
|
||||
|
||||
class UConfirmDialogWidget;
|
||||
class UCraftingComponent;
|
||||
class UHotbarWidget;
|
||||
class UInputAction;
|
||||
class UInventoryComponent;
|
||||
@@ -46,6 +49,20 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "UI")
|
||||
void ToggleCrafting();
|
||||
|
||||
/**
|
||||
* Ouvre l'ecran de fabrication depuis un poste de travail.
|
||||
*
|
||||
* L'acces au poste ne dure QUE le temps de l'ecran : il est accorde ici et
|
||||
* repris a la fermeture. C'est le contrat de l'interaction -- on fabrique a
|
||||
* l'etabli parce qu'on est en train de s'en servir, pas parce qu'on en a
|
||||
* croise un un jour.
|
||||
*
|
||||
* Le contrôleur s'en charge et non l'etabli : c'est lui qui sait quand
|
||||
* l'ecran se referme, et lui seul peut donc rendre le poste au bon moment.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "UI")
|
||||
void OpenCraftingAtStation(ECraftingStation Station);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "UI")
|
||||
bool IsInventoryOpen() const { return bInventoryOpen; }
|
||||
|
||||
@@ -277,6 +294,12 @@ private:
|
||||
*/
|
||||
void SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab);
|
||||
|
||||
/** Rend le poste accorde par le dernier OpenCraftingAtStation. Sans effet sinon. */
|
||||
void ReleaseActiveStation();
|
||||
|
||||
/** Raccourci vers le composant de fabrication du pawn possede. */
|
||||
UCraftingComponent* GetPawnCrafting() const;
|
||||
|
||||
/** L'ecran a change d'onglet : la barre rapide n'a rien a faire sous la page de craft. */
|
||||
UFUNCTION()
|
||||
void HandleInventoryTabChanged(bool bInventoryTabActive);
|
||||
@@ -311,6 +334,12 @@ private:
|
||||
/** Onglet Fabrication affiche. N'a de sens que si bInventoryOpen est vrai. */
|
||||
bool bCraftingTabActive = false;
|
||||
|
||||
/**
|
||||
* Poste accorde par l'ecran actuellement ouvert, a rendre a la fermeture.
|
||||
* Count = aucun, l'ecran a ete ouvert au clavier et pas depuis un etabli.
|
||||
*/
|
||||
ECraftingStation ActiveStation = ECraftingStation::Count;
|
||||
|
||||
/**
|
||||
* Visibilite d'origine de la barre rapide, relevee a sa creation.
|
||||
* On la restaure telle quelle plutot que de forcer Visible : le Blueprint a
|
||||
|
||||
@@ -40,10 +40,16 @@ public:
|
||||
* Texte affiche au joueur quand il vise l'objet ("Ramasser", "Ouvrir"...).
|
||||
* Renvoie du FText et non du FString : c'est le type localisable d'Unreal,
|
||||
* indispensable si tu traduis le jeu un jour.
|
||||
*
|
||||
* Interactor est fourni parce que le texte depend souvent de CELUI qui vise
|
||||
* et d'ou il vise : un feu de camp nomme l'emplacement precis qu'on regarde,
|
||||
* un coffre verrouille annonce quelle cle il attend. Sans lui, l'objet
|
||||
* devrait aller chercher le joueur local par un chemin detourne -- ce qui
|
||||
* marche exactement jusqu'au jour ou il y a deux joueurs.
|
||||
*/
|
||||
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Interaction")
|
||||
FText GetInteractionPrompt() const;
|
||||
virtual FText GetInteractionPrompt_Implementation() const { return FText::GetEmpty(); }
|
||||
FText GetInteractionPrompt(AActor* Interactor) const;
|
||||
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const { return FText::GetEmpty(); }
|
||||
|
||||
/**
|
||||
* Permet de refuser l'interaction selon le contexte : porte verrouillee,
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnInteractionFocusChanged, AActor*, NewFocus, AActor*, OldFocus);
|
||||
|
||||
/**
|
||||
* Diffuse quand le TEXTE du prompt change, ce qui n'est pas la meme chose qu'un
|
||||
* changement de cible : un feu de camp affiche un compte a rebours, et le fait
|
||||
* de balayer d'un emplacement a l'autre ne change pas l'acteur vise. S'abonner
|
||||
* uniquement a OnFocusChanged laisserait ces deux cas figes a l'ecran.
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInteractionPromptChanged, const FText&, NewPrompt);
|
||||
|
||||
/**
|
||||
* A poser sur le pawn du joueur. Lance regulierement un trace depuis le point
|
||||
* de vue de la camera, garde en memoire l'objet IInteractable vise, et declenche
|
||||
@@ -40,14 +48,24 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||
AActor* GetFocusedActor() const { return FocusedActor.Get(); }
|
||||
|
||||
/** Texte a afficher a l'ecran, vide si rien n'est vise. */
|
||||
/**
|
||||
* Texte a afficher a l'ecran, vide si rien n'est vise.
|
||||
*
|
||||
* Renvoie une valeur DEJA calculee : le prompt est reconstruit une fois par
|
||||
* trace, pas une fois par lecture. Un widget peut donc l'interroger sans
|
||||
* jamais declencher le moindre travail.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||
FText GetFocusedPrompt() const;
|
||||
const FText& GetFocusedPrompt() const { return CachedPrompt; }
|
||||
|
||||
/** Branche ton widget la-dessus pour afficher / cacher le prompt. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||
FOnInteractionFocusChanged OnFocusChanged;
|
||||
|
||||
/** Le texte affiche a change. C'est a ca qu'un widget de prompt s'abonne. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Interaction")
|
||||
FOnInteractionPromptChanged OnPromptChanged;
|
||||
|
||||
protected:
|
||||
/** Portee du trace en cm. 250 = un peu plus d'un bras et demi. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction", meta = (ClampMin = "1"))
|
||||
@@ -88,6 +106,9 @@ private:
|
||||
/** Point de vue reel du joueur, camera comprise. */
|
||||
bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const;
|
||||
|
||||
/** Reconstruit CachedPrompt et diffuse uniquement si le texte a reellement change. */
|
||||
void RefreshPrompt();
|
||||
|
||||
/** Cible courante. TWeakObjectPtr : si l'objet est detruit, on ne garde pas un pointeur mort. */
|
||||
TWeakObjectPtr<AActor> FocusedActor;
|
||||
|
||||
@@ -99,5 +120,11 @@ private:
|
||||
*/
|
||||
bool bHasFocusedActor = false;
|
||||
|
||||
/**
|
||||
* Dernier texte diffuse. Le garder permet deux choses : ne reveiller l'UI
|
||||
* que sur un vrai changement, et servir GetFocusedPrompt() sans recalculer.
|
||||
*/
|
||||
FText CachedPrompt;
|
||||
|
||||
float TimeSinceLastTrace = 0.f;
|
||||
};
|
||||
|
||||
@@ -19,8 +19,12 @@ class UTextBlock;
|
||||
* en UMG, l'equivalent d'un prefab d'UI avec son script en Unity.
|
||||
*
|
||||
* Le widget ne fait AUCUN travail par frame : il s'abonne au delegue
|
||||
* OnFocusChanged du composant d'interaction et ne se met a jour que lorsque
|
||||
* la cible change reellement.
|
||||
* OnPromptChanged du composant d'interaction et ne se met a jour que lorsque
|
||||
* le texte change reellement.
|
||||
*
|
||||
* OnPromptChanged et pas OnFocusChanged : le texte peut bouger sans que la
|
||||
* cible change -- un feu de camp qui egrene son compte a rebours, ou le regard
|
||||
* qui glisse d'un emplacement du grill a l'autre sur le meme acteur.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UInteractionPromptWidget : public UUserWidget
|
||||
@@ -54,9 +58,9 @@ protected:
|
||||
UInteractionPromptWidget(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
private:
|
||||
/** Appele par le composant d'interaction a chaque changement de cible. */
|
||||
/** Appele par le composant d'interaction a chaque changement du texte affiche. */
|
||||
UFUNCTION()
|
||||
void HandleFocusChanged(AActor* NewFocus, AActor* OldFocus);
|
||||
void HandlePromptChanged(const FText& NewPrompt);
|
||||
|
||||
/** Recalcule le texte et la visibilite. */
|
||||
void Refresh();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "ItemDataAsset.generated.h"
|
||||
|
||||
class UMaterialInterface;
|
||||
class UStaticMesh;
|
||||
class UTexture2D;
|
||||
|
||||
@@ -47,6 +48,18 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
|
||||
TObjectPtr<UStaticMesh> WorldMesh;
|
||||
|
||||
/**
|
||||
* Materiau force sur le WorldMesh. Vide = celui du mesh.
|
||||
*
|
||||
* Indispensable des qu'un pack partage une geometrie entre plusieurs
|
||||
* aliments : une chaine de cuisson demande un visuel par etape, et sans ca
|
||||
* il faudrait trois meshes distincts la ou un seul suffit avec trois
|
||||
* teintes. N'ecrase que le PREMIER slot de materiau -- un mesh qui en
|
||||
* expose plusieurs garde les autres tels quels.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item")
|
||||
TObjectPtr<UMaterialInterface> WorldMeshMaterial;
|
||||
|
||||
/**
|
||||
* Nombre maximum d'exemplaires par slot.
|
||||
* 1 = ne s'empile pas (outils, armes), 20 = ressource classique.
|
||||
@@ -75,11 +88,22 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Item")
|
||||
int32 GetEffectiveMaxStack() const { return MaxUses > 1 ? 1 : FMath::Max(1, MaxStackSize); }
|
||||
|
||||
/**
|
||||
* Pose le mesh et le materiau de l'objet sur un composant.
|
||||
*
|
||||
* Un DataAsset ne devrait pas piloter un composant, mais la regle "mesh plus
|
||||
* materiau optionnel" a deja deux appelants -- l'objet au sol et l'objet sur
|
||||
* le feu -- et en aura d'autres, l'objet en main le premier. La dupliquer
|
||||
* garantit qu'un jour l'un des deux oubliera le materiau.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Item")
|
||||
void ApplyWorldVisualsTo(class UStaticMeshComponent* Component) const;
|
||||
|
||||
/**
|
||||
* Slots ajoutes a l'inventaire quand cet objet est equipe comme sac.
|
||||
* Laisse a 0 pour tout ce qui n'est pas un contenant.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Sac", meta = (ClampMin = "0"))
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Backpack", meta = (ClampMin = "0"))
|
||||
int32 BonusInventorySlots = 0;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@@ -87,18 +111,101 @@ public:
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Coche pour qu'un clic gauche consomme l'objet et applique ses effets. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable")
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable")
|
||||
bool bIsConsumable = false;
|
||||
|
||||
/** Faim rendue a la consommation. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
|
||||
float HungerRestore = 0.f;
|
||||
|
||||
/** Soif rendue. Une valeur negative deshydrate -- utile pour l'eau salee. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
|
||||
float ThirstRestore = 0.f;
|
||||
|
||||
/** Vie rendue. Negative pour un aliment avarie ou toxique. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consommable", meta = (EditCondition = "bIsConsumable"))
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Consumable", meta = (EditCondition = "bIsConsumable"))
|
||||
float HealthRestore = 0.f;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Cuisson
|
||||
//
|
||||
// Modele en CHAINE D'OBJETS, sans recette : cuire n'est pas un choix que le
|
||||
// joueur fait dans un menu, c'est une propriete de l'objet. "Steak cru"
|
||||
// pointe vers "Steak cuit", qui pointe vers "Steak brule". Chaque maillon
|
||||
// est un asset complet, donc le changement de mesh sur le feu ne coute pas
|
||||
// une ligne : c'est le WorldMesh du maillon suivant.
|
||||
//
|
||||
// Ce qui n'est PAS ici, et ne doit jamais y venir : le temps deja passe sur
|
||||
// le feu. C'est de l'etat, il vit sur ACookingStation. Le mettre dans le
|
||||
// DataAsset ferait cuire tous les steaks du monde en meme temps.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Cet objet se transforme quand on le pose sur un feu allume. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking")
|
||||
bool bCanBeCooked = false;
|
||||
|
||||
/** Secondes de feu ACTIF necessaires. Un feu eteint met le compteur en pause. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBeCooked", ClampMin = "0.1", Units = "s"))
|
||||
float CookDuration = 20.f;
|
||||
|
||||
/** Ce que l'objet devient une fois cuit. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBeCooked"))
|
||||
TObjectPtr<UItemDataAsset> CookedResult;
|
||||
|
||||
/**
|
||||
* L'objet se degrade s'il reste sur le feu.
|
||||
*
|
||||
* Volontairement independant de bCanBeCooked : un objet peut bruler sans
|
||||
* jamais avoir ete cuisinable (une planche jetee dans les flammes), et un
|
||||
* plat peut rester indefiniment au chaud sans jamais se gacher. Une seule
|
||||
* chaine commune imposerait l'un ou l'autre a tout le monde.
|
||||
*
|
||||
* Si les deux sont coches, la CUISSON passe en premier : c'est le maillon
|
||||
* cuit qui portera bCanBurn pour continuer vers le brule.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking")
|
||||
bool bCanBurn = false;
|
||||
|
||||
/** Secondes de feu actif avant que l'objet se gache. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBurn", ClampMin = "0.1", Units = "s"))
|
||||
float BurnDuration = 40.f;
|
||||
|
||||
/** Ce que l'objet devient une fois brule. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Cooking", meta = (EditCondition = "bCanBurn"))
|
||||
TObjectPtr<UItemDataAsset> BurntResult;
|
||||
|
||||
/**
|
||||
* Le feu accepte-t-il cet objet sur un emplacement de cuisson.
|
||||
*
|
||||
* Un maillon final -- le steak brule -- repond non : on ne peut plus le
|
||||
* poser, et il ne fait plus rien s'il est deja la. La regle est centralisee
|
||||
* ici pour que la station n'ait pas a la reconstruire a chaque appel.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Cooking")
|
||||
bool CanBePlacedOnFire() const
|
||||
{
|
||||
return (bCanBeCooked && CookedResult != nullptr) || (bCanBurn && BurntResult != nullptr);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Carburant
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Cet objet peut alimenter un feu. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Fuel")
|
||||
bool bIsFuel = false;
|
||||
|
||||
/** Secondes de combustion apportees par exemplaire. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Fuel", meta = (EditCondition = "bIsFuel", ClampMin = "1", Units = "s"))
|
||||
float FuelDuration = 60.f;
|
||||
|
||||
#if WITH_EDITOR
|
||||
/**
|
||||
* Une chaine de cuisson mal branchee ne planterait pas : la viande resterait
|
||||
* simplement crue sur le feu, sans rien dire. C'est exactement 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
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
void InitPickup(UItemDataAsset* InItemData, int32 InQuantity, int32 InRemainingUses = -1);
|
||||
|
||||
// --- IInteractable ---
|
||||
virtual FText GetInteractionPrompt_Implementation() const override;
|
||||
virtual FText GetInteractionPrompt_Implementation(AActor* Interactor) const override;
|
||||
virtual bool CanInteract_Implementation(AActor* Interactor) const override;
|
||||
virtual void Interact_Implementation(AActor* Interactor) override;
|
||||
virtual void OnBeginFocus_Implementation() override;
|
||||
|
||||
@@ -11,7 +11,11 @@ public class Survival_projet : ModuleRules
|
||||
// GameplayTags : Enhanced Input rend ses erreurs de remappage sous forme
|
||||
// de FGameplayTagContainer. Sans ce module on peut declarer le type mais
|
||||
// pas l'inspecter, et on perdrait la raison d'un echec dans le log.
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags", "UMG" });
|
||||
// Niagara : ACookingStation pilote sa flamme depuis le C++ (allumage,
|
||||
// extinction). Passer par un evenement Blueprint marcherait aussi, mais
|
||||
// l'etat du feu appartient au C++ -- le Blueprint ne doit brancher que
|
||||
// l'asset, pas decider quand il s'allume.
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags", "UMG", "Niagara" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user