Concept: Augment System

Augments answer "how do I make THIS instance of this ability different" without duplicating the asset or branching inside it.

Scope

An augment attaches to one FGameplayAbilitySpecHandle. Two characters with the same ability granted can carry entirely different augments; the ability asset never changes.

The three parts

PartWhen it runsHolds state?
MutationOn property readNo - it transforms a value.
ActionOn an event tagNo - one shot.
Stateful listenerOn an event tagYes - accumulates across events.

Events are open-ended

The set of events is not fixed in C++. Any gameplay tag under Augment.Event is an event - declare it, broadcast it with BroadcastAugmentEvent, and it appears in the event picker with no registration step. The plugin ships only the events it fires itself (OnGranted, OnRemoved, OnActivate, OnEnd, OnEffectApplying); anything your game knows about, such as a crit or a kill, is yours to declare and broadcast. See How-To: Add Your Own Augment Event.

Slots

Slots are typed by Augment.Category.* tag and split into two pools. Fixed slots are the ability's declared capacity. Temp slots hold transient grants - a combo transition's reward is equipped into a temp slot and unequipped when the step ends.

Lifetimes

LifetimeRemoved when
UntilStepEndsThe current combo step exits.
UntilComboEndsThe combo finishes or resets.
ForDurationSecondsA timer expires.
ForUseCountIt has been used N times.
PermanentExplicitly removed.
Stacking scales potency
With Stack Mode = Potency, re-granting an already-equipped augment increments its stack count and the effective level becomes BaseLevel + (StackCount - 1). That is the mechanism behind "damage grows as the combo continues" - no condition required.

Ability-scoped attribute modifiers

Kind = Attribute is the one mutation that does not touch the contract. It names a real FGameplayAttribute, picked from the engine's own attribute picker - which lists every `UAttributeSet` in the project, so Crimson's sets, your game's, and any other plugin's all appear with no registration step.

It does not change the character's attribute
It contributes an additive and a multiplicative adjustment scoped to one ability's spec handle - "+15% crit chance for this ability", not "+15% crit chance". That scoping is the whole point: a plain GameplayEffect modifier on the attribute would change crit for everything the character does.

Two writers feed it: augment Attribute mutations (applied on equip, reverted on unequip) and UCrimsonModifierGameplayEffectComponent with ModifierTargetType = AbilitySpecific (for the parent effect's lifetime). Both land in replicated state and flatten into a per-handle cache.

Reading them is the consumer's job and belongs in a GameplayEffect execution. Because the key is a real attribute, it matches a capture definition directly - there is no tag-to-attribute lookup table, and none is needed.

cpp
float Value = 0.f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(CritChanceDef, EvalParams, Value);
float Add = 0.f, Mul = 0.f;
if (SourceASC->GetAbilitySpecificModifiers(AbilityHandle, CritChanceDef.AttributeToCapture, Add, Mul))
{
Value += Add;
if (Mul != 0.f) Value *= (1.f + Mul);
}
ConstraintWhy
TargetAttribute must be setAn unset picker is a no-op; validation catches it at author time.
Additive and Multiplicative onlyThe modifier cache holds one add and one multiply per attribute - there is no override slot. Use a Property mutation for override semantics.
The magnitude cannot be boundThe modifier is removed by matching value on unequip; a magnitude that resolved differently between equip and unequip would strand it permanently.
Which attributes actually read them
UCrimsonDamageExecution folds these into the four Source-captured attributes - Attack Power, Crit Chance, Crit Multiplier and Armor Penetration. Target-side captures (armor, block, resistances) deliberately do not: the handle identifies the attacker's ability, so applying it to the victim's stats would be wrong. The spec must be built through the ability - MakeAugmentedGameplayEffectSpec does this - or the execution cannot tell which ability the effect came from.

Binding a value to a property

Every number a designer types on an action, listener, gate or mutation is a FCrimsonAugmentBindableFloat. By default it is just a literal and behaves exactly like the plain value it replaced - binding is opt-in, and no contract property has to exist. Set its Source Property and it reads an ability property instead, which is how one augment changes another's numbers.

text
Contract publishes: Ability.Property.ChainTargets (Integer, base 1)
Augment A: FindTargets.MaxTargets -> bound to ChainTargets (chains to 1)
Augment B: Mutation: ChainTargets, Additive +2 (A now chains to 3)

Resolution order, first hit wins: the event group's blackboard (a value an earlier action in the same group wrote - stateful listeners have none and skip this), then the ability's property cache, then the literal. The literal is also the fallback: a binding that resolves to nothing yields it rather than zero, so unequipping the augment that declared a property leaves everything still working.

Bound magnitudes read pre-mutation values
A mutation whose own magnitude is bound is order-dependent, and augments have no ordering beyond Priority. The property cache is therefore evaluated twice - once with literal magnitudes only, then again with bound ones resolving against that first result. Bound magnitudes always see the value before any mutation, so chains cannot compound and the answer does not change when the player reorders their loadout.
Two bindings are rejected
A mutation cannot bind its magnitude to the property it mutates - the two-phase pass would hand it the value it is about to replace. An Attribute-kind mutation cannot bind its magnitude at all: attribute mutations are removed by matching value on unequip, so a magnitude that resolves differently between equip and unequip would strand the modifier permanently. Both are IsDataValid errors.
Level changes do not refresh bound values
RecalculateProperties runs on grant, on augment equip/unequip and on replication receive - not when the ability's level changes. Bound values and tooltip stats bake the level they were last recalculated at.

Tooltips

An augment describes itself to the player through a generated description you can override per step, with {Token} values that stay live. That has its own page - see How-To: Write Augment Tooltips.

See also

  • How-To: Create and Equip an Augment
  • How-To: Write a Stateful Augment