diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index 3fdb74e..b813938 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -598,7 +598,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe lit state, fuel seconds, and cooking placeholder progress using the shared world-actor persistence path. - [x] Connect fire to body temperature. -- [ ] Connect rain/weather to fire behavior. +- [x] Connect rain/weather to fire behavior. Campfires now read replicated + game-state weather, burn fuel faster in rain and storms, and deterministically + extinguish when wet weather pushes remaining fuel below the low-fuel threshold. ## 0.1.I Shelter Building diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index 5dc7e1e..d74416c 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -308,6 +308,12 @@ to write lit state, remaining fuel, cooking enabled state, required cook time, and cooking progress into numeric save state, then restores those values before reapplying the fire visual state on load. +Campfires now read the replicated `AAgrarianGameState::Weather` value while +burning. Rain and storms increase fuel drain through tunable multipliers, and +wet weather can deterministically extinguish a low-fuel fire so weather affects +fire reliability without adding random outcomes to save/load or multiplayer +state. + The first real-weather adapter is `UAgrarianWeatherProviderSubsystem`. It uses Open-Meteo forecast requests keyed by tile center latitude/longitude, parses the current temperature, daily low/high, precipitation, wind, humidity, cloud cover, diff --git a/Scripts/verify_fire_weather_behavior.py b/Scripts/verify_fire_weather_behavior.py new file mode 100644 index 0000000..fec0f77 --- /dev/null +++ b/Scripts/verify_fire_weather_behavior.py @@ -0,0 +1,52 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +REQUIRED = { + ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.h": [ + "#include \"AgrarianTypes.h\"", + "float RainFuelDrainMultiplier = 1.5f;", + "float StormFuelDrainMultiplier = 2.5f;", + "float WetWeatherExtinguishFuelThresholdSeconds = 6.0f;", + "bool bWetWeatherCanExtinguish = true;", + "float GetWeatherFuelDrainMultiplier() const;", + "bool IsWetWeatherActive() const;", + "EAgrarianWeatherType GetCurrentWeather() const;", + ], + ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp": [ + "#include \"AgrarianGameState.h\"", + "DeltaSeconds * GetWeatherFuelDrainMultiplier()", + "bWetWeatherCanExtinguish && IsWetWeatherActive()", + "WetWeatherExtinguishFuelThresholdSeconds", + "Extinguish();", + "case EAgrarianWeatherType::Rain:", + "case EAgrarianWeatherType::Storm:", + "World->GetGameState()", + "return EAgrarianWeatherType::Clear;", + ], + ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md": [ + "- [x] Connect rain/weather to fire behavior.", + ], + ROOT / "Docs" / "TechnicalDesignDocument.md": [ + "Campfires now read the replicated `AAgrarianGameState::Weather` value", + ], +} + + +def main(): + 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("Campfire weather behavior verification failed:\n" + "\n".join(missing)) + + print("PASS: campfire weather behavior is implemented and documented.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianCampfire.cpp b/Source/AgrarianGame/AgrarianCampfire.cpp index 806223d..7ba99e5 100644 --- a/Source/AgrarianGame/AgrarianCampfire.cpp +++ b/Source/AgrarianGame/AgrarianCampfire.cpp @@ -2,6 +2,7 @@ #include "AgrarianCampfire.h" #include "AgrarianGameCharacter.h" +#include "AgrarianGameState.h" #include "AgrarianInventoryComponent.h" #include "AgrarianPersistentActorComponent.h" #include "AgrarianSurvivalComponent.h" @@ -41,10 +42,10 @@ void AAgrarianCampfire::Tick(float DeltaSeconds) if (HasAuthority() && bLit) { - FuelSeconds = FMath::Max(0.0f, FuelSeconds - DeltaSeconds); - if (FuelSeconds <= 0.0f) + FuelSeconds = FMath::Max(0.0f, FuelSeconds - (DeltaSeconds * GetWeatherFuelDrainMultiplier())); + if (FuelSeconds <= 0.0f || (bWetWeatherCanExtinguish && IsWetWeatherActive() && FuelSeconds <= WetWeatherExtinguishFuelThresholdSeconds)) { - SetLit(false); + Extinguish(); } if (CanCook()) @@ -180,11 +181,43 @@ float AAgrarianCampfire::GetCookingProgressRatio() const return FMath::Clamp(CookingProgressSeconds / CookingSecondsRequired, 0.0f, 1.0f); } +float AAgrarianCampfire::GetWeatherFuelDrainMultiplier() const +{ + switch (GetCurrentWeather()) + { + case EAgrarianWeatherType::Rain: + return FMath::Max(1.0f, RainFuelDrainMultiplier); + case EAgrarianWeatherType::Storm: + return FMath::Max(1.0f, StormFuelDrainMultiplier); + default: + return 1.0f; + } +} + +bool AAgrarianCampfire::IsWetWeatherActive() const +{ + const EAgrarianWeatherType CurrentWeather = GetCurrentWeather(); + return CurrentWeather == EAgrarianWeatherType::Rain || CurrentWeather == EAgrarianWeatherType::Storm; +} + void AAgrarianCampfire::OnRep_FireState() { UpdateVisualState(); } +EAgrarianWeatherType AAgrarianCampfire::GetCurrentWeather() const +{ + if (const UWorld* World = GetWorld()) + { + if (const AAgrarianGameState* GameState = World->GetGameState()) + { + return GameState->Weather; + } + } + + return EAgrarianWeatherType::Clear; +} + void AAgrarianCampfire::SetLit(bool bNewLit) { if (bLit != bNewLit) diff --git a/Source/AgrarianGame/AgrarianCampfire.h b/Source/AgrarianGame/AgrarianCampfire.h index 7145fcf..f44bd5d 100644 --- a/Source/AgrarianGame/AgrarianCampfire.h +++ b/Source/AgrarianGame/AgrarianCampfire.h @@ -6,6 +6,7 @@ #include "GameFramework/Actor.h" #include "AgrarianInteractable.h" #include "AgrarianPersistentStateProvider.h" +#include "AgrarianTypes.h" #include "AgrarianCampfire.generated.h" class UPointLightComponent; @@ -57,6 +58,18 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Cooking", meta = (ClampMin = "0")) float CookingProgressSeconds = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1")) + float RainFuelDrainMultiplier = 1.5f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1")) + float StormFuelDrainMultiplier = 2.5f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "0")) + float WetWeatherExtinguishFuelThresholdSeconds = 6.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather") + bool bWetWeatherCanExtinguish = true; + 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; @@ -75,10 +88,17 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Cooking") float GetCookingProgressRatio() const; + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Weather") + float GetWeatherFuelDrainMultiplier() const; + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Weather") + bool IsWetWeatherActive() const; + protected: UFUNCTION() void OnRep_FireState(); + EAgrarianWeatherType GetCurrentWeather() const; void SetLit(bool bNewLit); void UpdateVisualState(); void WarmNearbyCharacters(float DeltaSeconds);