diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 2718936c..62ae6954 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -73,38 +73,16 @@ bSharedMaterialNativeLibraries=True ApplocalPrerequisitesDirectory=(Path="") IncludeCrashReporter=False InternationalizationPreset=English --CulturesToStage=en +CulturesToStage=en +CulturesToStage=ja +CulturesToStage=uk LocalizationTargetCatchAllChunkId=0 bCookAll=False +bCoolAllCultures=False bCookMapsOnly=False bTreatWarningsAsErrorsOnCook=False bSkipEditorContent=False bSkipMovies=False --IniKeyDenylist=KeyStorePassword --IniKeyDenylist=KeyPassword --IniKeyDenylist=DebugKeyStorePassword --IniKeyDenylist=DebugKeyPassword --IniKeyDenylist=rsa.privateexp --IniKeyDenylist=rsa.modulus --IniKeyDenylist=rsa.publicexp --IniKeyDenylist=aes.key --IniKeyDenylist=SigningPublicExponent --IniKeyDenylist=SigningModulus --IniKeyDenylist=SigningPrivateExponent --IniKeyDenylist=EncryptionKey --IniKeyDenylist=DevCenterUsername --IniKeyDenylist=DevCenterPassword --IniKeyDenylist=IOSTeamID --IniKeyDenylist=SigningCertificate --IniKeyDenylist=MobileProvision --IniKeyDenylist=AppStoreConnectKeyPath --IniKeyDenylist=AppStoreConnectIssuerID --IniKeyDenylist=AppStoreConnectKeyID --IniKeyDenylist=IniKeyDenylist --IniKeyDenylist=IniSectionDenylist +IniKeyDenylist=KeyStorePassword +IniKeyDenylist=KeyPassword +IniKeyDenylist=DebugKeyStorePassword @@ -127,9 +105,6 @@ bSkipMovies=False +IniKeyDenylist=AppStoreConnectKeyID +IniKeyDenylist=IniKeyDenylist +IniKeyDenylist=IniSectionDenylist --IniSectionDenylist=HordeStorageServers --IniSectionDenylist=StorageServers --IniSectionDenylist=/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings +IniSectionDenylist=HordeStorageServers +IniSectionDenylist=StorageServers +IniSectionDenylist=/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings diff --git a/Content/Blueprints/BP_MainMenuHUD.uasset b/Content/Blueprints/BP_MainMenuHUD.uasset new file mode 100644 index 00000000..04618bdd --- /dev/null +++ b/Content/Blueprints/BP_MainMenuHUD.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a74d0f331874ddef72b192d2050f08706b0a82868a84ed4aac19f74b52f04f43 +size 24466 diff --git a/Content/Blueprints/Data/AudioSettingsConfig.uasset b/Content/Blueprints/Data/AudioSettingsConfig.uasset new file mode 100644 index 00000000..b22cdf1f --- /dev/null +++ b/Content/Blueprints/Data/AudioSettingsConfig.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a72823f2a8db72479152b425127f0cf08635b268b59df7c85093425e0136b15 +size 2072 diff --git a/Content/Blueprints/GM_MainMenu.uasset b/Content/Blueprints/GM_MainMenu.uasset index 7c6fac05..b58eb749 100644 --- a/Content/Blueprints/GM_MainMenu.uasset +++ b/Content/Blueprints/GM_MainMenu.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec3e701bb49a260f9d3589a7e24ba974da54daa495e9d09700ede356de374df6 -size 22171 +oid sha256:969c0dd11352fea0a80a378292f93f47850861d69069f7bf11139d6133ee8cec +size 15027 diff --git a/Content/Blueprints/Player/BP_Player.uasset b/Content/Blueprints/Player/BP_Player.uasset index 182f972e..9da48ee0 100644 --- a/Content/Blueprints/Player/BP_Player.uasset +++ b/Content/Blueprints/Player/BP_Player.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0bb35097f2f39926da90eb5e9ba08cddc6d5f60c929eb5cf4d23242d1b41d665 -size 71867 +oid sha256:cf44ebf09c3c03027accb25492b3bfa34d7f14038dac8398cc5058f667753146 +size 72100 diff --git a/Content/Maps/MainMenu.umap b/Content/Maps/MainMenu.umap index 02c9da74..7abd9981 100644 --- a/Content/Maps/MainMenu.umap +++ b/Content/Maps/MainMenu.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f090602ec2799203964c49489a917b270648b6a1b9e44ac7ca01663d9270332 -size 147577 +oid sha256:70e1a3fa6ac97a63b4a07e0c1046853763b18021b768198b0069e164ab4043b6 +size 116453 diff --git a/Content/UI/Buttons/MI_MainMenuButtonText.uasset b/Content/UI/Buttons/MI_MainMenuButtonText.uasset new file mode 100644 index 00000000..66522d8b --- /dev/null +++ b/Content/UI/Buttons/MI_MainMenuButtonText.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6b79aaf07ac4dd2f61ea30798b90a2d9fbc490717b9039f9748a54d203626d4 +size 6212 diff --git a/Content/UI/Buttons/M_MainMenuButtonText.uasset b/Content/UI/Buttons/M_MainMenuButtonText.uasset new file mode 100644 index 00000000..ac5037d9 --- /dev/null +++ b/Content/UI/Buttons/M_MainMenuButtonText.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb604935c8133ef7755d9a1d004a2f729cf3eeb0da4eed82c4c73d1a234e566a +size 15607 diff --git a/Content/UI/Buttons/S_MainMenuButton.uasset b/Content/UI/Buttons/S_MainMenuButton.uasset new file mode 100644 index 00000000..f1ef2226 --- /dev/null +++ b/Content/UI/Buttons/S_MainMenuButton.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c62d6f736de7180ee6f832b5921cc1a388dd02bb2603bc843baa19124188f528 +size 6318 diff --git a/Content/UI/Buttons/Squircle.uasset b/Content/UI/Buttons/Squircle.uasset new file mode 100644 index 00000000..8093edb2 --- /dev/null +++ b/Content/UI/Buttons/Squircle.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44b7397ba6ca860b68f48cf6880617549269658b2733f90ddfd7ca4b49577d21 +size 12830 diff --git a/Content/UI/Buttons/WBP_MainMenuButton.uasset b/Content/UI/Buttons/WBP_MainMenuButton.uasset new file mode 100644 index 00000000..8119e976 --- /dev/null +++ b/Content/UI/Buttons/WBP_MainMenuButton.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99efd15dd749e8e7104ae6aacf667d18a22587cc80efd041e5b5e0d1d0d3782c +size 68947 diff --git a/Content/UI/MainMenu/WBP_ConfirmModal.uasset b/Content/UI/MainMenu/WBP_ConfirmModal.uasset new file mode 100644 index 00000000..c8cc1985 --- /dev/null +++ b/Content/UI/MainMenu/WBP_ConfirmModal.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2860dbb972d0acf927c90da66be27eda70b996e0a6eb94d9289011060b6fc55 +size 34039 diff --git a/Content/UI/MainMenu/WBP_Credits.uasset b/Content/UI/MainMenu/WBP_Credits.uasset index 337e1891..b83d5dda 100644 --- a/Content/UI/MainMenu/WBP_Credits.uasset +++ b/Content/UI/MainMenu/WBP_Credits.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b2f4de72975e4e85fbd6290390bb7fcd66d28d7e726cbf484d28db059fbe63a -size 26820 +oid sha256:6c11c7acab2b8339cfe579ccdb8b633f45ce3e53eeb20468249caa3c4c568ed0 +size 27823 diff --git a/Content/UI/MainMenu/WBP_Disclaimer.uasset b/Content/UI/MainMenu/WBP_Disclaimer.uasset deleted file mode 100644 index ea47f79c..00000000 --- a/Content/UI/MainMenu/WBP_Disclaimer.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f526d3669e3093c082d0ab62b394d6b87227bf2635c6cd4c0c2d93fe7a306ed2 -size 68130 diff --git a/Content/UI/MainMenu/WBP_MainMenu.uasset b/Content/UI/MainMenu/WBP_MainMenu.uasset index bdb66e9c..79ab77b9 100644 --- a/Content/UI/MainMenu/WBP_MainMenu.uasset +++ b/Content/UI/MainMenu/WBP_MainMenu.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b24eaa44aa2abd3f9536ad9d667548d73763361656bac08588b88fd733ad25e -size 107970 +oid sha256:9d5271e7e54b8722be1e93a27b039641fac3dddd1a16753ee920d839d789056d +size 40457 diff --git a/Content/UI/Pause/WBP_PauseMenu.uasset b/Content/UI/Pause/WBP_PauseMenu.uasset index 27f3fde5..7ab541cc 100644 --- a/Content/UI/Pause/WBP_PauseMenu.uasset +++ b/Content/UI/Pause/WBP_PauseMenu.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df862bd12fbf7339a10f249c35044b8123d0d94e7ad6400e38c13d063982165b -size 72000 +oid sha256:1ae79cb92a221d31e7277f790247302b61f47515667031f35c22e3b37ef9348d +size 35128 diff --git a/Content/UI/Settings/WBP_AudioSettingsTab.uasset b/Content/UI/Settings/WBP_AudioSettingsTab.uasset new file mode 100644 index 00000000..f5ab86ee --- /dev/null +++ b/Content/UI/Settings/WBP_AudioSettingsTab.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:420d1cb79731c9f5e8d25b12a8af3dff8eee8e5429ce4b607012bfdf4785f983 +size 37091 diff --git a/Content/UI/Settings/WBP_GameplaySettingsTab.uasset b/Content/UI/Settings/WBP_GameplaySettingsTab.uasset new file mode 100644 index 00000000..caeb7c51 --- /dev/null +++ b/Content/UI/Settings/WBP_GameplaySettingsTab.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620a0b956d3a20f879d751031374a661c8ffbe2d6e514a4b2e1eca1e86c0fc62 +size 32228 diff --git a/Content/UI/Settings/WBP_GraphicsSettingsTab.uasset b/Content/UI/Settings/WBP_GraphicsSettingsTab.uasset new file mode 100644 index 00000000..710a2406 --- /dev/null +++ b/Content/UI/Settings/WBP_GraphicsSettingsTab.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baa7931c7675e980323a11ddb7abced4cc8a0fef7eedd4ac7db7ae29c5c11d4f +size 40437 diff --git a/Content/UI/Settings/WBP_SettingsScreen.uasset b/Content/UI/Settings/WBP_SettingsScreen.uasset new file mode 100644 index 00000000..e843d168 --- /dev/null +++ b/Content/UI/Settings/WBP_SettingsScreen.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c70fb0f0bb16825c19313cce70c96e5bd8640172e4eaccb0a10ffe4527848a46 +size 42322 diff --git a/Content/UI/WBP_GameLayout.uasset b/Content/UI/WBP_GameLayout.uasset index 340eec4f..f3008288 100644 --- a/Content/UI/WBP_GameLayout.uasset +++ b/Content/UI/WBP_GameLayout.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:424276b717d29192ff79d50969e3449c212f2f50a9bd9e93615688ea20e32ad3 -size 26811 +oid sha256:d3f2f778b0e91ba64f2eb1fc916350f374deb2db65bed8ad314909b7a9e094f7 +size 28548 diff --git a/Content/UI/WBP_MainMenuLayout.uasset b/Content/UI/WBP_MainMenuLayout.uasset deleted file mode 100644 index df72880c..00000000 --- a/Content/UI/WBP_MainMenuLayout.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61aea1e739502e68d479db0d05416811e20f8c8fa35323b33fe2e2bc853f5a35 -size 131312 diff --git a/PLAN.md b/PLAN.md index 3038d2a6..312280f6 100644 --- a/PLAN.md +++ b/PLAN.md @@ -98,6 +98,8 @@ State of the C++ module as of the latest pass. File references use `Source/Naked - **Session loss resolver (GDD §4.4)** — `Global/SessionLossResolver.h/.cpp` (`UWorldSubsystem`). Single entry point `ResolveLoss(ESessionLossCause)`, bound to `USessionManagerSubsystem::OnSessionEnd`. Applies the police-chase precedence override (any cause → `PoliceCapture` while `bPoliceChaseActive`), then per cause: `EmbarrassmentMax` no-cost; `EnergyZero` destroys every world `AItemPickup` + clears its save record (guaranteed sleep loss); `PoliceCapture` deducts `PoliceCaptureMoneyPenalty` if affordable else flags a holding-cell outcome; `SafeReturn` no loss. Never strips equipped clothing. Autosaves, broadcasts `OnSessionLossResolved(FinalCause, bWentToHoldingCell)`, then runs `BeginLossPresentation(Cause)`: looks up the cause's cutscene in the data-driven `ULossPresentationConfig` (`UNakedDesireGameInstance::LossPresentation`, a `TMap>`), plays it via `ULevelSequencePlayer` (movement/look input disabled during playback), and on `OnFinished` teleports the player to the home `APlayerStart` (one tagged `"Home"` preferred). No cutscene authored for a cause → teleports immediately; `SafeReturn` → no presentation. See §1.3 for the per-cause time-skip extras still pending. - **Movement** — `EnhancedInput`, walk / run / crouch (`NakedDesireCharacter.cpp:115-127`), stamina-gated run (`Tick` lines 91-113). - **Wardrobe storage + management** — `Inventory/InventorySubsystem` (`UGameInstanceSubsystem`) is the single runtime owner of the off-body store. It holds live `UItemInstance`s mirrored from `UGlobalSaveGameData::WardrobeItems` and exposes the atomic moves `AddToWardrobe` / `RemoveFromWardrobe` / `EquipFromWardrobe` / `UnequipToWardrobe`, each mutating the wardrobe + equipped save buckets together and broadcasting `OnWardrobeChanged`. `AClothingManager` stays the body-state authority (owns the `EquippedItems` bucket via the new `EquipSlot` + existing `RemoveClothing`); the bodysuit exclusion rule is now the shared static `UClothingManager::GetBodysuitExcludedSlots` and routes displaced garments back to the wardrobe instead of dropping them to the world. `AWardrobe` is reduced to an interaction shell that forwards to the subsystem (its stale `ClothingItems` array is gone). UI: `WardrobeScreenWidget` inits the inventory list and hosts the `EquipmentSlotMenuWidget` popup (same plumbing as `InventoryScreenWidget`); `WardrobeInventoryWidget` renders the live list and re-renders on `OnWardrobeChanged`; clicking a wardrobe item calls `EquipFromWardrobe`. The slot menu has a single Remove button whose `Init(slot, bAtWardrobe)` flag decides the action — store via `UnequipToWardrobe` when opened at the wardrobe, drop to the world otherwise. **Home-storage model (GDD §6.5 / §10.4 / §28):** the wardrobe is the general home stockpile for **all non-food** items (clothing, sex toys, phones, keys, spare bags) — `WardrobeItems` is already a generic `UItemInstance` list, so this matches with no change. Food is **not** stored in the wardrobe; it lives in the **fridge** (separate fixture, pending — see §1.3). **Follow-ups:** `BuyItem` buy-flow not reattached (no caller yet); world pickup (`ItemPickup`→`TakeClothing`) still doesn't clear the `WorldItems` record; non-clothing wardrobe items (phones/toys) are stored but not yet rendered in the wardrobe UI. +- **Front-end + pause menu shell (C++ logic).** `UI/Menu/` holds the CommonUI menu stack: `UMainMenuWidget` (Continue / New Game / Settings / Credits / Quit — Continue gated on `USaveSubsystem::DoesSaveExist()`, New Game + Quit route through `UConfirmModalWidget`, gameplay map is a data-driven `TSoftObjectPtr`), `UPauseMenuWidget` (Resume / Settings / Main Menu — pauses on activate via `SetGamePaused`, unpauses on deactivate, Main Menu confirms first), reusable `UConfirmModalWidget` (`Setup(title, msg)` + `OnConfirmed`/`OnCancelled` C++ delegates, pushed onto a nested `ModalStack`), and `UCreditsWidget` (back button). New Game is backed by `USaveSubsystem::StartNewGame()` (wipes the slot, reseeds from `StartingSaveData`, replaces the cached `CurrentSave`). Display: `Global/MainMenuHUD` creates the main menu on the menu map (assign as the menu map's HUD class); the pause menu pushes onto the existing `UGameLayoutWidget::WidgetStack` via `OpenPauseMenu()`, triggered by a new `PauseAction` EnhancedInput on `ANakedDesireCharacter`. **BP wiring still required:** reparent `WBP_MainMenu`/`WBP_PauseMenu`/`WBP_ConfirmModal`/`WBP_Credits` to these classes, match the `BindWidget` names, set the class/map defaults, add the `PauseAction` IA + IMC mapping, and set `AMainMenuHUD` on the MainMenu map. The main-menu gameplay target defaults to `L_TestCity` in C++ (overridable per-asset). +- **Settings screen (C++ logic, three tabs).** `UI/Menu/Settings/`: `USettingsScreenWidget` (tab buttons + `WidgetSwitcher` + Apply/Back; persists on Apply and on close) hosting `UGameplaySettingsTab` (censorship toggle — live via `OnSettingsChanged` — plus a language selector: English/Ukrainian/Japanese → cultures `en`/`uk`/`ja` via `UKismetInternationalizationLibrary::SetCurrentLanguageAndLocale`, applied + persisted instantly), `UAudioSettingsTab` (Master/Music/SFX sliders), and `UGraphicsSettingsTab` (quality preset, window mode, resolution, VSync via the inherited `UGameUserSettings`). Backend: `UNakedDesireUserSettings` gained `MusicVolume`/`SfxVolume` config + clamped getters/setters; `Global/AudioSettingsSubsystem` (`UGameInstanceSubsystem`) applies the three volumes to SoundClasses through a SoundMix described by the new `Global/AudioSettingsConfig` data asset (`UNakedDesireGameInstance::AudioConfig`), re-pushing on `OnSettingsChanged` and `PostLoadMapWithWorld`. Assign `USettingsScreenWidget`'s BP as `SettingsWidgetClass` on both menus to enable the Settings button. **BP wiring:** author `WBP_Settings` (+ per-tab widgets) matching the `BindWidget` names, and author + assign a `UAudioSettingsConfig` (SoundMix + 3 SoundClasses) on the GI for the volume sliders to be audible. ### 1.2 Partially implemented (deviates from GDD) diff --git a/Source/NakedDesire/Global/AudioSettingsConfig.h b/Source/NakedDesire/Global/AudioSettingsConfig.h new file mode 100644 index 00000000..cca9b98c --- /dev/null +++ b/Source/NakedDesire/Global/AudioSettingsConfig.h @@ -0,0 +1,32 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "AudioSettingsConfig.generated.h" + +class USoundMix; +class USoundClass; + +// Data-driven audio routing for the settings sliders (§17.4). The UAudioSettingsSubsystem +// pushes per-class volume overrides through SettingsMix when the user changes a slider. +// Author one SoundMix + three SoundClasses and assign on UNakedDesireGameInstance::AudioConfig. +UCLASS() +class NAKEDDESIRE_API UAudioSettingsConfig : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + UPROPERTY(EditDefaultsOnly, Category = "Audio") + TObjectPtr SettingsMix; + + UPROPERTY(EditDefaultsOnly, Category = "Audio") + TObjectPtr MasterClass; + + UPROPERTY(EditDefaultsOnly, Category = "Audio") + TObjectPtr MusicClass; + + UPROPERTY(EditDefaultsOnly, Category = "Audio") + TObjectPtr SfxClass; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Global/AudioSettingsSubsystem.cpp b/Source/NakedDesire/Global/AudioSettingsSubsystem.cpp new file mode 100644 index 00000000..f1dbba2b --- /dev/null +++ b/Source/NakedDesire/Global/AudioSettingsSubsystem.cpp @@ -0,0 +1,76 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "AudioSettingsSubsystem.h" + +#include "AudioSettingsConfig.h" +#include "NakedDesireGameInstance.h" +#include "NakedDesireUserSettings.h" +#include "Kismet/GameplayStatics.h" +#include "Sound/SoundClass.h" +#include "Sound/SoundMix.h" +#include "UObject/UObjectGlobals.h" + +void UAudioSettingsSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->OnSettingsChanged.AddUniqueDynamic(this, &UAudioSettingsSubsystem::HandleSettingsChanged); + + // SoundMix overrides are world-scoped, so re-push them every time a level finishes loading. + MapLoadHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UAudioSettingsSubsystem::HandleMapLoaded); +} + +void UAudioSettingsSubsystem::Deinitialize() +{ + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->OnSettingsChanged.RemoveDynamic(this, &UAudioSettingsSubsystem::HandleSettingsChanged); + + FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(MapLoadHandle); + + Super::Deinitialize(); +} + +void UAudioSettingsSubsystem::HandleSettingsChanged(UNakedDesireUserSettings* Settings) +{ + ApplyVolumes(); +} + +void UAudioSettingsSubsystem::HandleMapLoaded(UWorld* LoadedWorld) +{ + ApplyVolumes(); +} + +void UAudioSettingsSubsystem::ApplyVolumes() +{ + const UNakedDesireGameInstance* GameInstance = Cast(GetGameInstance()); + if (!GameInstance || !GameInstance->AudioConfig) + return; + + const UAudioSettingsConfig* Config = GameInstance->AudioConfig; + if (!Config->SettingsMix) + return; + + UWorld* World = GetWorld(); + if (!World) + return; + + const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + auto ApplyClass = [&](USoundClass* SoundClass, float Volume) + { + if (!SoundClass) + return; + // Pitch 1.0, no fade — instantaneous so the slider feels responsive. + UGameplayStatics::SetSoundMixClassOverride(World, Config->SettingsMix, SoundClass, Volume, 1.0f, 0.0f, true); + }; + + ApplyClass(Config->MasterClass, Settings->GetGlobalVolume()); + ApplyClass(Config->MusicClass, Settings->GetMusicVolume()); + ApplyClass(Config->SfxClass, Settings->GetSfxVolume()); + + UGameplayStatics::PushSoundMixModifier(World, Config->SettingsMix); +} \ No newline at end of file diff --git a/Source/NakedDesire/Global/AudioSettingsSubsystem.h b/Source/NakedDesire/Global/AudioSettingsSubsystem.h new file mode 100644 index 00000000..1920c1c1 --- /dev/null +++ b/Source/NakedDesire/Global/AudioSettingsSubsystem.h @@ -0,0 +1,33 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "AudioSettingsSubsystem.generated.h" + +class UNakedDesireUserSettings; + +// Applies the master / music / SFX volume settings to SoundClasses via the configured +// SoundMix (UNakedDesireGameInstance::AudioConfig). Re-applies whenever settings change +// and on each world start so volumes survive level travel. +UCLASS() +class NAKEDDESIRE_API UAudioSettingsSubsystem : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + // Pushes the current settings volumes onto the configured SoundMix. Safe to call any time. + void ApplyVolumes(); + +private: + UFUNCTION() + void HandleSettingsChanged(UNakedDesireUserSettings* Settings); + + void HandleMapLoaded(UWorld* LoadedWorld); + + FDelegateHandle MapLoadHandle; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Global/MainMenuHUD.cpp b/Source/NakedDesire/Global/MainMenuHUD.cpp new file mode 100644 index 00000000..dfa6acad --- /dev/null +++ b/Source/NakedDesire/Global/MainMenuHUD.cpp @@ -0,0 +1,33 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "MainMenuHUD.h" + +#include "Blueprint/UserWidget.h" +#include "GameFramework/PlayerController.h" +#include "NakedDesire/UI/Menu/MainMenuWidget.h" + +void AMainMenuHUD::BeginPlay() +{ + Super::BeginPlay(); + + if (!MainMenuWidgetClass) + return; + + MainMenuWidget = CreateWidget(GetWorld(), MainMenuWidgetClass); + if (!MainMenuWidget) + return; + + MainMenuWidget->AddToViewport(); + MainMenuWidget->ActivateWidget(); + + if (APlayerController* PC = GetOwningPlayerController()) + { + PC->SetShowMouseCursor(true); + + FInputModeUIOnly InputMode; + InputMode.SetWidgetToFocus(MainMenuWidget->TakeWidget()); + InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); + PC->SetInputMode(InputMode); + } +} \ No newline at end of file diff --git a/Source/NakedDesire/Global/MainMenuHUD.h b/Source/NakedDesire/Global/MainMenuHUD.h new file mode 100644 index 00000000..b353e2a6 --- /dev/null +++ b/Source/NakedDesire/Global/MainMenuHUD.h @@ -0,0 +1,27 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/HUD.h" +#include "MainMenuHUD.generated.h" + +class UMainMenuWidget; + +// HUD for the main-menu map. Creates the front-end widget and routes input to it. +// Assign this (or a BP child) as the HUD class on the MainMenu map's GameMode. +UCLASS() +class NAKEDDESIRE_API AMainMenuHUD : public AHUD +{ + GENERATED_BODY() + +protected: + virtual void BeginPlay() override; + + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf MainMenuWidgetClass; + +private: + UPROPERTY() + TObjectPtr MainMenuWidget; +}; \ No newline at end of file diff --git a/Source/NakedDesire/Global/NakedDesireGameInstance.h b/Source/NakedDesire/Global/NakedDesireGameInstance.h index 55f00f33..d9c4f049 100644 --- a/Source/NakedDesire/Global/NakedDesireGameInstance.h +++ b/Source/NakedDesire/Global/NakedDesireGameInstance.h @@ -9,6 +9,7 @@ class UStartingSaveData; class UCommissionBoardConfig; class UNPCDirectorConfig; class ULossPresentationConfig; +class UAudioSettingsConfig; UCLASS() class NAKEDDESIRE_API UNakedDesireGameInstance : public UGameInstance @@ -30,4 +31,8 @@ public: // Cutscene-per-loss-cause map the USessionLossResolver plays before teleporting home (§4.4). UPROPERTY(EditDefaultsOnly, Category = "Session") TObjectPtr LossPresentation; + + // SoundMix / SoundClass routing the UAudioSettingsSubsystem drives from the audio settings sliders. + UPROPERTY(EditDefaultsOnly, Category = "Audio") + TObjectPtr AudioConfig; }; \ No newline at end of file diff --git a/Source/NakedDesire/Global/NakedDesireUserSettings.cpp b/Source/NakedDesire/Global/NakedDesireUserSettings.cpp index 8d4f9cb5..f781c021 100644 --- a/Source/NakedDesire/Global/NakedDesireUserSettings.cpp +++ b/Source/NakedDesire/Global/NakedDesireUserSettings.cpp @@ -5,7 +5,7 @@ void UNakedDesireUserSettings::SetGlobalVolume(float Value) { - GlobalVolume = Value; + GlobalVolume = FMath::Clamp(Value, 0.0f, 1.0f); } float UNakedDesireUserSettings::GetGlobalVolume() const @@ -13,6 +13,26 @@ float UNakedDesireUserSettings::GetGlobalVolume() const return GlobalVolume; } +void UNakedDesireUserSettings::SetMusicVolume(float Value) +{ + MusicVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +float UNakedDesireUserSettings::GetMusicVolume() const +{ + return MusicVolume; +} + +void UNakedDesireUserSettings::SetSfxVolume(float Value) +{ + SfxVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +float UNakedDesireUserSettings::GetSfxVolume() const +{ + return SfxVolume; +} + void UNakedDesireUserSettings::SetIsCensorshipEnabled(bool Value) { IsCensorshipEnabled = Value; diff --git a/Source/NakedDesire/Global/NakedDesireUserSettings.h b/Source/NakedDesire/Global/NakedDesireUserSettings.h index e759e7fe..4e95e8e7 100644 --- a/Source/NakedDesire/Global/NakedDesireUserSettings.h +++ b/Source/NakedDesire/Global/NakedDesireUserSettings.h @@ -23,6 +23,18 @@ public: UFUNCTION(BlueprintPure) float GetGlobalVolume() const; + UFUNCTION(BlueprintCallable) + void SetMusicVolume(float Value); + + UFUNCTION(BlueprintPure) + float GetMusicVolume() const; + + UFUNCTION(BlueprintCallable) + void SetSfxVolume(float Value); + + UFUNCTION(BlueprintPure) + float GetSfxVolume() const; + UFUNCTION(BlueprintCallable) void SetIsCensorshipEnabled(bool Value); @@ -46,6 +58,12 @@ protected: UPROPERTY(Config) float GlobalVolume = 0.5f; + UPROPERTY(Config) + float MusicVolume = 0.5f; + + UPROPERTY(Config) + float SfxVolume = 0.5f; + UPROPERTY(Config) bool IsCensorshipEnabled = false; diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.cpp b/Source/NakedDesire/Player/NakedDesireCharacter.cpp index 51ded976..b07a3906 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.cpp +++ b/Source/NakedDesire/Player/NakedDesireCharacter.cpp @@ -89,6 +89,7 @@ void ANakedDesireCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInp EnhancedInputComponent->BindAction(EquipmentAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnEquipmentPress); EnhancedInputComponent->BindAction(InteractAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnInteractPress); EnhancedInputComponent->BindAction(PhoneAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnPhonePress); + EnhancedInputComponent->BindAction(PauseAction, ETriggerEvent::Started, this, &ANakedDesireCharacter::OnPausePress); } } @@ -270,6 +271,11 @@ void ANakedDesireCharacter::OnPhonePress(const FInputActionValue& Value) HUD->GetGameLayoutWidget()->OpenPhone(); } +void ANakedDesireCharacter::OnPausePress(const FInputActionValue& Value) +{ + HUD->GetGameLayoutWidget()->OpenPauseMenu(); +} + void ANakedDesireCharacter::NotifyNoticed(ANPCAIController* NPCController) { OnNoticed.Broadcast(NPCController); diff --git a/Source/NakedDesire/Player/NakedDesireCharacter.h b/Source/NakedDesire/Player/NakedDesireCharacter.h index 55e90a69..ef0be8b9 100644 --- a/Source/NakedDesire/Player/NakedDesireCharacter.h +++ b/Source/NakedDesire/Player/NakedDesireCharacter.h @@ -61,6 +61,9 @@ public: UPROPERTY(EditDefaultsOnly, Category = "Input") UInputAction* PhoneAction; + UPROPERTY(EditDefaultsOnly, Category = "Input") + UInputAction* PauseAction; + // Clothing slot meshes are spawned per equipped slot at runtime by ClothingVisualsComponent. UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Clothing") UClothingVisualsComponent* ClothingVisualsComponent; @@ -145,6 +148,7 @@ private: void OnEquipmentPress(const FInputActionValue& Value); void OnInteractPress(const FInputActionValue& Value); void OnPhonePress(const FInputActionValue& Value); + void OnPausePress(const FInputActionValue& Value); bool CheckSight(const FVector& StartLocation, const FVector& EndLocation, FHitResult& HitResult, const AActor* IgnoreActor); diff --git a/Source/NakedDesire/SaveGame/SaveSubsystem.cpp b/Source/NakedDesire/SaveGame/SaveSubsystem.cpp index d3f28e4c..a164b6f3 100644 --- a/Source/NakedDesire/SaveGame/SaveSubsystem.cpp +++ b/Source/NakedDesire/SaveGame/SaveSubsystem.cpp @@ -9,6 +9,7 @@ #include "NakedDesire/Items/ItemDefinition.h" #include "NakedDesire/Items/ItemInstance.h" #include "NakedDesire/Global/NakedDesireGameInstance.h" +#include "Kismet/GameplayStatics.h" void USaveSubsystem::LoadGame(const FString& SlotName) { @@ -27,6 +28,20 @@ void USaveSubsystem::Initialize(FSubsystemCollectionBase& Collection) LoadGame(); } +bool USaveSubsystem::DoesSaveExist() const +{ + return UGameplayStatics::DoesSaveGameExist(ActiveSlotName, SLOT_PLAYER); +} + +void USaveSubsystem::StartNewGame() +{ + UGameplayStatics::DeleteGameInSlot(ActiveSlotName, SLOT_PLAYER); + + CurrentSave = UGlobalSaveGameData::CreateNewSaveGame(); + PopulateStartingData(CurrentSave); + UGlobalSaveGameData::SaveGame(CurrentSave, ActiveSlotName); +} + UGlobalSaveGameData* USaveSubsystem::GetCurrentSave() { if (!CurrentSave) diff --git a/Source/NakedDesire/SaveGame/SaveSubsystem.h b/Source/NakedDesire/SaveGame/SaveSubsystem.h index 1da26ff9..0bde1e6e 100644 --- a/Source/NakedDesire/SaveGame/SaveSubsystem.h +++ b/Source/NakedDesire/SaveGame/SaveSubsystem.h @@ -22,11 +22,22 @@ public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; UGlobalSaveGameData* GetCurrentSave(); - + + // True when a persisted save exists in the active slot — drives the main-menu + // Continue button's enabled state. + UFUNCTION(BlueprintPure, Category = "Save") + bool DoesSaveExist() const; + + // Wipes the active slot and reseeds a fresh save from StartingSaveData, replacing + // the cached CurrentSave so the next GetCurrentSave returns the new game. Call before + // opening the gameplay level from the main-menu "New Game" flow. + UFUNCTION(BlueprintCallable, Category = "Save") + void StartNewGame(); + protected: UFUNCTION(BlueprintCallable) void BP_LoadGame(); - + UFUNCTION(BlueprintCallable) void BP_SaveGame(); diff --git a/Source/NakedDesire/UI/GameLayoutWidget.cpp b/Source/NakedDesire/UI/GameLayoutWidget.cpp index d4fd66fa..9f10fc93 100644 --- a/Source/NakedDesire/UI/GameLayoutWidget.cpp +++ b/Source/NakedDesire/UI/GameLayoutWidget.cpp @@ -3,6 +3,7 @@ #include "Inventory/InventoryScreenWidget.h" #include "Inventory/Wardrobe/WardrobeScreenWidget.h" #include "Phone/PhoneScreenWidget.h" +#include "Menu/PauseMenuWidget.h" void UGameLayoutWidget::OpenInventory() { @@ -18,3 +19,8 @@ void UGameLayoutWidget::OpenPhone() { PhoneScreenWidget = WidgetStack->AddWidget(PhoneScreenWidgetClass); } + +void UGameLayoutWidget::OpenPauseMenu() +{ + PauseMenuWidget = WidgetStack->AddWidget(PauseMenuWidgetClass); +} diff --git a/Source/NakedDesire/UI/GameLayoutWidget.h b/Source/NakedDesire/UI/GameLayoutWidget.h index 48a30bfb..21d2878e 100644 --- a/Source/NakedDesire/UI/GameLayoutWidget.h +++ b/Source/NakedDesire/UI/GameLayoutWidget.h @@ -7,6 +7,7 @@ class UWardrobeScreenWidget; class UInventoryScreenWidget; class UPhoneScreenWidget; +class UPauseMenuWidget; class UHUDWidget; class UCommonActivatableWidgetStack; @@ -39,8 +40,15 @@ class NAKEDDESIRE_API UGameLayoutWidget : public UCommonUserWidget UPROPERTY() TObjectPtr PhoneScreenWidget; + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf PauseMenuWidgetClass; + + UPROPERTY() + TObjectPtr PauseMenuWidget; + public: void OpenInventory(); void OpenWardrobe(); void OpenPhone(); + void OpenPauseMenu(); }; diff --git a/Source/NakedDesire/UI/Menu/ConfirmModalWidget.cpp b/Source/NakedDesire/UI/Menu/ConfirmModalWidget.cpp new file mode 100644 index 00000000..57b9cb8a --- /dev/null +++ b/Source/NakedDesire/UI/Menu/ConfirmModalWidget.cpp @@ -0,0 +1,43 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "ConfirmModalWidget.h" + +#include "Components/Button.h" +#include "Components/TextBlock.h" + +void UConfirmModalWidget::Setup(const FText& InTitle, const FText& InMessage) +{ + PendingTitle = InTitle; + PendingMessage = InMessage; + + if (TitleText) + TitleText->SetText(PendingTitle); + if (MessageText) + MessageText->SetText(PendingMessage); +} + +void UConfirmModalWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + + if (TitleText) + TitleText->SetText(PendingTitle); + if (MessageText) + MessageText->SetText(PendingMessage); + + ConfirmButton->OnClicked.AddUniqueDynamic(this, &UConfirmModalWidget::HandleConfirmClicked); + CancelButton->OnClicked.AddUniqueDynamic(this, &UConfirmModalWidget::HandleCancelClicked); +} + +void UConfirmModalWidget::HandleConfirmClicked() +{ + OnConfirmed.Broadcast(); + DeactivateWidget(); +} + +void UConfirmModalWidget::HandleCancelClicked() +{ + OnCancelled.Broadcast(); + DeactivateWidget(); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/ConfirmModalWidget.h b/Source/NakedDesire/UI/Menu/ConfirmModalWidget.h new file mode 100644 index 00000000..e39757c8 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/ConfirmModalWidget.h @@ -0,0 +1,52 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "ConfirmModalWidget.generated.h" + +class UButton; +class UTextBlock; + +// Reusable yes/no confirmation popup. Pushed onto a stack by the menu that owns it; +// the opener calls Setup() for the copy and binds OnConfirmed / OnCancelled to react. +UCLASS(Abstract) +class NAKEDDESIRE_API UConfirmModalWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + // Sets the popup copy. Safe to call before the widget tree exists (cached and applied on activate). + void Setup(const FText& InTitle, const FText& InMessage); + + // C++-only result events — the opener binds whichever it cares about. + DECLARE_MULTICAST_DELEGATE(FOnConfirmModalResult); + FOnConfirmModalResult OnConfirmed; + FOnConfirmModalResult OnCancelled; + +protected: + virtual void NativeOnActivated() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr ConfirmButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CancelButton; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr TitleText; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr MessageText; + + FText PendingTitle; + FText PendingMessage; + + UFUNCTION() + void HandleConfirmClicked(); + + UFUNCTION() + void HandleCancelClicked(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/CreditsWidget.cpp b/Source/NakedDesire/UI/Menu/CreditsWidget.cpp new file mode 100644 index 00000000..a0e4c945 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/CreditsWidget.cpp @@ -0,0 +1,18 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "CreditsWidget.h" + +#include "Components/Button.h" + +void UCreditsWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + + BackButton->OnClicked.AddUniqueDynamic(this, &UCreditsWidget::HandleBackClicked); +} + +void UCreditsWidget::HandleBackClicked() +{ + DeactivateWidget(); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/CreditsWidget.h b/Source/NakedDesire/UI/Menu/CreditsWidget.h new file mode 100644 index 00000000..f48c65ab --- /dev/null +++ b/Source/NakedDesire/UI/Menu/CreditsWidget.h @@ -0,0 +1,26 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "CreditsWidget.generated.h" + +class UButton; + +// Credits screen. Pushed onto a stack by the menu; the Back button closes it. +UCLASS(Abstract) +class NAKEDDESIRE_API UCreditsWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +protected: + virtual void NativeOnActivated() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr BackButton; + + UFUNCTION() + void HandleBackClicked(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/MainMenuWidget.cpp b/Source/NakedDesire/UI/Menu/MainMenuWidget.cpp new file mode 100644 index 00000000..367e90d2 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/MainMenuWidget.cpp @@ -0,0 +1,126 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "MainMenuWidget.h" + +#include "ConfirmModalWidget.h" +#include "Components/Button.h" +#include "Kismet/GameplayStatics.h" +#include "Kismet/KismetSystemLibrary.h" +#include "NakedDesire/SaveGame/SaveSubsystem.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#define LOCTEXT_NAMESPACE "MainMenu" + +UMainMenuWidget::UMainMenuWidget() +{ + // Sensible default for the slice; override per-asset on WBP_MainMenu. + GameplayMap = TSoftObjectPtr(FSoftObjectPath(TEXT("/Game/Test/Maps/L_TestCity.L_TestCity"))); +} + +void UMainMenuWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + + // Continue is only available when a save exists in the active slot. + bool bHasSave = false; + if (const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this)) + { + if (const USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem()) + bHasSave = SaveSubsystem->DoesSaveExist(); + } + ContinueButton->SetIsEnabled(bHasSave); + + ContinueButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnContinueClicked); + NewGameButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnNewGameClicked); + SettingsButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnSettingsClicked); + CreditsButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnCreditsClicked); + QuitButton->OnClicked.AddUniqueDynamic(this, &UMainMenuWidget::OnQuitClicked); +} + +void UMainMenuWidget::OnContinueClicked() +{ + OpenGameplayLevel(); +} + +void UMainMenuWidget::OnNewGameClicked() +{ + if (!ConfirmModalClass || !ModalStack) + { + HandleNewGameConfirmed(); + return; + } + + UConfirmModalWidget* Modal = ModalStack->AddWidget(ConfirmModalClass); + if (!Modal) + return; + + Modal->Setup( + LOCTEXT("NewGameTitle", "New Game"), + LOCTEXT("NewGameBody", "Start a new game? Any existing progress will be lost.")); + Modal->OnConfirmed.AddUObject(this, &UMainMenuWidget::HandleNewGameConfirmed); +} + +void UMainMenuWidget::OnSettingsClicked() +{ + if (SettingsWidgetClass && ModalStack) + ModalStack->AddWidget(SettingsWidgetClass); +} + +void UMainMenuWidget::OnCreditsClicked() +{ + if (CreditsWidgetClass && ModalStack) + ModalStack->AddWidget(CreditsWidgetClass); +} + +void UMainMenuWidget::OnQuitClicked() +{ + if (!ConfirmModalClass || !ModalStack) + { + HandleQuitConfirmed(); + return; + } + + UConfirmModalWidget* Modal = ModalStack->AddWidget(ConfirmModalClass); + if (!Modal) + return; + + Modal->Setup( + LOCTEXT("QuitTitle", "Quit Game"), + LOCTEXT("QuitBody", "Quit to desktop?")); + Modal->OnConfirmed.AddUObject(this, &UMainMenuWidget::HandleQuitConfirmed); +} + +void UMainMenuWidget::HandleNewGameConfirmed() +{ + if (UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this)) + { + if (USaveSubsystem* SaveSubsystem = GameInstance->GetSubsystem()) + SaveSubsystem->StartNewGame(); + } + + OpenGameplayLevel(); +} + +void UMainMenuWidget::HandleQuitConfirmed() +{ + UKismetSystemLibrary::QuitGame(this, GetOwningPlayer(), EQuitPreference::Quit, false); +} + +void UMainMenuWidget::OpenGameplayLevel() +{ + if (GameplayMap.IsNull()) + return; + + if (APlayerController* PC = UGameplayStatics::GetPlayerController(GetWorld(), 0)) + { + PC->SetShowMouseCursor(false); + + const FInputModeGameOnly InputMode; + PC->SetInputMode(InputMode); + } + + UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameplayMap); +} + +#undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/MainMenuWidget.h b/Source/NakedDesire/UI/Menu/MainMenuWidget.h new file mode 100644 index 00000000..4ed0fb5d --- /dev/null +++ b/Source/NakedDesire/UI/Menu/MainMenuWidget.h @@ -0,0 +1,80 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "MainMenuWidget.generated.h" + +class UButton; +class UCommonActivatableWidgetStack; +class UConfirmModalWidget; + +// Front-end main menu. Owns the Continue / New Game / Settings / Credits / Quit flow. +// Confirmation popups, credits and settings are pushed onto ModalStack (a stack in the +// widget tree). The gameplay / menu maps are data-driven via the soft map references. +UCLASS(Abstract) +class NAKEDDESIRE_API UMainMenuWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + UMainMenuWidget(); + +protected: + virtual void NativeOnActivated() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr ContinueButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr NewGameButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr SettingsButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CreditsButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr QuitButton; + + // Stack the confirm popup / credits / settings screens are pushed onto. + UPROPERTY(meta = (BindWidget)) + TObjectPtr ModalStack; + + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf ConfirmModalClass; + + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf CreditsWidgetClass; + + // Optional — wired once the settings screen exists. Settings button no-ops until then. + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf SettingsWidgetClass; + + // Level "New Game" / "Continue" load into (the §0.2 slice district). + UPROPERTY(EditDefaultsOnly, Category = "UI", meta = (AllowedClasses = "/Script/Engine.World")) + TSoftObjectPtr GameplayMap; + + UFUNCTION() + void OnContinueClicked(); + + UFUNCTION() + void OnNewGameClicked(); + + UFUNCTION() + void OnSettingsClicked(); + + UFUNCTION() + void OnCreditsClicked(); + + UFUNCTION() + void OnQuitClicked(); + + void HandleNewGameConfirmed(); + void HandleQuitConfirmed(); + + void OpenGameplayLevel(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/PauseMenuWidget.cpp b/Source/NakedDesire/UI/Menu/PauseMenuWidget.cpp new file mode 100644 index 00000000..b5d6fbf1 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/PauseMenuWidget.cpp @@ -0,0 +1,84 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "PauseMenuWidget.h" + +#include "ConfirmModalWidget.h" +#include "Components/Button.h" +#include "Kismet/GameplayStatics.h" +#include "Widgets/CommonActivatableWidgetContainer.h" + +#define LOCTEXT_NAMESPACE "PauseMenu" + +UPauseMenuWidget::UPauseMenuWidget() +{ + // Default to the front-end map; override per-asset on WBP_PauseMenu. + MainMenuMap = TSoftObjectPtr(FSoftObjectPath(TEXT("/Game/Maps/MainMenu.MainMenu"))); +} + +void UPauseMenuWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + + GetWorld()->GetTimerManager().SetTimer(PauseTimerHandle, this, &UPauseMenuWidget::PauseGame, 0.1f); + + ResumeButton->OnClicked.AddUniqueDynamic(this, &UPauseMenuWidget::OnResumeClicked); + SettingsButton->OnClicked.AddUniqueDynamic(this, &UPauseMenuWidget::OnSettingsClicked); + MainMenuButton->OnClicked.AddUniqueDynamic(this, &UPauseMenuWidget::OnMainMenuClicked); +} + +void UPauseMenuWidget::NativeOnDeactivated() +{ + Super::NativeOnDeactivated(); + + // Unpause whenever the menu closes — Resume, back action, or being popped. + if (UGameplayStatics::IsGamePaused(this)) + UGameplayStatics::SetGamePaused(this, false); +} + +void UPauseMenuWidget::OnResumeClicked() +{ + DeactivateWidget(); +} + +void UPauseMenuWidget::OnSettingsClicked() +{ + if (SettingsWidgetClass && ModalStack) + ModalStack->AddWidget(SettingsWidgetClass); +} + +void UPauseMenuWidget::OnMainMenuClicked() +{ + if (!ConfirmModalClass || !ModalStack) + { + HandleMainMenuConfirmed(); + return; + } + + UConfirmModalWidget* Modal = ModalStack->AddWidget(ConfirmModalClass); + if (!Modal) + return; + + Modal->Setup( + LOCTEXT("MainMenuTitle", "Main Menu"), + LOCTEXT("MainMenuBody", "Return to the main menu? Unsaved progress may be lost.")); + Modal->OnConfirmed.AddUObject(this, &UPauseMenuWidget::HandleMainMenuConfirmed); +} + +void UPauseMenuWidget::HandleMainMenuConfirmed() +{ + // Unpause before travel — level transitions while paused are unreliable. + UGameplayStatics::SetGamePaused(this, false); + + if (MainMenuMap.IsNull()) + return; + + UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuMap); +} + +void UPauseMenuWidget::PauseGame() +{ + UGameplayStatics::SetGamePaused(this, true); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/NakedDesire/UI/Menu/PauseMenuWidget.h b/Source/NakedDesire/UI/Menu/PauseMenuWidget.h new file mode 100644 index 00000000..2711a27c --- /dev/null +++ b/Source/NakedDesire/UI/Menu/PauseMenuWidget.h @@ -0,0 +1,64 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "PauseMenuWidget.generated.h" + +class UButton; +class UCommonActivatableWidgetStack; +class UConfirmModalWidget; + +// In-game pause menu. Pauses the game while active and offers Resume / Settings / Main Menu. +// The Main Menu action confirms first. Confirm popup / settings push onto ModalStack. +UCLASS(Abstract) +class NAKEDDESIRE_API UPauseMenuWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +public: + UPauseMenuWidget(); + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr ResumeButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr SettingsButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr MainMenuButton; + + // Stack the confirm popup / settings screen are pushed onto (nested so this menu stays active). + UPROPERTY(meta = (BindWidget)) + TObjectPtr ModalStack; + + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf ConfirmModalClass; + + // Optional — wired once the settings screen exists. Settings button no-ops until then. + UPROPERTY(EditDefaultsOnly, Category = "UI") + TSubclassOf SettingsWidgetClass; + + UPROPERTY(EditDefaultsOnly, Category = "UI", meta = (AllowedClasses = "/Script/Engine.World")) + TSoftObjectPtr MainMenuMap; + + FTimerHandle PauseTimerHandle; + + UFUNCTION() + void OnResumeClicked(); + + UFUNCTION() + void OnSettingsClicked(); + + UFUNCTION() + void OnMainMenuClicked(); + + void HandleMainMenuConfirmed(); + void PauseGame(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/AudioSettingsTab.cpp b/Source/NakedDesire/UI/Menu/Settings/AudioSettingsTab.cpp new file mode 100644 index 00000000..f0e86387 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/AudioSettingsTab.cpp @@ -0,0 +1,61 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "AudioSettingsTab.h" + +#include "Components/Slider.h" +#include "Kismet/GameplayStatics.h" +#include "NakedDesire/Global/AudioSettingsSubsystem.h" +#include "NakedDesire/Global/NakedDesireUserSettings.h" + +void UAudioSettingsTab::NativeConstruct() +{ + Super::NativeConstruct(); + + MasterSlider->OnValueChanged.AddUniqueDynamic(this, &UAudioSettingsTab::OnMasterChanged); + MusicSlider->OnValueChanged.AddUniqueDynamic(this, &UAudioSettingsTab::OnMusicChanged); + SfxSlider->OnValueChanged.AddUniqueDynamic(this, &UAudioSettingsTab::OnSfxChanged); + + RefreshFromSettings(); +} + +void UAudioSettingsTab::RefreshFromSettings() +{ + const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + MasterSlider->SetValue(Settings->GetGlobalVolume()); + MusicSlider->SetValue(Settings->GetMusicVolume()); + SfxSlider->SetValue(Settings->GetSfxVolume()); +} + +void UAudioSettingsTab::OnMasterChanged(float Value) +{ + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->SetGlobalVolume(Value); + ApplyLive(); +} + +void UAudioSettingsTab::OnMusicChanged(float Value) +{ + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->SetMusicVolume(Value); + ApplyLive(); +} + +void UAudioSettingsTab::OnSfxChanged(float Value) +{ + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->SetSfxVolume(Value); + ApplyLive(); +} + +void UAudioSettingsTab::ApplyLive() +{ + if (const UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(this)) + { + if (UAudioSettingsSubsystem* Audio = GameInstance->GetSubsystem()) + Audio->ApplyVolumes(); + } +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/AudioSettingsTab.h b/Source/NakedDesire/UI/Menu/Settings/AudioSettingsTab.h new file mode 100644 index 00000000..60677bdc --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/AudioSettingsTab.h @@ -0,0 +1,44 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "AudioSettingsTab.generated.h" + +class USlider; + +// Audio settings tab. Master / Music / SFX volume sliders. Changes apply live through +// UAudioSettingsSubsystem (routed via the configured SoundMix) and persist on close. +UCLASS(Abstract) +class NAKEDDESIRE_API UAudioSettingsTab : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + void RefreshFromSettings(); + +protected: + virtual void NativeConstruct() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr MasterSlider; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr MusicSlider; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr SfxSlider; + + UFUNCTION() + void OnMasterChanged(float Value); + + UFUNCTION() + void OnMusicChanged(float Value); + + UFUNCTION() + void OnSfxChanged(float Value); + + void ApplyLive(); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/GameplaySettingsTab.cpp b/Source/NakedDesire/UI/Menu/Settings/GameplaySettingsTab.cpp new file mode 100644 index 00000000..f8125de0 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/GameplaySettingsTab.cpp @@ -0,0 +1,82 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "GameplaySettingsTab.h" + +#include "Components/CheckBox.h" +#include "Components/ComboBoxString.h" +#include "Kismet/KismetInternationalizationLibrary.h" +#include "NakedDesire/Global/NakedDesireUserSettings.h" + +namespace +{ + // Index-aligned: combo display name → UE culture code. Add a row here when a new + // translation ships, and make sure the culture is in DefaultGame.ini CulturesToStage. + struct FLanguageOption + { + const TCHAR* DisplayName; + const TCHAR* CultureCode; + }; + + constexpr FLanguageOption LanguageOptions[] = { + { TEXT("English"), TEXT("en") }, + { TEXT("Українська"), TEXT("uk") }, + { TEXT("日本語"), TEXT("ja") } + }; +} + +void UGameplaySettingsTab::NativeConstruct() +{ + Super::NativeConstruct(); + + CensorshipCheckBox->OnCheckStateChanged.AddUniqueDynamic(this, &UGameplaySettingsTab::OnCensorshipChanged); + LanguageCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGameplaySettingsTab::OnLanguageChanged); + + RefreshFromSettings(); +} + +void UGameplaySettingsTab::RefreshFromSettings() +{ + const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + CensorshipCheckBox->SetIsChecked(Settings->GetIsCensorshipEnabled()); + + // Language: match the active culture (e.g. "uk-UA") against our supported codes by prefix. + LanguageCombo->ClearOptions(); + const FString CurrentCulture = UKismetInternationalizationLibrary::GetCurrentLanguage(); + int32 SelectedIndex = 0; + for (int32 i = 0; i < static_cast(UE_ARRAY_COUNT(LanguageOptions)); ++i) + { + LanguageCombo->AddOption(LanguageOptions[i].DisplayName); + if (CurrentCulture.StartsWith(LanguageOptions[i].CultureCode)) + SelectedIndex = i; + } + LanguageCombo->SetSelectedIndex(SelectedIndex); +} + +void UGameplaySettingsTab::OnCensorshipChanged(bool bIsChecked) +{ + UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + Settings->SetIsCensorshipEnabled(bIsChecked); + // Broadcasts OnSettingsChanged so the censorship meshes update immediately. + Settings->ApplyNonResolutionSettings(); +} + +void UGameplaySettingsTab::OnLanguageChanged(FString SelectedItem, ESelectInfo::Type SelectionType) +{ + // Skip the programmatic SetSelectedIndex during RefreshFromSettings. + if (SelectionType == ESelectInfo::Direct) + return; + + const int32 Index = LanguageCombo->GetSelectedIndex(); + if (Index < 0 || Index >= static_cast(UE_ARRAY_COUNT(LanguageOptions))) + return; + + // Applies immediately (all LOCTEXT re-evaluates) and persists to the game config. + UKismetInternationalizationLibrary::SetCurrentLanguageAndLocale(LanguageOptions[Index].CultureCode, /*SaveToConfig*/ true); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/GameplaySettingsTab.h b/Source/NakedDesire/UI/Menu/Settings/GameplaySettingsTab.h new file mode 100644 index 00000000..05c0d301 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/GameplaySettingsTab.h @@ -0,0 +1,39 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "GameplaySettingsTab.generated.h" + +class UCheckBox; +class UComboBoxString; + +// Gameplay settings tab. Censorship toggle (§ compliance) — applies live through +// UNakedDesireUserSettings::OnSettingsChanged, which UCensorshipComponent listens to — +// plus the UI/text language selector (English / Ukrainian / Japanese). +UCLASS(Abstract) +class NAKEDDESIRE_API UGameplaySettingsTab : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + // Re-reads the current values into the controls. Called by the settings screen on show. + void RefreshFromSettings(); + +protected: + virtual void NativeConstruct() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr CensorshipCheckBox; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr LanguageCombo; + + UFUNCTION() + void OnCensorshipChanged(bool bIsChecked); + + UFUNCTION() + void OnLanguageChanged(FString SelectedItem, ESelectInfo::Type SelectionType); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/GraphicsSettingsTab.cpp b/Source/NakedDesire/UI/Menu/Settings/GraphicsSettingsTab.cpp new file mode 100644 index 00000000..be9087f4 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/GraphicsSettingsTab.cpp @@ -0,0 +1,137 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "GraphicsSettingsTab.h" + +#include "Components/CheckBox.h" +#include "Components/ComboBoxString.h" +#include "Kismet/KismetSystemLibrary.h" +#include "NakedDesire/Global/NakedDesireUserSettings.h" + +namespace +{ + // Combo order → EWindowMode. Index-aligned with the WindowModeCombo entries below. + constexpr EWindowMode::Type WindowModes[] = { + EWindowMode::Fullscreen, + EWindowMode::WindowedFullscreen, + EWindowMode::Windowed + }; + + FString ResolutionToText(const FIntPoint& Resolution) + { + return FString::Printf(TEXT("%d x %d"), Resolution.X, Resolution.Y); + } +} + +void UGraphicsSettingsTab::NativeConstruct() +{ + Super::NativeConstruct(); + + QualityCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnQualityChanged); + WindowModeCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnWindowModeChanged); + ResolutionCombo->OnSelectionChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnResolutionChanged); + VSyncCheckBox->OnCheckStateChanged.AddUniqueDynamic(this, &UGraphicsSettingsTab::OnVSyncChanged); + + RefreshFromSettings(); +} + +void UGraphicsSettingsTab::RefreshFromSettings() +{ + const UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + // Quality presets (Scalability levels 0-3). + QualityCombo->ClearOptions(); + QualityCombo->AddOption(TEXT("Low")); + QualityCombo->AddOption(TEXT("Medium")); + QualityCombo->AddOption(TEXT("High")); + QualityCombo->AddOption(TEXT("Epic")); + const int32 Quality = FMath::Clamp(Settings->GetOverallScalabilityLevel(), 0, 3); + QualityCombo->SetSelectedIndex(Quality); + + // Window mode. + WindowModeCombo->ClearOptions(); + WindowModeCombo->AddOption(TEXT("Fullscreen")); + WindowModeCombo->AddOption(TEXT("Borderless Windowed")); + WindowModeCombo->AddOption(TEXT("Windowed")); + const EWindowMode::Type CurrentMode = Settings->GetFullscreenMode(); + int32 ModeIndex = 0; + for (int32 i = 0; i < UE_ARRAY_COUNT(WindowModes); ++i) + { + if (WindowModes[i] == CurrentMode) + { + ModeIndex = i; + break; + } + } + WindowModeCombo->SetSelectedIndex(ModeIndex); + + // Resolutions. + ResolutionCombo->ClearOptions(); + AvailableResolutions.Reset(); + UKismetSystemLibrary::GetSupportedFullscreenResolutions(AvailableResolutions); + const FIntPoint CurrentResolution = Settings->GetScreenResolution(); + int32 ResolutionIndex = INDEX_NONE; + for (int32 i = 0; i < AvailableResolutions.Num(); ++i) + { + ResolutionCombo->AddOption(ResolutionToText(AvailableResolutions[i])); + if (AvailableResolutions[i] == CurrentResolution) + ResolutionIndex = i; + } + // Current resolution may not be in the supported list (e.g. windowed) — surface it anyway. + if (ResolutionIndex == INDEX_NONE) + { + ResolutionCombo->AddOption(ResolutionToText(CurrentResolution)); + AvailableResolutions.Add(CurrentResolution); + ResolutionIndex = AvailableResolutions.Num() - 1; + } + ResolutionCombo->SetSelectedIndex(ResolutionIndex); + + VSyncCheckBox->SetIsChecked(Settings->IsVSyncEnabled()); +} + +void UGraphicsSettingsTab::OnQualityChanged(FString SelectedItem, ESelectInfo::Type SelectionType) +{ + if (SelectionType == ESelectInfo::Direct) + return; + + UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + Settings->SetOverallScalabilityLevel(QualityCombo->GetSelectedIndex()); +} + +void UGraphicsSettingsTab::OnWindowModeChanged(FString SelectedItem, ESelectInfo::Type SelectionType) +{ + if (SelectionType == ESelectInfo::Direct) + return; + + UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + const int32 Index = FMath::Clamp(WindowModeCombo->GetSelectedIndex(), 0, static_cast(UE_ARRAY_COUNT(WindowModes)) - 1); + Settings->SetFullscreenMode(WindowModes[Index]); +} + +void UGraphicsSettingsTab::OnResolutionChanged(FString SelectedItem, ESelectInfo::Type SelectionType) +{ + if (SelectionType == ESelectInfo::Direct) + return; + + UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings(); + if (!Settings) + return; + + const int32 Index = ResolutionCombo->GetSelectedIndex(); + if (AvailableResolutions.IsValidIndex(Index)) + Settings->SetScreenResolution(AvailableResolutions[Index]); +} + +void UGraphicsSettingsTab::OnVSyncChanged(bool bIsChecked) +{ + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->SetVSyncEnabled(bIsChecked); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/GraphicsSettingsTab.h b/Source/NakedDesire/UI/Menu/Settings/GraphicsSettingsTab.h new file mode 100644 index 00000000..2a63d6c4 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/GraphicsSettingsTab.h @@ -0,0 +1,53 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "GraphicsSettingsTab.generated.h" + +class UComboBoxString; +class UCheckBox; + +// Graphics settings tab. Overall quality, window mode, resolution and VSync, backed by the +// inherited UGameUserSettings. Changes stage into the settings object; the settings screen's +// Apply button calls ApplySettings to commit resolution/quality changes. +UCLASS(Abstract) +class NAKEDDESIRE_API UGraphicsSettingsTab : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + void RefreshFromSettings(); + +protected: + virtual void NativeConstruct() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr QualityCombo; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr WindowModeCombo; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr ResolutionCombo; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr VSyncCheckBox; + + // Index-aligned with ResolutionCombo entries. + TArray AvailableResolutions; + + UFUNCTION() + void OnQualityChanged(FString SelectedItem, ESelectInfo::Type SelectionType); + + UFUNCTION() + void OnWindowModeChanged(FString SelectedItem, ESelectInfo::Type SelectionType); + + UFUNCTION() + void OnResolutionChanged(FString SelectedItem, ESelectInfo::Type SelectionType); + + UFUNCTION() + void OnVSyncChanged(bool bIsChecked); +}; \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/SettingsScreenWidget.cpp b/Source/NakedDesire/UI/Menu/Settings/SettingsScreenWidget.cpp new file mode 100644 index 00000000..88c3ede8 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/SettingsScreenWidget.cpp @@ -0,0 +1,72 @@ +// © 2025 Naked People Team. All Rights Reserved. + + +#include "SettingsScreenWidget.h" + +#include "AudioSettingsTab.h" +#include "GameplaySettingsTab.h" +#include "GraphicsSettingsTab.h" +#include "Components/Button.h" +#include "Components/WidgetSwitcher.h" +#include "NakedDesire/Global/NakedDesireUserSettings.h" + +void USettingsScreenWidget::NativeOnActivated() +{ + Super::NativeOnActivated(); + + GameplayTabButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::ShowGameplayTab); + AudioTabButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::ShowAudioTab); + GraphicsTabButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::ShowGraphicsTab); + ApplyButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::OnApplyClicked); + BackButton->OnClicked.AddUniqueDynamic(this, &USettingsScreenWidget::OnBackClicked); + + // Pull live values into every tab so the controls reflect the current state. + GameplayTab->RefreshFromSettings(); + AudioTab->RefreshFromSettings(); + GraphicsTab->RefreshFromSettings(); + + ShowGameplayTab(); +} + +void USettingsScreenWidget::NativeOnDeactivated() +{ + Super::NativeOnDeactivated(); + + // Persist whatever the player changed, even if they didn't hit Apply. + PersistSettings(); +} + +void USettingsScreenWidget::ShowGameplayTab() +{ + TabSwitcher->SetActiveWidget(GameplayTab); +} + +void USettingsScreenWidget::ShowAudioTab() +{ + TabSwitcher->SetActiveWidget(AudioTab); +} + +void USettingsScreenWidget::ShowGraphicsTab() +{ + TabSwitcher->SetActiveWidget(GraphicsTab); +} + +void USettingsScreenWidget::OnApplyClicked() +{ + // Commits resolution + scalability changes staged by the graphics tab. + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->ApplySettings(false); + + PersistSettings(); +} + +void USettingsScreenWidget::OnBackClicked() +{ + DeactivateWidget(); +} + +void USettingsScreenWidget::PersistSettings() +{ + if (UNakedDesireUserSettings* Settings = UNakedDesireUserSettings::GetNakedDesireUserSettings()) + Settings->SaveSettings(); +} \ No newline at end of file diff --git a/Source/NakedDesire/UI/Menu/Settings/SettingsScreenWidget.h b/Source/NakedDesire/UI/Menu/Settings/SettingsScreenWidget.h new file mode 100644 index 00000000..67ec7409 --- /dev/null +++ b/Source/NakedDesire/UI/Menu/Settings/SettingsScreenWidget.h @@ -0,0 +1,71 @@ +// © 2025 Naked People Team. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "CommonActivatableWidget.h" +#include "SettingsScreenWidget.generated.h" + +class UButton; +class UWidgetSwitcher; +class UGameplaySettingsTab; +class UAudioSettingsTab; +class UGraphicsSettingsTab; + +// Settings screen with Gameplay / Audio / Graphics tabs. Pushed onto a menu's ModalStack +// (assign as SettingsWidgetClass on the main / pause menus). Apply commits + persists; +// closing the screen also persists pending changes. +UCLASS(Abstract) +class NAKEDDESIRE_API USettingsScreenWidget : public UCommonActivatableWidget +{ + GENERATED_BODY() + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + +private: + UPROPERTY(meta = (BindWidget)) + TObjectPtr TabSwitcher; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr GameplayTabButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr AudioTabButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr GraphicsTabButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr ApplyButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr BackButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr GameplayTab; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr AudioTab; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr GraphicsTab; + + UFUNCTION() + void ShowGameplayTab(); + + UFUNCTION() + void ShowAudioTab(); + + UFUNCTION() + void ShowGraphicsTab(); + + UFUNCTION() + void OnApplyClicked(); + + UFUNCTION() + void OnBackClicked(); + + void PersistSettings(); +}; \ No newline at end of file