// Copyright (c) 2026 Reignance Studios. MIT License, see LICENSE. #include "Abilities/GKLSampleAbilities.h" #include "AbilitySystemComponent.h" #include "AbilitySystemGlobals.h" #include "Engine/OverlapResult.h" #include "Engine/World.h" #include "GameFramework/Character.h" #include "GKLCharacter.h" #include "GKLEffects.h" #include "GKLGameplayTags.h" // ------------------------------------------------------------------ Jump UGKLAbility_Jump::UGKLAbility_Jump() { AddAbilityTag(GKLTags::Ability_Jump); StaminaCost = 10.f; } bool UGKLAbility_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const { const ACharacter* Character = ActorInfo ? Cast(ActorInfo->AvatarActor.Get()) : nullptr; return Character && Character->CanJump() && Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags); } void UGKLAbility_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); return; } // The movement component predicts and replicates the jump itself. if (ACharacter* Character = Cast(ActorInfo->AvatarActor.Get())) { Character->Jump(); } EndAbility(Handle, ActorInfo, ActivationInfo, true, false); } // ------------------------------------------------------------------ Heal UGKLAbility_Heal::UGKLAbility_Heal() { AddAbilityTag(GKLTags::Ability_Heal); ManaCost = 25.f; CooldownDuration = 6.f; CooldownTags.AddTag(GKLTags::Cooldown_Heal); } void UGKLAbility_Heal::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { // Commit first, before any latent work, so cost and cooldown are predicted. if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); return; } const FGameplayEffectSpecHandle Spec = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, UGKLHealEffect::StaticClass(), GetAbilityLevel(Handle, ActorInfo)); if (Spec.IsValid()) { Spec.Data->SetSetByCallerMagnitude(GKLTags::Data_Healing, HealAmount); ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, Spec); } EndAbility(Handle, ActorInfo, ActivationInfo, true, false); } // ------------------------------------------------------------------ Strike UGKLAbility_Strike::UGKLAbility_Strike() { AddAbilityTag(GKLTags::Ability_Strike); StaminaCost = 15.f; CooldownDuration = 0.8f; CooldownTags.AddTag(GKLTags::Cooldown_Strike); } void UGKLAbility_Strike::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); return; } // Damage is server-authoritative: the client predicts the cost and cooldown, never the hit. AActor* Avatar = ActorInfo->AvatarActor.Get(); UAbilitySystemComponent* SourceASC = ActorInfo->AbilitySystemComponent.Get(); if (HasAuthority(&ActivationInfo) && Avatar && SourceASC) { const FVector Center = Avatar->GetActorLocation() + Avatar->GetActorForwardVector() * Range; FCollisionQueryParams Params(SCENE_QUERY_STAT(GKLStrike), false, Avatar); TArray Overlaps; Avatar->GetWorld()->OverlapMultiByObjectType(Overlaps, Center, FQuat::Identity, FCollisionObjectQueryParams(ECC_Pawn), FCollisionShape::MakeSphere(Radius), Params); TSet Hit; for (const FOverlapResult& Overlap : Overlaps) { UAbilitySystemComponent* TargetASC = UAbilitySystemGlobals::GetAbilitySystemComponentFromActor(Overlap.GetActor()); if (!TargetASC || TargetASC == SourceASC || Hit.Contains(TargetASC)) { continue; } Hit.Add(TargetASC); const FGameplayEffectSpecHandle Spec = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, UGKLDamageEffect::StaticClass(), GetAbilityLevel(Handle, ActorInfo)); if (Spec.IsValid()) { Spec.Data->SetSetByCallerMagnitude(GKLTags::Data_Damage, DamageAmount); SourceASC->ApplyGameplayEffectSpecToTarget(*Spec.Data.Get(), TargetASC); } } } EndAbility(Handle, ActorInfo, ActivationInfo, true, false); } // ------------------------------------------------------------------ Regen UGKLAbility_Regen::UGKLAbility_Regen() { AddAbilityTag(GKLTags::Ability_Passive_Regen); NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly; } void UGKLAbility_Regen::OnGiveAbility(const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilitySpec& Spec) { Super::OnGiveAbility(ActorInfo, Spec); // Passive: start as soon as the server grants it. if (ActorInfo && ActorInfo->IsNetAuthority() && !Spec.IsActive()) { ActorInfo->AbilitySystemComponent->TryActivateAbility(Spec.Handle); } } void UGKLAbility_Regen::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); return; } const FGameplayEffectSpecHandle Spec = MakeOutgoingGameplayEffectSpec(Handle, ActorInfo, ActivationInfo, UGKLRegenEffect::StaticClass(), GetAbilityLevel(Handle, ActorInfo)); RegenHandle = ApplyGameplayEffectSpecToOwner(Handle, ActorInfo, ActivationInfo, Spec); // Stays active until cancelled (Death cancels it). } void UGKLAbility_Regen::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) { if (RegenHandle.IsValid() && ActorInfo && ActorInfo->AbilitySystemComponent.IsValid()) { ActorInfo->AbilitySystemComponent->RemoveActiveGameplayEffect(RegenHandle); RegenHandle.Invalidate(); } Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); } // ------------------------------------------------------------------ Death UGKLAbility_Death::UGKLAbility_Death() { AddAbilityTag(GKLTags::Ability_Death); NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly; FAbilityTriggerData Trigger; Trigger.TriggerTag = GKLTags::Event_Death; Trigger.TriggerSource = EGameplayAbilityTriggerSource::GameplayEvent; AbilityTriggers.Add(Trigger); } void UGKLAbility_Death::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { if (!CommitAbility(Handle, ActorInfo, ActivationInfo)) { EndAbility(Handle, ActorInfo, ActivationInfo, true, true); return; } UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get(); ASC->CancelAllAbilities(this); // Replicated, so clients see State.Dead too. GKLTags::AddReplicatedLooseTag(ASC, GKLTags::State_Dead); if (AGKLCharacter* Character = GetLiteCharacter()) { Character->Die(); } EndAbility(Handle, ActorInfo, ActivationInfo, true, false); }