// Copyright 2025 https://yuewu.dev/en All Rights Reserved. #include "Abilities/GCS_ComboAbility.h" #include "AbilitySystemComponent.h" #include "GCS_CombatEntityInterface.h" #include "GCS_CombatSystemComponent.h" #include "GCS_LogChannels.h" #include "GGA_GameplayTags.h" #include "Abilities/Tasks/AbilityTask_WaitInputPress.h" #include "Combo/GCS_ComboDefinition.h" #include "Utilities/GGA_AbilitySystemFunctionLibrary.h" #include "Weapon/GCS_WeaponInterface.h" UGCS_ComboAbility::UGCS_ComboAbility() { ReplicationPolicy = EGameplayAbilityReplicationPolicy::ReplicateYes; NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::LocalPredicted; AbilityTags.AddTagFast(GGA_AbilityTraitTags::ActivationOnSpawn); } void UGCS_ComboAbility::PreActivate(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, FOnGameplayAbilityEnded::FDelegate* OnGameplayAbilityEndedDelegate, const FGameplayEventData* TriggerEventData) { Super::PreActivate(Handle, ActorInfo, ActivationInfo, OnGameplayAbilityEndedDelegate, TriggerEventData); } void UGCS_ComboAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { UGCS_CombatSystemComponent* CombatSys = GetCombatSystemFromActorInfo(); if (!CombatSys) { EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false); return; } UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); AbilityEndedDelegateHandle = ASC->OnAbilityEnded.AddUObject(this, &ThisClass::HandleAbilityEnd); Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); } bool UGCS_ComboAbility::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const { return Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags); } void UGCS_ComboAbility::OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) { Super::OnGiveAbility(ActorInfo, Spec); // GiveSubAbilities(Spec); } void UGCS_ComboAbility::OnRemoveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) { Super::OnRemoveAbility(ActorInfo, Spec); // RemoveSubAbilities(); } void UGCS_ComboAbility::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) { UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); if (IsValid(ASC) && AbilityEndedDelegateHandle.IsValid()) { ASC->OnAbilityEnded.Remove(AbilityEndedDelegateHandle); AbilityEndedDelegateHandle.Reset(); } // final check to make current ability ends(no callback will fired.)! if (CurrentAbility.IsValid() && !bCurrentAbilityEnded) { ASC->CancelAbilityHandle(CurrentAbility); CurrentAbility = FGameplayAbilitySpecHandle(); CurrentAbilityClass = nullptr; } ResetCombo(); Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); } bool UGCS_ComboAbility::AllowAdvanceCombo_Implementation() const { return true; } void UGCS_ComboAbility::StartCombo_Implementation(const FGameplayEventData& ComboEvent) { HandleComboExecution(ComboEvent); } void UGCS_ComboAbility::AdvanceCombo_Implementation(const FGameplayEventData& ComboEventData) { if (!AllowAdvanceCombo()) { GCS_CLOG(Verbose, "Can't advanced combo due to AllowAdvanceCombo.") return; } HandleComboExecution(ComboEventData); } void UGCS_ComboAbility::ResetCombo_Implementation() { DesiredComboStep = INDEX_NONE; bCurrentAbilityEnded = false; UGCS_CombatSystemComponent* CombatSys = GetCombatSystemFromActorInfo(); if (IsValid(CombatSys)) { CombatSys->ResetComboState(); } } void UGCS_ComboAbility::HandleAbilityEnd(const FAbilityEndedData& AbilityEndedData) { //Already ends if (bCurrentAbilityEnded) { return; } //no ability running. if (!CurrentAbility.IsValid() || CurrentAbility != AbilityEndedData.AbilitySpecHandle) { return; } // GCS_CLOG(Verbose, "Current ability:%s ended. bWasCancelled:%d", *CurrentAbilityClass->GetName(), AbilityEndedData.bWasCancelled) bCurrentAbilityEnded = true; CurrentAbility = FGameplayAbilitySpecHandle(); CurrentAbilityClass = nullptr; // No next combo. if (DesiredComboStep == INDEX_NONE) { ResetCombo(); } } bool UGCS_ComboAbility::SelectComboDefinition(const FGameplayEventData& ComboEventData, int32 CurrentStep, FGCS_ComboDefinition& OutDefinition) { UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); UObject* Combat = GetCombatEntityFromActorInfo(); if (!ASC || !Combat) return false; TObjectPtr ComboDefinitionTable = IGCS_CombatEntityInterface::Execute_GetComboDefinitionTable(Combat); if (ComboDefinitionTable == nullptr) { GCS_CLOG(Error, "No combo definition table found from combat interface:%s, check your GetComboDefinitionTable implementation!", *GetNameSafe(Combat)); return false; } FGameplayTagContainer CurrentTags; ASC->GetOwnedGameplayTags(CurrentTags); // Find best matching row in data table for (const auto& RowPair : ComboDefinitionTable->GetRowMap()) { const FGCS_ComboDefinition* Row = reinterpret_cast(RowPair.Value); if (!Row || Row->AbilityClass.IsNull()) continue; if (!Row->TagQuery.IsEmpty() && !Row->TagQuery.Matches(CurrentTags)) { continue; } // skip if event tag doesn't match. if (ComboEventData.EventTag.IsValid() && Row->EventTag.IsValid() && ComboEventData.EventTag != Row->EventTag) { continue; } // skip if event instigatorTags if (!Row->EventInstigatorTagQuery.IsEmpty() && !Row->EventInstigatorTagQuery.Matches(ComboEventData.InstigatorTags)) { continue; } if (CurrentStep > 0 && Row->MinComboStep == 0) { continue; } // skip if MinComboStep < CurrentStep(only for non resetting combo) if (!Row->bResetComboStep && Row->MinComboStep > 0 && Row->MinComboStep < CurrentStep) continue; // skip if ability class is invalid. TSubclassOf TempAbilityClass = Row->AbilityClass.LoadSynchronous(); if (TempAbilityClass == nullptr) { GCS_CLOG(Verbose, "invalid ability class found on row name:%s", *RowPair.Key.ToString()); continue; } FGameplayAbilitySpecHandle FoundAbility; if (!UGGA_AbilitySystemFunctionLibrary::FindAbilityFromClass(ASC, FoundAbility, TempAbilityClass, GetCurrentSourceObject())) { GCS_CLOG(Verbose, "skipped ability of class(%s) as it is not exists on ASC.", *TempAbilityClass->GetName()); continue; } if (Row->bRunActivationTest) { FGameplayTagContainer RelevantTags; if (!UGGA_AbilitySystemFunctionLibrary::CanActivateAbility(ASC, FoundAbility, RelevantTags)) { if (Row->bAbortIfActivationTestFailed) { GCS_CLOG(Verbose, "Activation test failed for ability:%s, reason:%s. Combo will be aborted.", *TempAbilityClass->GetName(), *RelevantTags.ToString()); return false; } { GCS_CLOG(Verbose, "skipped ability of class(%s) due to activation test failed, reason:%s", *TempAbilityClass->GetName(), *RelevantTags.ToString()); continue; } } } // skip if custom rule doesn't met. if (!CanSelectedComboDefinition(ComboEventData, CurrentStep, OutDefinition)) { continue; } GCS_CLOG(Verbose, "selected ability of class(%s) as next combo(%d).", *TempAbilityClass->GetName(), CurrentStep); OutDefinition = *Row; return true; } return false; } bool UGCS_ComboAbility::CanSelectedComboDefinition_Implementation(const FGameplayEventData& ComboEvent, int32 CurrentStep, const FGCS_ComboDefinition& ComboDefinition) const { // Example code. // FYourCustomData Data = ComboDefinition.Extension.Get(); // UObject* Weapon = IGCS_CombatInterface::Execute_GCS_GetWeapon(GetCombatInterfaceFromActorInfo(), nullptr); // return IGCS_WeaponInterface::Execute_GetWeaponTags(Weapon).MatchesQuery(Data.WeaponTagRequirements); return true; } void UGCS_ComboAbility::HandleComboExecution(const FGameplayEventData& ComboEventData) { if (!IsLocallyControlled()) { return; } UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); if (!ASC) return; UGCS_CombatSystemComponent* CombatSys = GetCombatSystemFromActorInfo(); int32 CurrentStep = CombatSys ? CombatSys->GetComboStep() : 0; bool bStartingCombo = CurrentStep == 0; FGCS_ComboDefinition ComboDefinition; if (!SelectComboDefinition(ComboEventData, CurrentStep, ComboDefinition)) { GCS_CLOG(Verbose, "No next combo definition with current combo step:%d", CurrentStep); DesiredComboStep = INDEX_NONE; // mark for no next action. return; } TSubclassOf FoundAbilityClass = ComboDefinition.AbilityClass.Get(); FGameplayAbilitySpecHandle FoundAbility; UGGA_AbilitySystemFunctionLibrary::FindAbilityFromClass(ASC, FoundAbility, FoundAbilityClass, GetCurrentSourceObject()); check(FoundAbility.IsValid()) bool bShouldResetComboStep = !bStartingCombo && ComboDefinition.MinComboStep > 0 && ComboDefinition.bResetComboStep; // Mark the desired step. DesiredComboStep = bShouldResetComboStep ? 0 : CombatSys->GetComboStep() + 1; // Cancel current ability if still running. if (CurrentAbility.IsValid() && !bCurrentAbilityEnded) { ASC->CancelAbilityHandle(CurrentAbility); CurrentAbility = FGameplayAbilitySpecHandle(); CurrentAbilityClass = nullptr; } bCurrentAbilityEnded = false; //Combo Ability本身是预测激活的. if (ASC->TryActivateAbility(FoundAbility, true)) { CurrentAbility = FoundAbility; CurrentAbilityClass = FoundAbilityClass; CombatSys->UpdateComboStep(DesiredComboStep); GCS_CLOG(Verbose, "combo advanced from step %d to %d with ability:%s", CurrentStep, DesiredComboStep, *FoundAbilityClass->GetName()); DesiredComboStep = INDEX_NONE; } else { GCS_CLOG(Verbose, "combo reset as the new ability(%s) can't be activated when advancing from step %d to %d.", *FoundAbilityClass->GetName(), CurrentStep, DesiredComboStep); ResetCombo(); } } // void UGCS_ComboAbility::GiveSubAbilities(const FGameplayAbilitySpec& CurrentSpec) // { // UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); // if (!IsValid(ASC) || !ASC->IsOwnerActorAuthoritative()) // { // return; // } // // // Find best matching row in data table // for (const auto& RowPair : ComboDefinitionTable->GetRowMap()) // { // const FGCS_ComboDefinition* Row = reinterpret_cast(RowPair.Value); // if (!Row || Row->AbilityClass.IsNull()) continue; // TSubclassOf AbilityClass = Row->AbilityClass.LoadSynchronous(); // // if (FGameplayAbilitySpec* ExistingSpec = UGGA_AbilitySystemFunctionLibrary::FindAbilitySpecFromClass(ASC, AbilityClass, CurrentSpec.SourceObject.Get())) // { // GCS_CLOG(Warning, "Found existing ability with same class(%s) and source object(%s),skipped.", *AbilityClass->GetName(), *GetNameSafe(CurrentSpec.SourceObject.Get())) // continue; // } // // FGameplayAbilitySpec NewSpec = ASC->BuildAbilitySpecFromClass(AbilityClass, Row->MinComboStep > 0 ? Row->MinComboStep : 0); // NewSpec.SourceObject = CurrentSpec.SourceObject; // const FGameplayAbilitySpecHandle& AbilitySpecHandle = ASC->GiveAbility(NewSpec); // if (AbilitySpecHandle.IsValid()) // { // AvailableAbilities.Add(AbilitySpecHandle); // } // } // } // // void UGCS_ComboAbility::RemoveSubAbilities() // { // UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo(); // if (!IsValid(ASC)) // { // return; // } // for (FGameplayAbilitySpecHandle SubAbilityHandle : AvailableAbilities) // { // ASC->ClearAbility(SubAbilityHandle); // } // AvailableAbilities.Empty(); // } #if WITH_EDITOR EDataValidationResult UGCS_ComboAbility::IsDataValid(class FDataValidationContext& Context) const { return Super::IsDataValid(Context); } #endif