diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index aea7c09..667335d 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -842,7 +842,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects. Added campfire structure ignition risk for nearby primitive shelters and flammable wood/fiber resource nodes, with containment, burn-duration, weather/wind, and fire-risk modifiers before a replicated structure ignition flag is set. - [x] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks. Added replicated grass, forest, and structure fire intensities plus active spread radius that grow only on the server from nearby fuel, ignition distance, wind/weather, and a suppression-pressure hook for later rain, carried water, dirt/sand, firebreaks, and tools. - [x] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous. Updated lit campfire interaction to maintain the fire, added watch, clear-area, and contain-fire hooks, and made maintenance reduce campfire, vegetation, forest, and structure ignition risks while extinguishing resets active risk state. -- [ ] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools. +- [x] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools. Added shared server-side suppression hooks plus water, dirt/sand, firebreak, and tool wrappers that raise suppression pressure, reduce ignition risks, reduce active fire intensity, shrink spread radius, and let rain/water drain fuel. - [ ] Persist active grass, forest, and structure fires across save/load without corrupting world state. - [ ] Add QA coverage for safe campfires, unsafe campfires, vegetation spread, shelter ignition, suppression, and save/load recovery. - [ ] Add weather sounds. diff --git a/Docs/TechnicalDesignDocument.md b/Docs/TechnicalDesignDocument.md index fe70f21..72853ee 100644 --- a/Docs/TechnicalDesignDocument.md +++ b/Docs/TechnicalDesignDocument.md @@ -376,6 +376,14 @@ Native hooks also let future UI/actions explicitly clear the fire area or contain the fire. Watched, cleared, contained, and extinguished fires reduce risk, while neglected fires continue accumulating ignition and spread pressure. +Fire suppression uses explicit server-side hooks on `AAgrarianCampfire`. +`ApplyFireSuppression` is the shared entry point, with named wrappers for water, +dirt/sand, cleared firebreaks, and future tools. Suppression raises +`FireSuppressionPressure`, reduces ignition risk, reduces active fire intensity, +shrinks spread radius, and lets water/rain also drain fuel. Wet weather +contributes passive suppression pressure so rain and storms naturally slow +dangerous fires before later UI and inventory actions call the same hooks. + 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_suppression_hooks.py b/Scripts/verify_fire_suppression_hooks.py new file mode 100644 index 0000000..ca04346 --- /dev/null +++ b/Scripts/verify_fire_suppression_hooks.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Verify fire suppression hooks cover rain, water, dirt/sand, firebreaks, and tools.""" + +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: [ + "WaterSuppressionStrength", + "DirtSandSuppressionStrength", + "FirebreakSuppressionStrength", + "ToolSuppressionStrength", + "void ApplyFireSuppression(float SuppressionAmount, FName SuppressionSource);", + "void ApplyWaterSuppression();", + "void ApplyDirtSandSuppression();", + "void ApplyFirebreakSuppression();", + "void ApplyToolSuppression();", + "void ReduceActiveFireIntensity(float Amount);", + ], + FIRE_CPP: [ + "AAgrarianCampfire::ApplyFireSuppression", + "ApplyFireSuppression(WaterSuppressionStrength, TEXT(\"water\"))", + "ApplyFireSuppression(DirtSandSuppressionStrength, TEXT(\"dirt_sand\"))", + "ApplyFireSuppression(FirebreakSuppressionStrength, TEXT(\"firebreak\"))", + "ApplyFireSuppression(ToolSuppressionStrength, TEXT(\"tool\"))", + "FireSuppressionPressure = FMath::Clamp", + "ReduceFireRisks(SafeSuppressionAmount);", + "ReduceActiveFireIntensity(SafeSuppressionAmount);", + "SuppressionSource == TEXT(\"rain\") || SuppressionSource == TEXT(\"water\")", + "AAgrarianCampfire::ReduceActiveFireIntensity", + "IsWetWeatherActive()", + ], + TDD: [ + "Fire suppression uses explicit server-side hooks", + "`ApplyFireSuppression`", + "water", + "dirt/sand", + "firebreaks", + "future tools", + ], + ROADMAP: [ + "[x] Add fire suppression hooks", + ], +} + + +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 suppression hooks are implemented.") + + +if __name__ == "__main__": + main() diff --git a/Source/AgrarianGame/AgrarianCampfire.cpp b/Source/AgrarianGame/AgrarianCampfire.cpp index 839cb65..68b9b0c 100644 --- a/Source/AgrarianGame/AgrarianCampfire.cpp +++ b/Source/AgrarianGame/AgrarianCampfire.cpp @@ -439,6 +439,55 @@ void AAgrarianCampfire::ContainFire() MaintainFire(false, true); } +void AAgrarianCampfire::ApplyFireSuppression(float SuppressionAmount, FName SuppressionSource) +{ + if (!HasAuthority()) + { + return; + } + + const float SafeSuppressionAmount = FMath::Max(0.0f, SuppressionAmount); + if (SafeSuppressionAmount <= 0.0f) + { + return; + } + + FireSuppressionPressure = FMath::Clamp(FireSuppressionPressure + (SafeSuppressionAmount / 100.0f), 0.0f, 1.0f); + ReduceFireRisks(SafeSuppressionAmount); + ReduceActiveFireIntensity(SafeSuppressionAmount); + + if (SuppressionSource == TEXT("rain") || SuppressionSource == TEXT("water")) + { + FuelSeconds = FMath::Max(0.0f, FuelSeconds - SafeSuppressionAmount); + } + + const float TotalActiveFire = GrassFireIntensity + ForestFireIntensity + StructureFireIntensity; + if (FuelSeconds <= 0.0f && TotalActiveFire <= 1.0f) + { + Extinguish(); + } +} + +void AAgrarianCampfire::ApplyWaterSuppression() +{ + ApplyFireSuppression(WaterSuppressionStrength, TEXT("water")); +} + +void AAgrarianCampfire::ApplyDirtSandSuppression() +{ + ApplyFireSuppression(DirtSandSuppressionStrength, TEXT("dirt_sand")); +} + +void AAgrarianCampfire::ApplyFirebreakSuppression() +{ + ApplyFireSuppression(FirebreakSuppressionStrength, TEXT("firebreak")); +} + +void AAgrarianCampfire::ApplyToolSuppression() +{ + ApplyFireSuppression(ToolSuppressionStrength, TEXT("tool")); +} + float AAgrarianCampfire::GetFireRiskRatio() const { return FMath::Clamp(FireRiskScore / 100.0f, 0.0f, 1.0f); @@ -602,6 +651,11 @@ void AAgrarianCampfire::UpdateFireRisk(float DeltaSeconds) LitDurationSeconds += DeltaSeconds; SecondsSinceMaintenance += DeltaSeconds; + if (IsWetWeatherActive()) + { + const float RainSuppression = GetCurrentWeather() == EAgrarianWeatherType::Storm ? 0.2f : 0.1f; + FireSuppressionPressure = FMath::Clamp(FireSuppressionPressure + (RainSuppression * DeltaSeconds), 0.0f, 1.0f); + } const float RiskGrowth = GetFireRiskGrowthPerSecond(); FireRiskScore = FMath::Clamp(FireRiskScore + (RiskGrowth * DeltaSeconds), 0.0f, 100.0f); @@ -863,3 +917,15 @@ void AAgrarianCampfire::ReduceFireRisks(float Amount) ForestIgnitionRiskScore = FMath::Clamp(ForestIgnitionRiskScore - (SafeAmount * 0.5f), 0.0f, 100.0f); StructureIgnitionRiskScore = FMath::Clamp(StructureIgnitionRiskScore - (SafeAmount * 0.75f), 0.0f, 100.0f); } + +void AAgrarianCampfire::ReduceActiveFireIntensity(float Amount) +{ + const float SafeAmount = FMath::Max(0.0f, Amount); + GrassFireIntensity = FMath::Clamp(GrassFireIntensity - SafeAmount, 0.0f, 100.0f); + ForestFireIntensity = FMath::Clamp(ForestFireIntensity - (SafeAmount * 0.75f), 0.0f, 100.0f); + StructureFireIntensity = FMath::Clamp(StructureFireIntensity - (SafeAmount * 0.85f), 0.0f, 100.0f); + const float TotalIntensity = GrassFireIntensity + ForestFireIntensity + StructureFireIntensity; + ActiveFireSpreadRadius = TotalIntensity > 0.0f + ? FMath::Clamp(BaseFireSpreadRadius + (TotalIntensity * 12.0f), 0.0f, MaxFireSpreadRadius) + : 0.0f; +} diff --git a/Source/AgrarianGame/AgrarianCampfire.h b/Source/AgrarianGame/AgrarianCampfire.h index 0d50662..5a08aff 100644 --- a/Source/AgrarianGame/AgrarianCampfire.h +++ b/Source/AgrarianGame/AgrarianCampfire.h @@ -183,6 +183,18 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0")) float FireSuppressionPressure = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Suppression", meta = (ClampMin = "0")) + float WaterSuppressionStrength = 35.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Suppression", meta = (ClampMin = "0")) + float DirtSandSuppressionStrength = 24.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Suppression", meta = (ClampMin = "0")) + float FirebreakSuppressionStrength = 30.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Suppression", meta = (ClampMin = "0")) + float ToolSuppressionStrength = 18.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1")) float RainFuelDrainMultiplier = 1.5f; @@ -228,6 +240,21 @@ public: UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire|Risk") void ContainFire(); + UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire|Suppression") + void ApplyFireSuppression(float SuppressionAmount, FName SuppressionSource); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire|Suppression") + void ApplyWaterSuppression(); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire|Suppression") + void ApplyDirtSandSuppression(); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire|Suppression") + void ApplyFirebreakSuppression(); + + UFUNCTION(BlueprintCallable, Category = "Agrarian|Fire|Suppression") + void ApplyToolSuppression(); + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Agrarian|Fire|Risk") float GetFireRiskRatio() const; @@ -265,4 +292,5 @@ protected: float GetFireSpreadWeatherMultiplier() const; float GetActiveBurningFuelScore() const; void ReduceFireRisks(float Amount); + void ReduceActiveFireIntensity(float Amount); };