// © 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& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(ThisClass, DeathState); } UOLSHealthComponent* UOLSHealthComponent::FindHealthComponent(const AActor* actor) { return (actor ? actor->FindComponentByClass() : 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(); 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 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(oldDeathState), static_cast(newDeathState), *GetNameSafe(GetOwner())); }