Add fire suppression hooks

This commit is contained in:
2026-05-19 12:23:56 -07:00
parent b4e9cced85
commit 6b43a234cf
5 changed files with 169 additions and 1 deletions
+1 -1
View File
@@ -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.
+8
View File
@@ -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.
+66
View File
@@ -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()
+66
View File
@@ -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;
}
+28
View File
@@ -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);
};