Add vegetation ignition risk checks

This commit is contained in:
2026-05-19 12:14:16 -07:00
parent 14cd8234e6
commit dca9d01f68
7 changed files with 311 additions and 1 deletions
+132
View File
@@ -1,6 +1,7 @@
// Copyright Pacificao. All Rights Reserved.
#include "AgrarianCampfire.h"
#include "AgrarianFoliagePatch.h"
#include "AgrarianGameCharacter.h"
#include "AgrarianGameState.h"
#include "AgrarianInventoryComponent.h"
@@ -141,6 +142,7 @@ void AAgrarianCampfire::Tick(float DeltaSeconds)
}
UpdateFireRisk(DeltaSeconds);
UpdateVegetationIgnitionRisk(DeltaSeconds);
WarmNearbyCharacters(DeltaSeconds);
}
}
@@ -158,6 +160,10 @@ void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Ou
DOREPLIFETIME(AAgrarianCampfire, SecondsSinceMaintenance);
DOREPLIFETIME(AAgrarianCampfire, bFireAreaCleared);
DOREPLIFETIME(AAgrarianCampfire, bFireContained);
DOREPLIFETIME(AAgrarianCampfire, GrassIgnitionRiskScore);
DOREPLIFETIME(AAgrarianCampfire, ForestIgnitionRiskScore);
DOREPLIFETIME(AAgrarianCampfire, bGrassOrBrushIgnited);
DOREPLIFETIME(AAgrarianCampfire, bForestFuelIgnited);
}
FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const
@@ -201,6 +207,10 @@ void AAgrarianCampfire::CapturePersistentState_Implementation(UAgrarianPersisten
PersistentComponent->NumberState.Add(TEXT("seconds_since_maintenance"), SecondsSinceMaintenance);
PersistentComponent->NumberState.Add(TEXT("fire_area_cleared"), bFireAreaCleared ? 1.0f : 0.0f);
PersistentComponent->NumberState.Add(TEXT("fire_contained"), bFireContained ? 1.0f : 0.0f);
PersistentComponent->NumberState.Add(TEXT("grass_ignition_risk_score"), GrassIgnitionRiskScore);
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);
}
void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent)
@@ -220,6 +230,10 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
const float* SavedSecondsSinceMaintenance = PersistentComponent->NumberState.Find(TEXT("seconds_since_maintenance"));
const float* SavedAreaCleared = PersistentComponent->NumberState.Find(TEXT("fire_area_cleared"));
const float* SavedContained = PersistentComponent->NumberState.Find(TEXT("fire_contained"));
const float* SavedGrassIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("grass_ignition_risk_score"));
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"));
if (SavedFuelSeconds)
{
@@ -266,6 +280,26 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
bFireContained = *SavedContained > 0.5f;
}
if (SavedGrassIgnitionRisk)
{
GrassIgnitionRiskScore = FMath::Clamp(*SavedGrassIgnitionRisk, 0.0f, 100.0f);
}
if (SavedForestIgnitionRisk)
{
ForestIgnitionRiskScore = FMath::Clamp(*SavedForestIgnitionRisk, 0.0f, 100.0f);
}
if (SavedGrassIgnited)
{
bGrassOrBrushIgnited = *SavedGrassIgnited > 0.5f;
}
if (SavedForestIgnited)
{
bForestFuelIgnited = *SavedForestIgnited > 0.5f;
}
SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f);
}
@@ -291,6 +325,8 @@ void AAgrarianCampfire::Extinguish()
{
FuelSeconds = 0.0f;
FireRiskScore = 0.0f;
GrassIgnitionRiskScore = 0.0f;
ForestIgnitionRiskScore = 0.0f;
LitDurationSeconds = 0.0f;
SecondsSinceMaintenance = 0.0f;
SetLit(false);
@@ -517,3 +553,99 @@ float AAgrarianCampfire::GetFireRiskGrowthPerSecond() const
return FMath::Max(0.0f, RiskGrowth);
}
void AAgrarianCampfire::UpdateVegetationIgnitionRisk(float DeltaSeconds)
{
if (!HasAuthority() || !bLit || bFireAreaCleared)
{
return;
}
float GrassFuelScore = 0.0f;
float ForestFuelScore = 0.0f;
const float TotalFuelScore = GetVegetationFuelScoreNearFire(GrassFuelScore, ForestFuelScore);
if (TotalFuelScore < VegetationIgnitionFuelScoreThreshold)
{
return;
}
const float BurnDurationMultiplier = FMath::GetMappedRangeValueClamped(
FVector2D(30.0f, 300.0f),
FVector2D(0.35f, 1.4f),
LitDurationSeconds);
const float RiskMultiplier = GetVegetationIgnitionWeatherMultiplier() * BurnDurationMultiplier * FMath::Max(0.0f, GetFireRiskRatio());
const float BaseRisk = VegetationIgnitionRiskPerSecond * DeltaSeconds * RiskMultiplier;
if (GrassFuelScore > 0.0f && !bGrassOrBrushIgnited)
{
GrassIgnitionRiskScore = FMath::Clamp(GrassIgnitionRiskScore + (BaseRisk * GrassFuelScore), 0.0f, 100.0f);
bGrassOrBrushIgnited = GrassIgnitionRiskScore >= 100.0f;
}
if (ForestFuelScore > 0.0f && !bForestFuelIgnited)
{
ForestIgnitionRiskScore = FMath::Clamp(ForestIgnitionRiskScore + (BaseRisk * ForestFuelScore * 0.6f), 0.0f, 100.0f);
bForestFuelIgnited = ForestIgnitionRiskScore >= 100.0f;
}
}
float AAgrarianCampfire::GetVegetationFuelScoreNearFire(float& OutGrassFuelScore, float& OutForestFuelScore) const
{
OutGrassFuelScore = 0.0f;
OutForestFuelScore = 0.0f;
TArray<AActor*> FoliageActors;
UGameplayStatics::GetAllActorsOfClass(this, AAgrarianFoliagePatch::StaticClass(), FoliageActors);
for (AActor* Actor : FoliageActors)
{
const AAgrarianFoliagePatch* FoliagePatch = Cast<AAgrarianFoliagePatch>(Actor);
if (!FoliagePatch)
{
continue;
}
int32 GrassCount = 0;
int32 ShrubCount = 0;
int32 TreeCount = 0;
FoliagePatch->GetFuelCountsNearLocation(GetActorLocation(), VegetationIgnitionCheckRadius, GrassCount, ShrubCount, TreeCount);
OutGrassFuelScore += static_cast<float>(GrassCount) + (static_cast<float>(ShrubCount) * 1.5f);
OutForestFuelScore += static_cast<float>(TreeCount) * 3.0f;
}
return OutGrassFuelScore + OutForestFuelScore;
}
float AAgrarianCampfire::GetVegetationIgnitionWeatherMultiplier() const
{
float Multiplier = 1.0f;
if (const UWorld* World = GetWorld())
{
if (const AAgrarianGameState* GameState = World->GetGameState<AAgrarianGameState>())
{
if (GameState->ActiveWeatherInputs.bHasProviderData)
{
Multiplier *= FMath::GetMappedRangeValueClamped(
FVector2D(0.0f, 55.0f),
FVector2D(0.85f, 1.65f),
GameState->ActiveWeatherInputs.WindSpeedKmh);
}
switch (GameState->Weather)
{
case EAgrarianWeatherType::Rain:
Multiplier *= 0.2f;
break;
case EAgrarianWeatherType::Storm:
Multiplier *= 0.1f;
break;
case EAgrarianWeatherType::ColdWind:
Multiplier *= 1.25f;
break;
default:
break;
}
}
}
return FMath::Max(0.0f, Multiplier);
}
+24
View File
@@ -114,6 +114,27 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Risk", meta = (ClampMin = "0", ClampMax = "1"))
float ContainedFireRiskMultiplier = 0.35f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0", ClampMax = "100"))
float GrassIgnitionRiskScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0", ClampMax = "100"))
float ForestIgnitionRiskScore = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation")
bool bGrassOrBrushIgnited = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Vegetation")
bool bForestFuelIgnited = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
float VegetationIgnitionCheckRadius = 700.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
float VegetationIgnitionFuelScoreThreshold = 8.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Vegetation", meta = (ClampMin = "0"))
float VegetationIgnitionRiskPerSecond = 0.035f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1"))
float RainFuelDrainMultiplier = 1.5f;
@@ -178,4 +199,7 @@ protected:
void WarmNearbyCharacters(float DeltaSeconds);
void UpdateFireRisk(float DeltaSeconds);
float GetFireRiskGrowthPerSecond() const;
void UpdateVegetationIgnitionRisk(float DeltaSeconds);
float GetVegetationFuelScoreNearFire(float& OutGrassFuelScore, float& OutForestFuelScore) const;
float GetVegetationIgnitionWeatherMultiplier() const;
};
@@ -106,3 +106,53 @@ int32 AAgrarianFoliagePatch::GetGrassInstanceCount() const
{
return GrassInstances ? GrassInstances->GetInstanceCount() : 0;
}
void AAgrarianFoliagePatch::GetFuelCountsNearLocation(
const FVector& WorldLocation,
float Radius,
int32& OutGrassCount,
int32& OutShrubCount,
int32& OutTreeCount) const
{
SCOPE_CYCLE_COUNTER(STAT_AgrarianFoliageInstanceMutation);
TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianFoliageFuelCountsNearLocation);
OutGrassCount = CountInstancesNearLocation(GrassInstances, WorldLocation, Radius);
OutShrubCount = CountInstancesNearLocation(ShrubInstances, WorldLocation, Radius);
OutTreeCount = CountInstancesNearLocation(TreeInstances, WorldLocation, Radius);
}
float AAgrarianFoliagePatch::GetDryVegetationFuelScoreNearLocation(const FVector& WorldLocation, float Radius) const
{
int32 GrassCount = 0;
int32 ShrubCount = 0;
int32 TreeCount = 0;
GetFuelCountsNearLocation(WorldLocation, Radius, GrassCount, ShrubCount, TreeCount);
return static_cast<float>(GrassCount) + (static_cast<float>(ShrubCount) * 2.0f) + (static_cast<float>(TreeCount) * 4.0f);
}
int32 AAgrarianFoliagePatch::CountInstancesNearLocation(
const UHierarchicalInstancedStaticMeshComponent* Component,
const FVector& WorldLocation,
float Radius) const
{
if (!Component || Radius <= 0.0f)
{
return 0;
}
const float RadiusSquared = FMath::Square(Radius);
int32 Count = 0;
for (int32 Index = 0; Index < Component->GetInstanceCount(); ++Index)
{
FTransform InstanceTransform;
if (Component->GetInstanceTransform(Index, InstanceTransform, true)
&& FVector::DistSquared2D(InstanceTransform.GetLocation(), WorldLocation) <= RadiusSquared)
{
++Count;
}
}
return Count;
}
@@ -49,4 +49,21 @@ public:
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage")
int32 GetGrassInstanceCount() const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Foliage|Fire")
void GetFuelCountsNearLocation(
const FVector& WorldLocation,
float Radius,
int32& OutGrassCount,
int32& OutShrubCount,
int32& OutTreeCount) const;
UFUNCTION(BlueprintPure, Category = "Agrarian|Foliage|Fire")
float GetDryVegetationFuelScoreNearLocation(const FVector& WorldLocation, float Radius) const;
protected:
int32 CountInstancesNearLocation(
const UHierarchicalInstancedStaticMeshComponent* Component,
const FVector& WorldLocation,
float Radius) const;
};