Ability Kits¶
Summary: An Ability Kit (
UEternalAbilitySet) is the unit of ability composition — a data asset bundling abilities (with input mapping, activation policy, and level) plus passive gameplay effects, granted to an ASC atomically and revoked in one call. Kits carry the non-weapon ability layers (crafted skills, chanting). A character's "starting kit" is just an authored kit asset. This document explains the kit data model, the server-authoritative grant path, the three-layer grant order, and the possession-race latch that grants each kit exactly once per session.
Why Kits?¶
Before kits, startup abilities were a flat per-ability list on the character class. Kits replace that with a composable unit so that:
- Composition is data, not code: a build is authored by stacking kit assets, no C++ per loadout.
- Grant/revoke is atomic: one
GiveAbilitySetgrants every ability + passive; oneRemoveAbilitySet(via the returned handle bundle) tears the whole kit down. - The same ability sits on different inputs in different builds: a kit entry can override the ability's default input slot.
- Layers stay separated by ownership: kits own crafted/chanting abilities; weapon abilities stay on equipment (
FAbilityFragment); Echo Mods stay on Remnant item modifiers. Each source grants through its own path onto the same ASC.
UEternalAbilitySet is the project's analog of Lyra's AbilitySet.
Kits are an authoring/preset construct, not a player-facing progression system. The GDD has no levels, no classes, and no chosen-abilities menu — every ability a player has is a consequence of what they wear (§2.4b.2). The four ability-layer names are gear-anchored (ruling 2026-07-09): Weapon = weapon skills on the equipment itself; Crafted = belt/neck equipables (vials, charms) with predefined slot counts; Echo = echo mods on Remnant items; Chanting = glyphwall stone-plate auras (no skill slots). A kit is how the authoring/preset pipeline bundles the Crafted + Chanting grants a given build's gear implies — the player never picks a kit.
Data Model¶
UEternalAbilitySet (UPrimaryDataAsset, PrimaryAssetType "AbilitySet")
| scanned from /Game/DataAssets/Kits
|
+-- Abilities : TArray<FEternalGrantedAbility>
| +-- Ability : TSubclassOf<UEternalAbility>
| +-- Level : int32 (default 1)
| +-- InputTagOverride : FGameplayTag (Input.*)
| +-- ActivationPolicy : OnInput | WhileInputHeld | OnSpawn
| +-- RoleTag : FGameplayTag (Kit.Role.*, authoring metadata only)
|
+-- GrantedEffects : TArray<TSubclassOf<UGameplayEffect>> (passives, infinite-duration)
FEternalGrantedAbility¶
| Field | Purpose |
|---|---|
Ability |
The ability class to grant. |
Level |
Ability level passed to the spec (ClampMin = 1). |
InputTagOverride |
When set, replaces the ability CDO's StartupInputTag for this grant only — the kit decides the slot, so one ability can bind to different inputs in different builds. When unset, the CDO's StartupInputTag is used. |
ActivationPolicy |
OnInput / WhileInputHeld bind to the input slot and activate on press / held routing. OnSpawn auto-activates the moment it is granted (passives, auras) — the generalized form of per-ability bAutoActivateOnGrant. |
RoleTag |
Kit-sheet role (Kit.Role.GapFiller / Payoff / Enabler). Authoring metadata only — no runtime meaning. Consumed by the Kit Composer sheet, never read during grant. |
GrantedEffects¶
Passive gameplay effects applied while the kit is granted. Infinite-duration GEs are expected; each is applied to self and its handle is tracked so RemoveAbilitySet can remove it.
Granted-Handle Bundle¶
FEternalAbilitySetGrantedHandles is the server-side receipt for one grant — a TArray<FGameplayAbilitySpecHandle> (abilities) plus a TArray<FActiveGameplayEffectHandle> (passives). It is never replicated (specs/effects replicate down through GAS on their own); it exists purely so the whole kit can be revoked in one call. Callers that intend to revoke later pass a handle bundle into GiveAbilitySet.
Grant & Revoke¶
UEternalAbilitySystemComponent::GiveAbilitySet(AbilitySet, OutHandles?)
|
+-- authority gate: IsOwnerActorAuthoritative()? no -> log + return (no-op)
|
+-- for each Abilities entry:
| EffectiveInputTag = InputTagOverride.IsValid() ? InputTagOverride : CDO.StartupInputTag
| if EffectiveInputTag valid -> UnbindInputTagFromExistingSpecs(EffectiveInputTag) (last-granted-wins)
| GiveAbility(spec) [SourceObject = the kit asset]
| if InputTagOverride -> swap CDO tag off the live spec, add the override tag
| if ActivationPolicy == OnSpawn -> TryActivateAbility(handle)
| record handle in OutHandles
|
+-- for each GrantedEffects entry:
MakeOutgoingSpec + ApplyGameplayEffectSpecToSelf -> record effect handle
RemoveAbilitySet(GrantedHandles) is likewise authority-gated: it ClearAbility on each ability handle, RemoveActiveGameplayEffect on each effect handle, then resets the bundle.
Server Authority¶
Both GiveAbilitySet and RemoveAbilitySet require IsOwnerActorAuthoritative(). Called on a client, they log a warning and no-op — the server owns every grant, and GAS replicates the resulting specs and active effects down to the owning client. There is no client-request path; grants originate from server-side spawn/possession logic only.
Input Slot Lanes¶
Input.Ability.Slot1–Slot6 are not six interchangeable ability slots. USkillbarController hard-maps each tag to a specific viewmodel, so the tag an ability claims decides which widget it renders into:
| Tag | Key | Viewmodel | What belongs here |
|---|---|---|---|
Input.Ability.Slot1 |
1 | Consumable1SlotViewModel |
Consumables (flask) |
Input.Ability.Slot2 |
2 | Consumable2SlotViewModel |
Consumables |
Input.Ability.Slot3 |
Q | Hotbar1SlotViewModel |
Skills |
Input.Ability.Slot4 |
E | Hotbar2SlotViewModel |
Skills |
Input.Ability.Slot5 |
R | Hotbar3SlotViewModel |
Skills |
Input.Ability.Slot6 |
V | Hotbar4SlotViewModel |
Skills |
A skill authored on Slot1 or Slot2 is granted, input-bound, and invisible on the skill hotbar — it renders into a consumable viewmodel and fights whatever flask ability also claims that lane. This is not a validator error today; it presents as "the ability exists in the debug view but the skillbar is empty", and it is what GA_CrashLanding did until 2026-07-08.
The four skill keys are a single shared pool, by convention rather than by code:
- Weapon skills author
Slot3by default (TPL_Skill,GA_Slash,GA_RisingMace);GA_ShieldSurgetakesSlot4. - Kit abilities and crafted abilities also draw from
Slot3–Slot6. A kit'sInputTagOverridepicks whichever key is free for that preset — e.g. greatswords carry no weapon skill, soSlot3is free for the kit in the GreatswordMomentum preset. - Last-granted-wins (below) is the conflict resolver, not the allocator.
Only four skill keys exist. A build carrying a weapon skill, a kit skill, and three crafted abilities runs out. That exhaustion — not designer preference — is the trigger for revisiting a player-assignable skillbar; until then there is no player-facing slot assignment.
Input-Slot Collision — Last-Granted-Wins¶
Two kits (or a kit and an item) can claim the same input slot. UnbindInputTagFromExistingSpecs runs before each bind and strips the tag from any earlier spec already bound to it, so the most recently granted ability owns the slot. The loser is not removed — it stays granted (and can still be activated by other means or re-bound), it is only unbound from that input. Because grant order is deterministic (see below), the winner is predictable.
This is a runtime resolution, not a guard: the compile-time guard against shipping a build with a silent collision is the Kit Composer validator (see Authoring).
Three-Layer Grant Order¶
A live character's abilities come from three sources, granted in a fixed order:
1. Class starter kit (CharacterClassInfo.StartupKit) GiveStartupAbilities
2. Preset kits (PresetCharacterAsset.GrantedKits) GiveComposedKits
3. Item abilities (equipment FAbilityFragment, Remnant Echo Mods)
Because input-slot collisions resolve last-granted-wins, this order defines precedence: a preset kit overrides a class default on the same slot; an item overrides both. Layer 3 is the equipment/Remnant apply path, documented under Equipment and Remnants.
| Source | Asset field | Granted by | Notes |
|---|---|---|---|
| Class starter kit | FCharacterClassDefaultInfo::StartupKit (UEternalAbilitySet) |
GiveStartupAbilities |
One kit per class row; replaced the legacy per-ability StartupAbilities list. |
| Preset kits | UPresetCharacterAsset::GrantedKits (TArray<TSoftObjectPtr<UEternalAbilitySet>>) |
GiveComposedKits |
Composed onto a curated build. Deliberately asset-level rather than inside the save payload, so kits need no save-version migration. Loaded synchronously (small assets, once at spawn). |
| Item abilities | Equipment / Remnant modifier grants | Equipment apply loop | Outside kit scope; documented with the item systems. |
Possession-Race Latch (Grant Exactly Once)¶
The ASC lives on the persistent AEternalPlayerState, so it outlives pawn death/respawn. Two facts drive the latch design:
- A pawn respawn re-runs
InitAbilityActorInfoon the same ASC — kits must not be granted a second time. - Possession and the persistence provider resolve in either order, depending on the boot path:
- PIE "Play Preset" relaunch possesses the pawn before the controller's provider resolves
ActivePreset. - A dedicated-server join resolves the provider before possession.
Two independent latch flags on the ASC solve both:
| Flag | Set by | Guards |
|---|---|---|
bStartupKitsGranted |
AEternalCharacter::InitAbilityActorInfo (once, on authority) |
Class starter kit + the first composed-kit attempt. Also the "possession already ran" signal the persistence hook reads. |
bComposedKitsGranted |
GiveComposedKits itself, after the grants run |
Preset kits. Self-latches only when it actually granted, so whichever side resolves last performs the grant. |
Flow¶
InitAbilityActorInfo (authority, first run for this ASC):
if !bStartupKitsGranted:
GiveStartupAbilities(class kit)
GiveComposedKits(controller) // no-ops if ActivePreset not resolved yet
bStartupKitsGranted = true
PersistenceComponent::InitializeProvider (when the preset resolves):
if controller has authority AND ASC.bStartupKitsGranted: // possession already happened
GiveComposedKits(controller) // grants now; the InitAbilityActorInfo attempt no-op'd
GiveComposedKits(controller, ASC):
if ASC.bComposedKitsGranted OR no authority -> return
if no ActivePreset -> return WITHOUT latching // provider not resolved; retried later
grant each preset kit
bComposedKitsGranted = true // latch AFTER granting; empty preset still latches
The key invariant: GiveComposedKits only latches when it had a resolved preset to act on. If the provider hasn't resolved, it returns without latching so the other side retries. A preset with zero kits still latches — there is nothing left to grant. This makes the composed-kit grant fire exactly once regardless of which of possession / provider-init wins the race.
Authoring Kits¶
Designers build kits with the Kit Composer editor tab, not by hand-editing the asset. See Ability Authoring Tools for the composer workflow.
Every kit and preset kit-list is checked by KitValidation — shared checks consumed by both the on-save / -run=DataValidation validator (commandlet available; not yet invoked by CI — B-1/B-2 in WorldSystemsScalingReview.plan.md) and the composer's inline validation strips. The checks that matter for a correct grant:
| Check | Severity | Why |
|---|---|---|
| Null ability / granted-effect entry | Error | Would silently skip at grant time. |
OnInput / WhileInputHeld with no effective input tag |
Error | Bound-input policy with nothing to bind to never activates. |
OnSpawn entry that still claims an input slot |
Error | Auto-activate abilities should not occupy a bindable slot. |
| Duplicate input slot within a kit | Error | The later entry silently unbinds the earlier at grant time. |
| Non-positive level / empty kit | Error | Malformed asset. |
| Cross-kit input collision between two preset kits | Error | A colliding build must never ship silently. |
| A preset kit claiming a class-starter-kit slot | Warning | Replacing a class default is last-granted-wins working as intended — but it should be a visible choice, not an accident. |
Source Reference¶
| Symbol | Location |
|---|---|
UEternalAbilitySet, FEternalGrantedAbility, FEternalAbilitySetGrantedHandles, EEternalAbilityActivationPolicy |
Source/ProjectEternal/Public/AbilitySystem/Data/EternalAbilitySet.h |
| Slot-tag → viewmodel mapping | Source/ProjectEternal/Private/UI/Controllers/SkillbarController.cpp → RefreshSlotBindings |
GiveAbilitySet / RemoveAbilitySet / UnbindInputTagFromExistingSpecs |
Source/ProjectEternal/Private/AbilitySystem/EternalAbilitySystemComponent.cpp |
GiveStartupAbilities / GiveComposedKits |
Source/ProjectEternal/Private/Utils/EternalAbilitySystemLibrary.cpp |
FCharacterClassDefaultInfo::StartupKit |
Source/ProjectEternal/Public/AbilitySystem/Data/CharacterClassInfo.h |
UPresetCharacterAsset::GrantedKits |
Source/ProjectEternal/Public/Persistence/Presets/PresetCharacterAsset.h |
Startup latch (bStartupKitsGranted) |
Source/ProjectEternal/Private/Character/EternalCharacter.cpp → InitAbilityActorInfo |
| Provider-side composed-kit grant | Source/ProjectEternal/Private/Persistence/Components/PersistenceComponent.cpp → InitializeProvider |
| Kit validation checks | Source/ProjectEternalEditor/Public/Validation/KitValidationCore.h |
Related Systems¶
- Ability Classes — the ability base classes a kit grants
- GAS Overview — ASC ownership on the PlayerState, input flow
- Ability Authoring Tools — Kit Composer and validator workflow
- Equipment System — item ability layer (grant order tier 3)
- Remnant System — Echo Mod ability layer
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-07-08 | Input slot lanes documented | Slot1/Slot2 are the consumable lanes (keys 1/2); skills live on Slot3–Slot6 (Q/E/R/V), shared between weapon, kit, and crafted abilities. A skill on a consumable lane binds but never reaches the skill hotbar |
| 2026-07-07 | Ability Kit system documented | UEternalAbilitySet as the composition unit; three-layer grant order (class → preset → items); last-granted-wins input collision; two-flag possession-race latch (bStartupKitsGranted / bComposedKitsGranted) grants each kit exactly once per session |