// 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" #include "Net/UnrealNetwork.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; // Acteur du monde : contenu et couvercle appartiennent au serveur. // Le UInventoryComponent, lui, se replique deja tout seul -- c'est la // classe entiere qui le fait, coffre comme sac. bReplicates = true; SetReplicateMovement(false); BaseMesh = CreateDefaultSubobject(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(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(TEXT("LidPivot")); LidPivot->SetupAttachment(BaseMesh); LidMesh = CreateDefaultSubobject(TEXT("LidMesh")); LidMesh->SetupAttachment(LidPivot); LidMesh->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName); Storage = CreateDefaultSubobject(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(Interactor); AFpsPlayerController* PlayerController = InteractingPawn ? Cast(InteractingPawn->GetController()) : nullptr; if (!PlayerController) { return; } PlayerController->OpenStorage(this); } void AStorageContainer::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AStorageContainer, bOpen); } void AStorageContainer::SetOpen(bool bInOpen) { // Le couvercle appartient au monde : seul le serveur en decide, et la // replication l'annonce aux autres. Un client qui forcerait bOpen verrait // son coffre s'ouvrir seul, et le prochain OnRep le refermerait. if (!HasAuthority() || bOpen == bInOpen) { return; } bOpen = bInOpen; ApplyOpenState(); } void AStorageContainer::OnRep_Open() { ApplyOpenState(); } void AStorageContainer::ApplyOpenState() { 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