diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 9108029..eaf742f 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -436,7 +436,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Connect weather to body temperature. - [x] Connect shelter to weather protection. Survival now calculates the best overlapping shelter protection volume, replicates current weather protection, reduces ambient exposure and cold damage by shelter coverage, trends care-history shelter quality toward active protection, and shows shelter protection on the dev HUD. - [x] Add first-pass sky and lighting. Added `AAgrarianSkyLightingController` with movable sun, skylight, and fog components driven by replicated time, local sunrise/sunset, weather state, and provider cloud cover; updated the Ground Zero setup script to place the controller and remove legacy static lighting actors. -- [ ] Add audio cues for weather. +- [x] Add audio cues for weather. Added `AAgrarianWeatherAudioController` with ambient, rain, wind, and storm audio components, assignable loop sound slots, weather/wind/night-driven volume fades, and Ground Zero map setup placement so placeholder or final loops can be assigned without changing gameplay code. ## 0.1.D Single Biome MVP Map diff --git a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap index 62d88ac..23cca6e 100644 --- a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap +++ b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b50fafbdcfb04a64be133adcdcb35d9e21a40ae0c7b762beb0b32ad8c8d6b65 -size 7482052 +oid sha256:b543e7a7ab079497a2c122d91710cf9883795eb97a5228a2aad0d15958f1e9a6 +size 7484993 diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index bf6f34c..a4124ea 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -164,6 +164,16 @@ the represented local day/night cycle and current weather without hard-coded static light settings. The Ground Zero map setup script places this controller and removes the earlier static demo sun/skylight/fog actors. +First-pass weather audio uses `AAgrarianWeatherAudioController`. The controller +owns ambient, rain, wind, and storm audio components with assignable loop sound +slots. It reads replicated weather state, provider wind speed, provider cloud +data, and local night/day state, then fades component volumes so rain, wind, and +storm cues follow the same authoritative weather mapping used by temperature and +lighting. The current MVP can ship without final sound assets because the +controller is silent until loops are assigned; placeholder or final audio can be +added by setting the exposed sound properties on the placed controller or a +Blueprint child. + The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the current temperature, daily low/high, precipitation, wind, humidity, cloud cover, diff --git a/Scripts/setup_ground_zero_demo_map.py b/Scripts/setup_ground_zero_demo_map.py index 8f1a29f..845fbe2 100644 --- a/Scripts/setup_ground_zero_demo_map.py +++ b/Scripts/setup_ground_zero_demo_map.py @@ -74,6 +74,13 @@ DEMO_ACTORS = [ "fixed_z": 12000.0, "rotation": unreal.Rotator(-42.0, -35.0, 0.0), }, + { + "label": "AGR_DemoWeatherAudioController", + "class": unreal.AgrarianWeatherAudioController, + "location_xy": unreal.Vector(-18000.0, -7000.0, 0.0), + "fixed_z": 11800.0, + "rotation": unreal.Rotator(0.0, 0.0, 0.0), + }, { "label": "AGR_DemoNoticeActor", "class": unreal.AgrarianDemoNoticeActor, @@ -322,7 +329,7 @@ def spawn_foliage_actor(height_values): reserved_points = [ spec["location_xy"] for spec in DEMO_ACTORS - if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoNoticeActor"} + if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoWeatherAudioController", "AGR_DemoNoticeActor"} ] foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world( diff --git a/Scripts/verify_weather_audio_controller.py b/Scripts/verify_weather_audio_controller.py new file mode 100644 index 0000000..37718b4 --- /dev/null +++ b/Scripts/verify_weather_audio_controller.py @@ -0,0 +1,62 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +AUDIO_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherAudioController.h" +AUDIO_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWeatherAudioController.cpp" +MAP_SETUP = ROOT / "Scripts" / "setup_ground_zero_demo_map.py" +TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + + +EXPECTED = { + AUDIO_H: [ + "class AAgrarianWeatherAudioController : public AActor", + "TObjectPtr AmbientAudio;", + "TObjectPtr RainAudio;", + "TObjectPtr WindAudio;", + "TObjectPtr StormAudio;", + "TObjectPtr RainLoopSound;", + "void RefreshWeatherAudio(float DeltaSeconds);", + ], + AUDIO_CPP: [ + "#include \"AgrarianGameState.h\"", + "#include \"Components/AudioComponent.h\"", + "AmbientAudio = CreateDefaultSubobject", + "RainAudio = CreateDefaultSubobject", + "WindAudio = CreateDefaultSubobject", + "StormAudio = CreateDefaultSubobject", + "GameState->Weather", + "GameState->ActiveWeatherInputs.WindSpeedKmh", + "GameState->IsNight()", + "ApplyComponentVolume(RainAudio, CurrentRainVolume);", + "AudioComponent->SetVolumeMultiplier", + ], + MAP_SETUP: [ + "AGR_DemoWeatherAudioController", + "unreal.AgrarianWeatherAudioController", + ], + TDD: [ + "`AAgrarianWeatherAudioController`", + "ambient, rain, wind, and storm audio components", + ], + ROADMAP: [ + "[x] Add audio cues for weather.", + ], +} + + +def main() -> None: + missing = [] + for path, snippets in EXPECTED.items(): + text = path.read_text(encoding="utf-8") + for snippet in snippets: + if snippet not in text: + missing.append(f"{path.relative_to(ROOT)}: {snippet}") + if missing: + raise RuntimeError("Weather audio controller verification failed: " + "; ".join(missing)) + print("Agrarian weather audio controller verification complete.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianWeatherAudioController.cpp b/Source/AgrarianGame/AgrarianWeatherAudioController.cpp new file mode 100644 index 0000000..366f01d --- /dev/null +++ b/Source/AgrarianGame/AgrarianWeatherAudioController.cpp @@ -0,0 +1,149 @@ +// Copyright Pacificao. All Rights Reserved. + +#include "AgrarianWeatherAudioController.h" + +#include "AgrarianGameState.h" +#include "Components/AudioComponent.h" +#include "Components/SceneComponent.h" +#include "Engine/World.h" + +AAgrarianWeatherAudioController::AAgrarianWeatherAudioController() +{ + PrimaryActorTick.bCanEverTick = true; + bReplicates = false; + + SceneRoot = CreateDefaultSubobject(TEXT("SceneRoot")); + RootComponent = SceneRoot; + + AmbientAudio = CreateDefaultSubobject(TEXT("AmbientAudio")); + AmbientAudio->SetupAttachment(SceneRoot); + AmbientAudio->bAutoActivate = false; + + RainAudio = CreateDefaultSubobject(TEXT("RainAudio")); + RainAudio->SetupAttachment(SceneRoot); + RainAudio->bAutoActivate = false; + + WindAudio = CreateDefaultSubobject(TEXT("WindAudio")); + WindAudio->SetupAttachment(SceneRoot); + WindAudio->bAutoActivate = false; + + StormAudio = CreateDefaultSubobject(TEXT("StormAudio")); + StormAudio->SetupAttachment(SceneRoot); + StormAudio->bAutoActivate = false; +} + +void AAgrarianWeatherAudioController::BeginPlay() +{ + Super::BeginPlay(); + AssignConfiguredSounds(); + RefreshWeatherAudio(0.0f); +} + +void AAgrarianWeatherAudioController::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + RefreshWeatherAudio(DeltaSeconds); +} + +void AAgrarianWeatherAudioController::RefreshWeatherAudio(float DeltaSeconds) +{ + const UWorld* World = GetWorld(); + const AAgrarianGameState* GameState = World ? World->GetGameState() : nullptr; + if (!GameState) + { + return; + } + + CurrentWeather = GameState->Weather; + const float WindAlpha = GetProviderWindAlpha(GameState->ActiveWeatherInputs.WindSpeedKmh, GameState->ActiveWeatherInputs.bHasProviderData); + float TargetRainVolume = 0.0f; + float TargetWindVolume = WindAlpha * MaxWindVolume; + float TargetStormVolume = 0.0f; + + switch (GameState->Weather) + { + case EAgrarianWeatherType::Rain: + TargetRainVolume = MaxRainVolume; + TargetWindVolume = FMath::Max(TargetWindVolume, MaxWindVolume * 0.25f); + break; + case EAgrarianWeatherType::ColdWind: + TargetWindVolume = FMath::Max(TargetWindVolume, MaxWindVolume * 0.7f); + break; + case EAgrarianWeatherType::Storm: + TargetRainVolume = MaxRainVolume; + TargetWindVolume = MaxWindVolume; + TargetStormVolume = MaxStormVolume; + break; + default: + break; + } + + const float TargetAmbientVolume = GameState->IsNight() ? AmbientNightVolume : AmbientDayVolume; + const float InterpSpeed = FMath::Max(0.1f, VolumeInterpSpeed); + CurrentAmbientVolume = FMath::FInterpTo(CurrentAmbientVolume, TargetAmbientVolume, DeltaSeconds, InterpSpeed); + CurrentRainVolume = FMath::FInterpTo(CurrentRainVolume, TargetRainVolume, DeltaSeconds, InterpSpeed); + CurrentWindVolume = FMath::FInterpTo(CurrentWindVolume, TargetWindVolume, DeltaSeconds, InterpSpeed); + CurrentStormVolume = FMath::FInterpTo(CurrentStormVolume, TargetStormVolume, DeltaSeconds, InterpSpeed); + + if (DeltaSeconds <= 0.0f) + { + CurrentAmbientVolume = TargetAmbientVolume; + CurrentRainVolume = TargetRainVolume; + CurrentWindVolume = TargetWindVolume; + CurrentStormVolume = TargetStormVolume; + } + + ApplyComponentVolume(AmbientAudio, CurrentAmbientVolume); + ApplyComponentVolume(RainAudio, CurrentRainVolume); + ApplyComponentVolume(WindAudio, CurrentWindVolume); + ApplyComponentVolume(StormAudio, CurrentStormVolume); +} + +void AAgrarianWeatherAudioController::AssignConfiguredSounds() +{ + if (AmbientAudio && ClearAmbientSound) + { + AmbientAudio->SetSound(ClearAmbientSound); + } + if (RainAudio && RainLoopSound) + { + RainAudio->SetSound(RainLoopSound); + } + if (WindAudio && WindLoopSound) + { + WindAudio->SetSound(WindLoopSound); + } + if (StormAudio && StormLoopSound) + { + StormAudio->SetSound(StormLoopSound); + } +} + +void AAgrarianWeatherAudioController::ApplyComponentVolume(UAudioComponent* AudioComponent, float Volume) const +{ + if (!AudioComponent) + { + return; + } + + const float SafeVolume = FMath::Clamp(Volume, 0.0f, 1.0f); + AudioComponent->SetVolumeMultiplier(SafeVolume); + if (AudioComponent->Sound && SafeVolume > 0.01f && !AudioComponent->IsPlaying()) + { + AudioComponent->Play(); + } + else if (SafeVolume <= 0.01f && AudioComponent->IsPlaying()) + { + AudioComponent->Stop(); + } +} + +float AAgrarianWeatherAudioController::GetProviderWindAlpha(float WindSpeedKmh, bool bHasProviderData) const +{ + if (!bHasProviderData) + { + return 0.0f; + } + + return FMath::Clamp(WindSpeedKmh / 55.0f, 0.0f, 1.0f); +} diff --git a/Source/AgrarianGame/AgrarianWeatherAudioController.h b/Source/AgrarianGame/AgrarianWeatherAudioController.h new file mode 100644 index 0000000..556c243 --- /dev/null +++ b/Source/AgrarianGame/AgrarianWeatherAudioController.h @@ -0,0 +1,92 @@ +// Copyright Pacificao. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "AgrarianTypes.h" +#include "AgrarianWeatherAudioController.generated.h" + +class UAudioComponent; +class USceneComponent; +class USoundBase; + +UCLASS(Blueprintable) +class AAgrarianWeatherAudioController : public AActor +{ + GENERATED_BODY() + +public: + AAgrarianWeatherAudioController(); + + virtual void BeginPlay() override; + virtual void Tick(float DeltaSeconds) override; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + TObjectPtr SceneRoot; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + TObjectPtr AmbientAudio; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + TObjectPtr RainAudio; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + TObjectPtr WindAudio; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + TObjectPtr StormAudio; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio") + TObjectPtr ClearAmbientSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio") + TObjectPtr RainLoopSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio") + TObjectPtr WindLoopSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio") + TObjectPtr StormLoopSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.1")) + float VolumeInterpSpeed = 1.5f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0")) + float AmbientDayVolume = 0.35f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0")) + float AmbientNightVolume = 0.22f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0")) + float MaxRainVolume = 0.8f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0")) + float MaxWindVolume = 0.7f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Weather Audio", meta = (ClampMin = "0.0", ClampMax = "1.0")) + float MaxStormVolume = 0.9f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + EAgrarianWeatherType CurrentWeather = EAgrarianWeatherType::Clear; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + float CurrentAmbientVolume = 0.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + float CurrentRainVolume = 0.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + float CurrentWindVolume = 0.0f; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Weather Audio") + float CurrentStormVolume = 0.0f; + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Weather Audio") + void RefreshWeatherAudio(float DeltaSeconds); + +protected: + void AssignConfiguredSounds(); + void ApplyComponentVolume(UAudioComponent* AudioComponent, float Volume) const; + float GetProviderWindAlpha(float WindSpeedKmh, bool bHasProviderData) const; +};