diff --git a/Source/ProjectEleri/Items/DataAssets/ItemDataAsset.h b/Source/ProjectEleri/Items/DataAssets/ItemDataAsset.h index da85ef8c..eaad29aa 100644 --- a/Source/ProjectEleri/Items/DataAssets/ItemDataAsset.h +++ b/Source/ProjectEleri/Items/DataAssets/ItemDataAsset.h @@ -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; diff --git a/Source/ProjectEleri/Items/ItemContainer.h b/Source/ProjectEleri/Items/ItemContainer.h index 0eccea53..c97d1b85 100644 --- a/Source/ProjectEleri/Items/ItemContainer.h +++ b/Source/ProjectEleri/Items/ItemContainer.h @@ -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; diff --git a/Source/ProjectEleri/Items/SeedBedActor.cpp b/Source/ProjectEleri/Items/SeedBedActor.cpp index 9a9cbdc6..41df2b13 100644 --- a/Source/ProjectEleri/Items/SeedBedActor.cpp +++ b/Source/ProjectEleri/Items/SeedBedActor.cpp @@ -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& 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& 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(); diff --git a/Source/ProjectEleri/Items/SeedBedActor.h b/Source/ProjectEleri/Items/SeedBedActor.h index 99101f2c..b637dec4 100644 --- a/Source/ProjectEleri/Items/SeedBedActor.h +++ b/Source/ProjectEleri/Items/SeedBedActor.h @@ -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 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 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 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 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; diff --git a/Source/ProjectEleri/Private/EleriSaveGame.cpp b/Source/ProjectEleri/Private/EleriSaveGame.cpp index b78dd4d5..661aed71 100644 --- a/Source/ProjectEleri/Private/EleriSaveGame.cpp +++ b/Source/ProjectEleri/Private/EleriSaveGame.cpp @@ -67,7 +67,7 @@ void FSaveData::WaterAllPlantedSeeds(const FTimeOfDayData& WateredTime) continue; } - SlotData.bWateredToday = true; + SlotData.MoistureHoursRemaining = SeedBedMoistureHoursPerWatering; SlotData.WateredTimes++; SlotData.LastWateredTime = WateredTime; } diff --git a/Source/ProjectEleri/Private/TimeOfDayManager.cpp b/Source/ProjectEleri/Private/TimeOfDayManager.cpp index 8bae24f7..5d0c6eb1 100644 --- a/Source/ProjectEleri/Private/TimeOfDayManager.cpp +++ b/Source/ProjectEleri/Private/TimeOfDayManager.cpp @@ -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); diff --git a/Source/ProjectEleri/Public/TimeOfDayManager.h b/Source/ProjectEleri/Public/TimeOfDayManager.h index e6404653..ae7fe192 100644 --- a/Source/ProjectEleri/Public/TimeOfDayManager.h +++ b/Source/ProjectEleri/Public/TimeOfDayManager.h @@ -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;