974 lines
30 KiB
C++
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);
|
|
}
|
|
|