# Gameplay Ability System (GAS) Layout

V4 is one game that has to feel like six different ones. A tactical-FPS
operator, a social-stealth assassin, a real-time-tactics specialist, a
Wukong-class action- RPG hero, an RTS unit-commander, and a 2D run-and-gun
arcade pilot all run on the **same actor and the same Ability System Component**
— they differ only in which stat block, which verbs, and which effects are
mounted at spawn. That is the job of V4's GAS layer: give every "cell" a clean,
mutually exclusive slice of Unreal's Gameplay Ability System while sharing one
base for attributes, one base for abilities, one base for effects, and one
gameplay-tag namespace. Unlike V2's combat core, this layer is **not**
rollback-deterministic or frame-data-driven; it is a conventional, replicated,
server-authoritative GAS stack whose value is in how cleanly it partitions six
rulesets behind shared machinery. This page is the architecture-side companion
for V4's GAS, part of the **Combat, Character & Input Systems** group; the
section hub is [../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md).

The source of truth is Unreal C++ under `V4/ue/Source/V4Gameplay` (the shared
base), the per-cell ability modules (`V4Tactical`, `V4Stealth`, `V4Tactics`,
`V4ActionRPG`, `V4RTS`, `V4Arcade`), and the native tag table at
`V4/ue/Config/DefaultGameplayTags.ini`. There is no TypeScript contract package
for any of it.

## What ships, honestly

The GAS layer is **real, substantive, and tested** — not a skeleton. Six
concrete attribute sets, a thirty-two-class ability family, twenty-three
gameplay-effect classes, a resolve-and-apply library, the operator pawn that
wires it together, and five automation specs all exist as working C++ with
domain-specific clamps, armor mitigation, and periodic decay. The
`V4.Gameplay.*` automation specs spawn a real `AV4Pawn_Operator` into a
`UWorld`, switch cells, apply effects, and assert computed attribute values
(armor-mitigated health of `85.0`, suppression decaying `40 → 20 → 0` over two
seconds, RTS unit health stacking `100 → 65 → 45`) — these would fail against
placeholders.

Five honest qualifications, so the rest of the page can be read at face value:

1. **The abilities and effects are C++ classes, not authored Blueprint assets.**
   The monolith says "GameplayAbility blueprints implement the verb library"; in
   the repo the verbs are `UCLASS(Blueprintable)` C++ types (`UGA_Fire`,
   `UGA_LightAttack`, …) that tests instantiate directly via `StaticClass()`.
   They are _Blueprintable_ — designers can subclass them — but V4 ships **zero
   binary `.uasset` ability or effect assets**. The C++ classes are canonical.
2. **Effects are thin SetByCaller appliers, not MMC/ExecCalc math.** V4 has no
   `UGameplayModMagnitudeCalculation` / `UGameplayEffectExecutionCalculation`
   pairs like V2's combat layer. Every effect is a single additive modifier fed
   a `SetByCaller` magnitude. The one piece of real "execution math" — armor
   mitigation — is hand-coded in `PostGameplayEffectExecute`, not in an exec
   calc.
3. **Armor mitigation is Tactical-only.** The mitigation hook keys specifically
   on `UV4Attr_Tactical::GetHealthAttribute()`. The RTS set has a `UnitArmor`
   attribute, but nothing in the effect path consumes it yet — it is a stored
   stat, not a damage reducer.
4. **The ASC is a thin cell-tracking subclass.** `UV4AbilitySystemComponent`
   adds an `ActiveCell` field and a cell-event delegate and nothing else. There
   is no integer `SimulationFrame`, no rollback snapshot, no input-tag queue —
   V4's determinism/netcode lives in `V4Netcode`, separate from this layer.
5. **The doc under-counts.** The monolith lists six Stealth and three Tactics
   abilities; the code has **seven** and **four** (each adds a takedown
   ability). Counts below come from the headers, not the prose.

## The shared spine

Every controllable character is an `AV4Pawn_Operator`
(`V4Gameplay/Public/V4Pawn_Operator.h:13`), which implements
`IAbilitySystemInterface` and owns exactly one `UV4AbilitySystemComponent`
created as a default subobject, replicated in
`EGameplayEffectReplicationMode::Mixed` (`V4Pawn_Operator.cpp:10`). On
`BeginPlay` it calls `InitializeForCell(DefaultCell)` with
`DefaultCell = Tactical` (`V4Pawn_Operator.h:34`). That single method is the
whole cell-switching mechanism: it `NewObject`s the one attribute set for the
requested cell, hands it to the ASC via `SetSpawnedAttributes`, calls
`InitAbilityActorInfo`, and records the cell on the ASC
(`V4Pawn_Operator.cpp:27-67`). Because `ActiveAttributeSet` is a single pointer
that is overwritten on each switch, a pawn is **either** a Tactical operator
**or** an ARPG hero, never both at once — the "mutually exclusive at run-time"
contract the monolith promises is enforced by construction.

The cell identity itself is `EV4RulesetCell` (`V4Core/Public/V4CellState.h:7`):
`None, Tactical, Stealth, Tactics, ActionRPG, RTS, Arcade`. It is the join key
for the entire layer — attribute sets, abilities, and effects all carry it, and
the ASC stores the live one (`UV4AbilitySystemComponent::ActiveCell`,
`V4AbilitySystemComponent.h:31`).

```mermaid
flowchart TD
    Pawn["AV4Pawn_Operator<br/>(IAbilitySystemInterface)"]
    Pawn -->|InitializeForCell| ASC["UV4AbilitySystemComponent<br/>ActiveCell : EV4RulesetCell"]
    ASC -->|SetSpawnedAttributes| AttrPick{Cell?}
    AttrPick -->|Tactical| AT["UV4Attr_Tactical"]
    AttrPick -->|Stealth| AS["UV4Attr_Stealth"]
    AttrPick -->|Tactics| AC["UV4Attr_Tactics"]
    AttrPick -->|ActionRPG| AR["UV4Attr_ARPG"]
    AttrPick -->|RTS| AY["UV4Attr_RTS"]
    AttrPick -->|Arcade| AA["UV4Attr_Arcade"]
    ASC -->|GiveAbility| Abil["UGA_* : UV4GameplayAbilityBase<br/>RequiredCell gate"]
    Abil -->|ExecuteV4Ability| Domain["domain components<br/>(combo, cover, grenade, build, …)"]
    Abil -->|ApplyAttributeEffect| Lib["UV4GameplayEffectLibrary"]
    Lib -->|ResolveAttributeEffectClass| GE["UGE_V4_* : UV4GameplayEffectBase<br/>SetByCaller additive modifier"]
    GE -->|modifies| AT
    AT -->|PreAttributeChange / PostGameplayEffectExecute| Clamp["clamp + Tactical armor mitigation"]
```

## Attribute sets — one base, six stat blocks

All attribute sets derive from `UV4AttributeSetBase`
(`V4Gameplay/Public/V4AttributeSets.h:16`), an `Abstract` set that holds the
replicated `OwningCell`, overrides `PreAttributeChange` and
`PostGameplayEffectExecute`, and exposes a virtual `GetClampForAttribute` that
subclasses fill in. The clamp machinery is genuinely defensive: `V4Clamp`
(`V4AttributeSets.cpp:26`) treats any non-finite value as the floor before
`FMath::Clamp`, so an effect can never push an attribute to `NaN`.
`PreAttributeChange` clamps the _incoming_ value; `PostGameplayEffectExecute`
re-clamps the _resulting_ value after an instant effect, and runs the armor
mitigation pass first (`V4AttributeSets.cpp:80-94`).

The six concrete sets, with their replicated defaults (each field uses
`ReplicatedUsing = OnRep_*` with `REPNOTIFY_Always`):

| Set                        | Cell      | Attributes (default)                                                                                  |
| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------- |
| `UV4Attr_Tactical` (`:36`) | Tactical  | Health 100, Armor 0, Suppression 0, StaminaSprint 100, AmmoCurrent 30, AmmoReserve 90                 |
| `UV4Attr_Stealth` (`:92`)  | Stealth   | Health 100, Detection 0, DisguiseClass 0, Noise 0                                                     |
| `UV4Attr_Tactics` (`:134`) | Tactics   | Health 100, ActionPoints 2, Vision 1                                                                  |
| `UV4Attr_ARPG` (`:169`)    | ActionRPG | Health 100, Stamina 100, Posture 0, Will 100, Mana 50                                                 |
| `UV4Attr_RTS` (`:218`)     | RTS       | UnitHealth 100, UnitArmor 0, UnitAttack 10, UnitRange 600, UnitMovementSpeed 400, UnitSightRange 1200 |
| `UV4Attr_Arcade` (`:274`)  | Arcade    | Health 1, Lives 3, ScoreMultiplier 1                                                                  |

That is **27 replicated `FGameplayAttributeData` fields across six sets**, each
exposed through the `V4_ATTRIBUTE_ACCESSORS` macro (`:9`) that synthesizes the
standard GAS getter/setter/initter quartet. The clamps are domain-specific, not
boilerplate: Tactical Health is `0–100` but Armor is `0–200`
(`V4AttributeSets.cpp:137`), ammo is `0–999`, Tactics ActionPoints cap at `12`
and Vision at `10`, RTS unit attributes share a wide `0–100000` ceiling, and
Arcade Health caps at `9`, Lives at `99`, and ScoreMultiplier has a **floor of
1.0** (`V4AttributeSets.cpp:429`) so a score multiplier can never drop below ×1.
The `V4.Gameplay.Attributes.Defaults` spec (`AttributeSetSpec.cpp:13`) asserts
every one of these defaults and owning-cell values.

## Abilities — one gated base, thirty-two verbs

`UV4GameplayAbilityBase` (`V4Gameplay/Public/V4GameplayAbilities.h:9`) extends
`UGameplayAbility` with three things a multi-cell roster needs. First, a **cell
gate**: `RequiredCell` (`:52`) plus `IsAbilityCellCompatible`
(`V4GameplayAbilities.cpp:80`), which returns `true` for `None` but otherwise
casts the actor's ASC to `UV4AbilitySystemComponent` and checks
`GetActiveCell() == RequiredCell`. This is folded into `CanActivateAbility`
(`:12`) so a Tactical `GA_Fire` simply cannot fire on a pawn currently mounted
as an ARPG hero. Second, a **fail-loud activation seam**: `ActivateAbility`
(`:23`) checks the cell, calls `CommitAbility`, then dispatches to a protected
`ExecuteV4Ability(..., FString& OutFailureReason)`, recording
`bLastActivationSuccessful` and a human-readable `LastActivationReason`
(`"wrong-cell"`, `"commit-failed"`, or the verb's own reason) before
`EndAbility`. Third, sane **defaults**: the constructor sets
`InstancedPerActor`, `LocalPredicted` net execution, and `ReplicateYes`
(`V4GameplayAbilities.cpp:5-10`), so every cell inherits client-predicted,
replicated activation without restating it.

Each cell module subclasses that base into its verb library. The full count is
**32 concrete `UGA_*` classes**:

| Cell module   | Abilities                                                                         | Count |
| ------------- | --------------------------------------------------------------------------------- | ----- |
| `V4Tactical`  | Fire, Reload, ADS, Lean, ThrowGrenade, BreachStack, Revive, Heal                  | 8     |
| `V4Stealth`   | Crouch, BodyDrag, LightKill, StealthTakedown, StickyCam, MarkAndExecute, Disguise | 7     |
| `V4Tactics`   | PlanQueueWaypoint, FireQueued, TacticsTakedown, ShowdownToggle                    | 4     |
| `V4ActionRPG` | LightAttack, HeavyAttack, Dodge, Parry, Transform, CastCharm                      | 6     |
| `V4RTS`       | BuildStructure, TrainUnit, ResearchTech, AttackMove                               | 4     |
| `V4Arcade`    | Shoot, Jump, DropPlatform                                                         | 3     |

The verbs are not stubs: each `ExecuteV4Ability` delegates to a real domain
component on the avatar and reports honest failure. `UGA_Fire`
(`V4Tactical/Private/V4TacticalAbilities.cpp:44`) pulls the Tactical attribute
set and calls `Weapon.ConsumeRound`, failing with `"no-ammo"`; `UGA_Heal`
(`:206`) routes through the effect library and fails with `"heal-failed"`;
`UGA_LightAttack` (`V4ActionRPG/Private/V4ActionRPGAbilities.cpp:30`) advances a
`UV4HeroComboComponent`, and `UGA_Transform` (`:122`) defaults to the
`ARPG.Form.Tiger` form (300 HP, 12 s, `WillCost = 1`) and activates it through a
`UV4TransformationComponent`. The `V4.Gameplay.Abilities.ActivationClasses` spec
(`AbilityClassSpec.cpp:170`) spawns an operator per cell, attaches the needed
components, gives-and-activates each ability, and asserts the **domain** side
effect — ammo `30 → 29`, reload back to `30`, Tactical heal `50 → 85`, ARPG
combo step, dodge i-frames, RTS build/train/tech queues, arcade jump raising the
pawn. A companion `V4.Gameplay.Abilities.Classes` spec (`:124`) pins every
ability's `RequiredCell`, and `V4.Gameplay.Ability.Activation`
(`AbilitySpec.cpp:28`) verifies the base activation path end-to-end through
`TryActivateAbility`.

How input reaches these is deliberately out of scope here: named input actions
map to the `GA.Activation.Tag.*` namespace and are dispatched by the input
router. See [./input-pipeline.md](./input-pipeline.md). The montages, combo
state machines, and dodge/parry frame windows the verbs drive are in
[./animation-pipeline.md](./animation-pipeline.md), and the cell-specific
component backbones (`V4HeroComboComponent`, `V4CommandQueueComponent`, the
posture system) are detailed in
[./per-cell-deep-dives.md](./per-cell-deep-dives.md).

## Gameplay effects — a typed kind, a resolve table, a scaling applier

Effects are organized by a single enum, `EV4AttributeEffect`
(`V4Gameplay/Public/V4GameplayEffects.h:12`), with **16 kinds** — `Damage`,
`Heal`, `SuppressionApply`/`SuppressionDecay`, `StaminaCost`, `DetectionApply`,
`NoiseApply`, `ActionPointCost`, `PostureDamage`/`PostureDecay`, `ManaCost`,
`UnitDamage`/`UnitRepair`, `ArcadeDamage`, `ExtraLife`, `ScoreMultiplierBoost`.
The base `UV4GameplayEffectBase` (`:33`) carries `Cell`, `EffectKind`, and a
`MagnitudePerLevel`, and offers two configurators — `ConfigureInstantAdditive`
and `ConfigurePeriodicAdditive` (`V4GameplayEffects.cpp:41`, `:62`) — both of
which build a single additive `FGameplayModifierInfo` whose magnitude is a
`SetByCaller` keyed `"V4.GameplayEffect.Magnitude"` (`:103`). From that base
descend **23 concrete `UGE_V4_*` classes** (5 Tactical, 4 Stealth, 3 Tactics, 6
ARPG, 2 RTS, 3 Arcade), each a one-line constructor binding a cell, a kind, an
attribute, and a per-level rate — e.g. `UGE_V4_TacticalDamage` is
`(Tactical, Damage, Health, -1.0)` (`:109`) and `UGE_V4_TacticalHeal` is
`(Tactical, Heal, Health, +1.0)` (`:118`).

The runtime entry point is `UV4GameplayEffectLibrary`
(`V4GameplayEffects.h:286`). `ResolveAttributeEffectClass(Cell, Kind)` (`:320`)
is an explicit nested switch that returns the right `UGE_V4_*` class **or
`nullptr` for any combination a cell doesn't support** — so a Tactical request
for `NoiseApply` resolves to null, which the `V4.Gameplay.Attributes.Effects`
spec asserts as a rejected cross-cell effect (`AttributeSetSpec.cpp:115`).
`ApplyAttributeEffect` (`:413`) is where the one genuinely subtle design
decision lives. A bare `FScalableFloat` magnitude would be _constant_ regardless
of the requested amount; instead the library reads the class's
`MagnitudePerLevel` and sets the SetByCaller magnitude to
`MagnitudePerLevel * RequestedMagnitude` (`:451-455`), so the same `Damage`
class scales linearly with the caller's number. That comment-as-rationale
(`V4GameplayEffects.h:44-50`) is the kind of domain-specific reasoning that
distinguishes real code from a generic wrapper.

### Armor mitigation, and the decay-on-apply trick

Two pieces of behaviour are worth tracing because they are where the layer earns
its keep.

**Armor mitigation** (`V4AttributeSets.cpp:37`) runs inside
`PostGameplayEffectExecute`. After an instant `Damage` effect has already
subtracted its full magnitude from Tactical Health, the mitigation pass
recomputes `RawDamage = -Magnitude`, finds
`MitigatedDamage = max(0, RawDamage - Armor)`, and **credits the prevented
portion back** to Health. With `Armor = 10` and a `25` damage request, Health
lands at `100 − 25 + 10 = 85` — exactly what the effects spec asserts
(`AttributeSetSpec.cpp:188`). Flat armor as damage subtraction, applied through
the standard GAS execution path, with a final clamp behind it.

**Decay-on-apply** is how V4 implements "suppression decays over 2 s" and
"posture decays over 3 s" without a per-frame tick. When `ApplyAttributeEffect`
lands a `SuppressionApply` or `PostureDamage`, it measures how much the tracked
attribute actually rose, then **recursively applies the matching periodic decay
effect for exactly that measured amount** (`V4GameplayEffects.cpp:431-473`,
`V4GetStackedAttributeForDecay` at `:13`). The decay effect is a `HasDuration`
periodic modifier whose per-period rate is fractional and negative —
`-(1.0/2.0)` per 1 s over `2.01 s` for suppression (`:136`), `-(1.0/3.0)` per 1
s over `3.01 s` for posture (`:255`) — so a 40-point suppression bleeds off
`20/s` and is gone in two seconds, and a 30-point posture bleeds `10/s` over
three. The spec drives the world's timer manager directly and asserts
`40 → 20 → 0` and `30 → 20 → 10 → 0` (`AttributeSetSpec.cpp:211-265`). The
`0.01 s` duration padding exists precisely so the engine's strict
`InternalTime > ExpireTime` period check fires the expected number of times — a
real timing nuance, documented in the spec.

## How cells share and specialize

The whole layer is a deliberate split between **one shared mechanism** and **six
narrow specializations**:

- **Shared:** the operator pawn, the `UV4AbilitySystemComponent` and its
  `ActiveCell`, the abstract `UV4AttributeSetBase` (clamp + finite-guard +
  replication + armor hook), the abstract `UV4GameplayAbilityBase` (cell gate +
  fail-loud reason + predicted/replicated defaults), the abstract
  `UV4GameplayEffectBase` and the resolve-and-apply library, and one
  `GA.Activation.Tag.*` input namespace.
- **Specialized per cell:** exactly one attribute set, one `UGA_*` verb family,
  one slice of `UGE_V4_*` effects, and one gameplay-tag subtree. Adding a
  seventh cell means a new `EV4RulesetCell` value, one attribute set, a verb
  module, its effects, a `ResolveAttributeEffectClass` arm, and a tag root —
  with no change to the shared base.

The tag side mirrors this. `V4/ue/Config/DefaultGameplayTags.ini` defines **595
native tags** under `FastReplication`, including all seven cell roots
(`Tactical`, `Stealth`, `Tactics`, `RTST`, `ARPG`, `RTS`, `Arcade`), a shared
`Operator.*` roster family (48 tags), and a flat `GA.Activation.Tag.*` namespace
of **47 input-to-ability activation tags** (`GA.Activation.Tag.Fire`,
`.PlanWaypoint`, `.Parry`, `.BuildStructure`, …). The cell-specific state tags
the monolith cites are present and correctly rooted —
`Tactical.State.Stance.{Stand, Crouch,Prone}`, `Tactical.Cover.{Left,Right}`,
`Stealth.Detection.{Hidden, Suspicious,Compromised}`, `Stealth.Disguise.Waiter`,
`Tactics.Order.{Move,Attack, Sync}`, `ARPG.Combat.Parry.Window`,
`ARPG.Form.{Tiger,Wolf,Bird,Stone,Wind}`, `RTS.Faction.*`, `RTS.Tech.Tier2`, and
`Arcade.Weapon.*` / `Arcade.PowerUp.Active`.

One last connecting note for the reader: the `UV4MatchStateSubsystem`
(`V4Gameplay/Public/V4MatchStateSubsystem.h`) that lives in the same module is a
**lobby/flow** machine (`MainMenu → ModeSelect → … → MatchResult`), not a combat
state machine — it sequences match setup, not in-fight phases, and is unrelated
to the per-hit GAS path described above. V4's combat "state" is the per-cell
component state (combo, posture, cover, detection), reached through the
abilities here.

## How it connects

GAS is the seam where input, animation, and per-cell systems meet. Input actions
activate these abilities through the `GA.Activation.Tag.*` namespace
([./input-pipeline.md](./input-pipeline.md)); the abilities drive montages,
combo machines, and the dodge/parry/posture frame windows
([./animation-pipeline.md](./animation-pipeline.md)); and the cell-specific
component backbones each verb delegates to — the ARPG combo and posture systems,
the tactical cover/grenade/revive components, the RTS build/train/tech
components — are unpacked cell by cell in
[./per-cell-deep-dives.md](./per-cell-deep-dives.md). The attribute defaults and
effect rates are the numbers the balance and live-ops layers tune. For where
`V4Gameplay` and the per-cell modules sit in the overall build, and the full GAS
section in prose, return to the hub:
[../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md).

## Related

- [Animation Pipeline](./animation-pipeline.md) — AnimBlueprints, motion
  matching, and the frame windows V4 abilities drive
- [Input Pipeline](./input-pipeline.md) — EnhancedInput IMCs and the
  `GA.Activation.Tag.*` mapping that fires these abilities
- [Per-Cell Deep-Dives](./per-cell-deep-dives.md) — the domain components each
  `UGA_*` verb delegates to
- The section hub: [../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md)
