From 175bef228c6ae2342830552c78d1f3f8445c1755 Mon Sep 17 00:00:00 2001 From: Mathew Date: Tue, 28 Jul 2026 14:10:17 +0200 Subject: [PATCH] (Feat) Add MainMenu --- .claude/CLAUDE.md | 42 ++- Config/DefaultEngine.ini | 3 + Content/Game/Input/IA_Pause.uasset | 3 + Content/Game/Input/IMC_Default.uasset | 4 +- .../Game/Player/BP_FpsPlayerController.uasset | 4 +- ...47313572-ui-sounds-pack-3-10-359727.uasset | 3 + ...47313572-ui-sounds-pack-3-11-359714.uasset | 3 + ...47313572-ui-sounds-pack-3-14-359717.uasset | 3 + ...47313572-ui-sounds-pack-5-18-359763.uasset | 3 + .../47313572-ui-sounds-pack-5-2-359749.uasset | 3 + Content/Game/UI/WBP_PauseMenu.uasset | 3 + Source/Survival_projet/Private/FpsPlayer.cpp | 102 +++++- .../Private/FpsPlayerController.cpp | 327 ++++++++++++++++++ .../Private/MainMenuPlayerController.cpp | 61 +++- .../Private/MainMenuWidget.cpp | 13 + .../Private/PauseMenuWidget.cpp | 71 ++++ .../Private/ScreenFadeComponent.cpp | 5 + .../Private/SettingRowWidget.cpp | 164 +++++++++ .../Private/SettingsGameplayWidget.cpp | 278 +++++++++++++++ .../Private/SettingsMenuWidget.cpp | 268 ++++++++++++++ .../Private/SurvivalGameInstance.cpp | 27 ++ .../Private/SurvivalUserSettings.cpp | 144 ++++++++ Source/Survival_projet/Public/FpsPlayer.h | 38 ++ .../Public/FpsPlayerController.h | 113 ++++++ .../Public/MainMenuPlayerController.h | 16 + .../Survival_projet/Public/MainMenuWidget.h | 7 + .../Survival_projet/Public/PauseMenuWidget.h | 57 +++ .../Survival_projet/Public/SettingRowWidget.h | 127 +++++++ .../Public/SettingsGameplayWidget.h | 89 +++++ .../Public/SettingsMenuWidget.h | 135 ++++++++ .../Public/SettingsTabWidget.h | 27 ++ .../Public/SurvivalGameInstance.h | 2 + .../Public/SurvivalUserSettings.h | 198 +++++++++++ 33 files changed, 2331 insertions(+), 12 deletions(-) create mode 100644 Content/Game/Input/IA_Pause.uasset create mode 100644 Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset create mode 100644 Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset create mode 100644 Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset create mode 100644 Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-18-359763.uasset create mode 100644 Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-2-359749.uasset create mode 100644 Content/Game/UI/WBP_PauseMenu.uasset create mode 100644 Source/Survival_projet/Private/PauseMenuWidget.cpp create mode 100644 Source/Survival_projet/Private/SettingRowWidget.cpp create mode 100644 Source/Survival_projet/Private/SettingsGameplayWidget.cpp create mode 100644 Source/Survival_projet/Private/SettingsMenuWidget.cpp create mode 100644 Source/Survival_projet/Private/SurvivalGameInstance.cpp create mode 100644 Source/Survival_projet/Private/SurvivalUserSettings.cpp create mode 100644 Source/Survival_projet/Public/PauseMenuWidget.h create mode 100644 Source/Survival_projet/Public/SettingRowWidget.h create mode 100644 Source/Survival_projet/Public/SettingsGameplayWidget.h create mode 100644 Source/Survival_projet/Public/SettingsMenuWidget.h create mode 100644 Source/Survival_projet/Public/SettingsTabWidget.h create mode 100644 Source/Survival_projet/Public/SurvivalUserSettings.h diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 1e890bb..84e4f2f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -44,8 +44,9 @@ qu'un assemblage de composants plus la caméra et le déplacement. | Nature | Emplacement | |---|---| | Le corps : déplacement, caméra, inventaire, stats, interaction | **Pawn** | -| Le joueur : widgets HUD, input d'interface (Tab, Q, hotbar) | **PlayerController** | +| Le joueur : widgets HUD, input d'interface (Tab, Q, hotbar, Échap) | **PlayerController** | | Les règles de la partie, le respawn | **GameMode** | +| Ce qui survit au changement de map : réglages, sauvegarde | **GameInstance** | | Ce que les autres savent de toi (plus tard, si multi) | **PlayerState** | **Données vs état.** `UItemDataAsset` décrit ce qu'un objet **est** (nom, mesh, icône, effets). @@ -64,6 +65,15 @@ pour itérer en PIE sans recompiler. `TWeakObjectPtr` ne permet pas de détecter une transition par simple comparaison de pointeurs, il faut un booléen d'état à côté. +**Le mode d'input vit sur le viewport, pas sur le controller.** `OpenLevel` détruit le +PlayerController mais pas la fenêtre : un controller qui arrive sur une map ne doit jamais +supposer l'état laissé par la précédente, il repose son `SetInputMode`. Symptôme quand on +l'oublie, en Standalone seulement : la caméra ne répond qu'au clic droit maintenu. + +**Enhanced Input en pause** : un jeu en pause ne fait qu'un tick d'input réduit, où seules les +actions cochées **« Trigger When Paused »** sont évaluées. C'est une case sur l'asset `IA_`, +pas du C++ — et son corollaire est qu'aucune action du pawn ne se déclenche en pause. + ## Systèmes en place | Système | Fichiers | @@ -74,6 +84,10 @@ il faut un booléen d'état à côté. | Objets | `ItemDataAsset`, `PickupItem` | | Inventaire | `InventoryComponent`, `InventoryWidget`, `InventorySlotWidget`, `HotbarWidget`, `InventoryDragDropOperation` | | Survie | `SurvivalStatsComponent`, `SurvivalStatsWidget` | +| Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` | +| Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` | +| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` | +| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` | ### Décisions de gameplay figées @@ -93,7 +107,18 @@ il faut un booléen d'état à côté. - **Fondus** : un unique voile plein écran `WBP_Fade` au ZOrder 1000. Fondre widget par widget est fragile — les cases réécrivent leur propre `RenderOpacity`. - Après un respawn, le pawn est un **nouvel acteur** : les widgets doivent se rebrancher via - `BindToOwningPawn()`, appelé depuis `OnPossess`. + `BindToOwningPawn()`, appelé depuis `OnPossess`. Même raison côté réglages : le pawn les + **relit** à son `BeginPlay` et s'abonne à `OnSurvivalSettingsApplied`. +- **Échap** a une priorité explicite dans `HandlePauseInput()` : boîte de confirmation → + réglages → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en même temps. +- **Menu de pause** : `SetPause(true)` fige le monde, mais **pas Slate** — les animations UMG et + `ScreenFadeComponent` continuent (d'où son `bTickEvenWhenPaused`). Pas de bouton « Quitter le + jeu » : la sortie se fait depuis le menu principal. +- **Réglages** : `USurvivalUserSettings` fait autorité, jamais le CDO du pawn. Application + immédiate, aucun bouton « Appliquer » ; la sauvegarde a lieu à la fermeture de l'écran. + L'écran ignore qui l'ouvre (`OnCloseRequested`), d'où un seul `WBP_Settings` pour le menu + principal et la pause. Une ligne = un index dans une liste de paliers, y compris les nombres : + d'où un seul `WBP_SettingRow` pour tout le menu. ## Conventions @@ -108,9 +133,18 @@ il faut un booléen d'état à côté. ## État actuel et suite Boucle complète : récolter → ranger → consommer → survivre → mourir → réapparaître. +Autour : menu principal, menu de pause, et un écran de réglages dont **seul l'onglet +« Jeu & Contrôles » est rempli**. -Prochaine étape recommandée : **spawner de ressources avec repousse** (`AResourceSpawner`, -liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se vide en dix minutes. +Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout, +prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix +curseurs de `UGameUserSettings` ; végétation et distance d'affichage comptent double avec +SoStylized), onglet **Audio** quand des `SoundClass` existeront, et enfin le **remappage des +touches** via `UEnhancedInputUserSettings` — le seul morceau réellement coûteux. + +Prochaine étape gameplay recommandée : **spawner de ressources avec repousse** +(`AResourceSpawner`, liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se +vide en dix minutes. Ensuite, par ordre de valeur : craft, objet visible en main, coffres, sauvegarde, cycle jour/nuit. diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index 190ec0d..072e288 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -87,6 +87,9 @@ FontDPI=72 [/Script/Engine.Engine] +ActiveGameNameRedirects=(OldGameName="TP_Blank",NewGameName="/Script/Survival_projet") +ActiveGameNameRedirects=(OldGameName="/Script/TP_Blank",NewGameName="/Script/Survival_projet") +; Sans cette ligne le moteur instancie UGameUserSettings et nos reglages de jeu +; n'existent tout simplement pas au runtime. +GameUserSettingsClassName=/Script/Survival_projet.SurvivalUserSettings [/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] bEnablePlugin=True diff --git a/Content/Game/Input/IA_Pause.uasset b/Content/Game/Input/IA_Pause.uasset new file mode 100644 index 0000000..fb6e817 --- /dev/null +++ b/Content/Game/Input/IA_Pause.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:810c69d5677db8bc0b5ab9b159cb51dc9d78fa82463cdfde263be8f1e09cf9a2 +size 1226 diff --git a/Content/Game/Input/IMC_Default.uasset b/Content/Game/Input/IMC_Default.uasset index 716e914..75cc6c8 100644 --- a/Content/Game/Input/IMC_Default.uasset +++ b/Content/Game/Input/IMC_Default.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd8a9481831dce13d00916f6c3986fcf7fd66a52ade105091b6159884a708004 -size 13593 +oid sha256:d0e53defd95013051ca7c366ae7a9fa8f40bf9a767a607371ac32f2ec4870a7d +size 14046 diff --git a/Content/Game/Player/BP_FpsPlayerController.uasset b/Content/Game/Player/BP_FpsPlayerController.uasset index 69de767..4a00cba 100644 --- a/Content/Game/Player/BP_FpsPlayerController.uasset +++ b/Content/Game/Player/BP_FpsPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dee7a2293d44cbc08a0576f39d22853da44dcf953d6d4934f37306f17f26ec06 -size 23040 +oid sha256:337a2b2d0fd89d5bb9aa1ec73d9b0881de10628ac9d9a414d49e8a9945f185ad +size 23795 diff --git a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset new file mode 100644 index 0000000..6a383e4 --- /dev/null +++ b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18ff8d4f869a985f4d8ecfc016b7f48898b743475aafb5b9618a514978a7e0af +size 14093 diff --git a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset new file mode 100644 index 0000000..710e751 --- /dev/null +++ b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7bdda65d6838e07e757c34ffae89819dee0bc0e347b58fe7aa9fdf9a401c281 +size 10732 diff --git a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset new file mode 100644 index 0000000..1e84df7 --- /dev/null +++ b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8331df2b3874695587e69461fe9c64dffb11f5d6643d26cddc99f47da075853b +size 16775 diff --git a/Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-18-359763.uasset b/Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-18-359763.uasset new file mode 100644 index 0000000..0394b31 --- /dev/null +++ b/Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-18-359763.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71c08a747a9cfc730e955a0f0c7b84bd97029cecbe75f5281b25df7be8cd269e +size 19562 diff --git a/Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-2-359749.uasset b/Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-2-359749.uasset new file mode 100644 index 0000000..c9a0e9b --- /dev/null +++ b/Content/Game/Sounds/UI/Validate/47313572-ui-sounds-pack-5-2-359749.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f766121337a3881bbfdbb9799830209dae397c7abaf6463077eeeffa6181ff45 +size 21942 diff --git a/Content/Game/UI/WBP_PauseMenu.uasset b/Content/Game/UI/WBP_PauseMenu.uasset new file mode 100644 index 0000000..b95d68e --- /dev/null +++ b/Content/Game/UI/WBP_PauseMenu.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07aef1bf35873673ae324f3bc62022adfc540e870719edc494c4feeea700f339 +size 50325 diff --git a/Source/Survival_projet/Private/FpsPlayer.cpp b/Source/Survival_projet/Private/FpsPlayer.cpp index a9bc63f..0414e1b 100644 --- a/Source/Survival_projet/Private/FpsPlayer.cpp +++ b/Source/Survival_projet/Private/FpsPlayer.cpp @@ -17,6 +17,7 @@ #include "ItemDataAsset.h" #include "PickupItem.h" #include "SurvivalStatsComponent.h" +#include "SurvivalUserSettings.h" // Sets default values AFpsPlayer::AFpsPlayer() @@ -72,6 +73,21 @@ void AFpsPlayer::BeginPlay() SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath); + // Les reglages d'abord : le pawn est recree a chaque respawn et repart des + // valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre + // ensuite les changements faits en cours de partie depuis le menu de pause. + ApplyUserSettings(); + + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->OnSurvivalSettingsApplied.AddDynamic(this, &AFpsPlayer::ApplyUserSettings); + } + + // Le FOV est interpole chaque frame vers BaseFOV : sans ce calage initial on + // verrait la camera s'ouvrir lentement jusqu'a la valeur reglee a chaque + // apparition. + FirstPersonCamera->SetFieldOfView(BaseFOV); + // On enregistre le mapping context aupres du sous-systeme Enhanced Input // du joueur local. Sans ca, aucune touche n'est lue. if (const APlayerController* PlayerController = Cast(GetController())) @@ -91,6 +107,45 @@ void AFpsPlayer::BeginPlay() } } +void AFpsPlayer::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + // Le singleton de reglages survit au monde : sans ce desabonnement, chaque + // cadavre laisserait derriere lui un abonne mort de plus. + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->OnSurvivalSettingsApplied.RemoveDynamic(this, &AFpsPlayer::ApplyUserSettings); + } + + Super::EndPlay(EndPlayReason); +} + +void AFpsPlayer::ApplyUserSettings() +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings) + { + // Pas de reglages : on garde les valeurs du Blueprint. Le jeu reste + // jouable, il n'est simplement pas configurable. + UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : USurvivalUserSettings introuvable, les valeurs du Blueprint sont conservees.")); + return; + } + + LookSensitivityX = Settings->GetLookMultiplierX(); + LookSensitivityY = Settings->GetLookMultiplierY(); + bInvertLookY = Settings->GetInvertLookY(); + BaseFOV = Settings->GetFieldOfView(); + CameraShakeMultiplier = Settings->GetCameraShakeMultiplier(); + bSprintToggleMode = Settings->GetSprintToggle(); + bCrouchToggleMode = Settings->GetCrouchToggle(); + + // Repasser en maintien alors que la touche n'est plus enfoncee laisserait le + // personnage bloque en sprint : on repart d'un etat propre. + if (!bSprintToggleMode && bIsSprinting) + { + StopSprint(); + } +} + // Called every frame void AFpsPlayer::Tick(float DeltaTime) { @@ -193,9 +248,12 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime) void AFpsPlayer::AddCameraPunch(float Strength) { + // Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui + // secoue la camera passe par cette porte, y compris les explosions et les + // reculs d'arme a venir. A 0, la camera ne bouge plus du tout. // Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects // qui l'injecte progressivement dans le ressort. - PendingPunch += Strength; + PendingPunch += Strength * CameraShakeMultiplier; } void AFpsPlayer::Landed(const FHitResult& Hit) @@ -263,14 +321,17 @@ void AFpsPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent if (SprintAction) { + // Le mode maintien / bascule se decide dans les handlers et non ici : + // rebrancher les bindings a chaud demanderait de reconstruire l'input + // component a chaque changement de reglage. Input->BindAction(SprintAction, ETriggerEvent::Started, this, &AFpsPlayer::StartSprint); - Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::StopSprint); + Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleSprintReleased); } if (CrouchAction) { Input->BindAction(CrouchAction, ETriggerEvent::Started, this, &AFpsPlayer::StartCrouch); - Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::StopCrouch); + Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleCrouchReleased); } if (InteractAction) @@ -330,6 +391,13 @@ void AFpsPlayer::StartSprint() return; } + // En mode bascule, la meme touche coupe le sprint. + if (bSprintToggleMode && bIsSprinting) + { + StopSprint(); + return; + } + bIsSprinting = true; GetCharacterMovement()->MaxWalkSpeed = SprintSpeed; } @@ -340,6 +408,17 @@ void AFpsPlayer::StopSprint() GetCharacterMovement()->MaxWalkSpeed = WalkSpeed; } +void AFpsPlayer::HandleSprintReleased() +{ + // En bascule, relacher la touche ne doit rien faire. + if (bSprintToggleMode) + { + return; + } + + StopSprint(); +} + void AFpsPlayer::UseSelectedItem() { const FInventorySlot Selected = InventoryComponent->GetSelectedSlot(); @@ -514,10 +593,27 @@ void AFpsPlayer::ClearTransientInputStates() void AFpsPlayer::StartCrouch() { + // En mode bascule, la meme touche releve le personnage. + if (bCrouchToggleMode && bIsCrouched) + { + StopCrouch(); + return; + } + StopSprint(); Crouch(); } +void AFpsPlayer::HandleCrouchReleased() +{ + if (bCrouchToggleMode) + { + return; + } + + StopCrouch(); +} + void AFpsPlayer::StopCrouch() { // UnCrouch() echoue silencieusement s'il y a un plafond : le perso diff --git a/Source/Survival_projet/Private/FpsPlayerController.cpp b/Source/Survival_projet/Private/FpsPlayerController.cpp index a9b5689..7fc861a 100644 --- a/Source/Survival_projet/Private/FpsPlayerController.cpp +++ b/Source/Survival_projet/Private/FpsPlayerController.cpp @@ -4,6 +4,7 @@ #include "FpsPlayerController.h" #include "Blueprint/UserWidget.h" +#include "ConfirmDialogWidget.h" #include "EnhancedInputComponent.h" #include "FpsPlayer.h" #include "GameFramework/GameModeBase.h" @@ -14,9 +15,14 @@ #include "InventoryComponent.h" #include "InteractionPromptWidget.h" #include "InventoryWidget.h" +#include "Kismet/GameplayStatics.h" +#include "PauseMenuWidget.h" #include "ScreenFadeComponent.h" +#include "SettingsMenuWidget.h" #include "SurvivalStatsWidget.h" +#define LOCTEXT_NAMESPACE "PauseMenu" + namespace { // Ordre d'empilement des widgets plein ecran, du fond vers l'avant. @@ -35,6 +41,12 @@ namespace constexpr int32 ZOrderInteractionPrompt = 10; constexpr int32 ZOrderInventory = 20; constexpr int32 ZOrderHotbar = 30; + + // Le menu de pause passe devant tout le HUD, et la boite de confirmation + // devant lui. Meme echelle que dans le menu principal. + constexpr int32 ZOrderPauseMenu = 100; + constexpr int32 ZOrderSettingsMenu = 150; + constexpr int32 ZOrderQuitToMenuDialog = 200; } AFpsPlayerController::AFpsPlayerController() @@ -45,12 +57,29 @@ AFpsPlayerController::AFpsPlayerController() // 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(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); + } + + // 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); + if (bFadeInOnStart) { ScreenFade->FadeIn(StartupFadeDuration); @@ -157,6 +186,15 @@ void AFpsPlayerController::SetupInputComponent() { Input->BindAction(HotbarScrollAction, ETriggerEvent::Triggered, this, &AFpsPlayerController::HandleHotbarScroll); } + + if (PauseAction) + { + Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandlePauseInput); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseAction n'est pas assignee, le menu de pause sera inaccessible.")); + } } UInventoryComponent* AFpsPlayerController::GetPawnInventory() const @@ -298,6 +336,24 @@ void AFpsPlayerController::CreateHudWidgets() } } + if (!PauseMenu) + { + if (PauseMenuClass) + { + PauseMenu = CreateWidget(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() @@ -356,8 +412,277 @@ void AFpsPlayerController::ToggleInventory() } } +void AFpsPlayerController::HandlePauseInput() +{ + // 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. Fermer l'inventaire est + // ce que le joueur attend en premier. + 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; + PauseMenu->SetVisibility(ESlateVisibility::Visible); + + // GameAndUI et non UIOnly, meme raison que l'inventaire : en UIOnly plus + // aucune action Enhanced Input ne passe, et Echap ne refermerait rien. + FInputModeGameAndUI Mode; + Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); + Mode.SetHideCursorDuringCapture(false); + SetInputMode(Mode); + SetShowMouseCursor(true); + + // Pas de DisableInput sur le pawn ici, contrairement a l'inventaire : le + // monde est reellement fige, et les actions du pawn n'ont pas + // "Trigger When Paused" -- elles ne se declenchent donc plus d'elles-memes. + SetPause(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->SetVisibility(ESlateVisibility::Collapsed); + } + + SetPause(false); + + SetInputMode(FInputModeGameOnly()); + SetShowMouseCursor(false); +} + +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(this, SettingsMenuClass); + if (!SettingsMenu) + { + return; + } + + SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested); + SettingsMenu->AddToViewport(ZOrderSettingsMenu); + } + + 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->SetVisibility(ESlateVisibility::HitTestInvisible); + } +} + +void AFpsPlayerController::HandleSettingsCloseRequested() +{ + bSettingsOpen = false; + + if (SettingsMenu) + { + SettingsMenu->Close(); + } + + if (PauseMenu) + { + PauseMenu->SetVisibility(ESlateVisibility::Visible); + } +} + +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(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->SetVisibility(ESlateVisibility::HitTestInvisible); + } +} + +void AFpsPlayerController::HandleQuitToMenuConfirmed() +{ + bQuitToMenuDialogOpen = false; + bQuittingToMainMenu = true; + + if (QuitToMenuDialog) + { + QuitToMenuDialog->Close(); + } + + if (PauseMenu) + { + PauseMenu->SetVisibility(ESlateVisibility::Collapsed); + } + + // On reste en pause pendant le fondu : relancer le monde une seconde pour + // l'abandonner ensuite laisserait le temps a la faim ou a une chute de tuer + // un joueur qui a deja quitte. C'est UScreenFadeComponent qui continue de + // ticker en pause, pas le monde. + ScreenFade->FadeOut(QuitToMenuFadeDuration); +} + +void AFpsPlayerController::HandleQuitToMenuCancelled() +{ + bQuitToMenuDialogOpen = false; + + if (QuitToMenuDialog) + { + QuitToMenuDialog->Close(); + } + + if (PauseMenu) + { + PauseMenu->SetVisibility(ESlateVisibility::Visible); + } +} + +void AFpsPlayerController::HandleFadeFinished() +{ + if (!bQuittingToMainMenu) + { + return; + } + + // La pause vit sur les WorldSettings, que OpenLevel detruit avec le monde. + // On la leve quand meme : un monde qu'on quitte en etat de pause est un + // piege si le chargement echoue. + SetPause(false); + + 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 (InventoryWidget) { InventoryWidget->RemoveFromParent(); @@ -392,3 +717,5 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason) Super::EndPlay(EndPlayReason); } + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Survival_projet/Private/MainMenuPlayerController.cpp b/Source/Survival_projet/Private/MainMenuPlayerController.cpp index ef9a593..f5b4833 100644 --- a/Source/Survival_projet/Private/MainMenuPlayerController.cpp +++ b/Source/Survival_projet/Private/MainMenuPlayerController.cpp @@ -10,6 +10,7 @@ #include "MainMenuWidget.h" #include "MenuCameraSpot.h" #include "ScreenFadeComponent.h" +#include "SettingsMenuWidget.h" #include "SurvivalGameInstance.h" #define LOCTEXT_NAMESPACE "MainMenu" @@ -19,7 +20,9 @@ namespace // Le menu reste sous le voile de fondu, qui est en 1000. constexpr int32 ZOrderMainMenu = 0; - // La boite de confirmation passe devant le menu, mais sous le fondu. + // L'ecran de reglages recouvre le menu, la boite de confirmation passe + // devant les deux, et le fondu reste au-dessus de tout a 1000. + constexpr int32 ZOrderSettingsMenu = 50; constexpr int32 ZOrderQuitDialog = 100; } @@ -201,6 +204,56 @@ void AMainMenuPlayerController::QuitGame() } } +void AMainMenuPlayerController::OpenSettings() +{ + // Une partie se charge deja : ouvrir un ecran maintenant serait un piege. + if (PendingAction != EMenuPendingAction::None) + { + return; + } + + if (!SettingsMenuClass) + { + UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : SettingsMenuClass n'est pas assignee, les reglages ne s'ouvriront pas.")); + return; + } + + // Creee a la premiere ouverture seulement, puis reutilisee. + if (!SettingsMenu) + { + SettingsMenu = CreateWidget(this, SettingsMenuClass); + if (!SettingsMenu) + { + return; + } + + SettingsMenu->OnCloseRequested.AddDynamic(this, &AMainMenuPlayerController::HandleSettingsCloseRequested); + SettingsMenu->AddToViewport(ZOrderSettingsMenu); + } + + SettingsMenu->Open(); + + // Le menu reste lisible derriere mais cesse de repondre : sans ca on + // pourrait lancer une partie a travers l'ecran de reglages. + if (MainMenu) + { + MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible); + } +} + +void AMainMenuPlayerController::HandleSettingsCloseRequested() +{ + if (SettingsMenu) + { + SettingsMenu->Close(); + } + + if (MainMenu) + { + MainMenu->SetVisibility(ESlateVisibility::Visible); + } +} + void AMainMenuPlayerController::HandleQuitConfirmed() { // Pas de fondu : le joueur vient de confirmer, le faire patienter une @@ -238,6 +291,12 @@ void AMainMenuPlayerController::HandleFadeFinished() void AMainMenuPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason) { + if (SettingsMenu) + { + SettingsMenu->RemoveFromParent(); + SettingsMenu = nullptr; + } + if (QuitDialog) { QuitDialog->RemoveFromParent(); diff --git a/Source/Survival_projet/Private/MainMenuWidget.cpp b/Source/Survival_projet/Private/MainMenuWidget.cpp index 15f4341..5dc9570 100644 --- a/Source/Survival_projet/Private/MainMenuWidget.cpp +++ b/Source/Survival_projet/Private/MainMenuWidget.cpp @@ -20,6 +20,11 @@ void UMainMenuWidget::NativeConstruct() QuitButton->OnClicked.AddDynamic(this, &UMainMenuWidget::HandleQuitClicked); } + if (SettingsButton) + { + SettingsButton->OnClicked.AddDynamic(this, &UMainMenuWidget::HandleSettingsClicked); + } + // Les boutons de coop sont visibles mais inertes : ils annoncent la couleur // sans promettre une partie qui echouerait. if (HostButton) @@ -48,3 +53,11 @@ void UMainMenuWidget::HandleQuitClicked() Controller->QuitGame(); } } + +void UMainMenuWidget::HandleSettingsClicked() +{ + if (AMainMenuPlayerController* Controller = GetOwningPlayer()) + { + Controller->OpenSettings(); + } +} diff --git a/Source/Survival_projet/Private/PauseMenuWidget.cpp b/Source/Survival_projet/Private/PauseMenuWidget.cpp new file mode 100644 index 0000000..02c805e --- /dev/null +++ b/Source/Survival_projet/Private/PauseMenuWidget.cpp @@ -0,0 +1,71 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "PauseMenuWidget.h" + +#include "Components/Button.h" +#include "FpsPlayerController.h" + +void UPauseMenuWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (ResumeButton) + { + ResumeButton->OnClicked.AddDynamic(this, &UPauseMenuWidget::HandleResumeClicked); + } + + if (QuitToMenuButton) + { + QuitToMenuButton->OnClicked.AddDynamic(this, &UPauseMenuWidget::HandleQuitToMenuClicked); + } + + if (SettingsButton) + { + SettingsButton->OnClicked.AddDynamic(this, &UPauseMenuWidget::HandleSettingsClicked); + } +} + +void UPauseMenuWidget::NativeDestruct() +{ + if (ResumeButton) + { + ResumeButton->OnClicked.RemoveDynamic(this, &UPauseMenuWidget::HandleResumeClicked); + } + + if (SettingsButton) + { + SettingsButton->OnClicked.RemoveDynamic(this, &UPauseMenuWidget::HandleSettingsClicked); + } + + if (QuitToMenuButton) + { + QuitToMenuButton->OnClicked.RemoveDynamic(this, &UPauseMenuWidget::HandleQuitToMenuClicked); + } + + Super::NativeDestruct(); +} + +void UPauseMenuWidget::HandleSettingsClicked() +{ + if (AFpsPlayerController* Controller = GetOwningPlayer()) + { + Controller->OpenSettings(); + } +} + +void UPauseMenuWidget::HandleResumeClicked() +{ + if (AFpsPlayerController* Controller = GetOwningPlayer()) + { + Controller->ResumeGame(); + } +} + +void UPauseMenuWidget::HandleQuitToMenuClicked() +{ + if (AFpsPlayerController* Controller = GetOwningPlayer()) + { + Controller->RequestQuitToMainMenu(); + } +} diff --git a/Source/Survival_projet/Private/ScreenFadeComponent.cpp b/Source/Survival_projet/Private/ScreenFadeComponent.cpp index 3c24b39..de28799 100644 --- a/Source/Survival_projet/Private/ScreenFadeComponent.cpp +++ b/Source/Survival_projet/Private/ScreenFadeComponent.cpp @@ -21,6 +21,11 @@ UScreenFadeComponent::UScreenFadeComponent() // pour une valeur qui ne bouge qu'une seconde par mort serait du gaspillage. PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bStartWithTickEnabled = false; + + // Le fondu doit continuer d'avancer quand le monde est fige : c'est le seul + // moyen de partir au noir depuis le menu de pause. Sans ce drapeau, l'ecran + // resterait bloque a mi-chemin et le delegue ne se declencherait jamais. + PrimaryComponentTick.bTickEvenWhenPaused = true; } void UScreenFadeComponent::BeginPlay() diff --git a/Source/Survival_projet/Private/SettingRowWidget.cpp b/Source/Survival_projet/Private/SettingRowWidget.cpp new file mode 100644 index 0000000..6c28f64 --- /dev/null +++ b/Source/Survival_projet/Private/SettingRowWidget.cpp @@ -0,0 +1,164 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "SettingRowWidget.h" + +#include "Components/Button.h" +#include "Components/ProgressBar.h" +#include "Components/TextBlock.h" + +void USettingRowWidget::NativeOnInitialized() +{ + Super::NativeOnInitialized(); + + // NativeOnInitialized et non NativeConstruct : l'onglet parent appelle + // SetupAs... depuis SON NativeConstruct, et rien ne garantit l'ordre entre + // le construct du parent et celui de ses enfants. Initialize, lui, tourne + // une seule fois a la creation de l'arbre, avant tout le monde. + if (LeftArrowButton) + { + LeftArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleLeftClicked); + } + + if (RightArrowButton) + { + RightArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleRightClicked); + } +} + +void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray& Options, int32 Index) +{ + bNumeric = false; + ChoiceLabels = Options; + StepCount = FMath::Max(Options.Num(), 1); + CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1); + + if (LabelText) + { + LabelText->SetText(Label); + } + + // Une barre de remplissage sur "Maintien / Bascule" ne veut rien dire : elle + // serait vide ou pleine sans que ca informe de quoi que ce soit. On la + // masque plutot que d'imposer un second WBP juste pour les choix. + if (ValueBar) + { + ValueBar->SetVisibility(ESlateVisibility::Collapsed); + } + + Refresh(); +} + +void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max, float Step, float Value, const FText& Suffix) +{ + bNumeric = true; + ChoiceLabels.Reset(); + NumericMin = Min; + NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER); + NumericSuffix = Suffix; + + // +1 : les deux extremites comptent. 70 a 120 par 5 fait 11 paliers, pas 10. + StepCount = FMath::Max(FMath::FloorToInt((Max - Min) / NumericStep) + 1, 1); + + if (LabelText) + { + LabelText->SetText(Label); + } + + // HitTestInvisible et non Visible : la barre est un decor, elle ne doit pas + // intercepter les clics destines aux fleches qui l'encadrent. + if (ValueBar) + { + ValueBar->SetVisibility(ESlateVisibility::HitTestInvisible); + } + + SetNumericValueSilently(Value); +} + +float USettingRowWidget::GetNumericValue() const +{ + return NumericMin + CurrentIndex * NumericStep; +} + +void USettingRowWidget::SetIndexSilently(int32 NewIndex) +{ + CurrentIndex = FMath::Clamp(NewIndex, 0, StepCount - 1); + Refresh(); +} + +void USettingRowWidget::SetNumericValueSilently(float Value) +{ + SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep)); +} + +void USettingRowWidget::HandleLeftClicked() +{ + StepIndex(-1); +} + +void USettingRowWidget::HandleRightClicked() +{ + StepIndex(1); +} + +void USettingRowWidget::StepIndex(int32 Delta) +{ + const int32 NewIndex = FMath::Clamp(CurrentIndex + Delta, 0, StepCount - 1); + + // Pas de bouclage, volontairement : passer de 100 a 5 de sensibilite d'un + // clic de trop rendrait le jeu injouable sans qu'on comprenne pourquoi. + if (NewIndex == CurrentIndex) + { + return; + } + + CurrentIndex = NewIndex; + Refresh(); + + OnIndexChanged.Broadcast(CurrentIndex); +} + +void USettingRowWidget::Refresh() +{ + if (ValueText) + { + if (bNumeric) + { + FNumberFormattingOptions Format; + Format.MaximumFractionalDigits = 0; + + const FText Number = FText::AsNumber(GetNumericValue(), &Format); + + // FormatOrdered plutot qu'une concatenation : selon la langue, l'unite + // ne se colle pas forcement du meme cote ni avec la meme espace. + ValueText->SetText(NumericSuffix.IsEmpty() + ? Number + : FText::FormatOrdered(NSLOCTEXT("Reglages", "ValeurAvecUnite", "{0}{1}"), Number, NumericSuffix)); + } + else if (ChoiceLabels.IsValidIndex(CurrentIndex)) + { + ValueText->SetText(ChoiceLabels[CurrentIndex]); + } + } + + if (ValueBar) + { + // Un seul palier : la barre n'a aucun sens, on la remplit entierement + // plutot que de diviser par zero. + ValueBar->SetPercent(StepCount > 1 + ? static_cast(CurrentIndex) / static_cast(StepCount - 1) + : 1.f); + } + + // Les fleches se grisent aux extremites : le joueur voit qu'il est au bout + // au lieu de cliquer dans le vide. + if (LeftArrowButton) + { + LeftArrowButton->SetIsEnabled(CurrentIndex > 0); + } + + if (RightArrowButton) + { + RightArrowButton->SetIsEnabled(CurrentIndex < StepCount - 1); + } +} diff --git a/Source/Survival_projet/Private/SettingsGameplayWidget.cpp b/Source/Survival_projet/Private/SettingsGameplayWidget.cpp new file mode 100644 index 0000000..85e4d43 --- /dev/null +++ b/Source/Survival_projet/Private/SettingsGameplayWidget.cpp @@ -0,0 +1,278 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "SettingsGameplayWidget.h" + +#include "SettingRowWidget.h" +#include "SurvivalUserSettings.h" + +#define LOCTEXT_NAMESPACE "Reglages" + +namespace +{ + /** Les deux seules listes de choix de cet onglet. */ + TArray MakeNoYesLabels() + { + return { LOCTEXT("Non", "Non"), LOCTEXT("Oui", "Oui") }; + } + + TArray MakeHoldToggleLabels() + { + return { LOCTEXT("Maintien", "Maintien"), LOCTEXT("Bascule", "Bascule") }; + } +} + +void USettingsGameplayWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + // Les libelles et les bornes sont poses une seule fois ; seules les valeurs + // bougent ensuite, via RefreshFromSettings. + using namespace SurvivalSettingsRange; + + if (SensitivityRow) + { + SensitivityRow->SetupAsNumber(LOCTEXT("Sensibilite", "Sensibilité"), + SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty()); + SensitivityRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityChanged); + } + + if (SeparateVerticalRow) + { + SeparateVerticalRow->SetupAsChoice(LOCTEXT("SensibiliteSeparee", "Sensibilité verticale séparée"), + MakeNoYesLabels(), 0); + SeparateVerticalRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSeparateVerticalChanged); + } + + if (SensitivityVerticalRow) + { + SensitivityVerticalRow->SetupAsNumber(LOCTEXT("SensibiliteVerticale", "Sensibilité verticale"), + SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty()); + SensitivityVerticalRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityVerticalChanged); + } + + if (InvertLookRow) + { + InvertLookRow->SetupAsChoice(LOCTEXT("InverserY", "Inverser l'axe vertical"), MakeNoYesLabels(), 0); + InvertLookRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleInvertLookChanged); + } + + if (FieldOfViewRow) + { + FieldOfViewRow->SetupAsNumber(LOCTEXT("ChampDeVision", "Champ de vision"), + FieldOfViewMin, FieldOfViewMax, FieldOfViewStep, FieldOfViewMin, LOCTEXT("Degres", "°")); + FieldOfViewRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleFieldOfViewChanged); + } + + if (CameraShakeRow) + { + CameraShakeRow->SetupAsNumber(LOCTEXT("BougeCamera", "Bougé de caméra"), + CameraShakeMin, CameraShakeMax, CameraShakeStep, CameraShakeMin, LOCTEXT("Pourcent", " %")); + CameraShakeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleCameraShakeChanged); + } + + if (SprintModeRow) + { + SprintModeRow->SetupAsChoice(LOCTEXT("Sprint", "Sprint"), MakeHoldToggleLabels(), 0); + SprintModeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSprintModeChanged); + } + + if (CrouchModeRow) + { + CrouchModeRow->SetupAsChoice(LOCTEXT("Accroupi", "Accroupi"), MakeHoldToggleLabels(), 0); + CrouchModeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleCrouchModeChanged); + } + + RefreshFromSettings(); +} + +void USettingsGameplayWidget::RefreshFromSettings() +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings) + { + UE_LOG(LogTemp, Error, TEXT("USettingsGameplayWidget : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, l'onglet restera inerte.")); + return; + } + + // SetNumericValueSilently et non un setter classique : diffuser ici ferait + // boucler l'onglet sur lui-meme. + if (SensitivityRow) + { + SensitivityRow->SetNumericValueSilently(Settings->GetLookSensitivity()); + } + + const bool bSeparate = Settings->GetSeparateVerticalSensitivity(); + + if (SeparateVerticalRow) + { + SeparateVerticalRow->SetIndexSilently(bSeparate ? 1 : 0); + } + + if (SensitivityVerticalRow) + { + SensitivityVerticalRow->SetNumericValueSilently(Settings->GetLookSensitivityVertical()); + } + + UpdateVerticalRowVisibility(bSeparate); + + if (InvertLookRow) + { + InvertLookRow->SetIndexSilently(Settings->GetInvertLookY() ? 1 : 0); + } + + if (FieldOfViewRow) + { + FieldOfViewRow->SetNumericValueSilently(Settings->GetFieldOfView()); + } + + if (CameraShakeRow) + { + CameraShakeRow->SetNumericValueSilently(Settings->GetCameraShakeScale()); + } + + if (SprintModeRow) + { + SprintModeRow->SetIndexSilently(Settings->GetSprintToggle() ? 1 : 0); + } + + if (CrouchModeRow) + { + CrouchModeRow->SetIndexSilently(Settings->GetCrouchToggle() ? 1 : 0); + } +} + +void USettingsGameplayWidget::ResetToDefaults() +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings) + { + return; + } + + Settings->SetSurvivalGameplayToDefaults(); + PushToPawn(Settings); + RefreshFromSettings(); +} + +void USettingsGameplayWidget::UpdateVerticalRowVisibility(bool bSeparate) +{ + if (SensitivityVerticalRow) + { + // Collapsed et non Hidden : la ligne ne doit pas laisser un trou dans la + // liste quand elle est inutile. + SensitivityVerticalRow->SetVisibility(bSeparate ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + } +} + +void USettingsGameplayWidget::PushToPawn(USurvivalUserSettings* Settings) +{ + // Pas de SaveSettings ici : ecrire le .ini a chaque clic de fleche est du + // gaspillage. C'est l'ecran de reglages qui sauvegarde en se fermant. + Settings->ApplySurvivalSettings(); +} + +void USettingsGameplayWidget::HandleSensitivityChanged(int32 NewIndex) +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings || !SensitivityRow) + { + return; + } + + Settings->SetLookSensitivity(SensitivityRow->GetNumericValue()); + + // Sans axe separe, le vertical suit : on garde les deux lignes coherentes + // pour que le joueur ne decouvre pas un ecart en activant la separation. + if (!Settings->GetSeparateVerticalSensitivity() && SensitivityVerticalRow) + { + SensitivityVerticalRow->SetNumericValueSilently(Settings->GetLookSensitivityVertical()); + } + + PushToPawn(Settings); +} + +void USettingsGameplayWidget::HandleSeparateVerticalChanged(int32 NewIndex) +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings) + { + return; + } + + const bool bSeparate = NewIndex == 1; + Settings->SetSeparateVerticalSensitivity(bSeparate); + + if (SensitivityVerticalRow) + { + SensitivityVerticalRow->SetNumericValueSilently(Settings->GetLookSensitivityVertical()); + } + + UpdateVerticalRowVisibility(bSeparate); + PushToPawn(Settings); +} + +void USettingsGameplayWidget::HandleSensitivityVerticalChanged(int32 NewIndex) +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings || !SensitivityVerticalRow) + { + return; + } + + Settings->SetLookSensitivityVertical(SensitivityVerticalRow->GetNumericValue()); + PushToPawn(Settings); +} + +void USettingsGameplayWidget::HandleInvertLookChanged(int32 NewIndex) +{ + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->SetInvertLookY(NewIndex == 1); + PushToPawn(Settings); + } +} + +void USettingsGameplayWidget::HandleFieldOfViewChanged(int32 NewIndex) +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings || !FieldOfViewRow) + { + return; + } + + Settings->SetFieldOfView(FieldOfViewRow->GetNumericValue()); + PushToPawn(Settings); +} + +void USettingsGameplayWidget::HandleCameraShakeChanged(int32 NewIndex) +{ + USurvivalUserSettings* Settings = USurvivalUserSettings::Get(); + if (!Settings || !CameraShakeRow) + { + return; + } + + Settings->SetCameraShakeScale(CameraShakeRow->GetNumericValue()); + PushToPawn(Settings); +} + +void USettingsGameplayWidget::HandleSprintModeChanged(int32 NewIndex) +{ + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->SetSprintToggle(NewIndex == 1); + PushToPawn(Settings); + } +} + +void USettingsGameplayWidget::HandleCrouchModeChanged(int32 NewIndex) +{ + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->SetCrouchToggle(NewIndex == 1); + PushToPawn(Settings); + } +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Survival_projet/Private/SettingsMenuWidget.cpp b/Source/Survival_projet/Private/SettingsMenuWidget.cpp new file mode 100644 index 0000000..b43bcdf --- /dev/null +++ b/Source/Survival_projet/Private/SettingsMenuWidget.cpp @@ -0,0 +1,268 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "SettingsMenuWidget.h" + +#include "Components/Button.h" +#include "Components/WidgetSwitcher.h" +#include "ConfirmDialogWidget.h" +#include "InputCoreTypes.h" +#include "SettingsTabWidget.h" +#include "SurvivalUserSettings.h" + +#define LOCTEXT_NAMESPACE "Reglages" + +namespace +{ + // Ordre des pages dans le WidgetSwitcher. Les trois dernieres n'existent pas + // encore : leur bouton se grise tout seul tant que la page est absente. + constexpr int32 TabIndexGameplay = 0; + constexpr int32 TabIndexDisplay = 1; + constexpr int32 TabIndexGraphics = 2; + constexpr int32 TabIndexAudio = 3; +} + +USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + // Sans ce drapeau le widget ne peut pas prendre le focus clavier, et + // NativeOnKeyDown ne serait jamais appele. Il ne se change qu'a la + // construction, d'ou sa presence ici et pas dans Open(). + SetIsFocusable(true); + + ResetDialogMessage = LOCTEXT("ResetConfirm", "Réinitialiser cet onglet ?"); +} + +void USettingsMenuWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (BackButton) + { + BackButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleBackClicked); + } + + if (ResetButton) + { + ResetButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleResetClicked); + } + + if (GameplayTabButton) + { + GameplayTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGameplayTabClicked); + } + + if (DisplayTabButton) + { + DisplayTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleDisplayTabClicked); + } + + if (GraphicsTabButton) + { + GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked); + } + + if (AudioTabButton) + { + AudioTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleAudioTabClicked); + } + + if (ResetDialog) + { + ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleResetConfirmed); + ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleResetCancelled); + ResetDialog->Close(); + } + + UpdateTabButtonStates(); +} + +void USettingsMenuWidget::NativeDestruct() +{ + if (BackButton) + { + BackButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleBackClicked); + } + + if (ResetButton) + { + ResetButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleResetClicked); + } + + Super::NativeDestruct(); +} + +void USettingsMenuWidget::Open() +{ + SetVisibility(ESlateVisibility::Visible); + + // L'ecran a pu etre ouvert, referme, puis rouvert apres un changement fait + // ailleurs : on relit toujours plutot que de faire confiance a l'affichage. + if (USettingsTabWidget* ActiveTab = GetActiveTab()) + { + ActiveTab->RefreshFromSettings(); + } + + // Le focus doit etre repris a CHAQUE ouverture, sinon Echap reste sans effet. + SetKeyboardFocus(); +} + +void USettingsMenuWidget::Close() +{ + if (ResetDialog) + { + ResetDialog->Close(); + } + + bResetDialogOpen = false; + + SetVisibility(ESlateVisibility::Collapsed); + + // La sauvegarde a lieu ici et pas a chaque clic de fleche : ecrire le .ini + // vingt fois pendant qu'on cherche la bonne sensibilite ne sert a rien. + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->SaveSettings(); + } +} + +FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) +{ + const FKey Key = InKeyEvent.GetKey(); + + if (Key == EKeys::Escape) + { + // La boite de confirmation gere Echap elle-meme quand elle a le focus. + // Si elle l'a perdu, on annule ici plutot que de fermer tout l'ecran en + // laissant la question affichee. + if (bResetDialogOpen) + { + HandleResetCancelled(); + return FReply::Handled(); + } + + OnCloseRequested.Broadcast(); + + // Handled : sans ca la touche continue de remonter et le menu de pause + // la consommerait une seconde fois. + return FReply::Handled(); + } + + return Super::NativeOnKeyDown(InGeometry, InKeyEvent); +} + +USettingsTabWidget* USettingsMenuWidget::GetActiveTab() const +{ + return TabSwitcher ? Cast(TabSwitcher->GetActiveWidget()) : nullptr; +} + +void USettingsMenuWidget::ShowTab(int32 Index) +{ + if (!TabSwitcher || Index < 0 || Index >= TabSwitcher->GetNumWidgets()) + { + return; + } + + TabSwitcher->SetActiveWidgetIndex(Index); + + if (USettingsTabWidget* ActiveTab = GetActiveTab()) + { + ActiveTab->RefreshFromSettings(); + } +} + +void USettingsMenuWidget::UpdateTabButtonStates() +{ + const int32 PageCount = TabSwitcher ? TabSwitcher->GetNumWidgets() : 0; + + if (GameplayTabButton) + { + GameplayTabButton->SetIsEnabled(PageCount > TabIndexGameplay); + } + + if (DisplayTabButton) + { + DisplayTabButton->SetIsEnabled(PageCount > TabIndexDisplay); + } + + if (GraphicsTabButton) + { + GraphicsTabButton->SetIsEnabled(PageCount > TabIndexGraphics); + } + + if (AudioTabButton) + { + AudioTabButton->SetIsEnabled(PageCount > TabIndexAudio); + } +} + +void USettingsMenuWidget::HandleBackClicked() +{ + OnCloseRequested.Broadcast(); +} + +void USettingsMenuWidget::HandleResetClicked() +{ + // Sans boite assignee on ne bloque pas le joueur : on reinitialise. + if (!ResetDialog) + { + HandleResetConfirmed(); + return; + } + + bResetDialogOpen = true; + ResetDialog->Open(ResetDialogMessage); +} + +void USettingsMenuWidget::HandleResetConfirmed() +{ + bResetDialogOpen = false; + + if (ResetDialog) + { + ResetDialog->Close(); + } + + // Uniquement l'onglet visible : un "Reinitialiser" qui remettrait aussi la + // resolution et le volume serait une mauvaise surprise. + if (USettingsTabWidget* ActiveTab = GetActiveTab()) + { + ActiveTab->ResetToDefaults(); + } + + SetKeyboardFocus(); +} + +void USettingsMenuWidget::HandleResetCancelled() +{ + bResetDialogOpen = false; + + if (ResetDialog) + { + ResetDialog->Close(); + } + + SetKeyboardFocus(); +} + +void USettingsMenuWidget::HandleGameplayTabClicked() +{ + ShowTab(TabIndexGameplay); +} + +void USettingsMenuWidget::HandleDisplayTabClicked() +{ + ShowTab(TabIndexDisplay); +} + +void USettingsMenuWidget::HandleGraphicsTabClicked() +{ + ShowTab(TabIndexGraphics); +} + +void USettingsMenuWidget::HandleAudioTabClicked() +{ + ShowTab(TabIndexAudio); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Survival_projet/Private/SurvivalGameInstance.cpp b/Source/Survival_projet/Private/SurvivalGameInstance.cpp new file mode 100644 index 0000000..1e5ef06 --- /dev/null +++ b/Source/Survival_projet/Private/SurvivalGameInstance.cpp @@ -0,0 +1,27 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "SurvivalGameInstance.h" + +#include "SurvivalUserSettings.h" + +void USurvivalGameInstance::Init() +{ + Super::Init(); + + // Les reglages sont deja LUS depuis le .ini par le moteur, mais rien ne les + // a encore APPLIQUES : sans cet appel, la resolution et la qualite + // choisies au dernier lancement resteraient lettre morte jusqu'au premier + // passage dans le menu. + // + // C'est le bon endroit : le GameInstance est le seul objet qui naisse avant + // la premiere map et survive a tous les changements de niveau. + if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) + { + Settings->ApplySettings(/*bCheckForCommandLineOverrides=*/false); + } + else + { + UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, aucun reglage ne sera pris en compte.")); + } +} diff --git a/Source/Survival_projet/Private/SurvivalUserSettings.cpp b/Source/Survival_projet/Private/SurvivalUserSettings.cpp new file mode 100644 index 0000000..b6552c2 --- /dev/null +++ b/Source/Survival_projet/Private/SurvivalUserSettings.cpp @@ -0,0 +1,144 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "SurvivalUserSettings.h" + +#include "Engine/Engine.h" + +namespace +{ + /** Arrondit sur le palier le plus proche, puis borne. */ + float SnapToStep(float Value, float Min, float Max, float Step) + { + const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step; + return FMath::Clamp(Snapped, Min, Max); + } +} + +USurvivalUserSettings::USurvivalUserSettings() +{ + // Le constructeur tourne sur le CDO, avant toute lecture du .ini : ces + // valeurs sont donc bien les defauts d'usine, ecrasees ensuite par le + // fichier s'il existe. + SetSurvivalGameplayToDefaults(); +} + +USurvivalUserSettings* USurvivalUserSettings::Get() +{ + return GEngine ? Cast(GEngine->GetGameUserSettings()) : nullptr; +} + +void USurvivalUserSettings::SetSurvivalGameplayToDefaults() +{ + LookSensitivity = 35.f; + bSeparateVerticalSensitivity = false; + LookSensitivityVertical = 35.f; + bInvertLookY = false; + FieldOfView = 90.f; + CameraShakeScale = 100.f; + bSprintToggle = false; + bCrouchToggle = false; +} + +void USurvivalUserSettings::SetToDefaults() +{ + // Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs : + // sans cette surcharge, l'onglet Jeu resterait seul inchange. + Super::SetToDefaults(); + + SetSurvivalGameplayToDefaults(); +} + +void USurvivalUserSettings::ApplySurvivalSettings() +{ + OnSurvivalSettingsApplied.Broadcast(); +} + +void USurvivalUserSettings::ApplyNonResolutionSettings() +{ + Super::ApplyNonResolutionSettings(); + + // ApplySettings() passe forcement par ici : brancher la diffusion a ce + // niveau garantit que nos reglages suivent, quel que soit le chemin. + ApplySurvivalSettings(); +} + +float USurvivalUserSettings::GetLookMultiplierX() const +{ + using namespace SurvivalSettingsRange; + + const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivity); + return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized); +} + +float USurvivalUserSettings::GetLookMultiplierY() const +{ + using namespace SurvivalSettingsRange; + + // Sans axe separe, le vertical suit l'horizontal : c'est ce que veulent + // 95 % des joueurs, et deux curseurs desynchronises par inadvertance + // donnent une visee bancale sans qu'on comprenne pourquoi. + if (!bSeparateVerticalSensitivity) + { + return GetLookMultiplierX(); + } + + const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivityVertical); + return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized); +} + +float USurvivalUserSettings::GetCameraShakeMultiplier() const +{ + return FMath::Clamp(CameraShakeScale / 100.f, 0.f, 1.f); +} + +void USurvivalUserSettings::SetLookSensitivity(float Value) +{ + using namespace SurvivalSettingsRange; + LookSensitivity = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep); +} + +void USurvivalUserSettings::SetSeparateVerticalSensitivity(bool bValue) +{ + // En repassant sur "non separe" on recale le vertical sur l'horizontal : + // sinon la valeur oubliee ressortirait telle quelle a la reactivation. + if (!bValue) + { + LookSensitivityVertical = LookSensitivity; + } + + bSeparateVerticalSensitivity = bValue; +} + +void USurvivalUserSettings::SetLookSensitivityVertical(float Value) +{ + using namespace SurvivalSettingsRange; + LookSensitivityVertical = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep); +} + +void USurvivalUserSettings::SetInvertLookY(bool bValue) +{ + bInvertLookY = bValue; +} + +void USurvivalUserSettings::SetFieldOfView(float Value) +{ + using namespace SurvivalSettingsRange; + FieldOfView = SnapToStep(Value, FieldOfViewMin, FieldOfViewMax, FieldOfViewStep); +} + +void USurvivalUserSettings::SetCameraShakeScale(float Value) +{ + using namespace SurvivalSettingsRange; + CameraShakeScale = SnapToStep(Value, CameraShakeMin, CameraShakeMax, CameraShakeStep); +} + +void USurvivalUserSettings::SetSprintToggle(bool bValue) +{ + bSprintToggle = bValue; +} + +void USurvivalUserSettings::SetCrouchToggle(bool bValue) +{ + bCrouchToggle = bValue; +} diff --git a/Source/Survival_projet/Public/FpsPlayer.h b/Source/Survival_projet/Public/FpsPlayer.h index 81545ba..f150572 100644 --- a/Source/Survival_projet/Public/FpsPlayer.h +++ b/Source/Survival_projet/Public/FpsPlayer.h @@ -89,10 +89,23 @@ public: UFUNCTION(BlueprintCallable, Category = "Camera") void AddCameraPunch(float Strength); + /** + * Recopie les reglages du joueur dans le pawn. + * + * Appelee au BeginPlay ET a chaque fois que USurvivalUserSettings diffuse : + * apres un respawn le pawn est un NOUVEL acteur, une valeur poussee une + * seule fois serait perdue avec le cadavre. UFUNCTION obligatoire, c'est ce + * qui permet le AddDynamic sur le delegue. + */ + UFUNCTION() + void ApplyUserSettings(); + protected: // Called when the game starts or when spawned virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + // Appelés par le CharacterMovement quand la capsule change de taille (accroupi) virtual void OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) override; virtual void OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) override; @@ -209,6 +222,10 @@ protected: /** * Sensibilité horizontale : degrés de rotation par unité de souris. * 1.0 = brut (beaucoup trop rapide), ~0.4 est un bon point de départ. + * + * ATTENTION : ces trois valeurs, comme BaseFOV, sont ECRASEES au BeginPlay + * par USurvivalUserSettings. Elles ne servent plus que de repli si l'objet + * de réglages est absent ; pour changer la sensibilité, passe par le menu. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera", meta = (ClampMin = "0.01")) float LookSensitivityX = 0.4f; @@ -344,6 +361,16 @@ protected: void StopCrouch(); void Interact(); + /** + * Relachement de la touche de sprint / d'accroupissement. + * + * Separes de StopSprint et StopCrouch, qui restent des ordres inconditionnels : + * en mode bascule le relachement ne doit rien faire, alors que la mort ou + * l'ouverture de l'inventaire doivent bel et bien couper le sprint. + */ + void HandleSprintReleased(); + void HandleCrouchReleased(); + /** * Clic gauche : agit avec l'objet actif de la barre rapide. * Aujourd'hui seuls les consommables font quelque chose ; c'est ici que @@ -392,4 +419,15 @@ private: /** Évite de spammer le log si IA_Look est mal configurée. */ bool bLoggedLookTypeWarning = false; + + // ------------------------------------------------------------------ + // Copies locales des réglages joueur, rafraîchies par ApplyUserSettings + // ------------------------------------------------------------------ + + /** 0 = caméra parfaitement fixe, 1 = amplitude d'origine. */ + float CameraShakeMultiplier = 1.f; + + bool bSprintToggleMode = false; + + bool bCrouchToggleMode = false; }; diff --git a/Source/Survival_projet/Public/FpsPlayerController.h b/Source/Survival_projet/Public/FpsPlayerController.h index 672b5c5..ddc5786 100644 --- a/Source/Survival_projet/Public/FpsPlayerController.h +++ b/Source/Survival_projet/Public/FpsPlayerController.h @@ -6,12 +6,15 @@ #include "GameFramework/PlayerController.h" #include "FpsPlayerController.generated.h" +class UConfirmDialogWidget; class UHotbarWidget; class UInputAction; class UInventoryComponent; class UInteractionPromptWidget; class UInventoryWidget; +class UPauseMenuWidget; class UScreenFadeComponent; +class USettingsMenuWidget; class USurvivalStatsWidget; class UUserWidget; struct FInputActionValue; @@ -36,6 +39,21 @@ public: UFUNCTION(BlueprintPure, Category = "UI") bool IsInventoryOpen() const { return bInventoryOpen; } + /** Ferme le menu de pause et relance le monde. */ + UFUNCTION(BlueprintCallable, Category = "UI|Pause") + void ResumeGame(); + + /** Ouvre la demande de confirmation. Ne quitte rien par elle-meme. */ + UFUNCTION(BlueprintCallable, Category = "UI|Pause") + void RequestQuitToMainMenu(); + + UFUNCTION(BlueprintPure, Category = "UI|Pause") + bool IsPauseMenuOpen() const { return bPauseMenuOpen; } + + /** Ouvre l'ecran de reglages par-dessus le menu de pause. */ + UFUNCTION(BlueprintCallable, Category = "UI|Reglages") + void OpenSettings(); + protected: virtual void BeginPlay() override; virtual void OnPossess(APawn* InPawn) override; @@ -105,6 +123,17 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") TObjectPtr HotbarScrollAction; + /** + * Echap : ferme l'inventaire s'il est ouvert, sinon bascule la pause. + * + * ATTENTION : coche "Trigger When Paused" sur l'asset IA_Pause. Un jeu en + * pause ne fait plus qu'un tick reduit ou seules les actions portant ce + * drapeau sont evaluees -- sans lui, on peut mettre en pause mais plus + * jamais en sortir au clavier. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") + TObjectPtr PauseAction; + /** * Viseur, affiche en permanence. Widget separe du prompt : ce dernier se * met en Collapsed des qu'on ne vise plus rien, et emporterait le viseur @@ -129,6 +158,42 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI") TSubclassOf SurvivalStatsClass; + /** Ton WBP_PauseMenu. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause") + TSubclassOf PauseMenuClass; + + /** + * Ton WBP_ConfirmDialog, le meme que celui du menu principal. Laisse-le + * assigne : sans lui on retourne au menu sans rien demander, et la partie + * est perdue sur un clic. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause") + TSubclassOf QuitToMenuDialogClass; + + /** La question posee avant d'abandonner la partie. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause") + FText QuitToMenuMessage; + + /** + * Ton WBP_Settings, le meme que celui du menu principal. C'est tout + * l'interet de son delegue OnCloseRequested : l'ecran ignore qui l'ouvre. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Reglages") + TSubclassOf SettingsMenuClass; + + /** + * Ta map de menu principal. + * + * TSoftObjectPtr comme dans AMainMenuPlayerController : une reference dure + * ferait charger la map du menu en meme temps que celle du jeu. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Pause") + TSoftObjectPtr MainMenuLevel; + + /** Fondu au noir avant de rendre la main au menu principal. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1")) + float QuitToMenuFadeDuration = 1.f; + UPROPERTY(Transient, BlueprintReadOnly, Category = "UI") TObjectPtr Crosshair; @@ -144,9 +209,43 @@ protected: UPROPERTY(Transient, BlueprintReadOnly, Category = "UI") TObjectPtr SurvivalStatsWidget; + UPROPERTY(Transient, BlueprintReadOnly, Category = "UI|Pause") + TObjectPtr PauseMenu; + + UPROPERTY(Transient, BlueprintReadOnly, Category = "UI|Pause") + TObjectPtr QuitToMenuDialog; + + UPROPERTY(Transient, BlueprintReadOnly, Category = "UI|Reglages") + TObjectPtr SettingsMenu; + private: void CreateHudWidgets(); + /** + * Echap. Une seule touche pour trois usages, d'ou une priorite explicite : + * boite de confirmation, puis inventaire, puis pause. Sans cet ordre on se + * retrouve avec l'inventaire et la pause ouverts en meme temps. + */ + void HandlePauseInput(); + + void OpenPauseMenu(); + + /** Oui : fondu au noir, puis retour au menu principal. */ + UFUNCTION() + void HandleQuitToMenuConfirmed(); + + /** Non : on referme la boite et on rend la main au menu de pause. */ + UFUNCTION() + void HandleQuitToMenuCancelled(); + + /** Le fondu est termine : on charge la map du menu, si c'est ce qu'on attendait. */ + UFUNCTION() + void HandleFadeFinished(); + + /** L'ecran de reglages demande a se fermer : on rend la main au menu de pause. */ + UFUNCTION() + void HandleSettingsCloseRequested(); + /** Jette la pile survolee dans la grille, si l'inventaire est ouvert. */ void DropHoveredItem(); @@ -166,4 +265,18 @@ private: FTimerHandle RespawnTimerHandle; bool bInventoryOpen = false; + + bool bPauseMenuOpen = false; + + /** + * La boite de confirmation avale Echap elle-meme quand elle a le focus, + * mais un clic ailleurs le lui retire : sans ce booleen, Echap refermerait + * alors le menu de pause en laissant la question affichee dans le vide. + */ + bool bQuitToMenuDialogOpen = false; + + bool bSettingsOpen = false; + + /** Distingue le fondu de sortie de tous les autres dans HandleFadeFinished. */ + bool bQuittingToMainMenu = false; }; diff --git a/Source/Survival_projet/Public/MainMenuPlayerController.h b/Source/Survival_projet/Public/MainMenuPlayerController.h index ceeb5ed..828b584 100644 --- a/Source/Survival_projet/Public/MainMenuPlayerController.h +++ b/Source/Survival_projet/Public/MainMenuPlayerController.h @@ -10,6 +10,7 @@ class AMenuCameraSpot; class UConfirmDialogWidget; class UMainMenuWidget; class UScreenFadeComponent; +class USettingsMenuWidget; /** * Ce qu'on fait une fois l'ecran devenu noir. @@ -45,6 +46,10 @@ public: UFUNCTION(BlueprintCallable, Category = "Menu") void QuitGame(); + /** Ouvre l'ecran de reglages par-dessus le menu. */ + UFUNCTION(BlueprintCallable, Category = "Menu") + void OpenSettings(); + protected: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; @@ -75,6 +80,10 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu") FText QuitDialogMessage; + /** Ton WBP_Settings, exactement le meme que celui du menu de pause. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu") + TSubclassOf SettingsMenuClass; + /** Fondu d'arrivee sur le menu, au lancement du jeu. */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Menu", meta = (ClampMin = "0.1")) float OpeningFadeDuration = 1.5f; @@ -93,6 +102,9 @@ protected: UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu") TObjectPtr QuitDialog; + UPROPERTY(Transient, BlueprintReadOnly, Category = "Menu") + TObjectPtr SettingsMenu; + private: /** Tire un angle de vue au hasard, en evitant celui du dernier passage. */ AMenuCameraSpot* PickCameraSpot(); @@ -109,6 +121,10 @@ private: UFUNCTION() void HandleQuitCancelled(); + /** L'ecran de reglages demande a se fermer : on rend la main au menu. */ + UFUNCTION() + void HandleSettingsCloseRequested(); + /** Ferme le jeu sans rien demander. */ void CloseGame(); diff --git a/Source/Survival_projet/Public/MainMenuWidget.h b/Source/Survival_projet/Public/MainMenuWidget.h index 6f3072e..10174de 100644 --- a/Source/Survival_projet/Public/MainMenuWidget.h +++ b/Source/Survival_projet/Public/MainMenuWidget.h @@ -35,6 +35,10 @@ protected: UPROPERTY(meta = (BindWidget)) TObjectPtr QuitButton; + /** BindWidgetOptional : un WBP_MainMenu sans bouton Reglages reste valide. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr SettingsButton; + /** * Coop a venir. BindWidgetOptional et non BindWidget : tu peux dessiner ton * WBP_MainMenu sans eux aujourd'hui, le Blueprint compilera quand meme. @@ -52,4 +56,7 @@ private: UFUNCTION() void HandleQuitClicked(); + + UFUNCTION() + void HandleSettingsClicked(); }; diff --git a/Source/Survival_projet/Public/PauseMenuWidget.h b/Source/Survival_projet/Public/PauseMenuWidget.h new file mode 100644 index 0000000..bd3b010 --- /dev/null +++ b/Source/Survival_projet/Public/PauseMenuWidget.h @@ -0,0 +1,57 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PauseMenuWidget.generated.h" + +class UButton; + +/** + * Le menu de pause : trois boutons par-dessus la partie figee. + * + * Comme UMainMenuWidget, il ne decide de rien -- il appelle + * AFpsPlayerController. Mettre SetPause ou OpenLevel ici serait doublement + * faux : un widget n'existe que sur la machine du joueur local, et la mise en + * pause appartient a l'autorite. + * + * Classe Abstract : on cree un WBP_PauseMenu qui en herite. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API UPauseMenuWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + +protected: + /** Reprend la partie. Nomme exactement "ResumeButton" dans le WBP. */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr ResumeButton; + + /** Retour au menu principal, apres confirmation. */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr QuitToMenuButton; + + /** + * BindWidgetOptional : un WBP_PauseMenu sans bouton Reglages reste valide. + * + * Pas de bouton "Quitter le jeu" ici, volontairement : la sortie se fait + * depuis le menu principal, ou l'on repasse de toute facon. + */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr SettingsButton; + +private: + UFUNCTION() + void HandleResumeClicked(); + + UFUNCTION() + void HandleSettingsClicked(); + + UFUNCTION() + void HandleQuitToMenuClicked(); +}; diff --git a/Source/Survival_projet/Public/SettingRowWidget.h b/Source/Survival_projet/Public/SettingRowWidget.h new file mode 100644 index 0000000..22b4e4c --- /dev/null +++ b/Source/Survival_projet/Public/SettingRowWidget.h @@ -0,0 +1,127 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "SettingRowWidget.generated.h" + +class UButton; +class UProgressBar; +class UTextBlock; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSettingRowIndexChanged, int32, NewIndex); + +/** + * Une ligne de reglage : un libelle, une fleche gauche, une valeur, une fleche + * droite, et une barre de remplissage optionnelle derriere la valeur. + * + * TOUT reglage est ramene a un index dans une liste de paliers, y compris les + * nombres : "Champ de vision, 70 a 120 par pas de 5" devient un index de 0 a 10. + * La ligne ne connait donc jamais que CurrentIndex et StepCount, et c'est ce qui + * permet de n'avoir qu'UN SEUL WBP a dessiner pour tout le menu, qu'il s'agisse + * d'un choix (Maintien / Bascule) ou d'une valeur continue. + * + * Elle ne sait pas ce qu'elle regle : elle previent via OnIndexChanged, et c'est + * l'onglet qui traduit l'index en appel sur USurvivalUserSettings. Meme principe + * que UConfirmDialogWidget, qui ignore ce qu'il confirme. + * + * Classe Abstract : on cree un WBP_SettingRow qui en herite. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API USettingRowWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeOnInitialized() override; + + /** Previent d'un changement venant du joueur, jamais d'un rafraichissement. */ + UPROPERTY(BlueprintAssignable, Category = "Reglages") + FOnSettingRowIndexChanged OnIndexChanged; + + /** + * Mode choix : la valeur affichee est prise telle quelle dans Options. + * Sert aussi bien a un oui/non qu'a une liste de qualites. + */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void SetupAsChoice(const FText& Label, const TArray& Options, int32 Index); + + /** + * Mode nombre : la valeur affichee est calculee a partir de l'index. + * Suffix sert aux unites ("%", "°") et peut rester vide. + */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void SetupAsNumber(const FText& Label, float Min, float Max, float Step, float Value, const FText& Suffix); + + UFUNCTION(BlueprintPure, Category = "Reglages") + int32 GetIndex() const { return CurrentIndex; } + + /** Reconvertit l'index en valeur reelle. Sans objet en mode choix. */ + UFUNCTION(BlueprintPure, Category = "Reglages") + float GetNumericValue() const; + + /** + * Repositionne la ligne sans rien diffuser. + * + * A utiliser pour tout rafraichissement venant des reglages : diffuser ici + * ferait boucler l'onglet sur lui-meme (il reecrit le reglage, qui rafraichit + * la ligne, qui rediffuse...). + */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void SetIndexSilently(int32 NewIndex); + + /** Repositionne la ligne sur une valeur reelle, sans rien diffuser. */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void SetNumericValueSilently(float Value); + +protected: + UPROPERTY(meta = (BindWidget)) + TObjectPtr LabelText; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr ValueText; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr LeftArrowButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr RightArrowButton; + + /** + * Facultative : une ligne de choix (Maintien / Bascule) n'a rien a remplir. + * Volontairement une ProgressBar et non un Slider : la barre montre ou l'on + * en est, ce sont les fleches qui reglent. Un slider glissable produirait des + * valeurs entre deux paliers que le setter refuserait ensuite. + */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ValueBar; + +private: + UFUNCTION() + void HandleLeftClicked(); + + UFUNCTION() + void HandleRightClicked(); + + /** Deplace l'index de Delta paliers et diffuse, si le mouvement est possible. */ + void StepIndex(int32 Delta); + + /** Reecrit le texte, la barre et l'etat des deux fleches. */ + void Refresh(); + + /** Vide en mode nombre. */ + TArray ChoiceLabels; + + FText NumericSuffix; + + float NumericMin = 0.f; + float NumericStep = 1.f; + + int32 CurrentIndex = 0; + + /** Nombre de paliers, donc d'index valides. Toujours au moins 1. */ + int32 StepCount = 1; + + bool bNumeric = false; +}; diff --git a/Source/Survival_projet/Public/SettingsGameplayWidget.h b/Source/Survival_projet/Public/SettingsGameplayWidget.h new file mode 100644 index 0000000..bfc45d8 --- /dev/null +++ b/Source/Survival_projet/Public/SettingsGameplayWidget.h @@ -0,0 +1,89 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "SettingsTabWidget.h" +#include "SettingsGameplayWidget.generated.h" + +class USettingRowWidget; +class USurvivalUserSettings; + +/** + * L'onglet "Jeu & Controles". + * + * Il ne stocke rien : chaque ligne ecrit directement dans USurvivalUserSettings, + * qui diffuse au pawn dans la foulee. C'est ce qui rend l'effet immediat sans + * bouton "Appliquer". + * + * Classe Abstract : on cree un WBP_SettingsGameplay qui en herite, avec un + * WBP_SettingRow par ligne, nomme exactement comme le BindWidget correspondant. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API USettingsGameplayWidget : public USettingsTabWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + + virtual void RefreshFromSettings() override; + virtual void ResetToDefaults() override; + +protected: + UPROPERTY(meta = (BindWidget)) + TObjectPtr SensitivityRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr SeparateVerticalRow; + + /** Masquee tant que la sensibilite verticale n'est pas separee. */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr SensitivityVerticalRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr InvertLookRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr FieldOfViewRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CameraShakeRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr SprintModeRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CrouchModeRow; + +private: + UFUNCTION() + void HandleSensitivityChanged(int32 NewIndex); + + UFUNCTION() + void HandleSeparateVerticalChanged(int32 NewIndex); + + UFUNCTION() + void HandleSensitivityVerticalChanged(int32 NewIndex); + + UFUNCTION() + void HandleInvertLookChanged(int32 NewIndex); + + UFUNCTION() + void HandleFieldOfViewChanged(int32 NewIndex); + + UFUNCTION() + void HandleCameraShakeChanged(int32 NewIndex); + + UFUNCTION() + void HandleSprintModeChanged(int32 NewIndex); + + UFUNCTION() + void HandleCrouchModeChanged(int32 NewIndex); + + /** Ecrit puis diffuse. Tous les handlers finissent ici. */ + void PushToPawn(USurvivalUserSettings* Settings); + + /** Affiche ou masque la ligne de sensibilite verticale. */ + void UpdateVerticalRowVisibility(bool bSeparate); +}; diff --git a/Source/Survival_projet/Public/SettingsMenuWidget.h b/Source/Survival_projet/Public/SettingsMenuWidget.h new file mode 100644 index 0000000..70355f3 --- /dev/null +++ b/Source/Survival_projet/Public/SettingsMenuWidget.h @@ -0,0 +1,135 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "SettingsMenuWidget.generated.h" + +class UButton; +class UConfirmDialogWidget; +class USettingsTabWidget; +class UWidgetSwitcher; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSettingsCloseRequested); + +/** + * L'ecran de reglages : une barre d'onglets, un WidgetSwitcher, Retour et + * Reinitialiser. + * + * Il ne sait PAS qui l'a ouvert. Il demande a se fermer via OnCloseRequested et + * c'est l'appelant -- menu principal ou menu de pause -- qui decide de ce que + * "Retour" veut dire. Sans ca il faudrait un ecran par contexte. + * + * Il ne connait pas davantage le contenu de ses onglets : il appelle + * RefreshFromSettings et ResetToDefaults sur USettingsTabWidget. Ajouter + * l'onglet Graphismes ne demandera aucune ligne ici. + * + * Classe Abstract : on cree un WBP_Settings qui en herite. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API USettingsMenuWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + USettingsMenuWidget(const FObjectInitializer& ObjectInitializer); + + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override; + + UPROPERTY(BlueprintAssignable, Category = "Reglages") + FOnSettingsCloseRequested OnCloseRequested; + + /** + * Affiche l'ecran, rafraichit l'onglet visible et prend le focus clavier. + * + * Passe par ici et jamais par SetVisibility : sans le focus, Echap ne + * refermerait pas l'ecran. + */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void Open(); + + /** Masque l'ecran et ecrit le fichier de reglages. */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void Close(); + +protected: + UPROPERTY(meta = (BindWidget)) + TObjectPtr TabSwitcher; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr BackButton; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ResetButton; + + /** + * Les onglets a venir. Grises automatiquement tant que le WidgetSwitcher ne + * contient pas la page correspondante -- meme principe que les boutons de + * coop du menu principal : ils annoncent la couleur sans mener nulle part. + */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr GameplayTabButton; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr DisplayTabButton; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr GraphicsTabButton; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr AudioTabButton; + + /** + * Boite de confirmation POSEE DANS le WBP_Settings, et non ajoutee au + * viewport : elle se retrouve ainsi naturellement au-dessus de l'ecran de + * reglages, sans avoir a lui trouver un ZOrder qui n'entre en conflit ni + * avec le menu de pause ni avec la boite d'abandon de partie. + * + * Absente, "Reinitialiser" agit sans rien demander. + */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ResetDialog; + + /** La question posee avant de reinitialiser l'onglet visible. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages") + FText ResetDialogMessage; + +private: + UFUNCTION() + void HandleBackClicked(); + + UFUNCTION() + void HandleResetClicked(); + + UFUNCTION() + void HandleResetConfirmed(); + + UFUNCTION() + void HandleResetCancelled(); + + UFUNCTION() + void HandleGameplayTabClicked(); + + UFUNCTION() + void HandleDisplayTabClicked(); + + UFUNCTION() + void HandleGraphicsTabClicked(); + + UFUNCTION() + void HandleAudioTabClicked(); + + /** Change de page et rafraichit la nouvelle. Ignore un index inexistant. */ + void ShowTab(int32 Index); + + /** La page actuellement affichee, ou nullptr si le switcher est vide. */ + USettingsTabWidget* GetActiveTab() const; + + /** Grise les boutons dont la page n'existe pas encore. */ + void UpdateTabButtonStates(); + + bool bResetDialogOpen = false; +}; diff --git a/Source/Survival_projet/Public/SettingsTabWidget.h b/Source/Survival_projet/Public/SettingsTabWidget.h new file mode 100644 index 0000000..c134f1d --- /dev/null +++ b/Source/Survival_projet/Public/SettingsTabWidget.h @@ -0,0 +1,27 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "SettingsTabWidget.generated.h" + +/** + * Base commune a tous les onglets de reglages. + * + * Deux verbes, rien de plus : l'ecran parent rafraichit ou reinitialise l'onglet + * visible sans jamais savoir ce qu'il contient. Ajouter l'onglet Graphismes plus + * tard ne demandera donc aucune ligne dans USettingsMenuWidget. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API USettingsTabWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** Relit les reglages et repositionne les lignes, sans rien diffuser. */ + virtual void RefreshFromSettings() {} + + /** Remet a l'usine les seuls reglages de cet onglet, puis se rafraichit. */ + virtual void ResetToDefaults() {} +}; diff --git a/Source/Survival_projet/Public/SurvivalGameInstance.h b/Source/Survival_projet/Public/SurvivalGameInstance.h index aca1b77..3b83bf5 100644 --- a/Source/Survival_projet/Public/SurvivalGameInstance.h +++ b/Source/Survival_projet/Public/SurvivalGameInstance.h @@ -24,6 +24,8 @@ class SURVIVAL_PROJET_API USurvivalGameInstance : public UGameInstance GENERATED_BODY() public: + virtual void Init() override; + /** Nom de la derniere camera de menu utilisee. NAME_None au premier lancement. */ UFUNCTION(BlueprintPure, Category = "Menu") FName GetLastMenuCameraName() const { return LastMenuCameraName; } diff --git a/Source/Survival_projet/Public/SurvivalUserSettings.h b/Source/Survival_projet/Public/SurvivalUserSettings.h new file mode 100644 index 0000000..981621a --- /dev/null +++ b/Source/Survival_projet/Public/SurvivalUserSettings.h @@ -0,0 +1,198 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameUserSettings.h" +#include "SurvivalUserSettings.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalSettingsApplied); + +/** + * Bornes et pas des reglages numeriques. + * + * Ici et pas dans le widget : c'est l'objet de reglages qui borne ses propres + * valeurs, et l'ecran doit proposer exactement les memes paliers. Deux jeux de + * constantes finiraient par diverger et le menu afficherait des valeurs que le + * setter refuse. + */ +namespace SurvivalSettingsRange +{ + constexpr float SensitivityMin = 5.f; + constexpr float SensitivityMax = 100.f; + constexpr float SensitivityStep = 5.f; + + constexpr float FieldOfViewMin = 70.f; + constexpr float FieldOfViewMax = 120.f; + constexpr float FieldOfViewStep = 5.f; + + constexpr float CameraShakeMin = 0.f; + constexpr float CameraShakeMax = 100.f; + constexpr float CameraShakeStep = 10.f; + + /** + * Multiplicateurs reels correspondant aux extremites de l'echelle affichee. + * + * Le joueur regle un nombre de 5 a 100 parce que "0.4" ne veut rien dire ; + * le pawn, lui, veut le multiplicateur. Le defaut de 35 retombe sur ~0.41, + * soit la valeur qui etait codee en dur dans AFpsPlayer. + */ + constexpr float LookMultiplierAtMin = 0.05f; + constexpr float LookMultiplierAtMax = 1.2f; +} + +/** + * Les reglages du joueur, et le seul endroit qui fasse autorite dessus. + * + * Sous-classe de UGameUserSettings et non un objet a nous : le moteur fournit + * deja le singleton, sa persistance dans Saved/Config/Windows/GameUserSettings.ini + * et tout le volet video (resolution, VSync, qualite). C'est l'equivalent d'un + * PlayerPrefs + QualitySettings + Screen d'Unity reunis, sauf qu'il est deja + * ecrit. On l'etend, on ne le remplace pas. + * + * Pour que le moteur instancie CETTE classe et pas la sienne, Config/DefaultEngine.ini + * contient : + * [/Script/Engine.Engine] + * GameUserSettingsClassName=/Script/Survival_projet.SurvivalUserSettings + * + * Les UPROPERTY(config) sont lues et ecrites automatiquement : aucun code de + * serialisation a maintenir. + */ +UCLASS() +class SURVIVAL_PROJET_API USurvivalUserSettings : public UGameUserSettings +{ + GENERATED_BODY() + +public: + USurvivalUserSettings(); + + /** + * Le raccourci typé vers le singleton. GEngine->GetGameUserSettings() + * renvoie la classe de base ; ce helper evite un cast a chaque appel. + */ + UFUNCTION(BlueprintPure, Category = "Reglages", meta = (DisplayName = "Get Survival User Settings")) + static USurvivalUserSettings* Get(); + + /** + * Previent tout le monde que les reglages de jeu ont change. + * + * Indispensable a cause du respawn : le pawn est un NOUVEL acteur a chaque + * mort, et une valeur poussee une fois dans l'ancien serait perdue. Chaque + * pawn lit les reglages a son BeginPlay et s'abonne ici pour la suite. + */ + UPROPERTY(BlueprintAssignable, Category = "Reglages") + FOnSurvivalSettingsApplied OnSurvivalSettingsApplied; + + /** + * Diffuse les reglages de jeu, sans toucher au volet video. + * + * Distinct d'ApplyNonResolutionSettings, qui repousse aussi la qualite et la + * limite de FPS : l'appeler a chaque clic de fleche sur la sensibilite + * provoquerait un a-coup pour rien. + */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void ApplySurvivalSettings(); + + /** Remet les seuls reglages de jeu a leur valeur d'usine. */ + UFUNCTION(BlueprintCallable, Category = "Reglages") + void SetSurvivalGameplayToDefaults(); + + virtual void SetToDefaults() override; + virtual void ApplyNonResolutionSettings() override; + + // ------------------------------------------------------------------ + // Valeurs pretes a l'emploi + // + // Le pawn ne doit jamais lire les champs bruts : ce sont des valeurs + // d'affichage (5 a 100), pas des multiplicateurs. + // ------------------------------------------------------------------ + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + float GetLookMultiplierX() const; + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + float GetLookMultiplierY() const; + + /** 0 = camera parfaitement fixe, 1 = amplitude d'origine. */ + UFUNCTION(BlueprintPure, Category = "Reglages|Jeu") + float GetCameraShakeMultiplier() const; + + // ------------------------------------------------------------------ + // Accesseurs bruts, pour l'ecran de reglages + // ------------------------------------------------------------------ + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + float GetLookSensitivity() const { return LookSensitivity; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Controles") + void SetLookSensitivity(float Value); + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + bool GetSeparateVerticalSensitivity() const { return bSeparateVerticalSensitivity; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Controles") + void SetSeparateVerticalSensitivity(bool bValue); + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + float GetLookSensitivityVertical() const { return LookSensitivityVertical; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Controles") + void SetLookSensitivityVertical(float Value); + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + bool GetInvertLookY() const { return bInvertLookY; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Controles") + void SetInvertLookY(bool bValue); + + UFUNCTION(BlueprintPure, Category = "Reglages|Jeu") + float GetFieldOfView() const { return FieldOfView; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Jeu") + void SetFieldOfView(float Value); + + UFUNCTION(BlueprintPure, Category = "Reglages|Jeu") + float GetCameraShakeScale() const { return CameraShakeScale; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Jeu") + void SetCameraShakeScale(float Value); + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + bool GetSprintToggle() const { return bSprintToggle; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Controles") + void SetSprintToggle(bool bValue); + + UFUNCTION(BlueprintPure, Category = "Reglages|Controles") + bool GetCrouchToggle() const { return bCrouchToggle; } + + UFUNCTION(BlueprintCallable, Category = "Reglages|Controles") + void SetCrouchToggle(bool bValue); + +private: + /** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */ + UPROPERTY(config) + float LookSensitivity; + + UPROPERTY(config) + bool bSeparateVerticalSensitivity; + + UPROPERTY(config) + float LookSensitivityVertical; + + UPROPERTY(config) + bool bInvertLookY; + + UPROPERTY(config) + float FieldOfView; + + /** Pourcentage. A 0, plus aucun impact camera : pour le mal des transports. */ + UPROPERTY(config) + float CameraShakeScale; + + UPROPERTY(config) + bool bSprintToggle; + + UPROPERTY(config) + bool bCrouchToggle; +};