From a43fc18456cb21735e9ae93b43d9474e3ba0dbdb Mon Sep 17 00:00:00 2001 From: LongLy Date: Wed, 15 Jan 2025 10:00:06 -0700 Subject: [PATCH] Implemented HealthAttributeSet and VerbMessage --- .../Attributes/OLSHealthAttributeSet.cpp | 243 ++++++++++++++++++ .../ols/Private/Messages/OLSVerbMessage.cpp | 11 + .../Attributes/OLSHealthAttributeSet.h | 96 +++++++ Source/ols/Public/Messages/OLSVerbMessage.h | 38 +++ 4 files changed, 388 insertions(+) create mode 100644 Source/ols/Private/AbilitySystem/Attributes/OLSHealthAttributeSet.cpp create mode 100644 Source/ols/Private/Messages/OLSVerbMessage.cpp create mode 100644 Source/ols/Public/AbilitySystem/Attributes/OLSHealthAttributeSet.h create mode 100644 Source/ols/Public/Messages/OLSVerbMessage.h diff --git a/Source/ols/Private/AbilitySystem/Attributes/OLSHealthAttributeSet.cpp b/Source/ols/Private/AbilitySystem/Attributes/OLSHealthAttributeSet.cpp new file mode 100644 index 0000000..8d505cc --- /dev/null +++ b/Source/ols/Private/AbilitySystem/Attributes/OLSHealthAttributeSet.cpp @@ -0,0 +1,243 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "AbilitySystem/Attributes/OLSHealthAttributeSet.h" + +#include "GameplayEffectExtension.h" +#include "AbilitySystem/OLSAbilitySystemComponent.h" +#include "GameFramework/GameplayMessageSubsystem.h" +#include "Messages/OLSVerbMessage.h" +#include "Net/UnrealNetwork.h" + +UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_Damage, "Gameplay.Damage"); +UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_DamageImmunity, "Gameplay.DamageImmunity"); +UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_DamageSelfDestruct, "Gameplay.Damage.SelfDestruct"); +UE_DEFINE_GAMEPLAY_TAG(TAG_Gameplay_FellOutOfWorld, "Gameplay.Damage.FellOutOfWorld"); +UE_DEFINE_GAMEPLAY_TAG(TAG_OLS_Damage_Message, "Lyra.Damage.Message"); + +UOLSHealthAttributeSet::UOLSHealthAttributeSet() + : Health(100.0f) + , MaxHealth(100.0f) +{ + bIsOutOfHealth = false; + MaxHealthBeforeAttributeChange = 0.0f; + HealthBeforeAttributeChange = 0.0f; +} + +void UOLSHealthAttributeSet::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME_CONDITION_NOTIFY(UOLSHealthAttributeSet, Health, COND_None, REPNOTIFY_Always); + DOREPLIFETIME_CONDITION_NOTIFY(UOLSHealthAttributeSet, MaxHealth, COND_None, REPNOTIFY_Always); +} + +void UOLSHealthAttributeSet::OnRep_Health(const FGameplayAttributeData& oldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UOLSHealthAttributeSet, Health, oldValue); + + // Call the change callback, but without an instigator + // This could be changed to an explicit RPC in the future + // These events on the client should not be changing attributes + + const float currentHealth = GetHealth(); + const float estimatedMagnitude = currentHealth - oldValue.GetCurrentValue(); + + OnHealthChanged.Broadcast( + nullptr, + nullptr, + nullptr, + estimatedMagnitude, + oldValue.GetCurrentValue(), + currentHealth); + + const bool isCurrentHealthEqualOrBelowZero = (currentHealth <= 0.0f); + + if (!bIsOutOfHealth && isCurrentHealthEqualOrBelowZero) + { + OnOutOfHealth.Broadcast(nullptr, nullptr, nullptr, estimatedMagnitude, oldValue.GetCurrentValue(), currentHealth); + } + + bIsOutOfHealth = isCurrentHealthEqualOrBelowZero; +} + +void UOLSHealthAttributeSet::OnRep_MaxHealth(const FGameplayAttributeData& OldValue) +{ + GAMEPLAYATTRIBUTE_REPNOTIFY(UOLSHealthAttributeSet, MaxHealth, OldValue); + + // Call the change callback, but without an instigator + // This could be changed to an explicit RPC in the future + OnMaxHealthChanged.Broadcast( + nullptr, + nullptr, + nullptr, + GetMaxHealth() - OldValue.GetCurrentValue(), + OldValue.GetCurrentValue(), GetMaxHealth()); +} + +bool UOLSHealthAttributeSet::PreGameplayEffectExecute(FGameplayEffectModCallbackData& data) +{ + if (!Super::PreGameplayEffectExecute(data)) + { + return false; + } + + // Handle modifying incoming normal damage + if (data.EvaluatedData.Attribute == GetDamageAttribute()) + { + if (data.EvaluatedData.Magnitude > 0.0f) + { + const bool isDamageFromSelfDestruct = data.EffectSpec.GetDynamicAssetTags().HasTagExact(TAG_Gameplay_DamageSelfDestruct); + + if (data.Target.HasMatchingGameplayTag(TAG_Gameplay_DamageImmunity) && !isDamageFromSelfDestruct) + { + // Do not take away any health. + data.EvaluatedData.Magnitude = 0.0f; + return false; + } + +#if !UE_BUILD_SHIPPING + // Check GodMode cheat, unlimited health is checked below + // if (data.Target.HasMatchingGameplayTag(LyraGameplayTags::Cheat_GodMode) && !isDamageFromSelfDestruct) + // { + // // Do not take away any health. + // data.EvaluatedData.Magnitude = 0.0f; + // return false; + // } +#endif // #if !UE_BUILD_SHIPPING + } + } + + // Save the current health + HealthBeforeAttributeChange = GetHealth(); + MaxHealthBeforeAttributeChange = GetMaxHealth(); + + return true; +} + +void UOLSHealthAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) +{ + Super::PostGameplayEffectExecute(Data); + + const bool isDamageFromSelfDestruct = Data.EffectSpec.GetDynamicAssetTags().HasTagExact(TAG_Gameplay_DamageSelfDestruct); + float minimumHealth = 0.0f; + +#if !UE_BUILD_SHIPPING + // Godmode and unlimited health stop death unless it's a self destruct + // if (!bIsDamageFromSelfDestruct && + // (Data.Target.HasMatchingGameplayTag(LyraGameplayTags::Cheat_GodMode) || Data.Target.HasMatchingGameplayTag(LyraGameplayTags::Cheat_UnlimitedHealth) )) + // { + // MinimumHealth = 1.0f; + // } +#endif // #if !UE_BUILD_SHIPPING + + const FGameplayEffectContextHandle& effectContext = Data.EffectSpec.GetEffectContext(); + AActor* instigator = effectContext.GetOriginalInstigator(); + AActor* causer = effectContext.GetEffectCauser(); + + if (Data.EvaluatedData.Attribute == GetDamageAttribute()) + { + // Send a standardized verb message that other systems can observe + if (Data.EvaluatedData.Magnitude > 0.0f) + { + FOLSVerbMessage message; + message.Verb = TAG_OLS_Damage_Message; + message.Instigator = Data.EffectSpec.GetEffectContext().GetEffectCauser(); + message.InstigatorTags = *Data.EffectSpec.CapturedSourceTags.GetAggregatedTags(); + message.Target = GetOwningActor(); + message.TargetTags = *Data.EffectSpec.CapturedTargetTags.GetAggregatedTags(); + //@TODO: Fill out context tags, and any non-ability-system source/instigator tags + //@TODO: Determine if it's an opposing team kill, self-own, team kill, etc... + message.Magnitude = Data.EvaluatedData.Magnitude; + + UGameplayMessageSubsystem& messageSystem = UGameplayMessageSubsystem::Get(GetWorld()); + messageSystem.BroadcastMessage(message.Verb, message); + } + + // Convert into -Health and then clamp + SetHealth(FMath::Clamp(GetHealth() - GetDamage(), minimumHealth, GetMaxHealth())); + SetDamage(0.0f); + } + else if (Data.EvaluatedData.Attribute == GetHealingAttribute()) + { + // Convert into +Health and then clamo + SetHealth(FMath::Clamp(GetHealth() + GetHealing(), minimumHealth, GetMaxHealth())); + SetHealing(0.0f); + } + else if (Data.EvaluatedData.Attribute == GetHealthAttribute()) + { + // Clamp and fall into out of health handling below + SetHealth(FMath::Clamp(GetHealth(), minimumHealth, GetMaxHealth())); + } + else if (Data.EvaluatedData.Attribute == GetMaxHealthAttribute()) + { + // TODO clamp current health? + + // Notify on any requested max health changes + OnMaxHealthChanged.Broadcast(instigator, causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, MaxHealthBeforeAttributeChange, GetMaxHealth()); + } + + // If health has actually changed activate callbacks + if (GetHealth() != HealthBeforeAttributeChange) + { + OnHealthChanged.Broadcast(instigator, causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, HealthBeforeAttributeChange, GetHealth()); + } + + if ((GetHealth() <= 0.0f) && !bIsOutOfHealth) + { + OnOutOfHealth.Broadcast(instigator, causer, &Data.EffectSpec, Data.EvaluatedData.Magnitude, HealthBeforeAttributeChange, GetHealth()); + } + + // Check health again in case an event above changed it. + bIsOutOfHealth = (GetHealth() <= 0.0f); +} + +void UOLSHealthAttributeSet::PreAttributeBaseChange(const FGameplayAttribute& attribute, float& newValue) const +{ + Super::PreAttributeBaseChange(attribute, newValue); + + ClampAttribute(attribute, newValue); +} + +void UOLSHealthAttributeSet::PreAttributeChange(const FGameplayAttribute& attribute, float& newValue) +{ + Super::PreAttributeChange(attribute, newValue); + + ClampAttribute(attribute, newValue); +} + +void UOLSHealthAttributeSet::PostAttributeChange(const FGameplayAttribute& attribute, float oldValue, float newValue) +{ + Super::PostAttributeChange(attribute, oldValue, newValue); + + if (attribute == GetMaxHealthAttribute()) + { + // Make sure current health is not greater than the new max health. + if (GetHealth() > newValue) + { + UOLSAbilitySystemComponent* asc = GetOLSAbilitySystemComponent(); + check(asc); + + asc->ApplyModToAttribute(GetHealthAttribute(), EGameplayModOp::Override, newValue); + } + } + + if (bIsOutOfHealth && (GetHealth() > 0.0f)) + { + bIsOutOfHealth = false; + } +} + +void UOLSHealthAttributeSet::ClampAttribute(const FGameplayAttribute& attribute, float& outNewValue) const +{ + if (attribute == GetHealthAttribute()) + { + // Do not allow health to go negative or above max health. + outNewValue = FMath::Clamp(outNewValue, 0.0f, GetMaxHealth()); + } + else if (attribute == GetMaxHealthAttribute()) + { + // Do not allow max health to drop below 1. + outNewValue = FMath::Max(outNewValue, 1.0f); + } +} diff --git a/Source/ols/Private/Messages/OLSVerbMessage.cpp b/Source/ols/Private/Messages/OLSVerbMessage.cpp new file mode 100644 index 0000000..f96c16b --- /dev/null +++ b/Source/ols/Private/Messages/OLSVerbMessage.cpp @@ -0,0 +1,11 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + + +#include "Messages/OLSVerbMessage.h" + +FString FOLSVerbMessage::ToString() const +{ + FString HumanReadableMessage; + FOLSVerbMessage::StaticStruct()->ExportText(/*out*/ HumanReadableMessage, this, /*Defaults=*/ nullptr, /*OwnerObject=*/ nullptr, PPF_None, /*ExportRootScope=*/ nullptr); + return HumanReadableMessage; +} diff --git a/Source/ols/Public/AbilitySystem/Attributes/OLSHealthAttributeSet.h b/Source/ols/Public/AbilitySystem/Attributes/OLSHealthAttributeSet.h new file mode 100644 index 0000000..9161ece --- /dev/null +++ b/Source/ols/Public/AbilitySystem/Attributes/OLSHealthAttributeSet.h @@ -0,0 +1,96 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "AbilitySystemComponent.h" +#include "NativeGameplayTags.h" +#include "OLSAttributeSetBase.h" +#include "OLSHealthAttributeSet.generated.h" + +OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_Damage); +OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_DamageImmunity); +OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_DamageSelfDestruct); +OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_Gameplay_FellOutOfWorld); +OLS_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_OLS_Damage_Message); + +/** + * UOLSHealthAttributeSet + * + * Class that defines attributes that are necessary for taking damage. + * Attribute examples include: health, shields, and resistances. + */ +UCLASS(BlueprintType) +class OLS_API UOLSHealthAttributeSet : public UOLSAttributeSetBase +{ + GENERATED_BODY() + +public: + + UOLSHealthAttributeSet(); + + //~ Begin UObject interface. + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + //~ End UObject interface. + + ATTRIBUTE_ACCESSORS(ThisClass, Health); + ATTRIBUTE_ACCESSORS(ThisClass, MaxHealth); + ATTRIBUTE_ACCESSORS(ThisClass, Healing); + ATTRIBUTE_ACCESSORS(ThisClass, Damage); + + // Delegate when health changes due to damage/healing, some information may be missing on the client + mutable FOLSAttributeEventNativeDelegate OnHealthChanged; + + // Delegate when max health changes + mutable FOLSAttributeEventNativeDelegate OnMaxHealthChanged; + + // Delegate to broadcast when the health attribute reaches zero + mutable FOLSAttributeEventNativeDelegate OnOutOfHealth; + +protected: + + UFUNCTION() + void OnRep_Health(const FGameplayAttributeData& oldValue); + + UFUNCTION() + void OnRep_MaxHealth(const FGameplayAttributeData& OldValue); + + virtual bool PreGameplayEffectExecute(FGameplayEffectModCallbackData& data) override; + virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; + + virtual void PreAttributeBaseChange(const FGameplayAttribute& attribute, float& newValue) const override; + virtual void PreAttributeChange(const FGameplayAttribute& attribute, float& newValue) override; + virtual void PostAttributeChange(const FGameplayAttribute& attribute, float oldValue, float newValue) override; + + void ClampAttribute(const FGameplayAttribute& attribute, float& outNewValue) const; + +private: + + + // The current health attribute. The health will be capped by the max health attribute. Health is hidden from modifiers so only executions can modify it. + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_Health, Category = "OLS|Health", Meta = (HideFromModifiers, AllowPrivateAccess = true)) + FGameplayAttributeData Health; + + // The current max health attribute. Max health is an attribute since gameplay effects can modify it. + UPROPERTY(BlueprintReadOnly, ReplicatedUsing = OnRep_MaxHealth, Category = "OLS|Health", Meta = (AllowPrivateAccess = true)) + FGameplayAttributeData MaxHealth; + + // Used to track when the health reaches 0. + uint8 bIsOutOfHealth : 1 = false; + + // Store the health before any changes + float MaxHealthBeforeAttributeChange = 0.f; + float HealthBeforeAttributeChange = 0.f + ; + // ------------------------------------------------------------------- + // Meta Attribute (please keep attributes that aren't 'stateful' below + // ------------------------------------------------------------------- + + // Incoming healing. This is mapped directly to +Health + UPROPERTY(BlueprintReadOnly, Category="Lyra|Health", Meta=(AllowPrivateAccess=true)) + FGameplayAttributeData Healing; + + // Incoming damage. This is mapped directly to -Health + UPROPERTY(BlueprintReadOnly, Category="Lyra|Health", Meta=(HideFromModifiers, AllowPrivateAccess=true)) + FGameplayAttributeData Damage; +}; diff --git a/Source/ols/Public/Messages/OLSVerbMessage.h b/Source/ols/Public/Messages/OLSVerbMessage.h new file mode 100644 index 0000000..7fdd7c1 --- /dev/null +++ b/Source/ols/Public/Messages/OLSVerbMessage.h @@ -0,0 +1,38 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "GameplayTagContainer.h" +#include "OLSVerbMessage.generated.h" + +// Represents a generic message of the form Instigator Verb Target (in Context, with Magnitude) +USTRUCT(BlueprintType) +struct FOLSVerbMessage +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + FGameplayTag Verb = FGameplayTag::EmptyTag; + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + TObjectPtr Instigator = nullptr; + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + TObjectPtr Target = nullptr; + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + FGameplayTagContainer InstigatorTags; + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + FGameplayTagContainer TargetTags; + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + FGameplayTagContainer ContextTags; + + UPROPERTY(BlueprintReadWrite, Category=Gameplay) + double Magnitude = 1.0; + + // Returns a debug string representation of this message + OLS_API FString ToString() const; +};