// 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(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(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(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(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(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& 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& 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 Found; GetComponents(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() : 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