Skip to content

Ability Classes

Summary: Project Eternal defines a hierarchy of gameplay abilities extending from UEternalAbility. This document explains the design rationale, class responsibilities, and how to choose the right base class for new abilities.


Why This Hierarchy?

The ability hierarchy solves several problems: - Code Reuse: Common patterns (montage playing, weapon access, stamina cost) centralized - Type Safety: Compile-time guarantees about ability capabilities - Blueprint Extensibility: Each level exposes appropriate hooks for designers - Combat Consistency: Shared damage/poise calculations across all player attacks


Hierarchy Overview

UGameplayAbility (Engine)
        |
        v
+---------------------------+
| UEternalAbility   |  Base: input binding, UI metadata
+---------------------------+
        |
        +---------------+------------------+------------------+------------------+
        |               |                  |                  |                  |
        v               v                  v                  v                  v
+---------------+ +---------------+ +---------------+ +---------------+ +---------------+
| UEternalDamage| | UEternalAura  | | UEternalConsum| | UEternalOnHit | | UDodgeAbility |
| GameplayAbility| Ability       | | ableAbility   | | Ability       | | USprintAbility|
+---------------+ +---------------+ +---------------+ +---------------+ | UHitReactAbil |
        |                                                   |           +---------------+
        v                                                   v
+---------------------------+               +---------------------------+
| UPlayerCombatAbility      |               | UEternalPressureBuilder   |  Stack builder +
+---------------------------+               | Ability (Echo Mod)        |  release pairing
        |                                   +---------------------------+
        |
        +---------------+
        |               |
        v               v
+---------------+ +---------------+
| UEternalMelee | | UEternalSkill |
| Attack        | | Ability       |
| (Combo/Charge)| | (Single use)  |
+---------------+ +---------------+
                        |
                        v
                +---------------------------+
                | UAOESkillAbility   |  Body-originated AOE skill
                | (own DamageTypes, no      |  (novas/cones/rings)
                |  weapon gate)             |
                +---------------------------+
                        |
                        v
                +---------------------------+
                | ULeapAbility       |  Ballistic launch +
                | (Crash Landing)           |  landing-driven impact
                +---------------------------+

Enemy Branch:
+---------------------------+
| UEnemyAbility             |  AI: montage selection, controller access
+---------------------------+
        |
        +---------------+
        |               |
        v               v
+---------------+ +---------------+
| UEnemyAoe     | | UEnemyProject |
| Ability*      | | ileAbility    |
+---------------+ +---------------+
                        |
                        v
                  +---------------+
                  | UEnemyRanged  |
                  | Ability       |
                  +---------------+

* UEnemyAoeAbility inherits from UAOEDamageAbility, not UEnemyAbility

Base Classes

UEternalAbility

Purpose: Foundation for all project abilities with input binding and UI support.

Responsibility How It Works
Input Binding StartupInputTag added to DynamicAbilityTags on grant
UI Display AbilityName, AbilityDescription, AbilityIcon for tooltips
Montage Lookup Delegates to IMontageManagerInterface on owning actor (the weapon-table route — see Montage Resolution)
Tooltip Detail Virtual GetTooltipDetailText() (default empty, pure const, CDO-safe) lets an ability describe its own mechanics for Alt-hold item tooltips — numbers stay co-located with the logic that consumes them

Tooltip Detail Pipeline (Echo Mods): Item tooltips ask a granted ability's CDO for GetTooltipDetailText() and surface it under Alt-hold for revealed Echo Mods. This keeps tooltip numbers in sync with the ability's actual config (no authoring drift). UPressureBuilderAbility is the first consumer.


Montage Resolution

There are two montage pipelines. Which one applies is decided by the ability's class, not by authoring choice — a player skill has exactly one route available to it, and so does an enemy.

The two routes

Route Who owns the montage Used by
Ability-owned (the only player-skill route) SkillMontages on the ability CDO — a TMap<FGameplayTag /*WeaponHand*/, UAnimMontage*> USkillAbility, UProjectileAbility, UAOESkillAbility, ULeapAbility — everything under UPlayerCombatAbility except UMeleeAbility
Tag-keyed table An FMontageAction row (ActionTagMontages[]) in a UDataTable, hanging off either the enemy's MontageManagerComponent or (for attacks and reactions) the equipped weapon's FWeaponFragment::MontageTables UEnemyAbility / UEnemyAoeAbility (own ActionTag), and — keyed by native tags rather than a CDO property — UMeleeAbility (combo index), UChargeComponent (the released charged swing only — the wind-up loop is an ability montage on UChargeWindupAbility), UHitReactAbility (direction × severity), UDodgeAbility

Resolution

UPlayerCombatAbility::ResolveSkillMontage() implements the whole player rule:

return SkillMontages[avatar's current WeaponHand]   // exact hand-context match
    or the first non-null entry                     // fallback: hand context doesn't apply
    or null                                         // the skill plays nothing — PlaySkillMontage ends the ability

There is no weapon-table fallback. ActionTag does not exist on UPlayerCombatAbility: it lives on the two enemy bases (UEnemyAbility, UEnemyAoeAbility), which sit on different branches of UDamageAbility and therefore each declare it.

Why skills own their montage

A skill's feel belongs to the skill, not to whatever the player happens to be holding. Crash Landing lands the same way with a greatsword or a mace. Routing skill animation through the weapon's table would mean re-authoring the same row on every weapon that can ever carry the skill, and crafted/kit abilities have no weapon to hang a table off at all — UAOESkillAbility deliberately skips the weapon-fragment gate, so GetAvailableCombatMontages() returns null on that branch anyway.

The route the weapon table nominally offered — "this skill animates differently per weapon" — was never used by any shipped ability, because a weapon skill is granted by the weapon that animates it (Mace grants GA_RisingMace). The indirection bought an affordance nobody wanted and cost the validator its only montage guard (see below).

Weapon attacks are the opposite case: the montage is the weapon's identity, it varies by combo step and hand context, and the ability (UMeleeAbility) is a shared driver. Those keep the table.

There is deliberately no central skill-montage table — it would be indirection with no gain over a direct reference on the CDO.

SkillMontages is keyed by WeaponHand even for weaponless AOE skills, where the key is semantically meaningless and only the first-non-null fallback fires. Authoring a single entry under any key is the intended pattern there.

What the validator can see

SkillMontages lives on the CDO, so CheckMontageWiring checks it directly: a player skill with no entry warns, or errors when it is an AOE skill that also has no spawn path. A table row cannot be checked — the tables hang off the runtime avatar and equipped weapon, and touching those accessors on a CDO trips GAS ensures — so for enemies the validator can only assert that an ActionTag key exists. Whether a row answers it stays a PIE check.

Two families are exempt outright, because both resolve montages where no CDO can reach: UMeleeAbility (the weapon's combo configs) and UAOEDamageAbility (trigger-driven, no avatar animation).

The check earned that shape the hard way. Until 2026-07-08 it returned early on any non-None ActionTag, and fifteen melee abilities passed it while holding a tag no row answered — so a typo'd tag on a real skill validated green and played nothing. The property's removal from the player branch had to be paired with restructuring the check, because an else if (ActionTag) branch that loses its field goes silent, it does not start failing. AbilityValidation.spec.cpp now pins each branch, and errors if UEnemyAbility::ActionTag ever disappears.

Hand-context variants

SkillMontages already supports per-hand variants (Right, Left, Both, RightWithShield). Start with a single entry; add hand-context overrides only when a skill actually needs to look different in different grips.

When to Extend: Non-combat abilities that need input and UI (menus, interactions).

UDamageAbility

Purpose: Base for anything that deals damage to targets.

Responsibility How It Works
Damage Config TMap<FGameplayTag, FScalableFloat> DamageTypes for multi-element damage
Hit Events WaitGameplayEvent listens for Event.Combat.Hit from animation notifies
Weapon Access GetMainHandWeapon(), GetOffHandWeapon() via EquipmentComponent
Attack Speed Virtual CalculateAttackSpeed() for montage rate scaling

When to Extend: Enemy abilities, environmental hazards, traps.

UPlayerCombatAbility

Purpose: Player-specific combat with weapon stats, stamina costs, and poise integration.

Responsibility How It Works
Damage Calculation Uses CombatComponent->CalculateFinalDamage() with weapon stats
Stamina Cost ConsumeStamina() validates and deducts before activation
Poise Damage CalculatePoiseDamage() with weapon multipliers
Cached Damage CachedFinalDamage prevents redundant calculations
Cancel on Activate Cancels Block and Crouch via CancelAbilitiesWithTag (State.Blocking, State.Crouching)

When to Extend: All player attacks (melee, skills, weapon arts).


Combat Ability Types

UMeleeAbility

Purpose: Combo chains and charged attacks for melee weapons.

The ability is payload-driven: the client resolves the whole swing at press time (combo step, aim, claimed hold) and ships it as FAttackIntentSnapshot. Both machines then read the same values, so montage index, play rate, multipliers and rotation cannot be re-derived per machine.

Activation Flow:
+----------------+     +----------------+     +----------------+
| Read intent    | --> | Validate the   | --> | Adopt the      |
| from payload   |     | claim (server) |     | combo step     |
+----------------+     +----------------+     +----------------+
                                                      |
                                                      v
+----------------+     +----------------+     +----------------+
| Play combo or  | <-- | Open rotation  | <-- | Price stamina  |
| charge montage |     | + warp from    |     | from the       |
+----------------+     | payload aim    |     | payload step   |
        |              +----------------+     +----------------+
        v
+----------------+
| Wait for Hit   |
| Events         |
+----------------+
Feature Implementation
Combo Tracking AdoptComboStep(PayloadStep, AttackType) on UComboComponent — the step is never re-derived locally
Charge Attacks HandleChargedAttack() classifies charged-or-not from the payload's claimed hold, never this machine's own charge state (the replica is ~RTT stale on the owning client). Returns true when the authority rejects a forged claim — the caller ends the ability
Stamina SetStamina(..., PayloadComboStep) — pricing reads the same step number as montage choice
Rotation / warp ActionRotationHandle = CombatComp->OpenAttackRotation(Intent) in ActivateAbility, released in EndAbility on every machine it ran on
Attack Speed Per-combo-step (CalculateComboAttackSpeed) or per-charge-level (CalculateChargedAttackSpeed)
IsHeavyAttack() Reads the class default StartupInputTag, never the live spec. A kit's InputTagOverride rewrites only the spec's dynamic tags, so resolving from the spec would make a heavy granted to an ability slot match neither RMB nor Charged and silently price and scale as light

UChargeWindupAbility

Purpose: The heavy-attack charge stance — the looping wind-up montage, as a predicted ability.

Feature Implementation
Net execution LocalPredicted, InstancedPerActor
Granting Granted in C++ (combat plumbing shared by every weapon, not kit content) and activated by class — no Blueprint child, no trigger tags
Montage The loop plays through the stock PlayMontageAndWait pipeline, so ending the ability stops exactly the instance it started — the loop cannot strand on a machine or race the follow-up swing
Measurement The server instance drives UChargeComponent::BeginCharge / EndCharge / CancelCharge; the released swing is a separate melee activation whose payload claims the hold

USkillAbility

Purpose: Single-use weapon-based skills (weapon arts, special moves). Also the home of the universal skill cooldown — every slotted activatable skill inherits it from here, including UAOESkillAbility and ULeapAbility.

Feature Implementation
Montage Flow PlaySkillMontage() (hoisted to UPlayerCombatAbility): resolve -> play -> wait for complete/interrupt. The montage comes from the ability's own SkillMontages; see Montage Resolution
Calculations Inherits all weapon math from UPlayerCombatAbility
Cooldown Overrides the four GAS cooldown virtuals — GetCooldownTags, ApplyCooldown, CheckCooldown, GetCooldownTimeRemainingAndDuration

Universal Skill Cooldowns

One shared duration GE (CooldownGameplayEffectClass) serves every skill. Per-ability blocking comes from the ability's own CooldownTag added to the spec's DynamicGrantedTags, with the duration passed via SetByCaller_Cooldown. Authoring a skill therefore means filling in two properties, not creating a GE asset:

Property Type Purpose
CooldownDuration FScalableFloat Cooldown seconds per rank. 0 = no cooldown
CooldownTag FGameplayTag Per-ability tag granted by the shared GE while on cooldown. Blueprint-authored

Overriding GetCooldownTimeRemainingAndDuration (via FindActiveCooldown, which locates the live cooldown window on the owner's ASC) is what lets the HUD read remaining and total for any skill through the standard GAS API — that is the contract the skillbar's radial sweep binds to. Without it, a skill with a cooldown renders no sweep.

GetCooldownDurationAtLevel() is a CDO-readable accessor: the headless Build Lab composer paces active-chain cadence from it without instantiating the ability.

CooldownTag is set in the Blueprint, not the constructor — FEternalGameplayTags::Get() is empty during CDO construction. See the pitfall in CLAUDE.md.

UAOESkillAbility

Purpose: Player-activated, body-originated AOE skill (novas, cones, ground rings — e.g. Thorn Emergence, Palm Discharge). Unlike a weapon skill it needs no equipped weapon: its ActivateAbility chains to UPlayerCombatAbility::ActivateAbility, skipping USkillAbility's weapon-fragment gate, and damage comes from the ability's own DamageTypes values (FScalableFloat), never weapon properties.

Feature Implementation
Net Policy LocalPredicted + InstancedPerActor (pinned in the constructor) — activation predicts for input feel; the cooldown GE predicts under the activation key; costs apply unpredicted and replicate server-authoritatively
Dual Cost TryPayActivationCosts() checks both pools, then deducts via the shared SetByCaller cost GEs — a dual-cost ability pays both Resonance and Stamina. No direct-set fallback: a missing cost effect class fails activation loudly (validated at authoring time)
Shared Cooldown Inherited from USkillAbility — see Universal Skill Cooldowns. Author CooldownDuration + CooldownTag; the machinery lives on the base
AOE Spawn Server-only, normally driven by AnimNotify_SpawnAOE on the skill montage; bSpawnAOEOnActivation covers instant novas and montage-less activation
Damage CauseDamage / CalculateFinalDamage sum the DamageTypes curves at ability level and feed them through the grandparent (UDamageAbility) SetByCaller path; poise carries no weapon scaling (the AOE hit path applies SkillAOEPoiseMultiplier)

Author-facing UPROPERTYs: ResonanceCostMagnitude, StaminaCostMagnitude, bSpawnAOEOnActivation, plus the inherited CooldownDuration / CooldownTag.

When to Extend: Player AOE skills that originate from the body rather than a weapon swing.

ULeapAbility

Purpose: Player leap skill (Crash Landing) — a UAOESkillAbility whose AOE fires at the landing point, never on activation or via a montage notify. The launch + landing detection are the new verb; damage, poise, cost, and cooldown are the inherited AOE-skill contract.

Activation:
  chain to UPlayerCombatAbility::ActivateAbility (skip weapon + AOE-skill flow)
  ClearLeapState  ->  ComputeLaunchVelocity  ->  TryPayActivationCosts  ->  ApplyCooldown
        |
        v
  bind LandedDelegate + LaunchCharacter(ballistic velocity) + start 5s timeout
        |
    (lands)                              (never lands: ragdoll/teleport)
        v                                        v
  OnLeapLanded:                            OnLeapTimeout:
   server -> SpawnSkillAOE at landing       end without impact
   end ability (server owns replicated end)
Feature Implementation
Ballistic Launch ComputeLaunchVelocity builds a flat-ground arc: LeapApexHeight (plain float) fixes flight time; LeapDistance (FScalableFloat) fixes forward speed. LaunchCharacter along the avatar's facing
Landing Impact LandedDelegate -> OnLeapLanded spawns the Skill AOE at the body's landing point (the inherited spawn transform reads the now-landed avatar)
Timeout 5s engineering backstop ends the ability without impact if a landing never arrives
Idempotent Cleanup ClearLeapState unbinds the landing hook + kills the timer; runs on land, on timeout, and in EndAbility, so a late Landed event can never fire a stale impact
Authority Contract The impact AOE spawns server-only (shared statics authority guard), and only the server owns the replicated EndAbility. A predicted client lands first; a replicated client end would tear down the server instance before its own landing and silently drop the impact under latency — so the client closes its local prediction without propagating

When to Extend: Movement-into-impact skills that must resolve their effect at a travel destination.

UAOEDamageAbility

Purpose: Area of effect damage with visual actors.

Feature Implementation
Shape Config FAOEShapeConfig defines sphere/box/cylinder
AOE Actor Spawns AEternalAOEActor for collision detection
Visual Effects Optional UNiagaraSystem attached to AOE actor
Hit Callback OnAOEHit(TArray<FHitResult>) processes all hits

AOE Actor shapes: The base AEternalAOEActor family historically used cone/forward-arc detection. AEternalSphereAOEActor is the project's first radial shape — sphere overlap with no angle filter, using Config.Range as the radius. Used by UPressureReleaseAbility.

Shared spawn helpers (FAOEStatics): The AOE spawn/transform helpers were extracted out of UAOEDamageAbility into FAOEStatics (Public/Abilities/AOE/AOEStatics.h), shared by both the enemy AOE branch and the player skill branch so neither carries a duplicate copy. CalculateSpawnTransform derives the spawn location/rotation from the avatar (honoring the config's origin socket + local offset); SpawnAOEActor is server-only (hits apply damage, so clients never spawn — it returns nullptr without authority) and returns an un-initialized actor: the caller binds its own OnAOEHit UFUNCTION then calls Initialize(), because the dynamic delegate bind cannot live in shared code.

UProjectileAbility

Purpose: Ranged attacks with projectile spawning and patterns.

Feature Implementation
Spawn Config Socket-based or offset-based spawn transform
Patterns UProjectilePatternDataAsset for spread/burst patterns
Homing FindHomingTarget() + hovering projectile support
Tracking SpawnedProjectiles array for cleanup

Utility Abilities

UAuraAbility

Purpose: Persistent passive effects that apply on grant.

Feature Implementation
Auto-Activate bAutoActivateOnGrant applies effect immediately
Persistent Effect Infinite-duration GE stored in ActiveEffectHandle
Optional Tick bShouldTick + TickInterval for periodic logic
Blueprint Events OnAuraActivated, OnAuraDeactivated, OnAuraTick

UOnHitAbility

Purpose: Passive proc effects triggered when the owner deals damage (bleed, ignite, poison, shock).

Activation Flow:
Event.Combat.DamageDealt --> ShouldProc() --> RollChance() --> ApplyOnHitEffect()
                                |                |                    |
                          Tag checks,       Attribute or         SetByCaller
                          stack limit       base chance          damage + duration
Feature Implementation
Trigger Listens for Event.Combat.DamageDealt gameplay event (fired from HandleIncomingDamage)
Conditions Required source tags, blocked target tags, direct hit check, MaxStacks cap
Per-Hit Conditions RequiredHitTagsEvent.Hit.Critical / Event.Hit.Blocked packed into the damage event's InstigatorTags by HandleIncomingDamage, matched HasAll (every listed tag must hold on this hit). Unlocks on-crit / on-block proc affixes; empty = proc on any hit
Chance Reads from proc chance attribute (e.g., BleedChance) or falls back to BaseChance. A set ChanceAttribute OVERRIDES BaseChance — a guaranteed proc needs BaseChance = 1.0 AND ChanceAttribute cleared, or a duplicated BP silently inherits the parent's attribute and degrades to whatever chance gear the wearer has (see chanceattribute-overrides-basechance)
Damage Scaling HitDamageMultiplier × triggering hit damage (PoE-style: "X% of hit damage as DoT")
Duration SetByCaller, optionally extended by DurationMultiplierAttribute. The 4 ailment GAs (Bleed/Ignite/Poison/Shock) read the new IncreasedStatusDuration attribute (the "Lingering" affix); final = BaseDuration × (1 + attr/100), source-side (see GAS Overview)
Loop Prevention Proc GEs carry Effect.Source.OnHitProc tag; damage from tagged GEs skips event firing
Execution Server-only, instanced per actor, fire-and-forget (activates and ends immediately)
Target Selection bApplyToSelf routes the proc GE onto the wearer (self-buff) instead of the hit target
Target Resolution Hook Virtual ResolveAppliedToASC(EventData) returns the ASC the proc lands on (default: bApplyToSelf ? self : target). UOnDamagedAbility overrides it to hit the attacker
Post-Apply Hook Virtual OnEffectApplied(...) fires after the GE lands, passing the receiving ASC — subclasses react to their own proc (see UPressureBuilderAbility, UStatusCascadeAbility)

Sources that grant on-hit abilities: - Item modifiers (weapon with "20% Chance to Cause Bleeding" via FModifierDefinition::GrantedAbility) - Enemy data assets (always-proc abilities granted alongside attacks) - Passives

Blueprint subclasses: GA_OnHit_Bleed, GA_OnHit_Ignite, GA_OnHit_Poison, GA_OnHit_Shock, GA_OnHit_Pressure, GA_OnHit_IgniteOnCrit (of Searing Criticals — guaranteed ignite on crit; RequiredHitTags = Event.Hit.Critical, BaseChance = 1.0, ChanceAttribute cleared)

When to Extend: Any new ailment or on-hit proc effect. Configure in Blueprint — no C++ needed. Self-targeted accumulators (charge/stack builders) override OnEffectApplied.

UPressureBuilderAbility

Purpose: On-hit accumulator for the Pressure Release Echo Mod — attacks build "pressure" stacks on the wearer, and reaching the cap detonates a one-shot AoE. Subclass of UOnHitAbility with bApplyToSelf = true.

On every qualifying hit:
ApplyOnHitEffect (self) --> OnEffectApplied --> read Spec.GetStackCount()
                                                       |
                                  stacks < cap --------+-------- stacks >= cap
                                       |                              |
                                    (wait)              strip stacking GE FIRST,
                                                        then fire Event.Pressure.Released
                                                        (strip-before-event = no re-entrance)
Responsibility How It Works
Stack Source Applies a native-stacking GE (GE_PressureStack); reads live count from FActiveGameplayEffect::Spec.GetStackCount() after apply
Release Trigger At cap, fires Event.Pressure.Released which activates the paired release ability
Strip Ordering Removes the stacking GE before broadcasting the release event, so on-hit procs fired during release cannot re-enter and re-stack
Stack Limit GetStackLimit() reads OnHitEffectClass CDO StackLimitCount directly — release trigger, {STACK_LIMIT} status token, and tooltip detail line all read one number (no duplicated ReleaseThreshold field)
Ability Pairing Grants ReleaseAbilityClass in OnGiveAbility and clears it in OnRemoveAbility (authority-only) — the Echo Mod pipeline grants only one ability per modifier, so the builder must hand-grant its release partner
Configurable Release ReleaseEventTag (EditDefaultsOnly, falls back to Event.Pressure.Released) fires a custom release event so a second builder (e.g. Static Charge, Event.StaticCharge.Released) doesn't cross-trigger Pressure's release
Tooltip Detail Overrides GetTooltipDetailText() to compose "Triggers at N stacks, dealing X% weapon damage as AoE" (see Tooltip Detail Pipeline below)

UPressureReleaseAbility

Purpose: The detonation half of the Pressure Release Echo Mod. A concrete UAOEDamageAbility that spawns its AoE instantly on activation with no montage wind-up.

Feature Implementation
Net Policy InstancedPerActor + ServerOnly (set in constructor) — no client prediction, no manual authority guard
Spawn Timing Spawns the AoE in ActivateAbility directly; mirrors the UEnemyAoeAbility "subclass owns spawn timing" pattern (base ActivateAbility stays empty)
AOE Shape Typically uses AEternalSphereAOEActor (radial, no angle filter)
Damage Source Scales with the wearer's attack power via UPlayerCombatComponent::CalculateBaseAttackDamage() (weapon base + local mods + hard-attribute scaling, no combo/charge multiplier)
Damage Caching Overrides OnAOEHit to roll damage once per burst so every target in one release takes the same amount; cleared after the burst
Damage Feed Overrides CauseDamage to push the cached value through ExecCalc_Damage via SetByCaller — crit, armor, resistances, DamageDealtMultiplier still apply
Tuning Blueprint DamageMultiplier (1.0 = one attack's worth per target) + optional DamageTypeTag override; inherited DamageTypes map intentionally unused

When to Extend: Future stack-builder / instant-detonation Echo Mods reuse this builder↔release pairing.

UOnDamagedAbility

Purpose: Retaliation procs — fire when the wearer is hit (not when they hit). Subclass of UOnHitAbility.

Feature Implementation
Trigger Event.Combat.DamageReceived (mirror of DamageDealt, sent to the target avatar from HandleIncomingDamage). On blocked hits EventMagnitude is the pre-block damage — a full block (chip = 0) still retaliates, and DoT ticks scale off the swing received rather than the chip that landed (commit 0c52bcf5c)
Target Resolution Overrides ResolveAppliedToASC() to return the attacker's ASC — the proc lands on whoever struck the wearer (ignores bApplyToSelf); stack/tag checks also route to the attacker
Per-Hit Conditions ShouldProc enforces RequiredHitTags (HasAll) against the attacker's InstigatorTags — e.g. Event.Hit.Blocked = "retaliate only on blocked hits" (the on-block Bleed channel)
Loop Prevention Shares the Effect.Source.OnHitProc guard so retaliation can't chain

Blueprint examples: GA_OnDamaged_EmberFeedback — 25% chance to Ignite the attacker when struck. GA_OnBlock_Bleed (of Reprisal) — guaranteed Bleed on the attacker when their hit is blocked (RequiredHitTags = Event.Hit.Blocked, BaseChance = 1.0, ChanceAttribute cleared). Set the Event.Combat.DamageReceived trigger in Class Defaults.

UStatusCascadeAbility

Purpose: Apply a status, then burst all stacks when a threshold is reached. Subclass of UOnHitAbility.

Feature Implementation
Stack Read Overrides OnEffectApplied() to read the target's aggregated stack count after the status applies
Cascade At CascadeThreshold, strips the matching status GEs first, then deals burst damage scaled by the consumed stack count (BurstDamageEffectClass, which must carry the OnHitProc tag)
Config CascadeThreshold, CascadeDamageMultiplier, CascadeDamageTypeTag, BurstDamageEffectClass

Blueprint example: GA_OnHit_HemorrhageCascade — hits apply Bleed; at 5 stacks all burst as Physical.

UOnKillAbility

Purpose: Kill-triggered effects. Direct subclass of UEternalAbility (no on-hit lineage). Server-only, instanced per actor. Carries two independent effect channels — either may be empty, but a proc with both empty is dead and the ability validator errors on it.

Channel Field Gate Behaviour
Unconditional OnKillEffectClasses (TArray<TSubclassOf<UGameplayEffect>>) None — fires on every credited kill Applies each GE to the wearer. Stacking / duration-refresh / per-stack magnitude policy lives in the GE, not the ability
Health-gated heal HealEffectClass + HealPercentOfMaxHealth Health / MaxHealth ≤ LowHealthThreshold (1.0 = unconditional) — the threshold now gates only the heal Applies HealEffectClass with SetByCaller.Heal = MaxHealth × HealPercentOfMaxHealth / 100
Feature Implementation
Trigger Event.Combat.KillCredited (broadcast to every credited contributor's pawn) — set on the Blueprint Class Defaults, never the C++ constructor

Blueprint examples: - GA_OnKill_VitalSurgeHealEffectClass only; kills while ≤35% Health restore 10% MaxHealth. - GA_KillingFervorOnKillEffectClasses only; every kill applies a stacking power buff (GE_KillingFervor_Power) paired with a drain (GE_KillingFervor_Drain), the exemplar of the unconditional channel where the stacking policy lives entirely in the GEs.

UConsumableAbility

Purpose: Consumable items (potions, food, scrolls).

Feature Implementation
Usage Tracking Queries FConsumableFragment from equipped item
Display Text Implements ISkillSlotDisplayInfo for "3/5" style UI
Effect Application ApplyConsumeEffect() with level-scaled magnitudes

Movement Abilities

Ability Purpose Key Feature
UDodgeAbility Invincibility frames + repositioning RotateDuringDodge() for directional control
USprintAbility Movement speed state GMS integration, restores previous state on end
UHitReactAbility Stagger response to damage Direction-based montage selection

Enemy Abilities

UEnemyAbility

Purpose: Base class for all enemy damage-dealing abilities with data-driven damage configuration.

Difference from Player Implementation
No Stamina Enemies have no stamina cost
AI Controller GetEternalAIController() for behavior tree access
Montage Source Enemy-specific data tables
Attack Speed No attribute-based scaling
Damage Config FEnemyAbilityDamageConfig with multiplier + element ratios

Damage Calculation: BaseDamage × DamageMultiplier × ElementRatio where BaseDamage comes from UEternalAttributeSet (scaled by UBalanceSubsystem).

UEnemyProjectileAbility

Purpose: Extends UEnemyAbility with projectile spawning and management.

Feature Implementation
Spawn Config Socket-based or offset-based spawn transform
Patterns UProjectilePatternDataAsset for spread/burst patterns
Homing Supports Homing and DelayedHoming behaviors
Tracking SpawnedProjectiles / HoveringProjectiles arrays for cleanup
Impact Effects Projectile carries WeaponTypeTag for CombatEffectsManager resolution

UEnemyRangedAbility

Purpose: Aim-hold telegraph with optional burst support. Uses a 4-section montage system: Draw → AimHold (looping) → Shoot → Recovery.

Feature Implementation
Aim Hold FAimHoldConfig with min/max duration and optional distance scaling
Burst Fire MinShotsPerBurst / MaxShotsPerBurst loops back to AimHold between shots
Montage Sections Configurable section names for Draw, AimHold, Shoot, Recovery
Projectile Spawn AnimNotify_SpawnProjectile fires during Shoot section

UEnemyAoeAbility

Purpose: Enemy AOE attacks. Inherits from UAOEDamageAbility (not UEnemyAbility) but mirrors the same enemy patterns (AI controller access, enemy data tables).


Interfaces

ISkillSlotDisplayInfo

Purpose: Dynamic text for skill slots (e.g., ammo counts, charges).

Method Returns
GetSkillSlotDisplayText(ActorInfo) FText shown on skill slot

Implementers: UConsumableAbility, any ability needing dynamic UI text.


Choosing the Right Base Class

Need to deal damage?
    |
    +-- No --> Is it a passive on-hit proc?
    |              |
    |              +-- Yes --> Does it accumulate stacks on the wearer then release?
    |              |              |
    |              |              +-- Yes --> UPressureBuilderAbility (+ paired UPressureReleaseAbility)
    |              |              +-- No  --> UOnHitAbility
    |              +-- No  --> Does it persist while active?
    |                            |
    |                            +-- Yes --> UAuraAbility
    |                            +-- No  --> UEternalAbility
    |
    +-- Yes --> Is it player-controlled?
                   |
                   +-- No  --> Is it a projectile attack?
                   |              |
                   |              +-- Yes --> Does it have an aim-hold phase?
                   |              |              |
                   |              |              +-- Yes --> UEnemyRangedAbility
                   |              |              +-- No  --> UEnemyProjectileAbility
                   |              |
                   |              +-- No  --> Is it an AOE attack?
                   |                            |
                   |                            +-- Yes --> UEnemyAoeAbility
                   |                            +-- No  --> UEnemyAbility
                   |
                   +-- Yes --> Does it use weapon stats?
                                  |
                                  +-- No  --> Is it a body-originated AOE (own DamageTypes, no weapon)?
                                  |              |
                                  |              +-- Yes --> Does it launch and resolve at a landing point?
                                  |              |              |
                                  |              |              +-- Yes --> ULeapAbility
                                  |              |              +-- No  --> UAOESkillAbility
                                  |              +-- No  --> UDamageAbility
                                  +-- Yes --> Is it a combo/charge attack?
                                                 |
                                                 +-- Yes --> UMeleeAbility
                                                 +-- No  --> USkillAbility

Source Reference

Class Location
UEternalAbility Source/ProjectEternal/Public/Abilities/EternalAbility.h
UDamageAbility Source/ProjectEternal/Public/Abilities/DamageAbility.h
UPlayerCombatAbility Source/ProjectEternal/Public/Abilities/PlayerCombatAbility.h
UMeleeAbility Source/ProjectEternal/Public/Abilities/MeleeAbility.h
UChargeWindupAbility Source/ProjectEternal/Public/Abilities/ChargeWindupAbility.h
FAttackIntentSnapshot Source/ProjectEternal/Public/Combat/Types/AttackIntentTargetData.h
USkillAbility Source/ProjectEternal/Public/Abilities/SkillAbility.h
UAOEDamageAbility Source/ProjectEternal/Public/Abilities/AOE/AOEDamageAbility.h
UAOESkillAbility Source/ProjectEternal/Public/Abilities/AOE/AOESkillAbility.h
ULeapAbility Source/ProjectEternal/Public/Abilities/LeapAbility.h
FAOEStatics Source/ProjectEternal/Public/Abilities/AOE/AOEStatics.h
UProjectileAbility Source/ProjectEternal/Public/Abilities/ProjectileAbility.h
UAuraAbility Source/ProjectEternal/Public/Abilities/AuraAbility.h
UOnHitAbility Source/ProjectEternal/Public/Abilities/OnHitAbility.h
UPressureBuilderAbility Source/ProjectEternal/Public/Abilities/PressureBuilderAbility.h
UPressureReleaseAbility Source/ProjectEternal/Public/Abilities/AOE/PressureReleaseAbility.h
UOnDamagedAbility Source/ProjectEternal/Public/Abilities/OnDamagedAbility.h
UStatusCascadeAbility Source/ProjectEternal/Public/Abilities/StatusCascadeAbility.h
UOnKillAbility Source/ProjectEternal/Public/Abilities/OnKillAbility.h
AEternalSphereAOEActor Source/ProjectEternal/Public/Actor/AOE/EternalSphereAOEActor.h
UConsumableAbility Source/ProjectEternal/Public/Abilities/ConsumableAbility.h
UDodgeAbility Source/ProjectEternal/Public/Abilities/DodgeAbility.h
USprintAbility Source/ProjectEternal/Public/Abilities/SprintAbility.h
UHitReactAbility Source/ProjectEternal/Public/Abilities/HitReactAbility.h
UEnemyAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemyAbility.h
UEnemyProjectileAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemyProjectileAbility.h
UEnemyRangedAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemyRangedAbility.h
UEnemyAoeAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemyAoeAbility.h
ISkillSlotDisplayInfo Source/ProjectEternal/Public/Abilities/Interfaces/ISkillSlotDisplayInfo.h
Attack power primitive Source/ProjectEternal/Public/Combat/Components/PlayerCombatComponent.h -> CalculateBaseAttackDamage()


Refactoring Considerations

Issue Current State Recommendation
Weapon Access Duplication GetMainHandWeapon() / GetOffHandWeapon() still live on UDamageAbility Extract to a shared weapon-access helper (never built — proposed)
Montage Logic Variance PlayMontage() differs slightly per type Template method: SelectMontage() + CalculatePlayRate()
Hit Event Signatures Different handling across abilities Standardize on a shared hit-context struct (never built — proposed)
Raw Timers Some abilities use timers instead of tasks Convert to UAbilityTask subclasses

Recent Changes

Date Change Impact
2026-08-06 UMeleeAbility documented as payload-driven (intent → claim validation → AdoptComboStep → stamina from the payload step → rotation/warp from the payload aim → montage); IsHeavyAttack() and its class-default rule documented; UChargeWindupAbility added to the catalogue and the montage-route table corrected. Removes the stale "reads local combo/charge state" description. Two traps are now written down: reading attack type from the live spec makes a kit-remapped heavy price as light, and reading local charge state on the owning client desyncs the montage choice (the replica is ~RTT stale)
2026-07-08 Player montage pipeline collapsed to SkillMontages; ActionTag moved down to the enemy bases UPlayerCombatAbility::ResolveSkillMontage() no longer falls back to the weapon table, and ActionTag/PlayMontage() left UDamageAbility for UEnemyAbility and UEnemyAoeAbility (different branches, so each declares it). GA_RisingMace — the one real user, granted by Mace — migrated to SkillMontages. CheckMontageWiring restructured around bHasSkillMontage, UMeleeAbility exempted explicitly (it animates from combo configs), and a missing enemy ActionTag property is now an error so the check cannot go dead in silence. Dead Event.Montage.Attack.Skill.* rows remain in DT_Mace/DT_Sword_Right/DT_Sword_Left/DT_Shield/DT_Hammer — unreachable, pending manual removal
2026-07-08 ActionTag dropped from every archetype's RequiredTagSlots/TagNamespaces; player templates blanked The wizard no longer derives Action.Attack.<Name> — a namespace only enemy abilities use, so the derived tag matched no montage row and registered dead tags into DefaultGameplayTags.ini. ActionTag remained an optional weapon-table key at the time; it was removed from the player branch entirely later the same day (row above). TPL_Skill no longer ships GA_Slash's montages, which every stamped weapon skill silently inherited
2026-07-21 Universal skill cooldowns hoisted to USkillAbility (07894a447): the four GAS cooldown virtuals plus CooldownDuration / CooldownTag / FindActiveCooldown / GetCooldownDurationAtLevel moved from UAOESkillAbility onto the shared base Every slotted activatable skill now reports remaining/total through the standard GAS API, which is what the HUD skillbar's cooldown sweep binds to; authoring a cooldown is two properties on any skill instead of an AOE-only feature
2026-07-08 Montage resolution documented; ability-owned montage made canonical for skills SkillMontages (already shipped, previously undocumented) is the source of a skill's animation and short-circuits the weapon-table ActionTag lookup. PlaySkillMontage() hoisted from USkillAbility/UProjectileAbility into UPlayerCombatAbility. CheckMontageWiring now accepts either route instead of warning on every montage-owning ability with no ActionTag
2026-07-07 Body-originated player AOE skills (UAOESkillAbility) Player AOE that skips the weapon-fragment gate (chains to UPlayerCombatAbility::ActivateAbility) and scales off its own DamageTypes curves; dual Resonance/Stamina cost via shared SetByCaller GEs with no direct-set fallback; one shared cooldown GE keyed by a per-ability CooldownTag; AOE spawn server-only, normally montage-notify-driven
2026-07-07 Leap skill (ULeapAbility, Crash Landing) UAOESkillAbility child that launches on a ballistic arc (LeapDistance per rank + LeapApexHeight feel constant) and fires its AOE at the landing point via LandedDelegate; 5s timeout + idempotent ClearLeapState; authority contract — impact spawns server-only and only the server owns the replicated EndAbility so a predicted client landing can't preempt the server
2026-07-07 Shared AOE spawn helpers extracted (FAOEStatics) SpawnAOEActor (server-only) + CalculateSpawnTransform pulled out of UAOEDamageAbility and shared with the player skill branch; caller binds OnAOEHit then calls Initialize()
2026-07-07 UOnKillAbility gains the unconditional OnKillEffectClasses channel Per-kill effects that fire regardless of health (stacking policy lives in the GE) alongside the now-heal-only health-gated HealEffectClass; either channel may be empty, both-empty errors in the validator; GA_KillingFervor is the exemplar
2026-07-02 On-crit / on-block proc affixes (RequiredHitTags) UOnHitAbility gains RequiredHitTags (HasAll against the hit's Event.Hit.* tags); UOnDamagedAbility::ShouldProc enforces the same gate. Guaranteed-proc recipe = BaseChance = 1.0 AND ChanceAttribute cleared — a set ChanceAttribute overrides BaseChance, so a duplicated BP silently degrades to gear chance (learning chanceattribute-overrides-basechance). New BPs GA_OnHit_IgniteOnCrit (of Searing Criticals) + GA_OnBlock_Bleed (of Reprisal)
2026-07-02 Blocked-hit pre-block mirror (commit 0c52bcf5c) The Event.Combat.DamageReceived mirror carries pre-block EventMagnitude on blocked hits, so UOnDamagedAbility retaliation fires on full blocks (chip = 0) and scales off the swing received; attacker-side DamageDealt events are unchanged
2026-06-17 UHitReactAbility scope narrowed (commit 4df2921ab) Knockback (ApplyKnockback + severity helpers) removed from the ability and moved to UCombatComponent; the ability is now directional flinch only, blocked by State.HyperArmor (ActivationBlockedTags)
2026-06-17 Status-duration affix wiring (commit 4977aa7e2) The 4 ailment GAs (Bleed/Ignite/Poison/Shock) read the new IncreasedStatusDuration attribute (the "Lingering" affix) through UOnHitAbility::DurationMultiplierAttribute; final = BaseDuration × (1 + attr/100), applied source-side
2026-06-11 Remnant Echo proc ability bases (pool fill) UOnDamagedAbility (retaliation, Event.Combat.DamageReceived, applies to attacker via ResolveAppliedToASC), UStatusCascadeAbility (status threshold burst via OnEffectApplied), UOnKillAbility (kill heal, Event.Combat.KillCredited, health-gated); UPressureBuilderAbility gains configurable ReleaseEventTag for Static Charge
2026-04-23 Echo Mod tooltip detail pipeline + Pressure stack-limit consolidation UEternalAbility::GetTooltipDetailText() lets abilities describe their own mechanics under Alt-hold; GetStackLimit() reads the GE CDO StackLimitCount so trigger/token/tooltip share one number
2026-04-21 Pressure Release Echo Mod (builder/release ability pair + sphere AOE) New UPressureBuilderAbility (self-targeting stack accumulator) and UPressureReleaseAbility (instant-spawn AoE scaling off wearer attack power); UOnHitAbility gains bApplyToSelf + OnEffectApplied hook; first radial AEternalSphereAOEActor; status controllers read Spec.GetStackCount() so native single-handle stacks render correctly
2026-03 UPlayerCombatAbility cancels block/crouch Combat abilities cancel State.Blocking and State.Crouching on activation
2026-03 Added UOnHitAbility Passive proc system: bleed, ignite, poison, shock. Granted by items/enemies/passives
2026-02 Added UEnemyRangedAbility Aim-hold telegraph + burst fire for ranged enemies
2026-02 Added UEnemyProjectileAbility Projectile spawning, patterns, impact effects via CombatEffectsManager
2024-Q4 Added UPlayerCombatAbility Consolidated weapon calculations between melee and skills
2024-Q4 ISkillSlotDisplayInfo Consumables now show remaining uses on skillbar
2024-Q3 USkillAbility New class for non-combo weapon skills