From 280fa76af2e123424724876c5b08026e826d4e16 Mon Sep 17 00:00:00 2001 From: nathan Date: Tue, 19 May 2026 12:08:16 -0700 Subject: [PATCH] Add MVP campfire audio hooks --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- Docs/TechnicalDesignDocument.md | 7 +++ Scripts/verify_fire_sounds.py | 59 ++++++++++++++++++++++++ Source/AgrarianGame/AgrarianCampfire.cpp | 53 +++++++++++++++++++++ Source/AgrarianGame/AgrarianCampfire.h | 20 ++++++++ 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 Scripts/verify_fire_sounds.py diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index a431136..e0214bf 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -836,7 +836,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. - [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. +- [x] Add fire sounds. Added campfire loop, ignition, and extinguish audio hooks with spatialized components, replicated lit-state loop control, and server-triggered multicast event cues so fire audio follows the authoritative campfire state once assets are assigned. - [ ] 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 shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 134042a..eb2374d 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -330,6 +330,13 @@ 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. +Campfire audio is split between a persistent spatialized loop and short +server-triggered event cues. `AAgrarianCampfire` exposes `FireLoopSound`, +`IgniteSound`, and `ExtinguishSound` slots. Replicated lit-state updates start +or stop the loop on clients, while the authoritative server multicasts ignition +and extinguish events so the audio follows the same state changes as light, +smoke, warmth, fuel, and persistence. + 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_fire_sounds.py b/Scripts/verify_fire_sounds.py new file mode 100644 index 0000000..11a4364 --- /dev/null +++ b/Scripts/verify_fire_sounds.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Verify MVP fire sound hooks follow authoritative campfire state.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FIRE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.h" +FIRE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp" +TDD = ROOT / "Docs" / "TechnicalDesignDocument.md" +ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md" + +REQUIRED = { + FIRE_H: [ + "TObjectPtr FireLoopAudioComponent;", + "TObjectPtr FireEventAudioComponent;", + "TObjectPtr FireLoopSound;", + "TObjectPtr IgniteSound;", + "TObjectPtr ExtinguishSound;", + "UFUNCTION(NetMulticast, Unreliable)", + "void MulticastPlayFireEventSound(bool bIgnited);", + ], + FIRE_CPP: [ + "#include \"Components/AudioComponent.h\"", + "FireLoopAudioComponent = CreateDefaultSubobject", + "FireEventAudioComponent = CreateDefaultSubobject", + "FireLoopAudioComponent->bAllowSpatialization = true", + "FireEventAudioComponent->bAllowSpatialization = true", + "AAgrarianCampfire::MulticastPlayFireEventSound_Implementation", + "MulticastPlayFireEventSound(bLit);", + "FireLoopAudioComponent->Play();", + "FireLoopAudioComponent->Stop();", + ], + TDD: [ + "Campfire audio is split between a persistent spatialized loop", + "`FireLoopSound`", + "`IgniteSound`", + "`ExtinguishSound`", + ], + ROADMAP: [ + "[x] Add fire 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: fire sound hooks follow authoritative campfire state.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianCampfire.cpp b/Source/AgrarianGame/AgrarianCampfire.cpp index b2a394f..e9043e0 100644 --- a/Source/AgrarianGame/AgrarianCampfire.cpp +++ b/Source/AgrarianGame/AgrarianCampfire.cpp @@ -7,6 +7,7 @@ #include "AgrarianPersistentActorComponent.h" #include "AgrarianSurvivalComponent.h" #include "Particles/ParticleSystemComponent.h" +#include "Components/AudioComponent.h" #include "Components/PointLightComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" @@ -108,6 +109,16 @@ AAgrarianCampfire::AAgrarianCampfire() SmokeEffect->SetRelativeLocation(FVector(0.0f, 0.0f, 80.0f)); SmokeEffect->SetVisibility(false); + FireLoopAudioComponent = CreateDefaultSubobject(TEXT("FireLoopAudioComponent")); + FireLoopAudioComponent->SetupAttachment(RootComponent); + FireLoopAudioComponent->bAutoActivate = false; + FireLoopAudioComponent->bAllowSpatialization = true; + + FireEventAudioComponent = CreateDefaultSubobject(TEXT("FireEventAudioComponent")); + FireEventAudioComponent->SetupAttachment(RootComponent); + FireEventAudioComponent->bAutoActivate = false; + FireEventAudioComponent->bAllowSpatialization = true; + PersistentActorComponent = CreateDefaultSubobject(TEXT("PersistentActorComponent")); PersistentActorComponent->ActorTypeId = TEXT("campfire"); } @@ -281,6 +292,23 @@ void AAgrarianCampfire::OnRep_FireState() UpdateVisualState(); } +void AAgrarianCampfire::MulticastPlayFireEventSound_Implementation(bool bIgnited) +{ + if (GetNetMode() == NM_DedicatedServer || !FireEventAudioComponent) + { + return; + } + + USoundBase* EventSound = bIgnited ? IgniteSound : ExtinguishSound; + if (!EventSound) + { + return; + } + + FireEventAudioComponent->SetSound(EventSound); + FireEventAudioComponent->Play(); +} + EAgrarianWeatherType AAgrarianCampfire::GetCurrentWeather() const { if (const UWorld* World = GetWorld()) @@ -296,11 +324,17 @@ EAgrarianWeatherType AAgrarianCampfire::GetCurrentWeather() const void AAgrarianCampfire::SetLit(bool bNewLit) { + const bool bChanged = bLit != bNewLit; if (bLit != bNewLit) { bLit = bNewLit; } + if (HasAuthority() && bChanged) + { + MulticastPlayFireEventSound(bLit); + } + UpdateVisualState(); } @@ -323,6 +357,25 @@ void AAgrarianCampfire::UpdateVisualState() SmokeEffect->DeactivateSystem(); } } + + if (FireLoopAudioComponent) + { + if (bLit && FireLoopSound) + { + if (FireLoopAudioComponent->Sound != FireLoopSound) + { + FireLoopAudioComponent->SetSound(FireLoopSound); + } + if (!FireLoopAudioComponent->IsPlaying()) + { + FireLoopAudioComponent->Play(); + } + } + else if (FireLoopAudioComponent->IsPlaying()) + { + FireLoopAudioComponent->Stop(); + } + } } void AAgrarianCampfire::WarmNearbyCharacters(float DeltaSeconds) diff --git a/Source/AgrarianGame/AgrarianCampfire.h b/Source/AgrarianGame/AgrarianCampfire.h index a620a84..d8cf808 100644 --- a/Source/AgrarianGame/AgrarianCampfire.h +++ b/Source/AgrarianGame/AgrarianCampfire.h @@ -12,6 +12,8 @@ class UPointLightComponent; class UParticleSystemComponent; class UAgrarianPersistentActorComponent; +class UAudioComponent; +class USoundBase; class UStaticMeshComponent; UCLASS(Blueprintable) @@ -49,6 +51,12 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Effects") TObjectPtr SmokeEffect; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Audio") + TObjectPtr FireLoopAudioComponent; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Audio") + TObjectPtr FireEventAudioComponent; + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Agrarian|Fire|Persistence") TObjectPtr PersistentActorComponent; @@ -85,6 +93,15 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather") bool bWetWeatherCanExtinguish = true; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Audio") + TObjectPtr FireLoopSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Audio") + TObjectPtr IgniteSound; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Audio") + TObjectPtr ExtinguishSound; + 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; @@ -113,6 +130,9 @@ protected: UFUNCTION() void OnRep_FireState(); + UFUNCTION(NetMulticast, Unreliable) + void MulticastPlayFireEventSound(bool bIgnited); + EAgrarianWeatherType GetCurrentWeather() const; void SetLit(bool bNewLit); void UpdateVisualState();