// © 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); } }