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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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 "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 namespace
ASeedBedActor::ASeedBedActor()
{ {
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. /** Destroys the components of a slot and clears the pointers so nothing stale is left in the map */
PrimaryActorTick.bCanEverTick = true; 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() void ASeedBedActor::BeginPlay()
{ {
Super::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) void ASeedBedActor::Tick(float DeltaTime)
{ {
Super::Tick(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 #pragma once
@@ -6,20 +6,242 @@
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "SeedBedActor.generated.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() UCLASS()
class PROJECTELERI_API ASeedBedActor : public AActor class PROJECTELERI_API ASeedBedActor : public AActor
{ {
GENERATED_BODY() GENERATED_BODY()
public: public:
// Sets default values for this actor's properties
ASeedBedActor(); ASeedBedActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override; 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;
}; };

View File

@@ -6,67 +6,71 @@
#include "MyCharacter.h" #include "MyCharacter.h"
#include "../System/Subsystem/ObjectPersistenceSubsystem.h" #include "../System/Subsystem/ObjectPersistenceSubsystem.h"
#include "EngineUtils.h" #include "EngineUtils.h"
#include "TimeOfDayManager.h"
#include "Components/StatForgeComponent.h" #include "Components/StatForgeComponent.h"
#include "ProjectEleri/System/MainBlueprintFunctionLibrary.h" #include "ProjectEleri/System/MainBlueprintFunctionLibrary.h"
#include "ProjectEleri/GameObjects/EleriBaseActor.h" #include "ProjectEleri/GameObjects/EleriBaseActor.h"
AEleriGameState* AEleriGameState::Get(const UObject* WorldContext) { AEleriGameState* AEleriGameState::Get(const UObject* WorldContext)
if (WorldContext && WorldContext->GetWorld() && WorldContext->GetWorld()->GetGameState()) { {
return Cast<AEleriGameState>(WorldContext->GetWorld()->GetGameState()); if (WorldContext && WorldContext->GetWorld() && WorldContext->GetWorld()->GetGameState())
} {
return nullptr; return Cast<AEleriGameState>(WorldContext->GetWorld()->GetGameState());
}
return nullptr;
} }
int32 AEleriGameState::GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag) { int32 AEleriGameState::GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag)
if (!Get(WorldContext)) return 0; {
if (!Get(WorldContext)) return 0;
if (const int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag)) { if (const int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag))
return *Value; {
} return *Value;
return 0; }
return 0;
} }
int32 AEleriGameState::ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount) { int32 AEleriGameState::ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount)
if (!Get(WorldContext)) return 0; {
if (!Get(WorldContext)) return 0;
if (int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag)) { if (int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag))
*Value += Amount; {
return *Value; *Value += Amount;
} return *Value;
else { }
Get(WorldContext)->FriendshipData.Add(NpcTag, Amount); else
return Amount; {
} Get(WorldContext)->FriendshipData.Add(NpcTag, Amount);
return Amount;
}
} }
void AEleriGameState::SpawnAllSeeds() { void AEleriGameState::PropagateAllSeedData()
{
ItemDatabase = GetWorld()->GetGameInstance()->GetSubsystem<UItemDatabase>(); ItemDatabase = GetWorld()->GetGameInstance()->GetSubsystem<UItemDatabase>();
if (!ItemDatabase) return; if (!ItemDatabase) return;
TArray<AActor*> OutActors;
CurrentSpawnIndex = 0; UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors);
GetWorld()->GetTimerManager().SetTimer(WaitActorSpawnHandle, [this]() { for (AActor* Actor : OutActors)
int32 CurrentLoopIndex = 0; {
for (int32 i = 0; i < SaveData.PlantedSeedsList.Num(); i++) { if (ASeedBedActor* SeedActor = Cast<ASeedBedActor>(Actor))
if (i == this->CurrentSpawnIndex && CurrentLoopIndex < 10 && this->CurrentSpawnIndex < SaveData.PlantedSeedsList.Num()) { {
CurrentLoopIndex++; if (FSeedBedSaveData* SeedSaveData = SaveData.SeedBedData.Find(SeedActor->SeedBedIndex))
this->CurrentSpawnIndex++; {
const FTransform ObjectTransform = SaveData.PlantedSeedsList[i].ObjectTransform; SeedActor->PropagateSeedSaveData(*SeedSaveData);
if (ASeedCarousel* SpawnedSeed = Cast<ASeedCarousel>(UGameplayStatics::BeginDeferredActorSpawnFromClass(GetWorld(), SeedCarouselBpClass, ObjectTransform))) { }
SpawnedSeed->LoadDataFromSaveStruct(SaveData.PlantedSeedsList[i], ItemDatabase); else
UE_LOG(LogTemp, Verbose, TEXT("Spawning actor: %s"), *SpawnedSeed->GetName()); {
UGameplayStatics::FinishSpawningActor(SpawnedSeed, ObjectTransform, ESpawnActorScaleMethod::OverrideRootScale); CacheSeedData(SeedActor->SeedBedIndex, SeedActor->PrepareSeedSaveData());
SpawnedSeed->OnUpdatePlantEvent();
}
} }
} }
if (this->CurrentSpawnIndex == SaveData.PlantedSeedsList.Num() - 1) { }
GetWorld()->GetTimerManager().ClearTimer(this->WaitActorSpawnHandle);
OnPropagateSaveDataComplete.Broadcast();
}
}, 0.1f, true);
} }
void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData) { void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData)
{
check(InSaveData); check(InSaveData);
AMyCharacter* PlayerCharacter = Cast<AMyCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); AMyCharacter* PlayerCharacter = Cast<AMyCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
check(PlayerCharacter); check(PlayerCharacter);
@@ -77,61 +81,56 @@ void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData) {
// Load all inventories // Load all inventories
UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors); UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors);
for (int32 i = 0; i < OutActors.Num(); i++) { for (int32 i = 0; i < OutActors.Num(); i++)
if (UInventoryComponent* Inv = Cast<UInventoryComponent>(OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass()))) { {
if (UInventoryComponent* Inv = Cast<UInventoryComponent>(
OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass())))
{
Inv->LoadFromSave(SaveData.InventoryData[FName(OutActors[i]->GetName())]); Inv->LoadFromSave(SaveData.InventoryData[FName(OutActors[i]->GetName())]);
} }
} }
//Load placed objects //Load placed objects
if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UObjectPersistenceSubsystem>()) { if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<
UObjectPersistenceSubsystem>())
{
ObjectPersistenceSubsystem->LoadSavedData(SaveData.PlacedActors); ObjectPersistenceSubsystem->LoadSavedData(SaveData.PlacedActors);
} }
//Seed delete and spawn should always go last
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedCarousel::StaticClass(), OutActors);
ActorsToDestroy = OutActors;
GetWorld()->GetTimerManager().SetTimer(WaitActorDestructionHandle, [this]() {
for (int i = CurrentSpawnIndex; i <= 100; i++) {
if (i < ActorsToDestroy.Num()) {
UE_LOG(LogTemp, Verbose, TEXT("Destroying actor: %s"), *ActorsToDestroy[i]->GetName());
ActorsToDestroy[i]->Destroy();
CurrentSpawnIndex++;
}
}
if (CurrentSpawnIndex >= ActorsToDestroy.Num()) {
ActorsToDestroy.Empty();
GetWorld()->GetTimerManager().ClearTimer(WaitActorDestructionHandle);
SpawnAllSeeds();
}
}, 0.1f, true);
//Promote actor data //Promote actor data
for (FActorIterator It(GetWorld()); It; ++It) { for (FActorIterator It(GetWorld()); It; ++It)
{
AActor* Actor = *It; AActor* Actor = *It;
if (!Actor->IsValidLowLevel() || Actor->IsPendingKillPending() || !Cast<ISaveableObjectInterface>(Actor) || !Cast<IActorPersistentIdInterface>(Actor)) { if (!Actor->IsValidLowLevel() || Actor->IsPendingKillPending() || !Cast<ISaveableObjectInterface>(Actor) || !
Cast<IActorPersistentIdInterface>(Actor))
{
continue; continue;
} }
if (auto ActorSaveData = SaveData.ActorSaveData.Find(Cast<IActorPersistentIdInterface>(Actor)->GetPersistentId())) { if (auto ActorSaveData = SaveData.ActorSaveData.Find(
Cast<IActorPersistentIdInterface>(Actor)->GetPersistentId()))
{
Cast<ISaveableObjectInterface>(Actor)->RequestLoad(*ActorSaveData); Cast<ISaveableObjectInterface>(Actor)->RequestLoad(*ActorSaveData);
} }
} }
//Game event data //Game event data
if (GetWorld()->GetGameInstance()) { if (GetWorld()->GetGameInstance())
if (UGameEventSubsystem* GES = GetWorld()->GetGameInstance()->GetSubsystem<UGameEventSubsystem>()) { {
if (UGameEventSubsystem* GES = GetWorld()->GetGameInstance()->GetSubsystem<UGameEventSubsystem>())
{
GES->LoadGameEventMapData(SaveData.EventDataMap); GES->LoadGameEventMapData(SaveData.EventDataMap);
} }
} }
//Load player data //Load player data
PlayerCharacter->SetActorTransform(SaveData.PlayerTransform); PlayerCharacter->SetActorTransform(SaveData.PlayerTransform);
if (UStatForgeComponent* StatForgeComponent = UMainBlueprintFunctionLibrary::GetStatForgeComponentFromPlayer(GetWorld())) { if (UStatForgeComponent* StatForgeComponent =
UMainBlueprintFunctionLibrary::GetStatForgeComponentFromPlayer(GetWorld()))
{
StatForgeComponent->RemoveAllGameplayStatEffects(); StatForgeComponent->RemoveAllGameplayStatEffects();
for (const FStatRuntimeDef& Stat : SaveData.StatData) { for (const FStatRuntimeDef& Stat : SaveData.StatData)
{
FGameplayStatEffectMod Mod; FGameplayStatEffectMod Mod;
Mod.AffectedStat = Stat.StatDef.StatName; Mod.AffectedStat = Stat.StatDef.StatName;
Mod.CalculationType = EGameplayStatEffectModCalculation::Flat; Mod.CalculationType = EGameplayStatEffectModCalculation::Flat;
@@ -143,22 +142,28 @@ void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData) {
for (const FActiveGameplayStatEffect_SerializedData& SavedGe : SaveData.GameEffects) for (const FActiveGameplayStatEffect_SerializedData& SavedGe : SaveData.GameEffects)
{ {
const FGuid Id = StatForgeComponent->ApplyGameplayStatEffect(SavedGe.GameplayEffectClass, SavedGe.DataValueMap, SavedGe.StackCount); const FGuid Id = StatForgeComponent->ApplyGameplayStatEffect(
SavedGe.GameplayEffectClass, SavedGe.DataValueMap, SavedGe.StackCount);
StatForgeComponent->ModifyGeDuration(Id, SavedGe.RemainingDuration, SavedGe.NextTickDuration); StatForgeComponent->ModifyGeDuration(Id, SavedGe.RemainingDuration, SavedGe.NextTickDuration);
} }
} }
//Seed delete and spawn should always go last
PropagateAllSeedData();
} }
const FSaveData& AEleriGameState::PrepareSaveData() { const FSaveData& AEleriGameState::PrepareSaveData()
SaveData.Initialize(); {
//SAVE TIME OF DAY //SAVE TIME OF DAY
ATimeOfDayManager* TimeOfDay = Cast<ATimeOfDayManager>(UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass())); ATimeOfDayManager* TimeOfDay = Cast<ATimeOfDayManager>(
UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass()));
check(TimeOfDay); check(TimeOfDay);
SaveData.SetTimeOFDayData(TimeOfDay->GetDaysPassed(), TimeOfDay->GetMonth(), TimeOfDay->GetYear(), TimeOfDay->GetExactMinutes(), TimeOfDay->GetCurrentHours()); SaveData.SetTimeOFDayData(TimeOfDay->GetDaysPassed(), TimeOfDay->GetMonth(), TimeOfDay->GetYear(),
TimeOfDay->GetExactMinutes(), TimeOfDay->GetCurrentHours());
//SAVE BOOK //SAVE BOOK
const ABookOfAlchemyManager* Book = Cast<ABookOfAlchemyManager>(UGameplayStatics::GetActorOfClass(GetWorld(), ABookOfAlchemyManager::StaticClass())); const ABookOfAlchemyManager* Book = Cast<ABookOfAlchemyManager>(
UGameplayStatics::GetActorOfClass(GetWorld(), ABookOfAlchemyManager::StaticClass()));
check(Book); check(Book);
SaveData.SetBookProgressionData(Book->BookProgression); SaveData.SetBookProgressionData(Book->BookProgression);
@@ -166,24 +171,32 @@ const FSaveData& AEleriGameState::PrepareSaveData() {
//SAVE INVENTORIES //SAVE INVENTORIES
UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors); UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors);
for (int32 i = 0; i < OutActors.Num(); i++) { for (int32 i = 0; i < OutActors.Num(); i++)
if (UInventoryComponent* Inv = Cast<UInventoryComponent>(OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass()))) { {
if (UInventoryComponent* Inv = Cast<UInventoryComponent>(
OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass())))
{
SaveData.SetInventoryDataForActor(OutActors[i], Inv); SaveData.SetInventoryDataForActor(OutActors[i], Inv);
} }
} }
//SAVE SEEDS IN WORLD //SAVE PLACED OBJECTS
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedCarousel::StaticClass(), OutActors); if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<
for (int32 i = 0; i < OutActors.Num(); i++) { UObjectPersistenceSubsystem>())
if (ASeedCarousel* SeedActor = Cast<ASeedCarousel>(OutActors[i])) { {
const FSeedSaveData SeedSaveData = FSeedSaveData(SeedActor); SaveData.SetPlacedObjectsData(ObjectPersistenceSubsystem->PrepareSaveData());
SaveData.SetPlantedSeedData(FName(SeedActor->GetName()), SeedSaveData);
}
} }
//SAVE PLACED OBJECTS //SAVE SEEDS
if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem<UObjectPersistenceSubsystem>()) { OutActors.Empty();
SaveData.SetPlacedObjectsData(ObjectPersistenceSubsystem->PrepareSaveData()); UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors);
for (AActor* SeedActor : OutActors)
{
if (ASeedBedActor* SeedBedActor = Cast<ASeedBedActor>(SeedActor))
{
FSeedBedSaveData SeedSaveData = SeedBedActor->PrepareSeedSaveData();
CacheSeedData(SeedSaveData.Index, SeedSaveData);
}
} }
//SAVE ACTORS //SAVE ACTORS
@@ -199,22 +212,43 @@ const FSaveData& AEleriGameState::PrepareSaveData() {
return SaveData; return SaveData;
} }
void AEleriGameState::CacheActorData(AEleriBaseActor* EleriActor) { void AEleriGameState::CacheActorData(AEleriBaseActor* EleriActor)
{
if (!IsValid(EleriActor)) return; if (!IsValid(EleriActor)) return;
if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId())) { if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId()))
{
*FoundActorData = Cast<ISaveableObjectInterface>(EleriActor)->RequestSave(); *FoundActorData = Cast<ISaveableObjectInterface>(EleriActor)->RequestSave();
} }
else { else
SaveData.ActorSaveData.Add(Cast<IActorPersistentIdInterface>(EleriActor)->GetPersistentId(), Cast<ISaveableObjectInterface>(EleriActor)->RequestSave()); {
SaveData.ActorSaveData.Add(Cast<IActorPersistentIdInterface>(EleriActor)->GetPersistentId(),
Cast<ISaveableObjectInterface>(EleriActor)->RequestSave());
} }
} }
bool AEleriGameState::RequestActorSaveData(const AEleriBaseActor* EleriActor, FActorSaveData& OutData) const { bool AEleriGameState::RequestActorSaveData(const AEleriBaseActor* EleriActor, FActorSaveData& OutData) const
{
if (!IsValid(EleriActor)) return false; if (!IsValid(EleriActor)) return false;
if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId())) { if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId()))
{
OutData = *FoundActorData; OutData = *FoundActorData;
return true; return true;
} }
return false; return false;
} }
void AEleriGameState::CacheSeedData(int32 Index, const FSeedBedSaveData& Data)
{
SaveData.SetPlantedSeedData(Index, Data);
}
bool AEleriGameState::GetSeedData(int32 Index, FSeedBedSaveData& OutData)
{
if (const FSeedBedSaveData* FoundData = SaveData.SeedBedData.Find(Index))
{
OutData = *FoundData;
}
return false;
}

View File

@@ -22,6 +22,8 @@
#include "ProjectEleri/DialogueSystem/DialogueWidget.h" #include "ProjectEleri/DialogueSystem/DialogueWidget.h"
#include "Subsystem/InteractionSubsystem.h" #include "Subsystem/InteractionSubsystem.h"
#include "Interface/InteractableActorInterface.h" #include "Interface/InteractableActorInterface.h"
#include "EngineUtils.h"
#include "ProjectEleri/Items/SeedBedActor.h"
AEleriPlayerController::AEleriPlayerController(const FObjectInitializer &ObjectInitializer) AEleriPlayerController::AEleriPlayerController(const FObjectInitializer &ObjectInitializer)
: Super(ObjectInitializer) : Super(ObjectInitializer)
@@ -188,6 +190,11 @@ void AEleriPlayerController::SetupInputComponent()
PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed); PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed);
} }
if (PlantSeedAction)
{
PlayerEnhancedInputComponent->BindAction(PlantSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnPlantSeedAction);
}
if (UiBackAction) if (UiBackAction)
{ {
PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction); PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction);
@@ -1125,7 +1132,7 @@ void AEleriPlayerController::OnInteract(const FInputActionValue &Value)
return; return;
if (PlayerCharacter->GetIsPlanting()) if (PlayerCharacter->GetIsPlanting())
PlayerCharacter->PlantSeed(); TryPlantSelectedSeed();
// Ask the InteractionSystem plugin's subsystem for the closest valid interactable // Ask the InteractionSystem plugin's subsystem for the closest valid interactable
// and fire the interaction on it if we have one. // and fire the interaction on it if we have one.
@@ -1152,6 +1159,115 @@ void AEleriPlayerController::OnHarvestSeed(const FInputActionValue &Value)
{ {
} }
// PLANTING
void AEleriPlayerController::SetSeedToPlant(UItemDataAsset *SeedData, bool bHighQuality)
{
// Anything that is not a seed that fits into a seed bed is rejected, the UI cannot leave us in a bad state
SeedToPlant = ASeedBedActor::IsPlantableSeedData(SeedData) ? SeedData : nullptr;
bSeedToPlantHighQuality = IsValid(SeedToPlant) && bHighQuality;
// Selecting a seed is what starts the planting, the beds in range light up for it right away
if (IsValid(SeedToPlant))
{
SetPlantingMode(true);
}
}
void AEleriPlayerController::ClearSeedToPlant()
{
if (!IsValid(SeedToPlant) && !bSeedToPlantHighQuality)
return;
SeedToPlant = nullptr;
bSeedToPlantHighQuality = false;
// Nothing is being planted anymore, so the beds stop being highlighted
if (IsPlantingModeActive())
{
SetPlantingMode(false);
}
}
bool AEleriPlayerController::IsPlantingModeActive() const
{
return IsValid(PlayerCharacter) && PlayerCharacter->GetIsPlanting();
}
void AEleriPlayerController::SetPlantingMode(bool bActive)
{
if (!IsValid(PlayerCharacter))
return;
PlayerCharacter->SetPlanting(bActive);
// Planting gets its own context so it can take a key over without touching the other contexts
if (IsValid(PlantingContext))
{
if (UEnhancedInputLocalPlayerSubsystem *Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer()))
{
if (bActive)
{
Subsystem->AddMappingContext(PlantingContext, 6);
}
else
{
Subsystem->RemoveMappingContext(PlantingContext);
}
}
}
}
void AEleriPlayerController::TryPlantSelectedSeed()
{
if (!IsValid(PlayerCharacter))
return;
// A seed that is not plantable anymore, for example when the inventory was emptied, drops out of planting
if (!ASeedBedActor::IsPlantableSeedData(SeedToPlant))
{
ClearSeedToPlant();
return;
}
ASeedBedActor *SeedBed = PlayerCharacter->GetClosestSeedBedActor();
if (!IsValid(SeedBed))
return;
const FVector PlayerLocation = PlayerCharacter->GetActorLocation();
FIntVector2 SlotCoordinate = SeedBed->GetHighlightedSlotIndex();
if (SlotCoordinate == FIntVector2::NoneValue)
return;
UInventoryComponent *Inventory = UMainBlueprintFunctionLibrary::GetPlayerInventory(this);
if (!IsValid(Inventory))
return;
// The seed is paid for before it is planted, so a slot can never hold a seed the inventory never had
const FPrimaryAssetId SeedAssetId = SeedToPlant->GetPrimaryAssetId();
if (Inventory->HasItem(SeedAssetId, true, bSeedToPlantHighQuality) <= 0)
return;
if (!SeedBed->PlantSeedInSlot(SlotCoordinate, SeedToPlant, bSeedToPlantHighQuality))
return;
Inventory->RemoveItemFromInventory(SeedAssetId, 1, true, bSeedToPlantHighQuality);
if (Inventory->HasItem(SeedAssetId, true, bSeedToPlantHighQuality) == INDEX_NONE)
{
ClearSeedToPlant();
}
}
void AEleriPlayerController::OnPlantSeedAction(const FInputActionValue &Value)
{
if (!IsValid(PlayerCharacter))
return;
TryPlantSelectedSeed();
}
void AEleriPlayerController::StartMinigame(EMinigameType MinigameType, const TSoftObjectPtr<UBaseMinigameDataAsset> &DataAsset) void AEleriPlayerController::StartMinigame(EMinigameType MinigameType, const TSoftObjectPtr<UBaseMinigameDataAsset> &DataAsset)
{ {
if (!IsValid(PlayerCharacter) || if (!IsValid(PlayerCharacter) ||

View File

@@ -14,7 +14,7 @@ void FSaveData::Initialize()
BookProgression.Empty(); BookProgression.Empty();
InventoryData.Empty(); InventoryData.Empty();
QuestList.Empty(); QuestList.Empty();
PlantedSeedsList.Empty(); SeedBedData.Empty();
PlacedActors.Empty(); PlacedActors.Empty();
ActorSaveData.Empty(); ActorSaveData.Empty();
EventDataMap.Empty(); EventDataMap.Empty();
@@ -46,11 +46,11 @@ void FSaveData::SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inv
InventoryData.Add(ActorID, Inventory->PrepareSaveData()); InventoryData.Add(ActorID, Inventory->PrepareSaveData());
} }
void FSaveData::SetPlantedSeedData(FName ActorId, FSeedSaveData SeedSaveData) void FSaveData::SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData)
{ {
if(ActorId.IsNone()) return; if(Index < 0) return;
PlantedSeedsList.Add(SeedSaveData); SeedBedData.Add(Index, SeedSaveData);
} }
void FSaveData::SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors) void FSaveData::SetPlacedObjectsData(TArray<FPlaceableActorInfo> InPlacedActors)

View File

@@ -17,6 +17,7 @@
#include "Components/CapsuleComponent.h" #include "Components/CapsuleComponent.h"
#include "Components/StatForgeComponent.h" #include "Components/StatForgeComponent.h"
#include "GameFramework/SpringArmComponent.h" #include "GameFramework/SpringArmComponent.h"
#include "ProjectEleri/Items/SeedBedActor.h"
#if WITH_EDITOR #if WITH_EDITOR
@@ -124,12 +125,39 @@ void AMyCharacter::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
// if (CurrentZoomTimer > 0.f) { if (bIsPlanting)
// const float ZoomDelta = (NextZoomOffsetTarget * DeltaTime) / ZoomTime; {
// SpringArmRef->TargetArmLength += ZoomDelta; ASeedBedActor* ClosestActor = nullptr;
// CurrentZoomTimer -= DeltaTime; float ClosestDistance = 9999999999.f;
// UE_LOG(LogTemp, Warning, TEXT("Zooming in: ZoomDelta:%f"), ZoomDelta); const FVector PointToCheck = GetActorLocation() + (GetActorForwardVector() * 100.f);
// } const float ThresholdDist = FMath::Pow(1000.f, 2.f);
for (ASeedBedActor* SeedBeds : SeedBedActors)
{
const double Dist = FVector::DistSquaredXY(PointToCheck, SeedBeds->GetActorLocation());
if (Dist <= ClosestDistance && Dist <= ThresholdDist)
{
ClosestDistance = Dist;
ClosestActor = SeedBeds;
}
}
if (LastClosestSeedBedActor)
{
LastClosestSeedBedActor->StopHighlightingSlot();
}
if (ClosestActor)
{
FIntVector2 OutSlotCoordinate;
if (ClosestActor->FindClosestFreeSlot(PointToCheck, OutSlotCoordinate))
{
ClosestActor->HighlightSlot(OutSlotCoordinate);
}
}
LastClosestSeedBedActor = ClosestActor;
}
} }
void AMyCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason) void AMyCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
@@ -305,9 +333,28 @@ void AMyCharacter::InitiateLoad()
GameInstance->LoadGame(); GameInstance->LoadGame();
} }
void AMyCharacter::PlantSeed() void AMyCharacter::SetPlanting(bool bActive)
{ {
bIs_Planting = !bIs_Planting; bIsPlanting = bActive;
if (bIsPlanting)
{
SeedBedActors.Empty();
TArray<AActor*> OutActors;
UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OutActors);
for (AActor* Actor : OutActors)
{
SeedBedActors.Add(Cast<ASeedBedActor>(Actor));
}
}
else
{
if (LastClosestSeedBedActor)
{
LastClosestSeedBedActor->StopHighlightingSlot();
LastClosestSeedBedActor = nullptr;
}
}
} }
void AMyCharacter::BeginWatering() void AMyCharacter::BeginWatering()

View File

@@ -27,19 +27,21 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
TSubclassOf<ASeedCarousel> SeedCarouselBpClass; TSubclassOf<ASeedCarousel> SeedCarouselBpClass;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Game State", meta=(WorldContext="WorldContext", GameplayTagFilter = "Characters")) UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Game State", meta=(WorldContext="WorldContext", GameplayTagFilter = "Characters"))
static int32 GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag); static int32 GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag);
UFUNCTION(BlueprintCallable, Category = "Game State", meta = (WorldContext = "WorldContext", GameplayTagFilter = "Characters")) UFUNCTION(BlueprintCallable, Category = "Game State", meta = (WorldContext = "WorldContext", GameplayTagFilter = "Characters"))
static int32 ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount); static int32 ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount);
void PropagateSaveData(UEleriSaveGame* InSaveData); void PropagateSaveData(UEleriSaveGame* InSaveData);
void SpawnAllSeeds(); void PropagateAllSeedData();
const FSaveData& PrepareSaveData(); const FSaveData& PrepareSaveData();
//Caches actor data but doesn't save it to disk //Caches actor data but doesn't save it to disk
void CacheActorData(AEleriBaseActor* EleriActor); void CacheActorData(AEleriBaseActor* EleriActor);
bool RequestActorSaveData(const AEleriBaseActor* EleriActor, FActorSaveData& OutData) const; bool RequestActorSaveData(const AEleriBaseActor* EleriActor, FActorSaveData& OutData) const;
void CacheSeedData(int32 Index, const FSeedBedSaveData& Data);
bool GetSeedData(int32 Index, FSeedBedSaveData& OutData);
UPROPERTY() UPROPERTY()
FOnPropagateSaveDataComplete OnPropagateSaveDataComplete; FOnPropagateSaveDataComplete OnPropagateSaveDataComplete;

View File

@@ -17,6 +17,9 @@
#include "EleriPlayerController.generated.h" #include "EleriPlayerController.generated.h"
class ASeedBedActor;
class UItemDataAsset;
UENUM(BlueprintType) UENUM(BlueprintType)
enum class UiInputType : uint8 { enum class UiInputType : uint8 {
NONE, NONE,
@@ -222,6 +225,14 @@ public:
UPROPERTY(EditDefaultsOnly, Category = "Action Input") UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* WaterSeedAction; UInputAction* WaterSeedAction;
/** Action used to plant the selected seed into the closest seed bed slot */
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputAction* PlantSeedAction;
/** Mapping context pushed while planting, it may only contain actions that planting needs */
UPROPERTY(EditDefaultsOnly, Category = "Action Input")
UInputMappingContext* PlantingContext;
UPROPERTY(EditDefaultsOnly, Category = "Book Input") UPROPERTY(EditDefaultsOnly, Category = "Book Input")
UInputAction* BookNextPageAction; UInputAction* BookNextPageAction;
UPROPERTY(EditDefaultsOnly, Category = "Book Input") UPROPERTY(EditDefaultsOnly, Category = "Book Input")
@@ -373,6 +384,41 @@ public:
void CloseMinigame(int32 Score); void CloseMinigame(int32 Score);
FOnMinigameComplete OnMinigameComplete; FOnMinigameComplete OnMinigameComplete;
// PLANTING
/** Seed the player selected in the inventory, it is what the planting action plants */
UFUNCTION(BlueprintCallable)
void SetSeedToPlant(UItemDataAsset* SeedData, bool bHighQuality = false);
/** Forgets the selected seed, the seed beds stop being highlighted for it */
UFUNCTION(BlueprintCallable)
void ClearSeedToPlant();
UFUNCTION(BlueprintCallable, BlueprintPure)
UItemDataAsset* GetSeedToPlant() const { return SeedToPlant; }
UFUNCTION(BlueprintCallable, BlueprintPure)
bool IsSeedToPlantHighQuality() const { return bSeedToPlantHighQuality; }
/** Plants the selected seed into the closest free slot of the closest seed bed, removes it from the inventory */
UFUNCTION(BlueprintCallable)
void TryPlantSelectedSeed();
/** True while the player is in planting mode, the seed beds in range are highlighted then */
UFUNCTION(BlueprintCallable, BlueprintPure)
bool IsPlantingModeActive() const;
/** Enters / leaves planting mode, the highlights of the seed beds in range follow it */
UFUNCTION(BlueprintCallable)
void SetPlantingMode(bool bActive);
UPROPERTY(BlueprintReadOnly, Category = "Planting")
UItemDataAsset* SeedToPlant;
UPROPERTY(BlueprintReadOnly, Category = "Planting")
bool bSeedToPlantHighQuality = false;
void OnPlantSeedAction(const FInputActionValue& Value);
// REMOVE MODE // REMOVE MODE
protected: protected:
public: public:

View File

@@ -4,7 +4,6 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/SaveGame.h" #include "GameFramework/SaveGame.h"
#include "ProjectEleri/DialogueSystem/OrderManager.h"
#include "ProjectEleri/DialogueSystem/QuestDatabase.h" #include "ProjectEleri/DialogueSystem/QuestDatabase.h"
#include "BookOfAlchemyManager.h" #include "BookOfAlchemyManager.h"
#include "InventoryComponent.h" #include "InventoryComponent.h"
@@ -13,6 +12,7 @@
#include "ProjectEleri/GameEventSystem/GameEventSubsystem.h" #include "ProjectEleri/GameEventSystem/GameEventSubsystem.h"
#include "Data/GameplayStatEffect.h" #include "Data/GameplayStatEffect.h"
#include "Data/StatForgeDefs.h" #include "Data/StatForgeDefs.h"
#include "ProjectEleri/Items/SeedBedActor.h"
#include "ProjectEleri/System/TimeOfDayStruct.h" #include "ProjectEleri/System/TimeOfDayStruct.h"
#include "EleriSaveGame.generated.h" #include "EleriSaveGame.generated.h"
@@ -54,7 +54,7 @@ struct FSaveData {
TArray<FQuestItem> QuestList; TArray<FQuestItem> QuestList;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seeds") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seeds")
TArray<FSeedSaveData> PlantedSeedsList; TMap<int32, FSeedBedSaveData> SeedBedData;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placed Objects") UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placed Objects")
TArray<FPlaceableActorInfo> PlacedActors; TArray<FPlaceableActorInfo> PlacedActors;
@@ -79,7 +79,7 @@ struct FSaveData {
void SetTimeOFDayData(int32 _Day, int32 _Month, int32 _Year, int32 _Minute, int32 _Hour); void SetTimeOFDayData(int32 _Day, int32 _Month, int32 _Year, int32 _Minute, int32 _Hour);
void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression); void SetBookProgressionData(TArray<FBookProgressionInfo> _BookProgression);
void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory); void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory);
void SetPlantedSeedData(FName ActorId, FSeedSaveData SeedSaveData); void SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData);
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

@@ -16,6 +16,7 @@
#include "MyCharacter.generated.h" #include "MyCharacter.generated.h"
class ASeedBedActor;
class USpringArmComponent; class USpringArmComponent;
class UStatForgeComponent; class UStatForgeComponent;
class AEleriPlayerController; class AEleriPlayerController;
@@ -51,8 +52,14 @@ protected:
UPROPERTY() UPROPERTY()
AActor* OverlapsWithInteractable; AActor* OverlapsWithInteractable;
//PLANTING STUFF START
UPROPERTY() UPROPERTY()
bool bIs_Planting; bool bIsPlanting = false;
UPROPERTY()
TArray<ASeedBedActor*> SeedBedActors;
UPROPERTY()
ASeedBedActor* LastClosestSeedBedActor = nullptr;
//PLANTING STUFF END
UPROPERTY() UPROPERTY()
AWaterBall* WaterBallSpawned; AWaterBall* WaterBallSpawned;
@@ -226,8 +233,13 @@ public:
UFUNCTION(BlueprintCallable) UFUNCTION(BlueprintCallable)
void InitiateLoad(); void InitiateLoad();
void PlantSeed(); /** Sets the planting flag directly, the player controller uses this to keep both sides in sync */
inline bool GetIsPlanting() { return bIs_Planting; } UFUNCTION(BlueprintCallable)
void SetPlanting(bool bActive);
ASeedBedActor* GetClosestSeedBedActor() { return LastClosestSeedBedActor; }
UFUNCTION(BlueprintCallable, BlueprintPure)
inline bool GetIsPlanting() { return bIsPlanting; }
void BeginWatering(); void BeginWatering();
void EndWatering() const; void EndWatering() const;