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                  v                  v
+---------------+ +---------------+ +---------------+ +---------------+
| UEnemyAoe     | | UEnemyProject | | UEnemyCombo   | | UEnemySelf    |
| Ability*      | | ileAbility    | | Ability       | | BuffAbility   |
+---------------+ +---------------+ | (multi-step   | | (montage +    |
                        |           |  string)      | |  self GE)     |
                        v           +---------------+ +---------------+
                  +---------------+         |
                  | UEnemyRanged  |         v
                  | Ability       | +---------------+
                  +---------------+ | UBossCombo    |  Empowered final
                                    | Ability       |  step (finisher cone)
                                    +---------------+

* 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 (the enemy combo-string archetype is a per-string variant of the second). 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 (ActionTag → Montages[]) 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
Tag-keyed table, per string (EMontageRoute::EnemyComboStrings) The same enemy table, but the key is each FEnemyComboString::ActionTag in the CDO's Strings array and the row's Montages[] is read by step index (Montages[0] = step 0, …). The base ActionTag is deliberately unused UEnemyComboAbility / UBossComboAbility — see UEnemyComboAbility

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
AOE-notify claim Virtual TrySpawnNotifyAOE() — AnimNotify_SpawnAOE walks the owner's active abilities and stops at the first that returns true. The base declines (no AOE concept), so the notify keeps searching; only the branches that own AOE spawning answer: UPlayerCombatAbility claims the whole player branch (spawns when HasSkillAOE(), returns true regardless so a skill montage's notify always belongs to the skill running it), UBossComboAbility claims its finisher cone

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. On gamepad the aim point the resolver classifies is a synthetic world point built from the aim stick (ComputeGamepadAimOffset), not a cursor — see Gamepad aim + sticky lock.

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, which reaches the ability through the TrySpawnNotifyAOE() claim on UPlayerCombatAbility (→ SpawnSkillAOE()); 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 RequiredHitTags — Event.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)
Release Multiplier GetReleaseDamageMultiplier() reads the release ability's CDO (ReleaseAbilityClass → its DamageMultiplier), so the tooltip token and the actual burst can never disagree; returns 0 when unset/miswired

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_VitalSurge — HealEffectClass only; kills while ≤35% Health restore 10% MaxHealth. - GA_KillingFervor — OnKillEffectClasses only; every kill applies the stacking GE_KillingFervor_Power + GE_KillingFervor_Conversion (asset tag Status.Fervor), the exemplar of the unconditional channel where the stacking policy lives entirely in the GEs. The stacks are permanent (no drain GE); the price is paid per swing instead — GE_HealthAttackCost is seated on every attack template (TPL_Combat / TPL_MeleeAttack / TPL_Skill / TPL_AOESkill / TPL_Projectile / TPL_Leap) so attacks spend health while stacked, and the conversion GE returns spent health as bonus damage. Stacks erode on direct hits taken via GA_FervorShed, a UStackShedAbility (below).

UStackShedAbility

Purpose: Strip stacks of a status GE from the wearer when a trigger event fires — the pressure half of a permanent-stack buff. Direct subclass of UEternalAbility.

Feature Implementation
Trigger Authored on the Blueprint Class Defaults (e.g. Event.Combat.DamageReceived, which only fires for non-terminal damage with a source actor — so DoT ticks and self-paid health attack costs never shed)
Target ShedStatusTag matched against effect asset tags (e.g. Status.Fervor)
Amount StacksRemovedPerTrigger — graduated loss (1–2) rather than a full clear is deliberate for on-hit sheds: slow committed melee trades hits by design, so one routine hit erodes, not erases, the buff. 0 strips the whole effect
CDO facts GetShedStatusTag() / GetStacksRemovedPerTrigger() for tooltip token resolution

Blueprint example: GA_FervorShed (Killing Fervor).

UConsumableAbility

Purpose: Consumable items (potions, food, scrolls). The whole consume flow lives in C++ — montage, notify event, apply, end — and the Blueprints are data-only, because every termination path must be code-owned (montage pins in a BP graph are not).

Feature Implementation
Usage Tracking Queries FConsumableFragment from the item in EquipmentSlotTag (unset = the flask slot)
Display Text Implements ISkillSlotDisplayInfo for "3/5" style UI
Flow ActivateAbility plays the montage-table row for ResolveConsumeTag() (ConsumeEffectTag, falling back to Action.Buff.Heal — the shared drink/eat row), waits for the same tag as a gameplay event (OnConsumeEventReceived → ApplyConsumeEffect()), and ends on OnConsumeMontageCompleted / OnConsumeMontageAborted
Effect Application ApplyConsumeEffect() with level-scaled magnitudes; optional SelfStatusEffectClass (+ duration / damage-type / DPS) via ApplySelfStatusEffect() for self-inflicted statuses such as the mushroom's poison — the spec is stamped terminal so a swallowed poison's ticks never proc
State.Consuming Added through ActivationOwnedTags, so GAS drops it on every termination path. The tag drives the gait clamp and weapon concealment while drinking — there is no snapshot/restore of gait in the ability
Gates CanActivateAbility refuses while State.Attacking or State.Consuming is present, or while an ALS locomotion action (roll, mantle, ragdoll) owns the body. Checked in code rather than ActivationBlockedTags because BP-serialized tag containers override constructor additions. Drink/dodge are mutually exclusive in both directions: UDodgeAbility::CanActivateAbility refuses while State.Consuming is held

Movement Abilities

Ability Purpose Key Feature
UDodgeAbility Invincibility frames + repositioning Directional control from the move input; CanActivateAbility refuses while State.Consuming (no dodge-cancelling a drink)
USprintAbility Movement speed state Sets SprintMovementStateTag (ALS gait) while held; always returns to DefaultMovementStateTag on end — there is no previous-state restore
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).

UEnemyComboAbility

Purpose: Multi-step enemy attack string — one activation owns the whole string. Each step is its own montage; at every step's blend-out the server re-decides whether to continue toward the blackboard target or bail and end. The AI-side seams (attack range, avoidance suspension) live on AEternalAIController — see Enemy Combos (AI side).

Activate -> SelectStringIndex (target's range band; weights break ties) -> PlayStep(0)
                                                                              |
           +------------------------------------------------------------------+
           |
           v
   step montage blends out (OnMontageCompleted)
           |
           +-- ShouldContinueString(target, AttackRange x ContinueRangeFactor, ContinueArcDegrees)
           |        yes -> PlayStep(n+1)  (starts INSIDE the outgoing blend; bBoundaryHandled marks
           |                              the resulting interrupt as expected)
           |        no  -> EndCombo
           +-- Effects.HitReact / Staggered / Broken appears on owner -> cancel
Feature Implementation
Data Strings : TArray<FEnemyComboString> — each string carries its own montage-table ActionTag (Action.Attack.Combo.*, set in Blueprint), StepParams, SelectionWeight, a range band (MinRangeFraction/MaxRangeFraction of AttackRange — a gap-closer owns the outer band, an in-close string the inner one, so selection is a consequence of spacing the player can read), and the boundary knobs ContinueRangeFactor (1.2) / ContinueArcDegrees (90)
Per-step tuning FEnemyComboStepParams: DamageMultiplier (on top of DamageConfig), bHyperArmor (grants State.HyperArmor for the step — the sanctioned per-step commitment tool), EntryTimeOffset (skip wind-up frames the previous step's motion already covered; ignored for step 0). Indices past the authored array clamp to the last entry (ResolveStepParams)
Montages GetStepMontage(StringActionTag, StepIndex) indexes the row's Montages[] by step — the string length is the row's array length (EMontageRoute::EnemyComboStrings)
Boundary ShouldContinueString (pure static, spec-tested): target present, within AttackRange × ContinueRangeFactor (GetBoundaryAttackRange() reads the controller's cached enemy data asset), inside the arc. Losing the target mid-step finishes the swing (committed weight) and bails at the boundary
Interruption OnMontageInterrupted is expected when the string advanced (bBoundaryHandled); otherwise external displacement ends the combo. Effects.HitReact / Effects.Staggered / Effects.Broken on the owner cancel the ability — interruption authority is tag-driven
Budget / avoidance The attack-budget claim taken by BTTask_ActivateAbility spans the activation (whole string, no per-step churn); crowd avoidance is suspended for the activation so separation cannot steer a committed string off-line
Damage GetEffectiveDamageMultiplier() folds the step's DamageMultiplier into the enemy damage path

When to Extend: Any enemy whose attack is a readable multi-hit string rather than a single swing. StartStepMontageTask / IsStepMontagePlaying / GetBlackboardTarget / GetBoundaryAttackRange are virtual only for headless test rigs.

UBossComboAbility

Purpose: A combo string whose final step becomes an empowered finisher while the owner carries a buff tag. The montage is identical either way — only the cone AOE, its cue and the buff consumption differ, so the empowered swing needs no second animation set and adds no combo-system surface.

Feature Implementation
Arming FinisherRequiredTag on the owner + the running string is FinisherStringTag + it is that string's last step (IsFinisherArmed()). Without the buff this is the base class, step for step
Spawn The finisher fires at the montage's AOE notify (TrySpawnNotifyAOE() override → SpawnFinisherAOE() with FinisherShape / FinisherAOEActorClass), so the burst lands with the blow rather than at the step's start or end. One cone per activation (bFinisherFired) however often the notify fires
Consumption ConsumeFinisherBuff() strips the GE matching FinisherEffectRemovalQueryTag (defaults to FinisherRequiredTag) before the cone dispatches damage — a reaction re-reading the owner's tags must not see a still-standing buff and fire a second finisher
Damage FinisherDamageMultiplier applies to the cone's hits only (bScoringFinisherDamage); the swing's ordinary hit window still pays base step damage, so the finisher adds a blast rather than inflating the swing. FinisherCueTag is the once-at-owner burst cue; per-target impact cues come from the ability's own CueTag

UEnemySelfBuffAbility

Purpose: Play a montage and put a buff GE on the owner. The GE carries the granted tag and the status cue, so visuals attach when the buff lands; the buff deliberately outlives the ability — a standing state consumed elsewhere by tag query (e.g. the boss finisher above), not a stance held for the montage's duration.

Feature Implementation
Config BuffEffectClass + BlockedByTag (normally the buff's own granted tag), set in Blueprint alongside ActionTag
Gate CanActivateAbility refuses while the owner already carries BlockedByTag — re-casting a standing buff spends a whole montage to change nothing and reads as the boss stalling
Interrupt OnMontageInterrupted override ends the ability; the buff is not removed. GetBuffHandle() is diagnostic only — removal is by effect-tag query at the consuming site

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  --> Is it a multi-step attack string?
                   |                                          |
                   |                                          +-- Yes --> Empowered final step? UBossComboAbility : UEnemyComboAbility
                   |                                          +-- No  --> Buffs itself? UEnemySelfBuffAbility : 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
UEnemyComboAbility / FEnemyComboString / FEnemyComboStepParams Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemyComboAbility.h
UBossComboAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/BossComboAbility.h
UEnemySelfBuffAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemySelfBuffAbility.h
UStackShedAbility Source/ProjectEternal/Public/Abilities/StackShedAbility.h
EMontageRoute Source/ProjectEternal/Public/AbilitySystem/Data/AbilityArchetypeCatalog.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: ResolveSkillMontage() + PlayMontage()
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-20 Doc reconciled with code: enemy combo archetype added (UEnemyComboAbility + FEnemyComboString/FEnemyComboStepParams, UBossComboAbility finisher, UEnemySelfBuffAbility, EMontageRoute::EnemyComboStrings route row); AOE notify dispatch documented as the TrySpawnNotifyAOE() claim walk; Killing Fervor corrected to permanent stacks + GE_HealthAttackCost blood price + GA_FervorShed (UStackShedAbility); UConsumableAbility described as the C++-owned flow with State.Consuming (gait-snapshot story removed) and the drink/dodge exclusion; USprintAbility always returns to DefaultMovementStateTag; UPressureBuilderAbility::GetReleaseDamageMultiplier(); gamepad synthetic aim point noted on the intent payload Removes four stale descriptions (sprint restore, GE_KillingFervor_Drain, notify → SpawnSkillAOE direct call, consumable gait snapshot) and gives the enemy-combo and boss-finisher work a home in the class catalogue
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