Combat Animation¶
The Combat Animation System manages montage selection and playback through UMontageManagerComponent. Montages are stored in weapon DataTables and looked up by gameplay tags. Animation notifies embedded in montages drive combat timing for hits, input windows, and effects.
Weapon animation is resolved along two orthogonal axes: WeaponHand (which arms hold equipment) selects the montage table and drives ABP arm-layer blending; WeaponSet (the weapon combination) drives the ALS locomotion overlay. See Two-Axis Animation Resolution.
Architecture¶
+------------------------+
| Ability Activation |
+------------------------+
|
v
+------------------------+
| IMontageManagerInterface|
| ::GetMontageByTag() |
+------------------------+
|
v
+------------------------+
| UMontageManagerComponent|
+------------------------+
|
v
+------------------------+
| DataTable Lookup |
| (FMontageAction rows) |
+------------------------+
| Row: LightAttack_1 |
| ActionTag: InputTag.LMB
| Montages: [Sword_L1] |
| Row: LightAttack_2 |
| ActionTag: InputTag.LMB
| Montages: [Sword_L2] |
+------------------------+
|
v
+------------------------+
| UAnimMontage returned |
+------------------------+
|
v
+------------------------+
| UAbilityTask_PlayMontageAndWait |
+------------------------+
|
v
+-----------------------------+
| Montage Playback |
+-----------------------------+
| Notify: ComboHitWindow |---> Hit Tracing
| Notify: ComboInputWindow |---> Input Acceptance
| Notify: CombatEffect |---> VFX/Audio/Camera Impulse
| Notify: WeaponTrail |---> Trail Effects
| Notify: IsInvincibleState |---> I-Frames
+-----------------------------+
Why This Design?¶
Tag-Based Lookup¶
Abilities request montages by FGameplayTag rather than direct asset references. This allows:
- Weapon swapping without ability changes
- Data-driven animation assignment
- Random selection from montage variants
DataTable Storage¶
Each weapon type has its own DataTable containing all its montages. This keeps data organized and allows weapon artists to iterate without touching code.
Notify-Driven Timing¶
All combat timing comes from animation notifies rather than hardcoded durations. This means: - Animators control game feel directly - Different montages can have different timing - Playback rate changes don't break combat windows
Two-Axis Animation Resolution¶
Weapon animation is split into two independent concerns rather than one "weapon type" concept. This lets the posture/overlay (driven by the weapon combination) vary independently from the montage moveset and arm blending (driven by which hands are occupied).
| Axis | What it represents | Drives | Source of truth |
|---|---|---|---|
| WeaponHand | Which arms hold equipment (solo, shield-only, sword+shield, dual-wield) | ABP arm-layer blending; montage-table lookup; per-hand combo/charge config | Resolved at runtime; tags WeaponHand.Right/Left/Both/RightWithShield/ShieldOnly |
| WeaponSet | The weapon combination (1H solo, 1H+shield, dual-wield, 2H, unarmed) | ALS locomotion/idle overlay (and block overlay) | UEquipmentComponent resolves a WeaponSet.* tag; UWeaponSetConfigDataAsset maps it to overlay tags |
Equipment changes (main hand / off hand)
|
+---------------+----------------+
| |
v v
WeaponSet axis WeaponHand axis
(weapon combination) (which arms occupied)
| |
v v
UEquipmentComponent ICombatDataProvider
::UpdateWeaponSetOnCharacter() ::GetCurrentWeaponHand()
| |
v v
UWeaponSetConfigDataAsset FWeaponFragment
Find(WeaponSet) -> OverlayMode ::GetMontagesForHand(WeaponHand)
| | ::GetComboConfigForHand(WeaponHand)
v | ::GetChargeConfigForHand(WeaponHand)
AlsCharacter::SetOverlayMode() v
(locomotion posture) Montage table + combo/charge tuning
(moveset) + ABP arm-layer blend
Why Two Axes?¶
- Shield disambiguation: shield-only and sword+shield share the
WeaponSet_1H_Shieldoverlay but differ in arm blending — distinguished by theWeaponHand.ShieldOnlyvsWeaponHand.RightWithShieldtags. - Centralized overlay mapping: overlay activation lives in
UEquipmentComponent+ a singleUWeaponSetConfigDataAsset, instead of each weapon fragment activating its own overlay. Adding/retuning a weapon set is a data edit, not per-weapon wiring. - Per-hand-context moveset: a versatile weapon can ship different montages, combo counts, and charge levels depending on whether it is solo, paired with a shield, or dual-wielded.
- Single resolution path: tooltips and runtime both call
GetCurrentWeaponHand(), so displayed and actual behavior cannot diverge.
Known issue:
WeaponHandmay not replicate correctly for left-hand weapons on clients (off-hand resolution can be wrong remotely).
WeaponHand-Keyed Montage Tables¶
FWeaponFragment stores montage tables as TMap<WeaponHand tag, UDataTable*> (MontageTables). A weapon only defines the entries it needs — a 2H hammer has one entry; a versatile sword may have several (right, shield, dual-wield). Lookup falls back to the first entry when the current hand context has no explicit mapping. Combo and charge configuration are stored the same way (TMap<WeaponHand, FComboConfiguration> / TMap<WeaponHand, FChargeConfiguration>) directly on the fragment — the standalone ComboConfiguration/ChargeConfiguration data assets are no longer the source of truth.
Montage Lookup Flow¶
This page documents the weapon-table route only: weapon attacks, combos, charges, hit reacts, dodges, and enemy abilities. Skills own their montage directly (
SkillMontageson the ability CDO) and never reach the lookup below. See Montage Resolution.
How Abilities Get Montages¶
This is the tag-keyed table route: weapon attacks, charge, hit reacts, dodge, and every enemy ability.
Player skills do not use it — they read their montage straight off the ability CDO's SkillMontages.
See Montage Resolution.
Charge is split across both homes. The looping wind-up plays as an ability montage on
UChargeWindupAbility (predicted through the stock PlayMontageAndWait pipeline, so ending the ability stops exactly
the instance it started). Only the released charged swing goes through the tag-keyed table, via
UChargeComponent::GetChargedAttackMontage() — keyed by the charge config's ChargeMontageTag, falling back to the
weapon's heavy attack montage.
Ability needs attack montage
|
v
GetMontageForActionTag(InputTag.LMB, CombatMontageTable)
|
v
IMontageManagerInterface::GetMontageByTag()
|
v
UMontageManagerComponent::GetMontageByTag()
|
+---> Iterate DataTable rows
+---> Find row where ActionTag matches
+---> Select montage by index (or random)
|
v
Return UAnimMontage*
Montage Index Selection¶
| MontageIndex | Behavior |
|---|---|
| 0, 1, 2... | Select specific montage from array |
| INDEX_NONE (-1) | Random selection from array |
Combo systems typically use the combo count as the index, giving each hit its own montage.
DataTable Structure¶
FMontageAction Row Format¶
| Column | Type | Purpose |
|---|---|---|
ActionTag |
FGameplayTag | Tag to match during lookup |
Montages |
TArray |
Array of montage variants |
Example DataTable (Sword)¶
| Row Name | ActionTag | Montages |
|---|---|---|
| LightAttack_1 | InputTag.LMB | [Sword_Light_01, Sword_Light_01_Alt] |
| LightAttack_2 | InputTag.LMB | [Sword_Light_02] |
| LightAttack_3 | InputTag.LMB | [Sword_Light_03] |
| HeavyAttack_1 | InputTag.RMB | [Sword_Heavy_01] |
| ChargedAttack | Input.Attack.Charged | [Sword_Charged_01] |
| HitReact_Front | Effects.HitFront | [HitReact_Front_01] |
| HitReact_Back | Effects.HitBack | [HitReact_Back_01] |
Weapon-Based Montage Selection¶
How PlayerCombatComponent Gets Montages¶
Weapon equipped
|
v
OnWeaponChanged() delegate fires
|
v
UPlayerCombatComponent::OnRightWeaponChanged()
|
v
UpdateCombatMontages()
|
+---> Get active weapon (right, then left)
+---> Get FWeaponFragment from ItemManifest
+---> Resolve current WeaponHand (GetCurrentWeaponHand)
+---> GetMontagesForHand(WeaponHand) on the fragment
+---> SetCombatMontage(DataTable)
|
v
Montages now available for lookup
The PlayerCombatComponent owns this flow - it reacts to weapon changes rather than being told which montages to use. This decouples the equipment system from montage management. The selected table is the entry in the fragment's MontageTables map matching the current WeaponHand, falling back to the first entry.
Priority Order¶
- Right-hand weapon fragment (main hand), table chosen by current WeaponHand
- Left-hand weapon fragment (off-hand)
- nullptr if unarmed (abilities use default/unarmed montages)
Animation Notifies¶
Combat montages contain these notify types:
Combat Timing Notifies¶
| Notify | Type | Purpose |
|---|---|---|
| ComboHitWindow | NotifyState | Enable/disable hit tracing |
| ComboInputWindowState | NotifyState | Accept combo input |
| ComboInputBufferWindowState | NotifyState | Buffer input for next attack |
| ComboComplete | Notify (instant) | Reset the chain immediately (authority only) |
Effect Notifies¶
| Notify | Type | Purpose |
|---|---|---|
| CombatEffect | Notify | Trigger VFX/audio/camera impulse at specific frame |
| WeaponTrail | NotifyState | Spawn and update trail effect |
State Notifies¶
| Notify | Type | Purpose |
|---|---|---|
| IsInvincibleState | NotifyState | Grant i-frames via GAS tag |
| BlockAbilityInput | NotifyState | Prevent new ability activation |
| MovementAnimationCancel | NotifyState | Allow movement to cancel anim |
Action Rotation and the ALS Rotation Lock¶
An attack steers the character to face its target through UTargetingComponent's action rotation override,
which drives actor yaw directly. ALS drives yaw too, so the two must never run at once —
AAlsCharacter::RefreshGroundedRotation early-returns while LocomotionAction is set, and that tag
(Als.LocomotionAction.Attacking) is the only thing standing between them.
The tag is a single slot with no owner identity, so it cannot be shared. While an action rotation is live,
UTargetingComponent owns it: it sets the tag on start and re-asserts it every tick. A single set is not
enough, because Als Set Locomotion Action notify states end at the end of their montage's blend-out — during
a combo that lands after the next attack has already claimed the tag, and ALS's equality check cannot tell the
two claims apart, so the finished attack clears a lock that is still in use.
Consequences worth knowing before you author or debug:
- The montage notify is advisory. Several player attack montages carry no
Als Set Locomotion Actionnotify at all and behave identically. Do not add one expecting it to control facing, and do not remove one expecting facing to break. - The component backs off only for a different locomotion action — mantle, roll, ragdoll, get-up. Those outrank an attack's facing and carry their own rotation handling. Every release is equality-guarded, so no owner ever clears a tag it did not set.
- The ability opens the override from the payload, and must release it.
UMeleeAbility::ActivateAbilitycallsUPlayerCombatComponent::OpenAttackRotation(Intent)— each machine opens its own override toward the yaw and target the swing payload carried — and releases the returned handle inEndAbility. The handle is what stops a finished combo step from cancelling the step that replaced it.UDodgeAbilitydoes the same; a new ability that steers the character must too. - A press that activates nothing has nothing to unwind. The input path is resolve-only: it builds the intent and fires the gameplay event, so if hit react, stun or death beat the input, no override was ever opened. The activation counts those calls return are not a rotation-unwinding mechanism.
- Each machine owns its own override; none of this state replicates. The interpolation runs from wherever
that character actually is, so the start rotation is necessarily local, and
LocomotionActionis per-machine in ALS too. Opens and releases therefore cross the wire as RPCs in both directions — including the ones the tick decides on. A replicated flag over local interpolation state is the trap here: the far end overwrites a machine that has already released, and it resumes with a stale start and a finished alpha, snapping straight to the stored target. Sub-degree in most attacks, violent after a break-away, and it looks like client and server fighting over rotation. Simulated proxies take rotation from movement replication and sit this out.
Symptom of getting this wrong: yaw oscillating at zero velocity after an attack, which reads as a fast idle because the ABP's lean and turn-in-place react to the jitter.
Combat Feel / Impact Notifies¶
These four notifies are pure game feel — they sell the weight of a swing (hyper-armor, the attacker's step-in, a camera lean, a deeper freeze-frame). They are authored on the montage by the animator and, unlike knockback distance, do not scale with item tier. See Where Per-Attack Feel Lives.
| Notify | Type | Purpose |
|---|---|---|
UAnimNotifyState_HyperArmor |
NotifyState | Server-only: adds the loose State.HyperArmor tag over the window so the attacker shrugs off flinch + knockback but not poise. Author over active frames only; leave wind-up/recovery unarmored so trading hits stays fair. |
UAnimNotify_AttackFollowThrough |
Notify (instant) | Additive forward ConstantForce root-motion push on the attacker after contact, so they press in behind a launched enemy. Distance ~60uu, Duration ~0.18s. Net-guarded to IsLocallyControlled() || HasAuthority(); simulated proxies skip it and receive the resulting movement via replication. |
UAnimNotify_CameraSwingGesture |
Notify (instant) | Local-player-only camera boom impulse via UDynamicCameraComponent::AddCameraImpulse. LocalDirection is projected to the ground plane, Strength ~25uu. Author across the windup/strike, NOT the contact frame (already saturated by the impact impulse + hitstop). Only manifests in PIE — Persona preview has no gameplay camera. |
UAnimNotify_HitStopWeight |
Notify (instant) | Stamps a per-attack hitstop multiplier on the owner's UCombatComponent (SetHitStopIntensity, Weight default 1.0). Consumed by the next landed hit to scale its freeze-frame (a finisher freezes harder than a poke). Place at the start of the hit-detection window so it is set before the trace fires; cleared on a whiff by ComboHitWindow NotifyEnd. |
Where Per-Attack Feel Lives¶
Impact "weight" is split between two homes by what kind of number it is. Balance numbers (which scale with the item) live in data on the weapon fragment; feel/timing numbers (which do not) live as notifies on the montage. Keeping them separate means an animator can retune the feel of a swing without touching balance, and a designer can rebalance an item without re-authoring animation.
| Lever | Home | Owner | Scales with item tier? |
|---|---|---|---|
| Knockback distance | Weapon fragment ComboConfig (per-step arrays, indexed by combo step) |
Designer / balance | Yes |
| Follow-through (step-in push) | UAnimNotify_AttackFollowThrough on the montage |
Animator / feel | No |
| Hit-stop weight | UAnimNotify_HitStopWeight on the montage |
Animator / feel | No |
| Camera swing gesture | UAnimNotify_CameraSwingGesture on the montage |
Animator / feel | No |
Authoring caveats:
- Motion-warp notifies must target the name AttackTarget. That is the name the combat component registers the
warp target under, and it is set before the montage starts so the notify sees a valid target at its NotifyBegin.
A notify authored against any other target name warps nowhere and fails silently — a recent pass found 47 montages
whose notifies carried a mismatched name, so every one of their swings played with dead motion warping.
- Hyper-armor: arm only the active frames, never wind-up or recovery — an always-armored attack is unfair to trade against.
- Follow-through needs its net guard; without it the push double-applies on simulated proxies (it is additive on top of replicated movement).
- Camera swing must not sit on the contact frame — that frame is already saturated by the impact camera impulse + hitstop, so the lean should read as anticipation/follow-through across the windup/strike instead.
- Hit-react anims must be root-locked (in-place / root motion off) so their baked root motion does not fight the enemy's knockback root-motion source.
Notify Timeline Example¶
Attack Montage Timeline:
|----Startup----|-----Active-----|----Recovery----|
0.0s 0.2s 0.5s 0.8s
| | | |
| |<--HitWindow--> | |
| 0.2s 0.4s | |
| | |
| |<--InputWindow---------->|
| 0.35s 0.75s|
| |
|<-----------BufferWindow-------------------->|
0.1s 0.7s|
| |
| CombatEffect (swing audio) |
| 0.15s |
| |
|<----WeaponTrail----> |
0.1s 0.45s |
Hit React Montages¶
Directional hit reactions use the hit direction to select the correct montage:
Hit received with direction
|
v
UCombatComponent::GetDirectionalHit()
|
v
IMontageManagerInterface::GetHitReactMontage(Direction)
|
+---> Map direction to tag:
| Front -> Effects.HitFront
| Back -> Effects.HitBack
| Left -> Effects.HitLeft
| Right -> Effects.HitRight
|
v
GetMontageByTag(DirectionTag, 0, DefaultMontages)
|
v
Return appropriate hit react montage
Key Contracts¶
IMontageManagerInterface¶
| Method | Parameters | Returns | Purpose |
|---|---|---|---|
GetMontageByTag() |
ActionTag, Index, DataTable | UAnimMontage* | Look up montage |
GetHitReactMontage() |
HitDirection | UAnimMontage* | Get directional hit react |
GetDefaultMontageTable() |
- | UDataTable* | Get character's default table |
ICombatDataProvider (animation-relevant)¶
| Method | Parameters | Returns | Purpose |
|---|---|---|---|
GetCurrentWeaponHand() |
- | FGameplayTag | Shared WeaponHand resolution used by montage/combo/charge lookup and tooltips |
FWeaponFragment (animation-relevant)¶
| Method | Parameters | Returns | Purpose |
|---|---|---|---|
GetMontagesForHand() |
WeaponHand tag | UDataTable* | Montage table for the hand context (first-entry fallback) |
GetComboConfigForHand() |
WeaponHand tag | FComboConfiguration* | Per-hand combo tuning |
GetChargeConfigForHand() |
WeaponHand tag | FChargeConfiguration* | Per-hand charge tuning |
UWeaponSetConfigDataAsset¶
| Member | Type | Purpose |
|---|---|---|
WeaponSetConfigs |
TMap |
WeaponSet tag → overlay config |
Find() |
FGameplayTag → FWeaponSetConfig* | Resolve overlay/block-overlay tags for a weapon set |
UMontageManagerComponent Properties¶
| Property | Type | Purpose |
|---|---|---|
DefaultMontages |
UDataTable* | Fallback montage table |
OwnerCharacter |
AEternalCharacter* | Cached owner reference |
Combat Effect Notify Properties¶
| Property | Type | Purpose |
|---|---|---|
EffectTag |
FGameplayTag | Which effect to trigger |
SocketName |
FName | Spawn location socket |
LocationOffset |
FVector | Offset from socket |
IntensityMultiplier |
float | Effect intensity scale |
bTriggerCameraShake |
bool | Trigger camera shake |
bTriggerTimeEffects |
bool | Trigger hit stop/slow-mo |
DirectionalImpulseStrength |
float | Camera push in owner's forward direction (0 = disabled) |
ZoomImpulseStrength |
float | Camera zoom push-in strength (0 = disabled) |
Source References¶
| Concept | File | Line |
|---|---|---|
| UMontageManagerComponent class | Source/ProjectEternal/Public/Combat/Components/MontageManagerComponent.h | 20 |
| IMontageManagerInterface | Source/ProjectEternal/Public/Combat/Components/MontageManagerComponent.h | 10 |
| FMontageAction struct | Source/ProjectEternal/Public/Data/MontageAction.h | 10 |
| GetMontageByTag() | Source/ProjectEternal/Private/Combat/Components/MontageManagerComponent.cpp | 30 |
| UpdateCombatMontages() | Source/ProjectEternal/Private/Combat/Components/PlayerCombatComponent.cpp | 279 |
| OpenAttackRotation() / warp target setup | Source/ProjectEternal/Private/Combat/Components/PlayerCombatComponent.cpp | 607-700 |
| Ability-side rotation open / release | Source/ProjectEternal/Private/Abilities/MeleeAbility.cpp | 133, 637 |
| Charged swing montage lookup | Source/ProjectEternal/Private/Combat/Components/ChargeComponent.cpp | 225 |
| Charge wind-up montage (ability-owned) | Source/ProjectEternal/Public/Abilities/ChargeWindupAbility.h | 26 |
| ComboHitWindow notify | Source/ProjectEternal/Public/Combat/AnimNotifies/ComboAnimNotifies.h | 10 |
| CombatEffect notify | Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_CombatEffect.h | 15 |
| WeaponTrail notify | Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotifyState_WeaponTrail.h | 10 |
| Action rotation override | Source/ProjectEternal/Private/Combat/Components/TargetingComponent.cpp | 66 |
| ALS rotation lock (early-return) | Plugins/ALS/Source/ALS/Private/AlsCharacter.cpp | 1517 |
| HyperArmor notify state | Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotifyState_HyperArmor.h | - |
| AttackFollowThrough notify | Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_AttackFollowThrough.h | - |
| CameraSwingGesture notify | Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_CameraSwingGesture.h | - |
| HitStopWeight notify | Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_HitStopWeight.h | - |
| Whiff hitstop-weight clear (ComboHitWindow NotifyEnd) | Source/ProjectEternal/Private/Combat/AnimNotifies/ComboAnimNotifies.cpp | - |
| WeaponSet overlay config | Source/ProjectEternal/Public/Combat/Data/WeaponSetConfigDataAsset.h | - |
| WeaponSet → overlay resolution | Source/ProjectEternal/Private/EquipmentManagement/Components/EquipmentComponent.cpp -> UpdateWeaponSetOnCharacter() | - |
| GetCurrentWeaponHand() | Source/ProjectEternal/Public/Interface/CombatDataProviderInterface.h | - |
| WeaponHand montage tables / per-hand config | Source/ProjectEternal/Public/Inventory/Items/Fragments/ItemFragment.h -> FWeaponFragment | - |
| WeaponHand / WeaponSet tags | Source/ProjectEternal/Public/EternalGameplayTags.h | - |
Related Documentation¶
- Combat Overview - Component hierarchy
- Combo System - Combo montage selection
- Charge System - Charged attack montages
- Poise System - What
State.HyperArmorsuppresses (flinch + knockback, not poise) - Hit Tracing - Hit window activation
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-08-06 | Doc reconciled with the payload-driven attack flow: the attack ability now opens its rotation override from the swing payload (OpenAttackRotation(Intent)) and releases it in EndAbility; the input path is resolve-only. Charge documented as split — wind-up loop is an ability montage on UChargeWindupAbility, only the released charged swing uses the tag-keyed table. ComboComplete resets the chain immediately on authority (no timer). Motion-warp target-name authoring rule added. |
Removes the stale "input layer releases the override" rationale (a press that activates nothing never opened one) and documents the mismatched motion-warp target name that left 47 montages warping nowhere |
| 2026-07-26 | Action rotation state no longer replicates; opens and releases mirror by RPC in both directions, including tick-decided releases. | Fixes a client/server rotation fight visible when breaking off a locked target. The replicated flag over local interpolation state let the far end reopen an override that had already released, snapping yaw to the stored target |
| 2026-07-26 | Action rotation and the ALS rotation lock documented. UTargetingComponent now owns Als.LocomotionAction.Attacking for the life of an override and re-asserts it each tick; abilities release via a handle; the input layer releases when no ability activated. |
Fixes yaw oscillation after a combo (two writers on actor rotation). The Als Set Locomotion Action notify is now advisory — montages without one behave identically, so facing no longer depends on montage authoring |
| 2026-06-17 | Combat-feel / impact notifies added (commit 4df2921ab): UAnimNotifyState_HyperArmor (server-only loose State.HyperArmor over active frames — suppresses flinch + knockback, not poise), UAnimNotify_AttackFollowThrough (additive forward push, net-guarded), UAnimNotify_CameraSwingGesture (local-only camera boom impulse, PIE-only), UAnimNotify_HitStopWeight (per-attack hitstop multiplier on UCombatComponent, consumed by next hit, whiff-cleared by ComboHitWindow NotifyEnd). |
Animators can author swing weight (hyper-armor, step-in, camera lean, freeze-frame) per montage. Feel/timing lives on the montage; only knockback distance scales with item tier (weapon fragment ComboConfig) |
| 2026-03-26 | Hand-aware combat config: combo/charge configuration moved onto FWeaponFragment as TMap<WeaponHand, FComboConfiguration/FChargeConfiguration>; ICombatDataProvider::GetCurrentWeaponHand() added as shared resolution path (tooltip + runtime). Shield-only resolves to WeaponSet_1H_Shield with WeaponHand.ShieldOnly for ABP distinction. |
Combo counts, multipliers, and charge levels can be tuned per hand context; standalone Combo/Charge config data assets no longer the source of truth |
| 2026-03-25 | WeaponSet system: weapon animation split into two axes — WeaponHand (drives ABP arm blending + montage-table lookup) and WeaponSet (drives ALS overlay via UWeaponSetConfigDataAsset). Fragment montage tables became TMap<WeaponHand, DataTable>. Overlay activation moved from per-weapon fragment to UEquipmentComponent. |
Centralized, data-driven overlay resolution; weapons only define the montage entries they need. Known issue: WeaponHand may not replicate correctly for left-hand weapons on clients |
| 2026-03-24 | Added WeaponSet tags and WeaponSetConfig data asset for weapon-set → overlay tag mapping. |
Foundation for centralized overlay resolution |
| - | PlayerCombatComponent now owns montage updates: calls UpdateCombatMontages() internally on weapon change via OnWeaponChanged() rather than being told which montages to use. |
Inverts dependency; decouples equipment system from montage management |
| - | Right-hand weapon takes priority when determining active montages, falling back to left-hand, then clearing if unarmed. | Predictable main-hand-first moveset selection |