325 lines
11 KiB
C++
325 lines
11 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "LyraHealthComponent.h"
|
|
#include "LyraLogChannels.h"
|
|
#include "System/LyraAssetManager.h"
|
|
#include "System/LyraGameData.h"
|
|
#include "LyraGameplayTags.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "GameplayEffect.h"
|
|
#include "GameplayEffectExtension.h"
|
|
#include "GameplayPrediction.h"
|
|
#include "Abilities/GameplayAbilityTypes.h"
|
|
#include "AbilitySystem/LyraAbilitySystemComponent.h"
|
|
#include "AbilitySystem/Attributes/LyraHealthSet.h"
|
|
#include "Messages/LyraVerbMessage.h"
|
|
#include "Messages/LyraVerbMessageHelpers.h"
|
|
#include "NativeGameplayTags.h"
|
|
#include "Components/GameFrameworkComponentManager.h"
|
|
#include "GameFramework/GameplayMessageSubsystem.h"
|
|
#include "GameFramework/PlayerState.h"
|
|
|
|
UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_Lyra_Elimination_Message, "Lyra.Elimination.Message");
|
|
|
|
|
|
ULyraHealthComponent::ULyraHealthComponent(const FObjectInitializer& ObjectInitializer)
|
|
: Super(ObjectInitializer)
|
|
{
|
|
PrimaryComponentTick.bStartWithTickEnabled = false;
|
|
PrimaryComponentTick.bCanEverTick = false;
|
|
|
|
SetIsReplicatedByDefault(true);
|
|
|
|
AbilitySystemComponent = nullptr;
|
|
HealthSet = nullptr;
|
|
DeathState = ELyraDeathState::NotDead;
|
|
}
|
|
|
|
void ULyraHealthComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
|
|
DOREPLIFETIME(ULyraHealthComponent, DeathState);
|
|
}
|
|
|
|
void ULyraHealthComponent::OnUnregister()
|
|
{
|
|
UninitializeFromAbilitySystem();
|
|
|
|
Super::OnUnregister();
|
|
}
|
|
|
|
void ULyraHealthComponent::InitializeWithAbilitySystem(ULyraAbilitySystemComponent* InASC)
|
|
{
|
|
AActor* Owner = GetOwner();
|
|
check(Owner);
|
|
|
|
if (AbilitySystemComponent)
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Health component for owner [%s] has already been initialized with an ability system."), *GetNameSafe(Owner));
|
|
return;
|
|
}
|
|
|
|
AbilitySystemComponent = InASC;
|
|
if (!AbilitySystemComponent)
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Cannot initialize health component for owner [%s] with NULL ability system."), *GetNameSafe(Owner));
|
|
return;
|
|
}
|
|
|
|
HealthSet = AbilitySystemComponent->GetSet<ULyraHealthSet>();
|
|
if (!HealthSet)
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Cannot initialize health component for owner [%s] with NULL health set on the ability system."), *GetNameSafe(Owner));
|
|
return;
|
|
}
|
|
|
|
// Register to listen for attribute changes.
|
|
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(ULyraHealthSet::GetHealthAttribute()).AddUObject(this, &ThisClass::HandleHealthChanged);
|
|
AbilitySystemComponent->GetGameplayAttributeValueChangeDelegate(ULyraHealthSet::GetMaxHealthAttribute()).AddUObject(this, &ThisClass::HandleMaxHealthChanged);
|
|
HealthSet->OnOutOfHealth.AddUObject(this, &ThisClass::HandleOutOfHealth);
|
|
|
|
// TEMP: Reset attributes to default values. Eventually this will be driven by a spread sheet.
|
|
AbilitySystemComponent->SetNumericAttributeBase(ULyraHealthSet::GetHealthAttribute(), HealthSet->GetMaxHealth());
|
|
|
|
ClearGameplayTags();
|
|
|
|
OnHealthChanged.Broadcast(this, HealthSet->GetHealth(), HealthSet->GetHealth(), nullptr);
|
|
OnMaxHealthChanged.Broadcast(this, HealthSet->GetHealth(), HealthSet->GetHealth(), nullptr);
|
|
|
|
//UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(GetOwner(), UGameFrameworkComponentManager::NAME_HealthComponentReady);
|
|
}
|
|
|
|
void ULyraHealthComponent::UninitializeFromAbilitySystem()
|
|
{
|
|
ClearGameplayTags();
|
|
|
|
if (HealthSet)
|
|
{
|
|
HealthSet->OnOutOfHealth.RemoveAll(this);
|
|
}
|
|
|
|
HealthSet = nullptr;
|
|
AbilitySystemComponent = nullptr;
|
|
}
|
|
|
|
void ULyraHealthComponent::ClearGameplayTags()
|
|
{
|
|
if (AbilitySystemComponent)
|
|
{
|
|
const FLyraGameplayTags& GameplayTags = FLyraGameplayTags::Get();
|
|
|
|
AbilitySystemComponent->SetLooseGameplayTagCount(GameplayTags.Status_Death_Dying, 0);
|
|
AbilitySystemComponent->SetLooseGameplayTagCount(GameplayTags.Status_Death_Dead, 0);
|
|
}
|
|
}
|
|
|
|
float ULyraHealthComponent::GetHealth() const
|
|
{
|
|
return (HealthSet ? HealthSet->GetHealth() : 0.0f);
|
|
}
|
|
|
|
float ULyraHealthComponent::GetMaxHealth() const
|
|
{
|
|
return (HealthSet ? HealthSet->GetMaxHealth() : 0.0f);
|
|
}
|
|
|
|
float ULyraHealthComponent::GetHealthNormalized() const
|
|
{
|
|
if (HealthSet)
|
|
{
|
|
const float Health = HealthSet->GetHealth();
|
|
const float MaxHealth = HealthSet->GetMaxHealth();
|
|
|
|
return ((MaxHealth > 0.0f) ? (Health / MaxHealth) : 0.0f);
|
|
}
|
|
|
|
return 0.0f;
|
|
}
|
|
|
|
static AActor* GetInstigatorFromAttrChangeData(const FOnAttributeChangeData& ChangeData)
|
|
{
|
|
if (ChangeData.GEModData != nullptr)
|
|
{
|
|
const FGameplayEffectContextHandle& EffectContext = ChangeData.GEModData->EffectSpec.GetEffectContext();
|
|
return EffectContext.GetOriginalInstigator();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void ULyraHealthComponent::HandleHealthChanged(const FOnAttributeChangeData& ChangeData)
|
|
{
|
|
OnHealthChanged.Broadcast(this, ChangeData.OldValue, ChangeData.NewValue, GetInstigatorFromAttrChangeData(ChangeData));
|
|
}
|
|
|
|
void ULyraHealthComponent::HandleMaxHealthChanged(const FOnAttributeChangeData& ChangeData)
|
|
{
|
|
OnMaxHealthChanged.Broadcast(this, ChangeData.OldValue, ChangeData.NewValue, GetInstigatorFromAttrChangeData(ChangeData));
|
|
}
|
|
|
|
void ULyraHealthComponent::HandleOutOfHealth(AActor* DamageInstigator, AActor* DamageCauser, const FGameplayEffectSpec& DamageEffectSpec, float DamageMagnitude)
|
|
{
|
|
#if WITH_SERVER_CODE
|
|
if (AbilitySystemComponent)
|
|
{
|
|
// Send the "GameplayEvent.Death" gameplay event through the owner's ability system. This can be used to trigger a death gameplay ability.
|
|
{
|
|
FGameplayEventData Payload;
|
|
Payload.EventTag = FLyraGameplayTags::Get().GameplayEvent_Death;
|
|
Payload.Instigator = DamageInstigator;
|
|
Payload.Target = AbilitySystemComponent->GetAvatarActor();
|
|
Payload.OptionalObject = DamageEffectSpec.Def;
|
|
Payload.ContextHandle = DamageEffectSpec.GetEffectContext();
|
|
Payload.InstigatorTags = *DamageEffectSpec.CapturedSourceTags.GetAggregatedTags();
|
|
Payload.TargetTags = *DamageEffectSpec.CapturedTargetTags.GetAggregatedTags();
|
|
Payload.EventMagnitude = DamageMagnitude;
|
|
|
|
FScopedPredictionWindow NewScopedWindow(AbilitySystemComponent, true);
|
|
AbilitySystemComponent->HandleGameplayEvent(Payload.EventTag, &Payload);
|
|
}
|
|
|
|
// Send a standardized verb message that other systems can observe
|
|
{
|
|
FLyraVerbMessage Message;
|
|
Message.Verb = TAG_Lyra_Elimination_Message;
|
|
Message.Instigator = DamageInstigator;
|
|
Message.InstigatorTags = *DamageEffectSpec.CapturedSourceTags.GetAggregatedTags();
|
|
Message.Target = ULyraVerbMessageHelpers::GetPlayerStateFromObject(AbilitySystemComponent->GetAvatarActor());
|
|
Message.TargetTags = *DamageEffectSpec.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...
|
|
|
|
UGameplayMessageSubsystem& MessageSystem = UGameplayMessageSubsystem::Get(GetWorld());
|
|
MessageSystem.BroadcastMessage(Message.Verb, Message);
|
|
}
|
|
|
|
//@TODO: assist messages (could compute from damage dealt elsewhere)?
|
|
}
|
|
|
|
#endif // #if WITH_SERVER_CODE
|
|
}
|
|
|
|
void ULyraHealthComponent::OnRep_DeathState(ELyraDeathState OldDeathState)
|
|
{
|
|
const ELyraDeathState NewDeathState = DeathState;
|
|
|
|
// Revert the death state for now since we rely on StartDeath and FinishDeath to change it.
|
|
DeathState = OldDeathState;
|
|
|
|
if (OldDeathState > NewDeathState)
|
|
{
|
|
// The server is trying to set us back but we've already predicted past the server state.
|
|
UE_LOG(LogLyra, Warning, TEXT("LyraHealthComponent: Predicted past server death state [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner()));
|
|
return;
|
|
}
|
|
|
|
if (OldDeathState == ELyraDeathState::NotDead)
|
|
{
|
|
if (NewDeathState == ELyraDeathState::DeathStarted)
|
|
{
|
|
StartDeath();
|
|
}
|
|
else if (NewDeathState == ELyraDeathState::DeathFinished)
|
|
{
|
|
StartDeath();
|
|
FinishDeath();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Invalid death transition [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner()));
|
|
}
|
|
}
|
|
else if (OldDeathState == ELyraDeathState::DeathStarted)
|
|
{
|
|
if (NewDeathState == ELyraDeathState::DeathFinished)
|
|
{
|
|
FinishDeath();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Invalid death transition [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner()));
|
|
}
|
|
}
|
|
|
|
ensureMsgf((DeathState == NewDeathState), TEXT("LyraHealthComponent: Death transition failed [%d] -> [%d] for owner [%s]."), (uint8)OldDeathState, (uint8)NewDeathState, *GetNameSafe(GetOwner()));
|
|
}
|
|
|
|
void ULyraHealthComponent::StartDeath()
|
|
{
|
|
if (DeathState != ELyraDeathState::NotDead)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DeathState = ELyraDeathState::DeathStarted;
|
|
|
|
if (AbilitySystemComponent)
|
|
{
|
|
AbilitySystemComponent->SetLooseGameplayTagCount(FLyraGameplayTags::Get().Status_Death_Dying, 1);
|
|
}
|
|
|
|
AActor* Owner = GetOwner();
|
|
check(Owner);
|
|
|
|
OnDeathStarted.Broadcast(Owner);
|
|
|
|
Owner->ForceNetUpdate();
|
|
}
|
|
|
|
void ULyraHealthComponent::FinishDeath()
|
|
{
|
|
if (DeathState != ELyraDeathState::DeathStarted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DeathState = ELyraDeathState::DeathFinished;
|
|
|
|
if (AbilitySystemComponent)
|
|
{
|
|
AbilitySystemComponent->SetLooseGameplayTagCount(FLyraGameplayTags::Get().Status_Death_Dead, 1);
|
|
}
|
|
|
|
AActor* Owner = GetOwner();
|
|
check(Owner);
|
|
|
|
OnDeathFinished.Broadcast(Owner);
|
|
|
|
Owner->ForceNetUpdate();
|
|
}
|
|
|
|
void ULyraHealthComponent::DamageSelfDestruct(bool bFellOutOfWorld)
|
|
{
|
|
if ((DeathState == ELyraDeathState::NotDead) && AbilitySystemComponent)
|
|
{
|
|
const TSubclassOf<UGameplayEffect> DamageGE = ULyraAssetManager::GetSubclass(ULyraGameData::Get().DamageGameplayEffect_SetByCaller);
|
|
if (!DamageGE)
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: DamageSelfDestruct failed for owner [%s]. Unable to find gameplay effect [%s]."), *GetNameSafe(GetOwner()), *ULyraGameData::Get().DamageGameplayEffect_SetByCaller.GetAssetName());
|
|
return;
|
|
}
|
|
|
|
FGameplayEffectSpecHandle SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(DamageGE, 1.0f, AbilitySystemComponent->MakeEffectContext());
|
|
FGameplayEffectSpec* Spec = SpecHandle.Data.Get();
|
|
|
|
if (!Spec)
|
|
{
|
|
UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: DamageSelfDestruct failed for owner [%s]. Unable to make outgoing spec for [%s]."), *GetNameSafe(GetOwner()), *GetNameSafe(DamageGE));
|
|
return;
|
|
}
|
|
|
|
Spec->AddDynamicAssetTag(TAG_Gameplay_DamageSelfDestruct);
|
|
|
|
if (bFellOutOfWorld)
|
|
{
|
|
Spec->AddDynamicAssetTag(TAG_Gameplay_FellOutOfWorld);
|
|
}
|
|
|
|
const float DamageAmount = GetMaxHealth();
|
|
|
|
Spec->SetSetByCallerMagnitude(FLyraGameplayTags::Get().SetByCaller_Damage, DamageAmount);
|
|
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*Spec);
|
|
}
|
|
}
|