diff --git a/Source/ols/Private/Components/OLSHealthComponent.cpp b/Source/ols/Private/Components/OLSHealthComponent.cpp new file mode 100644 index 0000000..d7401e5 --- /dev/null +++ b/Source/ols/Private/Components/OLSHealthComponent.cpp @@ -0,0 +1,402 @@ +// © 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())); +} diff --git a/Source/ols/Private/DataAssets/OLSGameDataAsset.cpp b/Source/ols/Private/DataAssets/OLSGameDataAsset.cpp new file mode 100644 index 0000000..b88026b --- /dev/null +++ b/Source/ols/Private/DataAssets/OLSGameDataAsset.cpp @@ -0,0 +1,15 @@ +// © 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 "DataAssets/OLSGameDataAsset.h" + +#include "Systems/OLSAssetManager.h" + +UOLSGameDataAsset::UOLSGameDataAsset() +{ +} + +const UOLSGameDataAsset& UOLSGameDataAsset::Get() +{ + return UOLSAssetManager::Get().GetGameData(); +} diff --git a/Source/ols/Private/Messages/OLSVerbMessageHelpers.cpp b/Source/ols/Private/Messages/OLSVerbMessageHelpers.cpp index 78ac430..33206f9 100644 --- a/Source/ols/Private/Messages/OLSVerbMessageHelpers.cpp +++ b/Source/ols/Private/Messages/OLSVerbMessageHelpers.cpp @@ -7,6 +7,9 @@ #include "GameFramework/PlayerState.h" #include "Messages/OLSVerbMessage.h" + +#include UE_INLINE_GENERATED_CPP_BY_NAME(OLSVerbMessageHelpers) + APlayerState* UOLSVerbMessageHelpers::GetPlayerStateFromObject(UObject* object) { if (APlayerController* playerController = Cast(object)) diff --git a/Source/ols/Private/Systems/OLSAssetManager.cpp b/Source/ols/Private/Systems/OLSAssetManager.cpp index 5f49b4a..a1219ca 100644 --- a/Source/ols/Private/Systems/OLSAssetManager.cpp +++ b/Source/ols/Private/Systems/OLSAssetManager.cpp @@ -2,6 +2,8 @@ #include "Systems/OLSAssetManager.h" + +#include "DataAssets/OLSGameDataAsset.h" #include "Misc/App.h" #include "Stats/StatsMisc.h" #include "Misc/ScopedSlowTask.h" @@ -49,62 +51,15 @@ UOLSAssetManager& UOLSAssetManager::Get() return *NewObject(); } -template -AssetType* UOLSAssetManager::GetAsset(const TSoftObjectPtr& assetPointer, bool shouldKeepInMemory) -{ - AssetType* loadedAsset = nullptr; - - const FSoftObjectPath& assetPath = assetPointer.ToSoftObjectPath(); - - if (assetPath.IsValid()) - { - loadedAsset = assetPointer.Get(); - if (!loadedAsset) - { - loadedAsset = Cast(SynchronousLoadAsset(assetPath)); - ensureAlwaysMsgf(loadedAsset, TEXT("Failed to load asset [%s]"), *assetPointer.ToString()); - } - - if (loadedAsset && shouldKeepInMemory) - { - // Added to loaded asset list. - Get().AddLoadedAsset(Cast(loadedAsset)); - } - } - - return loadedAsset; -} - -template -TSubclassOf UOLSAssetManager::GetSubclass(const TSoftClassPtr& assetPointer, bool shouldKeepInMemory) -{ - TSubclassOf loadedSubclass = nullptr; - - const FSoftObjectPath& assetPath = assetPointer.ToSoftObjectPath(); - - if (assetPath.IsValid()) - { - loadedSubclass = assetPointer.Get(); - if (!loadedSubclass) - { - loadedSubclass = Cast(SynchronousLoadAsset(assetPath)); - ensureAlwaysMsgf(loadedSubclass, TEXT("Failed to load asset class [%s]"), *assetPointer.ToString()); - } - - if (loadedSubclass && shouldKeepInMemory) - { - // Added to loaded asset list. - Get().AddLoadedAsset(Cast(loadedSubclass)); - } - } - - return loadedSubclass; -} - void UOLSAssetManager::DumpLoadedAssets() { } +const UOLSGameDataAsset& UOLSAssetManager::GetGameData() +{ + return GetOrLoadTypedGameData(OLSGameDataPath); +} + const UOLSPawnDataAsset* UOLSAssetManager::GetDefaultPawnData() const { return GetAsset(DefaultPawnData); diff --git a/Source/ols/Public/Components/OLSHealthComponent.h b/Source/ols/Public/Components/OLSHealthComponent.h new file mode 100644 index 0000000..910c10e --- /dev/null +++ b/Source/ols/Public/Components/OLSHealthComponent.h @@ -0,0 +1,138 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/GameFrameworkComponent.h" +#include "OLSHealthComponent.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOLSDeathEventDynamicDelegate, class AActor*, owningActor); +DECLARE_MULTICAST_DELEGATE_OneParam(FOLSDeathEventNativeDelegate, class AActor* /* owningActor */) +DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOLSAttributeChangedDynamicDelegate, class UOLSHealthComponent*, healthComponent, float, oldValue, float, newValue, class AActor*, instigator); +DECLARE_MULTICAST_DELEGATE_FourParams(FOLSAttributeChangedNativeDelegate, class UOLSHealthComponent* /* healthComponent */, float /* oldValue */, float /* newValue */, class AActor* /* instigator */) + +/** + * EOLSDeathState + * + * Defines current state of death. + */ +UENUM(BlueprintType) +enum class EOLSDeathState : uint8 +{ + NotDead = 0, + DeathStarted, + DeathFinished +}; + +/** + * UOLSHealthComponent + * + * An actor component used to handle anything related to health. + */ +UCLASS(Blueprintable, Meta=(BlueprintSpawnableComponent)) +class OLS_API UOLSHealthComponent : public UGameFrameworkComponent +{ + GENERATED_BODY() + +public: + + // Sets default values for this component's properties + UOLSHealthComponent(const FObjectInitializer& objectInitializer); + + //~ Begin UActorComponent interface. + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + //~ End UActorComponent interface. + + // Returns the health component if one exists on the specified actor. + UFUNCTION(BlueprintPure, Category = "OLS|Health") + static class UOLSHealthComponent* FindHealthComponent(const AActor* actor); + + // Initialize the component using an ability system component. + UFUNCTION(BlueprintCallable, Category = "OLS|Health") + void InitializeWithAbilitySystem(class UOLSAbilitySystemComponent* asc); + + // Uninitialize the component, clearing any references to the ability system. + UFUNCTION(BlueprintCallable, Category = "OLS|Health") + void UninitializeFromAbilitySystem(); + + // Returns the current health value. + UFUNCTION(BlueprintCallable, Category = "OLS|Health") + float GetHealth() const; + + // Returns the current maximum health value. + UFUNCTION(BlueprintCallable, Category = "OLS|Health") + float GetMaxHealth() const; + + // Returns the current health in the range [0.0, 1.0]. + UFUNCTION(BlueprintCallable, Category = "OLS|Health") + float GetHealthNormalized() const; + + UFUNCTION(BlueprintCallable, Category = "OLS|Health") + EOLSDeathState GetDeathState() const; + + UFUNCTION(BlueprintCallable, BlueprintPure = false, Category = "OLS|Health", Meta = (ExpandBoolAsExecs = "ReturnValue")) + bool IsDeadOrDying() const; + + // Begins the death sequence for the owner. + virtual void StartDeath(); + + // Ends the death sequence for the owner. + virtual void FinishDeath(); + + // Applies enough damage to kill the owner. + virtual void DamageSelfDestruct(bool isFellOutOfWorld = false); + +public: + + // Delegate fired when the health value has changed. This is called on the client but the instigator may not be valid + UPROPERTY(BlueprintAssignable) + FOLSAttributeChangedDynamicDelegate OnHealthChangedDynamicDelegate; + FOLSAttributeChangedNativeDelegate OnHealthChangedNativeDelegate; + + // Delegate fired when the max health value has changed. This is called on the client but the instigator may not be valid + UPROPERTY(BlueprintAssignable) + FOLSAttributeChangedDynamicDelegate OnMaxHealthChangedDynamicDelegate; + FOLSAttributeChangedNativeDelegate OnMaxHealthChangedNativeDelegate; + + // Delegate fired when the death sequence has started. + UPROPERTY(BlueprintAssignable) + FOLSDeathEventDynamicDelegate OnDeathStartedDynamicDelegate; + FOLSDeathEventNativeDelegate OnDeathStartNativeDelegate; + + // Delegate fired when the death sequence has finished. + UPROPERTY(BlueprintAssignable) + FOLSDeathEventDynamicDelegate OnDeathFinishedDynamicDelegate; + FOLSDeathEventNativeDelegate OnDeathFinishedNativeDelegate; + + void Broadcast_OnHealthChanged(UOLSHealthComponent* healthComponent, float oldValue, float newValue, AActor* instigator) const; + void Broadcast_OnMaxHealthChanged(UOLSHealthComponent* healthComponent, float oldValue, float newValue, AActor* instigator) const; + void Broadcast_OnDeathStarted(class AActor* owningActor) const; + void Broadcast_OnDeathFinished(class AActor* owningActor) const; + +protected: + + virtual void OnUnregister() override; + + void ClearGameplayTags(); + + virtual void HandleHealthChanged(AActor* damageInstigator, AActor* damageCauser, const struct FGameplayEffectSpec* damageEffectSpec, float damageMagnitude, float oldValue, float newValue); + virtual void HandleMaxHealthChanged(AActor* damageInstigator, AActor* damageCauser, const struct FGameplayEffectSpec* damageEffectSpec, float damageMagnitude, float dldValue, float newValue); + virtual void HandleOutOfHealth(AActor* damageInstigator, AActor* damageCauser, const struct FGameplayEffectSpec* damageEffectSpec, float damageMagnitude, float oldValue, float newValue); + + UFUNCTION() + virtual void OnRep_DeathState(EOLSDeathState oldDeathState = EOLSDeathState::NotDead); + +protected: + + // Ability system used by this component. + UPROPERTY() + TObjectPtr AbilitySystemComponent = nullptr; + + // Health set used by this component. + UPROPERTY() + TObjectPtr HealthSet = nullptr; + + // Replicated state used to handle dying. + UPROPERTY(ReplicatedUsing = OnRep_DeathState) + EOLSDeathState DeathState = EOLSDeathState::NotDead; +}; diff --git a/Source/ols/Public/DataAssets/OLSGameDataAsset.h b/Source/ols/Public/DataAssets/OLSGameDataAsset.h new file mode 100644 index 0000000..338d158 --- /dev/null +++ b/Source/ols/Public/DataAssets/OLSGameDataAsset.h @@ -0,0 +1,39 @@ +// © 2024 Long Ly. All rights reserved. Any unauthorized use, reproduction, or distribution of this trademark is strictly prohibited and may result in legal action. + +#pragma once + +#include "CoreMinimal.h" +#include "OLSPrimaryDataAsset.h" +#include "OLSGameDataAsset.generated.h" + +/** + * UOLSGameDataAsset + * + * Non-mutable data asset that contains global game data. + */ +UCLASS(BlueprintType, Const, Meta = (DisplayName = "OLS Game Data", ShortTooltip = "Data asset containing global game data.")) +class OLS_API UOLSGameDataAsset : public UOLSPrimaryDataAsset +{ + GENERATED_BODY() + +public: + + UOLSGameDataAsset(); + + // Returns the loaded game data. + static const UOLSGameDataAsset& Get(); + +public: + + // Gameplay effect used to apply damage. Uses SetByCaller for the damage magnitude. + UPROPERTY(EditDefaultsOnly, Category = "Default Gameplay Effects", meta = (DisplayName = "Damage Gameplay Effect (SetByCaller)")) + TSoftClassPtr DamageGameplayEffect_SetByCaller; + + // Gameplay effect used to apply healing. Uses SetByCaller for the healing magnitude. + UPROPERTY(EditDefaultsOnly, Category = "Default Gameplay Effects", meta = (DisplayName = "Heal Gameplay Effect (SetByCaller)")) + TSoftClassPtr HealGameplayEffect_SetByCaller; + + // Gameplay effect used to add and remove dynamic tags. + UPROPERTY(EditDefaultsOnly, Category = "Default Gameplay Effects") + TSoftClassPtr DynamicTagGameplayEffect; +}; diff --git a/Source/ols/Public/Messages/OLSVerbMessageHelpers.h b/Source/ols/Public/Messages/OLSVerbMessageHelpers.h index 74210bd..f022f63 100644 --- a/Source/ols/Public/Messages/OLSVerbMessageHelpers.h +++ b/Source/ols/Public/Messages/OLSVerbMessageHelpers.h @@ -16,15 +16,15 @@ class OLS_API UOLSVerbMessageHelpers : public UBlueprintFunctionLibrary public: - UFUNCTION(BlueprintCallable, Category = "Lyra") + UFUNCTION(BlueprintCallable, Category = "OLS") static class APlayerState* GetPlayerStateFromObject(UObject* object); - UFUNCTION(BlueprintCallable, Category = "Lyra") + UFUNCTION(BlueprintCallable, Category = "OLS") static class APlayerController* GetPlayerControllerFromObject(UObject* object); - UFUNCTION(BlueprintCallable, Category = "Lyra") + UFUNCTION(BlueprintCallable, Category = "OLS") static struct FGameplayCueParameters VerbMessageToCueParameters(const struct FOLSVerbMessage& message); - UFUNCTION(BlueprintCallable, Category = "Lyra") + UFUNCTION(BlueprintCallable, Category = "OLS") static struct FOLSVerbMessage CueParametersToVerbMessage(const FGameplayCueParameters& params); }; diff --git a/Source/ols/Public/Systems/OLSAssetManager.h b/Source/ols/Public/Systems/OLSAssetManager.h index 27d0d34..3a8571a 100644 --- a/Source/ols/Public/Systems/OLSAssetManager.h +++ b/Source/ols/Public/Systems/OLSAssetManager.h @@ -33,17 +33,62 @@ public: // Returns the asset referenced by a TSoftObjectPtr. This will synchronously load the asset if it's not already loaded. template - static AssetType* GetAsset(const TSoftObjectPtr& assetPointer, bool shouldKeepInMemory = true); + static AssetType* GetAsset(const TSoftObjectPtr& assetPointer, bool shouldKeepInMemory = true) + { + AssetType* loadedAsset = nullptr; + + const FSoftObjectPath& assetPath = assetPointer.ToSoftObjectPath(); + + if (assetPath.IsValid()) + { + loadedAsset = assetPointer.Get(); + if (!loadedAsset) + { + loadedAsset = Cast(SynchronousLoadAsset(assetPath)); + ensureAlwaysMsgf(loadedAsset, TEXT("Failed to load asset [%s]"), *assetPointer.ToString()); + } + + if (loadedAsset && shouldKeepInMemory) + { + // Added to loaded asset list. + Get().AddLoadedAsset(Cast(loadedAsset)); + } + } + + return loadedAsset; + } // Returns the subclass referenced by a TSoftClassPtr. This will synchronously load the asset if it's not already loaded. template - static TSubclassOf GetSubclass(const TSoftClassPtr& assetPointer, bool shouldKeepInMemory = true); - + static TSubclassOf GetSubclass(const TSoftClassPtr& assetPointer, bool shouldKeepInMemory = true) + { + TSubclassOf loadedSubclass = nullptr; + + const FSoftObjectPath& assetPath = assetPointer.ToSoftObjectPath(); + + if (assetPath.IsValid()) + { + loadedSubclass = assetPointer.Get(); + if (!loadedSubclass) + { + loadedSubclass = Cast(SynchronousLoadAsset(assetPath)); + ensureAlwaysMsgf(loadedSubclass, TEXT("Failed to load asset class [%s]"), *assetPointer.ToString()); + } + + if (loadedSubclass && shouldKeepInMemory) + { + // Added to loaded asset list. + Get().AddLoadedAsset(Cast(loadedSubclass)); + } + } + + return loadedSubclass; + } + // Logs all assets currently loaded and tracked by the asset manager. static void DumpLoadedAssets(); - // @Todo implement this function. - // const class UOLSPawnPrimaryDataAsset& GetGameData(); + const class UOLSGameDataAsset& GetGameData(); const class UOLSPawnDataAsset* GetDefaultPawnData() const; protected: @@ -82,10 +127,9 @@ private: protected: - // @Todo implement this. // Global game data asset to use. - // UPROPERTY(Config) - // TSoftObjectPtr LyraGameDataPath; + UPROPERTY(Config) + TSoftObjectPtr OLSGameDataPath; // Loaded version of the game data UPROPERTY(Transient)