Watering system implement
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include "SeedBedActor.h"
|
||||
|
||||
#include "EleriGameState.h"
|
||||
#include "TimeOfDayManager.h"
|
||||
#include "Components/DecalComponent.h"
|
||||
#include "Components/SceneComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
@@ -90,6 +91,12 @@ namespace
|
||||
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)
|
||||
{
|
||||
@@ -126,18 +133,40 @@ void ASeedBedActor::BeginPlay()
|
||||
|
||||
ClearSeedBedGrid();
|
||||
GenerateSeedBedGrid();
|
||||
|
||||
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this));
|
||||
ensure(GameState);
|
||||
|
||||
FSeedBedSaveData SaveData;
|
||||
if (GameState->GetSeedData(SeedBedIndex, SaveData))
|
||||
|
||||
// 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)
|
||||
{
|
||||
PropagateSeedSaveData(SaveData);
|
||||
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;
|
||||
}
|
||||
}
|
||||
else
|
||||
#endif
|
||||
|
||||
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
|
||||
{
|
||||
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
||||
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
|
||||
@@ -146,9 +175,19 @@ void ASeedBedActor::BeginPlay()
|
||||
|
||||
void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this));
|
||||
ensure(GameState);
|
||||
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
||||
// 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->OnDayPassed.RemoveDynamic(this, &ASeedBedActor::HandleDayPassed);
|
||||
}
|
||||
|
||||
CacheCurrentTimeOfDay();
|
||||
LastUpdateTime = LastKnownTimeOfDay;
|
||||
|
||||
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
|
||||
{
|
||||
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
@@ -158,6 +197,166 @@ 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->OnDayPassed.AddUniqueDynamic(this, &ASeedBedActor::HandleDayPassed);
|
||||
}
|
||||
|
||||
void ASeedBedActor::CacheCurrentTimeOfDay()
|
||||
{
|
||||
if (IsValid(TimeOfDayManager))
|
||||
{
|
||||
LastKnownTimeOfDay = TimeOfDayManager->GetTimeOfDay();
|
||||
}
|
||||
}
|
||||
|
||||
void ASeedBedActor::HandleDayPassed()
|
||||
{
|
||||
AdvanceSeedBedByDays(1);
|
||||
}
|
||||
|
||||
void ASeedBedActor::AdvanceSeedBedByDays(int32 Days)
|
||||
{
|
||||
if (Days <= 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);
|
||||
ApplyDayPassedToSlot(SlotPair.Value, Now);
|
||||
|
||||
if (bWasAlive && !SlotHasLivingPlant(SlotPair.Value))
|
||||
{
|
||||
DiedSlots.Add(SlotPair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
LastUpdateTime = Now;
|
||||
RefreshAllSlotVisuals();
|
||||
|
||||
for (const FIntVector2& SlotCoordinate : DiedSlots)
|
||||
{
|
||||
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, false);
|
||||
}
|
||||
}
|
||||
|
||||
void ASeedBedActor::ApplyDayPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now)
|
||||
{
|
||||
FSeedBedSeedSlotData& SlotData = Slot.SlotData;
|
||||
|
||||
// The moisture of the soil only lasts for the day it was watered on
|
||||
const bool bWasWatered = SlotData.bWateredToday;
|
||||
SlotData.bWateredToday = false;
|
||||
|
||||
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 DaysSinceWatering = SlotData.LastWateredTime.IsValidTime()
|
||||
? Now.ToAbsoluteDays() - SlotData.LastWateredTime.ToAbsoluteDays()
|
||||
: 0;
|
||||
|
||||
if (DaysSinceWatering >= FMath::Max(1, DaysWithoutWaterToDie))
|
||||
{
|
||||
SlotData.bDied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only a watered plant grows, and it never grows past its own grow time
|
||||
if (bWasWatered)
|
||||
{
|
||||
SlotData.SeedAge = FMath::Min(SlotData.SeedAge + 1, FMath::Max(0, SlotData.GrowTime));
|
||||
}
|
||||
}
|
||||
|
||||
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 DaysPassed = LastKnownTimeOfDay.ToAbsoluteDays() - LastUpdateTime.ToAbsoluteDays();
|
||||
if (DaysPassed > 0)
|
||||
{
|
||||
// Growth is linear and nothing can be watered while the bed is gone, so a single pass is enough
|
||||
AdvanceSeedBedByDays(DaysPassed);
|
||||
}
|
||||
else
|
||||
{
|
||||
LastUpdateTime = LastKnownTimeOfDay;
|
||||
}
|
||||
}
|
||||
|
||||
void ASeedBedActor::WaterSeedInSlot(FIntVector2 SlotCoordinate)
|
||||
{
|
||||
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
|
||||
if (!Slot || !SlotHasLivingPlant(*Slot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CacheCurrentTimeOfDay();
|
||||
|
||||
// Wetness is a per day state, the stamp is what the death check measures the days since
|
||||
Slot->SlotData.bWateredToday = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
//------------------------------------------------------------------------------------------------
|
||||
// GRID GENERATION
|
||||
//------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -233,7 +432,10 @@ 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))
|
||||
{
|
||||
return Slot->SeedBedDecal->GetRelativeLocation();
|
||||
if (IsValid(Slot->SeedBedDecal))
|
||||
{
|
||||
return Slot->SeedBedDecal->GetRelativeLocation();
|
||||
}
|
||||
}
|
||||
|
||||
return FVector::ZeroVector;
|
||||
@@ -243,6 +445,7 @@ FSeedBedSaveData ASeedBedActor::PrepareSeedSaveData()
|
||||
{
|
||||
FSeedBedSaveData SaveData;
|
||||
SaveData.Index = SeedBedIndex;
|
||||
SaveData.LastUpdateTime = LastUpdateTime;
|
||||
for (const auto& Slot : SeedBedSeedSlotsMap)
|
||||
{
|
||||
SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData);
|
||||
@@ -257,6 +460,7 @@ void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
|
||||
ensure(ItemDatabase);
|
||||
|
||||
SeedBedIndex = SeedSaveData.Index;
|
||||
LastUpdateTime = SeedSaveData.LastUpdateTime;
|
||||
for (const auto& SavedSlot : SeedSaveData.SlotData)
|
||||
{
|
||||
if (FSeedBedSeedSlot* SeedSlot = SeedBedSeedSlotsMap.Find(SavedSlot.Key))
|
||||
@@ -266,6 +470,9 @@ void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
|
||||
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();
|
||||
}
|
||||
@@ -293,6 +500,17 @@ FSeedBedSeedSlot& ASeedBedActor::CreateSlot(FIntVector2 SlotCoordinate, bool bRe
|
||||
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.PlantStaticMesh))
|
||||
{
|
||||
Slot.PlantStaticMesh = CreateSlotMeshComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Plant")));
|
||||
if (IsValid(Slot.PlantStaticMesh))
|
||||
{
|
||||
Slot.PlantStaticMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||
Slot.PlantStaticMesh->SetVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
ApplySlotVisuals(SlotCoordinate, Slot);
|
||||
|
||||
return Slot;
|
||||
@@ -309,37 +527,52 @@ void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlo
|
||||
if (IsValid(Slot.SeedBedDecal))
|
||||
{
|
||||
Slot.SeedBedDecal->SetVisibility(Slot.SlotData.ItemAssetID.IsValid());
|
||||
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.bWateredToday);
|
||||
|
||||
if (IsValid(Slot.SeedBedDynamicMat))
|
||||
{
|
||||
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.bWateredToday ? 1.f : 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
// PLANT ----------------------------------------------------------------------------------------
|
||||
const bool bHasPlant = SlotHasPlant(Slot) && !Slot.SlotData.bDied;
|
||||
const bool bHasPlant = SlotHasLivingPlant(Slot);
|
||||
|
||||
if (IsValid(Slot.PlantStaticMesh))
|
||||
if (!IsValid(Slot.PlantStaticMesh))
|
||||
{
|
||||
UStaticMesh* PlantMesh = nullptr;
|
||||
FVector PlantOffset = FVector::ZeroVector;
|
||||
FVector PlantScale = FVector::OneVector;
|
||||
|
||||
if (bHasPlant)
|
||||
{
|
||||
const int32 StageIndex = ResolveGrowthStageIndex(Slot);
|
||||
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
|
||||
if (IsValid(SeedData) && SeedData->SeedStages.IsValidIndex(StageIndex))
|
||||
{
|
||||
const FSeedStage& Stage = SeedData->SeedStages[StageIndex];
|
||||
PlantMesh = ResolveSoftObject(Stage.Mesh);
|
||||
PlantOffset = Stage.MeshOffset;
|
||||
PlantScale = SanitizeScale(Stage.MeshScale);
|
||||
}
|
||||
|
||||
// A seed without stage meshes still has to show up, the fallback keeps the plant visible
|
||||
if (!PlantMesh)
|
||||
{
|
||||
PlantMesh = ResolveSoftObject(DefaultPlantMesh);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bHasPlant)
|
||||
{
|
||||
// A plant that died or was harvested keeps its mesh, the slot is only emptied when it is cleared for real
|
||||
Slot.PlantStaticMesh->SetVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
UStaticMesh* PlantMesh = nullptr;
|
||||
FVector PlantOffset = FVector::ZeroVector;
|
||||
FVector PlantScale = FVector::OneVector;
|
||||
|
||||
const int32 StageIndex = ResolveGrowthStageIndex(Slot);
|
||||
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
|
||||
if (IsValid(SeedData) && SeedData->SeedStages.IsValidIndex(StageIndex))
|
||||
{
|
||||
const FSeedStage& Stage = SeedData->SeedStages[StageIndex];
|
||||
PlantMesh = ResolveSoftObject(Stage.Mesh);
|
||||
PlantOffset = Stage.MeshOffset;
|
||||
PlantScale = SanitizeScale(Stage.MeshScale);
|
||||
}
|
||||
|
||||
// A seed without stage meshes still has to show up, the fallback keeps the plant visible
|
||||
if (!PlantMesh)
|
||||
{
|
||||
PlantMesh = ResolveSoftObject(DefaultPlantMesh);
|
||||
}
|
||||
|
||||
Slot.PlantStaticMesh->SetStaticMesh(PlantMesh);
|
||||
Slot.PlantStaticMesh->SetRelativeLocation(SlotLocation + PlantOffset);
|
||||
Slot.PlantStaticMesh->SetRelativeScale3D(PlantScale);
|
||||
Slot.PlantStaticMesh->SetVisibility(PlantMesh != nullptr);
|
||||
}
|
||||
void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
|
||||
{
|
||||
@@ -351,7 +584,7 @@ void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
|
||||
|
||||
ApplySlotVisuals(SlotCoordinate, *Slot);
|
||||
|
||||
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasPlant(*Slot));
|
||||
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasLivingPlant(*Slot));
|
||||
}
|
||||
|
||||
void ASeedBedActor::RefreshAllSlotVisuals()
|
||||
@@ -397,7 +630,8 @@ bool ASeedBedActor::IsSlotFree(FIntVector2 SlotCoordinate) const
|
||||
{
|
||||
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
|
||||
|
||||
return Slot != nullptr && !SlotHasPlant(*Slot);
|
||||
// A dead plant still holds its mesh, but the slot can be planted again
|
||||
return Slot != nullptr && !SlotHasLivingPlant(*Slot);
|
||||
}
|
||||
|
||||
int32 ASeedBedActor::GetFreeSlotCount() const
|
||||
@@ -406,7 +640,7 @@ int32 ASeedBedActor::GetFreeSlotCount() const
|
||||
|
||||
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
||||
{
|
||||
if (!SlotHasPlant(SlotPair.Value))
|
||||
if (!SlotHasLivingPlant(SlotPair.Value))
|
||||
{
|
||||
++FreeSlotCount;
|
||||
}
|
||||
@@ -421,7 +655,7 @@ bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector
|
||||
|
||||
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
|
||||
{
|
||||
if (SlotHasPlant(SlotPair.Value))
|
||||
if (!SlotHasLivingPlant(SlotPair.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -498,6 +732,7 @@ bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset*
|
||||
}
|
||||
|
||||
// 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();
|
||||
Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf);
|
||||
@@ -507,8 +742,14 @@ bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset*
|
||||
Slot->SlotData.bWateredToday = false;
|
||||
Slot->SlotData.bDied = false;
|
||||
Slot->SlotData.bHighQuality = bHighQuality;
|
||||
|
||||
Slot->SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), 0.f);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -532,6 +773,7 @@ void ASeedBedActor::ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePl
|
||||
Slot->SlotData.bWateredToday = false;
|
||||
Slot->SlotData.bDied = false;
|
||||
Slot->SlotData.bHighQuality = false;
|
||||
Slot->SlotData.LastWateredTime = FTimeOfDayData();
|
||||
|
||||
if (bRemovePlantMesh)
|
||||
{
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "ProjectEleri/System/TimeOfDayStruct.h"
|
||||
#include "SeedBedActor.generated.h"
|
||||
|
||||
class ATimeOfDayManager;
|
||||
class UDecalComponent;
|
||||
class UMaterialInstanceDynamic;
|
||||
class UMaterialInterface;
|
||||
class UStaticMesh;
|
||||
class UStaticMeshComponent;
|
||||
@@ -47,6 +49,10 @@ struct FSeedBedSeedSlotData
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
bool bHighQuality = false;
|
||||
|
||||
/** In game time of the last watering, the death check measures the days that passed since then */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FTimeOfDayData LastWateredTime;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
@@ -77,6 +83,10 @@ struct FSeedBedSaveData
|
||||
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
||||
TMap<FIntVector2, FSeedBedSeedSlotData> SlotData;
|
||||
|
||||
/** In game time the bed last applied growth to its slots, the resume math starts from this stamp */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FTimeOfDayData LastUpdateTime;
|
||||
};
|
||||
|
||||
UCLASS()
|
||||
@@ -168,6 +178,14 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||
void ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePlantMesh = true);
|
||||
|
||||
/** Waters a single slot: refills the soil and restarts the countdown the death check measures */
|
||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||
void WaterSeedInSlot(FIntVector2 SlotCoordinate);
|
||||
|
||||
/** Waters every planted slot of the bed */
|
||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||
void WaterAllSeedSlots();
|
||||
|
||||
/** Finds the free slot that is closest to the given world location */
|
||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
|
||||
bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const;
|
||||
@@ -185,6 +203,18 @@ public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed")
|
||||
bool HasFreeSlot() const { return GetFreeSlotCount() > 0; }
|
||||
|
||||
//------------------------------------------------------------------------------------------------
|
||||
// TIME
|
||||
//------------------------------------------------------------------------------------------------
|
||||
|
||||
/** The in game time the bed last applied growth for, the stamp the resume math continues from */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed|Time")
|
||||
FTimeOfDayData GetLastUpdateTime() const { return LastUpdateTime; }
|
||||
|
||||
/** Applies the given amount of in game days to every plant of the bed, used by the day tick and the resume catch up */
|
||||
UFUNCTION(BlueprintCallable, Category = "Seed Bed|Time")
|
||||
void AdvanceSeedBedByDays(int32 Days);
|
||||
|
||||
//------------------------------------------------------------------------------------------------
|
||||
// VISUALS
|
||||
//------------------------------------------------------------------------------------------------
|
||||
@@ -228,6 +258,22 @@ protected:
|
||||
|
||||
FVector GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const;
|
||||
|
||||
/** Subscribes to the time of day actor, the bed ages its plants on every day that passes */
|
||||
void BindToTimeOfDay();
|
||||
|
||||
/** Applies one in game day of growth, moisture handling and death checks to a single slot */
|
||||
void ApplyDayPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now);
|
||||
|
||||
/** Catches the plants up on the in game time that passed while the bed was not in the world */
|
||||
void ApplyOfflineProgression();
|
||||
|
||||
/** Stores the current in game time, the manager may already be gone when the bed is torn down */
|
||||
void CacheCurrentTimeOfDay();
|
||||
|
||||
/** Day tick of the time of day actor */
|
||||
UFUNCTION()
|
||||
void HandleDayPassed();
|
||||
|
||||
|
||||
/** Whether the highlight decals of the bed are currently shown */
|
||||
bool bPlantingHighlightActive = false;
|
||||
@@ -238,7 +284,19 @@ protected:
|
||||
/** Decal used to highlight this slot when the player is looking for a place to plant a seed */
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
|
||||
UDecalComponent* HighlightDecal = nullptr;
|
||||
|
||||
|
||||
/** Days a plant survives without being watered before it dies */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Time")
|
||||
int32 DaysWithoutWaterToDie = 2;
|
||||
|
||||
/** In game time the bed last applied growth for, written on every day and again when the bed leaves the world */
|
||||
UPROPERTY(Transient)
|
||||
FTimeOfDayData LastUpdateTime;
|
||||
|
||||
/** Copy of the last time the manager reported, used when the bed is torn down after the manager is gone */
|
||||
UPROPERTY(Transient)
|
||||
FTimeOfDayData LastKnownTimeOfDay;
|
||||
|
||||
UPROPERTY()
|
||||
ATimeOfDayManager* TimeOfDayManager = nullptr;
|
||||
|
||||
|
||||
@@ -69,6 +69,41 @@ void AEleriGameState::PropagateAllSeedData()
|
||||
}
|
||||
}
|
||||
|
||||
void AEleriGameState::WaterAllSeedBeds()
|
||||
{
|
||||
const ATimeOfDayManager* TimeOfDay = Cast<ATimeOfDayManager>(
|
||||
UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
|
||||
if (!TimeOfDay) return;
|
||||
|
||||
TArray<AActor*> OutActors;
|
||||
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors);
|
||||
|
||||
TArray<ASeedBedActor*> LoadedSeedBeds;
|
||||
LoadedSeedBeds.Reserve(OutActors.Num());
|
||||
|
||||
// Only the cached data also covers the beds that are streamed out, so it is the source of truth.
|
||||
// Loaded beds know more than the cache does, so they are cached first, exactly like the save does it.
|
||||
for (AActor* Actor : OutActors)
|
||||
{
|
||||
ASeedBedActor* SeedBedActor = Cast<ASeedBedActor>(Actor);
|
||||
if (!SeedBedActor) continue;
|
||||
|
||||
LoadedSeedBeds.Add(SeedBedActor);
|
||||
CacheSeedData(SeedBedActor->SeedBedIndex, SeedBedActor->PrepareSeedSaveData());
|
||||
}
|
||||
|
||||
SaveData.WaterAllPlantedSeeds(TimeOfDay->GetTimeOfDay());
|
||||
|
||||
// The loaded beds adopt the watered state right away, the others pick it up when they are streamed in
|
||||
for (ASeedBedActor* SeedBedActor : LoadedSeedBeds)
|
||||
{
|
||||
if (const FSeedBedSaveData* SeedBedData = SaveData.SeedBedData.Find(SeedBedActor->SeedBedIndex))
|
||||
{
|
||||
SeedBedActor->PropagateSeedSaveData(*SeedBedData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData)
|
||||
{
|
||||
check(InSaveData);
|
||||
@@ -248,6 +283,7 @@ bool AEleriGameState::GetSeedData(int32 Index, FSeedBedSaveData& OutData)
|
||||
if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index))
|
||||
{
|
||||
OutData = *FoundData;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -53,6 +53,27 @@ void FSaveData::SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSave
|
||||
SeedBedData.Add(Index, SeedSaveData);
|
||||
}
|
||||
|
||||
void FSaveData::WaterAllPlantedSeeds(const FTimeOfDayData& WateredTime)
|
||||
{
|
||||
for (TPair<int32, FSeedBedSaveData>& SeedBedPair : SeedBedData)
|
||||
{
|
||||
for (TPair<FIntVector2, FSeedBedSeedSlotData>& SlotPair : SeedBedPair.Value.SlotData)
|
||||
{
|
||||
FSeedBedSeedSlotData& SlotData = SlotPair.Value;
|
||||
|
||||
// Rain refills the soil of living plants, it neither plants nor revives anything
|
||||
if (!SlotData.ItemAssetID.IsValid() || SlotData.bDied)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SlotData.bWateredToday = true;
|
||||
SlotData.WateredTimes++;
|
||||
SlotData.LastWateredTime = WateredTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FSaveData::SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors)
|
||||
{
|
||||
PlacedActors = InPlacedActors;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Kismet/KismetMaterialLibrary.h"
|
||||
#include "Materials/MaterialParameterCollection.h"
|
||||
#include "ProjectEleri/Data/WeatherPresetDataAsset.h"
|
||||
#include "EleriGameState.h"
|
||||
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
|
||||
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
|
||||
|
||||
@@ -120,6 +121,15 @@ void ATimeOfDayManager::Tick(float DeltaTime)
|
||||
CurrentWeaterFadeAlpha = 1.f;
|
||||
QueuedWeatherPreset = GetWeatherDistributionValue();
|
||||
K2_WeatherFadeStart(QueuedWeatherPreset->bIsRain);
|
||||
|
||||
if (QueuedWeatherPreset->bIsRain)
|
||||
{
|
||||
// Rain refills the soil of every plant, the game state also reaches the beds that are streamed out
|
||||
if (AEleriGameState* GameState = AEleriGameState::Get(this))
|
||||
{
|
||||
GameState->WaterAllSeedBeds();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -43,6 +43,10 @@ public:
|
||||
void CacheSeedData(int32 Index, const FSeedBedSaveData& Data);
|
||||
bool GetSeedData(int32 Index, FSeedBedSaveData& OutData);
|
||||
|
||||
/** Waters every planted seed of every seed bed, loaded or not. Called when it starts raining. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Game State")
|
||||
void WaterAllSeedBeds();
|
||||
|
||||
UPROPERTY()
|
||||
FOnPropagateSaveDataComplete OnPropagateSaveDataComplete;
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ struct FSaveData {
|
||||
void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression);
|
||||
void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory);
|
||||
void SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData);
|
||||
/** Refills the soil of every planted seed of the cached seed beds, used by rain */
|
||||
void WaterAllPlantedSeeds(const FTimeOfDayData& WateredTime);
|
||||
void SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors);
|
||||
void CollectSaveableActorsData(const UWorld* World);
|
||||
void SetGameEventData(const UWorld* World);
|
||||
|
||||
@@ -31,4 +31,20 @@ public:
|
||||
bool operator==(const FTimeOfDayData& Other) const {
|
||||
return Year == Other.Year && Month == Other.Month && Day == Other.Day && Hour == Other.Hour && Minute == Other.Minute;
|
||||
}
|
||||
|
||||
/** Sums up years, months and days into a single monotonic day counter so two stamps can be compared.
|
||||
* The formula mirrors ATimeOfDayManager::GetAllDaysPassed(), keep both in sync. */
|
||||
int32 ToAbsoluteDays() const {
|
||||
return ((Year - 1) * 4 * 28) + ((Month - 1) * 28) + Day;
|
||||
}
|
||||
|
||||
/** The same counter in minutes, for the places that need a finer resolution than whole days */
|
||||
int32 ToAbsoluteMinutes() const {
|
||||
return (ToAbsoluteDays() * 24 * 60) + (Hour * 60) + Minute;
|
||||
}
|
||||
|
||||
/** A stamp that was never written, for example a save from before the stamp existed, is all zeroes */
|
||||
bool IsValidTime() const {
|
||||
return Year > 0 || Month > 0 || Day > 0;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user