// Copyright Pacificao. All Rights Reserved. #include "AgrarianCampfire.h" #include "AgrarianGameCharacter.h" #include "AgrarianGameState.h" #include "AgrarianInventoryComponent.h" #include "AgrarianPersistentActorComponent.h" #include "AgrarianSurvivalComponent.h" #include "Particles/ParticleSystemComponent.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; NetCullDistanceSquared = 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); 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); } 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); } FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const { return bLit ? FText::FromString(TEXT("Add fuel")) : 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); } } 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); } 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")); 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); } 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; SetLit(false); } } 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(); } 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) { if (bLit != bNewLit) { bLit = bNewLit; } 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(); } } } 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); } } }