diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 3976075..3eedf0a 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -849,7 +849,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add wildlife sounds. Added spatialized wildlife audio hooks with assignable idle, flee/chase, death, and harvest sound slots plus server-triggered multicast playback from authoritative wildlife state changes and harvest events. - [x] Add UI sounds. Added optional 2D confirm, back, selection, and save/quit sound hooks to the MVP frontend widget, with keyboard and mouse actions sharing the same feedback path while remaining silent until UI audio assets are assigned. - [x] Add mix settings. Added MVP audio mix settings for master, ambient, weather, foley, fire, wildlife, and UI buses with conservative investor-build defaults and documentation for future SoundClass/MetaSound replacement. -- [ ] Add volume sliders. +- [x] Add volume sliders. Added MVP frontend volume sliders for master, ambient, weather, effects, wildlife, and UI levels, with runtime value storage and immediate UI-volume application to frontend feedback sounds while leaving final SoundClass binding for authored audio assets. ## 0.1.Q MVP QA Gates diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 39be7a3..1c1a4b4 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -328,6 +328,12 @@ exposes confirm, back, selection, and save/quit sound slots. Keyboard and mouse actions play the same cues so menu feedback stays consistent across input methods while the widget remains silent until UI assets are assigned. +The MVP pause/main menu exposes volume sliders for master, ambient, weather, +effects, wildlife, and UI levels. The current native sliders store runtime +values and apply the UI bus to frontend sounds immediately; final SoundClass or +audio-subsystem work can bind the remaining buses to authored assets once those +assets exist. + Player movement audio starts with native footstep placeholders on `AAgrarianGameCharacter`. The character owns a spatialized `FootstepAudioComponent` plus assignable walk, sprint, crouch, and prone sound diff --git a/Scripts/verify_volume_sliders.py b/Scripts/verify_volume_sliders.py new file mode 100644 index 0000000..823e7f4 --- /dev/null +++ b/Scripts/verify_volume_sliders.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Verify MVP frontend volume sliders are present.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +UI_H = ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.h" +UI_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianMvpFrontendWidget.cpp" +TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + +REQUIRED = { + UI_H: [ + "class USlider;", + "float MasterVolume", + "float AmbientVolume", + "float WeatherVolume", + "float EffectsVolume", + "float WildlifeVolume", + "float UiVolume", + "USlider* AddVolumeSlider", + "void HandleMasterVolumeChanged(float Value);", + "void HandleUiVolumeChanged(float Value);", + ], + UI_CPP: [ + "#include \"Components/Slider.h\"", + "AddVolumeSlider(Panel, FText::FromString(TEXT(\"Master\"))", + "AddVolumeSlider(Panel, FText::FromString(TEXT(\"Ambient\"))", + "AddVolumeSlider(Panel, FText::FromString(TEXT(\"Weather\"))", + "AddVolumeSlider(Panel, FText::FromString(TEXT(\"Effects\"))", + "AddVolumeSlider(Panel, FText::FromString(TEXT(\"Wildlife\"))", + "AddVolumeSlider(Panel, FText::FromString(TEXT(\"UI\"))", + "Slider->OnValueChanged.AddDynamic", + "UGameplayStatics::PlaySound2D(this, Sound, FMath::Clamp(MasterVolume * UiVolume", + "UAgrarianMvpFrontendWidget::HandleMasterVolumeChanged", + "UAgrarianMvpFrontendWidget::HandleUiVolumeChanged", + ], + TDD: [ + "volume sliders for master, ambient, weather", + "apply the UI bus to frontend sounds immediately", + "SoundClass", + ], + ROADMAP: [ + "[x] Add volume sliders.", + ], +} + + +def main() -> None: + missing = [] + for path, snippets in REQUIRED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}") + if missing: + raise SystemExit("FAILED: " + "; ".join(missing)) + print("OK: MVP frontend volume sliders are present.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp index 01c794b..0a62d28 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.cpp @@ -9,6 +9,7 @@ #include "Components/HorizontalBox.h" #include "Components/HorizontalBoxSlot.h" #include "Components/SizeBox.h" +#include "Components/Slider.h" #include "Components/TextBlock.h" #include "Components/VerticalBox.h" #include "Components/VerticalBoxSlot.h" @@ -303,6 +304,31 @@ void UAgrarianMvpFrontendWidget::RebuildFrontendTree() UButton* QuitButton = AddButton(Panel, FText::FromString(TEXT("Save & Quit")), QuitButtonColor, FLinearColor(0.58f, 0.28f, 0.22f, 1.0f), 34.0f * Scale); QuitButton->OnClicked.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleSaveAndQuitClicked); + AddText(Panel, FText::FromString(TEXT("Audio")), FMath::RoundToInt(18.0f * Scale), true, AccentColor, 8.0f * Scale); + if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Master")), MasterVolume, 8.0f * Scale)) + { + Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleMasterVolumeChanged); + } + if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Ambient")), AmbientVolume, 8.0f * Scale)) + { + Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleAmbientVolumeChanged); + } + if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Weather")), WeatherVolume, 8.0f * Scale)) + { + Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleWeatherVolumeChanged); + } + if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Effects")), EffectsVolume, 8.0f * Scale)) + { + Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleEffectsVolumeChanged); + } + if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("Wildlife")), WildlifeVolume, 8.0f * Scale)) + { + Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleWildlifeVolumeChanged); + } + if (USlider* Slider = AddVolumeSlider(Panel, FText::FromString(TEXT("UI")), UiVolume, 24.0f * Scale)) + { + Slider->OnValueChanged.AddDynamic(this, &UAgrarianMvpFrontendWidget::HandleUiVolumeChanged); + } AddText(Panel, FText::FromString(TEXT("Escape opens this menu. Save & Quit writes the current world save before closing.")), FMath::RoundToInt(16.0f * Scale), false, MutedTextColor, 0.0f); return; } @@ -479,10 +505,74 @@ void UAgrarianMvpFrontendWidget::PlayUiSound(USoundBase* Sound) const { if (Sound) { - UGameplayStatics::PlaySound2D(this, Sound); + UGameplayStatics::PlaySound2D(this, Sound, FMath::Clamp(MasterVolume * UiVolume, 0.0f, 1.0f)); } } +USlider* UAgrarianMvpFrontendWidget::AddVolumeSlider(UVerticalBox* Parent, const FText& Label, float Value, float BottomPadding) +{ + if (!Parent || !WidgetTree) + { + return nullptr; + } + + UHorizontalBox* Row = WidgetTree->ConstructWidget(UHorizontalBox::StaticClass()); + if (UVerticalBoxSlot* RowSlot = Parent->AddChildToVerticalBox(Row)) + { + RowSlot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, BottomPadding)); + RowSlot->SetHorizontalAlignment(HAlign_Fill); + } + + UTextBlock* LabelText = AddText(nullptr, Label, 15, false, FLinearColor(0.82f, 0.90f, 0.76f, 1.0f), 0.0f); + if (UHorizontalBoxSlot* LabelSlot = Row->AddChildToHorizontalBox(LabelText)) + { + LabelSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + LabelSlot->SetHorizontalAlignment(HAlign_Left); + LabelSlot->SetVerticalAlignment(VAlign_Center); + } + + USlider* Slider = WidgetTree->ConstructWidget(USlider::StaticClass()); + Slider->SetValue(FMath::Clamp(Value, 0.0f, 1.0f)); + if (UHorizontalBoxSlot* SliderSlot = Row->AddChildToHorizontalBox(Slider)) + { + SliderSlot->SetSize(FSlateChildSize(ESlateSizeRule::Fill)); + SliderSlot->SetHorizontalAlignment(HAlign_Fill); + SliderSlot->SetVerticalAlignment(VAlign_Center); + } + + return Slider; +} + +void UAgrarianMvpFrontendWidget::HandleMasterVolumeChanged(float Value) +{ + MasterVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +void UAgrarianMvpFrontendWidget::HandleAmbientVolumeChanged(float Value) +{ + AmbientVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +void UAgrarianMvpFrontendWidget::HandleWeatherVolumeChanged(float Value) +{ + WeatherVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +void UAgrarianMvpFrontendWidget::HandleEffectsVolumeChanged(float Value) +{ + EffectsVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +void UAgrarianMvpFrontendWidget::HandleWildlifeVolumeChanged(float Value) +{ + WildlifeVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + +void UAgrarianMvpFrontendWidget::HandleUiVolumeChanged(float Value) +{ + UiVolume = FMath::Clamp(Value, 0.0f, 1.0f); +} + void UAgrarianMvpFrontendWidget::FocusPrimaryButton() { if (PrimaryFocusButton) diff --git a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h index 4761865..b315ba7 100644 --- a/Source/AgrarianGame/AgrarianMvpFrontendWidget.h +++ b/Source/AgrarianGame/AgrarianMvpFrontendWidget.h @@ -7,6 +7,7 @@ #include "AgrarianMvpFrontendWidget.generated.h" class UButton; +class USlider; class USoundBase; class UTextBlock; class UVerticalBox; @@ -70,6 +71,24 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio") TObjectPtr UiSaveQuitSound; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1")) + float MasterVolume = 1.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1")) + float AmbientVolume = 0.70f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1")) + float WeatherVolume = 0.75f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1")) + float EffectsVolume = 0.80f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1")) + float WildlifeVolume = 0.70f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|MVP UI|Audio", meta = (ClampMin = "0", ClampMax = "1")) + float UiVolume = 0.65f; + UFUNCTION(BlueprintCallable, Category = "Agrarian|MVP UI") void SetActiveScreen(EAgrarianMvpFrontendScreen NewScreen); @@ -104,6 +123,25 @@ private: UTextBlock* AddText(UVerticalBox* Parent, const FText& Text, int32 FontSize, bool bBold, const FLinearColor& Color, float BottomPadding); UButton* AddButton(UVerticalBox* Parent, const FText& Text, const FLinearColor& NormalColor, const FLinearColor& HoveredColor, float BottomPadding); void PlayUiSound(USoundBase* Sound) const; + USlider* AddVolumeSlider(UVerticalBox* Parent, const FText& Label, float Value, float BottomPadding); + + UFUNCTION() + void HandleMasterVolumeChanged(float Value); + + UFUNCTION() + void HandleAmbientVolumeChanged(float Value); + + UFUNCTION() + void HandleWeatherVolumeChanged(float Value); + + UFUNCTION() + void HandleEffectsVolumeChanged(float Value); + + UFUNCTION() + void HandleWildlifeVolumeChanged(float Value); + + UFUNCTION() + void HandleUiVolumeChanged(float Value); UFUNCTION() void FocusPrimaryButton();