Improvements to watering and some stat forge fixes

This commit is contained in:
2026-09-14 21:15:31 +02:00
parent f7e5adc1cc
commit aad09dbaa5
26 changed files with 282 additions and 151 deletions

View File

@@ -195,6 +195,7 @@ ExpIncreaseEffectClass=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/G
GlobalSettingsDataSheet=/Game/GameData/CSV/GlobalKeyValueSettings.GlobalKeyValueSettings GlobalSettingsDataSheet=/Game/GameData/CSV/GlobalKeyValueSettings.GlobalKeyValueSettings
ItemSeedCarouselClass=None ItemSeedCarouselClass=None
RemovableStaticMeshClass=/Script/Engine.BlueprintGeneratedClass'/Game/Prefabs/BP_RemovableStaticMeshActor.BP_RemovableStaticMeshActor_C' RemovableStaticMeshClass=/Script/Engine.BlueprintGeneratedClass'/Game/Prefabs/BP_RemovableStaticMeshActor.BP_RemovableStaticMeshActor_C'
MovementDisableEffectClass=/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/GameEffects/GSE_MovementDisabled.GSE_MovementDisabled_C'
[/Script/InteractionSystem.InteractionSettings] [/Script/InteractionSystem.InteractionSettings]
DefaultInteractionWidgetClass=/Script/UMG.WidgetBlueprintGeneratedClass'/Game/UI/InteractionUI/WBP_Eleri_Interaction.WBP_Eleri_Interaction_C' DefaultInteractionWidgetClass=/Script/UMG.WidgetBlueprintGeneratedClass'/Game/UI/InteractionUI/WBP_Eleri_Interaction.WBP_Eleri_Interaction_C'

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -15,7 +15,7 @@ FActiveGameplayStatEffect FActiveGameplayStatEffect::CreateNew(UObject* SourceOb
ActiveEffect.GameplayEffectClass = Effect->GetClass(); ActiveEffect.GameplayEffectClass = Effect->GetClass();
ActiveEffect.StackCount = Stacks; ActiveEffect.StackCount = Stacks;
ActiveEffect.GameplayEffectInstance = Effect; ActiveEffect.GameplayEffectInstance = Effect;
ActiveEffect.RemainingDuration = Effect->Duration; ActiveEffect.RemainingDuration = Effect->GetDuration();
ActiveEffect.NextTickDuration = Effect->TickPeriod; ActiveEffect.NextTickDuration = Effect->TickPeriod;
ActiveEffect.bIsTicking = ActiveEffect.NextTickDuration > 0.f; ActiveEffect.bIsTicking = ActiveEffect.NextTickDuration > 0.f;
ActiveEffect.Handle = FGuid::NewGuid(); ActiveEffect.Handle = FGuid::NewGuid();
@@ -192,7 +192,7 @@ FGuid UStatForgeComponent::ApplyGameplayStatEffect(const TSubclassOf<UGameplaySt
Effect->AppendDataValueMap(DataValueMap); Effect->AppendDataValueMap(DataValueMap);
FGuid GuidToReturn; FGuid GuidToReturn;
if (Effect->Duration == GameplayStatEffectDuration_Instant) if (Effect->DurationType == EDurationType::Instant)
{ {
Internal_ApplyGameplayStatEffect(Effect); Internal_ApplyGameplayStatEffect(Effect);
} }
@@ -341,20 +341,23 @@ void UStatForgeComponent::TickEffects(float DeltaTime)
{ {
StatNames.Append(GetAffectedStatsFromEffect(ActiveEffectsArray[i].GameplayEffectInstance)); StatNames.Append(GetAffectedStatsFromEffect(ActiveEffectsArray[i].GameplayEffectInstance));
if (!CanTickEffect(ActiveEffectsArray[i].GameplayEffectInstance.Get())) if (ActiveEffectsArray[i].GameplayEffectInstance->DurationType == EDurationType::Duration)
{ {
ActiveEffectsArray[i].RemainingDuration = 0; if (!CanTickEffect(ActiveEffectsArray[i].GameplayEffectInstance.Get()))
} {
else ActiveEffectsArray[i].RemainingDuration = 0;
{ }
ActiveEffectsArray[i].RemainingDuration -= DeltaTime; else
} {
ActiveEffectsArray[i].RemainingDuration -= DeltaTime;
}
if (ActiveEffectsArray[i].RemainingDuration <= 0.f) if (ActiveEffectsArray[i].RemainingDuration <= 0.f)
{ {
RemoveGameplayStatEffect(ActiveEffectsArray[i].Handle); RemoveGameplayStatEffect(ActiveEffectsArray[i].Handle);
continue; continue;
}
} }
if (ActiveEffectsArray[i].GameplayEffectInstance->TickPeriod > 0.f) if (ActiveEffectsArray[i].GameplayEffectInstance->TickPeriod > 0.f)

View File

@@ -15,8 +15,18 @@ float UStatForgeFunctionLibrary::GetStatValue(const FName StatName, AActor* Acto
return StatForgeComponent->GetStatValue(StatName); return StatForgeComponent->GetStatValue(StatName);
} }
bool UStatForgeFunctionLibrary::HasTag(const FGameplayTag& Tag, AActor* Actor)
{
if (!Actor) return false;
const UStatForgeComponent* StatForgeComponent = Actor->GetComponentByClass<UStatForgeComponent>();
if(!StatForgeComponent) return false;
return StatForgeComponent->GetGameplayTags().HasTagExact(Tag);
}
FGuid UStatForgeFunctionLibrary::ApplyGameplayStatEffect(const TSubclassOf<UGameplayStatEffect> EffectClass, FGuid UStatForgeFunctionLibrary::ApplyGameplayStatEffect(const TSubclassOf<UGameplayStatEffect> EffectClass,
TMap<FName, float> DataValueMap, AActor* Actor, int32 Stacks) TMap<FName, float> DataValueMap, AActor* Actor, int32 Stacks)
{ {
if (!EffectClass || !Actor) return FGuid(); if (!EffectClass || !Actor) return FGuid();

View File

@@ -9,6 +9,14 @@
class UGameplayStatEffect; class UGameplayStatEffect;
UENUM(BlueprintType)
enum class EDurationType : uint8
{
Instant,
Duration,
Infinite
};
UENUM(BlueprintType) UENUM(BlueprintType)
enum class EModOperation : uint8 enum class EModOperation : uint8
{ {
@@ -64,9 +72,12 @@ class STATFORGE_API UGameplayStatEffect : public UObject
public: public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Duration") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Duration")
EDurationType DurationType;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Duration", meta=(EditCondition = "DurationType==EDurationType::Duration", EditConditionHides))
float Duration = 0.f; float Duration = 0.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Duration") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Duration", meta=(EditCondition = "DurationType==EDurationType::Duration", EditConditionHides))
float TickPeriod = 0.f; float TickPeriod = 0.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modifiers") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Modifiers")
@@ -78,16 +89,29 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags")
FGameplayTagContainer CharacterTags; FGameplayTagContainer CharacterTags;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags|Requirements", meta = (EditCondition = "Duration==0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags|Requirements", meta = (EditCondition = "DurationType!=EDurationType::Duration"))
FGameplayTagContainer TagsRequiredToApply; FGameplayTagContainer TagsRequiredToApply;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags|Requirements", meta = (EditCondition = "Duration!=0")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tags|Requirements", meta = (EditCondition = "DurationType==EDurationType::Duration"))
FGameplayTagContainer TagsRequiredToTick; FGameplayTagContainer TagsRequiredToTick;
UFUNCTION(BlueprintCallable, Category = "Stat Forge|Effect") UFUNCTION(BlueprintCallable, Category = "Stat Forge|Effect")
void AppendDataValueMap(TMap<FName, float> InDataValueMap) { DataValueMap.Append(InDataValueMap); } void AppendDataValueMap(TMap<FName, float> InDataValueMap) { DataValueMap.Append(InDataValueMap); }
const TMap<FName, float>& GetDataValueMap() { return DataValueMap; } const TMap<FName, float>& GetDataValueMap() { return DataValueMap; }
float GetDuration() const
{
switch (DurationType)
{
case EDurationType::Instant:
return 0.f;
case EDurationType::Duration:
return Duration;
case EDurationType::Infinite:
return -1.f;
}
return 0.f;
}
protected: protected:

View File

@@ -3,6 +3,7 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "Kismet/BlueprintFunctionLibrary.h" #include "Kismet/BlueprintFunctionLibrary.h"
#include "StatForgeFunctionLibrary.generated.h" #include "StatForgeFunctionLibrary.generated.h"
@@ -14,10 +15,13 @@ UCLASS()
class STATFORGE_API UStatForgeFunctionLibrary : public UBlueprintFunctionLibrary class STATFORGE_API UStatForgeFunctionLibrary : public UBlueprintFunctionLibrary
{ {
GENERATED_BODY() GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "Stat Forge") UFUNCTION(BlueprintPure, Category = "Stat Forge")
static float GetStatValue(UPARAM(meta = (GetOptions = "StatForgeDeveloperSettings.GetStatNames")) const FName StatName, AActor* Actor); static float GetStatValue(UPARAM(meta = (GetOptions = "StatForgeDeveloperSettings.GetStatNames")) const FName StatName, AActor* Actor);
UFUNCTION(BlueprintPure, Category = "Stat Forge")
static bool HasTag(UPARAM(ref) const FGameplayTag& Tag, AActor* Actor);
UFUNCTION(BlueprintCallable, Category = "Stat Forge", meta = (AutoCreateRefTerm = "DataValueMap")) UFUNCTION(BlueprintCallable, Category = "Stat Forge", meta = (AutoCreateRefTerm = "DataValueMap"))
static FGuid ApplyGameplayStatEffect(const TSubclassOf<UGameplayStatEffect> EffectClass, TMap<FName, float> DataValueMap, AActor* Actor, int32 Stacks = 1); static FGuid ApplyGameplayStatEffect(const TSubclassOf<UGameplayStatEffect> EffectClass, TMap<FName, float> DataValueMap, AActor* Actor, int32 Stacks = 1);

View File

@@ -15,6 +15,7 @@ STATFORGE_MAKE_STAT(FriendPointsMultiplier)
STATFORGE_MAKE_STAT(AlchemyMultiplier) STATFORGE_MAKE_STAT(AlchemyMultiplier)
STATFORGE_MAKE_STAT(BazarCustomerSpawnRate) STATFORGE_MAKE_STAT(BazarCustomerSpawnRate)
STATFORGE_MAKE_STAT(BazarCustomerWill) STATFORGE_MAKE_STAT(BazarCustomerWill)
STATFORGE_MAKE_STAT(WateringAreaScaleMultiplier)
STATFORGE_MAKE_STAT(AlchemyXp) STATFORGE_MAKE_STAT(AlchemyXp)
STATFORGE_MAKE_STAT(BotanyXp) STATFORGE_MAKE_STAT(BotanyXp)

View File

@@ -17,6 +17,7 @@ STATFORGE_DECLARE_STAT(FriendPointsMultiplier)
STATFORGE_DECLARE_STAT(AlchemyMultiplier) STATFORGE_DECLARE_STAT(AlchemyMultiplier)
STATFORGE_DECLARE_STAT(BazarCustomerSpawnRate) STATFORGE_DECLARE_STAT(BazarCustomerSpawnRate)
STATFORGE_DECLARE_STAT(BazarCustomerWill) STATFORGE_DECLARE_STAT(BazarCustomerWill)
STATFORGE_DECLARE_STAT(WateringAreaScaleMultiplier)
STATFORGE_DECLARE_STAT(AlchemyXp) STATFORGE_DECLARE_STAT(AlchemyXp)
STATFORGE_DECLARE_STAT(BotanyXp) STATFORGE_DECLARE_STAT(BotanyXp)

View File

@@ -1,14 +1,12 @@
// 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 "WaterBall.h" #include "WaterBall.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h" #include "Kismet/KismetMathLibrary.h"
#include "DrawDebugHelpers.h" #include "DrawDebugHelpers.h"
#include "Engine/OverlapResult.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "ProjectEleri/Items/SeedCarousel.h" #include "Helpers/StatForgeFunctionLibrary.h"
#include "ProjectEleri/Data/StatDefinitions.h"
// Sets default values // Sets default values
AWaterBall::AWaterBall() AWaterBall::AWaterBall()
@@ -27,18 +25,14 @@ void AWaterBall::BeginPlay()
Super::BeginPlay(); Super::BeginPlay();
PlayerCharacter = Cast<AMyCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); PlayerCharacter = Cast<AMyCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0));
PlayerCameraManager = Cast<APlayerCameraManager>(UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0)); PlayerCameraManager = Cast<APlayerCameraManager>(UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0));
MaxPitch = FMath::Abs(PlayerCameraManager->ViewPitchMin) + PlayerCameraManager->ViewPitchMax;
if (MaxPitch == 0) MaxPitch = 1;
PitchOffset = FMath::Abs(PlayerCameraManager->ViewPitchMax);
HighlightBox->SetStaticMesh(HighlightBoxMesh); HighlightBox->SetStaticMesh(HighlightBoxMesh);
HighlightBox->SetVisibility(true); HighlightBox->SetVisibility(true);
HighlightBox->SetMaterial(0, HighlightBoxMaterial); HighlightBox->SetMaterial(0, HighlightBoxMaterial);
HighlightBox->SetRelativeScale3D(FVector(10.0f, 10.0f, 10.0f));
HighlightBox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
ActorsToIgnore.Add(this); const float ScaleMultiplier = FMath::Min(UStatForgeFunctionLibrary::GetStatValue(FStat::WateringAreaScaleMultiplier, PlayerCharacter), 1.f);
ActorsToIgnore.Add(PlayerCharacter); HighlightBox->SetRelativeScale3D(FVector(5.0f * ScaleMultiplier));
HighlightBox->SetCollisionEnabled(ECollisionEnabled::NoCollision);
} }
// Called every frame // Called every frame
@@ -52,64 +46,14 @@ void AWaterBall::Tick(float DeltaTime)
HighlightBox->SetVisibility(false); HighlightBox->SetVisibility(false);
if (!PlayerCharacter->IsWatering()) return; if (!PlayerCharacter->IsWatering()) return;
CurrentPitch = PlayerCharacter->Pitch;
CurrentPitch += PitchOffset;
if (CurrentPitch > 100.0f)
{
CurrentPitch -= 360.0f;
}
FPredictProjectilePathParams PredictParams;
float Drag = FMath::Lerp(4.0f, 1.1f, FMath::Abs(CurrentPitch / MaxPitch));
FVector ForwardVec = UKismetMathLibrary::FindLookAtRotation(PlayerCharacter->GetCameraWorldPosition(), PlayerCharacter->GetActorLocation()).Quaternion().GetForwardVector();
FVector Offset = (ForwardVec * FVector(2000.f, 2000.f, 0.f));
FVector EndLocation = PlayerCharacter->GetActorLocation() + Offset;
FVector StartingLoc = PlayerCharacter->GetActorLocation();
PredictParams.StartLocation = StartingLoc;
LaunchVelocity =
UKismetMathLibrary::GetDirectionUnitVector(StartingLoc, EndLocation) * FVector(500.0f * Drag, 500.0f * Drag, -30000.f);
PredictParams.LaunchVelocity = LaunchVelocity;
PredictParams.MaxSimTime = 10.0f;
PredictParams.bTraceWithCollision = true;
PredictParams.SimFrequency = 10.0f;
PredictParams.bTraceComplex = true;
for (int32 i = 0; i < ActorsToIgnore.Num(); i++)
{
PredictParams.ActorsToIgnore.Add(ActorsToIgnore[i]);
}
FPredictProjectilePathResult PredictResults;
UGameplayStatics::PredictProjectilePath(GetWorld(), PredictParams, PredictResults);
HighlightBox->SetVisibility(true); HighlightBox->SetVisibility(true);
FVector HighlightBoxLocation = PredictResults.PathData[PredictResults.PathData.Num() - 1].Location;
HighlightBoxLocation.Z += 5; const FVector ForwardVec = UKismetMathLibrary::FindLookAtRotation(PlayerCharacter->GetCameraWorldPosition(), PlayerCharacter->GetActorLocation()).Quaternion().GetForwardVector();
HighlightBox->SetWorldLocation(HighlightBoxLocation); const FVector Offset = (ForwardVec * FVector(700.f, 700.f, 0.f)) + FVector(0.f, 0.f, -115.f);
HighlightBox->SetWorldLocation(PlayerCharacter->GetActorLocation() + Offset);
} }
TArray<ASeedCarousel*> AWaterBall::GetOverlappingSeeds()
{
TArray<ASeedCarousel*> Seeds;
TArray<FOverlapResult> OverlapResults;
FCollisionObjectQueryParams OverlapParams;
OverlapParams.AddObjectTypesToQuery(ECC_WorldDynamic);
FVector BoundsMin, BoundsMax;
HighlightBox->GetLocalBounds(BoundsMin, BoundsMax);
float Radius = (FVector::Distance(BoundsMin, BoundsMax) * HighlightBox->GetComponentScale().X) / PI;
GetWorld()->OverlapMultiByObjectType(OverlapResults, HighlightBox->GetComponentLocation(), FQuat::Identity, OverlapParams, FCollisionShape::MakeSphere(Radius));
//DrawDebugSphere(GetWorld(), HighlightBox->GetComponentLocation(), Radius, 16, FColor::Red, false, 5.f);
for (const FOverlapResult& OverlapResult : OverlapResults)
{
if (ASeedCarousel* Carousel = Cast<ASeedCarousel>(OverlapResult.GetActor()))
{
Seeds.Add(Carousel);
}
}
return Seeds;
}
void AWaterBall::ThrowBall_Implementation() void AWaterBall::ThrowBall_Implementation()
{ {

View File

@@ -36,10 +36,6 @@ protected:
FVector LaunchVelocity; FVector LaunchVelocity;
public: public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<AActor*> ActorsToIgnore;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
AMyCharacter* PlayerCharacter; AMyCharacter* PlayerCharacter;
@@ -49,15 +45,6 @@ public:
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
UMaterialInstance* HighlightBoxMaterial; UMaterialInstance* HighlightBoxMaterial;
UPROPERTY(BlueprintReadWrite)
float CurrentPitch;
UPROPERTY(BlueprintReadWrite)
float PitchOffset;
UPROPERTY(BlueprintReadWrite)
float MaxPitch;
UPROPERTY(BlueprintReadWrite) UPROPERTY(BlueprintReadWrite)
bool bThrowing; bool bThrowing;
@@ -79,6 +66,4 @@ public:
UFUNCTION(BlueprintCallable) UFUNCTION(BlueprintCallable)
FVector GetLaunchVelocity() { return LaunchVelocity; } FVector GetLaunchVelocity() { return LaunchVelocity; }
UFUNCTION(BlueprintCallable)
TArray<ASeedCarousel*> GetOverlappingSeeds();
}; };

View File

@@ -357,6 +357,68 @@ void ASeedBedActor::WaterAllSeedSlots()
WaterSeedInSlot(SlotCoordinate); WaterSeedInSlot(SlotCoordinate);
} }
} }
int32 ASeedBedActor::GatherSlotsInRadius(const FVector& WorldLocation, float Radius, TArray<FIntVector2>& OutSlotCoordinates) const
{
OutSlotCoordinates.Reset();
// A radius of zero only ever covers the point itself, so there is nothing to gather
if (Radius <= 0.f)
{
return 0;
}
const float RadiusSquared = FMath::Square(Radius);
for (const TPair<FIntVector2, FSeedBedSeedSlot>& SlotPair : SeedBedSeedSlotsMap)
{
// Only living plants are watered, an empty or dead slot has nothing to soak up
if (!SlotHasLivingPlant(SlotPair.Value))
{
continue;
}
FVector SlotWorldLocation;
if (!GetSlotWorldLocation(SlotPair.Key, SlotWorldLocation))
{
continue;
}
// Only the plane of the bed counts, so watering from above or from uphill reaches the same slots
if (FVector::DistSquaredXY(WorldLocation, SlotWorldLocation) > RadiusSquared)
{
continue;
}
OutSlotCoordinates.Add(SlotPair.Key);
}
return OutSlotCoordinates.Num();
}
int32 ASeedBedActor::WaterSlotsInRadius(const FVector& WorldLocation, float Radius)
{
// The slots are gathered first, a listener of the refresh is free to change the map while we water
TArray<FIntVector2> SlotCoordinates;
const int32 WateredSlotCount = GatherSlotsInRadius(WorldLocation, Radius, SlotCoordinates);
for (const FIntVector2& SlotCoordinate : SlotCoordinates)
{
WaterSeedInSlot(SlotCoordinate);
}
// The cache is what the game state falls back to once the bed is streamed out, so it has to know
// about the watering right away. Nothing was watered, so there is nothing to cache either.
if (WateredSlotCount > 0)
{
if (AEleriGameState* GameState = Cast<AEleriGameState>(UGameplayStatics::GetGameState(this)))
{
GameState->CacheSeedData(SeedBedIndex, PrepareSeedSaveData());
}
}
return WateredSlotCount;
}
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
// GRID GENERATION // GRID GENERATION
//------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------
@@ -623,7 +685,12 @@ void ASeedBedActor::HighlightSlot(FIntVector2 SlotCoordinate)
void ASeedBedActor::StopHighlightingSlot() void ASeedBedActor::StopHighlightingSlot()
{ {
HighlightDecal->SetVisibility(false); // The decal only exists once BeginPlay created it, a highlight is stopped on beds that never got one as well
if (IsValid(HighlightDecal))
{
HighlightDecal->SetVisibility(false);
}
HighlightedSlotIndex = FIntVector2::NoneValue; HighlightedSlotIndex = FIntVector2::NoneValue;
} }
@@ -650,6 +717,14 @@ int32 ASeedBedActor::GetFreeSlotCount() const
return FreeSlotCount; return FreeSlotCount;
} }
bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const
{
float DistanceSquared = 0.f;
// The distance of the slot is only interesting for the planting highlight, the plain lookup ignores it
return FindClosestFreeSlotWithDistance(WorldLocation, OutSlotCoordinate, DistanceSquared);
}
bool ASeedBedActor::FindClosestFreeSlotWithDistance(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate, float& OutDistanceSquared) const
{ {
bool bFoundSlot = false; bool bFoundSlot = false;
float ClosestDistanceSquared = 0.f; float ClosestDistanceSquared = 0.f;
@@ -667,7 +742,8 @@ bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector
continue; continue;
} }
const float DistanceSquared = FVector::DistSquared(WorldLocation, SlotWorldLocation); // Only the plane of the bed counts, a player standing uphill has to reach the slot just as well
const float DistanceSquared = FVector::DistSquaredXY(WorldLocation, SlotWorldLocation);
if (!bFoundSlot || DistanceSquared < ClosestDistanceSquared) if (!bFoundSlot || DistanceSquared < ClosestDistanceSquared)
{ {
bFoundSlot = true; bFoundSlot = true;
@@ -676,6 +752,8 @@ bool ASeedBedActor::FindClosestFreeSlot(const FVector& WorldLocation, FIntVector
} }
} }
OutDistanceSquared = ClosestDistanceSquared;
return bFoundSlot; return bFoundSlot;
} }

View File

@@ -196,10 +196,24 @@ public:
UFUNCTION(BlueprintCallable, Category = "Seed Bed") UFUNCTION(BlueprintCallable, Category = "Seed Bed")
void WaterAllSeedSlots(); void WaterAllSeedSlots();
/** Waters every planted slot whose world location lies within the given radius around a world point.
* Returns the number of slots that were watered and caches the new slot data into the game state. */
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
int32 WaterSlotsInRadius(const FVector& WorldLocation, float Radius);
/** Collects the coordinates of every planted slot whose world location lies within the given radius.
* Returns the number of collected slots, OutSlotCoordinates is emptied first. */
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
int32 GatherSlotsInRadius(const FVector& WorldLocation, float Radius, TArray<FIntVector2>& OutSlotCoordinates) const;
/** Finds the free slot that is closest to the given world location */ /** Finds the free slot that is closest to the given world location */
UFUNCTION(BlueprintCallable, Category = "Seed Bed") UFUNCTION(BlueprintCallable, Category = "Seed Bed")
bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const; bool FindClosestFreeSlot(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate) const;
/** Same as FindClosestFreeSlot, but also reports how far away the slot is, as a squared distance in XY */
UFUNCTION(BlueprintCallable, Category = "Seed Bed")
bool FindClosestFreeSlotWithDistance(const FVector& WorldLocation, FIntVector2& OutSlotCoordinate, float& OutDistanceSquared) const;
/** World location a plant of the given slot is placed at */ /** World location a plant of the given slot is placed at */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed") UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Seed Bed")
bool GetSlotWorldLocation(FIntVector2 SlotCoordinate, FVector& OutWorldLocation) const; bool GetSlotWorldLocation(FIntVector2 SlotCoordinate, FVector& OutWorldLocation) const;

View File

@@ -23,7 +23,12 @@
#include "Subsystem/InteractionSubsystem.h" #include "Subsystem/InteractionSubsystem.h"
#include "Interface/InteractableActorInterface.h" #include "Interface/InteractableActorInterface.h"
#include "EngineUtils.h" #include "EngineUtils.h"
#include "Components/StatForgeComponent.h"
#include "Helpers/StatForgeFunctionLibrary.h"
#include "ProjectEleri/Data/StatDefinitions.h"
#include "ProjectEleri/Items/SeedBedActor.h" #include "ProjectEleri/Items/SeedBedActor.h"
#include "ProjectEleri/Settings/EleriGameSettings.h"
#include "ProjectEleri/System/EleriGameplayTags.h"
AEleriPlayerController::AEleriPlayerController(const FObjectInitializer &ObjectInitializer) AEleriPlayerController::AEleriPlayerController(const FObjectInitializer &ObjectInitializer)
: Super(ObjectInitializer) : Super(ObjectInitializer)
@@ -53,6 +58,30 @@ void AEleriPlayerController::BeginPlay()
AlchemyManager = Cast<AAlchemyManager>(UGameplayStatics::GetActorOfClass(GetWorld(), AAlchemyManager::StaticClass())); AlchemyManager = Cast<AAlchemyManager>(UGameplayStatics::GetActorOfClass(GetWorld(), AAlchemyManager::StaticClass()));
SelectionManagerInventorySlots = NewObject<USelectionManager>(); SelectionManagerInventorySlots = NewObject<USelectionManager>();
ToggleMovement(true);
}
void AEleriPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
ToggleMovement(true);
Super::EndPlay(EndPlayReason);
}
void AEleriPlayerController::ToggleMovement(bool bActive)
{
UStatForgeComponent* StatForgeComponent = UMainBlueprintFunctionLibrary::GetStatForgeComponentFromPlayer(this);
if (!StatForgeComponent) return;
if (bActive)
{
StatForgeComponent->RemoveGameplayStatEffect(MovementDisabledEffect);
return;
}
const UEleriGameSettings* GameSettings = UEleriGameSettings::GetEleriGameSettings();
ensure(GameSettings);
MovementDisabledEffect = StatForgeComponent->ApplyGameplayStatEffect(GameSettings->MovementDisableEffectClass, TMap<FName, float>());
} }
void AEleriPlayerController::Tick(float DeltaTime) void AEleriPlayerController::Tick(float DeltaTime)
@@ -332,19 +361,20 @@ void AEleriPlayerController::OnMove(const FInputActionValue &Value)
if (IsInMenu()) if (IsInMenu())
return; return;
FVector2D AnalogueValue = Value.Get<FInputActionValue::Axis2D>(); const FVector2D AnalogueValue = Value.Get<FInputActionValue::Axis2D>();
MovementDirection = AnalogueValue; MovementDirection = AnalogueValue;
if (PlayerCharacter) if (PlayerCharacter)
{ {
FRotator CtrlRotation = GetControlRotation(); if (UStatForgeFunctionLibrary::HasTag(CharacterState::MovementDisabled, PlayerCharacter))
FRotator YawRotation(0, CtrlRotation.Yaw, 0); {
return;
}
// FVector ForwardFacing = UKismetMathLibrary::GetForwardVector(bFlying? PlayerCameraManager->GetCameraRotation() : CharRotForward); const float AppliedMovementVelocity = UStatForgeFunctionLibrary::GetStatValue(FStat::MovementSpeedMultiplier, PlayerCharacter);
float AppliedMovementVelocity = 1.f; // MovementVelocity * (bFlying? 0.5f : 1.f);
FVector ForwardVec = PlayerCharacter->GetPlayerCamera()->GetForwardVector(); const FVector ForwardVec = PlayerCharacter->GetPlayerCamera()->GetForwardVector();
FVector RightVec = PlayerCharacter->GetPlayerCamera()->GetRightVector(); const FVector RightVec = PlayerCharacter->GetPlayerCamera()->GetRightVector();
PlayerCharacter->AddMovementInput(ForwardVec, MovementDirection.Y * AppliedMovementVelocity); PlayerCharacter->AddMovementInput(ForwardVec, MovementDirection.Y * AppliedMovementVelocity);
PlayerCharacter->AddMovementInput(RightVec, MovementDirection.X * AppliedMovementVelocity); PlayerCharacter->AddMovementInput(RightVec, MovementDirection.X * AppliedMovementVelocity);
@@ -446,10 +476,13 @@ void AEleriPlayerController::OnWateringActionStart(const FInputActionValue &Valu
MainGameUI->IsAlchemyOpen() || MainGameUI->IsAlchemyOpen() ||
IsInMenu()) IsInMenu())
return; return;
Watering = true; Watering = true;
PlayerCharacter->BeginWatering(); PlayerCharacter->BeginWatering();
OnWatering.Broadcast(Watering); OnWatering.Broadcast(Watering);
OnChangeInputContext.Broadcast(2); OnChangeInputContext.Broadcast(2);
ToggleMovement(false);
} }
void AEleriPlayerController::OnWateringActionEnd(const FInputActionValue &Value) void AEleriPlayerController::OnWateringActionEnd(const FInputActionValue &Value)
@@ -463,6 +496,8 @@ void AEleriPlayerController::OnWateringActionEnd(const FInputActionValue &Value)
PlayerCharacter->EndWatering(); PlayerCharacter->EndWatering();
OnWatering.Broadcast(Watering); OnWatering.Broadcast(Watering);
OnChangeInputContext.Broadcast(1); OnChangeInputContext.Broadcast(1);
ToggleMovement(true);
} }
void AEleriPlayerController::OnWateringThrowAction(const FInputActionValue &Value) void AEleriPlayerController::OnWateringThrowAction(const FInputActionValue &Value)

View File

@@ -128,19 +128,38 @@ void AMyCharacter::Tick(float DeltaTime)
if (bIsPlanting) if (bIsPlanting)
{ {
// The reach is measured from the closest free slot and not from the center of the bed: a bed is as big as
// its grid, so its outer slots sit up to 1273 units away from that center and would never be in reach
const float SlotReachSquared = FMath::Square(200.f);
ASeedBedActor* ClosestActor = nullptr; ASeedBedActor* ClosestActor = nullptr;
float ClosestDistance = 9999999999.f; FIntVector2 ClosestSlotCoordinate = FIntVector2::NoneValue;
float ClosestSlotDistance = SlotReachSquared;
const FVector PointToCheck = GetActorLocation() + (GetActorForwardVector() * 100.f); const FVector PointToCheck = GetActorLocation() + (GetActorForwardVector() * 100.f);
const float ThresholdDist = FMath::Pow(1000.f, 2.f);
for (ASeedBedActor* SeedBeds : SeedBedActors) for (ASeedBedActor* SeedBeds : SeedBedActors)
{ {
const double Dist = FVector::DistSquaredXY(PointToCheck, SeedBeds->GetActorLocation()); if (!IsValid(SeedBeds))
if (Dist <= ClosestDistance && Dist <= ThresholdDist)
{ {
ClosestDistance = Dist; continue;
ClosestActor = SeedBeds;
} }
FIntVector2 OutSlotCoordinate;
float OutSlotDistance = 0.f;
if (!SeedBeds->FindClosestFreeSlotWithDistance(PointToCheck, OutSlotCoordinate, OutSlotDistance))
{
continue;
}
// A bed is only a candidate while its closest free slot is in reach, the closest slot wins
if (OutSlotDistance > ClosestSlotDistance)
{
continue;
}
ClosestSlotDistance = OutSlotDistance;
ClosestSlotCoordinate = OutSlotCoordinate;
ClosestActor = SeedBeds;
} }
if (LastClosestSeedBedActor) if (LastClosestSeedBedActor)
@@ -150,11 +169,7 @@ void AMyCharacter::Tick(float DeltaTime)
if (ClosestActor) if (ClosestActor)
{ {
FIntVector2 OutSlotCoordinate; ClosestActor->HighlightSlot(ClosestSlotCoordinate);
if (ClosestActor->FindClosestFreeSlot(PointToCheck, OutSlotCoordinate))
{
ClosestActor->HighlightSlot(OutSlotCoordinate);
}
} }
LastClosestSeedBedActor = ClosestActor; LastClosestSeedBedActor = ClosestActor;

View File

@@ -102,14 +102,23 @@ private:
UPROPERTY() UPROPERTY()
USelectionManager* SelectionManagerInventorySlots; USelectionManager* SelectionManagerInventorySlots;
UPROPERTY()
FGuid MovementDisabledEffect;
public: public:
UPROPERTY() UPROPERTY()
bool PlacingStuff; bool PlacingStuff;
protected: protected:
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
public: public:
// Enable or disable movement with a character tag
UFUNCTION(BlueprintCallable)
void ToggleMovement(bool bActive);
UFUNCTION(BlueprintCallable, BlueprintPure) UFUNCTION(BlueprintCallable, BlueprintPure)
FORCEINLINE USelectionManager* GetSelectionManagerInventorySlot() { return SelectionManagerInventorySlots; } FORCEINLINE USelectionManager* GetSelectionManagerInventorySlot() { return SelectionManagerInventorySlots; }

View File

@@ -39,6 +39,8 @@ public:
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Ability System|Effects") UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Ability System|Effects")
TSubclassOf<UGameplayStatEffect> ExpIncreaseEffectClass; TSubclassOf<UGameplayStatEffect> ExpIncreaseEffectClass;
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Ability System|Effects")
TSubclassOf<UGameplayStatEffect> MovementDisableEffectClass;
UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Ability System|Professions") UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Ability System|Professions")
TArray<float> ProfessionExpArray; TArray<float> ProfessionExpArray;

View File

@@ -1,11 +1,18 @@
#include "EleriGameplayTags.h" #include "EleriGameplayTags.h"
namespace TimeOfDay { namespace CharacterState
{
UE_DEFINE_GAMEPLAY_TAG(MovementDisabled, TEXT("CharacterState.MovementDisabled"))
}
namespace TimeOfDay
{
UE_DEFINE_GAMEPLAY_TAG(TimePassed, TEXT("TimeOfDay.TimePassed")) UE_DEFINE_GAMEPLAY_TAG(TimePassed, TEXT("TimeOfDay.TimePassed"))
} }
namespace Stats { namespace Stats
{
UE_DEFINE_GAMEPLAY_TAG(Energy, TEXT("Stats.Energy")) UE_DEFINE_GAMEPLAY_TAG(Energy, TEXT("Stats.Energy"))
UE_DEFINE_GAMEPLAY_TAG(TimesEatenToday, TEXT("Stats.TimesEatenToday")) UE_DEFINE_GAMEPLAY_TAG(TimesEatenToday, TEXT("Stats.TimesEatenToday"))
UE_DEFINE_GAMEPLAY_TAG(FriendPointsObtainAmount, TEXT("Stats.FriendPointsObtainAmount")) UE_DEFINE_GAMEPLAY_TAG(FriendPointsObtainAmount, TEXT("Stats.FriendPointsObtainAmount"))

View File

@@ -2,11 +2,18 @@
#pragma once #pragma once
#include "NativeGameplayTags.h" #include "NativeGameplayTags.h"
namespace TimeOfDay { namespace CharacterState
{
PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(MovementDisabled)
}
namespace TimeOfDay
{
PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TimePassed) PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TimePassed)
} }
namespace Stats { namespace Stats
{
PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Energy) PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Energy)
PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TimesEatenToday) PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TimesEatenToday)
PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(FriendPointsObtainAmount) PROJECTELERI_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(FriendPointsObtainAmount)