939 lines
30 KiB
C++
939 lines
30 KiB
C++
// 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<UStaticMeshComponent>(TEXT("Mesh"));
|
|
RootComponent = Mesh;
|
|
Mesh->SetCollisionProfileName(TEXT("BlockAll"));
|
|
|
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> CylinderMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_Cylinder.SM_AGR_Placeholder_Cylinder"));
|
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> ChamferCubeMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_ChamferCube.SM_AGR_Placeholder_ChamferCube"));
|
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeMesh(TEXT("/Game/Agrarian/Environment/PlaceholderMeshes/SM_AGR_Placeholder_Cube.SM_AGR_Placeholder_Cube"));
|
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> StoneMaterial(TEXT("/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone.M_AGR_GZ_Stone_Sandstone"));
|
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> WoodMaterial(TEXT("/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource.M_AGR_GZ_Wood_Resource"));
|
|
static ConstructorHelpers::FObjectFinder<UMaterialInterface> 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<UStaticMeshComponent>(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<UStaticMeshComponent>(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<UStaticMeshComponent>(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<UStaticMeshComponent>(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<UStaticMeshComponent>(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<UPointLightComponent>(TEXT("FireLight"));
|
|
FireLight->SetupAttachment(RootComponent);
|
|
FireLight->SetIntensity(0.0f);
|
|
FireLight->SetAttenuationRadius(WarmthRadius);
|
|
FireLight->SetLightColor(FLinearColor(1.0f, 0.45f, 0.18f));
|
|
|
|
SmokeEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SmokeEffect"));
|
|
SmokeEffect->SetupAttachment(RootComponent);
|
|
SmokeEffect->bAutoActivate = false;
|
|
SmokeEffect->SetRelativeLocation(FVector(0.0f, 0.0f, 80.0f));
|
|
SmokeEffect->SetVisibility(false);
|
|
|
|
FireLoopAudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("FireLoopAudioComponent"));
|
|
FireLoopAudioComponent->SetupAttachment(RootComponent);
|
|
FireLoopAudioComponent->bAutoActivate = false;
|
|
FireLoopAudioComponent->bAllowSpatialization = true;
|
|
|
|
FireEventAudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("FireEventAudioComponent"));
|
|
FireEventAudioComponent->SetupAttachment(RootComponent);
|
|
FireEventAudioComponent->bAutoActivate = false;
|
|
FireEventAudioComponent->bAllowSpatialization = true;
|
|
|
|
PersistentActorComponent = CreateDefaultSubobject<UAgrarianPersistentActorComponent>(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<FLifetimeProperty>& 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<AAgrarianGameState>())
|
|
{
|
|
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<AActor*> Characters;
|
|
UGameplayStatics::GetAllActorsOfClass(this, AAgrarianGameCharacter::StaticClass(), Characters);
|
|
|
|
for (AActor* Actor : Characters)
|
|
{
|
|
AAgrarianGameCharacter* Character = Cast<AAgrarianGameCharacter>(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<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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|