Add structure ignition risk checks

This commit is contained in:
2026-05-19 12:16:52 -07:00
parent dca9d01f68
commit 168cd0e61f
5 changed files with 169 additions and 1 deletions
+1 -1
View File
@@ -839,7 +839,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [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.
- [x] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. Added server-side campfire risk state for lit duration, seconds since maintenance, cleared area, containment, high fuel, wet weather mitigation, and a replicated `FireRiskScore` that later ignition/spread systems can consume.
- [x] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration. Added foliage fuel counting, campfire vegetation ignition risk scores, grass/brush and forest ignition flags, and weather/wind/burn-duration modifiers so unsafe fire placement near dry fuel can now become a server-authoritative ignition risk.
- [ ] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects.
- [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.
- [ ] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks.
- [ ] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous.
- [ ] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools.
+8
View File
@@ -353,6 +353,14 @@ duration, wind speed, current weather, cleared-area maintenance, and the
campfire's current risk ratio. Reaching the threshold marks the relevant
ignition flag, while later spread rules decide how active fires propagate.
Structure ignition risk uses the same server-authoritative campfire risk model.
Open fires check nearby primitive shelters plus flammable wood/fiber resource
nodes as MVP stand-ins for wood piles, flammable crafting stations, and future
settlement objects. Contained fires skip this structure check; otherwise
structure risk accumulates with distance-based fuel presence, burn duration,
weather/wind, and the current fire-risk ratio before setting a replicated
structure ignition flag.
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.
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Verify shelter/structure ignition risk is tracked from nearby flammable actors."""
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: [
"StructureIgnitionRiskScore",
"bStructureIgnited",
"StructureIgnitionCheckRadius",
"StructureIgnitionFuelScoreThreshold",
"StructureIgnitionRiskPerSecond",
"UpdateStructureIgnitionRisk",
"GetStructureFuelScoreNearFire",
],
FIRE_CPP: [
"#include \"AgrarianResourceNode.h\"",
"#include \"AgrarianShelterActor.h\"",
"DOREPLIFETIME(AAgrarianCampfire, StructureIgnitionRiskScore)",
"DOREPLIFETIME(AAgrarianCampfire, bStructureIgnited)",
"UpdateStructureIgnitionRisk(DeltaSeconds);",
"AAgrarianCampfire::UpdateStructureIgnitionRisk",
"AAgrarianCampfire::GetStructureFuelScoreNearFire",
"AAgrarianShelterActor::StaticClass()",
"AAgrarianResourceNode::StaticClass()",
"YieldId == TEXT(\"wood\") || YieldId == TEXT(\"fiber\")",
"bFireContained",
"bStructureIgnited = StructureIgnitionRiskScore >= 100.0f",
"structure_ignition_risk_score",
"structure_ignited",
],
TDD: [
"Structure ignition risk uses the same server-authoritative campfire risk model",
"primitive shelters",
"flammable wood/fiber resource",
"structure ignition flag",
],
ROADMAP: [
"[x] Add shelter/structure ignition risk",
],
}
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: structure ignition risk checks are wired to nearby flammable actors.")
if __name__ == "__main__":
main()
+79
View File
@@ -6,6 +6,8 @@
#include "AgrarianGameState.h"
#include "AgrarianInventoryComponent.h"
#include "AgrarianPersistentActorComponent.h"
#include "AgrarianResourceNode.h"
#include "AgrarianShelterActor.h"
#include "AgrarianSurvivalComponent.h"
#include "Particles/ParticleSystemComponent.h"
#include "Components/AudioComponent.h"
@@ -143,6 +145,7 @@ void AAgrarianCampfire::Tick(float DeltaSeconds)
UpdateFireRisk(DeltaSeconds);
UpdateVegetationIgnitionRisk(DeltaSeconds);
UpdateStructureIgnitionRisk(DeltaSeconds);
WarmNearbyCharacters(DeltaSeconds);
}
}
@@ -164,6 +167,8 @@ void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Ou
DOREPLIFETIME(AAgrarianCampfire, ForestIgnitionRiskScore);
DOREPLIFETIME(AAgrarianCampfire, bGrassOrBrushIgnited);
DOREPLIFETIME(AAgrarianCampfire, bForestFuelIgnited);
DOREPLIFETIME(AAgrarianCampfire, StructureIgnitionRiskScore);
DOREPLIFETIME(AAgrarianCampfire, bStructureIgnited);
}
FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const
@@ -211,6 +216,8 @@ void AAgrarianCampfire::CapturePersistentState_Implementation(UAgrarianPersisten
PersistentComponent->NumberState.Add(TEXT("forest_ignition_risk_score"), ForestIgnitionRiskScore);
PersistentComponent->NumberState.Add(TEXT("grass_or_brush_ignited"), bGrassOrBrushIgnited ? 1.0f : 0.0f);
PersistentComponent->NumberState.Add(TEXT("forest_fuel_ignited"), bForestFuelIgnited ? 1.0f : 0.0f);
PersistentComponent->NumberState.Add(TEXT("structure_ignition_risk_score"), StructureIgnitionRiskScore);
PersistentComponent->NumberState.Add(TEXT("structure_ignited"), bStructureIgnited ? 1.0f : 0.0f);
}
void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent)
@@ -234,6 +241,8 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
const float* SavedForestIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("forest_ignition_risk_score"));
const float* SavedGrassIgnited = PersistentComponent->NumberState.Find(TEXT("grass_or_brush_ignited"));
const float* SavedForestIgnited = PersistentComponent->NumberState.Find(TEXT("forest_fuel_ignited"));
const float* SavedStructureIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("structure_ignition_risk_score"));
const float* SavedStructureIgnited = PersistentComponent->NumberState.Find(TEXT("structure_ignited"));
if (SavedFuelSeconds)
{
@@ -300,6 +309,16 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
bForestFuelIgnited = *SavedForestIgnited > 0.5f;
}
if (SavedStructureIgnitionRisk)
{
StructureIgnitionRiskScore = FMath::Clamp(*SavedStructureIgnitionRisk, 0.0f, 100.0f);
}
if (SavedStructureIgnited)
{
bStructureIgnited = *SavedStructureIgnited > 0.5f;
}
SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f);
}
@@ -327,6 +346,7 @@ void AAgrarianCampfire::Extinguish()
FireRiskScore = 0.0f;
GrassIgnitionRiskScore = 0.0f;
ForestIgnitionRiskScore = 0.0f;
StructureIgnitionRiskScore = 0.0f;
LitDurationSeconds = 0.0f;
SecondsSinceMaintenance = 0.0f;
SetLit(false);
@@ -649,3 +669,62 @@ float AAgrarianCampfire::GetVegetationIgnitionWeatherMultiplier() const
return FMath::Max(0.0f, Multiplier);
}
void AAgrarianCampfire::UpdateStructureIgnitionRisk(float DeltaSeconds)
{
if (!HasAuthority() || !bLit || bFireContained || bStructureIgnited)
{
return;
}
const float StructureFuelScore = GetStructureFuelScoreNearFire();
if (StructureFuelScore < StructureIgnitionFuelScoreThreshold)
{
return;
}
const float BurnDurationMultiplier = FMath::GetMappedRangeValueClamped(
FVector2D(15.0f, 240.0f),
FVector2D(0.45f, 1.5f),
LitDurationSeconds);
const float RiskMultiplier = BurnDurationMultiplier * GetVegetationIgnitionWeatherMultiplier() * FMath::Max(0.0f, GetFireRiskRatio());
StructureIgnitionRiskScore = FMath::Clamp(
StructureIgnitionRiskScore + (StructureIgnitionRiskPerSecond * StructureFuelScore * RiskMultiplier * DeltaSeconds),
0.0f,
100.0f);
bStructureIgnited = StructureIgnitionRiskScore >= 100.0f;
}
float AAgrarianCampfire::GetStructureFuelScoreNearFire() const
{
float StructureFuelScore = 0.0f;
TArray<AActor*> ShelterActors;
UGameplayStatics::GetAllActorsOfClass(this, AAgrarianShelterActor::StaticClass(), ShelterActors);
for (const AActor* Actor : ShelterActors)
{
if (Actor && Actor != this && FVector::DistSquared2D(Actor->GetActorLocation(), GetActorLocation()) <= FMath::Square(StructureIgnitionCheckRadius))
{
StructureFuelScore += 4.0f;
}
}
TArray<AActor*> ResourceActors;
UGameplayStatics::GetAllActorsOfClass(this, AAgrarianResourceNode::StaticClass(), ResourceActors);
for (const AActor* Actor : ResourceActors)
{
const AAgrarianResourceNode* ResourceNode = Cast<AAgrarianResourceNode>(Actor);
if (!ResourceNode || FVector::DistSquared2D(ResourceNode->GetActorLocation(), GetActorLocation()) > FMath::Square(StructureIgnitionCheckRadius))
{
continue;
}
const FName YieldId = ResourceNode->YieldItem.ItemId;
if (YieldId == TEXT("wood") || YieldId == TEXT("fiber"))
{
StructureFuelScore += 1.5f;
}
}
return StructureFuelScore;
}
+17
View File
@@ -135,6 +135,21 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
float VegetationIgnitionRiskPerSecond = 0.035f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Structure", meta = (ClampMin = "0", ClampMax = "100"))
float StructureIgnitionRiskScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Structure")
bool bStructureIgnited = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Structure", meta = (ClampMin = "0"))
float StructureIgnitionCheckRadius = 650.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Structure", meta = (ClampMin = "0"))
float StructureIgnitionFuelScoreThreshold = 2.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Structure", meta = (ClampMin = "0"))
float StructureIgnitionRiskPerSecond = 0.08f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1"))
float RainFuelDrainMultiplier = 1.5f;
@@ -202,4 +217,6 @@ protected:
void UpdateVegetationIgnitionRisk(float DeltaSeconds);
float GetVegetationFuelScoreNearFire(float& OutGrassFuelScore, float& OutForestFuelScore) const;
float GetVegetationIgnitionWeatherMultiplier() const;
void UpdateStructureIgnitionRisk(float DeltaSeconds);
float GetStructureFuelScoreNearFire() const;
};