added main and pause menus

This commit is contained in:
2026-06-05 20:00:33 +03:00
parent 0792f7cdfd
commit 61d5a57d8d
53 changed files with 1402 additions and 49 deletions
+1 -26
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -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<ESessionLossCause, TSoftObjectPtr<ULevelSequence>>`), 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<UWorld>`), `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)
@@ -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<USoundMix> SettingsMix;
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundClass> MasterClass;
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundClass> MusicClass;
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<USoundClass> SfxClass;
};
@@ -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<UNakedDesireGameInstance>(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);
}
@@ -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;
};
+33
View File
@@ -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<UMainMenuWidget>(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);
}
}
+27
View File
@@ -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<UMainMenuWidget> MainMenuWidgetClass;
private:
UPROPERTY()
TObjectPtr<UMainMenuWidget> MainMenuWidget;
};
@@ -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<ULossPresentationConfig> LossPresentation;
// SoundMix / SoundClass routing the UAudioSettingsSubsystem drives from the audio settings sliders.
UPROPERTY(EditDefaultsOnly, Category = "Audio")
TObjectPtr<UAudioSettingsConfig> AudioConfig;
};
@@ -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;
@@ -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;
@@ -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);
@@ -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);
@@ -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)
+13 -2
View File
@@ -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();
@@ -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<UPhoneScreenWidget>(PhoneScreenWidgetClass);
}
void UGameLayoutWidget::OpenPauseMenu()
{
PauseMenuWidget = WidgetStack->AddWidget<UPauseMenuWidget>(PauseMenuWidgetClass);
}
+8
View File
@@ -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<UPhoneScreenWidget> PhoneScreenWidget;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UPauseMenuWidget> PauseMenuWidgetClass;
UPROPERTY()
TObjectPtr<UPauseMenuWidget> PauseMenuWidget;
public:
void OpenInventory();
void OpenWardrobe();
void OpenPhone();
void OpenPauseMenu();
};
@@ -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();
}
@@ -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<UButton> ConfirmButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CancelButton;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> TitleText;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> MessageText;
FText PendingTitle;
FText PendingMessage;
UFUNCTION()
void HandleConfirmClicked();
UFUNCTION()
void HandleCancelClicked();
};
@@ -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();
}
@@ -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<UButton> BackButton;
UFUNCTION()
void HandleBackClicked();
};
@@ -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<UWorld>(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<USaveSubsystem>())
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<UConfirmModalWidget>(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<UConfirmModalWidget>(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<USaveSubsystem>())
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
@@ -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<UButton> ContinueButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> NewGameButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> SettingsButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CreditsButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> QuitButton;
// Stack the confirm popup / credits / settings screens are pushed onto.
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCommonActivatableWidgetStack> ModalStack;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UConfirmModalWidget> ConfirmModalClass;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UCommonActivatableWidget> CreditsWidgetClass;
// Optional — wired once the settings screen exists. Settings button no-ops until then.
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UCommonActivatableWidget> SettingsWidgetClass;
// Level "New Game" / "Continue" load into (the §0.2 slice district).
UPROPERTY(EditDefaultsOnly, Category = "UI", meta = (AllowedClasses = "/Script/Engine.World"))
TSoftObjectPtr<UWorld> GameplayMap;
UFUNCTION()
void OnContinueClicked();
UFUNCTION()
void OnNewGameClicked();
UFUNCTION()
void OnSettingsClicked();
UFUNCTION()
void OnCreditsClicked();
UFUNCTION()
void OnQuitClicked();
void HandleNewGameConfirmed();
void HandleQuitConfirmed();
void OpenGameplayLevel();
};
@@ -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<UWorld>(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<UConfirmModalWidget>(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
@@ -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<UButton> ResumeButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> SettingsButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> MainMenuButton;
// Stack the confirm popup / settings screen are pushed onto (nested so this menu stays active).
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCommonActivatableWidgetStack> ModalStack;
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UConfirmModalWidget> ConfirmModalClass;
// Optional — wired once the settings screen exists. Settings button no-ops until then.
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UCommonActivatableWidget> SettingsWidgetClass;
UPROPERTY(EditDefaultsOnly, Category = "UI", meta = (AllowedClasses = "/Script/Engine.World"))
TSoftObjectPtr<UWorld> MainMenuMap;
FTimerHandle PauseTimerHandle;
UFUNCTION()
void OnResumeClicked();
UFUNCTION()
void OnSettingsClicked();
UFUNCTION()
void OnMainMenuClicked();
void HandleMainMenuConfirmed();
void PauseGame();
};
@@ -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<UAudioSettingsSubsystem>())
Audio->ApplyVolumes();
}
}
@@ -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<USlider> MasterSlider;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USlider> MusicSlider;
UPROPERTY(meta = (BindWidget))
TObjectPtr<USlider> SfxSlider;
UFUNCTION()
void OnMasterChanged(float Value);
UFUNCTION()
void OnMusicChanged(float Value);
UFUNCTION()
void OnSfxChanged(float Value);
void ApplyLive();
};
@@ -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<int32>(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<int32>(UE_ARRAY_COUNT(LanguageOptions)))
return;
// Applies immediately (all LOCTEXT re-evaluates) and persists to the game config.
UKismetInternationalizationLibrary::SetCurrentLanguageAndLocale(LanguageOptions[Index].CultureCode, /*SaveToConfig*/ true);
}
@@ -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<UCheckBox> CensorshipCheckBox;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> LanguageCombo;
UFUNCTION()
void OnCensorshipChanged(bool bIsChecked);
UFUNCTION()
void OnLanguageChanged(FString SelectedItem, ESelectInfo::Type SelectionType);
};
@@ -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<int32>(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);
}
@@ -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<UComboBoxString> QualityCombo;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> WindowModeCombo;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UComboBoxString> ResolutionCombo;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UCheckBox> VSyncCheckBox;
// Index-aligned with ResolutionCombo entries.
TArray<FIntPoint> 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);
};
@@ -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();
}
@@ -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<UWidgetSwitcher> TabSwitcher;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> GameplayTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> AudioTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> GraphicsTabButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> ApplyButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> BackButton;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UGameplaySettingsTab> GameplayTab;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UAudioSettingsTab> AudioTab;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UGraphicsSettingsTab> GraphicsTab;
UFUNCTION()
void ShowGameplayTab();
UFUNCTION()
void ShowAudioTab();
UFUNCTION()
void ShowGraphicsTab();
UFUNCTION()
void OnApplyClicked();
UFUNCTION()
void OnBackClicked();
void PersistSettings();
};