// Fill out your copyright notice in the Description page of Project Settings.


#include "FpsPlayerController.h"

#include "Blueprint/UserWidget.h"
#include "ConfirmDialogWidget.h"
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
#include "SurvivalGameInstance.h"
#include "SurvivalPlayerState.h"
#include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"
#include "HotbarWidget.h"
#include "InputActionValue.h"
#include "InputCoreTypes.h"
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "CraftingComponent.h"
#include "InventoryScreenWidget.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerState.h"
#include "Interfaces/OnlineIdentityInterface.h"
#include "Kismet/GameplayStatics.h"
#include "OnlineSubsystem.h"
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
#include "SessionSubsystem.h"
#include "SettingsMenuWidget.h"
#include "StorageContainer.h"
#include "SurvivalStatsWidget.h"

#define LOCTEXT_NAMESPACE "PauseMenu"

namespace
{
	// Ordre d'empilement des widgets plein ecran, du fond vers l'avant.
	//
	// La barre rapide passe AU-DESSUS de l'inventaire, et pas l'inverse : le
	// Background Blur de l'inventaire floute tout ce qui est rendu en dessous
	// de lui. Avec un ZOrder inferieur, la barre se retrouverait floutee et
	// inutilisable des l'ouverture de l'inventaire.
	//
	// Bonus : etant devant, elle reste une cible de depot valide -- on peut
	// glisser un objet de la grille directement dans la barre.
	//
	// Pas de 10 pour laisser de la place aux widgets a venir.
	constexpr int32 ZOrderCrosshair = 0;
	constexpr int32 ZOrderSurvivalStats = 5;
	constexpr int32 ZOrderInteractionPrompt = 10;
	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.
	//
	// Les noms doivent rester uniques dans TOUT le module : le build unity
	// concatene les .cpp, et un namespace anonyme n'isole plus rien. D'ou le
	// prefixe InGame, qui evite la collision avec le menu principal.
	constexpr int32 ZOrderPauseMenu = 100;
	constexpr int32 ZOrderInGameSettingsMenu = 150;
	constexpr int32 ZOrderQuitToMenuDialog = 200;
}

AFpsPlayerController::AFpsPlayerController()
{
	// Le fondu est un systeme a part entiere, donc un composant -- et le menu
	// principal reutilise exactement le meme.
	//
	// Rappel Unreal : ce constructeur tourne aussi dans l'editeur, sur le CDO.
	// Il ne doit contenir que de la construction, jamais de logique de jeu.
	ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));

	QuitToMenuMessage = LOCTEXT("QuitToMenuConfirm", "Retourner au menu principal ?\nTa progression sera perdue.");
}

void AFpsPlayerController::BeginPlay()
{
	Super::BeginPlay();


	// Le mode d'input vit sur le VIEWPORT, pas sur le controller. OpenLevel
	// detruit le controller du menu principal mais pas la fenetre : son
	// GameAndUI et son curseur visible restent en place, et la camera ne
	// repondrait qu'au clic droit maintenu. On ne suppose donc jamais l'etat
	// laisse par la map precedente, on le repose.
	if (IsLocalController())
	{
		SetInputMode(FInputModeGameOnly());
		SetShowMouseCursor(false);

		// Annonce notre pseudo Steam. Seul le client connait le sien : l'API
		// d'identite ne renseigne que l'utilisateur local de chaque machine.
		const FString Nickname = ResolveLocalPlayerNickname();
		if (!Nickname.IsEmpty())
		{
			// L'hote est deja l'autorite : passer par la RPC marcherait, mais
			// l'appel direct evite un aller-retour pour rien.
			if (HasAuthority())
			{
				Server_SetPlayerNickname_Implementation(Nickname);
			}
			else
			{
				Server_SetPlayerNickname(Nickname);
			}
		}

		// Dans la foulee du pseudo : les deux repondent au meme constat, le
		// serveur ne peut connaitre ni l'un ni l'autre. Ils partent donc
		// ensemble, au seul moment ou l'on est certain que le PlayerState local
		// existe.
		AnnounceCharacterAppearance();
	}

	// Le delegue se declenche a la fin de CHAQUE fondu : c'est
	// bQuittingToMainMenu qui distingue le fondu de sortie du reste.
	ScreenFade->OnFadeFinished.AddDynamic(this, &AFpsPlayerController::HandleFadeFinished);

	// Une invitation peut tomber en pleine partie : sans ce fondu, l'ecran
	// sauterait directement dans le monde de l'autre hote.
	if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
	{
		Sessions->OnTravelPending.AddDynamic(this, &AFpsPlayerController::HandleSessionTravelPending);
	}

	// IsLocalController est INDISPENSABLE ici : sur la machine de l'hote, chaque
	// joueur distant a AUSSI un controller cote serveur. Sans cette garde, le
	// controller d'un ami qui se connecte irait poser un fondu et rallumer le
	// rendu du viewport de l'HOTE, a un moment totalement arbitraire.
	if (bFadeInOnStart && IsLocalController())
	{
		// On NOIRCIT et on attend que la camera soit sur notre corps, au lieu
		// d'eclaircir tout de suite. Un client demarre sur un SpectatorPawn ne
		// au centre du monde -- sous le terrain -- et y reste encore un instant
		// apres l'acquittement de la possession. C'est TryStartupFadeIn qui
		// decidera du bon moment.
		ScreenFade->SnapToBlack();
		bStartupFadePending = true;

		// Filet : si la possession n'arrivait jamais, l'ecran resterait noir a
		// vie et le joueur croirait le jeu plante.
		// Le filet appelle CompleteStartupFadeIn et non TryStartupFadeIn : sans
		// ca il retomberait dans la boucle de reessai et l'ecran resterait noir
		// pour toujours.
		GetWorldTimerManager().SetTimer(
			StartupFadeTimeoutHandle, this, &AFpsPlayerController::CompleteStartupFadeIn,
			FMath::Max(StartupFadeTimeout, 1.f), /*bLoop=*/false);

		// L'hote possede deja son pawn ET sa camera est dessus : il n'attendra
		// rien. Le test reel est dans TryStartupFadeIn.
		TryStartupFadeIn();
	}
	else if (IsLocalController())
	{
		// Mode sans fondu : personne ne rallumera le rendu, il faut le faire ici
		// sous peine d'ecran noir jusqu'au filet de securite.
		if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
		{
			SurvivalGameInstance->ResumeWorldRendering();
		}
	}
}

void AFpsPlayerController::TryStartupFadeIn()
{
	if (!bStartupFadePending)
	{
		return;
	}

	// Un controller distant vu depuis le serveur n'a pas d'ecran a eclaircir, et
	// surtout pas celui de l'hote.
	if (!IsLocalController())
	{
		return;
	}

	// La camera doit deja regarder par NOS yeux. Un client demarre sur un
	// SpectatorPawn ne au centre du monde -- sous le terrain -- et y reste
	// encore un instant apres l'acquittement de la possession. Eclaircir a cet
	// instant montre exactement ca.
	const APawn* OwnPawn = GetPawn();
	if (!OwnPawn || GetViewTarget() != OwnPawn)
	{
		GetWorldTimerManager().SetTimer(
			StartupFadeRetryHandle, this, &AFpsPlayerController::TryStartupFadeIn,
			StartupFadeRetry, /*bLoop=*/false);
		return;
	}

	CompleteStartupFadeIn();
}

void AFpsPlayerController::CompleteStartupFadeIn()
{
	if (!bStartupFadePending)
	{
		return;
	}

	bStartupFadePending = false;
	GetWorldTimerManager().ClearTimer(StartupFadeTimeoutHandle);
	GetWorldTimerManager().ClearTimer(StartupFadeRetryHandle);

	// Le rendu du monde est suspendu depuis le chargement de la map : on le
	// rend AVANT le fondu, alors que le voile est encore parfaitement noir.
	// L'ordre inverse laisserait passer les images qu'on cherche a cacher.
	USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
	if (!SurvivalGameInstance)
	{
		StartStartupFade();
		return;
	}

	SurvivalGameInstance->ResumeWorldRendering();

	// On laisse le monde rendre quelques images DERRIERE le voile noir avant de
	// l'eclaircir : c'est ce qui donne a Nanite, aux ombres virtuelles et a
	// Lumen le temps de converger sans que personne le voie.
	SurvivalGameInstance->BeginSceneWarmup(
		FSimpleDelegate::CreateUObject(this, &AFpsPlayerController::StartStartupFade));
}

void AFpsPlayerController::StartStartupFade()
{
	ScreenFade->FadeIn(StartupFadeDuration);
}

void AFpsPlayerController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	SetupForPossessedPawn(InPawn);
}

void AFpsPlayerController::AcknowledgePossession(APawn* P)
{
	Super::AcknowledgePossession(P);


	// Le chemin CLIENT. Sans lui, un joueur qui rejoint n'a aucun HUD : le
	// serveur a bien appele OnPossess, mais chez lui, pas chez le client.
	SetupForPossessedPawn(P);
}

void AFpsPlayerController::SetupForPossessedPawn(APawn* InPawn)
{
	// On cree les widgets ici et pas dans BeginPlay : a ce stade le pawn est
	// garanti possede, et le prompt comme l'inventaire ont besoin de lui pour
	// trouver leurs composants des leur NativeConstruct.
	CreateHudWidgets();

	// Apres un respawn les widgets existent deja, mais pointent vers les
	// composants du cadavre. On les rebranche sur le nouveau pawn.
	if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); }
	if (Hotbar) { Hotbar->BindToOwningPawn(); }
	if (InventoryScreen) { InventoryScreen->BindToOwningPawn(); }
	if (SurvivalStatsWidget) { SurvivalStatsWidget->BindToOwningPawn(); }

	if (AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(InPawn))
	{
		if (USurvivalStatsComponent* Stats = PlayerPawn->GetSurvivalStats())
		{
			// AddUnique et non Add : chez l'hote, OnPossess ET
			// AcknowledgePossession passent tous les deux ici. Un double
			// abonnement declencherait deux fondus de mort et deux respawns.
			Stats->OnDied.AddUniqueDynamic(this, &AFpsPlayerController::HandlePawnDied);
		}
	}

	// Le corps est la : on peut enfin montrer le monde. Sans effet si le fondu
	// d'arrivee a deja eu lieu, ce qui est le cas a chaque respawn.
	TryStartupFadeIn();
}

void AFpsPlayerController::HandlePawnDied()
{
	// 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();
	}

	ScreenFade->FadeOut(DeathFadeDuration);

	GetWorldTimerManager().SetTimer(
		RespawnTimerHandle, this, &AFpsPlayerController::RespawnPlayer,
		FMath::Max(RespawnDelay, DeathFadeDuration), /*bLoop=*/false);
}

void AFpsPlayerController::RespawnPlayer()
{
	// Le fondu de retour est purement local : il appartient a l'ecran de ce
	// joueur, et il doit remonter meme si la reponse du serveur tarde.
	ScreenFade->FadeIn(StartupFadeDuration);

	if (!HasAuthority())
	{
		Server_RequestRespawn();
		return;
	}

	Server_RequestRespawn_Implementation();
}

void AFpsPlayerController::Server_RequestRespawn_Implementation()
{
	// On depossede AVANT de detruire : detruire un pawn encore possede laisse
	// le controller dans un etat incoherent le temps d'une frame.
	APawn* PreviousPawn = GetPawn();
	UnPossess();
	if (IsValid(PreviousPawn))
	{
		PreviousPawn->Destroy();
	}

	// RestartPlayer fait apparaitre un nouveau pawn sur un PlayerStart et le
	// possede. Le nouveau USurvivalStatsComponent repart plein tout seul dans
	// son BeginPlay : rien a reinitialiser a la main.
	if (AGameModeBase* GameMode = GetWorld()->GetAuthGameMode())
	{
		GameMode->RestartPlayer(this);
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : aucun GameMode, impossible de reapparaitre."));
	}
}

void AFpsPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(InputComponent);
	if (!Input)
	{
		return;
	}

	if (ToggleInventoryAction)
	{
		Input->BindAction(ToggleInventoryAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleInventory);
	}

	if (ToggleCraftingAction)
	{
		Input->BindAction(ToggleCraftingAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleCrafting);
	}

	if (DropItemAction)
	{
		Input->BindAction(DropItemAction, ETriggerEvent::Started, this, &AFpsPlayerController::DropHoveredItem);
	}

	// La barre rapide est bindee sur le controller et non sur le pawn : ainsi
	// changer d'objet actif reste possible meme quand l'inventaire est ouvert
	// et que les inputs du pawn sont coupes.
	if (HotbarSelectAction)
	{
		Input->BindAction(HotbarSelectAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandleHotbarSelect);
	}

	if (HotbarScrollAction)
	{
		Input->BindAction(HotbarScrollAction, ETriggerEvent::Triggered, this, &AFpsPlayerController::HandleHotbarScroll);
	}

	if (PauseAction)
	{
		Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandleEscapeInput);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseAction n'est pas assignee, le menu de pause sera inaccessible."));
	}
}

UInventoryComponent* AFpsPlayerController::GetPawnInventory() const
{
	AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
	return PlayerPawn ? PlayerPawn->GetInventoryComponent() : nullptr;
}

void AFpsPlayerController::HandleHotbarSelect(const FInputActionValue& Value)
{
	UInventoryComponent* Inventory = GetPawnInventory();
	if (!Inventory)
	{
		return;
	}

	// La valeur vient du modificateur Scalar de la touche : 1 pour la touche 1,
	// 2 pour la touche 2, etc. On repasse en index a partir de zero.
	const int32 Number = FMath::RoundToInt(Value.Get<float>());
	Inventory->SelectHotbarSlot(Number - 1);
}

void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value)
{
	const float Axis = Value.Get<float>();
	if (FMath::IsNearlyZero(Axis))
	{
		return;
	}

	if (UInventoryComponent* Inventory = GetPawnInventory())
	{
		// Molette vers le haut = slot precedent, comme dans la plupart des FPS.
		Inventory->CycleHotbarSelection(Axis > 0.f ? -1 : 1);
	}
}

void AFpsPlayerController::HandleInventoryTabChanged(bool bInventoryTabActive)
{
	bCraftingTabActive = !bInventoryTabActive;
	UpdateHotbarVisibility();
}

void AFpsPlayerController::UpdateHotbarVisibility()
{
	if (!Hotbar)
	{
		return;
	}

	// La barre rapide reste affichee sur l'onglet Inventaire : elle en fait
	// partie, on y glisse des objets. Elle n'a en revanche aucun sens sous la
	// page de craft, ou elle ne ferait qu'encombrer.
	// Surtout pas "bHidden" : AActor en a deja un, le masquer donne une erreur
	// de compilation (C4458) plutot qu'un simple avertissement.
	const bool bHideHotbar = bInventoryOpen && bCraftingTabActive;

	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()
{
	UInventoryComponent* HoveredInventory = nullptr;
	int32 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;
	}

	AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());
	if (!PlayerPawn)
	{
		return;
	}

	// Ctrl enfonce : un seul exemplaire, comme pour le glisser. On lit l'etat
	// de la touche plutot que d'en faire une action separee : le geste est
	// modal, pas une commande distincte.
	const bool bSingle = IsInputKeyDown(EKeys::LeftControl) || IsInputKeyDown(EKeys::RightControl);

	// 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()
{
	// Un widget n'existe que sur la machine du joueur local : inutile et
	// incorrect d'en creer un sur un serveur dedie.
	if (!IsLocalController())
	{
		return;
	}

	if (!Crosshair && CrosshairClass)
	{
		Crosshair = CreateWidget<UUserWidget>(this, CrosshairClass);
		if (Crosshair)
		{
			Crosshair->AddToViewport(ZOrderCrosshair);
		}
	}

	if (!InteractionPrompt)
	{
		if (InteractionPromptClass)
		{
			InteractionPrompt = CreateWidget<UInteractionPromptWidget>(this, InteractionPromptClass);
			if (InteractionPrompt)
			{
				InteractionPrompt->AddToViewport(ZOrderInteractionPrompt);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InteractionPromptClass n'est pas assignee, aucun prompt ne s'affichera."));
		}
	}

	// Le voile de fondu n'est plus cree ici : UScreenFadeComponent s'en charge
	// dans son propre BeginPlay, et son ZOrder de 1000 le garde devant tout le
	// HUD quel que soit l'ordre d'ajout.

	if (!SurvivalStatsWidget && SurvivalStatsClass)
	{
		SurvivalStatsWidget = CreateWidget<USurvivalStatsWidget>(this, SurvivalStatsClass);
		if (SurvivalStatsWidget)
		{
			SurvivalStatsWidget->AddToViewport(ZOrderSurvivalStats);
		}
	}

	if (!Hotbar && HotbarClass)
	{
		Hotbar = CreateWidget<UHotbarWidget>(this, HotbarClass);
		if (Hotbar)
		{
			// Relevee avant tout masquage : c'est la valeur a laquelle on
			// 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);
		}
	}

	if (!InventoryScreen)
	{
		if (InventoryScreenClass)
		{
			InventoryScreen = CreateWidget<UInventoryScreenWidget>(this, InventoryScreenClass);
			if (InventoryScreen)
			{
				// Abonnement AVANT l'ajout au viewport : c'est lui qui declenche
				// NativeConstruct, donc la premiere diffusion d'onglet. S'abonner
				// apres, on la manquerait.
				InventoryScreen->OnTabChanged.AddDynamic(this, &AFpsPlayerController::HandleInventoryTabChanged);

				InventoryScreen->AddToViewport(ZOrderInventory);
				// Cree des le depart pour que les abonnements de ses deux pages
				// soient actifs, mais cache : ainsi l'ecran reste a jour meme
				// ferme, et l'ouverture est instantanee.
				InventoryScreen->SetVisibility(ESlateVisibility::Collapsed);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryScreenClass n'est pas assignee, l'inventaire ne s'ouvrira pas."));
		}
	}

	if (!PauseMenu)
	{
		if (PauseMenuClass)
		{
			PauseMenu = CreateWidget<UPauseMenuWidget>(this, PauseMenuClass);
			if (PauseMenu)
			{
				PauseMenu->AddToViewport(ZOrderPauseMenu);
				// Cree une fois pour toutes et simplement cache, comme
				// l'inventaire : le menu de pause survit ainsi aux respawns.
				PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
			}
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseMenuClass n'est pas assignee, le menu de pause ne s'ouvrira pas."));
		}
	}
}

void AFpsPlayerController::ToggleInventory()
{
	// Tab veut dire "ouvre mon sac". Rouvrir sur l'onglet Fabrication parce
	// qu'on l'avait laisse la serait une surprise a chaque fois.
	SetInventoryScreenOpen(!bInventoryOpen, /*bShowCraftingTab=*/false);
}

void AFpsPlayerController::ToggleCrafting()
{
	// Deja sur l'onglet Fabrication : la meme touche referme, comme Tab le fait
	// pour l'inventaire. Depuis l'onglet Inventaire, elle bascule sans fermer --
	// c'est le geste attendu quand on cherche quoi construire avec ce qu'on vient
	// de ramasser.
	if (bInventoryOpen && bCraftingTabActive)
	{
		SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
		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);
}

void AFpsPlayerController::OpenCraftingAtStation(ECraftingStation Station)
{
	if (Station == ECraftingStation::Count)
	{
		return;
	}

	// Appelee depuis Interact_Implementation, donc sur le SERVEUR. Pour un
	// client distant on accorde le poste ici -- c'est ce controller-la que
	// Server_Craft interrogera -- et on demande a l'ecran de s'ouvrir la-bas.
	// Chez un hote, IsLocalController() est vrai et on passe tout droit.
	if (!IsLocalController())
	{
		GrantStation(Station);
		Client_OpenCraftingAtStation(Station);
		return;
	}

	GrantStation(Station);
	SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}

void AFpsPlayerController::Client_OpenCraftingAtStation_Implementation(ECraftingStation Station)
{
	// On rejoue le chemin local : accorder le poste sur NOTRE copie du
	// composant, sans quoi la grille de recettes filtrerait sur {Hands} et
	// l'onglet paraitrait vide devant un etabli.
	GrantStation(Station);
	SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true);
}

void AFpsPlayerController::GrantStation(ECraftingStation Station)
{
	// 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;
	}
}

void AFpsPlayerController::Server_ReleaseStation_Implementation(ECraftingStation Station)
{
	if (UCraftingComponent* Crafting = GetPawnCrafting())
	{
		Crafting->RemoveAvailableStation(Station);
	}
}

void AFpsPlayerController::Server_ReleaseStorage_Implementation(AStorageContainer* Container)
{
	if (Container)
	{
		Container->SetOpen(false);
	}
}

void AFpsPlayerController::ReleaseActiveStation()
{
	if (ActiveStation == ECraftingStation::Count)
	{
		return;
	}

	if (UCraftingComponent* Crafting = GetPawnCrafting())
	{
		Crafting->RemoveAvailableStation(ActiveStation);
	}

	// Le serveur tient sa propre liste de postes accessibles, celle qui sert a
	// valider Server_Craft. La laisser ouverte rendrait les recettes d'etabli
	// fabricables n'importe ou pour le reste de la partie.
	if (!HasAuthority())
	{
		Server_ReleaseStation(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)
	{
		return;
	}

	// Ecran deja ouvert : on ne fait que changer d'onglet. Repasser par le mode
	// d'input serait au mieux inutile, au pire nuisible -- SetIgnoreLookInput
	// gere un compteur qu'un appel en trop desequilibrerait durablement.
	if (bOpen && bInventoryOpen)
	{
		if (bShowCraftingTab)
		{
			InventoryScreen->ShowCraftingTab();
		}
		else
		{
			InventoryScreen->ShowInventoryTab();
		}
		return;
	}

	if (bOpen == bInventoryOpen)
	{
		return;
	}

	bInventoryOpen = bOpen;

	// Une fermeture, quelle qu'en soit la cause -- Tab, Echap, la mort --
	// 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)
	{
		if (bShowCraftingTab)
		{
			InventoryScreen->ShowCraftingTab();
		}
		else
		{
			InventoryScreen->ShowInventoryTab();
		}
	}

	InventoryScreen->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);

	// Fermer l'ecran doit toujours rendre la barre, meme si on l'avait laissee
	// sur l'onglet Fabrication.
	UpdateHotbarVisibility();

	SetUiInputMode(bInventoryOpen);
}

void AFpsPlayerController::SetUiInputMode(bool bUiOpen)
{
	AFpsPlayer* PlayerPawn = Cast<AFpsPlayer>(GetPawn());

	if (bUiOpen)
	{
		// GameAndUI et non UIOnly : en UIOnly, plus aucune action Enhanced
		// Input ne passerait et on ne pourrait plus refermer avec Tab.
		FInputModeGameAndUI Mode;
		Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
		Mode.SetHideCursorDuringCapture(false);
		SetInputMode(Mode);
		SetShowMouseCursor(true);

		// Le mode GameAndUI laisse passer le jeu : sans ces deux appels, la
		// souris ferait tourner la camera pendant qu'on navigue dans la grille.
		SetIgnoreLookInput(true);
		SetIgnoreMoveInput(true);

		// Les deux appels ci-dessus ne couvrent que le deplacement et le regard.
		// Accroupi, sprint, saut et interaction resteraient actifs -- appuyer
		// sur Ctrl pour glisser un objet ferait s'accroupir le personnage.
		// DisableInput retire l'input component du PAWN de la pile ; celui du
		// controller reste, donc Tab continue de repondre.
		if (PlayerPawn)
		{
			PlayerPawn->ClearTransientInputStates();
			PlayerPawn->DisableInput(this);
		}
	}
	else
	{
		SetInputMode(FInputModeGameOnly());
		SetShowMouseCursor(false);

		// Reset et non SetIgnoreXxx(false) : ces fonctions gerent un compteur,
		// et un desequilibre finirait par bloquer definitivement les inputs.
		ResetIgnoreLookInput();
		ResetIgnoreMoveInput();

		if (PlayerPawn)
		{
			PlayerPawn->EnableInput(this);
		}
	}
}

void AFpsPlayerController::OpenStorage(AStorageContainer* Container)
{
	if (!Container)
	{
		return;
	}

	// Appelee depuis Interact_Implementation, donc sur le SERVEUR. Le couvercle
	// est de l'etat de monde : il se leve ICI, et se replique aux quatre
	// joueurs. Seul l'ecran repart chez le client.
	if (!IsLocalController())
	{
		Container->SetOpen(true);
		Client_OpenStorage(Container);
		return;
	}

	if (!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);

	// SetOpen ne fait rien sans autorite : chez un client c'est la replication
	// de bOpen qui animera le couvercle. L'appel reste la pour l'hote, qui est
	// bien l'autorite et doit lever le sien tout de suite.
	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::Client_OpenStorage_Implementation(AStorageContainer* Container)
{
	// Chez le client IsLocalController() est vrai : OpenStorage prend donc sa
	// branche locale et ouvre reellement l'ecran. Le couvercle, lui, a deja ete
	// leve par le serveur et arrive par replication de bOpen.
	OpenStorage(Container);
}

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())
	{
		// Chez un client SetOpen ne fait rien : le couvercle appartient au
		// serveur. On le lui demande, sinon le coffre resterait ouvert pour les
		// trois autres joueurs alors que l'ecran est referme ici.
		if (HasAuthority())
		{
			Container->SetOpen(false);
		}
		else
		{
			Server_ReleaseStorage(Container);
		}
	}
	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::HandleEscapeInput()
{
	// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
	// on l'ignore quand meme : repondre ici refermerait le menu en laissant la
	// question posee.
	if (bQuitToMenuDialogOpen)
	{
		return;
	}

	// L'ecran de reglages consomme Echap lui-meme tant qu'il a le focus. Comme
	// pour la boite, on ne repond ici que s'il l'a perdu.
	if (bSettingsOpen)
	{
		HandleSettingsCloseRequested();
		return;
	}

	// 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();
		return;
	}

	if (bPauseMenuOpen)
	{
		ResumeGame();
		return;
	}

	OpenPauseMenu();
}

void AFpsPlayerController::OpenPauseMenu()
{
	if (!PauseMenu || bPauseMenuOpen)
	{
		return;
	}

	// Mettre en pause pendant l'ecran de mort figerait le timer de respawn : le
	// joueur resterait devant un ecran noir dont plus rien ne le sortirait.
	if (GetWorldTimerManager().IsTimerActive(RespawnTimerHandle))
	{
		return;
	}

	bPauseMenuOpen = true;

	// Open() et pas un simple SetVisibility : le menu prend AUSSI le focus
	// clavier. Sans lui, Echap ne reviendrait plus des le premier clic sur un
	// bouton -- un UButton prend le focus, et un widget focalise coupe la route
	// des touches vers Enhanced Input.
	PauseMenu->Open();

	// AUCUN SetPause : en coop la pause est impossible par nature. Cote client
	// l'appel est ignore -- la pause appartient a l'autorite -- et cote hote il
	// figerait le monde des quatre joueurs parce qu'un seul a ouvert son menu.
	// Le monde continue donc de tourner, comme dans Valheim ou Raft : ouvrir son
	// menu ne met pas a l'abri, et c'est coherent avec un jeu de survie.
	//
	// Consequence directe : il faut desormais couper l'input du PAWN, ce que la
	// pause faisait gratuitement. C'est exactement le travail de SetUiInputMode,
	// deja utilise par l'inventaire et le coffre -- un second chemin finirait
	// par diverger, et le symptome (camera qui tourne derriere le menu, sprint
	// bloque en sortant) se cherche tres longtemps.
	SetUiInputMode(true);
}

void AFpsPlayerController::ResumeGame()
{
	if (!bPauseMenuOpen)
	{
		return;
	}

	// Reprendre alors qu'une boite ou les reglages sont encore ouverts les
	// laisserait affiches par-dessus la partie relancee.
	if (bQuitToMenuDialogOpen)
	{
		HandleQuitToMenuCancelled();
	}

	if (bSettingsOpen)
	{
		HandleSettingsCloseRequested();
	}

	bPauseMenuOpen = false;

	if (PauseMenu)
	{
		PauseMenu->Close();
	}

	// FInputModeGameOnly repose le focus clavier sur le viewport : c'est ce qui
	// rend leurs touches au pawn et au controller apres le passage du menu.
	SetUiInputMode(false);
}

FString AFpsPlayerController::ResolveLocalPlayerNickname() const
{
	// On passe par l'interface generique, jamais par du Steam explicite : le
	// meme code rend le pseudo Steam en ligne et le nom local en LAN.
	if (const IOnlineSubsystem* Online = IOnlineSubsystem::Get())
	{
		const IOnlineIdentityPtr Identity = Online->GetIdentityInterface();
		if (Identity.IsValid())
		{
			const FString Nickname = Identity->GetPlayerNickname(0);
			if (!Nickname.IsEmpty())
			{
				return Nickname;
			}
		}
	}

	// Repli sans service en ligne : le nom du LocalPlayer, qui vaut le plus
	// souvent "Player". Mieux qu'une etiquette vide.
	if (const ULocalPlayer* LocalPlayerRef = GetLocalPlayer())
	{
		return LocalPlayerRef->GetNickname();
	}

	return FString();
}

void AFpsPlayerController::Server_SetPlayerNickname_Implementation(const FString& Nickname)
{
	APlayerState* State = GetPlayerState<APlayerState>();
	if (!State || Nickname.IsEmpty())
	{
		return;
	}

	// Borne de longueur : un pseudo Steam monte a 32 caracteres, mais rien
	// n'empeche un client modifie d'en envoyer un de dix mille. On tronque
	// plutot que de refuser -- refuser laisserait une etiquette vide, tronquer
	// donne un nom lisible.
	State->SetPlayerName(Nickname.Left(32));

	UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : pseudo [%s]."), *State->GetPlayerName());
}

void AFpsPlayerController::SubmitCharacterAppearance(const FCharacterAppearance& InAppearance)
{
	if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		Settings->SetCharacterAppearance(InAppearance);

		// Ecrit tout de suite, contrairement aux reglages de l'ecran d'options
		// qui attendent sa fermeture : un changement d'apparence n'a pas
		// d'ecran a fermer quand il vient de la console, et le perdre a la
		// sortie du jeu serait le plus sur moyen de croire que rien ne marche.
		Settings->SaveSettings();
	}

	// Meme raccourci que pour le pseudo : l'hote est deja l'autorite, passer
	// par la RPC marcherait mais ferait un aller-retour pour rien.
	if (HasAuthority())
	{
		Server_SetCharacterAppearance_Implementation(InAppearance);
	}
	else
	{
		Server_SetCharacterAppearance(InAppearance);
	}
}

void AFpsPlayerController::AnnounceCharacterAppearance()
{
	if (const USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
	{
		SubmitCharacterAppearance(Settings->GetCharacterAppearance());
	}
}

void AFpsPlayerController::Server_SetCharacterAppearance_Implementation(const FCharacterAppearance& InAppearance)
{
	if (ASurvivalPlayerState* State = GetPlayerState<ASurvivalPlayerState>())
	{
		State->SetCharacterAppearance(InAppearance);
	}
}

void AFpsPlayerController::ShowInviteFriends()
{
	if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
	{
		Sessions->ShowInviteUI();
	}
}

bool AFpsPlayerController::CanInviteFriends() const
{
	const USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
	return Sessions && Sessions->CanInvite();
}

void AFpsPlayerController::OpenSettings()
{
	if (bSettingsOpen)
	{
		return;
	}

	if (!SettingsMenuClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : SettingsMenuClass n'est pas assignee, les reglages ne s'ouvriront pas."));
		return;
	}

	// Creee a la premiere ouverture seulement : un ecran de reglages qu'on ne
	// visite peut-etre jamais n'a pas a peser sur chaque demarrage de partie.
	if (!SettingsMenu)
	{
		SettingsMenu = CreateWidget<USettingsMenuWidget>(this, SettingsMenuClass);
		if (!SettingsMenu)
		{
			return;
		}

		SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
		SettingsMenu->AddToViewport(ZOrderInGameSettingsMenu);

		// L'ecran de reglages est partage avec le menu principal, dont
		// l'empilement n'a rien a voir avec celui du jeu : c'est a nous, qui
		// possedons la pile, de lui dire ou poser sa boite. +10 la place devant
		// lui sans empieter sur la boite d'abandon de partie a 200.
		SettingsMenu->SetDialogZOrder(ZOrderInGameSettingsMenu + 10);
	}

	bSettingsOpen = true;
	SettingsMenu->Open();

	// Le menu de pause reste lisible derriere mais cesse de repondre : sans ca
	// on pourrait cliquer Reprendre a travers l'ecran de reglages.
	if (PauseMenu)
	{
		PauseMenu->SetInteractive(false);
	}
}

void AFpsPlayerController::HandleSettingsCloseRequested()
{
	bSettingsOpen = false;

	if (SettingsMenu)
	{
		SettingsMenu->Close();
	}

	// SetInteractive(true) reprend le focus clavier, que l'ecran de reglages
	// avait pris pour lui et ne rend a personne en se fermant. L'oublier laisse
	// le menu de pause affiche mais sourd a Echap.
	if (PauseMenu)
	{
		PauseMenu->SetInteractive(true);
	}
}

void AFpsPlayerController::RequestQuitToMainMenu()
{
	// Le fondu de sortie est deja lance : on ignore le second clic plutot que
	// d'empiler deux chargements.
	if (bQuittingToMainMenu)
	{
		return;
	}

	if (MainMenuLevel.IsNull())
	{
		UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : MainMenuLevel n'est pas assignee, impossible de retourner au menu."));
		return;
	}

	// Sans boite assignee on ne bloque pas le joueur dans sa partie : on part.
	if (!QuitToMenuDialogClass)
	{
		UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : QuitToMenuDialogClass n'est pas assignee, retour au menu sans confirmation."));
		HandleQuitToMenuConfirmed();
		return;
	}

	// Creee au premier clic seulement, puis reutilisee.
	if (!QuitToMenuDialog)
	{
		QuitToMenuDialog = CreateWidget<UConfirmDialogWidget>(this, QuitToMenuDialogClass);
		if (!QuitToMenuDialog)
		{
			return;
		}

		QuitToMenuDialog->OnConfirmed.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuConfirmed);
		QuitToMenuDialog->OnCancelled.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuCancelled);
		QuitToMenuDialog->AddToViewport(ZOrderQuitToMenuDialog);
	}

	bQuitToMenuDialogOpen = true;
	QuitToMenuDialog->Open(QuitToMenuMessage);

	// Le menu reste lisible derriere mais cesse de repondre : sans ca, on
	// pourrait cliquer Reprendre alors que la question est encore posee.
	if (PauseMenu)
	{
		PauseMenu->SetInteractive(false);
	}
}

void AFpsPlayerController::HandleQuitToMenuConfirmed()
{
	bQuitToMenuDialogOpen = false;

	// L'hote emmene tout le monde avec lui : sa partie EST la partie. On
	// previent avant de fondre, pour que les quatre fondus tournent ensemble et
	// que personne ne voie l'ecran se figer.
	if (HasAuthority())
	{
		KickRemoteClientsToMainMenu();
	}

	BeginQuitToMainMenu();
}

void AFpsPlayerController::HandleSessionTravelPending()
{
	USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
	if (!Sessions || bQuittingToMainMenu)
	{
		return;
	}

	// Les ecrans ouverts resteraient affiches par-dessus le noir, curseur
	// compris, et arriveraient tels quels dans la partie de l'ami.
	if (bInventoryOpen)
	{
		SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
	}

	if (bPauseMenuOpen)
	{
		ResumeGame();
	}

	ScreenFade->FadeOut(Sessions->GetTravelFadeDuration());
}

void AFpsPlayerController::Client_ReturnToMainMenu_Implementation()
{
	UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : l'hote a quitte la partie, retour au menu."));

	BeginQuitToMainMenu();
}

void AFpsPlayerController::KickRemoteClientsToMainMenu()
{
	UWorld* World = GetWorld();
	if (!World || !HasAuthority())
	{
		return;
	}

	for (FConstPlayerControllerIterator It = World->GetPlayerControllerIterator(); It; ++It)
	{
		AFpsPlayerController* Other = Cast<AFpsPlayerController>(It->Get());

		// Soi-meme exclu : on a notre propre chemin, et l'hote n'est de toute
		// facon pas joignable par une RPC Client_ depuis lui-meme.
		if (Other && Other != this)
		{
			Other->Client_ReturnToMainMenu();
		}
	}
}

void AFpsPlayerController::BeginQuitToMainMenu()
{
	// Un client peut avoir clique Quitter juste avant de recevoir l'ordre de
	// l'hote : deux fondus simultanes se marcheraient dessus.
	if (bQuittingToMainMenu)
	{
		return;
	}

	bQuittingToMainMenu = true;

	// Le GameInstance doit savoir que ce depart est VOULU : sans ca, la
	// connexion qui tombe pendant le fondu declencherait son filet de secours,
	// qui chargerait le menu immediatement et couperait le fondu en deux.
	if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
	{
		SurvivalGameInstance->BeginReturnToMainMenu();
	}

	if (QuitToMenuDialog)
	{
		QuitToMenuDialog->Close();
	}

	if (PauseMenu)
	{
		PauseMenu->Close();
	}

	// L'ecran d'inventaire ou de coffre resterait affiche par-dessus le noir,
	// curseur compris. Sa fermeture rend le coffre au passage.
	if (bInventoryOpen)
	{
		SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
	}

	ScreenFade->FadeOut(QuitToMenuFadeDuration);
}

void AFpsPlayerController::HandleQuitToMenuCancelled()
{
	bQuitToMenuDialogOpen = false;

	if (QuitToMenuDialog)
	{
		QuitToMenuDialog->Close();
	}

	// Meme raison qu'a la fermeture des reglages : la boite avait le focus
	// clavier, on le reprend en redevenant la couche active.
	if (PauseMenu)
	{
		PauseMenu->SetInteractive(true);
	}
}

void AFpsPlayerController::HandleFadeFinished()
{
	if (!bQuittingToMainMenu)
	{
		return;
	}

	// Fermer la session AVANT de voyager. Chez l'hote c'est ce qui la retire de
	// la liste de tes amis a l'instant ou tu quittes, plutot qu'a l'arrivee sur
	// le menu ; chez un client c'est ce qui libere sa place.
	//
	// Les autres joueurs, eux, perdront la connexion : c'est
	// USurvivalGameInstance::HandleNetworkFailure qui les ramene a leur menu.
	if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
	{
		Sessions->LeaveSession();
	}

	UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuLevel);
}

void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	if (SettingsMenu)
	{
		SettingsMenu->RemoveFromParent();
		SettingsMenu = nullptr;
	}

	if (QuitToMenuDialog)
	{
		QuitToMenuDialog->RemoveFromParent();
		QuitToMenuDialog = nullptr;
	}

	if (PauseMenu)
	{
		PauseMenu->RemoveFromParent();
		PauseMenu = nullptr;
	}

	if (InventoryScreen)
	{
		InventoryScreen->RemoveFromParent();
		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();
		InteractionPrompt = nullptr;
	}

	if (Hotbar)
	{
		Hotbar->RemoveFromParent();
		Hotbar = nullptr;
	}

	if (SurvivalStatsWidget)
	{
		SurvivalStatsWidget->RemoveFromParent();
		SurvivalStatsWidget = nullptr;
	}

	if (Crosshair)
	{
		Crosshair->RemoveFromParent();
		Crosshair = nullptr;
	}

	// Le voile de fondu est nettoye par UScreenFadeComponent::EndPlay.

	Super::EndPlay(EndPlayReason);
}

#undef LOCTEXT_NAMESPACE
