Skip to content

Block System

Summary: The Block System is a hold-to-block defensive mechanic. Holding the block input swaps the ALS overlay, slows movement, and reduces incoming damage to "chip" damage while spending stamina per blocked hit. When stamina is exhausted a guard break passes the remaining damage through and staggers the defender. Block stats are weapon-defined and, when a shield is equipped, resolved from the shield instead of the main-hand weapon. A fresh block press also opens a short timed-parry window: a hit landing inside it is fully negated and the attacker is staggered through the poise system — a mistimed parry simply degrades to a normal block.

Table of Contents


Architecture Overview

Blocking is split between an ability that owns the input/visual/state lifecycle and the attribute set that intercepts incoming damage. The two halves communicate through a delegate so that all resource costs flow through the standard GAS pipeline rather than mutating attributes directly.

            Hold Block Input (Alt)
                     |
                     v
          +---------------------+        InputReleased / guard break / stamina = 0
          |    UBlockAbility    |--------------------------------------------------+
          | (UEternalGameplay-  |                                                  |
          |   Ability)          |                                                  |
          +---------------------+                                                  |
            |   |   |   |                                                          |
            |   |   |   +-- ApplyGameplayEffect: GE_BlockingStateEffect            |
            |   |   |        (grants State.Blocking + SetByCaller mvmt slow)       |
            |   |   |                                                              |
            |   |   +-- ApplyParryWindow(): GE_ParryWindow                         |
            |   |        (grants State.Parrying for ParryWindowDuration)          |
            |   |                                                                  |
            |   +-- SetOverlayMode(Overlay.Blocking / shield overlay)             |
            |        (resolved from FWeaponSetConfig)                             |
            |                                                                      |
            +-- bind OnDamageBlocked + OnDamageParried (server only) <--+         |
                                                                        |         |
   Incoming damage GE                                                   |         |
        |                                                               |         |
        v                                                               |         |
  PostGameplayEffectExecute -> HandleIncomingDamage (UEternalAttributeSet)        |
        |                                                               |         |
        v                                                               |         |
  ProcessParryDamage()  -- parried --> OnDamageParried(raw, Props) -----+         |
        | not parried                  (damage = 0, attacker staggered) |         |
        v                                                               |         |
  ProcessBlockDamage()  -- blocked --> OnDamageBlocked(cost, ...) ------+         |
        |                                                                         |
        v                                                                         v
  reduced (chip) damage applied to Health                              EndAbility: remove GEs,
                                                                       restore overlay, unbind

Core Concepts (Why)

Ability owns lifecycle, attribute set owns interception

The block state (overlay, movement slow, State.Blocking tag) is a normal UGameplayEffect granted by UBlockAbility. Damage interception must happen wherever damage is finally applied, which is UEternalAttributeSet::PostGameplayEffectExecute. Putting the angle/stamina/chip math in ProcessBlockDamage there guarantees every damage source is filtered consistently, regardless of which ability dealt it.

Costs go through the GAS pipeline, not direct setters

An earlier implementation called SetStamina directly inside PostGameplayEffectExecute; the stamina recovery effect immediately overwrote it. The system was refactored so the attribute set only signals a blocked hit via the OnDamageBlocked delegate, and UBlockAbility applies the stamina cost through a SetByCaller GE plus a recovery-block window. This mirrors how melee attacks spend stamina and is the canonical pattern for resource costs in this project.

The defender supplies its own resource

Blocking spends a resource per hit, but which resource and in what unit is the defender's business, not the damage pipeline's. The player spends stamina, priced as a fraction of its own health pool so blocking stays equally demanding as gear scales. An enemy has no live stamina at all and spends a dedicated guard pool, priced in the raw damage it received. Both resolve through the same three virtuals on UCombatComponent (GetBlockWeaponFragment, GetBlockResourcePool, GetBlockResourceCost), so UEternalAttributeSet needs no knowledge of who is blocking. See The Enemy Guard Stance.

Poise is the exception and is deliberately not normalized against anything: it is authored in flat poise units on both sides. It used to be divided by the target's MaxHealth, which cancelled MaxPoise out of the arithmetic and made every poise knob in the game inert.

Hold-to-block, not toggle

UBlockAbility activates on input press and ends on InputReleased, guard break, or full stamina depletion. There is no toggle state to desync; the block window is exactly as long as the input is held and stamina allows.

Damage Pipeline

ProcessBlockDamage() runs inside the target's PostGameplayEffectExecute and decides how much of the raw damage reaches Health.

RawDamage in
     |
     v
Target has State.Blocking? -------- no --> return RawDamage (no block)
     | yes
     v
Source actor exists? -------------- no --> return RawDamage (sourceless DoT/hazard = unblockable)
     | yes
     v
Source has Attack.Unblockable? ---- yes -> return RawDamage (bypasses block)
     | no
     v
Resolve block FWeaponFragment ----- none -> return RawDamage
     | (shield > main weapon)
     v
Attacker within BlockAngle arc? --- no --> return RawDamage (hit from the side/back)
     | yes
     v
StaminaCost = pool-normalized cost
     |
     +-- Stamina >= cost? --- yes --> broadcast OnDamageBlocked(cost, false)
     |                                 return RawDamage * (1 - BlockDamageReduction)   [chip]
     |
     +-- Stamina <  cost? (GUARD BREAK)
              broadcast OnDamageBlocked(remainingStamina, true)
              return  blocked-portion-chip + unblocked-portion        [partial passthrough]
  • Chip damage: a fully-funded block still lets (1 - BlockDamageReduction) of the hit through, so blocking mitigates but is never free immunity. Chip reduction affects only what reaches the defender's Health — it does not weaken on-damaged / retaliation procs (e.g. of Reprisal on-block Bleed): the Event.Combat.DamageReceived mirror carries the pre-block magnitude, so a full block (chip = 0) still retaliates and the counter-DoT scales off the swing received. See Damage Execution.
  • Guard break: when stamina cannot cover the hit, only the affordable fraction is reduced; the remainder passes through at full value, and UBlockAbility applies GE_GuardBreakEffect (grants State.GuardBroken, a stagger window).
  • Poise: while State.Blocking is active, UPoiseSystemComponent reduces incoming poise damage (BlockPoiseDamageReduction), so a held block also softens stagger pressure.

Timed-Block Parry

Parry is not a separate button — it is a sub-mode of blocking that rides the same hold-to-block Alt input. Every time UBlockAbility::ActivateAbility runs, it applies a short HasDuration GE (GE_ParryWindow) that grants State.Parrying. A hit that lands while both State.Parrying and State.Blocking are present is parried — fully negated, with the attacker staggered — instead of merely blocked. When the window has expired the same hit falls through to the normal chip/stamina block path.

GAS owns the window's lifetime: timing, replication, and client prediction all come from the duration GE rather than bespoke timestamps. The decision is made server-authoritatively inside PostGameplayEffectExecute (via HandleIncomingDamage), the same authoritative spot as block, so every damage source is filtered consistently.

Why timed-block over a separate parry button

A Souls-style dedicated parry button conflicts with hold-to-block (the Alt key is already committed to guard) and punishes a mistimed press with a whiff-recovery animation. Timing the parry off a fresh block press keeps the existing input with zero new bindings, and a mistimed parry degrades gracefully into a normal block — the player still guards the hit and pays only chip/stamina, never a punish. This makes parry the purest timing-mastery layer without making blocking riskier.

Parry Resolution Flow

ProcessParryDamage() runs as a sibling check before ProcessBlockDamage(). It shares the ResolveDefenseFragment() gate (sourceless filter, Attack.Unblockable filter, shield-priority fragment lookup, and the BlockAngle frontal-arc test) with the block path — ProcessBlockDamage was refactored to call that same helper so the two stay in lockstep.

HandleIncomingDamage(RawDamage, Props)
        |
        v
ProcessParryDamage():
   State.Parrying AND State.Blocking? ---- no --> fall through to ProcessBlockDamage
        | yes
        v
   Source NOT Attack.Unparryable? -------- no --> fall through to ProcessBlockDamage
        | yes
        v
   ResolveDefenseFragment() != null? ----- no --> fall through to ProcessBlockDamage
        | yes  (source avatar exists, not Unblockable, in shared BlockAngle arc)
        v
   damage = 0
   Props.bWasParried = true
   broadcast OnDamageParried(RawDamage, Props)
   return true  --> HandleIncomingDamage returns early
                    (skips ProcessBlockDamage, Health change,
                     OnDamageReceived, and the on-hit proc events)

A parry costs no stamina and no poise to the defender, and suppresses the defender's hit-react and damage numbers entirely (the early return skips OnDamageReceived and the Event.Combat.DamageDealt / DamageReceived procs). All reaction comes from the OnDamageParried broadcast.

Attacker Reaction

On a successful parry, UBlockAbility::OnDamageParried runs server-side and turns the parry into pressure on the attacker:

  • Hit direction — sets the attacker's hit direction Defender → Attacker via ServerSetHitDirection, so the stagger plays from the correct facing.
  • Poise-gauge damage — applies ParryPoiseDamageFraction of the attacker's max poise (default 0.5, so ~2 fresh parries = poise Break), passed directly in poise units through UPoiseSystemComponent::ApplyPoiseDamageToActor. Because it is a fraction of the attacker's own pool rather than accumulated pressure, a parry reads identically against trash and against a boss — deliberate, since it is defensive mastery rather than weapon pressure.
  • Directional hit-react — activates the attacker's Effects.HitReact ability (severity → knockback was just set by the poise damage). Normal enemies flinch on every parry; elites/bosses accumulate gauge until Break.
  • AI hook — sends Event.Combat.Parried to the attacker's ASC so behavior trees can react (hesitation, combo abort).

See Poise System for how the gauge, severity, and Stagger/Break thresholds work.

Anti-Spam Window Shrink

To stop players mashing block to fish for free parries, ApplyParryWindow() shrinks the window when the ability is re-pressed quickly. A re-press within ParrySpamResetTime (default 0.5s) of the previous release halves the next window (0.25 → 0.125 → 0.0625 → …); once the scaled window would drop below 0.05s the parry is forfeited and only a normal block remains. A successful parry resets the scale to full, so legitimate deflect chains against a multi-hit combo keep the full window on every swing.

Defender Feedback

On parry, OnDamageParried calls ExecuteGameplayCue(GameplayCue.Combat.Parry), which replicates to all clients. UGameplayCueNotify_Parry (a static C++ cue) routes the cue through the defender's UCombatEffectsManager under the Effects.Parry config key — spark VFX, camera shake, and a ~100ms hitstop all come from the standard combat-effects data. Routing through the defender's manager matters because hitstop / camera impulses only fire when the manager's owner is the locally controlled pawn.

The parry's sound is deliberately one sound. It used to be a metallic clang layered under a separate posture-crack from the poise side, and in playtest they fought: the beat read as noise rather than as "you damaged their posture". The crack now replaces the clang in the Effects.Parry row — one sound, one meaning — which is the same one-dominant-channel rule the break presentation applies everywhere else.

Attacker Feedback

The parry's whole point is that it turns the gauge around on the attacker, and that half was invisible until the poise pass. Two cues now carry it, both fired on the attacker:

Cue What it does
GameplayCue.Poise.ParryReflect Posture-crack row on the attacker (VFX-only; the parry row already owns shake and hitstop), gated to enemy attackers. Also reveals the attacker's stance bar — a parry deals no health damage, and the bar's only other spawn path is the damage-number cue, so without this no amount of reflected poise could make the bar appear
GameplayCue.Poise.Sunder Fires for free when the reflected poise is what finally breaks the attacker — the FSM cue is source-agnostic, so a parry-caused break shatters exactly like a weapon-caused one

The reveal is gated on the defender pawn being locally controlled (the cue reaches every client, but only the parrying player should get the bar) and keeps the Elite+ tier gate, so parrying trash still shows nothing.

Separately, ParryRecoilMontage is played through ASC->PlayMontage from the ability (so it replicates). It is purely cosmetic — the block state, overlay, and input are never touched; the recoil blends straight back into the held block pose.

The Enemy Guard Stance

Shield-carrying enemies opt into a held guard (FEnemyBlockConfig.bHasGuardStance) that runs through the same mitigation path the player uses. The block pipeline in UEternalAttributeSet was already source-agnostic except for stat resolution, so the split was drawn there rather than by branching on actor type.

UEternalAttributeSet::ProcessBlockDamage   (one implementation, both sides)
        |
        +-- GetBlockWeaponFragment()  --> player: shield, else weapon
        |                                 enemy:  fragment synthesized from FEnemyBlockConfig
        +-- GetBlockResourcePool()    --> player: stamina attribute
        |                                 enemy:  private guard pool on UEnemyCombatComponent
        +-- GetBlockResourceCost()    --> player: health-normalized stamina cost
                                          enemy:  the raw damage received

Why the guard pool is charged in received damage

V1 reused the player's health-normalized cost, where a hit's cost scales with the pool's own maximum — so the maximum cancelled out, every authored pool size broke after the same number of hits, and that number amounted to "a full health bar of blocked damage", i.e. never. The pool now has one lever, GuardCapacityHealthFraction, and a blocked hit costs exactly the damage it received — the raw figure, not the share the guard absorbed.

Charging 100% while mitigating 70% is deliberate: it keeps hits-to-break equal to capacity / raw damage and therefore decoupled from BlockDamageReduction, so tuning mitigation never silently retunes durability. Capacity resolves live from MaxHealth rather than at configure time, because spawn init pushes the guard config before the AI controller applies balanced stats.

The guard races the health bar

A guard break is only reachable if the pool empties before the enemy dies. With p = the share of landed hits that actually meet a raised guard and R = BlockDamageReduction, the break beats death only when:

p  >  GuardCapacityHealthFraction / (1 + R × GuardCapacityHealthFraction)

At R = 0.7 that is p > 21% for a 0.25 capacity fraction, 25% for 0.3, and 37% for 0.5 — while measured p in live PIE sat at 14-25%. So capacity is capped by the behavior tree's duty cycle, not chosen freely. Raising the fraction on its own tunes the guard break out of existence silently, since nothing reports a break that never happens. It moves only together with the BT work that raises how often the guard is actually up when a hit lands.

Regen is held by any damage

Once the cost formula was fixed the pool drained and the guard still never broke — the drain was losing a race with the refill. Two things fed that: the regen clock was armed only by blocked hits, and the enemy drops its guard to attack, so hits landing in that window cost the pool nothing and let the delay expire. The enemy's own attack animation was paying for its next shield. Every damage event of any kind (including DoTs and hazards) now re-stamps the same pressure window, and the delay is authored wider than the enemy's own raise-attack-recover cycle, so recovery rewards the player disengaging rather than resolving inside one exchange.

A break pays a stagger, and hands back half the pool

ApplyGuardBreak applies the optional State.GuardBroken GE and activates the enemy's Effects.Staggered ability — deliberately the same reaction a poise stagger fires, so both ways of cracking an enemy's defense read identically and the guard break needs no bespoke presentation. It is a pair, not a single call: the severity is written to Stagger first, mirroring the poise path, because the reaction sizes its knockback from that value and only Stagger powers through hyper armor.

The pool is handed back GuardBreakRefillFraction of capacity — a fixed share, not a share of whatever remained. Both extremes fail: at 0 the stance returns empty and re-breaks on sight (regen cannot cover it, since a player who keeps swinging holds regen off by design), locking the enemy out of guarding for the rest of the fight; at 1 every break resets a fresh shield and erases any sense of wearing the enemy down.

Timing lives in the ability, not the tree

The first wiring had no punish window at all — the guard re-raised the same frame any drop path ran. Fixing it at the ability layer covers every drop path (attack commit, guard break, hit-react interrupt, BT branch change), which a BT-structure fix would not.

Field Default Meaning
GuardCapacityHealthFraction 0.25 Blocked damage the guard absorbs, as a fraction of max health
GuardBreakRefillFraction 0.5 Share of capacity returned on a break
GuardPoolRefillSeconds 9s Time to refill an empty pool once out of pressure (authored as a duration, matching poise, so retuning capacity cannot silently retune recovery)
GuardPoolRegenDelay 4s Delay after the last damage of any kind before regen starts
GuardReraiseCooldown 1.5s Guard stays down this long after a guard break — the opening the player earned
GuardRecommitDelay 0.9s Guard stays down this long after any drop the player did not earn
GuardRaiseDelay 0.3s Gap between the overlay tell and mitigation actually applying, so the tell leads the protection
BlockDamageReduction 0.7 Fraction of a blocked hit mitigated
BlockAngle 120° Frontal arc the guard covers
PostHitGuardRaiseChance 0.5 Probability the BT re-raises after an attack's recovery; read by the tree

Splitting the break cooldown from the ordinary recommit delay was measured, not guessed: charging the full 1.5s for every drop left the stance down for 8 of 11 landed hits against a 1.3-2.4s attack cooldown — the punish window had become the default state. The drop is only stamped when mitigation was actually live, so a stance torn down inside its own raise delay (measured as short as 41ms) protects nothing and costs nothing.

Contract notes

  • UEnemyBlockAbility is a sibling of UBlockAbility, not a subclass: the player ability carries input bindings, parry windows, spam scaling and player-component lookups the enemy must not inherit. The enemy version is minimal — apply the blocking-state GE, swap the ALS overlay, drain the pool, break on empty. No parry in V1.
  • ServerOnly / InstancedPerActor. The guard pool, cooldown stamps and raise timer are all server-side; clients converge purely through the replicated State.Blocking GE and the ALS overlay.
  • The BT holds the stance rather than pulsing it — see Enemy AI for the tree grammar and the deadlock it avoids.
  • Known gap: the guard pool has no presentation at all — no bar, no strain audio, no distinct break cue. A pool that drains correctly and one that never drains look identical from behind the camera, which is exactly why the original inertness survived a whole feature.

Stat Resolution: Weapon vs Shield

Block stats are not global — they live on the equipped gear's FWeaponFragment (Combat|Block category). Resolution prioritises a shield:

GetBlockWeaponFragment():
   left-hand item is a Shield?  --> use the SHIELD's FWeaponFragment
   otherwise                    --> fall back to current weapon (right, then left, then unarmed)

This lets a sword-and-board loadout block with the shield's superior stats while the sword still drives attacks. Each FWeaponFragment exposes six block properties:

Property Default Meaning
BlockDamageReduction 0.5 Fraction of damage negated on a funded block (chip = 1 − this)
BlockPoiseDamageReduction 0.4 Fraction of poise damage negated while blocking
BlockEfficiency 1.0 Higher = less stamina spent per blocked hit
BlockAngle 120° Frontal arc within which attacks can be blocked
BlockStaminaCostRatio 0.5 Fraction of the hit converted into stamina cost
BlockMovementSpeedMultiplier 0.5 Movement speed while blocking (via SetByCaller)

The block overlay is no longer a weapon-fragment property. It is resolved from the active FWeaponSetConfig (BlockOverlayMode) via the equipment/weapon-set system, so overlay choice (e.g. shield-block vs unarmed-block pose) stays centralized with the rest of the overlay configuration.

Unblockable & Guard-Breaking Attacks

Tag Effect on blocking
Attack.Unblockable Source attack ignores block entirely — full damage passes through.
Attack.GuardBreaker Marks an attack intended to deal extra stamina damage / break guard (heavy/charged attacks).

Sourceless damage (DoTs, ground hazards, environmental) is inherently unblockable because ProcessBlockDamage requires a source avatar actor to consider the hit blockable.

Stamina & Normalization

Stamina cost per blocked hit is derived from the hit size relative to the defender's health pool, not the raw damage number:

StaminaCost = (RawDamage / MaxHealth) * MaxStamina * BlockStaminaCostRatio / max(BlockEfficiency, 0.1)

A hit worth X% of max health costs X% of max stamina scaled by the weapon's cost ratio and efficiency. This form is the player's cost only — the enemy guard pool uses a different unit entirely, resolved through GetBlockResourceCost so the two can never mix again. Poise is not normalized at all: it is flat poise units on both sides, and is reduced by BlockPoiseDamageReduction while a block is held. See Poise System.

Public Contracts

UBlockAbility — Configuration

Property Purpose
MinimumStaminaToBlock Minimum stamina required to start a block
BlockingStateEffectClass GE granting State.Blocking + movement-speed SetByCaller
GuardBreakEffectClass GE applied on guard break (State.GuardBroken stagger)
StaminaCostEffectClass SetByCaller GE used to spend stamina (shared with melee)
StaminaRecoveryBlockDuration How long stamina recovery is blocked after a blocked hit

UBlockAbility — Parry Configuration

Property Default Purpose
ParryWindowEffectClass GE_ParryWindow HasDuration GE granting State.Parrying; duration set dynamically on the spec.
ParryWindowDuration 0.25s Length of the parry window from block activation (200ms design target + latency pad).
ParrySpamResetTime 0.5s A re-press within this time of the previous release halves the next window.
ParryPoiseDamageFraction 0.5 Poise damage dealt to the parried attacker, as a fraction of their max poise.
ParryRecoilMontage Cosmetic defender recoil played via ASC->PlayMontage; block state/overlay/input untouched.

Block Delegate

Delegate Payload When Fired
OnDamageBlocked (on UEternalAttributeSet) StaminaCost, bGuardBroken Inside ProcessBlockDamage after a hit is blocked or breaks guard. Bound server-side by UBlockAbility.
OnDamageParried (on UEternalAttributeSet) RawDamage, FEffectProperties Inside ProcessParryDamage on a successful parry, before damage/procs are skipped. Bound server-side by UBlockAbility.

FEffectProperties::bWasParried is set on a successful parry (sibling to bWasBlocked), so downstream subscribers can distinguish a parried hit from a blocked one.

Gameplay Tags

Tag Purpose
State.Blocking Present while a block is held; gate checked by ProcessBlockDamage, ProcessParryDamage, and poise reduction.
State.Parrying Present during the timed-parry window; required (with State.Blocking) for a parry.
State.GuardBroken Applied on guard break; stagger/vulnerability window.
Overlay.Blocking Default block overlay mode (when no shield/weapon-set override).
Attack.Unblockable Source-side; bypasses block (and parry) entirely.
Attack.Unparryable Source-side; attack can still be blocked but never parried (heavy boss attacks).
Attack.GuardBreaker Source-side; attack meant to break guard / extra stamina damage.
GameplayCue.Combat.Parry Executed cue on a successful parry; handled by UGameplayCueNotify_Parry.
Effects.Parry UCombatEffectsManager config key for the parry impact (spark/ring/shake/hitstop).
Event.Combat.Parried Sent to the attacker's ASC on a parry; AI reaction hook.
Input.Ability.Block Block input mapping (bound to Alt in the gameplay IMC).
Ability.Action.Block Block ability action tag.
SetByCaller.BlockMovementSpeed SetByCaller key for the movement-speed reduction magnitude.

Source References

ClassName / concept Path/File
UBlockAbility (block + parry lifecycle) Source/ProjectEternal/Public/Abilities/BlockAbility.h, Source/ProjectEternal/Private/Abilities/BlockAbility.cpp
OnDamageParried / ApplyParryWindow (attacker reaction, anti-spam, cue) Source/ProjectEternal/Private/Abilities/BlockAbility.cpp
ProcessBlockDamage / ProcessParryDamage / ResolveDefenseFragment Source/ProjectEternal/Private/AbilitySystem/EternalAttributeSet.cpp
FEffectProperties::bWasParried, OnDamageParried delegate Source/ProjectEternal/Public/AbilitySystem/EternalAttributeSet.h
UGameplayCueNotify_Parry (defender feedback routing) Source/ProjectEternal/Public/AbilitySystem/GameplayCues/GameplayCueNotify_Parry.h, Source/ProjectEternal/Private/AbilitySystem/GameplayCues/GameplayCueNotify_Parry.cpp
GetBlockWeaponFragment() (shield-priority resolution) Source/ProjectEternal/Private/Combat/Components/PlayerCombatComponent.cpp
Block-stat resolution virtuals (GetBlockWeaponFragment / GetBlockResourcePool / GetBlockResourceCost) Source/ProjectEternal/Public/Combat/Components/CombatComponent.h
UEnemyBlockAbility (held guard stance, cooldown gate, deferred mitigation) Source/ProjectEternal/Public/Abilities/EnemyBlockAbility.h
Guard pool, synthesized fragment, NotifyGuardDropped / CanRaiseGuard Source/ProjectEternal/Public/Combat/Components/EnemyCombatComponent.h
FEnemyBlockConfig (all guard tuning) Source/ProjectEternal/Public/AI/Data/BaseEnemyDataAsset.h
UGameplayCueNotify_PoiseParryReflect (attacker crack + stance reveal) Source/ProjectEternal/Public/AbilitySystem/GameplayCues/GameplayCueNotify_PoiseParryReflect.h
FWeaponFragment (Combat\|Block properties) Source/ProjectEternal/Public/Inventory/Items/Fragments/ItemFragment.h
FWeaponSetConfig::BlockOverlayMode Source/ProjectEternal/Public/Combat/Data/WeaponSetConfigDataAsset.h
Poise reduction while blocking / ApplyPoiseDamageToActor Source/ProjectEternal/Private/Combat/Components/PoiseSystemComponent.cpp
Block / parry gameplay tags Source/ProjectEternal/Public/EternalGameplayTags.h, Source/ProjectEternal/Private/EternalGameplayTags.cpp
GE_ParryWindow content asset /Game/Gameplay/GameplayEffects/Block/GE_ParryWindow
GC_Parry content asset (BP child of UGameplayCueNotify_Parry) /Game/Gameplay/GameplayCues/GC_Parry

Recent Changes

Date Change Impact
2026-03-12 Introduced the Block System: hold-to-block via UBlockAbility, ProcessBlockDamage chip/stamina/guard-break pipeline, poise reduction while blocking, six block stats on FWeaponFragment, Alt-key input, and State.Blocking/State.GuardBroken/Overlay.Blocking/Attack.Unblockable/Attack.GuardBreaker tags. Stamina/poise costs are pool-normalized and applied through the GAS delegate pipeline. New core defensive mechanic.
2026-03-25 Removed BlockOverlayMode from FWeaponFragment; block overlay is now resolved from FWeaponSetConfig via the equipment/weapon-set system. Overlay config centralized; weapon fragments hold combat stats only.
2026-03-25 Added shield block support: dedicated shield block overlay and shield-priority stat resolution (GetBlockWeaponFragment) so an equipped shield supplies block stats over the main-hand weapon. Sword-and-board loadouts block with shield stats.
2026-07-02 Blocked-hit pre-block mirror (commit 0c52bcf5c): the Event.Combat.DamageReceived mirror now carries the pre-block magnitude, so chip reduction no longer weakens on-damaged / retaliation procs — a full block (chip = 0) still triggers retaliation (e.g. of Reprisal on-block Bleed via UOnDamagedAbility), scaled off the swing received rather than the chip. On-damaged / retaliation procs decoupled from chip mitigation.
2026-07-27 Enemy guard stance shipped (FSH-431): block-stat resolution moved to UCombatComponent virtuals, UEnemyBlockAbility as a sibling of UBlockAbility, a dedicated guard pool charged in received damage (GuardCapacityHealthFraction), regen held by any damage, a break that pays a stagger and returns half of capacity, and split re-raise / recommit timing. Enemies can hold a readable guard with a real punish window, without UEternalAttributeSet learning anything about enemies.
2026-07-27 Parry feedback folded to one channel per beat: the posture-crack replaces the metallic clang in Effects.Parry, and the attacker-side GameplayCue.Poise.ParryReflect adds the crack row plus a damage-less stance-bar reveal. The parry → reflect → break chain is legible for the first time; previously the reflected poise was completely invisible.
2026-07-27 Corrected stale poise claims: the parry reflect passes poise units directly (no health normalization), and poise is not pool-normalized on either side. Docs match the poise decoupling shipped 2026-07-25.
2026-06-17 Documented the timed-block parry system (shipped 2026-06-10): a fresh block press grants State.Parrying via GE_ParryWindow; ProcessParryDamage (sharing the refactored ResolveDefenseFragment gate with block) fully negates an in-window hit, broadcasts OnDamageParried, and UBlockAbility::OnDamageParried staggers the attacker (poise gauge + Effects.HitReact + Event.Combat.Parried). Added anti-spam window shrink, the GameplayCue.Combat.Parry / Effects.Parry defender feedback path via UGameplayCueNotify_Parry, and the State.Parrying / Attack.Unparryable tags. New parry layer on top of blocking; reuses the existing Alt input and poise system.