Bind to time of day and progress seed age

This commit is contained in:
2026-09-14 13:14:35 +02:00
parent 60c88fd97e
commit f7e5adc1cc
7 changed files with 83 additions and 53 deletions

View File

@@ -156,7 +156,7 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed", AssetRegistrySearchable, meta = (AssetBundles = "Inventory, Book", EditCondition="ItemCategory==Category::Plant", EditConditionHides))
int32 SeedCost;
/** Grow time for a seed, if the seed is a tree or wine this is the growth for them */
/** Grow time for a seed in game days, if the seed is a tree or wine this is the growth for them */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed", AssetRegistrySearchable, meta = (AssetBundles = "Inventory, Book", EditCondition="ItemCategory==Category::Plant", EditConditionHides))
int32 GrowTimeSelf;

View File

@@ -118,6 +118,7 @@ struct FSeedStage {
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FVector MeshScale;
/** In game days after planting at which this stage is shown, the seed bed ages its plants per hour and scales it */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 ChangeOnDay;

View File

@@ -178,7 +178,7 @@ 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->OnDayPassed.RemoveDynamic(this, &ASeedBedActor::HandleDayPassed);
TimeOfDayManager->OnHourPassed.RemoveDynamic(this, &ASeedBedActor::HandleHourPassed);
}
CacheCurrentTimeOfDay();
@@ -209,7 +209,7 @@ void ASeedBedActor::BindToTimeOfDay()
}
// AddUniqueDynamic only ever leaves one binding of the bed on the delegate, even after a reload
TimeOfDayManager->OnDayPassed.AddUniqueDynamic(this, &ASeedBedActor::HandleDayPassed);
TimeOfDayManager->OnHourPassed.AddUniqueDynamic(this, &ASeedBedActor::HandleHourPassed);
}
void ASeedBedActor::CacheCurrentTimeOfDay()
@@ -220,14 +220,14 @@ void ASeedBedActor::CacheCurrentTimeOfDay()
}
}
void ASeedBedActor::HandleDayPassed()
void ASeedBedActor::HandleHourPassed()
{
AdvanceSeedBedByDays(1);
AdvanceSeedBedByHours(1);
}
void ASeedBedActor::AdvanceSeedBedByDays(int32 Days)
void ASeedBedActor::AdvanceSeedBedByHours(int32 Hours)
{
if (Days <= 0)
if (Hours <= 0)
{
return;
}
@@ -241,7 +241,12 @@ void ASeedBedActor::AdvanceSeedBedByDays(int32 Days)
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
const bool bWasAlive = SlotHasLivingPlant(SlotPair.Value);
ApplyDayPassedToSlot(SlotPair.Value, Now);
// 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);
}
if (bWasAlive && !SlotHasLivingPlant(SlotPair.Value))
{
@@ -258,13 +263,13 @@ void ASeedBedActor::AdvanceSeedBedByDays(int32 Days)
}
}
void ASeedBedActor::ApplyDayPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now)
void ASeedBedActor::ApplyHourPassedToSlot(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;
// 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))
{
@@ -272,21 +277,17 @@ void ASeedBedActor::ApplyDayPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDa
}
// 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()
const int32 MinutesSinceWatering = SlotData.LastWateredTime.IsValidTime()
? Now.ToAbsoluteMinutes() - SlotData.LastWateredTime.ToAbsoluteMinutes()
: 0;
if (DaysSinceWatering >= FMath::Max(1, DaysWithoutWaterToDie))
if (MinutesSinceWatering >= FMath::Max(1, DaysWithoutWaterToDie) * SeedBedMinutesPerDay)
{
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));
}
SlotData.SeedAgeHours = FMath::Min(SlotData.SeedAgeHours + 1, FMath::Max(0, SlotData.GrowTime) * SeedBedHoursPerDay);
}
void ASeedBedActor::ApplyOfflineProgression()
@@ -315,11 +316,11 @@ void ASeedBedActor::ApplyOfflineProgression()
return;
}
const int32 DaysPassed = LastKnownTimeOfDay.ToAbsoluteDays() - LastUpdateTime.ToAbsoluteDays();
if (DaysPassed > 0)
const int32 HoursPassed = (LastKnownTimeOfDay.ToAbsoluteMinutes() - LastUpdateTime.ToAbsoluteMinutes()) / SeedBedMinutesPerHour;
if (HoursPassed > 0)
{
// Growth is linear and nothing can be watered while the bed is gone, so a single pass is enough
AdvanceSeedBedByDays(DaysPassed);
// Nothing can be watered while the bed is gone, but the plants still grow and dry hour by hour
AdvanceSeedBedByHours(HoursPassed);
}
else
{
@@ -337,8 +338,8 @@ void ASeedBedActor::WaterSeedInSlot(FIntVector2 SlotCoordinate)
CacheCurrentTimeOfDay();
// Wetness is a per day state, the stamp is what the death check measures the days since
Slot->SlotData.bWateredToday = true;
// 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;
@@ -530,7 +531,7 @@ void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlo
if (IsValid(Slot.SeedBedDynamicMat))
{
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.bWateredToday ? 1.f : 0.f);
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.MoistureHoursRemaining > 0 ? 1.f : 0.f);
}
}
@@ -655,7 +656,7 @@ bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (!SlotHasLivingPlant(SlotPair.Value))
if (SlotHasLivingPlant(SlotPair.Value))
{
continue;
}
@@ -699,12 +700,12 @@ int32 ASeedBedActor::ResolveGrowthStageIndex(const FSeedBedSeedSlot& Slot) const
return INDEX_NONE;
}
// Stages are set up ordered by ChangeOnDay, so the last day that has been reached is the visible one
// Stages are set up per day while the age of a plant counts hours, so the last day that was reached wins
int32 StageIndex = 0;
for (int32 Index = 0; Index < SeedData->SeedStages.Num(); ++Index)
{
if (Slot.SlotData.SeedAge < SeedData->SeedStages[Index].ChangeOnDay)
if (Slot.SlotData.SeedAgeHours < SeedData->SeedStages[Index].ChangeOnDay * SeedBedHoursPerDay)
{
break;
}
@@ -735,11 +736,12 @@ bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset*
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.SeedAge = 0;
Slot->SlotData.SeedAgeHours = 0;
Slot->SlotData.WateredTimes = 0;
Slot->SlotData.bBloomed = false;
Slot->SlotData.bWateredToday = false;
Slot->SlotData.MoistureHoursRemaining = 0;
Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = bHighQuality;
@@ -767,10 +769,10 @@ void ASeedBedActor::ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePl
Slot->SlotData.ItemDataAsset = nullptr;
Slot->SlotData.ItemAssetID = FPrimaryAssetId();
Slot->SlotData.GrowTime = 0;
Slot->SlotData.SeedAge = 0;
Slot->SlotData.SeedAgeHours = 0;
Slot->SlotData.WateredTimes = 0;
Slot->SlotData.bBloomed = false;
Slot->SlotData.bWateredToday = false;
Slot->SlotData.MoistureHoursRemaining = 0;
Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = false;
Slot->SlotData.LastWateredTime = FTimeOfDayData();

View File

@@ -15,6 +15,14 @@ class UStaticMesh;
class UStaticMeshComponent;
class UItemDataAsset;
/** In game time units the seed bed works with, the seed data itself stays authored in days */
constexpr int32 SeedBedHoursPerDay = 24;
constexpr int32 SeedBedMinutesPerHour = 60;
constexpr int32 SeedBedMinutesPerDay = SeedBedHoursPerDay * SeedBedMinutesPerHour;
/** In game growth hours a single watering covers, one watering makes a plant grow for a whole day */
constexpr int32 SeedBedMoistureHoursPerWatering = SeedBedHoursPerDay;
/** Called every time a slot of the seed bed has been planted, cleared or visually refreshed */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSeedBedSlotChanged, FIntVector2, SlotCoordinate, bool, bHasPlant);
@@ -32,8 +40,9 @@ struct FSeedBedSeedSlotData
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 GrowTime = 0;
/** In game hours of growth the plant has collected, the seed data grows for GrowTime days */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 SeedAge = 0;
int32 SeedAgeHours = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 WateredTimes = 0;
@@ -41,8 +50,9 @@ struct FSeedBedSeedSlotData
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bBloomed = false;
/** In game growth hours the last watering still covers, one watering covers a whole in game day */
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bWateredToday = false;
int32 MoistureHoursRemaining = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bDied = false;
@@ -103,7 +113,7 @@ public:
//------------------------------------------------------------------------------------------------
/** Every slot of the seed bed, keyed by its X / Y coordinate inside the grid */
UPROPERTY(EditAnywhere, BlueprintReadOnly)
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TMap<FIntVector2, FSeedBedSeedSlot> SeedBedSeedSlotsMap;
UPROPERTY(BlueprintAssignable)
@@ -116,17 +126,17 @@ public:
// SEED BED SETUP
//------------------------------------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup")
TSoftObjectPtr<UMaterialInterface> SeedBedDecalMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup")
FVector SeedBedDecalSize = FVector(40.f, 40.f, 40.f);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup")
FRotator SeedBedDecalRotation = FRotator(-90.f, 0.f, 0.f);
/** Fallback mesh used when the planted seed has no growth stage set up */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup")
TSoftObjectPtr<UStaticMesh> DefaultPlantMesh;
/** Dimensions of the grid generated by GenerateSeedBedGrid, X = columns, Y = rows */
@@ -137,13 +147,13 @@ public:
// HIGHLIGHT DECAL
//------------------------------------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Highlight")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Highlight")
TSoftObjectPtr<UMaterialInterface> HighlightDecalMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Highlight")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Highlight")
FVector HighlightDecalSize = FVector(40.f, 40.f, 40.f);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Highlight")
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Highlight")
FRotator HighlightDecalRotation = FRotator(-90.f, 0.f, 0.f);
//------------------------------------------------------------------------------------------------
@@ -211,9 +221,9 @@ public:
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 */
/** Applies the given amount of in game hours to every plant of the bed, used by the hour tick and the resume catch up */
UFUNCTION(BlueprintCallable, Category = "Seed Bed|Time")
void AdvanceSeedBedByDays(int32 Days);
void AdvanceSeedBedByHours(int32 Hours);
//------------------------------------------------------------------------------------------------
// VISUALS
@@ -258,11 +268,11 @@ protected:
FVector GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const;
/** Subscribes to the time of day actor, the bed ages its plants on every day that passes */
/** Subscribes to the time of day actor, the bed ages its plants on every in game hour 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);
/** Applies one in game hour of growth, moisture handling and death checks to a single slot */
void ApplyHourPassedToSlot(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();
@@ -270,9 +280,9 @@ protected:
/** 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 */
/** Hour tick of the time of day actor */
UFUNCTION()
void HandleDayPassed();
void HandleHourPassed();
/** Whether the highlight decals of the bed are currently shown */
@@ -289,7 +299,7 @@ protected:
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 */
/** In game time the bed last applied growth for, written on every hour and again when the bed leaves the world */
UPROPERTY(Transient)
FTimeOfDayData LastUpdateTime;

View File

@@ -67,7 +67,7 @@ void FSaveData::WaterAllPlantedSeeds(const FTimeOfDayData& WateredTime)
continue;
}
SlotData.bWateredToday = true;
SlotData.MoistureHoursRemaining = SeedBedMoistureHoursPerWatering;
SlotData.WateredTimes++;
SlotData.LastWateredTime = WateredTime;
}

View File

@@ -399,6 +399,9 @@ void ATimeOfDayManager::AddMinutes(float minutes)
if (!TimeTicking)
return;
// A single call can move the clock over several hours at once (sleep, warps), so the hours are counted
const int32 AbsoluteHoursBefore = (GetAllDaysPassed() * 24) + currentHours;
FloatMinutes += minutes;
currentHours = FMath::Clamp(FMath::Floor(FloatMinutes / 60.f), 0, 24);
@@ -427,6 +430,14 @@ void ATimeOfDayManager::AddMinutes(float minutes)
}
}
// The hour tick is sent after the rollover so a listener already sees the day and hour the step landed on.
// Every hour the clock covered is announced on its own, a clock that jumps backwards announces none.
const int32 AbsoluteHoursAfter = (GetAllDaysPassed() * 24) + currentHours;
for (int32 HourIndex = AbsoluteHoursBefore; HourIndex < AbsoluteHoursAfter; ++HourIndex)
{
OnHourPassed.Broadcast();
}
NextWeatherChangeMinutes -= minutes;
OnMinutesAdded.Broadcast(minutes);

View File

@@ -44,6 +44,7 @@ enum class EWeatherType : uint8
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMinutesAdded, float, Minutes);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTimePassed, FTimeOfDayData, TimeOfDay);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDayPassed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnHourPassed);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnUpdateMajorLightning);
@@ -120,6 +121,11 @@ public:
UPROPERTY(BlueprintAssignable)
FOnDayPassed OnDayPassed;
/** Fired whenever the in game clock crosses an hour, also when a jump of several hours moves it over one.
* Listeners that have to age something on a finer scale than days use this instead of OnDayPassed. */
UPROPERTY(BlueprintAssignable)
FOnHourPassed OnHourPassed;
UPROPERTY(BlueprintAssignable)
FOnMinutesAdded OnMinutesAdded;