diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 6c40ce7..1e7b679 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -846,7 +846,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Persist active grass, forest, and structure fires across save/load without corrupting world state. Extended campfire persistence coverage for ignition flags, ignition risk scores, active grass/forest/structure fire intensities, spread radius, and suppression pressure so save/load recovery preserves active and partially suppressed fire state. - [x] Add QA coverage for safe campfires, unsafe campfires, vegetation spread, shelter ignition, suppression, and save/load recovery. Added a fire-risk QA coverage document and verifier requiring safe/unsafe campfire, vegetation spread, shelter ignition, suppression, and save/load recovery scenarios plus the supporting fire-risk verification scripts. - [x] Add weather sounds. Formalized the existing placed weather audio controller as the MVP weather-sound path, documenting rain, wind, storm, clear ambient, and biome loop slots plus verification that weather playback follows replicated weather state, provider wind speed, and day/night state while remaining silent until assets are assigned. -- [ ] Add wildlife sounds. +- [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. - [ ] Add UI sounds. - [ ] Add mix settings. - [ ] Add volume sliders. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index f988458..71c548b 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -318,6 +318,11 @@ controller or a Blueprint child. Weather sound requirements are tracked in `Docs/Audio/WeatherSounds.md`. +Wildlife sounds are optional, spatialized hooks on `AAgrarianWildlifeBase`. +Wildlife expose idle, flee/chase, death, and harvest sound slots. The server +multicasts state-change and harvest cues so clients hear wildlife reactions from +the authoritative AI state while the dedicated server remains silent. + 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_wildlife_sounds.py b/Scripts/verify_wildlife_sounds.py new file mode 100644 index 0000000..0e40e26 --- /dev/null +++ b/Scripts/verify_wildlife_sounds.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Verify MVP wildlife sound hooks are authoritative and spatialized.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +WILDLIFE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianWildlifeBase.h" +WILDLIFE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianWildlifeBase.cpp" +TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + +REQUIRED = { + WILDLIFE_H: [ + "TObjectPtr WildlifeAudioComponent;", + "TObjectPtr IdleWildlifeSound;", + "TObjectPtr FleeWildlifeSound;", + "TObjectPtr DeathWildlifeSound;", + "TObjectPtr HarvestWildlifeSound;", + "UFUNCTION(NetMulticast, Unreliable)", + "void MulticastPlayWildlifeStateSound(EAgrarianWildlifeState NewState);", + "void MulticastPlayWildlifeHarvestSound();", + "USoundBase* GetSoundForWildlifeState(EAgrarianWildlifeState State) const;", + ], + WILDLIFE_CPP: [ + "#include \"Components/AudioComponent.h\"", + "WildlifeAudioComponent = CreateDefaultSubobject", + "WildlifeAudioComponent->bAllowSpatialization = true", + "MulticastPlayWildlifeStateSound(WildlifeState);", + "AAgrarianWildlifeBase::MulticastPlayWildlifeStateSound_Implementation", + "AAgrarianWildlifeBase::MulticastPlayWildlifeHarvestSound_Implementation", + "MulticastPlayWildlifeHarvestSound();", + "EAgrarianWildlifeState::Fleeing", + "EAgrarianWildlifeState::Dead", + ], + TDD: [ + "Wildlife sounds are optional, spatialized hooks", + "idle, flee/chase, death, and harvest", + "server\nmulticasts state-change and harvest cues", + ], + ROADMAP: [ + "[x] Add wildlife sounds.", + ], +} + + +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: wildlife sound hooks are authoritative and spatialized.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianWildlifeBase.cpp b/Source/AgrarianGame/AgrarianWildlifeBase.cpp index 68045ad..b0b4ac4 100644 --- a/Source/AgrarianGame/AgrarianWildlifeBase.cpp +++ b/Source/AgrarianGame/AgrarianWildlifeBase.cpp @@ -4,6 +4,7 @@ #include "AgrarianGameCharacter.h" #include "AgrarianInventoryComponent.h" #include "AIController.h" +#include "Components/AudioComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "GameFramework/CharacterMovementComponent.h" @@ -86,6 +87,11 @@ AAgrarianWildlifeBase::AAgrarianWildlifeBase() WildlifeTailProxy->SetRelativeRotation(FRotator(0.0f, 90.0f, 90.0f)); WildlifeTailProxy->SetRelativeScale3D(FVector(0.12f, 0.12f, 0.24f)); + WildlifeAudioComponent = CreateDefaultSubobject(TEXT("WildlifeAudioComponent")); + WildlifeAudioComponent->SetupAttachment(RootComponent); + WildlifeAudioComponent->bAutoActivate = false; + WildlifeAudioComponent->bAllowSpatialization = true; + DisplayName = FText::FromString(TEXT("Wildlife")); } @@ -168,6 +174,10 @@ void AAgrarianWildlifeBase::SetWildlifeState(EAgrarianWildlifeState NewState) } WildlifeState = NewState; + if (HasAuthority()) + { + MulticastPlayWildlifeStateSound(WildlifeState); + } BroadcastStateChanged(); } @@ -204,6 +214,34 @@ void AAgrarianWildlifeBase::OnRep_WildlifeState() BroadcastStateChanged(); } +void AAgrarianWildlifeBase::MulticastPlayWildlifeStateSound_Implementation(EAgrarianWildlifeState NewState) +{ + if (GetNetMode() == NM_DedicatedServer || !WildlifeAudioComponent) + { + return; + } + + USoundBase* StateSound = GetSoundForWildlifeState(NewState); + if (!StateSound) + { + return; + } + + WildlifeAudioComponent->SetSound(StateSound); + WildlifeAudioComponent->Play(); +} + +void AAgrarianWildlifeBase::MulticastPlayWildlifeHarvestSound_Implementation() +{ + if (GetNetMode() == NM_DedicatedServer || !WildlifeAudioComponent || !HarvestWildlifeSound) + { + return; + } + + WildlifeAudioComponent->SetSound(HarvestWildlifeSound); + WildlifeAudioComponent->Play(); +} + bool AAgrarianWildlifeBase::ShouldRunServerThink(float DeltaSeconds) { if (!bEnablePerformanceLimits || WildlifeState == EAgrarianWildlifeState::Dead) @@ -524,6 +562,7 @@ bool AAgrarianWildlifeBase::Harvest(AAgrarianGameCharacter* Interactor) } bHarvested = true; + MulticastPlayWildlifeHarvestSound(); return true; } @@ -536,3 +575,19 @@ void AAgrarianWildlifeBase::BroadcastStateChanged() { OnWildlifeStateChanged.Broadcast(WildlifeState); } + +USoundBase* AAgrarianWildlifeBase::GetSoundForWildlifeState(EAgrarianWildlifeState State) const +{ + switch (State) + { + case EAgrarianWildlifeState::Fleeing: + case EAgrarianWildlifeState::Chasing: + return FleeWildlifeSound; + case EAgrarianWildlifeState::Dead: + return DeathWildlifeSound; + case EAgrarianWildlifeState::Idle: + case EAgrarianWildlifeState::Wandering: + default: + return IdleWildlifeSound; + } +} diff --git a/Source/AgrarianGame/AgrarianWildlifeBase.h b/Source/AgrarianGame/AgrarianWildlifeBase.h index 2d38c7e..b3bfde0 100644 --- a/Source/AgrarianGame/AgrarianWildlifeBase.h +++ b/Source/AgrarianGame/AgrarianWildlifeBase.h @@ -9,6 +9,8 @@ #include "AgrarianWildlifeBase.generated.h" class AAgrarianGameCharacter; +class UAudioComponent; +class USoundBase; class UStaticMeshComponent; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianWildlifeStateChangedSignature, EAgrarianWildlifeState, NewState); @@ -46,6 +48,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Visuals") TObjectPtr WildlifeTailProxy; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Wildlife|Audio") + TObjectPtr WildlifeAudioComponent; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife") FName WildlifeId = TEXT("wildlife"); @@ -109,6 +114,18 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Wildlife|Harvest") bool bHarvested = false; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio") + TObjectPtr IdleWildlifeSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio") + TObjectPtr FleeWildlifeSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio") + TObjectPtr DeathWildlifeSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Wildlife|Audio") + TObjectPtr HarvestWildlifeSound; + UFUNCTION(BlueprintCallable, Category = "Agrarian|Wildlife") bool IsAlive() const; @@ -131,6 +148,12 @@ protected: UFUNCTION() void OnRep_WildlifeState(); + UFUNCTION(NetMulticast, Unreliable) + void MulticastPlayWildlifeStateSound(EAgrarianWildlifeState NewState); + + UFUNCTION(NetMulticast, Unreliable) + void MulticastPlayWildlifeHarvestSound(); + bool ShouldRunServerThink(float DeltaSeconds); void ServerThink(float DeltaSeconds); void ChooseWanderTarget(); @@ -146,6 +169,7 @@ protected: bool Harvest(AAgrarianGameCharacter* Interactor); void BroadcastHealthChanged(); void BroadcastStateChanged(); + USoundBase* GetSoundForWildlifeState(EAgrarianWildlifeState State) const; UPROPERTY() FVector SpawnLocation = FVector::ZeroVector;