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
- Core Concepts (Why)
- Damage Pipeline
- Timed-Block Parry
- The Enemy Guard Stance
- Stat Resolution: Weapon vs Shield
- Unblockable & Guard-Breaking Attacks
- Stamina & Normalization
- Public Contracts
- Source References
- Related Systems
- Recent Changes
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): theEvent.Combat.DamageReceivedmirror 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
UBlockAbilityappliesGE_GuardBreakEffect(grantsState.GuardBroken, a stagger window). - Poise: while
State.Blockingis active,UPoiseSystemComponentreduces 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 → AttackerviaServerSetHitDirection, so the stagger plays from the correct facing. - Poise-gauge damage — applies
ParryPoiseDamageFractionof the attacker's max poise (default0.5, so ~2 fresh parries = poise Break), passed directly in poise units throughUPoiseSystemComponent::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.HitReactability (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.Parriedto 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:
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¶
UEnemyBlockAbilityis a sibling ofUBlockAbility, 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 replicatedState.BlockingGE 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::bWasParriedis set on a successful parry (sibling tobWasBlocked), 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 |
Related Systems¶
- Combat Overview — combat component hierarchy
- Poise System — stagger pressure and poise reduction while blocking
- Charge System — heavy/charged attacks pair with
Attack.GuardBreaker - Damage Execution — full incoming-damage pipeline that hosts
ProcessBlockDamage
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. |