Add MVP gathering audio hooks

This commit is contained in:
2026-05-19 12:06:14 -07:00
parent bb5ed3883b
commit 47cd7a5479
5 changed files with 100 additions and 1 deletions
+1 -1
View File
@@ -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 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. - [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 fire sounds.
- [ ] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. - [ ] 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. - [ ] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration.
+7
View File
@@ -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 remain silent until placeholder or final surface-aware cues are assigned while
still giving designers a real hook for step audio in packaged builds. 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`. Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish`.
Extinguishing clears remaining fuel, turns off replicated lit state, and reuses Extinguishing clears remaining fuel, turns off replicated lit state, and reuses
the same visual update path as natural fuel depletion. the same visual update path as natural fuel depletion.
+54
View File
@@ -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<UAudioComponent> GatheringAudioComponent;",
"TObjectPtr<USoundBase> GatheringSound;",
"TObjectPtr<USoundBase> DepletedGatheringSound;",
"UFUNCTION(NetMulticast, Unreliable)",
"void MulticastPlayGatheringSound(bool bDepletedAfterGather);",
],
RESOURCE_CPP: [
"#include \"Components/AudioComponent.h\"",
"GatheringAudioComponent = CreateDefaultSubobject<UAudioComponent>",
"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()
@@ -5,6 +5,7 @@
#include "AgrarianInventoryComponent.h" #include "AgrarianInventoryComponent.h"
#include "AgrarianItemDefinitionAsset.h" #include "AgrarianItemDefinitionAsset.h"
#include "AgrarianSaveGame.h" #include "AgrarianSaveGame.h"
#include "Components/AudioComponent.h"
#include "Components/StaticMeshComponent.h" #include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h" #include "Engine/StaticMesh.h"
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
@@ -67,6 +68,11 @@ AAgrarianResourceNode::AAgrarianResourceNode()
HarvestableMarkerProxy->SetRelativeLocation(FVector(0.0f, 22.0f, 36.0f)); HarvestableMarkerProxy->SetRelativeLocation(FVector(0.0f, 22.0f, 36.0f));
HarvestableMarkerProxy->SetRelativeScale3D(FVector(0.28f, 0.18f, 0.22f)); HarvestableMarkerProxy->SetRelativeScale3D(FVector(0.28f, 0.18f, 0.22f));
GatheringAudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("GatheringAudioComponent"));
GatheringAudioComponent->SetupAttachment(RootComponent);
GatheringAudioComponent->bAutoActivate = false;
GatheringAudioComponent->bAllowSpatialization = true;
YieldItem.ItemId = TEXT("wood"); YieldItem.ItemId = TEXT("wood");
YieldItem.DisplayName = FText::FromString(TEXT("Wood")); YieldItem.DisplayName = FText::FromString(TEXT("Wood"));
YieldItem.Quantity = 1; YieldItem.Quantity = 1;
@@ -134,6 +140,7 @@ void AAgrarianResourceNode::Interact_Implementation(AAgrarianGameCharacter* Inte
if (Inventory->AddItem(Granted)) if (Inventory->AddItem(Granted))
{ {
RemainingHarvests--; RemainingHarvests--;
MulticastPlayGatheringSound(RemainingHarvests <= 0);
UpdateDepletedState(); UpdateDepletedState();
ScheduleRespawnIfNeeded(); ScheduleRespawnIfNeeded();
} }
@@ -145,6 +152,23 @@ void AAgrarianResourceNode::OnRep_RemainingHarvests()
UpdateDepletedState(); 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 FName AAgrarianResourceNode::GetResourcePersistenceId() const
{ {
return PersistenceNodeId != NAME_None ? PersistenceNodeId : GetFName(); return PersistenceNodeId != NAME_None ? PersistenceNodeId : GetFName();
@@ -11,6 +11,8 @@
#include "AgrarianResourceNode.generated.h" #include "AgrarianResourceNode.generated.h"
class UStaticMeshComponent; class UStaticMeshComponent;
class UAudioComponent;
class USoundBase;
class UAgrarianItemDefinitionAsset; class UAgrarianItemDefinitionAsset;
UCLASS(Blueprintable) UCLASS(Blueprintable)
@@ -33,6 +35,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource|Visuals") UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource|Visuals")
TObjectPtr<UStaticMeshComponent> HarvestableMarkerProxy; TObjectPtr<UStaticMeshComponent> HarvestableMarkerProxy;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource|Audio")
TObjectPtr<UAudioComponent> GatheringAudioComponent;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource")
FAgrarianItemStack YieldItem; FAgrarianItemStack YieldItem;
@@ -66,6 +71,12 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn", meta = (ClampMin = "1")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Respawn", meta = (ClampMin = "1"))
int32 MaxHarvests = 5; int32 MaxHarvests = 5;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Audio")
TObjectPtr<USoundBase> GatheringSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Audio")
TObjectPtr<USoundBase> DepletedGatheringSound;
virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual FText GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const override;
virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override; virtual bool CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const override;
virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override; virtual void Interact_Implementation(AAgrarianGameCharacter* Interactor) override;
@@ -83,6 +94,9 @@ protected:
UFUNCTION() UFUNCTION()
void OnRep_RemainingHarvests(); void OnRep_RemainingHarvests();
UFUNCTION(NetMulticast, Unreliable)
void MulticastPlayGatheringSound(bool bDepletedAfterGather);
bool HasRequiredTool(const AAgrarianGameCharacter* Interactor) const; bool HasRequiredTool(const AAgrarianGameCharacter* Interactor) const;
int32 GetHarvestQuantityFor(const AAgrarianGameCharacter* Interactor) const; int32 GetHarvestQuantityFor(const AAgrarianGameCharacter* Interactor) const;
FAgrarianItemStack MakeYieldStack(const AAgrarianGameCharacter* Interactor) const; FAgrarianItemStack MakeYieldStack(const AAgrarianGameCharacter* Interactor) const;