// Copyright Pacificao. All Rights Reserved. #include "AgrarianCampfire.h" #include "AgrarianFoliagePatch.h" #include "AgrarianGameCharacter.h" #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" #include "Components/PointLightComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Kismet/GameplayStatics.h" #include "Materials/MaterialInterface.h" #include "Net/UnrealNetwork.h" #include "UObject/ConstructorHelpers.h" namespace { void ConfigureCampfireProxyComponent(UStaticMeshComponent* Component, UStaticMesh* MeshAsset, UMaterialInterface* MaterialAsset) { if (!Component) { return; } Component->SetCollisionEnabled(ECollisionEnabled::NoCollision); Component->SetGenerateOverlapEvents(false); if (MeshAsset) { Component->SetStaticMesh(MeshAsset); } if (MaterialAsset) { Component->SetMaterial(0, MaterialAsset); } } } AAgrarianCampfire::AAgrarianCampfire() { PrimaryActorTick.bCanEverTick = true; bReplicates = true; SetNetCullDistanceSquared(FMath::Square(6000.0f)); Mesh = CreateDefaultSubobject(TEXT("Mesh")); RootComponent = Mesh; Mesh->SetCollisionProfileName(TEXT("BlockAll")); static ConstructorHelpers::FObjectFinder CylinderMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_Cylinder.SM_AGR_Placeholder_Cylinder")); static ConstructorHelpers::FObjectFinder ChamferCubeMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_ChamferCube.SM_AGR_Placeholder_ChamferCube")); static ConstructorHelpers::FObjectFinder CubeMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_Cube.SM_AGR_Placeholder_Cube")); static ConstructorHelpers::FObjectFinder StoneMaterial(TEXT("/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.M_AGR_GZ_Stone_Sandstone")); static ConstructorHelpers::FObjectFinder WoodMaterial(TEXT("/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource.M_AGR_GZ_Wood_Resource")); static ConstructorHelpers::FObjectFinder FiberMaterial(TEXT("/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource.M_AGR_GZ_Fiber_Resource")); if (CylinderMesh.Succeeded()) { Mesh->SetStaticMesh(CylinderMesh.Object); Mesh->SetRelativeScale3D(FVector(0.72f, 0.72f, 0.08f)); } if (StoneMaterial.Succeeded()) { Mesh->SetMaterial(0, StoneMaterial.Object); } StoneRingProxy = CreateDefaultSubobject(TEXT("StoneRingProxy")); StoneRingProxy->SetupAttachment(RootComponent); ConfigureCampfireProxyComponent(StoneRingProxy, CylinderMesh.Succeeded() ? CylinderMesh.Object : nullptr, StoneMaterial.Succeeded() ? StoneMaterial.Object : nullptr); StoneRingProxy->SetRelativeScale3D(FVector(1.05f, 1.05f, 0.12f)); LogProxyA = CreateDefaultSubobject(TEXT("LogProxyA")); LogProxyA->SetupAttachment(RootComponent); ConfigureCampfireProxyComponent(LogProxyA, CylinderMesh.Succeeded() ? CylinderMesh.Object : nullptr, WoodMaterial.Succeeded() ? WoodMaterial.Object : nullptr); LogProxyA->SetRelativeLocation(FVector(0.0f, -12.0f, 18.0f)); LogProxyA->SetRelativeRotation(FRotator(0.0f, 90.0f, 90.0f)); LogProxyA->SetRelativeScale3D(FVector(0.16f, 0.16f, 0.72f)); LogProxyB = CreateDefaultSubobject(TEXT("LogProxyB")); LogProxyB->SetupAttachment(RootComponent); ConfigureCampfireProxyComponent(LogProxyB, CylinderMesh.Succeeded() ? CylinderMesh.Object : nullptr, WoodMaterial.Succeeded() ? WoodMaterial.Object : nullptr); LogProxyB->SetRelativeLocation(FVector(-12.0f, 10.0f, 20.0f)); LogProxyB->SetRelativeRotation(FRotator(0.0f, 28.0f, 90.0f)); LogProxyB->SetRelativeScale3D(FVector(0.16f, 0.16f, 0.68f)); LogProxyC = CreateDefaultSubobject(TEXT("LogProxyC")); LogProxyC->SetupAttachment(RootComponent); ConfigureCampfireProxyComponent(LogProxyC, CylinderMesh.Succeeded() ? CylinderMesh.Object : nullptr, WoodMaterial.Succeeded() ? WoodMaterial.Object : nullptr); LogProxyC->SetRelativeLocation(FVector(12.0f, 10.0f, 22.0f)); LogProxyC->SetRelativeRotation(FRotator(0.0f, -28.0f, 90.0f)); LogProxyC->SetRelativeScale3D(FVector(0.16f, 0.16f, 0.68f)); EmberProxy = CreateDefaultSubobject(TEXT("EmberProxy")); EmberProxy->SetupAttachment(RootComponent); ConfigureCampfireProxyComponent(EmberProxy, ChamferCubeMesh.Succeeded() ? ChamferCubeMesh.Object : (CubeMesh.Succeeded() ? CubeMesh.Object : nullptr), FiberMaterial.Succeeded() ? FiberMaterial.Object : nullptr); EmberProxy->SetRelativeLocation(FVector(0.0f, 0.0f, 13.0f)); EmberProxy->SetRelativeScale3D(FVector(0.38f, 0.38f, 0.09f)); FireLight = CreateDefaultSubobject(TEXT("FireLight")); FireLight->SetupAttachment(RootComponent); FireLight->SetIntensity(0.0f); FireLight->SetAttenuationRadius(WarmthRadius); FireLight->SetLightColor(FLinearColor(1.0f, 0.45f, 0.18f)); SmokeEffect = CreateDefaultSubobject(TEXT("SmokeEffect")); SmokeEffect->SetupAttachment(RootComponent); SmokeEffect->bAutoActivate = false; SmokeEffect->SetRelativeLocation(FVector(0.0f, 0.0f, 80.0f)); SmokeEffect->SetVisibility(false); FireLoopAudioComponent = CreateDefaultSubobject(TEXT("FireLoopAudioComponent")); FireLoopAudioComponent->SetupAttachment(RootComponent); FireLoopAudioComponent->bAutoActivate = false; FireLoopAudioComponent->bAllowSpatialization = true; FireEventAudioComponent = CreateDefaultSubobject(TEXT("FireEventAudioComponent")); FireEventAudioComponent->SetupAttachment(RootComponent); FireEventAudioComponent->bAutoActivate = false; FireEventAudioComponent->bAllowSpatialization = true; PersistentActorComponent = CreateDefaultSubobject(TEXT("PersistentActorComponent")); PersistentActorComponent->ActorTypeId = TEXT("campfire"); } void AAgrarianCampfire::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); if (HasAuthority() && bLit) { FuelSeconds = FMath::Max(0.0f, FuelSeconds - (DeltaSeconds * GetWeatherFuelDrainMultiplier())); if (FuelSeconds <= 0.0f || (bWetWeatherCanExtinguish && IsWetWeatherActive() && FuelSeconds <= WetWeatherExtinguishFuelThresholdSeconds)) { Extinguish(); } if (CanCook()) { CookingProgressSeconds = FMath::Min(CookingSecondsRequired, CookingProgressSeconds + DeltaSeconds); } UpdateFireRisk(DeltaSeconds); UpdateVegetationIgnitionRisk(DeltaSeconds); UpdateStructureIgnitionRisk(DeltaSeconds); UpdateServerAuthoritativeFireSpread(DeltaSeconds); WarmNearbyCharacters(DeltaSeconds); } } void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(AAgrarianCampfire, bLit); DOREPLIFETIME(AAgrarianCampfire, FuelSeconds); DOREPLIFETIME(AAgrarianCampfire, bCookingPlaceholderEnabled); DOREPLIFETIME(AAgrarianCampfire, CookingSecondsRequired); DOREPLIFETIME(AAgrarianCampfire, CookingProgressSeconds); DOREPLIFETIME(AAgrarianCampfire, FireRiskScore); DOREPLIFETIME(AAgrarianCampfire, LitDurationSeconds); DOREPLIFETIME(AAgrarianCampfire, SecondsSinceMaintenance); DOREPLIFETIME(AAgrarianCampfire, bFireAreaCleared); DOREPLIFETIME(AAgrarianCampfire, bFireContained); DOREPLIFETIME(AAgrarianCampfire, GrassIgnitionRiskScore); DOREPLIFETIME(AAgrarianCampfire, ForestIgnitionRiskScore); DOREPLIFETIME(AAgrarianCampfire, bGrassOrBrushIgnited); DOREPLIFETIME(AAgrarianCampfire, bForestFuelIgnited); DOREPLIFETIME(AAgrarianCampfire, StructureIgnitionRiskScore); DOREPLIFETIME(AAgrarianCampfire, bStructureIgnited); DOREPLIFETIME(AAgrarianCampfire, GrassFireIntensity); DOREPLIFETIME(AAgrarianCampfire, ForestFireIntensity); DOREPLIFETIME(AAgrarianCampfire, StructureFireIntensity); DOREPLIFETIME(AAgrarianCampfire, ActiveFireSpreadRadius); } FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const { return bLit ? FText::FromString(TEXT("Maintain fire")) : FText::FromString(TEXT("Light fire")); } bool AAgrarianCampfire::CanInteract_Implementation(const AAgrarianGameCharacter* Interactor) const { return Interactor != nullptr; } void AAgrarianCampfire::Interact_Implementation(AAgrarianGameCharacter* Interactor) { if (!HasAuthority() || !Interactor) { return; } UAgrarianInventoryComponent* Inventory = Interactor->GetInventoryComponent(); if (Inventory && Inventory->RemoveItem(TEXT("wood"), 1)) { AddFuel(90.0f); } else if (bLit) { WatchFire(); } } void AAgrarianCampfire::CapturePersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) const { if (!PersistentComponent) { return; } PersistentComponent->NumberState.Add(TEXT("lit"), bLit ? 1.0f : 0.0f); PersistentComponent->NumberState.Add(TEXT("fuel_seconds"), FuelSeconds); PersistentComponent->NumberState.Add(TEXT("cooking_placeholder_enabled"), bCookingPlaceholderEnabled ? 1.0f : 0.0f); PersistentComponent->NumberState.Add(TEXT("cooking_seconds_required"), CookingSecondsRequired); PersistentComponent->NumberState.Add(TEXT("cooking_progress_seconds"), CookingProgressSeconds); PersistentComponent->NumberState.Add(TEXT("fire_risk_score"), FireRiskScore); PersistentComponent->NumberState.Add(TEXT("lit_duration_seconds"), LitDurationSeconds); 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); PersistentComponent->NumberState.Add(TEXT("structure_ignition_risk_score"), StructureIgnitionRiskScore); PersistentComponent->NumberState.Add(TEXT("structure_ignited"), bStructureIgnited ? 1.0f : 0.0f); PersistentComponent->NumberState.Add(TEXT("grass_fire_intensity"), GrassFireIntensity); PersistentComponent->NumberState.Add(TEXT("forest_fire_intensity"), ForestFireIntensity); PersistentComponent->NumberState.Add(TEXT("structure_fire_intensity"), StructureFireIntensity); PersistentComponent->NumberState.Add(TEXT("active_fire_spread_radius"), ActiveFireSpreadRadius); PersistentComponent->NumberState.Add(TEXT("fire_suppression_pressure"), FireSuppressionPressure); } void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent) { if (!HasAuthority() || !PersistentComponent) { return; } const float* SavedFuelSeconds = PersistentComponent->NumberState.Find(TEXT("fuel_seconds")); const float* SavedCookingEnabled = PersistentComponent->NumberState.Find(TEXT("cooking_placeholder_enabled")); const float* SavedCookingRequired = PersistentComponent->NumberState.Find(TEXT("cooking_seconds_required")); const float* SavedCookingProgress = PersistentComponent->NumberState.Find(TEXT("cooking_progress_seconds")); const float* SavedLit = PersistentComponent->NumberState.Find(TEXT("lit")); const float* SavedRiskScore = PersistentComponent->NumberState.Find(TEXT("fire_risk_score")); const float* SavedLitDuration = PersistentComponent->NumberState.Find(TEXT("lit_duration_seconds")); 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")); const float* SavedStructureIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("structure_ignition_risk_score")); const float* SavedStructureIgnited = PersistentComponent->NumberState.Find(TEXT("structure_ignited")); const float* SavedGrassFireIntensity = PersistentComponent->NumberState.Find(TEXT("grass_fire_intensity")); const float* SavedForestFireIntensity = PersistentComponent->NumberState.Find(TEXT("forest_fire_intensity")); const float* SavedStructureFireIntensity = PersistentComponent->NumberState.Find(TEXT("structure_fire_intensity")); const float* SavedActiveFireSpreadRadius = PersistentComponent->NumberState.Find(TEXT("active_fire_spread_radius")); const float* SavedFireSuppressionPressure = PersistentComponent->NumberState.Find(TEXT("fire_suppression_pressure")); if (SavedFuelSeconds) { FuelSeconds = FMath::Max(0.0f, *SavedFuelSeconds); } if (SavedCookingEnabled) { bCookingPlaceholderEnabled = *SavedCookingEnabled > 0.5f; } if (SavedCookingRequired) { CookingSecondsRequired = FMath::Max(0.0f, *SavedCookingRequired); } if (SavedCookingProgress) { CookingProgressSeconds = FMath::Clamp(*SavedCookingProgress, 0.0f, CookingSecondsRequired); } if (SavedRiskScore) { FireRiskScore = FMath::Clamp(*SavedRiskScore, 0.0f, 100.0f); } if (SavedLitDuration) { LitDurationSeconds = FMath::Max(0.0f, *SavedLitDuration); } if (SavedSecondsSinceMaintenance) { SecondsSinceMaintenance = FMath::Max(0.0f, *SavedSecondsSinceMaintenance); } if (SavedAreaCleared) { bFireAreaCleared = *SavedAreaCleared > 0.5f; } if (SavedContained) { 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; } if (SavedStructureIgnitionRisk) { StructureIgnitionRiskScore = FMath::Clamp(*SavedStructureIgnitionRisk, 0.0f, 100.0f); } if (SavedStructureIgnited) { bStructureIgnited = *SavedStructureIgnited > 0.5f; } if (SavedGrassFireIntensity) { GrassFireIntensity = FMath::Clamp(*SavedGrassFireIntensity, 0.0f, 100.0f); } if (SavedForestFireIntensity) { ForestFireIntensity = FMath::Clamp(*SavedForestFireIntensity, 0.0f, 100.0f); } if (SavedStructureFireIntensity) { StructureFireIntensity = FMath::Clamp(*SavedStructureFireIntensity, 0.0f, 100.0f); } if (SavedActiveFireSpreadRadius) { ActiveFireSpreadRadius = FMath::Clamp(*SavedActiveFireSpreadRadius, 0.0f, MaxFireSpreadRadius); } if (SavedFireSuppressionPressure) { FireSuppressionPressure = FMath::Clamp(*SavedFireSuppressionPressure, 0.0f, 1.0f); } SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f); } void AAgrarianCampfire::AddFuel(float Seconds) { if (HasAuthority()) { FuelSeconds += FMath::Max(0.0f, Seconds); if (FuelSeconds > 0.0f) { SetLit(true); } else { UpdateVisualState(); } } } void AAgrarianCampfire::Extinguish() { if (HasAuthority()) { FuelSeconds = 0.0f; FireRiskScore = 0.0f; GrassIgnitionRiskScore = 0.0f; ForestIgnitionRiskScore = 0.0f; StructureIgnitionRiskScore = 0.0f; GrassFireIntensity = 0.0f; ForestFireIntensity = 0.0f; StructureFireIntensity = 0.0f; ActiveFireSpreadRadius = 0.0f; LitDurationSeconds = 0.0f; SecondsSinceMaintenance = 0.0f; SetLit(false); } } void AAgrarianCampfire::MaintainFire(bool bClearArea, bool bContainFire) { if (!HasAuthority()) { return; } SecondsSinceMaintenance = 0.0f; if (bClearArea) { bFireAreaCleared = true; } if (bContainFire) { bFireContained = true; } float RiskReduction = WatchedMaintenanceRiskReduction; if (bClearArea) { RiskReduction += ClearedAreaRiskReduction; } if (bContainFire) { RiskReduction += ContainedFireRiskReduction; } ReduceFireRisks(RiskReduction); } void AAgrarianCampfire::WatchFire() { MaintainFire(false, false); } void AAgrarianCampfire::ClearAreaAroundFire() { MaintainFire(true, false); } 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); } bool AAgrarianCampfire::CanCook() const { return bLit && bCookingPlaceholderEnabled && CookingSecondsRequired > 0.0f; } float AAgrarianCampfire::GetCookingProgressRatio() const { if (CookingSecondsRequired <= 0.0f) { return 0.0f; } 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(); } void AAgrarianCampfire::MulticastPlayFireEventSound_Implementation(bool bIgnited) { if (GetNetMode() == NM_DedicatedServer || !FireEventAudioComponent) { return; } USoundBase* EventSound = bIgnited ? IgniteSound : ExtinguishSound; if (!EventSound) { return; } FireEventAudioComponent->SetSound(EventSound); FireEventAudioComponent->Play(); } 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) { const bool bChanged = bLit != bNewLit; if (bLit != bNewLit) { bLit = bNewLit; if (bLit) { SecondsSinceMaintenance = 0.0f; } } if (HasAuthority() && bChanged) { MulticastPlayFireEventSound(bLit); } UpdateVisualState(); } void AAgrarianCampfire::UpdateVisualState() { if (FireLight) { FireLight->SetIntensity(bLit ? 4200.0f : 0.0f); } if (SmokeEffect) { SmokeEffect->SetVisibility(bLit); if (bLit) { SmokeEffect->ActivateSystem(); } else { SmokeEffect->DeactivateSystem(); } } if (FireLoopAudioComponent) { if (bLit && FireLoopSound) { if (FireLoopAudioComponent->Sound != FireLoopSound) { FireLoopAudioComponent->SetSound(FireLoopSound); } if (!FireLoopAudioComponent->IsPlaying()) { FireLoopAudioComponent->Play(); } } else if (FireLoopAudioComponent->IsPlaying()) { FireLoopAudioComponent->Stop(); } } } void AAgrarianCampfire::WarmNearbyCharacters(float DeltaSeconds) { TArray Characters; UGameplayStatics::GetAllActorsOfClass(this, AAgrarianGameCharacter::StaticClass(), Characters); for (AActor* Actor : Characters) { AAgrarianGameCharacter* Character = Cast(Actor); if (!Character || FVector::DistSquared(Character->GetActorLocation(), GetActorLocation()) > FMath::Square(WarmthRadius)) { continue; } if (UAgrarianSurvivalComponent* SurvivalComponent = Character->GetSurvivalComponent()) { SurvivalComponent->AddWarmth(WarmthPerSecond * DeltaSeconds); } } } void AAgrarianCampfire::UpdateFireRisk(float DeltaSeconds) { if (!HasAuthority() || !bLit) { return; } 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); } float AAgrarianCampfire::GetFireRiskGrowthPerSecond() const { float RiskGrowth = 0.0f; if (SecondsSinceMaintenance >= UnmaintainedRiskDelaySeconds) { RiskGrowth += PoorMaintenanceRiskPerSecond; } if (FuelSeconds >= HighFuelRiskThresholdSeconds) { RiskGrowth += HighFuelRiskPerSecond; } if (bFireAreaCleared) { RiskGrowth *= FMath::Clamp(ClearedAreaRiskMultiplier, 0.0f, 1.0f); } if (bFireContained) { RiskGrowth *= FMath::Clamp(ContainedFireRiskMultiplier, 0.0f, 1.0f); } if (IsWetWeatherActive()) { RiskGrowth *= 0.25f; } 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 FoliageActors; UGameplayStatics::GetAllActorsOfClass(this, AAgrarianFoliagePatch::StaticClass(), FoliageActors); for (AActor* Actor : FoliageActors) { const AAgrarianFoliagePatch* FoliagePatch = Cast(Actor); if (!FoliagePatch) { continue; } int32 GrassCount = 0; int32 ShrubCount = 0; int32 TreeCount = 0; FoliagePatch->GetFuelCountsNearLocation(GetActorLocation(), VegetationIgnitionCheckRadius, GrassCount, ShrubCount, TreeCount); OutGrassFuelScore += static_cast(GrassCount) + (static_cast(ShrubCount) * 1.5f); OutForestFuelScore += static_cast(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()) { 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); } 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 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 ResourceActors; UGameplayStatics::GetAllActorsOfClass(this, AAgrarianResourceNode::StaticClass(), ResourceActors); for (const AActor* Actor : ResourceActors) { const AAgrarianResourceNode* ResourceNode = Cast(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; } void AAgrarianCampfire::UpdateServerAuthoritativeFireSpread(float DeltaSeconds) { if (!HasAuthority()) { return; } const bool bAnyActiveFire = bGrassOrBrushIgnited || bForestFuelIgnited || bStructureIgnited; if (!bAnyActiveFire) { return; } const float WeatherMultiplier = GetFireSpreadWeatherMultiplier(); const float SuppressionMultiplier = FMath::Clamp(1.0f - FireSuppressionPressure, 0.0f, 1.0f); const float FuelScore = FMath::Max(1.0f, GetActiveBurningFuelScore()); const float IntensityDelta = FireSpreadIntensityPerSecond * WeatherMultiplier * SuppressionMultiplier * FuelScore * DeltaSeconds; if (bGrassOrBrushIgnited) { GrassFireIntensity = FMath::Clamp(GrassFireIntensity + IntensityDelta, 0.0f, 100.0f); } if (bForestFuelIgnited) { ForestFireIntensity = FMath::Clamp(ForestFireIntensity + (IntensityDelta * 0.75f), 0.0f, 100.0f); } if (bStructureIgnited) { StructureFireIntensity = FMath::Clamp(StructureFireIntensity + (IntensityDelta * 0.9f), 0.0f, 100.0f); } const float TotalIntensity = GrassFireIntensity + ForestFireIntensity + StructureFireIntensity; ActiveFireSpreadRadius = FMath::Clamp( BaseFireSpreadRadius + (TotalIntensity * 12.0f * WeatherMultiplier), 0.0f, MaxFireSpreadRadius); } float AAgrarianCampfire::GetFireSpreadWeatherMultiplier() const { float Multiplier = GetVegetationIgnitionWeatherMultiplier(); if (IsWetWeatherActive()) { Multiplier *= 0.5f; } return FMath::Max(0.0f, Multiplier); } float AAgrarianCampfire::GetActiveBurningFuelScore() const { float GrassFuelScore = 0.0f; float ForestFuelScore = 0.0f; const float VegetationFuelScore = GetVegetationFuelScoreNearFire(GrassFuelScore, ForestFuelScore); const float StructureFuelScore = GetStructureFuelScoreNearFire(); return FMath::Max(0.0f, VegetationFuelScore + StructureFuelScore); } void AAgrarianCampfire::ReduceFireRisks(float Amount) { const float SafeAmount = FMath::Max(0.0f, Amount); FireRiskScore = FMath::Clamp(FireRiskScore - SafeAmount, 0.0f, 100.0f); GrassIgnitionRiskScore = FMath::Clamp(GrassIgnitionRiskScore - (SafeAmount * 0.75f), 0.0f, 100.0f); 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; }