2025-01-15 22:30:52 +00:00
// © 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 ;
}
2025-01-15 22:53:15 +00:00
FGameplayEffectSpecHandle specHandle = AbilitySystemComponent - > MakeOutgoingSpec ( damageGE , 1.0f , AbilitySystemComponent - > MakeEffectContext ( ) ) ;
FGameplayEffectSpec * spec = specHandle . Data . Get ( ) ;
2025-01-15 22:30:52 +00:00
2025-01-15 22:53:15 +00:00
if ( ! spec )
2025-01-15 22:30:52 +00:00
{
// @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 ;
}
2025-01-15 22:53:15 +00:00
spec - > AddDynamicAssetTag ( TAG_Gameplay_DamageSelfDestruct ) ;
2025-01-15 22:30:52 +00:00
if ( isFellOutOfWorld )
{
2025-01-15 22:53:15 +00:00
spec - > AddDynamicAssetTag ( TAG_Gameplay_FellOutOfWorld ) ;
2025-01-15 22:30:52 +00:00
}
2025-01-16 18:09:31 +00:00
const float damageAmount = GetMaxHealth ( ) ;
2025-01-15 22:30:52 +00:00
// @TODO: Add LyraGameplayTags::SetByCaller_Damage.
2025-01-16 18:09:31 +00:00
// Spec->SetSetByCallerMagnitude(LyraGameplayTags::SetByCaller_Damage, damageAmount);
2025-01-15 22:53:15 +00:00
AbilitySystemComponent - > ApplyGameplayEffectSpecToSelf ( * spec ) ;
2025-01-15 22:30:52 +00:00
}
}
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 ( ) ) ) ;
}