Files
Unreal_EmberWild/Source/Survival_projet/Private/StorageContainer.cpp
T
Mathew 41d6e6a7f3 (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>
2026-08-01 22:51:24 +02:00

225 lines
6.4 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "StorageContainer.h"
#include "Components/BoxComponent.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/CollisionProfile.h"
#include "Engine/StaticMesh.h"
#include "FpsPlayerController.h"
#include "GameFramework/Pawn.h"
#include "InventoryComponent.h"
#include "ItemDataAsset.h"
#include "Kismet/GameplayStatics.h"
#define LOCTEXT_NAMESPACE "StorageContainer"
AStorageContainer::AStorageContainer()
{
// Le tick n'existe que pour animer le couvercle : il demarre eteint et ne
// s'allume que le temps du mouvement.
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMesh"));
SetRootComponent(BaseMesh);
// Les meshes ne portent AUCUNE collision : elle vit entierement dans la
// boite. Un mesh de pack sans collision simple rendrait le coffre muet sans
// qu'aucune erreur ne le signale, et celle du couvercle partirait avec lui
// des l'ouverture.
BaseMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
InteractionBox = CreateDefaultSubobject<UBoxComponent>(TEXT("InteractionBox"));
InteractionBox->SetupAttachment(BaseMesh);
// Explicite plutot que de compter sur le profil par defaut : ce composant a
// deux roles indissociables, empecher le joueur de traverser le coffre et
// bloquer le canal Visibility pour que le trace d'interaction le trouve.
// Un profil qui laisserait passer Visibility rendrait le coffre inutilisable
// sans qu'aucune erreur ne le signale.
InteractionBox->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
// Valeur de depart pour un coffre qui n'aurait pas encore de mesh :
// FitInteractionBox() l'ecrase des qu'il y en a un.
InteractionBox->SetBoxExtent(FVector(40.f, 30.f, 30.f));
LidPivot = CreateDefaultSubobject<USceneComponent>(TEXT("LidPivot"));
LidPivot->SetupAttachment(BaseMesh);
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("LidMesh"));
LidMesh->SetupAttachment(LidPivot);
LidMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));
DisplayName = LOCTEXT("ChestName", "Chest");
InteractionPrompt = LOCTEXT("OpenChest", "Ouvrir le coffre");
}
void AStorageContainer::PreInitializeComponents()
{
// Avant InitializeComponents(), donc avant que le composant fige sa taille
// par defaut de 30 slots avec une barre rapide.
if (Storage)
{
Storage->ConfigureAsContainer(SlotCount);
}
Super::PreInitializeComponents();
}
void AStorageContainer::BeginPlay()
{
Super::BeginPlay();
// Le couvercle repose a zero, quelle que soit la rotation laissee dans le
// Blueprint : le repos EST la reference de l'animation.
LidAlpha = 0.f;
ApplyLidRotation();
if (!Storage)
{
return;
}
for (const FStorageStartingItem& Entry : StartingItems)
{
if (!Entry.Item || Entry.Quantity <= 0)
{
continue;
}
const int32 Left = Storage->AddItem(Entry.Item, Entry.Quantity);
if (Left > 0)
{
UE_LOG(LogTemp, Warning, TEXT("%s : %d x %s n'ont pas tenu dans le coffre (%d cases)."),
*GetName(), Left, *GetNameSafe(Entry.Item), SlotCount);
}
}
}
void AStorageContainer::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
// Tourne aussi dans l'editeur : la boite se recale a l'instant ou tu
// changes de mesh, sans avoir a lancer le jeu pour t'en assurer.
if (bAutoFitInteractionBox)
{
FitInteractionBox();
}
}
void AStorageContainer::FitInteractionBox()
{
if (!InteractionBox || !BaseMesh)
{
return;
}
// Volume combine des deux meshes, exprime dans le repere de la caisse --
// celui de la boite, qui en est fille.
FBox Combined(ForceInit);
const UStaticMeshComponent* Meshes[] = { BaseMesh, LidMesh };
for (const UStaticMeshComponent* Component : Meshes)
{
const UStaticMesh* Mesh = Component ? Component->GetStaticMesh() : nullptr;
if (!Mesh)
{
continue;
}
// Transform RELATIVE a la racine et non absolue : la boite doit rester
// juste quel que soit l'endroit ou l'acteur est pose dans le niveau.
const FTransform Relative = Component->GetComponentTransform().GetRelativeTransform(BaseMesh->GetComponentTransform());
Combined += Mesh->GetBoundingBox().TransformBy(Relative);
}
if (!Combined.IsValid)
{
return;
}
InteractionBox->SetRelativeLocation(Combined.GetCenter());
InteractionBox->SetBoxExtent(Combined.GetExtent() + FVector(InteractionBoxPadding));
}
FText AStorageContainer::GetInteractionPrompt_Implementation(AActor* Interactor) const
{
return InteractionPrompt;
}
void AStorageContainer::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->OpenStorage(this);
}
void AStorageContainer::SetOpen(bool bInOpen)
{
if (bOpen == bInOpen)
{
return;
}
bOpen = bInOpen;
if (USoundBase* Sound = bOpen ? OpenSound : CloseSound)
{
UGameplayStatics::PlaySoundAtLocation(this, Sound, GetActorLocation());
}
// Le tick s'eteint tout seul en fin de course. Le rallumer ici suffit, y
// compris si l'animation precedente n'etait pas terminee : l'alpha repart
// de sa valeur courante, donc refermer un coffre a moitie ouvert ne saute
// pas.
SetActorTickEnabled(true);
}
void AStorageContainer::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
const float Target = bOpen ? 1.f : 0.f;
const float Speed = 1.f / FMath::Max(0.05f, LidDuration);
LidAlpha = FMath::FInterpConstantTo(LidAlpha, Target, DeltaTime, Speed);
ApplyLidRotation();
if (FMath::IsNearlyEqual(LidAlpha, Target))
{
// On force la valeur exacte avant de se rendormir : sans ca le couvercle
// resterait a un poil de sa position finale pour le reste de la partie.
LidAlpha = Target;
ApplyLidRotation();
SetActorTickEnabled(false);
}
}
void AStorageContainer::ApplyLidRotation()
{
if (!LidPivot)
{
return;
}
// Lissage aux extremites : un couvercle a vitesse constante demarre et
// s'arrete d'un coup, ce qui se lit comme une saccade sur un objet lourd.
const float Eased = FMath::SmoothStep(0.f, 1.f, LidAlpha);
LidPivot->SetRelativeRotation(OpenRotation * Eased);
}
#undef LOCTEXT_NAMESPACE