From f9384cc3a07c3a6bd63426a2ca325ce9057cdf0b Mon Sep 17 00:00:00 2001 From: "adnan.mujkic" Date: Sun, 13 Sep 2026 23:58:27 +0200 Subject: [PATCH] Some massive reworks for seed planting --- Content/Input/IA_PlantSeedAction.uasset | 3 + Content/Input/IMC_PlantingContext.uasset | 3 + .../InventorySystem/ItemManagerBP.uasset | 4 +- .../Seed/BP_SeedBedActor.uasset | 3 + .../MI_PotGroundDecalMaterial_Inst.uasset | 4 +- .../Seed/MI_SeedBedDecal_Inst.uasset | 3 + .../InventorySystem/Seed/MI_SeedBedMat.uasset | 3 + .../Seed/M_PotGroundDecalMaterial.uasset | 4 +- .../Seed/M_SeedBedDecal.uasset | 3 + .../UI/InventoryScreenWrapper.uasset | 4 +- Content/Textures/Vegetation/T_SeedBedMask.png | 3 + .../Textures/Vegetation/T_SeedBedMask.uasset | 3 + .../Blueprints/MainPlayerController.uasset | 4 +- .../0/61/A6L7ILYYQ9VI7SFIYFHFI5.uasset | 3 + Source/ProjectEleri/Items/SeedBedActor.cpp | 540 +++++++++++++++++- Source/ProjectEleri/Items/SeedBedActor.h | 240 +++++++- .../ProjectEleri/Private/EleriGameState.cpp | 230 ++++---- .../Private/EleriPlayerController.cpp | 118 +++- Source/ProjectEleri/Private/EleriSaveGame.cpp | 8 +- Source/ProjectEleri/Private/MyCharacter.cpp | 63 +- Source/ProjectEleri/Public/EleriGameState.h | 6 +- .../Public/EleriPlayerController.h | 46 ++ Source/ProjectEleri/Public/EleriSaveGame.h | 6 +- Source/ProjectEleri/Public/MyCharacter.h | 18 +- 24 files changed, 1177 insertions(+), 145 deletions(-) create mode 100644 Content/Input/IA_PlantSeedAction.uasset create mode 100644 Content/Input/IMC_PlantingContext.uasset create mode 100644 Content/Scripts/InventorySystem/Seed/BP_SeedBedActor.uasset create mode 100644 Content/Scripts/InventorySystem/Seed/MI_SeedBedDecal_Inst.uasset create mode 100644 Content/Scripts/InventorySystem/Seed/MI_SeedBedMat.uasset create mode 100644 Content/Scripts/InventorySystem/Seed/M_SeedBedDecal.uasset create mode 100644 Content/Textures/Vegetation/T_SeedBedMask.png create mode 100644 Content/Textures/Vegetation/T_SeedBedMask.uasset create mode 100644 Content/__ExternalActors__/Maps/Persistent_GameWorld/0/61/A6L7ILYYQ9VI7SFIYFHFI5.uasset diff --git a/Content/Input/IA_PlantSeedAction.uasset b/Content/Input/IA_PlantSeedAction.uasset new file mode 100644 index 00000000..95e6eb86 --- /dev/null +++ b/Content/Input/IA_PlantSeedAction.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71a10239960ca7b777bcf1274ac04f90920d973a9f0e0194082196596e49a258 +size 1193 diff --git a/Content/Input/IMC_PlantingContext.uasset b/Content/Input/IMC_PlantingContext.uasset new file mode 100644 index 00000000..679e2b72 --- /dev/null +++ b/Content/Input/IMC_PlantingContext.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe9b8badee0f8f4a6427d14a280c2419775d682b4e23e075323315fe679e27b +size 2351 diff --git a/Content/Scripts/InventorySystem/ItemManagerBP.uasset b/Content/Scripts/InventorySystem/ItemManagerBP.uasset index 76a936d6..ed41e3d9 100644 --- a/Content/Scripts/InventorySystem/ItemManagerBP.uasset +++ b/Content/Scripts/InventorySystem/ItemManagerBP.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4571ff43bdb0c5620057a156d1ecf4cce5357abdb6a20623fa2f1fd4916d510 -size 616718 +oid sha256:f3ce125286b18768f30789a4a951a7db3ccfe41fefcdb91830e7c38a87485087 +size 616277 diff --git a/Content/Scripts/InventorySystem/Seed/BP_SeedBedActor.uasset b/Content/Scripts/InventorySystem/Seed/BP_SeedBedActor.uasset new file mode 100644 index 00000000..8bc99865 --- /dev/null +++ b/Content/Scripts/InventorySystem/Seed/BP_SeedBedActor.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eca1bd593ff760890a775b22a64c2bc018eeaf495d4843b51f4159f3a19b4a72 +size 98351 diff --git a/Content/Scripts/InventorySystem/Seed/MI_PotGroundDecalMaterial_Inst.uasset b/Content/Scripts/InventorySystem/Seed/MI_PotGroundDecalMaterial_Inst.uasset index 322efcc0..221fd52f 100644 --- a/Content/Scripts/InventorySystem/Seed/MI_PotGroundDecalMaterial_Inst.uasset +++ b/Content/Scripts/InventorySystem/Seed/MI_PotGroundDecalMaterial_Inst.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdbc48bd52e349940de6285306734bc2d5e7fbe02d11163f68898443ba119228 -size 11287 +oid sha256:c1e25292b560f4db92af5253b8fa49e41df3ddb0f3707d1c0bab5a21231fff89 +size 9058 diff --git a/Content/Scripts/InventorySystem/Seed/MI_SeedBedDecal_Inst.uasset b/Content/Scripts/InventorySystem/Seed/MI_SeedBedDecal_Inst.uasset new file mode 100644 index 00000000..ed1ffcb1 --- /dev/null +++ b/Content/Scripts/InventorySystem/Seed/MI_SeedBedDecal_Inst.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e6b6ead6e22d05ab129f04617305ab4ee3ca4146568b4057fad5715e15de9b3 +size 10156 diff --git a/Content/Scripts/InventorySystem/Seed/MI_SeedBedMat.uasset b/Content/Scripts/InventorySystem/Seed/MI_SeedBedMat.uasset new file mode 100644 index 00000000..e91fae55 --- /dev/null +++ b/Content/Scripts/InventorySystem/Seed/MI_SeedBedMat.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fffb5b2271b8dce594b4b5b19e80733edf30054ae086ba2ce39a68037dc74f4a +size 8978 diff --git a/Content/Scripts/InventorySystem/Seed/M_PotGroundDecalMaterial.uasset b/Content/Scripts/InventorySystem/Seed/M_PotGroundDecalMaterial.uasset index ee195272..65c8758e 100644 --- a/Content/Scripts/InventorySystem/Seed/M_PotGroundDecalMaterial.uasset +++ b/Content/Scripts/InventorySystem/Seed/M_PotGroundDecalMaterial.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f405bec1242b1c8990c61490f7bab90245b937d441c168ae742c4e433d0188f2 -size 24747 +oid sha256:4dfbfd91212c6264cfde4097d9bd4fa0a7e6e107ab1f3cc57faa830a14c3aa7e +size 18886 diff --git a/Content/Scripts/InventorySystem/Seed/M_SeedBedDecal.uasset b/Content/Scripts/InventorySystem/Seed/M_SeedBedDecal.uasset new file mode 100644 index 00000000..bd7eacca --- /dev/null +++ b/Content/Scripts/InventorySystem/Seed/M_SeedBedDecal.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1869e3375e38d2e0ca7b8811ca2ce1ae3e05038cc077f4f7abea441196a1df21 +size 17811 diff --git a/Content/Scripts/InventorySystem/UI/InventoryScreenWrapper.uasset b/Content/Scripts/InventorySystem/UI/InventoryScreenWrapper.uasset index 54016638..986483e3 100644 --- a/Content/Scripts/InventorySystem/UI/InventoryScreenWrapper.uasset +++ b/Content/Scripts/InventorySystem/UI/InventoryScreenWrapper.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5370c3122f7cec749235da6b6e74d5556035f9b769169ba25f377d5ca1c0d0d8 -size 1329204 +oid sha256:648a4a85b97b6c36b9769af3510da534faea40f9bc733522bcd2ab18ea37b750 +size 1336716 diff --git a/Content/Textures/Vegetation/T_SeedBedMask.png b/Content/Textures/Vegetation/T_SeedBedMask.png new file mode 100644 index 00000000..097775fb --- /dev/null +++ b/Content/Textures/Vegetation/T_SeedBedMask.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1289ca911d4bcf235ec34f9dfef6a994c6216676c42f6e6d3798c8651a7b249d +size 146 diff --git a/Content/Textures/Vegetation/T_SeedBedMask.uasset b/Content/Textures/Vegetation/T_SeedBedMask.uasset new file mode 100644 index 00000000..3a3df98b --- /dev/null +++ b/Content/Textures/Vegetation/T_SeedBedMask.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b6b59a3fc74ecee4cca80fdd332622549c8fd7efa9841fe007949c0b0c3c46e +size 11707 diff --git a/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset b/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset index adcd5155..6c851246 100644 --- a/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset +++ b/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ed82e4f24b2a367b23f03b93c8839c25e5beac9834e345a25272ccb6d909751 -size 74975 +oid sha256:590049ccaf27c690fdb492d457300d7ddce3ce238dd298c4ec6829d6cb1c600e +size 75379 diff --git a/Content/__ExternalActors__/Maps/Persistent_GameWorld/0/61/A6L7ILYYQ9VI7SFIYFHFI5.uasset b/Content/__ExternalActors__/Maps/Persistent_GameWorld/0/61/A6L7ILYYQ9VI7SFIYFHFI5.uasset new file mode 100644 index 00000000..cb55b096 --- /dev/null +++ b/Content/__ExternalActors__/Maps/Persistent_GameWorld/0/61/A6L7ILYYQ9VI7SFIYFHFI5.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2daf35d5acaf7f7d5613eed9d3e80e38a36b405c9c63a01764d5b4ed3bbdd818 +size 87993 diff --git a/Source/ProjectEleri/Items/SeedBedActor.cpp b/Source/ProjectEleri/Items/SeedBedActor.cpp index a2c77a19..d96f7163 100644 --- a/Source/ProjectEleri/Items/SeedBedActor.cpp +++ b/Source/ProjectEleri/Items/SeedBedActor.cpp @@ -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(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(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 + TObjectType* ResolveSoftObject(TSoftObjectPtr 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(TEXT("Root")); + Root->SetMobility(EComponentMobility::Movable); + SetRootComponent(Root); } -// Called when the game starts or when spawned void ASeedBedActor::BeginPlay() { Super::BeginPlay(); + HighlightDecal = NewObject(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(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(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 CoordinatesToRemove; + for (const TPair& 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& 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(); + 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& 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& 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& 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); +} diff --git a/Source/ProjectEleri/Items/SeedBedActor.h b/Source/ProjectEleri/Items/SeedBedActor.h index b1f00f7e..250f0549 100644 --- a/Source/ProjectEleri/Items/SeedBedActor.h +++ b/Source/ProjectEleri/Items/SeedBedActor.h @@ -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 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 SeedBedSeedSlotsMap; + + UPROPERTY(BlueprintAssignable) + FOnSeedBedSlotChanged OnSeedBedSlotChanged; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) + int32 SeedBedIndex = 0; + + //------------------------------------------------------------------------------------------------ + // SEED BED SETUP + //------------------------------------------------------------------------------------------------ + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seed Bed|Setup") + TSoftObjectPtr 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 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 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; + }; + diff --git a/Source/ProjectEleri/Private/EleriGameState.cpp b/Source/ProjectEleri/Private/EleriGameState.cpp index be4d5423..b9eb38ea 100644 --- a/Source/ProjectEleri/Private/EleriGameState.cpp +++ b/Source/ProjectEleri/Private/EleriGameState.cpp @@ -6,67 +6,71 @@ #include "MyCharacter.h" #include "../System/Subsystem/ObjectPersistenceSubsystem.h" #include "EngineUtils.h" +#include "TimeOfDayManager.h" #include "Components/StatForgeComponent.h" #include "ProjectEleri/System/MainBlueprintFunctionLibrary.h" #include "ProjectEleri/GameObjects/EleriBaseActor.h" -AEleriGameState* AEleriGameState::Get(const UObject* WorldContext) { - if (WorldContext && WorldContext->GetWorld() && WorldContext->GetWorld()->GetGameState()) { - return Cast(WorldContext->GetWorld()->GetGameState()); - } - return nullptr; +AEleriGameState* AEleriGameState::Get(const UObject* WorldContext) +{ + if (WorldContext && WorldContext->GetWorld() && WorldContext->GetWorld()->GetGameState()) + { + return Cast(WorldContext->GetWorld()->GetGameState()); + } + return nullptr; } -int32 AEleriGameState::GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag) { - if (!Get(WorldContext)) return 0; +int32 AEleriGameState::GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag) +{ + if (!Get(WorldContext)) return 0; - if (const int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag)) { - return *Value; - } - return 0; + if (const int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag)) + { + return *Value; + } + return 0; } -int32 AEleriGameState::ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount) { - if (!Get(WorldContext)) return 0; +int32 AEleriGameState::ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount) +{ + if (!Get(WorldContext)) return 0; - if (int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag)) { - *Value += Amount; - return *Value; - } - else { - Get(WorldContext)->FriendshipData.Add(NpcTag, Amount); - return Amount; - } + if (int32* Value = Get(WorldContext)->FriendshipData.Find(NpcTag)) + { + *Value += Amount; + return *Value; + } + else + { + Get(WorldContext)->FriendshipData.Add(NpcTag, Amount); + return Amount; + } } -void AEleriGameState::SpawnAllSeeds() { +void AEleriGameState::PropagateAllSeedData() +{ ItemDatabase = GetWorld()->GetGameInstance()->GetSubsystem(); if (!ItemDatabase) return; - - CurrentSpawnIndex = 0; - GetWorld()->GetTimerManager().SetTimer(WaitActorSpawnHandle, [this]() { - int32 CurrentLoopIndex = 0; - for (int32 i = 0; i < SaveData.PlantedSeedsList.Num(); i++) { - if (i == this->CurrentSpawnIndex && CurrentLoopIndex < 10 && this->CurrentSpawnIndex < SaveData.PlantedSeedsList.Num()) { - CurrentLoopIndex++; - this->CurrentSpawnIndex++; - const FTransform ObjectTransform = SaveData.PlantedSeedsList[i].ObjectTransform; - if (ASeedCarousel* SpawnedSeed = Cast(UGameplayStatics::BeginDeferredActorSpawnFromClass(GetWorld(), SeedCarouselBpClass, ObjectTransform))) { - SpawnedSeed->LoadDataFromSaveStruct(SaveData.PlantedSeedsList[i], ItemDatabase); - UE_LOG(LogTemp, Verbose, TEXT("Spawning actor: %s"), *SpawnedSeed->GetName()); - UGameplayStatics::FinishSpawningActor(SpawnedSeed, ObjectTransform, ESpawnActorScaleMethod::OverrideRootScale); - SpawnedSeed->OnUpdatePlantEvent(); - } + TArray OutActors; + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors); + for (AActor* Actor : OutActors) + { + if (ASeedBedActor* SeedActor = Cast(Actor)) + { + if (FSeedBedSaveData* SeedSaveData = SaveData.SeedBedData.Find(SeedActor->SeedBedIndex)) + { + SeedActor->PropagateSeedSaveData(*SeedSaveData); + } + else + { + CacheSeedData(SeedActor->SeedBedIndex, SeedActor->PrepareSeedSaveData()); } } - 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); AMyCharacter* PlayerCharacter = Cast(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); check(PlayerCharacter); @@ -77,61 +81,56 @@ void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData) { // Load all inventories UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors); - for (int32 i = 0; i < OutActors.Num(); i++) { - if (UInventoryComponent* Inv = Cast(OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass()))) { + for (int32 i = 0; i < OutActors.Num(); i++) + { + if (UInventoryComponent* Inv = Cast( + OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass()))) + { Inv->LoadFromSave(SaveData.InventoryData[FName(OutActors[i]->GetName())]); } } //Load placed objects - if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) { + if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem< + UObjectPersistenceSubsystem>()) + { 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 - for (FActorIterator It(GetWorld()); It; ++It) { + for (FActorIterator It(GetWorld()); It; ++It) + { AActor* Actor = *It; - if (!Actor->IsValidLowLevel() || Actor->IsPendingKillPending() || !Cast(Actor) || !Cast(Actor)) { + if (!Actor->IsValidLowLevel() || Actor->IsPendingKillPending() || !Cast(Actor) || ! + Cast(Actor)) + { continue; } - if (auto ActorSaveData = SaveData.ActorSaveData.Find(Cast(Actor)->GetPersistentId())) { + if (auto ActorSaveData = SaveData.ActorSaveData.Find( + Cast(Actor)->GetPersistentId())) + { Cast(Actor)->RequestLoad(*ActorSaveData); } } //Game event data - if (GetWorld()->GetGameInstance()) { - if (UGameEventSubsystem* GES = GetWorld()->GetGameInstance()->GetSubsystem()) { + if (GetWorld()->GetGameInstance()) + { + if (UGameEventSubsystem* GES = GetWorld()->GetGameInstance()->GetSubsystem()) + { GES->LoadGameEventMapData(SaveData.EventDataMap); } } //Load player data PlayerCharacter->SetActorTransform(SaveData.PlayerTransform); - if (UStatForgeComponent* StatForgeComponent = UMainBlueprintFunctionLibrary::GetStatForgeComponentFromPlayer(GetWorld())) { + if (UStatForgeComponent* StatForgeComponent = + UMainBlueprintFunctionLibrary::GetStatForgeComponentFromPlayer(GetWorld())) + { StatForgeComponent->RemoveAllGameplayStatEffects(); - for (const FStatRuntimeDef& Stat : SaveData.StatData) { + for (const FStatRuntimeDef& Stat : SaveData.StatData) + { FGameplayStatEffectMod Mod; Mod.AffectedStat = Stat.StatDef.StatName; Mod.CalculationType = EGameplayStatEffectModCalculation::Flat; @@ -143,22 +142,28 @@ void AEleriGameState::PropagateSaveData(UEleriSaveGame* InSaveData) { 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); } } + + //Seed delete and spawn should always go last + PropagateAllSeedData(); } -const FSaveData& AEleriGameState::PrepareSaveData() { - SaveData.Initialize(); - +const FSaveData& AEleriGameState::PrepareSaveData() +{ //SAVE TIME OF DAY - ATimeOfDayManager* TimeOfDay = Cast(UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass())); + ATimeOfDayManager* TimeOfDay = Cast( + UGameplayStatics::GetActorOfClass(GetWorld(), ATimeOfDayManager::StaticClass())); 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 - const ABookOfAlchemyManager* Book = Cast(UGameplayStatics::GetActorOfClass(GetWorld(), ABookOfAlchemyManager::StaticClass())); + const ABookOfAlchemyManager* Book = Cast( + UGameplayStatics::GetActorOfClass(GetWorld(), ABookOfAlchemyManager::StaticClass())); check(Book); SaveData.SetBookProgressionData(Book->BookProgression); @@ -166,24 +171,32 @@ const FSaveData& AEleriGameState::PrepareSaveData() { //SAVE INVENTORIES UGameplayStatics::GetAllActorsWithTag(GetWorld(), INVENTORY_TAG, OutActors); - for (int32 i = 0; i < OutActors.Num(); i++) { - if (UInventoryComponent* Inv = Cast(OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass()))) { + for (int32 i = 0; i < OutActors.Num(); i++) + { + if (UInventoryComponent* Inv = Cast( + OutActors[i]->GetComponentByClass(UInventoryComponent::StaticClass()))) + { SaveData.SetInventoryDataForActor(OutActors[i], Inv); } } - //SAVE SEEDS IN WORLD - UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedCarousel::StaticClass(), OutActors); - for (int32 i = 0; i < OutActors.Num(); i++) { - if (ASeedCarousel* SeedActor = Cast(OutActors[i])) { - const FSeedSaveData SeedSaveData = FSeedSaveData(SeedActor); - SaveData.SetPlantedSeedData(FName(SeedActor->GetName()), SeedSaveData); - } + //SAVE PLACED OBJECTS + if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem< + UObjectPersistenceSubsystem>()) + { + SaveData.SetPlacedObjectsData(ObjectPersistenceSubsystem->PrepareSaveData()); } - //SAVE PLACED OBJECTS - if (UObjectPersistenceSubsystem* ObjectPersistenceSubsystem = GetWorld()->GetGameInstance()->GetSubsystem()) { - SaveData.SetPlacedObjectsData(ObjectPersistenceSubsystem->PrepareSaveData()); + //SAVE SEEDS + OutActors.Empty(); + UGameplayStatics::GetAllActorsOfClass(GetWorld(), ASeedBedActor::StaticClass(), OutActors); + for (AActor* SeedActor : OutActors) + { + if (ASeedBedActor* SeedBedActor = Cast(SeedActor)) + { + FSeedBedSaveData SeedSaveData = SeedBedActor->PrepareSeedSaveData(); + CacheSeedData(SeedSaveData.Index, SeedSaveData); + } } //SAVE ACTORS @@ -199,22 +212,43 @@ const FSaveData& AEleriGameState::PrepareSaveData() { return SaveData; } -void AEleriGameState::CacheActorData(AEleriBaseActor* EleriActor) { +void AEleriGameState::CacheActorData(AEleriBaseActor* EleriActor) +{ if (!IsValid(EleriActor)) return; - if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId())) { + if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId())) + { *FoundActorData = Cast(EleriActor)->RequestSave(); } - else { - SaveData.ActorSaveData.Add(Cast(EleriActor)->GetPersistentId(), Cast(EleriActor)->RequestSave()); + else + { + SaveData.ActorSaveData.Add(Cast(EleriActor)->GetPersistentId(), + Cast(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 (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId())) { + if (auto FoundActorData = SaveData.ActorSaveData.Find(EleriActor->GetPersistentId())) + { OutData = *FoundActorData; return true; } 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; +} diff --git a/Source/ProjectEleri/Private/EleriPlayerController.cpp b/Source/ProjectEleri/Private/EleriPlayerController.cpp index 89f798b6..be63d7b3 100644 --- a/Source/ProjectEleri/Private/EleriPlayerController.cpp +++ b/Source/ProjectEleri/Private/EleriPlayerController.cpp @@ -22,6 +22,8 @@ #include "ProjectEleri/DialogueSystem/DialogueWidget.h" #include "Subsystem/InteractionSubsystem.h" #include "Interface/InteractableActorInterface.h" +#include "EngineUtils.h" +#include "ProjectEleri/Items/SeedBedActor.h" AEleriPlayerController::AEleriPlayerController(const FObjectInitializer &ObjectInitializer) : Super(ObjectInitializer) @@ -188,6 +190,11 @@ void AEleriPlayerController::SetupInputComponent() PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed); } + if (PlantSeedAction) + { + PlayerEnhancedInputComponent->BindAction(PlantSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnPlantSeedAction); + } + if (UiBackAction) { PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction); @@ -1125,7 +1132,7 @@ void AEleriPlayerController::OnInteract(const FInputActionValue &Value) return; if (PlayerCharacter->GetIsPlanting()) - PlayerCharacter->PlantSeed(); + TryPlantSelectedSeed(); // Ask the InteractionSystem plugin's subsystem for the closest valid interactable // 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(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 &DataAsset) { if (!IsValid(PlayerCharacter) || diff --git a/Source/ProjectEleri/Private/EleriSaveGame.cpp b/Source/ProjectEleri/Private/EleriSaveGame.cpp index 5297a327..e84a53e0 100644 --- a/Source/ProjectEleri/Private/EleriSaveGame.cpp +++ b/Source/ProjectEleri/Private/EleriSaveGame.cpp @@ -14,7 +14,7 @@ void FSaveData::Initialize() BookProgression.Empty(); InventoryData.Empty(); QuestList.Empty(); - PlantedSeedsList.Empty(); + SeedBedData.Empty(); PlacedActors.Empty(); ActorSaveData.Empty(); EventDataMap.Empty(); @@ -46,11 +46,11 @@ void FSaveData::SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inv 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 InPlacedActors) diff --git a/Source/ProjectEleri/Private/MyCharacter.cpp b/Source/ProjectEleri/Private/MyCharacter.cpp index fa1a515c..a4b154fe 100644 --- a/Source/ProjectEleri/Private/MyCharacter.cpp +++ b/Source/ProjectEleri/Private/MyCharacter.cpp @@ -17,6 +17,7 @@ #include "Components/CapsuleComponent.h" #include "Components/StatForgeComponent.h" #include "GameFramework/SpringArmComponent.h" +#include "ProjectEleri/Items/SeedBedActor.h" #if WITH_EDITOR @@ -124,12 +125,39 @@ void AMyCharacter::Tick(float DeltaTime) { Super::Tick(DeltaTime); - // if (CurrentZoomTimer > 0.f) { - // const float ZoomDelta = (NextZoomOffsetTarget * DeltaTime) / ZoomTime; - // SpringArmRef->TargetArmLength += ZoomDelta; - // CurrentZoomTimer -= DeltaTime; - // UE_LOG(LogTemp, Warning, TEXT("Zooming in: ZoomDelta:%f"), ZoomDelta); - // } + if (bIsPlanting) + { + ASeedBedActor* ClosestActor = nullptr; + float ClosestDistance = 9999999999.f; + 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) @@ -305,9 +333,28 @@ void AMyCharacter::InitiateLoad() GameInstance->LoadGame(); } -void AMyCharacter::PlantSeed() +void AMyCharacter::SetPlanting(bool bActive) { - bIs_Planting = !bIs_Planting; + bIsPlanting = bActive; + + if (bIsPlanting) + { + SeedBedActors.Empty(); + TArray OutActors; + UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OutActors); + for (AActor* Actor : OutActors) + { + SeedBedActors.Add(Cast(Actor)); + } + } + else + { + if (LastClosestSeedBedActor) + { + LastClosestSeedBedActor->StopHighlightingSlot(); + LastClosestSeedBedActor = nullptr; + } + } } void AMyCharacter::BeginWatering() diff --git a/Source/ProjectEleri/Public/EleriGameState.h b/Source/ProjectEleri/Public/EleriGameState.h index 1aacbe8b..943434ab 100644 --- a/Source/ProjectEleri/Public/EleriGameState.h +++ b/Source/ProjectEleri/Public/EleriGameState.h @@ -27,19 +27,21 @@ public: UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) TSubclassOf SeedCarouselBpClass; - UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Game State", meta=(WorldContext="WorldContext", GameplayTagFilter = "Characters")) static int32 GetFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag); UFUNCTION(BlueprintCallable, Category = "Game State", meta = (WorldContext = "WorldContext", GameplayTagFilter = "Characters")) static int32 ModifyFriendshipAmount(const UObject* WorldContext, FGameplayTag NpcTag, int32 Amount); void PropagateSaveData(UEleriSaveGame* InSaveData); - void SpawnAllSeeds(); + void PropagateAllSeedData(); const FSaveData& PrepareSaveData(); //Caches actor data but doesn't save it to disk void CacheActorData(AEleriBaseActor* EleriActor); bool RequestActorSaveData(const AEleriBaseActor* EleriActor, FActorSaveData& OutData) const; + + void CacheSeedData(int32 Index, const FSeedBedSaveData& Data); + bool GetSeedData(int32 Index, FSeedBedSaveData& OutData); UPROPERTY() FOnPropagateSaveDataComplete OnPropagateSaveDataComplete; diff --git a/Source/ProjectEleri/Public/EleriPlayerController.h b/Source/ProjectEleri/Public/EleriPlayerController.h index a5d6e9bb..699e0a76 100644 --- a/Source/ProjectEleri/Public/EleriPlayerController.h +++ b/Source/ProjectEleri/Public/EleriPlayerController.h @@ -17,6 +17,9 @@ #include "EleriPlayerController.generated.h" +class ASeedBedActor; +class UItemDataAsset; + UENUM(BlueprintType) enum class UiInputType : uint8 { NONE, @@ -222,6 +225,14 @@ public: UPROPERTY(EditDefaultsOnly, Category = "Action Input") 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") UInputAction* BookNextPageAction; UPROPERTY(EditDefaultsOnly, Category = "Book Input") @@ -373,6 +384,41 @@ public: void CloseMinigame(int32 Score); 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 protected: public: diff --git a/Source/ProjectEleri/Public/EleriSaveGame.h b/Source/ProjectEleri/Public/EleriSaveGame.h index 4617c6bb..484eef79 100644 --- a/Source/ProjectEleri/Public/EleriSaveGame.h +++ b/Source/ProjectEleri/Public/EleriSaveGame.h @@ -4,7 +4,6 @@ #include "CoreMinimal.h" #include "GameFramework/SaveGame.h" -#include "ProjectEleri/DialogueSystem/OrderManager.h" #include "ProjectEleri/DialogueSystem/QuestDatabase.h" #include "BookOfAlchemyManager.h" #include "InventoryComponent.h" @@ -13,6 +12,7 @@ #include "ProjectEleri/GameEventSystem/GameEventSubsystem.h" #include "Data/GameplayStatEffect.h" #include "Data/StatForgeDefs.h" +#include "ProjectEleri/Items/SeedBedActor.h" #include "ProjectEleri/System/TimeOfDayStruct.h" #include "EleriSaveGame.generated.h" @@ -54,7 +54,7 @@ struct FSaveData { TArray QuestList; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Seeds") - TArray PlantedSeedsList; + TMap SeedBedData; UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placed Objects") TArray PlacedActors; @@ -79,7 +79,7 @@ struct FSaveData { void SetTimeOFDayData(int32 _Day, int32 _Month, int32 _Year, int32 _Minute, int32 _Hour); void SetBookProgressionData(TArray _BookProgression); void SetInventoryDataForActor(AActor* Actor, UInventoryComponent* Inventory); - void SetPlantedSeedData(FName ActorId, FSeedSaveData SeedSaveData); + void SetPlantedSeedData(int32 Index, const FSeedBedSaveData& SeedSaveData); void SetPlacedObjectsData(TArray InPlacedActors); void CollectSaveableActorsData(const UWorld* World); void SetGameEventData(const UWorld* World); diff --git a/Source/ProjectEleri/Public/MyCharacter.h b/Source/ProjectEleri/Public/MyCharacter.h index 40c473c1..6d387504 100644 --- a/Source/ProjectEleri/Public/MyCharacter.h +++ b/Source/ProjectEleri/Public/MyCharacter.h @@ -16,6 +16,7 @@ #include "MyCharacter.generated.h" +class ASeedBedActor; class USpringArmComponent; class UStatForgeComponent; class AEleriPlayerController; @@ -51,8 +52,14 @@ protected: UPROPERTY() AActor* OverlapsWithInteractable; + //PLANTING STUFF START UPROPERTY() - bool bIs_Planting; + bool bIsPlanting = false; + UPROPERTY() + TArray SeedBedActors; + UPROPERTY() + ASeedBedActor* LastClosestSeedBedActor = nullptr; + //PLANTING STUFF END UPROPERTY() AWaterBall* WaterBallSpawned; @@ -226,8 +233,13 @@ public: UFUNCTION(BlueprintCallable) void InitiateLoad(); - void PlantSeed(); - inline bool GetIsPlanting() { return bIs_Planting; } + /** Sets the planting flag directly, the player controller uses this to keep both sides in sync */ + UFUNCTION(BlueprintCallable) + void SetPlanting(bool bActive); + ASeedBedActor* GetClosestSeedBedActor() { return LastClosestSeedBedActor; } + + UFUNCTION(BlueprintCallable, BlueprintPure) + inline bool GetIsPlanting() { return bIsPlanting; } void BeginWatering(); void EndWatering() const;