// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ProjectEleri/System/TimeOfDayStruct.h" #include "SeedBedActor.generated.h" class UNiagaraSystem; class UNiagaraComponent; class UEleriPaperSpriteComponent; class UPaperSpriteComponent; class ATimeOfDayManager; class UDecalComponent; class UMaterialInstanceDynamic; class UMaterialInterface; class UStaticMesh; class UStaticMeshComponent; class AStaticMeshActor; class UItemDataAsset; struct FSeedStage; /** 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); USTRUCT(BlueprintType) struct FSeedBedSeedSlotData { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Transient) UItemDataAsset* ItemDataAsset; UPROPERTY(EditAnywhere, BlueprintReadWrite) FPrimaryAssetId ItemAssetID; 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 SeedAgeHours = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) int32 WateredTimes = 0; 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) int32 MoistureHoursRemaining = 0; UPROPERTY(EditAnywhere, BlueprintReadWrite) bool bDied = false; 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) struct FSeedBedSeedSlot { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FSeedBedSeedSlotData SlotData; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UDecalComponent* SeedBedDecal = nullptr; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UMaterialInstanceDynamic* SeedBedDynamicMat = nullptr; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UEleriPaperSpriteComponent* PlantSprite = nullptr; /** Mesh actor of a slot that reached a stage which brings a mesh, it is created once for such a stage and only hidden or re-used after that, a stage without a mesh leaves the slot to its sprite */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly) AStaticMeshActor* PlantMeshActor = nullptr; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) UNiagaraComponent* NiagaraComponent = nullptr; }; USTRUCT(BlueprintType) struct FSeedBedSaveData { GENERATED_BODY() UPROPERTY(VisibleAnywhere, BlueprintReadOnly) int32 Index = 0; UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TMap 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() class PROJECTELERI_API ASeedBedActor : public AActor { GENERATED_BODY() public: ASeedBedActor(); virtual void Tick(float DeltaTime) override; //------------------------------------------------------------------------------------------------ // DATA //------------------------------------------------------------------------------------------------ /** Every slot of the seed bed, keyed by its X / Y coordinate inside the grid */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly) TMap SeedBedSeedSlotsMap; UPROPERTY(BlueprintAssignable) FOnSeedBedSlotChanged OnSeedBedSlotChanged; UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) int32 SeedBedIndex = 0; //------------------------------------------------------------------------------------------------ // SEED BED SETUP //------------------------------------------------------------------------------------------------ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup") TSoftObjectPtr SeedBedDecalMaterial; UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup") FVector SeedBedDecalSize = FVector(40.f, 40.f, 40.f); 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(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Setup") TSoftObjectPtr DefaultPlantSpriteMaterial; /** Dimensions of the grid generated by GenerateSeedBedGrid, X = columns, Y = rows */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup") FIntVector2 GridSize = FIntVector2(3, 3); UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup") TSoftObjectPtr PlantBloomedVfx; //------------------------------------------------------------------------------------------------ // HIGHLIGHT DECAL //------------------------------------------------------------------------------------------------ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Highlight") TSoftObjectPtr HighlightDecalMaterial; UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Highlight") FVector HighlightDecalSize = FVector(40.f, 40.f, 40.f); UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Seed Bed|Highlight") FRotator HighlightDecalRotation = FRotator(-90.f, 0.f, 0.f); //------------------------------------------------------------------------------------------------ // GRID GENERATION //------------------------------------------------------------------------------------------------ /** Generates the seed bed grid using the GridSize set up above (editor helper) */ UFUNCTION(BlueprintCallable, CallInEditor, Category = "Seed Bed|Setup") void GenerateSeedBedGrid(); /** Generates / regenerates the seed bed grid with an explicit size (editor helper) */ UFUNCTION(BlueprintCallable, CallInEditor, Category = "Seed Bed|Setup") void GenerateSeedBedGridSized(int32 Columns, int32 Rows, bool bRebuildExistingSlots); /** Destroys every slot of the seed bed and clears the slot map (editor helper) */ UFUNCTION(BlueprintCallable, CallInEditor, Category = "Seed Bed|Setup") void ClearSeedBedGrid(); UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") int32 GetSlotCount() const { return SeedBedSeedSlotsMap.Num(); } //------------------------------------------------------------------------------------------------ // PLANTING //------------------------------------------------------------------------------------------------ /** Plants a seed into the given slot, returns false when the slot is unknown or already taken */ UFUNCTION(BlueprintCallable, Category = "Seed Bed") bool PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset* SeedData, bool bHighQuality = false); /** Empties a slot, optionally removing the plant mesh as well */ 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(); /** Waters every planted slot whose world location lies within the given radius around a world point. * Returns the number of slots that were watered and caches the new slot data into the game state. */ UFUNCTION(BlueprintCallable, Category = "Seed Bed") int32 WaterSlotsInRadius(const FVector& WorldLocation, float Radius); /** Collects the coordinates of every planted slot whose world location lies within the given radius. * Returns the number of collected slots, OutSlotCoordinates is emptied first. */ UFUNCTION(BlueprintCallable, Category = "Seed Bed") int32 GatherSlotsInRadius(const FVector& WorldLocation, float Radius, TArray& OutSlotCoordinates) const; /** 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; /** Same as FindClosestFreeSlot, but also reports how far away the slot is, as a squared distance in XY */ UFUNCTION(BlueprintCallable, Category = "Seed Bed") bool FindClosestFreeSlotWithDistance(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate, float& OutDistanceSquared) const; /** World location a plant of the given slot is placed at */ UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") bool GetSlotWorldLocation(FIntVector2 SlotCoordinate, FVector& OutWorldLocation) const; UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") bool IsSlotFree(FIntVector2 SlotCoordinate) const; UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") int32 GetFreeSlotCount() const; 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 hours to every plant of the bed, used by the hour tick and the resume catch up */ UFUNCTION(BlueprintCallable, Category = "Seed Bed|Time") void AdvanceSeedBedByHours(int32 Hours); //------------------------------------------------------------------------------------------------ // VISUALS //------------------------------------------------------------------------------------------------ UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed|Highlight") bool IsHighlightActive() const { return bPlantingHighlightActive; } /** Re-applies mesh, material and decal of a single slot */ UFUNCTION(BlueprintCallable, Category = "Seed Bed|Visuals") void RefreshSlotVisual(const FIntVector2& SlotCoordinate); /** Re-applies mesh, material and decal of every slot of the bed */ UFUNCTION(BlueprintCallable, Category = "Seed Bed|Visuals") void RefreshAllSlotVisuals(); /** True when the given asset can be planted in a seed bed at all */ UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") static bool IsPlantableSeedData(const UItemDataAsset* SeedData); FIntVector2 GetHighlightedSlotIndex() const { return HighlightedSlotIndex; } void HighlightSlot(FIntVector2 SlotCoordinate); void StopHighlightingSlot(); FSeedBedSaveData PrepareSeedSaveData(); void PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData); protected: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; /** Creates (or rebuilds) the components of a single slot and adds it to the slot map */ FSeedBedSeedSlot& CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting); /** Re-applies material, plant mesh and highlight decal of a slot */ void ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlot& Slot); /** Places, moves or hides the mesh actor of a slot, a null stage or a stage whose mesh has not streamed in yet hides it. The actor is created once for the first stage that brings a mesh and re-used by every later stage */ void ApplySlotPlantMesh(FIntVector2 SlotCoordinate, FSeedBedSeedSlot& Slot, const FSeedStage* Stage); /** Resolves which growth stage mesh of the planted seed has to be shown */ int32 ResolveGrowthStageIndex(const FSeedBedSeedSlot& Slot) const; /** Name of a mesh / decal component of a slot, names have to stay stable for serialization */ static FName GetSlotComponentName(FIntVector2 SlotCoordinate, const TCHAR* Prefix); FVector GetSlotRelativeLocation(FIntVector2 SlotCoordinate) const; /** 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 hour of growth, moisture handling and death checks to a single slot */ void ApplyHourPassedToSlot(FSeedBedSeedSlot& Slot, const FTimeOfDayData& Now, const FIntVector2& SlotCoordinate); /** 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(); /** Hour tick of the time of day actor */ UFUNCTION() void HandleHourPassed(); /** Whether the highlight decals of the bed are currently shown */ bool bPlantingHighlightActive = false; UPROPERTY(Transient) FIntVector2 HighlightedSlotIndex = FIntVector2::NoneValue; /** 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 hour 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; };