Some massive reworks for seed planting

This commit is contained in:
2026-09-13 23:58:27 +02:00
parent 4f029309d5
commit f9384cc3a0
24 changed files with 1177 additions and 145 deletions

View File

@@ -1,26 +1,552 @@
// Fill out your copyright notice in the Description page of Project Settings.
// Fill out your copyright notice in the Description page of Project Settings.
#include "SeedBedActor.h"
#include "EleriGameState.h"
#include "Components/DecalComponent.h"
#include "Components/SceneComponent.h"
#include "Components/StaticMeshComponent.h"
#include "DataAssets/ItemDataAsset.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "ItemContainer.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMaterialLibrary.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
// Sets default values
ASeedBedActor::ASeedBedActor()
namespace
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
/** Destroys the components of a slot and clears the pointers so nothing stale is left in the map */
void DestroySlotComponents(FSeedBedSeedSlot& Slot)
{
if (Slot.SeedBedDecal) Slot.SeedBedDecal->DestroyComponent();
if (Slot.PlantStaticMesh) Slot.PlantStaticMesh->DestroyComponent();
Slot.SeedBedDecal = nullptr;
Slot.PlantStaticMesh = nullptr;
}
/** Slots are created at runtime as well, so the components are created with stable, deterministic names */
UStaticMeshComponent* CreateSlotMeshComponent(AActor* Owner, USceneComponent* AttachParent, FName ComponentName)
{
if (!IsValid(Owner) || !IsValid(AttachParent))
{
return nullptr;
}
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(Owner, ComponentName);
if (!MeshComponent)
{
return nullptr;
}
MeshComponent->SetMobility(EComponentMobility::Movable);
MeshComponent->SetupAttachment(AttachParent);
MeshComponent->RegisterComponent();
Owner->AddInstanceComponent(MeshComponent);
return MeshComponent;
}
UDecalComponent* CreateSlotDecalComponent(AActor* Owner, USceneComponent* AttachParent, FName ComponentName)
{
if (!IsValid(Owner) || !IsValid(AttachParent))
{
return nullptr;
}
UDecalComponent* DecalComponent = NewObject<UDecalComponent>(Owner, ComponentName);
if (!DecalComponent)
{
return nullptr;
}
DecalComponent->SetMobility(EComponentMobility::Movable);
DecalComponent->SetupAttachment(AttachParent);
DecalComponent->RegisterComponent();
DecalComponent->SetVisibility(false);
Owner->AddInstanceComponent(DecalComponent);
return DecalComponent;
}
/** Soft references are filled in by hand, an unset mesh or material must never break the visuals */
template <typename TObjectType>
TObjectType* ResolveSoftObject(TSoftObjectPtr<TObjectType> SoftObject)
{
if (SoftObject.IsNull())
{
return nullptr;
}
return SoftObject.Get() ? SoftObject.Get() : SoftObject.LoadSynchronous();
}
/** A slot counts as planted as soon as an asset or its id is stored, saves may only restore the id */
bool SlotHasPlant(const FSeedBedSeedSlot& Slot)
{
return IsValid(Slot.SlotData.ItemDataAsset) || Slot.SlotData.ItemAssetID.IsValid();
}
/** Growth stages are set up by hand, so a zeroed scale should never make a plant invisible */
FVector SanitizeScale(const FVector& Scale)
{
return FVector(
FMath::IsNearlyZero(Scale.X) ? 1.f : Scale.X,
FMath::IsNearlyZero(Scale.Y) ? 1.f : Scale.Y,
FMath::IsNearlyZero(Scale.Z) ? 1.f : Scale.Z);
}
}
ASeedBedActor::ASeedBedActor()
{
PrimaryActorTick.bCanEverTick = true;
USceneComponent* Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
Root->SetMobility(EComponentMobility::Movable);
SetRootComponent(Root);
}
// Called when the game starts or when spawned
void ASeedBedActor::BeginPlay()
{
Super::BeginPlay();
HighlightDecal = NewObject<UDecalComponent>(this, TEXT("Highlight Decal Component"));
if (HighlightDecal)
{
HighlightDecal->SetMobility(EComponentMobility::Movable);
HighlightDecal->SetupAttachment(RootComponent);
HighlightDecal->RegisterComponent();
HighlightDecal->SetVisibility(false);
HighlightDecal->SetRelativeRotation(HighlightDecalRotation);
AddInstanceComponent(HighlightDecal);
HighlightDecal->SetDecalMaterial(ResolveSoftObject(HighlightDecalMaterial));
}
ClearSeedBedGrid();
GenerateSeedBedGrid();
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this));
ensure(GameState);
FSeedBedSaveData SaveData;
if (GameState->GetSeedData(SeedBedIndex, SaveData))
{
PropagateSeedSaveData(SaveData);
}
else
{
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
}
// The bed can be built in the editor, but slots added through blueprints need their visuals as well
RefreshAllSlotVisuals();
}
void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this));
ensure(GameState);
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
Super::EndPlay(EndPlayReason);
}
void ASeedBedActor::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
RefreshAllSlotVisuals();
}
// Called every frame
void ASeedBedActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
//------------------------------------------------------------------------------------------------
// GRID GENERATION
//------------------------------------------------------------------------------------------------
void ASeedBedActor::GenerateSeedBedGrid()
{
GenerateSeedBedGridSized(GridSize.X, GridSize.Y, true);
}
void ASeedBedActor::GenerateSeedBedGridSized(int32 Columns, int32 Rows, bool bRebuildExistingSlots)
{
const int32 SafeColumns = FMath::Max(1, Columns);
const int32 SafeRows = FMath::Max(1, Rows);
GridSize = FIntVector2(SafeColumns, SafeRows);
if (bRebuildExistingSlots)
{
// Slots falling outside of the new grid are dropped, the rest is rebuilt in place
TArray<FIntVector2> CoordinatesToRemove;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (SlotPair.Key.X < 0 || SlotPair.Key.Y < 0 || SlotPair.Key.X >= SafeColumns || SlotPair.Key.Y >= SafeRows)
{
CoordinatesToRemove.Add(SlotPair.Key);
}
}
for (const FIntVector2& Coordinate : CoordinatesToRemove)
{
if (FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(Coordinate))
{
DestroySlotComponents(*Slot);
}
SeedBedSeedSlotsMap.Remove(Coordinate);
}
}
for (int32 Column = 0; Column < SafeColumns; ++Column)
{
for (int32 Row = 0; Row < SafeRows; ++Row)
{
const FIntVector2 Coordinate(Column, Row);
if (bRebuildExistingSlots || !SeedBedSeedSlotsMap.Contains(Coordinate))
{
CreateSlot(Coordinate, bRebuildExistingSlots);
}
}
}
// Slot visuals also refresh the highlight, so the decals follow the new grid
RefreshAllSlotVisuals();
}
void ASeedBedActor::ClearSeedBedGrid()
{
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
DestroySlotComponents(SlotPair.Value);
}
SeedBedSeedSlotsMap.Empty();
bPlantingHighlightActive = false;
}
FName ASeedBedActor::GetSlotComponentName(FIntVector2 SlotCoordinate, const TCHAR* Prefix)
{
return FName(*FString::Printf(TEXT("%s_%d_%d"), Prefix ? Prefix : TEXT("Slot"), SlotCoordinate.X, SlotCoordinate.Y));
}
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();
}
return FVector::ZeroVector;
}
FSeedBedSaveData ASeedBedActor::PrepareSeedSaveData()
{
FSeedBedSaveData SaveData;
SaveData.Index = SeedBedIndex;
for (const auto& Slot : SeedBedSeedSlotsMap)
{
SaveData.SlotData.Add(Slot.Key, Slot.Value.SlotData);
}
return SaveData;
}
void ASeedBedActor::PropagateSeedSaveData(const FSeedBedSaveData& SeedSaveData)
{
UItemDatabase* ItemDatabase = UGameplayStatics::GetGameInstance(this)->GetSubsystem<UItemDatabase>();
ensure(ItemDatabase);
SeedBedIndex = SeedSaveData.Index;
for (const auto& SavedSlot : SeedSaveData.SlotData)
{
if (FSeedBedSeedSlot* SeedSlot = SeedBedSeedSlotsMap.Find(SavedSlot.Key))
{
SeedSlot->SlotData = SavedSlot.Value;
SeedSlot->SlotData.ItemDataAsset = ItemDatabase->GetDataAssetFromId(SeedSlot->SlotData.ItemAssetID);
}
}
RefreshAllSlotVisuals();
}
FSeedBedSeedSlot& ASeedBedActor::CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting)
{
FSeedBedSeedSlot& Slot = SeedBedSeedSlotsMap.FindOrAdd(SlotCoordinate);
if (bRebuildIfExisting)
{
DestroySlotComponents(Slot);
}
Slot.SeedBedDecal = CreateSlotDecalComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Slot")));
Slot.SeedBedDynamicMat = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, ResolveSoftObject(SeedBedDecalMaterial), NAME_None, EMIDCreationFlags::Transient);
Slot.SeedBedDecal->SetDecalMaterial(Slot.SeedBedDynamicMat);
Slot.SeedBedDecal->DecalSize = SeedBedDecalSize;
Slot.SeedBedDecal->SetRelativeRotation(SeedBedDecalRotation);
const FVector2D Offset = FVector2D(GridSize.X * SeedBedDecalSize.Z, GridSize.Y * SeedBedDecalSize.Y) * -1.f;
const float X = SlotCoordinate.Y * SeedBedDecalSize.Z * 2.f;
const float Y = SlotCoordinate.X * SeedBedDecalSize.Y * 2.f;
Slot.SeedBedDecal->SetRelativeLocation(
FVector(
Offset.X + X + SeedBedDecalSize.Z,
Offset.Y + Y + SeedBedDecalSize.Y,
0.f));
ApplySlotVisuals(SlotCoordinate, Slot);
return Slot;
}
//------------------------------------------------------------------------------------------------
// VISUALS
//------------------------------------------------------------------------------------------------
void ASeedBedActor::ApplySlotVisuals(FIntVector2 SlotCoordinate, FSeedBedSeedSlot& Slot)
{
const FVector SlotLocation = GetSlotRelativeLocation(SlotCoordinate);
// SOIL -----------------------------------------------------------------------------------------
if (IsValid(Slot.SeedBedDecal))
{
Slot.SeedBedDecal->SetVisibility(Slot.SlotData.ItemAssetID.IsValid());
Slot.SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), Slot.SlotData.bWateredToday);
}
// PLANT ----------------------------------------------------------------------------------------
const bool bHasPlant = SlotHasPlant(Slot) && !Slot.SlotData.bDied;
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);
}
}
}
}
void ASeedBedActor::RefreshSlotVisual(FIntVector2 SlotCoordinate)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot)
{
return;
}
ApplySlotVisuals(SlotCoordinate, *Slot);
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, SlotHasPlant(*Slot));
}
void ASeedBedActor::RefreshAllSlotVisuals()
{
for (TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
ApplySlotVisuals(SlotPair.Key, SlotPair.Value);
}
}
bool ASeedBedActor::IsPlantableSeedData(const UItemDataAsset* SeedData)
{
// Only seeds of plants make it into a bed, the meshes are optional and fall back to DefaultPlantMesh
return IsValid(SeedData) && SeedData->IsSeed;
}
void ASeedBedActor::HighlightSlot(FIntVector2 SlotCoordinate)
{
if (!IsSlotFree(SlotCoordinate)) return;
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot) return;
const FVector SlotLocation = GetSlotRelativeLocation(SlotCoordinate);
if (IsValid(HighlightDecal))
{
HighlightDecal->SetRelativeLocation(SlotLocation + FVector(0.f, 0.f, 0.f));
HighlightDecal->DecalSize = HighlightDecalSize;
HighlightDecal->SetVisibility(true);
}
HighlightedSlotIndex = SlotCoordinate;
}
void ASeedBedActor::StopHighlightingSlot()
{
HighlightDecal->SetVisibility(false);
HighlightedSlotIndex = FIntVector2::NoneValue;
}
bool ASeedBedActor::IsSlotFree(FIntVector2 SlotCoordinate) const
{
const FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
return Slot != nullptr && !SlotHasPlant(*Slot);
}
int32 ASeedBedActor::GetFreeSlotCount() const
{
int32 FreeSlotCount = 0;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (!SlotHasPlant(SlotPair.Value))
{
++FreeSlotCount;
}
}
return FreeSlotCount;
}
bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const
{
bool bFoundSlot = false;
float ClosestDistanceSquared = 0.f;
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
if (SlotHasPlant(SlotPair.Value))
{
continue;
}
FVector SlotWorldLocation;
if (!GetSlotWorldLocation(SlotPair.Key, SlotWorldLocation))
{
continue;
}
const float DistanceSquared = FVector::DistSquared(WorldLocation, SlotWorldLocation);
if (!bFoundSlot || DistanceSquared < ClosestDistanceSquared)
{
bFoundSlot = true;
ClosestDistanceSquared = DistanceSquared;
OutSlotCoordinate = SlotPair.Key;
}
}
return bFoundSlot;
}
bool ASeedBedActor::GetSlotWorldLocation(FIntVector2 SlotCoordinate, FVector& OutWorldLocation) const
{
if (!SeedBedSeedSlotsMap.Contains(SlotCoordinate))
{
return false;
}
// The location is derived from the grid and the actor transform, so it is valid without any component
OutWorldLocation = GetActorTransform().TransformPosition(GetSlotRelativeLocation(SlotCoordinate));
return true;
}
int32 ASeedBedActor::ResolveGrowthStageIndex(const FSeedBedSeedSlot& Slot) const
{
const UItemDataAsset* SeedData = Slot.SlotData.ItemDataAsset;
if (!IsValid(SeedData))
{
return INDEX_NONE;
}
// Stages are set up ordered by ChangeOnDay, so the last day that has been reached is the visible one
int32 StageIndex = 0;
for (int32 Index = 0; Index < SeedData->SeedStages.Num(); ++Index)
{
if (Slot.SlotData.SeedAge < SeedData->SeedStages[Index].ChangeOnDay)
{
break;
}
StageIndex = Index;
}
return StageIndex;
}
//------------------------------------------------------------------------------------------------
// PLANTING
//------------------------------------------------------------------------------------------------
bool ASeedBedActor::PlantSeedInSlot(FIntVector2 SlotCoordinate, UItemDataAsset* SeedData, bool bHighQuality)
{
if (!IsSlotFree(SlotCoordinate))
{
return false;
}
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot)
{
return false;
}
// A fresh plant starts at day zero, everything that is stored about it is the state of the new seed
Slot->SlotData.ItemDataAsset = SeedData;
Slot->SlotData.ItemAssetID = SeedData->GetPrimaryAssetId();
Slot->SlotData.GrowTime = FMath::Max(0, SeedData->GrowTimeSelf);
Slot->SlotData.SeedAge = 0;
Slot->SlotData.WateredTimes = 0;
Slot->SlotData.bBloomed = false;
Slot->SlotData.bWateredToday = false;
Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = bHighQuality;
Slot->SeedBedDynamicMat->SetScalarParameterValue(TEXT("Wetness"), 0.f);
RefreshSlotVisual(SlotCoordinate);
return true;
}
void ASeedBedActor::ClearSeedFromSlot(FIntVector2 SlotCoordinate, bool bRemovePlantMesh)
{
FSeedBedSeedSlot* Slot = SeedBedSeedSlotsMap.Find(SlotCoordinate);
if (!Slot)
{
return;
}
Slot->SlotData.ItemDataAsset = nullptr;
Slot->SlotData.ItemAssetID = FPrimaryAssetId();
Slot->SlotData.GrowTime = 0;
Slot->SlotData.SeedAge = 0;
Slot->SlotData.WateredTimes = 0;
Slot->SlotData.bBloomed = false;
Slot->SlotData.bWateredToday = false;
Slot->SlotData.bDied = false;
Slot->SlotData.bHighQuality = false;
if (bRemovePlantMesh)
{
RefreshSlotVisual(SlotCoordinate);
return;
}
// The mesh is left alone on purpose, a harvested plant stays visible until the slot is cleared for real
OnSeedBedSlotChanged.Broadcast(SlotCoordinate, false);
}

View File

@@ -1,4 +1,4 @@
// Fill out your copyright notice in the Description page of Project Settings.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
@@ -6,20 +6,242 @@
#include "GameFramework/Actor.h"
#include "SeedBedActor.generated.h"
class ATimeOfDayManager;
class UDecalComponent;
class UMaterialInterface;
class UStaticMesh;
class UStaticMeshComponent;
class UItemDataAsset;
/** 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;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 SeedAge = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 WateredTimes = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bBloomed = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bWateredToday = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bDied = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bHighQuality = false;
};
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)
UStaticMeshComponent* PlantStaticMesh;
};
USTRUCT(BlueprintType)
struct FSeedBedSaveData
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
int32 Index = 0;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
TMap<FIntVector2, FSeedBedSeedSlotData> SlotData;
};
UCLASS()
class PROJECTELERI_API ASeedBedActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
ASeedBedActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
//------------------------------------------------------------------------------------------------
// DATA
//------------------------------------------------------------------------------------------------
/** Every slot of the seed bed, keyed by its X / Y coordinate inside the grid */
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TMap<FIntVector2, FSeedBedSeedSlot> SeedBedSeedSlotsMap;
UPROPERTY(BlueprintAssignable)
FOnSeedBedSlotChanged OnSeedBedSlotChanged;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
int32 SeedBedIndex = 0;
//------------------------------------------------------------------------------------------------
// SEED BED SETUP
//------------------------------------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
TSoftObjectPtr<UMaterialInterface> SeedBedDecalMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
FVector SeedBedDecalSize = FVector(40.f, 40.f, 40.f);
UPROPERTY(EditAnywhere, 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")
TSoftObjectPtr<UStaticMesh> DefaultPlantMesh;
/** Dimensions of the grid generated by GenerateSeedBedGrid, X = columns, Y = rows */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup")
FIntVector2 GridSize = FIntVector2(3, 3);
//------------------------------------------------------------------------------------------------
// HIGHLIGHT DECAL
//------------------------------------------------------------------------------------------------
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Highlight")
TSoftObjectPtr<UMaterialInterface> HighlightDecalMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Highlight")
FVector HighlightDecalSize = FVector(40.f, 40.f, 40.f);
UPROPERTY(EditAnywhere, 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);
/** 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;
/** 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; }
//------------------------------------------------------------------------------------------------
// 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(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;
virtual void OnConstruction(const FTransform& Transform) 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);
/** 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;
/** 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;
UPROPERTY()
ATimeOfDayManager* TimeOfDayManager = nullptr;
};