This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
AgrarianGameArchive/Source/AgrarianGame/AgrarianCampfire.cpp
T
2026-05-19 12:08:16 -07:00

400 lines
13 KiB
C++

// 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/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;
NetCullDistanceSquared = 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);
}
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);
}
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();
}
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 (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);
}
}
}