Watering system implement

This commit is contained in:
2026-09-14 11:03:40 +02:00
parent ca6f0debe9
commit 60c88fd97e
8 changed files with 434 additions and 45 deletions

View File

@@ -4,6 +4,7 @@
#include "SeedBedActor.h" #include "SeedBedActor.h"
#include "EleriGameState.h" #include "EleriGameState.h"
#include "TimeOfDayManager.h"
#include "Components/DecalComponent.h" #include "Components/DecalComponent.h"
#include "Components/SceneComponent.h" #include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h" #include "Components/StaticMeshComponent.h"
@@ -90,6 +91,12 @@ namespace
return IsValid(Slot.SlotData.ItemDataAsset) || Slot.SlotData.ItemAssetID.IsValid(); 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 */ /** Growth stages are set up by hand, so a zeroed scale should never make a plant invisible */
FVector SanitizeScale(const FVector& Scale) FVector SanitizeScale(const FVector& Scale)
{ {
@@ -126,18 +133,40 @@ void ASeedBedActor::BeginPlay()
ClearSeedBedGrid(); ClearSeedBedGrid();
GenerateSeedBedGrid(); GenerateSeedBedGrid();
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)); // The bed is streamed in and out together with its world partition cell, so it tracks the days itself
ensure(GameState); BindToTimeOfDay();
CacheCurrentTimeOfDay();
FSeedBedSaveData SaveData;
if (GameState->GetSeedData(SeedBedIndex, SaveData)) #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 // 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) void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{ {
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)); // This is the moment the bed stops receiving broadcasts, so the resume math starts from the time of the day here
ensure(GameState); if (IsValid(TimeOfDayManager))
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData()); {
TimeOfDayManager->OnDayPassed.RemoveDynamic(this, &ASeedBedActor::HandleDayPassed);
}
CacheCurrentTimeOfDay();
LastUpdateTime = LastKnownTimeOfDay;
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
{
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
}
Super::EndPlay(EndPlayReason); Super::EndPlay(EndPlayReason);
} }
@@ -158,6 +197,166 @@ void ASeedBedActor::Tick(float DeltaTime)
Super::Tick(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 // 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 // The grid is centered on the actor, so the coordinates are shifted by half a cell
if (const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate)) if (const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate))
{ {
return Slot->SeedBedDecal->GetRelativeLocation(); if (IsValid(Slot->SeedBedDecal))
{
return Slot->SeedBedDecal->GetRelativeLocation();
}
} }
return FVector::ZeroVector; return FVector::ZeroVector;
@@ -243,6 +445,7 @@ FSeedBedSaveData ASeedBedActor::PrepareSeedSaveData()
{ {
FSeedBedSaveData SaveData; FSeedBedSaveData SaveData;
SaveData.Index = SeedBedIndex; SaveData.Index = SeedBedIndex;
SaveData.LastUpdateTime = LastUpdateTime;
for (const auto& Slot : SeedBedSeedSlotsMap) for (const auto& Slot : SeedBedSeedSlotsMap)
{ {
SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData); SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData);
@@ -257,6 +460,7 @@ void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
ensure(ItemDatabase); ensure(ItemDatabase);
SeedBedIndex = SeedSaveData.Index; SeedBedIndex = SeedSaveData.Index;
LastUpdateTime = SeedSaveData.LastUpdateTime;
for (const auto& SavedSlot : SeedSaveData.SlotData) for (const auto& SavedSlot : SeedSaveData.SlotData)
{ {
if (FSeedBedSeedSlot* SeedSlot = SeedBedSeedSlotsMap.Find(SavedSlot.Key)) 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); 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(); RefreshAllSlotVisuals();
} }
@@ -293,6 +500,17 @@ FSeedBedSeedSlot& ASeedBedActor::CreateSlot(FIntVector2 SlotCoordinate, bool bRe
Offset.Y + Y + SeedBedDecalSize.Y, Offset.Y + Y + SeedBedDecalSize.Y,
0.f)); 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); ApplySlotVisuals(SlotCoordinate, Slot);
return Slot; return Slot;
@@ -309,37 +527,52 @@ void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlo
if (IsValid(Slot.SeedBedDecal)) if (IsValid(Slot.SeedBedDecal))
{ {
Slot.SeedBedDecal->SetVisibility(Slot.SlotData.ItemAssetID.IsValid()); 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 ---------------------------------------------------------------------------------------- // 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; return;
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);
}
}
} }
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) void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
{ {
@@ -351,7 +584,7 @@ void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
ApplySlotVisuals(SlotCoordinate, *Slot); ApplySlotVisuals(SlotCoordinate, *Slot);
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasPlant(*Slot)); OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasLivingPlant(*Slot));
} }
void ASeedBedActor::RefreshAllSlotVisuals() void ASeedBedActor::RefreshAllSlotVisuals()
@@ -397,7 +630,8 @@ bool ASeedBedActor::IsSlotFree(FIntVector2 SlotCoordinate) const
{ {
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate); 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 int32 ASeedBedActor::GetFreeSlotCount() const
@@ -406,7 +640,7 @@ int32 ASeedBedActor::GetFreeSlotCount() const
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap) for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{ {
if (!SlotHasPlant(SlotPair.Value)) if (!SlotHasLivingPlant(SlotPair.Value))
{ {
++FreeSlotCount; ++FreeSlotCount;
} }
@@ -421,7 +655,7 @@ bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap) for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{ {
if (SlotHasPlant(SlotPair.Value)) if (!SlotHasLivingPlant(SlotPair.Value))
{ {
continue; 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 // 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.ItemDataAsset = SeedData;
Slot->SlotData.ItemAssetID = SeedData->GetPrimaryAssetId(); Slot->SlotData.ItemAssetID = SeedData->GetPrimaryAssetId();
Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf); Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf);
@@ -507,8 +742,14 @@ bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset*
Slot->SlotData.bWateredToday = false; Slot->SlotData.bWateredToday = false;
Slot->SlotData.bDied = false; Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = bHighQuality; 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); RefreshSlotVisual(SlotCoordinate);
@@ -532,6 +773,7 @@ void ASeedBedActor::ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePl
Slot->SlotData.bWateredToday = false; Slot->SlotData.bWateredToday = false;
Slot->SlotData.bDied = false; Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = false; Slot->SlotData.bHighQuality = false;
Slot->SlotData.LastWateredTime = FTimeOfDayData();
if (bRemovePlantMesh) if (bRemovePlantMesh)
{ {

View File

@@ -4,10 +4,12 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "ProjectEleri/System/TimeOfDayStruct.h"
#include "SeedBedActor.generated.h" #include "SeedBedActor.generated.h"
class ATimeOfDayManager; class ATimeOfDayManager;
class UDecalComponent; class UDecalComponent;
class UMaterialInstanceDynamic;
class UMaterialInterface; class UMaterialInterface;
class UStaticMesh; class UStaticMesh;
class UStaticMeshComponent; class UStaticMeshComponent;
@@ -47,6 +49,10 @@ struct FSeedBedSeedSlotData
UPROPERTY(EditAnywhere, BlueprintReadWrite) UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bHighQuality = false; 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) USTRUCT(BlueprintType)
@@ -77,6 +83,10 @@ struct FSeedBedSaveData
UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TMap<FIntVector2, FSeedBedSeedSlotData> SlotData; 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() UCLASS()
@@ -168,6 +178,14 @@ public:
UFUNCTION(BlueprintCallable, Category = "Seed Bed") UFUNCTION(BlueprintCallable, Category = "Seed Bed")
void ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePlantMesh = true); 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 */ /** Finds the free slot that is closest to the given world location */
UFUNCTION(BlueprintCallable, Category = "Seed Bed") UFUNCTION(BlueprintCallable, Category = "Seed Bed")
bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const; bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const;
@@ -185,6 +203,18 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed")
bool HasFreeSlot() const { return GetFreeSlotCount() > 0; } 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 // VISUALS
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
@@ -228,6 +258,22 @@ protected:
FVector GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const; 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 */ /** Whether the highlight decals of the bed are currently shown */
bool bPlantingHighlightActive = false; 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 */ /** Decal used to highlight this slot when the player is looking for a place to plant a seed */
UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
UDecalComponent* HighlightDecal = nullptr; 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() UPROPERTY()
ATimeOfDayManager* TimeOfDayManager = nullptr; ATimeOfDayManager* TimeOfDayManager = nullptr;

View File

@@ -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) void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData)
{ {
check(InSaveData); check(InSaveData);
@@ -248,6 +283,7 @@ bool AEleriGameState::GetSeedData(int32 Index, FSeedBedSaveData& OutData)
if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index)) if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index))
{ {
OutData = *FoundData; OutData = *FoundData;
return true;
} }
return false; return false;

View File

@@ -53,6 +53,27 @@ void FSaveData::SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSave
SeedBedData.Add(Index, SeedSaveData); 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) void FSaveData::SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors)
{ {
PlacedActors = InPlacedActors; PlacedActors = InPlacedActors;

View File

@@ -17,6 +17,7 @@
#include "Kismet/KismetMaterialLibrary.h" #include "Kismet/KismetMaterialLibrary.h"
#include "Materials/MaterialParameterCollection.h" #include "Materials/MaterialParameterCollection.h"
#include "ProjectEleri/Data/WeatherPresetDataAsset.h" #include "ProjectEleri/Data/WeatherPresetDataAsset.h"
#include "EleriGameState.h"
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h" #include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h" #include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
@@ -120,6 +121,15 @@ void ATimeOfDayManager::Tick(float DeltaTime)
CurrentWeaterFadeAlpha = 1.f; CurrentWeaterFadeAlpha = 1.f;
QueuedWeatherPreset = GetWeatherDistributionValue(); QueuedWeatherPreset = GetWeatherDistributionValue();
K2_WeatherFadeStart(QueuedWeatherPreset->bIsRain); 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 else
{ {

View File

@@ -43,6 +43,10 @@ public:
void CacheSeedData(int32 Index, const FSeedBedSaveData& Data); void CacheSeedData(int32 Index, const FSeedBedSaveData& Data);
bool GetSeedData(int32 Index, FSeedBedSaveData& OutData); 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() UPROPERTY()
FOnPropagateSaveDataComplete OnPropagateSaveDataComplete; FOnPropagateSaveDataComplete OnPropagateSaveDataComplete;

View File

@@ -80,6 +80,8 @@ struct FSaveData {
void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression); void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression);
void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory); void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory);
void SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData); 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 SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors);
void CollectSaveableActorsData(const UWorld* World); void CollectSaveableActorsData(const UWorld* World);
void SetGameEventData(const UWorld* World); void SetGameEventData(const UWorld* World);

View File

@@ -31,4 +31,20 @@ public:
bool operator==(const FTimeOfDayData& Other) const { bool operator==(const FTimeOfDayData& Other) const {
return Year == Other.Year && Month == Other.Month && Day == Other.Day && Hour == Other.Hour && Minute == Other.Minute; 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;
}
}; };