403 lines
13 KiB
C++
403 lines
13 KiB
C++
// © 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 "Components/OLSHealthComponent.h"
|
|
|
|
#include "AbilitySystem/OLSAbilitySystemComponent.h"
|
|
#include "AbilitySystem/Attributes/OLSHealthAttributeSet.h"
|
|
#include "DataAssets/OLSGameDataAsset.h"
|
|
#include "GameFramework/GameplayMessageSubsystem.h"
|
|
#include "GameFramework/PlayerState.h"
|
|
#include "Messages/OLSVerbMessage.h"
|
|
#include "Messages/OLSVerbMessageHelpers.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
#include "Systems/OLSAssetManager.h"
|
|
|
|
UOLSHealthComponent::UOLSHealthComponent(const FObjectInitializer& objectInitializer)
|
|
: Super(objectInitializer)
|
|
{
|
|
PrimaryComponentTick.bStartWithTickEnabled = false;
|
|
PrimaryComponentTick.bCanEverTick = false;
|
|
|
|
SetIsReplicatedByDefault(true);
|
|
|
|
AbilitySystemComponent = nullptr;
|
|
HealthSet = nullptr;
|
|
DeathState = EOLSDeathState::NotDead;
|
|
}
|
|
|
|
void UOLSHealthComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
|
|
DOREPLIFETIME(ThisClass, DeathState);
|
|
}
|
|
|
|
UOLSHealthComponent* UOLSHealthComponent::FindHealthComponent(const AActor* actor)
|
|
{
|
|
return (actor ? actor->FindComponentByClass<UOLSHealthComponent>() : nullptr);
|
|
}
|
|
|
|
|
|
void UOLSHealthComponent::InitializeWithAbilitySystem(UOLSAbilitySystemComponent* asc)
|
|
{
|
|
AActor* owner = GetOwner();
|
|
check(owner);
|
|
|
|
if (AbilitySystemComponent)
|
|
{
|
|
// @TODO replace this by our custom log.
|
|
// UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Health component for owner [%s] has already been initialized with an ability system."), *GetNameSafe(Owner));
|
|
return;
|
|
}
|
|
|
|
AbilitySystemComponent = asc;
|
|
if (!AbilitySystemComponent)
|
|
{
|
|
// @TODO replace this by our custom log.
|
|
// UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Cannot initialize health component for owner [%s] with NULL ability system."), *GetNameSafe(Owner));
|
|
return;
|
|
}
|
|
|
|
HealthSet = AbilitySystemComponent->GetSet<UOLSHealthAttributeSet>();
|
|
if (!HealthSet)
|
|
{
|
|
// @TODO replace this by our custom log.
|
|
// 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.
|
|
HealthSet->OnHealthChanged.AddUObject(this, &ThisClass::HandleHealthChanged);
|
|
HealthSet->OnMaxHealthChanged.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(UOLSHealthAttributeSet::GetHealthAttribute(), HealthSet->GetMaxHealth());
|
|
|
|
ClearGameplayTags();
|
|
|
|
Broadcast_OnHealthChanged(this, HealthSet->GetHealth(), HealthSet->GetHealth(), nullptr);
|
|
Broadcast_OnMaxHealthChanged(this, HealthSet->GetHealth(), HealthSet->GetMaxHealth(), nullptr);
|
|
}
|
|
|
|
void UOLSHealthComponent::UninitializeFromAbilitySystem()
|
|
{
|
|
ClearGameplayTags();
|
|
|
|
if (HealthSet)
|
|
{
|
|
HealthSet->OnHealthChanged.RemoveAll(this);
|
|
HealthSet->OnMaxHealthChanged.RemoveAll(this);
|
|
HealthSet->OnOutOfHealth.RemoveAll(this);
|
|
}
|
|
|
|
HealthSet = nullptr;
|
|
AbilitySystemComponent = nullptr;
|
|
}
|
|
|
|
float UOLSHealthComponent::GetHealth() const
|
|
{
|
|
return (HealthSet ? HealthSet->GetHealth() : 0.0f);
|
|
}
|
|
|
|
float UOLSHealthComponent::GetMaxHealth() const
|
|
{
|
|
return (HealthSet ? HealthSet->GetMaxHealth() : 0.0f);
|
|
}
|
|
|
|
float UOLSHealthComponent::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;
|
|
}
|
|
|
|
EOLSDeathState UOLSHealthComponent::GetDeathState() const
|
|
{
|
|
return DeathState;
|
|
}
|
|
|
|
bool UOLSHealthComponent::IsDeadOrDying() const
|
|
{
|
|
return (DeathState > EOLSDeathState::NotDead);
|
|
}
|
|
|
|
void UOLSHealthComponent::StartDeath()
|
|
{
|
|
if (DeathState != EOLSDeathState::NotDead)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DeathState = EOLSDeathState::DeathStarted;
|
|
|
|
if (AbilitySystemComponent)
|
|
{
|
|
// @TODO: Add LyraGameplayTags::Status_Death_Dying.
|
|
// AbilitySystemComponent->SetLooseGameplayTagCount(LyraGameplayTags::Status_Death_Dying, 1);
|
|
}
|
|
|
|
AActor* owner = GetOwner();
|
|
check(owner);
|
|
|
|
Broadcast_OnDeathStarted(owner);
|
|
|
|
owner->ForceNetUpdate();
|
|
}
|
|
|
|
void UOLSHealthComponent::FinishDeath()
|
|
{
|
|
if (DeathState != EOLSDeathState::DeathStarted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DeathState = EOLSDeathState::DeathFinished;
|
|
|
|
if (AbilitySystemComponent)
|
|
{
|
|
// @TODO: Add LyraGameplayTags::Status_Death_Dead.
|
|
// AbilitySystemComponent->SetLooseGameplayTagCount(LyraGameplayTags::Status_Death_Dead, 1);
|
|
}
|
|
|
|
AActor* owner = GetOwner();
|
|
check(owner);
|
|
|
|
Broadcast_OnDeathFinished(owner);
|
|
|
|
owner->ForceNetUpdate();
|
|
}
|
|
|
|
void UOLSHealthComponent::DamageSelfDestruct(bool isFellOutOfWorld)
|
|
{
|
|
if ((DeathState == EOLSDeathState::NotDead) && AbilitySystemComponent)
|
|
{
|
|
const TSubclassOf<UGameplayEffect> damageGE = UOLSAssetManager::GetSubclass(UOLSGameDataAsset::Get().DamageGameplayEffect_SetByCaller);
|
|
if (!damageGE)
|
|
{
|
|
// @TODO: replace this with our custom log.
|
|
// 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)
|
|
{
|
|
// @TODO: replace this with our custom log.
|
|
// 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 (isFellOutOfWorld)
|
|
{
|
|
spec->AddDynamicAssetTag(TAG_Gameplay_FellOutOfWorld);
|
|
}
|
|
|
|
const float damageAmount = GetMaxHealth();
|
|
|
|
|
|
// @TODO: Add LyraGameplayTags::SetByCaller_Damage.
|
|
// Spec->SetSetByCallerMagnitude(LyraGameplayTags::SetByCaller_Damage, damageAmount);
|
|
AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*spec);
|
|
}
|
|
}
|
|
|
|
void UOLSHealthComponent::Broadcast_OnHealthChanged(UOLSHealthComponent* healthComponent, float oldValue,
|
|
float newValue, AActor* instigator) const
|
|
{
|
|
if (OnHealthChangedDynamicDelegate.IsBound())
|
|
{
|
|
OnHealthChangedDynamicDelegate.Broadcast(healthComponent, oldValue, newValue, instigator);
|
|
}
|
|
|
|
if (OnHealthChangedNativeDelegate.IsBound())
|
|
{
|
|
OnHealthChangedNativeDelegate.Broadcast(healthComponent, oldValue, newValue, instigator);
|
|
}
|
|
}
|
|
|
|
void UOLSHealthComponent::Broadcast_OnMaxHealthChanged(UOLSHealthComponent* healthComponent, float oldValue,
|
|
float newValue, AActor* instigator) const
|
|
{
|
|
if (OnMaxHealthChangedDynamicDelegate.IsBound())
|
|
{
|
|
OnMaxHealthChangedDynamicDelegate.Broadcast(healthComponent, oldValue, newValue, instigator);
|
|
}
|
|
|
|
if (OnMaxHealthChangedNativeDelegate.IsBound())
|
|
{
|
|
OnMaxHealthChangedNativeDelegate.Broadcast(healthComponent, oldValue, newValue, instigator);
|
|
}
|
|
}
|
|
|
|
void UOLSHealthComponent::Broadcast_OnDeathStarted(AActor* owningActor) const
|
|
{
|
|
if (OnDeathStartedDynamicDelegate.IsBound())
|
|
{
|
|
OnDeathStartedDynamicDelegate.Broadcast(owningActor);
|
|
}
|
|
|
|
if (OnDeathStartNativeDelegate.IsBound())
|
|
{
|
|
OnDeathStartNativeDelegate.Broadcast(owningActor);
|
|
}
|
|
}
|
|
|
|
void UOLSHealthComponent::Broadcast_OnDeathFinished(AActor* owningActor) const
|
|
{
|
|
if (OnDeathFinishedDynamicDelegate.IsBound())
|
|
{
|
|
OnDeathFinishedDynamicDelegate.Broadcast(owningActor);
|
|
}
|
|
|
|
if (OnDeathFinishedNativeDelegate.IsBound())
|
|
{
|
|
OnDeathFinishedNativeDelegate.Broadcast(owningActor);
|
|
}
|
|
}
|
|
|
|
void UOLSHealthComponent::OnUnregister()
|
|
{
|
|
UninitializeFromAbilitySystem();
|
|
|
|
Super::OnUnregister();
|
|
}
|
|
|
|
void UOLSHealthComponent::ClearGameplayTags()
|
|
{
|
|
if (HealthSet)
|
|
{
|
|
HealthSet->OnHealthChanged.RemoveAll(this);
|
|
HealthSet->OnMaxHealthChanged.RemoveAll(this);
|
|
HealthSet->OnOutOfHealth.RemoveAll(this);
|
|
}
|
|
|
|
HealthSet = nullptr;
|
|
AbilitySystemComponent = nullptr;
|
|
}
|
|
|
|
void UOLSHealthComponent::HandleHealthChanged(AActor* damageInstigator, AActor* damageCauser,
|
|
const FGameplayEffectSpec* damageEffectSpec, float damageMagnitude,
|
|
float oldValue, float newValue)
|
|
{
|
|
Broadcast_OnHealthChanged(this, oldValue, newValue, damageInstigator);
|
|
}
|
|
|
|
void UOLSHealthComponent::HandleMaxHealthChanged(AActor* damageInstigator, AActor* damageCauser,
|
|
const FGameplayEffectSpec* damageEffectSpec, float damageMagnitude,
|
|
float oldValue, float newValue)
|
|
{
|
|
Broadcast_OnMaxHealthChanged(this, oldValue, newValue, damageInstigator);
|
|
}
|
|
|
|
void UOLSHealthComponent::HandleOutOfHealth(AActor* damageInstigator, AActor* damageCauser,
|
|
const FGameplayEffectSpec* damageEffectSpec, float damageMagnitude,
|
|
float oldValue, float newValue)
|
|
{
|
|
#if WITH_SERVER_CODE
|
|
if (AbilitySystemComponent && damageEffectSpec)
|
|
{
|
|
// Send the "GameplayEvent.Death" gameplay event through the owner's ability system. This can be used to trigger a death gameplay ability.
|
|
{
|
|
FGameplayEventData payload;
|
|
// @TODO: Add LyraGameplayTags::GameplayEvent_Death.
|
|
// payload.EventTag = LyraGameplayTags::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
|
|
{
|
|
FOLSVerbMessage message;
|
|
// @TODO: Add LyraGameplayTags::TAG_Lyra_Elimination_Message.
|
|
// message.Verb = TAG_Lyra_Elimination_Message;
|
|
message.Instigator = damageInstigator;
|
|
message.InstigatorTags = *damageEffectSpec->CapturedSourceTags.GetAggregatedTags();
|
|
message.Target = UOLSVerbMessageHelpers::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 UOLSHealthComponent::OnRep_DeathState(EOLSDeathState oldDeathState)
|
|
{
|
|
const EOLSDeathState 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.
|
|
// @TODO replace this with our custom log.
|
|
// UE_LOG(LogLyra, Warning,
|
|
// TEXT("LyraHealthComponent: Predicted past server death state [%d] -> [%d] for owner [%s]."),
|
|
// (uint8)OldDeathState, (uint8)newDeathStateameSafe(GetOwner()));
|
|
return;
|
|
}
|
|
|
|
if (oldDeathState == EOLSDeathState::NotDead)
|
|
{
|
|
if (newDeathState == EOLSDeathState::DeathStarted)
|
|
{
|
|
StartDeath();
|
|
}
|
|
else if (newDeathState == EOLSDeathState::DeathFinished)
|
|
{
|
|
StartDeath();
|
|
FinishDeath();
|
|
}
|
|
else
|
|
{
|
|
// @TODO replace this with our custom log.
|
|
// UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Invalid death transition [%d] -> [%d] for owner [%s]."),
|
|
// (uint8)OldDeathState, (uint8)newDeathStateameSafe(GetOwner()));
|
|
}
|
|
}
|
|
else if (oldDeathState == EOLSDeathState::DeathStarted)
|
|
{
|
|
if (newDeathState == EOLSDeathState::DeathFinished)
|
|
{
|
|
FinishDeath();
|
|
}
|
|
else
|
|
{
|
|
// @TODO replace this with our custom log.
|
|
// UE_LOG(LogLyra, Error, TEXT("LyraHealthComponent: Invalid death transition [%d] -> [%d] for owner [%s]."),
|
|
// (uint8)OldDeathState, (uint8)newDeathStateameSafe(GetOwner()));
|
|
}
|
|
}
|
|
|
|
ensureMsgf((DeathState == newDeathState),
|
|
TEXT("OLSHealthComponent: Death transition failed [%d] -> [%d] for owner [%s]."),
|
|
static_cast<uint8>(oldDeathState), static_cast<uint8>(newDeathState), *GetNameSafe(GetOwner()));
|
|
}
|