From 47cd7a547965e3c7e6b61e406218d28554b56198 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 19 May 2026 12:06:14 -0700 Subject: [PATCH] Add MVP gathering audio hooks --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- Docs/TechnicalDesignDocument.md | 7 +++ Scripts/verify_gathering_sounds.py | 54 ++++++++++++++++++++ Source/AgrarianGame/AgrarianResourceNode.cpp | 24 +++++++++ Source/AgrarianGame/AgrarianResourceNode.h | 14 +++++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_gathering_sounds.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index a80cc8a..a431136 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -835,7 +835,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add ambient biome audio. Extended the placed `AAgrarianWeatherAudioController` so its ambient component explicitly owns a Ground Zero coastal-scrub biome loop slot with separate day/night volume targets, keeping the current MVP silent until placeholder or final audio assets are assigned while giving the map a real ambient audio attachment point. - [x] Add footstep placeholders. Added native player-character footstep hooks with assignable walk, sprint, crouch, and prone sound slots plus movement-state cadence, keeping the MVP silent until placeholder or final surface-aware audio assets are assigned. -- [ ] Add gathering sounds. +- [x] Add gathering sounds. Added spatialized resource-node gathering audio hooks with assignable normal/depleted gathering cues and a server-authoritative multicast trigger after successful harvests, keeping multiplayer clients aligned while remaining silent until audio assets are assigned. - [ ] Add fire sounds. - [ ] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. - [ ] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 217318f..134042a 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -323,6 +323,13 @@ slots. Cadence is state-aware and driven by horizontal movement, so the MVP can remain silent until placeholder or final surface-aware cues are assigned while still giving designers a real hook for step audio in packaged builds. +Gathering audio is owned by `AAgrarianResourceNode`. Successful +server-authoritative harvests call a multicast placeholder cue so nearby +clients can hear the action without trusting client-side gathering requests. +Each resource node exposes `GatheringSound` and `DepletedGatheringSound` slots, +with a spatialized `GatheringAudioComponent`; the system remains silent until +resource-specific placeholder or final cues are assigned. + Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish`. Extinguishing clears remaining fuel, turns off replicated lit state, and reuses the same visual update path as natural fuel depletion. diff --git a/Scripts/verify_gathering_sounds.py b/Scripts/verify_gathering_sounds.py new file mode 100644 index 0000000..abeb8c4 --- /dev/null +++ b/Scripts/verify_gathering_sounds.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Verify MVP gathering sound hooks are server-triggered and multicast.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +RESOURCE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianResourceNode.h" +RESOURCE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianResourceNode.cpp" +TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + +REQUIRED = { + RESOURCE_H: [ + "TObjectPtr GatheringAudioComponent;", + "TObjectPtr GatheringSound;", + "TObjectPtr DepletedGatheringSound;", + "UFUNCTION(NetMulticast, Unreliable)", + "void MulticastPlayGatheringSound(bool bDepletedAfterGather);", + ], + RESOURCE_CPP: [ + "#include \"Components/AudioComponent.h\"", + "GatheringAudioComponent = CreateDefaultSubobject", + "GatheringAudioComponent->bAllowSpatialization = true", + "MulticastPlayGatheringSound(RemainingHarvests <= 0);", + "AAgrarianResourceNode::MulticastPlayGatheringSound_Implementation", + "GetNetMode() == NM_DedicatedServer", + "GatheringAudioComponent->Play();", + ], + TDD: [ + "Gathering audio is owned by `AAgrarianResourceNode`", + "server-authoritative harvests call a multicast placeholder cue", + "`GatheringSound` and `DepletedGatheringSound`", + ], + ROADMAP: [ + "[x] Add gathering 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: gathering sound hooks are server-triggered and multicast.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianResourceNode.cpp b/Source/AgrarianGame/AgrarianResourceNode.cpp index 584f139..70e0df9 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.cpp +++ b/Source/AgrarianGame/AgrarianResourceNode.cpp @@ -5,6 +5,7 @@ #include "AgrarianInventoryComponent.h" #include "AgrarianItemDefinitionAsset.h" #include "AgrarianSaveGame.h" +#include "Components/AudioComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Materials/MaterialInterface.h" @@ -67,6 +68,11 @@ AAgrarianResourceNode::AAgrarianResourceNode() HarvestableMarkerProxy->SetRelativeLocation(FVector(0.0f, 22.0f, 36.0f)); HarvestableMarkerProxy->SetRelativeScale3D(FVector(0.28f, 0.18f, 0.22f)); + GatheringAudioComponent = CreateDefaultSubobject(TEXT("GatheringAudioComponent")); + GatheringAudioComponent->SetupAttachment(RootComponent); + GatheringAudioComponent->bAutoActivate = false; + GatheringAudioComponent->bAllowSpatialization = true; + YieldItem.ItemId = TEXT("wood"); YieldItem.DisplayName = FText::FromString(TEXT("Wood")); YieldItem.Quantity = 1; @@ -134,6 +140,7 @@ void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Inte if (Inventory->AddItem(Granted)) { RemainingHarvests--; + MulticastPlayGatheringSound(RemainingHarvests <= 0); UpdateDepletedState(); ScheduleRespawnIfNeeded(); } @@ -145,6 +152,23 @@ void AAgrarianResourceNode::OnRep_RemainingHarvests() UpdateDepletedState(); } +void AAgrarianResourceNode::MulticastPlayGatheringSound_Implementation(bool bDepletedAfterGather) +{ + if (GetNetMode() == NM_DedicatedServer || !GatheringAudioComponent) + { + return; + } + + USoundBase* SoundToPlay = (bDepletedAfterGather && DepletedGatheringSound) ? DepletedGatheringSound : GatheringSound; + if (!SoundToPlay) + { + return; + } + + GatheringAudioComponent->SetSound(SoundToPlay); + GatheringAudioComponent->Play(); +} + FName AAgrarianResourceNode::GetResourcePersistenceId() const { return PersistenceNodeId != NAME_None ? PersistenceNodeId : GetFName(); diff --git a/Source/AgrarianGame/AgrarianResourceNode.h b/Source/AgrarianGame/AgrarianResourceNode.h index bf20a84..7684672 100644 --- a/Source/AgrarianGame/AgrarianResourceNode.h +++ b/Source/AgrarianGame/AgrarianResourceNode.h @@ -11,6 +11,8 @@ #include "AgrarianResourceNode.generated.h" class UStaticMeshComponent; +class UAudioComponent; +class USoundBase; class UAgrarianItemDefinitionAsset; UCLASS(Blueprintable) @@ -33,6 +35,9 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource|Visuals") TObjectPtr HarvestableMarkerProxy; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource|Audio") + TObjectPtr GatheringAudioComponent; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource") FAgrarianItemStack YieldItem; @@ -66,6 +71,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn", meta = (ClampMin = "1")) int32 MaxHarvests = 5; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Audio") + TObjectPtr GatheringSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Audio") + TObjectPtr DepletedGatheringSound; + virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override; @@ -83,6 +94,9 @@ protected: UFUNCTION() void OnRep_RemainingHarvests(); + UFUNCTION(NetMulticast, Unreliable) + void MulticastPlayGatheringSound(bool bDepletedAfterGather); + bool HasRequiredTool(const AAgrarianGameCharacter* Interactor) const; int32 GetHarvestQuantityFor(const AAgrarianGameCharacter* Interactor) const; FAgrarianItemStack MakeYieldStack(const AAgrarianGameCharacter* Interactor) const;