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 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.
+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
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.
+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 "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<UAudioComponent>(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();
@@ -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<UStaticMeshComponent> HarvestableMarkerProxy;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Resource|Audio")
TObjectPtr<UAudioComponent> 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<USoundBase> GatheringSound;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Resource|Audio")
TObjectPtr<USoundBase> 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;