(Feat) Add Storage Chests + Item Details Panel
Coffres : AStorageContainer, un UInventoryComponent configure en conteneur, affiche comme panneau de WBP_InventoryScreen plutot que dans un ecran a lui. Deplacement unifie par UInventoryComponent::TransferSlot / TransferAllTo. Panneau de detail : UItemDetailsWidget, pose DANS WBP_Inventory a droite de la grille. Icone, nom, separation, description, barre d'usure. Il recoit un couple (inventaire, index) et s'abonne a OnInventoryChanged, donc une charge consommee sous le curseur se voit. Une case ne connait plus sa grille : elle diffuse OnHoverChanged. La barre rapide s'y abonne aussi et relaie par le PlayerController, seul a posseder a la fois le HUD et l'ecran. Le panneau efface la case qu'on QUITTE et non lui-meme, sinon passer de la barre a la grille viderait ce qui vient d'etre affiche. L'ecran allume et eteint le panneau (SetItemDetailsEnabled) : WBP_Inventory etant instancie deux fois en mode coffre, une case a cocher par instance serait un bug qui attend qu'on oublie de la decocher. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
#include "PauseMenuWidget.h"
|
||||
#include "ScreenFadeComponent.h"
|
||||
#include "SettingsMenuWidget.h"
|
||||
#include "StorageContainer.h"
|
||||
#include "SurvivalStatsWidget.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "PauseMenu"
|
||||
@@ -43,6 +44,13 @@ namespace
|
||||
constexpr int32 ZOrderInventory = 20;
|
||||
constexpr int32 ZOrderHotbar = 30;
|
||||
|
||||
/**
|
||||
* Periode de surveillance de l'eloignement du coffre. Quatre fois par
|
||||
* seconde suffit largement : c'est une comparaison de distance, pas une
|
||||
* requete physique, et un quart de seconde de tolerance ne se voit pas.
|
||||
*/
|
||||
constexpr float StorageDistanceCheckPeriod = 0.25f;
|
||||
|
||||
// Le menu de pause passe devant tout le HUD, et la boite de confirmation
|
||||
// devant lui. Meme echelle que dans le menu principal.
|
||||
//
|
||||
@@ -119,8 +127,9 @@ void AFpsPlayerController::OnPossess(APawn* InPawn)
|
||||
|
||||
void AFpsPlayerController::HandlePawnDied()
|
||||
{
|
||||
// L'inventaire ouvert au moment de la mort resterait affiche par-dessus
|
||||
// l'ecran noir, curseur compris.
|
||||
// L'ecran ouvert au moment de la mort resterait affiche par-dessus l'ecran
|
||||
// noir, curseur compris. Sa fermeture rend le coffre au passage, couvercle
|
||||
// compris.
|
||||
if (bInventoryOpen)
|
||||
{
|
||||
ToggleInventory();
|
||||
@@ -265,17 +274,23 @@ void AFpsPlayerController::UpdateHotbarVisibility()
|
||||
Hotbar->SetVisibility(bHideHotbar ? ESlateVisibility::Collapsed : HotbarDefaultVisibility);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::HandleHotbarSlotHovered(UInventoryComponent* Inventory, int32 SlotIndex, bool bHovered)
|
||||
{
|
||||
if (InventoryScreen && bInventoryOpen)
|
||||
{
|
||||
InventoryScreen->NotifyExternalSlotHovered(Inventory, SlotIndex, bHovered);
|
||||
}
|
||||
}
|
||||
|
||||
void AFpsPlayerController::DropHoveredItem()
|
||||
{
|
||||
if (!bInventoryOpen || !InventoryScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
UInventoryComponent* HoveredInventory = nullptr;
|
||||
int32 SlotIndex = INDEX_NONE;
|
||||
|
||||
// Renvoie INDEX_NONE quand l'onglet Fabrication est affiche : la touche ne
|
||||
// doit pas agir sur une case cachee derriere la grille de craft.
|
||||
const int32 SlotIndex = InventoryScreen->GetHoveredSlotIndex();
|
||||
if (SlotIndex == INDEX_NONE)
|
||||
// L'ecran renvoie INDEX_NONE quand l'onglet Fabrication est affiche : la
|
||||
// touche ne doit pas agir sur une case cachee derriere la grille de craft.
|
||||
// Il consulte le panneau de coffre en premier quand il est ouvert.
|
||||
if (!bInventoryOpen || !InventoryScreen || !InventoryScreen->GetHoveredSlot(HoveredInventory, SlotIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -291,8 +306,10 @@ void AFpsPlayerController::DropHoveredItem()
|
||||
// modal, pas une commande distincte.
|
||||
const bool bSingle = IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);
|
||||
|
||||
// MAX_int32 pour "tout" : DropSlot borne de lui-meme a ce que contient le slot.
|
||||
PlayerPawn->DropSlot(SlotIndex, bSingle ? 1 : MAX_int32);
|
||||
// MAX_int32 pour "tout" : la fonction borne d'elle-meme a ce que contient
|
||||
// le slot. On passe l'inventaire survole et non celui du pawn : viser une
|
||||
// case du coffre doit sortir l'objet du coffre.
|
||||
PlayerPawn->DropFromInventory(HoveredInventory, SlotIndex, bSingle ? 1 : MAX_int32);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::CreateHudWidgets()
|
||||
@@ -351,6 +368,12 @@ void AFpsPlayerController::CreateHudWidgets()
|
||||
// reviendra en quittant l'onglet Fabrication.
|
||||
HotbarDefaultVisibility = Hotbar->GetVisibility();
|
||||
Hotbar->AddToViewport(ZOrderHotbar);
|
||||
|
||||
// La barre est un widget de HUD, l'ecran d'inventaire un autre : ni
|
||||
// l'un ni l'autre ne se connait. Le controller les possede tous les
|
||||
// deux, c'est donc lui qui fait le pont -- exactement comme pour la
|
||||
// visibilite de la barre sur l'onglet Fabrication.
|
||||
Hotbar->OnSlotHovered.AddDynamic(this, &AFpsPlayerController::HandleHotbarSlotHovered);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +441,11 @@ void AFpsPlayerController::ToggleCrafting()
|
||||
return;
|
||||
}
|
||||
|
||||
// Le panneau de coffre est un FRERE du TabSwitcher : il resterait affiche a
|
||||
// cote de la grille de recettes. On rend le coffre plutot que d'ignorer la
|
||||
// touche, qui exprime une intention claire.
|
||||
ReleaseActiveStorage();
|
||||
|
||||
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
|
||||
}
|
||||
|
||||
@@ -493,12 +521,13 @@ 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.
|
||||
// rend le poste ET le coffre. 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, ni un couvercle leve derriere soi.
|
||||
if (!bInventoryOpen)
|
||||
{
|
||||
ReleaseActiveStation();
|
||||
ReleaseActiveStorage();
|
||||
}
|
||||
|
||||
if (bInventoryOpen)
|
||||
@@ -519,9 +548,14 @@ void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCrafting
|
||||
// sur l'onglet Fabrication.
|
||||
UpdateHotbarVisibility();
|
||||
|
||||
SetUiInputMode(bInventoryOpen);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::SetUiInputMode(bool bUiOpen)
|
||||
{
|
||||
AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
|
||||
|
||||
if (bInventoryOpen)
|
||||
if (bUiOpen)
|
||||
{
|
||||
// GameAndUI et non UIOnly : en UIOnly, plus aucune action Enhanced
|
||||
// Input ne passerait et on ne pourrait plus refermer avec Tab.
|
||||
@@ -564,6 +598,109 @@ void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCrafting
|
||||
}
|
||||
}
|
||||
|
||||
void AFpsPlayerController::OpenStorage(AStorageContainer* Container)
|
||||
{
|
||||
if (!Container || !IsLocalController() || !InventoryScreen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Deja devant un autre coffre : on rend le precedent avant d'ouvrir le
|
||||
// nouveau, sinon son couvercle resterait leve pour le reste de la partie.
|
||||
ReleaseActiveStorage();
|
||||
|
||||
ActiveStorage = Container;
|
||||
bStorageOpen = true;
|
||||
|
||||
InventoryScreen->ShowStorage(Container);
|
||||
Container->SetOpen(true);
|
||||
|
||||
// La barre rapide reste visible et cliquable par-dessus l'ecran : on lui
|
||||
// donne le coffre pour que le clic droit y marche aussi.
|
||||
if (Hotbar)
|
||||
{
|
||||
Hotbar->SetQuickTransferTarget(Container->GetStorage());
|
||||
}
|
||||
|
||||
// Le coffre n'est pas un ecran a part : c'est un panneau de plus sur
|
||||
// l'ecran d'inventaire, sur l'onglet Inventaire. Consequence directe -- le
|
||||
// sac du joueur ne bouge pas d'un pixel entre les deux situations.
|
||||
SetInventoryScreenOpen(true, /*bShowCraftingTab=*/false);
|
||||
|
||||
// La surveillance ne tourne QUE pendant l'ecran : un timer permanent pour
|
||||
// une condition vraie deux minutes par partie serait du gaspillage.
|
||||
GetWorldTimerManager().SetTimer(
|
||||
StorageDistanceTimerHandle, this, &AFpsPlayerController::CheckStorageDistance,
|
||||
StorageDistanceCheckPeriod, /*bLoop=*/true);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::CloseStorage()
|
||||
{
|
||||
if (!bStorageOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Le panneau vit dans l'ecran d'inventaire : le refermer ferme l'ecran
|
||||
// entier, qui rend le coffre au passage par ReleaseActiveStorage().
|
||||
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
|
||||
}
|
||||
|
||||
void AFpsPlayerController::ReleaseActiveStorage()
|
||||
{
|
||||
if (!bStorageOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bStorageOpen = false;
|
||||
|
||||
GetWorldTimerManager().ClearTimer(StorageDistanceTimerHandle);
|
||||
|
||||
if (AStorageContainer* Container = ActiveStorage.Get())
|
||||
{
|
||||
Container->SetOpen(false);
|
||||
}
|
||||
ActiveStorage.Reset();
|
||||
|
||||
if (InventoryScreen)
|
||||
{
|
||||
InventoryScreen->HideStorage();
|
||||
}
|
||||
|
||||
if (Hotbar)
|
||||
{
|
||||
Hotbar->SetQuickTransferTarget(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void AFpsPlayerController::CheckStorageDistance()
|
||||
{
|
||||
if (!bStorageOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const AStorageContainer* Container = ActiveStorage.Get();
|
||||
const APawn* PlayerPawn = GetPawn();
|
||||
|
||||
// Coffre detruit ou joueur sans corps : dans les deux cas l'ecran pilote
|
||||
// quelque chose qui n'existe plus.
|
||||
if (!Container || !PlayerPawn)
|
||||
{
|
||||
CloseStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
// DistSquared : on evite une racine carree quatre fois par seconde pour une
|
||||
// comparaison qui n'a pas besoin de la distance reelle.
|
||||
const float MaxDistSquared = FMath::Square(MaxStorageDistance);
|
||||
if (FVector::DistSquared(Container->GetActorLocation(), PlayerPawn->GetActorLocation()) > MaxDistSquared)
|
||||
{
|
||||
CloseStorage();
|
||||
}
|
||||
}
|
||||
|
||||
void AFpsPlayerController::HandlePauseInput()
|
||||
{
|
||||
// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
|
||||
@@ -582,8 +719,15 @@ void AFpsPlayerController::HandlePauseInput()
|
||||
return;
|
||||
}
|
||||
|
||||
// Echap sert d'abord a annuler ce qui est ouvert. Fermer l'inventaire est
|
||||
// ce que le joueur attend en premier.
|
||||
// Echap sert d'abord a annuler ce qui est ouvert. Le coffre passe avant
|
||||
// l'inventaire : les deux ne sont jamais ouverts ensemble, mais l'ordre
|
||||
// resterait juste le jour ou ca changerait.
|
||||
if (bStorageOpen)
|
||||
{
|
||||
CloseStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bInventoryOpen)
|
||||
{
|
||||
ToggleInventory();
|
||||
@@ -847,6 +991,10 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
InventoryScreen = nullptr;
|
||||
}
|
||||
|
||||
// Le timer d'eloignement rappellerait une methode d'un controller en train
|
||||
// de disparaitre. ClearTimer est sans effet s'il ne tourne pas.
|
||||
GetWorldTimerManager().ClearTimer(StorageDistanceTimerHandle);
|
||||
|
||||
if (InteractionPrompt)
|
||||
{
|
||||
InteractionPrompt->RemoveFromParent();
|
||||
|
||||
Reference in New Issue
Block a user