Files
ProjectEleri/Source/ProjectEleri/Items/SeedBedActor.cpp
2026-09-17 20:59:12 +02:00

974 lines
30 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "SeedBedActor.h"
#include "EleriGameState.h"
#include "TimeOfDayManager.h"
#include "Components/DecalComponent.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "DataAssets/ItemDataAsset.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "ItemContainer.h"
#include "NiagaraComponent.h"
#include "PaperSpriteComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "ProjectEleri/Components/EleriPaperSpriteComponent.h"
#include "ProjectEleri/Settings/EleriGameSettings.h"
#include "NiagaraComponent.h"
namespace
{
/** Destroys the components of a slot and clears the pointers so nothing stale is left in the map */
void DestroySlotComponents(FSeedBedSeedSlot& Slot)
{
if (Slot.SeedBedDecal) Slot.SeedBedDecal->DestroyComponent();
if (Slot.PlantSprite) Slot.PlantSprite->DestroyComponent();
Slot.SeedBedDecal = nullptr;
Slot.PlantSprite = nullptr;
}
/** Slots are created at runtime as well, so the components are created with stable, deterministic names */
UEleriPaperSpriteComponent* CreatePaperSpriteComponent(AActor* Owner, USceneComponent* AttachParent, FName ComponentName, TSoftObjectPtr<UMaterialInterface> Material)
{
if (!IsValid(Owner) || !IsValid(AttachParent))
{
return nullptr;
}
UEleriPaperSpriteComponent* SpriteComponent = NewObject<UEleriPaperSpriteComponent>(Owner, ComponentName);
if (!SpriteComponent)
{
return nullptr;
}
SpriteComponent->SetMobility(EComponentMobility::Movable);
SpriteComponent->SetupAttachment(AttachParent);
SpriteComponent->DecalSize = FVector(150.f);
SpriteComponent->DecalOffset = FVector(0.f, 0.f, 40.f);
SpriteComponent->DecalMaterial = UEleriGameSettings::GetEleriGameSettings()->SpriteShadowMaterial;
SpriteComponent->SetReceivesDecals(false);
SpriteComponent->RegisterComponent();
SpriteComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
SpriteComponent->SetVisibility(false);
SpriteComponent->DecalComponent->SetVisibility(false);
TWeakObjectPtr<UEleriPaperSpriteComponent> SpriteComp = SpriteComponent;
if (Material.IsValid())
{
SpriteComp->SetMaterial(0, Material.Get());
}
else
{
Material.LoadAsync(FLoadSoftObjectPathAsyncDelegate::CreateWeakLambda(Owner, [Owner, SpriteComp](const FSoftObjectPath& Path, UObject* MaterialObj)
{
if (SpriteComp.IsValid())
{
SpriteComp->SetMaterial(0, Cast<UMaterialInterface>(MaterialObj));
}
}));
}
Owner->AddInstanceComponent(SpriteComponent);
return SpriteComponent;
}
UDecalComponent* CreateSlotDecalComponent(AActor* Owner, USceneComponent* AttachParent, FName ComponentName)
{
if (!IsValid(Owner) || !IsValid(AttachParent))
{
return nullptr;
}
UDecalComponent* DecalComponent = NewObject<UDecalComponent>(Owner, ComponentName);
if (!DecalComponent)
{
return nullptr;
}
DecalComponent->SetMobility(EComponentMobility::Movable);
DecalComponent->SetupAttachment(AttachParent);
DecalComponent->RegisterComponent();
DecalComponent->SetVisibility(false);
Owner->AddInstanceComponent(DecalComponent);
return DecalComponent;
}
/** A slot counts as planted as soon as an asset or its id is stored, saves may only restore the id */
bool SlotHasPlant(const FSeedBedSeedSlot& Slot)
{
return IsValid(Slot.SlotData.ItemDataAsset) || Slot.SlotData.ItemAssetID.IsValid();
}
/** A dead plant keeps its id until the slot is cleared, but it neither grows nor blocks the slot anymore */
bool SlotHasLivingPlant(const FSeedBedSeedSlot& Slot)
{
return SlotHasPlant(Slot) && !Slot.SlotData.bDied;
}
/** Growth stages are set up by hand, so a zeroed scale should never make a plant invisible */
FVector SanitizeScale(const FVector& Scale)
{
return FVector(
FMath::IsNearlyZero(Scale.X) ? 1.f : Scale.X,
FMath::IsNearlyZero(Scale.Y) ? 1.f : Scale.Y,
FMath::IsNearlyZero(Scale.Z) ? 1.f : Scale.Z);
}
}
ASeedBedActor::ASeedBedActor()
{
PrimaryActorTick.bCanEverTick = true;
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Root->SetMobility(EComponentMobility::Movable);
SetRootComponent(Root);
}
void ASeedBedActor::BeginPlay()
{
Super::BeginPlay();
HighlightDecal = NewObject<UDecalComponent>(this, TEXT("Highlight Decal Component"));
if (HighlightDecal)
{
HighlightDecal->SetMobility(EComponentMobility::Movable);
HighlightDecal->SetupAttachment(RootComponent);
HighlightDecal->RegisterComponent();
HighlightDecal->SetVisibility(false);
HighlightDecal->SetRelativeRotation(HighlightDecalRotation);
AddInstanceComponent(HighlightDecal);
TWeakObjectPtr<UDecalComponent> WeakDecal = HighlightDecal;
if (HighlightDecalMaterial.IsValid())
{
HighlightDecal->SetDecalMaterial(HighlightDecalMaterial.Get());
}
else
{
HighlightDecalMaterial.LoadAsync(FLoadSoftObjectPathAsyncDelegate::CreateWeakLambda(this, [WeakDecal](const FSoftObjectPath& Path, UObject* MaterialObj)
{
if (WeakDecal.IsValid())
{
WeakDecal->SetDecalMaterial(Cast<UMaterialInterface>(MaterialObj));
}
}));
}
}
ClearSeedBedGrid();
GenerateSeedBedGrid();
// The bed is streamed in and out together with its world partition cell, so it tracks the days itself
BindToTimeOfDay();
CacheCurrentTimeOfDay();
#if WITH_EDITOR
// Beds are addressed by SeedBedIndex in the save data, two beds sharing one would overwrite each other
TArray<AActor*> OtherSeedBeds;
UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OtherSeedBeds);
for (AActor* OtherSeedBed : OtherSeedBeds)
{
const ASeedBedActor* OtherBed = Cast<ASeedBedActor>(OtherSeedBed);
if (OtherBed && OtherBed != this && OtherBed->SeedBedIndex == SeedBedIndex)
{
UE_LOG(LogTemp, Warning, TEXT("%s shares SeedBedIndex %d with %s, their save data overwrite each other"),
*GetName(), SeedBedIndex, *OtherBed->GetName());
break;
}
}
#endif
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
{
FSeedBedSaveData SaveData;
if (GameState->GetSeedData(SeedBedIndex, SaveData))
{
// The bed was in the world before, so its plants have to catch up on the time that passed without it
PropagateSeedSaveData(SaveData);
}
else
{
LastUpdateTime = LastKnownTimeOfDay;
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
}
}
// The bed can be built in the editor, but slots added through blueprints need their visuals as well
RefreshAllSlotVisuals();
}
void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// This is the moment the bed stops receiving broadcasts, so the resume math starts from the time of the day here
if (IsValid(TimeOfDayManager))
{
TimeOfDayManager->OnHourPassed.RemoveDynamic(this, &ASeedBedActor::HandleHourPassed);
}
CacheCurrentTimeOfDay();
LastUpdateTime = LastKnownTimeOfDay;
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
{
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
}
Super::EndPlay(EndPlayReason);
}
void ASeedBedActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
//------------------------------------------------------------------------------------------------
// TIME
//------------------------------------------------------------------------------------------------
void ASeedBedActor::BindToTimeOfDay()
{
TimeOfDayManager = Cast<ATimeOfDayManager>(UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
if (!IsValid(TimeOfDayManager))
{
return;
}
// AddUniqueDynamic only ever leaves one binding of the bed on the delegate, even after a reload
TimeOfDayManager->OnHourPassed.AddUniqueDynamic(this, &ASeedBedActor::HandleHourPassed);
}
void ASeedBedActor::CacheCurrentTimeOfDay()
{
if (IsValid(TimeOfDayManager))
{
LastKnownTimeOfDay = TimeOfDayManager->GetTimeOfDay();
}
}
void ASeedBedActor::HandleHourPassed()
{
AdvanceSeedBedByHours(1);
}
void ASeedBedActor::AdvanceSeedBedByHours(int32 Hours)
{
if (Hours <= 0)
{
return;
}
CacheCurrentTimeOfDay();
const FTimeOfDayData Now = LastKnownTimeOfDay;
// Plants that die are reported after the map has been walked, listeners are free to change the bed
TArray<FIntVector2> DiedSlots;
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
const bool bWasAlive = SlotHasLivingPlant(SlotPair.Value);
// Every hour is applied on its own, a jump over several of them grows, dries and kills hour by hour
for (int32 HourIndex = 0; HourIndex < Hours && SlotHasLivingPlant(SlotPair.Value); ++HourIndex)
{
ApplyHourPassedToSlot(SlotPair.Value, Now, SlotPair.Key);
}
if (bWasAlive && !SlotHasLivingPlant(SlotPair.Value))
{
DiedSlots.Add(SlotPair.Key);
}
}
LastUpdateTime = Now;
RefreshAllSlotVisuals();
for (const FIntVector2& SlotCoordinate : DiedSlots)
{
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, false);
}
}
void ASeedBedActor::ApplyHourPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now, const FIntVector2& SlotCoordinate)
{
FSeedBedSeedSlotData& SlotData = Slot.SlotData;
if (SlotData.bBloomed)
{
return;
}
// The moisture of the soil drains with every in game hour, so the wetness is a clock of its own
const bool bWasMoist = SlotData.MoistureHoursRemaining > 0;
SlotData.MoistureHoursRemaining = FMath::Max(0, SlotData.MoistureHoursRemaining - 1);
if (!SlotHasLivingPlant(Slot))
{
return;
}
// A plant that was dry for too long never grows again, so the death check comes before the growth
const int32 MinutesSinceWatering = SlotData.LastWateredTime.IsValidTime()
? Now.ToAbsoluteMinutes() - SlotData.LastWateredTime.ToAbsoluteMinutes()
: 0;
if (MinutesSinceWatering >= FMath::Max(1, DaysWithoutWaterToDie) * SeedBedMinutesPerDay)
{
SlotData.bDied = true;
return;
}
SlotData.SeedAgeHours = FMath::Min(SlotData.SeedAgeHours + 1, FMath::Max(0, SlotData.GrowTime) * SeedBedHoursPerDay);
SlotData.bBloomed = SlotData.SeedAgeHours >= SlotData.GrowTime * SeedBedHoursPerDay;
if (SlotData.bBloomed)
{
RefreshSlotVisual(SlotCoordinate);
}
}
void ASeedBedActor::ApplyOfflineProgression()
{
CacheCurrentTimeOfDay();
// Without a time of day actor there is nothing to compare against, the bed stays exactly as it was saved
if (!LastKnownTimeOfDay.IsValidTime())
{
return;
}
// A save from before the stamps existed cannot be measured, the countdown simply starts now
if (!LastUpdateTime.IsValidTime())
{
LastUpdateTime = LastKnownTimeOfDay;
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (SlotHasLivingPlant(SlotPair.Value) && !SlotPair.Value.SlotData.LastWateredTime.IsValidTime())
{
SlotPair.Value.SlotData.LastWateredTime = LastKnownTimeOfDay;
}
}
return;
}
const int32 HoursPassed = (LastKnownTimeOfDay.ToAbsoluteMinutes() - LastUpdateTime.ToAbsoluteMinutes()) / SeedBedMinutesPerHour;
if (HoursPassed > 0)
{
// Nothing can be watered while the bed is gone, but the plants still grow and dry hour by hour
AdvanceSeedBedByHours(HoursPassed);
}
else
{
LastUpdateTime = LastKnownTimeOfDay;
}
}
void ASeedBedActor::WaterSeedInSlot(FIntVector2 SlotCoordinate)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot || !SlotHasLivingPlant(*Slot))
{
return;
}
CacheCurrentTimeOfDay();
// One watering covers a whole in game day of growth, the stamp is what the death check counts from
Slot->SlotData.MoistureHoursRemaining = SeedBedMoistureHoursPerWatering;
Slot->SlotData.WateredTimes++;
Slot->SlotData.LastWateredTime = LastKnownTimeOfDay;
RefreshSlotVisual(SlotCoordinate);
}
void ASeedBedActor::WaterAllSeedSlots()
{
// The slots are collected first, a listener of the refresh is free to change the map while we water
TArray<FIntVector2> SlotCoordinates;
SeedBedSeedSlotsMap.GetKeys(SlotCoordinates);
for (const FIntVector2& SlotCoordinate : SlotCoordinates)
{
WaterSeedInSlot(SlotCoordinate);
}
}
int32 ASeedBedActor::GatherSlotsInRadius(const FVector& WorldLocation, float Radius, TArray<FIntVector2>& OutSlotCoordinates) const
{
OutSlotCoordinates.Reset();
// A radius of zero only ever covers the point itself, so there is nothing to gather
if (Radius <= 0.f)
{
return 0;
}
const float RadiusSquared = FMath::Square(Radius);
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
// Only living plants are watered, an empty or dead slot has nothing to soak up
if (!SlotHasLivingPlant(SlotPair.Value))
{
continue;
}
FVector SlotWorldLocation;
if (!GetSlotWorldLocation(SlotPair.Key, SlotWorldLocation))
{
continue;
}
// Only the plane of the bed counts, so watering from above or from uphill reaches the same slots
if (FVector::DistSquaredXY(WorldLocation, SlotWorldLocation) > RadiusSquared)
{
continue;
}
OutSlotCoordinates.Add(SlotPair.Key);
}
return OutSlotCoordinates.Num();
}
int32 ASeedBedActor::WaterSlotsInRadius(const FVector& WorldLocation, float Radius)
{
// The slots are gathered first, a listener of the refresh is free to change the map while we water
TArray<FIntVector2> SlotCoordinates;
const int32 WateredSlotCount = GatherSlotsInRadius(WorldLocation, Radius, SlotCoordinates);
for (const FIntVector2& SlotCoordinate : SlotCoordinates)
{
WaterSeedInSlot(SlotCoordinate);
}
// The cache is what the game state falls back to once the bed is streamed out, so it has to know
// about the watering right away. Nothing was watered, so there is nothing to cache either.
if (WateredSlotCount > 0)
{
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
{
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
}
}
return WateredSlotCount;
}
//------------------------------------------------------------------------------------------------
// GRID GENERATION
//------------------------------------------------------------------------------------------------
void ASeedBedActor::GenerateSeedBedGrid()
{
GenerateSeedBedGridSized(GridSize.X, GridSize.Y, true);
}
void ASeedBedActor::GenerateSeedBedGridSized(int32 Columns, int32 Rows, bool bRebuildExistingSlots)
{
const int32 SafeColumns = FMath::Max(1, Columns);
const int32 SafeRows = FMath::Max(1, Rows);
GridSize = FIntVector2(SafeColumns, SafeRows);
if (bRebuildExistingSlots)
{
// Slots falling outside of the new grid are dropped, the rest is rebuilt in place
TArray<FIntVector2> CoordinatesToRemove;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (SlotPair.Key.X < 0 || SlotPair.Key.Y < 0 || SlotPair.Key.X >= SafeColumns || SlotPair.Key.Y >= SafeRows)
{
CoordinatesToRemove.Add(SlotPair.Key);
}
}
for (const FIntVector2& Coordinate : CoordinatesToRemove)
{
if (FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(Coordinate))
{
DestroySlotComponents(*Slot);
}
SeedBedSeedSlotsMap.Remove(Coordinate);
}
}
for (int32 Column = 0; Column < SafeColumns; ++Column)
{
for (int32 Row = 0; Row < SafeRows; ++Row)
{
const FIntVector2 Coordinate(Column, Row);
if (bRebuildExistingSlots || !SeedBedSeedSlotsMap.Contains(Coordinate))
{
CreateSlot(Coordinate, bRebuildExistingSlots);
}
}
}
// Slot visuals also refresh the highlight, so the decals follow the new grid
RefreshAllSlotVisuals();
}
void ASeedBedActor::ClearSeedBedGrid()
{
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
DestroySlotComponents(SlotPair.Value);
}
SeedBedSeedSlotsMap.Empty();
bPlantingHighlightActive = false;
}
FName ASeedBedActor::GetSlotComponentName(FIntVector2 SlotCoordinate, const TCHAR* Prefix)
{
return FName(*FString::Printf(TEXT("%s_%d_%d"), Prefix ? Prefix : TEXT("Slot"), SlotCoordinate.X, SlotCoordinate.Y));
}
FVector ASeedBedActor::GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const
{
// The grid is centered on the actor, so the coordinates are shifted by half a cell
if (const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate))
{
if (IsValid(Slot->SeedBedDecal))
{
return Slot->SeedBedDecal->GetRelativeLocation();
}
}
return FVector::ZeroVector;
}
FSeedBedSaveData ASeedBedActor::PrepareSeedSaveData()
{
FSeedBedSaveData SaveData;
SaveData.Index = SeedBedIndex;
SaveData.LastUpdateTime = LastUpdateTime;
for (const auto& Slot : SeedBedSeedSlotsMap)
{
SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData);
}
return SaveData;
}
void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
{
UItemDatabase* ItemDatabase = UGameplayStatics::GetGameInstance(this)->GetSubsystem<UItemDatabase>();
ensure(ItemDatabase);
SeedBedIndex = SeedSaveData.Index;
LastUpdateTime = SeedSaveData.LastUpdateTime;
for (const auto& SavedSlot : SeedSaveData.SlotData)
{
if (FSeedBedSeedSlot* SeedSlot = SeedBedSeedSlotsMap.Find(SavedSlot.Key))
{
SeedSlot->SlotData = SavedSlot.Value;
SeedSlot->SlotData.ItemDataAsset = ItemDatabase->GetDataAssetFromId(SeedSlot->SlotData.ItemAssetID);
}
}
// Time kept running while the bed was not in the world, every plant is brought up to date in one pass
ApplyOfflineProgression();
RefreshAllSlotVisuals();
}
FSeedBedSeedSlot& ASeedBedActor::CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting)
{
FSeedBedSeedSlot& Slot = SeedBedSeedSlotsMap.FindOrAdd(SlotCoordinate);
if (bRebuildIfExisting)
{
DestroySlotComponents(Slot);
}
Slot.SeedBedDecal = CreateSlotDecalComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Slot")));
if (SeedBedDecalMaterial.IsValid())
{
Slot.SeedBedDynamicMat = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, SeedBedDecalMaterial.Get(), NAME_None, EMIDCreationFlags::Transient);
Slot.SeedBedDecal->SetDecalMaterial(Slot.SeedBedDynamicMat);
}
else
{
SeedBedDecalMaterial.LoadAsync(FLoadSoftObjectPathAsyncDelegate::CreateWeakLambda(this, [this, SlotCoordinate](const FSoftObjectPath& Path, UObject* MaterialObj)
{
FSeedBedSeedSlot& Slot = SeedBedSeedSlotsMap.FindOrAdd(SlotCoordinate);
if (Slot.SeedBedDecal)
{
Slot.SeedBedDynamicMat = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, SeedBedDecalMaterial.Get(), NAME_None, EMIDCreationFlags::Transient);
Slot.SeedBedDecal->SetDecalMaterial(Slot.SeedBedDynamicMat);
}
}));
}
Slot.SeedBedDecal->DecalSize = SeedBedDecalSize;
Slot.SeedBedDecal->SetRelativeRotation(SeedBedDecalRotation);
const FVector2D Offset = FVector2D(GridSize.X * SeedBedDecalSize.Z, GridSize.Y * SeedBedDecalSize.Y) * -1.f;
const float X = SlotCoordinate.Y * SeedBedDecalSize.Z * 2.f;
const float Y = SlotCoordinate.X * SeedBedDecalSize.Y * 2.f;
Slot.SeedBedDecal->SetRelativeLocation(
FVector(
Offset.X + X + SeedBedDecalSize.Z,
Offset.Y + Y + SeedBedDecalSize.Y,
0.f));
// The plant mesh of a slot lives as long as the slot does, the visuals only ever show or hide it
if (!IsValid(Slot.PlantSprite))
{
Slot.PlantSprite = CreatePaperSpriteComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Plant")), DefaultPlantSpriteMaterial);
}
Slot.NiagaraComponent = NewObject<UNiagaraComponent>(this, GetSlotComponentName(SlotCoordinate, TEXT("Vfx")));
Slot.NiagaraComponent->SetMobility(EComponentMobility::Movable);
Slot.NiagaraComponent->SetupAttachment(GetRootComponent());
Slot.NiagaraComponent->SetAutoActivate(false);
Slot.NiagaraComponent->RegisterComponent();
Slot.NiagaraComponent->SetRelativeLocation(FVector(
Offset.X + X + SeedBedDecalSize.Z,
Offset.Y + Y + SeedBedDecalSize.Y,
50.f));
Slot.NiagaraComponent->SetVisibility(false);
AddInstanceComponent(Slot.NiagaraComponent);
ApplySlotVisuals(SlotCoordinate, Slot);
return Slot;
}
//------------------------------------------------------------------------------------------------
// VISUALS
//------------------------------------------------------------------------------------------------
void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlot& Slot)
{
const FVector SlotLocation = GetSlotRelativeLocation(SlotCoordinate);
// SOIL -----------------------------------------------------------------------------------------
if (IsValid(Slot.SeedBedDecal))
{
Slot.SeedBedDecal->SetVisibility(Slot.SlotData.ItemAssetID.IsValid());
if (IsValid(Slot.SeedBedDynamicMat))
{
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.MoistureHoursRemaining > 0 ? 1.f : 0.f);
}
}
// PLANT ----------------------------------------------------------------------------------------
if (!IsValid(Slot.PlantSprite))
{
return;
}
if (!SlotHasLivingPlant(Slot))
{
// A plant that died or was harvested keeps its mesh, the slot is only emptied when it is cleared for real
Slot.PlantSprite->SetVisibility(false);
Slot.PlantSprite->DecalComponent->SetVisibility(false);
return;
}
FVector PlantOffset = GetSlotRelativeLocation(SlotCoordinate) + FVector(0.f, 0.f, 75.f);
FVector PlantScale = FVector(5.f);
const int32 StageIndex = ResolveGrowthStageIndex(Slot);
if (StageIndex == INDEX_NONE)
{
Slot.PlantSprite->SetVisibility(false);
Slot.PlantSprite->DecalComponent->SetVisibility(false);
return;
}
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
if (IsValid(SeedData) && SeedData->SeedStages.IsValidIndex(StageIndex))
{
PlantScale *= FVector(SeedData->SeedStages[StageIndex].SpriteScale);
PlantOffset += SeedData->SeedStages[StageIndex].Offset;
}
Slot.PlantSprite->SetRelativeScale3D(PlantScale);
Slot.PlantSprite->SetRelativeLocation(PlantOffset);
Slot.PlantSprite->UpdateRelativeLocation(FVector(PlantOffset.X, PlantOffset.Y, 0.f));
if (SeedData->SeedStages[StageIndex].Sprite.IsValid())
{
Slot.PlantSprite->SetSprite(SeedData->SeedStages[StageIndex].Sprite.Get());
Slot.PlantSprite->SetVisibility(true);
Slot.PlantSprite->DecalComponent->SetVisibility(true);
}
else
{
SeedData->SeedStages[StageIndex].Sprite.LoadAsync(FLoadSoftObjectPathAsyncDelegate::CreateWeakLambda(this, [this, SlotCoordinate](const FSoftObjectPath& Path, UObject* SpriteObj)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot || !IsValid(Slot->PlantSprite)) return;
UPaperSprite* Sprite = Cast<UPaperSprite>(SpriteObj);
if (!Sprite) return;
Slot->PlantSprite->SetSprite(Sprite);
}));
}
if (Slot.SlotData.bBloomed && !Slot.NiagaraComponent->IsActive())
{
Slot.NiagaraComponent->SetAsset(nullptr);
Slot.NiagaraComponent->Activate(false);
if (PlantBloomedVfx.IsValid())
{
Slot.NiagaraComponent->SetAsset(PlantBloomedVfx.Get());
Slot.NiagaraComponent->Activate(true);
Slot.NiagaraComponent->SetVisibility(true);
}
else
{
PlantBloomedVfx.LoadAsync(FLoadSoftObjectPathAsyncDelegate::CreateWeakLambda(this, [this, SlotCoordinate](const FSoftObjectPath& Path, UObject* EffectObj)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot || !IsValid(Slot->NiagaraComponent)) return;
UNiagaraSystem* NiagaraSystem = Cast<UNiagaraSystem>(EffectObj);
if (!IsValid(NiagaraSystem)) return;
Slot->NiagaraComponent->SetAsset(NiagaraSystem);
Slot->NiagaraComponent->ResetSystem();
Slot->NiagaraComponent->SetVisibility(true);
}));
}
}
}
void ASeedBedActor::RefreshSlotVisual(const FIntVector2& SlotCoordinate)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot)
{
return;
}
ApplySlotVisuals(SlotCoordinate, *Slot);
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasLivingPlant(*Slot));
}
void ASeedBedActor::RefreshAllSlotVisuals()
{
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
RefreshSlotVisual(SlotPair.Key);
}
}
bool ASeedBedActor::IsPlantableSeedData(const UItemDataAsset* SeedData)
{
// Only seeds of plants make it into a bed, the meshes are optional and fall back to DefaultPlantMesh
return IsValid(SeedData) && SeedData->ItemCategory == Category::Seed;
}
void ASeedBedActor::HighlightSlot(FIntVector2 SlotCoordinate)
{
if (!IsSlotFree(SlotCoordinate)) return;
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot) return;
const FVector SlotLocation = GetSlotRelativeLocation(SlotCoordinate);
if (IsValid(HighlightDecal))
{
HighlightDecal->SetRelativeLocation(SlotLocation + FVector(0.f, 0.f, 0.f));
HighlightDecal->DecalSize = HighlightDecalSize;
HighlightDecal->SetVisibility(true);
}
HighlightedSlotIndex = SlotCoordinate;
}
void ASeedBedActor::StopHighlightingSlot()
{
// The decal only exists once BeginPlay created it, a highlight is stopped on beds that never got one as well
if (IsValid(HighlightDecal))
{
HighlightDecal->SetVisibility(false);
}
HighlightedSlotIndex = FIntVector2::NoneValue;
}
bool ASeedBedActor::IsSlotFree(FIntVector2 SlotCoordinate) const
{
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
// A dead plant still holds its mesh, but the slot can be planted again
return Slot != nullptr && !SlotHasLivingPlant(*Slot);
}
int32 ASeedBedActor::GetFreeSlotCount() const
{
int32 FreeSlotCount = 0;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (!SlotHasLivingPlant(SlotPair.Value))
{
++FreeSlotCount;
}
}
return FreeSlotCount;
}
bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const
{
float DistanceSquared = 0.f;
// The distance of the slot is only interesting for the planting highlight, the plain lookup ignores it
return FindClosestFreeSlotWithDistance(WorldLocation, OutSlotCoordinate, DistanceSquared);
}
bool ASeedBedActor::FindClosestFreeSlotWithDistance(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate, float& OutDistanceSquared) const
{
bool bFoundSlot = false;
float ClosestDistanceSquared = 0.f;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (SlotHasLivingPlant(SlotPair.Value))
{
continue;
}
FVector SlotWorldLocation;
if (!GetSlotWorldLocation(SlotPair.Key, SlotWorldLocation))
{
continue;
}
// Only the plane of the bed counts, a player standing uphill has to reach the slot just as well
const float DistanceSquared = FVector::DistSquaredXY(WorldLocation, SlotWorldLocation);
if (!bFoundSlot || DistanceSquared < ClosestDistanceSquared)
{
bFoundSlot = true;
ClosestDistanceSquared = DistanceSquared;
OutSlotCoordinate = SlotPair.Key;
}
}
OutDistanceSquared = ClosestDistanceSquared;
return bFoundSlot;
}
bool ASeedBedActor::GetSlotWorldLocation(FIntVector2 SlotCoordinate, FVector& OutWorldLocation) const
{
if (!SeedBedSeedSlotsMap.Contains(SlotCoordinate))
{
return false;
}
// The location is derived from the grid and the actor transform, so it is valid without any component
OutWorldLocation = GetActorTransform().TransformPosition(GetSlotRelativeLocation(SlotCoordinate));
return true;
}
int32 ASeedBedActor::ResolveGrowthStageIndex(const FSeedBedSeedSlot& Slot) const
{
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
if (!IsValid(SeedData))
{
return INDEX_NONE;
}
// Stages are set up per day while the age of a plant counts hours, so the last day that was reached wins
int32 StageIndex = INDEX_NONE;
for (int32 Index = 0; Index < SeedData->SeedStages.Num(); ++Index)
{
if (Slot.SlotData.SeedAgeHours < SeedData->SeedStages[Index].ChangeOnDay * SeedBedHoursPerDay)
{
break;
}
StageIndex = Index;
}
return StageIndex;
}
//------------------------------------------------------------------------------------------------
// PLANTING
//------------------------------------------------------------------------------------------------
bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset* SeedData, bool bHighQuality)
{
if (!IsSlotFree(SlotCoordinate))
{
return false;
}
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot)
{
return false;
}
// A fresh plant starts at day zero, everything that is stored about it is the state of the new seed
CacheCurrentTimeOfDay();
Slot->SlotData.ItemDataAsset = SeedData;
Slot->SlotData.ItemAssetID = SeedData->GetPrimaryAssetId();
// Grow time is authored in days, the age of a plant itself is counted in in game hours
Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf);
Slot->SlotData.SeedAgeHours = 0;
Slot->SlotData.WateredTimes = 0;
Slot->SlotData.bBloomed = false;
Slot->SlotData.MoistureHoursRemaining = 0;
Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = bHighQuality;
// A plant that is never watered has to dry out from the day it was planted on
Slot->SlotData.LastWateredTime = LastKnownTimeOfDay;
if (IsValid(Slot->SeedBedDynamicMat))
{
Slot->SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), 0.f);
}
RefreshSlotVisual(SlotCoordinate);
return true;
}
void ASeedBedActor::ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePlantMesh)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot)
{
return;
}
Slot->SlotData.ItemDataAsset = nullptr;
Slot->SlotData.ItemAssetID = FPrimaryAssetId();
Slot->SlotData.GrowTime = 0;
Slot->SlotData.SeedAgeHours = 0;
Slot->SlotData.WateredTimes = 0;
Slot->SlotData.bBloomed = false;
Slot->SlotData.MoistureHoursRemaining = 0;
Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = false;
Slot->SlotData.LastWateredTime = FTimeOfDayData();
if (bRemovePlantMesh)
{
RefreshSlotVisual(SlotCoordinate);
return;
}
// The mesh is left alone on purpose, a harvested plant stays visible until the slot is cleared for real
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, false);
}