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 (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 |
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.
CooldownTagis set in the Blueprint, not the constructor —FEternalGameplayTags::Get()is empty during CDO construction. See the pitfall inCLAUDE.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 | 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) |
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 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() |
Related Systems¶
- GAS Overview - Ability System Component and attributes
- Damage Execution - Damage calculation pipeline
- Combo System - Combo chain mechanics
- Charge System - Charged attack mechanics
- Combat Animation - Montage and notify system
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 |