Skip to content

Damage Execution

Summary: Damage calculation in Project Eternal uses a custom Execution Calculation that processes armor, resistances, and penetration. This document explains the damage pipeline, formulas, and integration points.


Why a Custom ExecCalc?

GAS provides multiple ways to modify attributes, but Execution Calculations offer: - Multi-Attribute Access: Read source and target stats in one operation - Complex Formulas: Armor penetration, resistance stacking, crit calculations - Meta Attributes: Route through IncomingDamage for centralized handling - Server Authority: All calculations run on server, results replicated


Damage Pipeline Overview

+-------------------+
| Ability Hit Event |  Animation notify triggers Event.Combat.Hit
+-------------------+
         |
         v
+-------------------+
| Create GE Spec    |  Ability sets damage values via SetByCaller
+-------------------+
         |
         v
+-------------------+
| Apply to Target   |  GE with ExecCalc applied to target ASC
+-------------------+
         |
         v
+------------------------------------------+
|         UExecCalc_Damage::Execute()      |
|------------------------------------------|
| 1. Determine damage source (Player/NPC) |
| 2. Calculate base damage (per type)      |
| 3. Apply Increased buckets (per type)    |
| 4. Apply armor (physical component only) |
| 5. Apply elemental resistances           |
| 6. Roll critical strike                  |
| 7. Apply area modifier multipliers       |
| 8. Output to IncomingDamage + Critical   |
+------------------------------------------+
         |
         v
+-------------------+
| PostGameplayEffect|  AttributeSet handles IncomingDamage
| Execute()         |
+-------------------+
         |
         v
+------------------------------------------+
|     HandleIncomingDamage()               |
|------------------------------------------|
| 1. Read IncomingCritical meta attribute   |
| 2. Subtract from Health                  |
| 3. Broadcast OnDamageReceived            |
|    (amount, health, bWasCriticalHit)     |
| 4. Fire Event.Combat.DamageDealt         |
|    (if not from an on-hit proc)          |
+------------------------------------------+
         |
         v
+------------------------------------------+
|     On-Hit Proc System (passive)         |
|------------------------------------------|
| UOnHitAbility listeners receive   |
| the event, check conditions, roll chance,|
| and apply status effect GEs to target    |
| (see Ability Classes / On-Hit Ability)   |
+------------------------------------------+

Damage Formula

Step-by-Step Calculation

1. BASE DAMAGE (per type)
   +--------------------+
   | Player: CombatComponent->CalculateFinalDamage()
   | NPC: Sum of SetByCaller damage type values
   +--------------------+

2. INCREASED DAMAGE BUCKETS (per type)
   +--------------------+
   | TypeDamage *= max(0, 1 + (IncreasedType% + ConditionalVsStatus%) / 100)
   | Additive sum across all sources (PoE "Increased"); Conditional vs-status
   | joins this line only if the target has the matching Status.* tag.
   | Reduced past -100% deals zero.
   +--------------------+

3. ARMOR REDUCTION (physical component ONLY, diminishing returns, level-scaled K)
   +--------------------+
   | EffectiveArmor = TargetArmor * (100 - ArmorPen%) / 100
   | Reduction% = EffectiveArmor / (EffectiveArmor + K(AttackerLevel)) * 100
   | PhysicalDamage = PhysicalDamage * (100 - Reduction%) / 100
   | K scales with the ATTACKER'S level (FCombatBalanceConfig::GetArmorK, curve in
   | Config/Balance/CombatScaling.json: growth/exponent anchored at K(40)=250; same
   | shape as the enemy-armor curve, so on-curve gear holds ~constant mitigation and
   | stale gear visibly decays). AttackerLevel arrives via FHitInputs (stamped at emit
   | sites: player = weapon ilvl, enemies = EnemyLevel, unarmed/hazards = area level;
   | unstamped default = level 1). Tuning override: eternal.Combat.ArmorKMultiplier
   | (multiplies curve K; the old fixed eternal.Combat.ArmorK CVar is deleted).
   | Live reload: Eternal.Balance.ReloadCombatScaling.
   | Armor is the PHYSICAL mitigation axis exactly as resistances are the elemental
   | one — elemental damage never touches armor. Never reaches 100%; shared helper
   | UCombatStatics::ArmorToReductionPercent(EffectiveArmor, AttackerLevel) is also
   | what the character sheet (vs current area depth) and Cog balance window display.
   +--------------------+

4. RESISTANCE (per element)
   +--------------------+
   | ElementDamage = ElementDamage * (100 - Resistance%) / 100
   +--------------------+

5. CRITICAL STRIKE (on the post-mitigation total)
   +--------------------+
   | Roll CritChance vs random 0-100
   | If crit: Damage *= (1 + CritDamage% / 100)
   +--------------------+

6. AREA MODIFIER MULTIPLIERS
   +--------------------+
   | FinalDamage = Damage * SourceDamageDealt * TargetDamageTaken
   | (Multipliers default to 1.0 if unset)
   +--------------------+

Example Calculation

Given:
- Base Damage: 100
- Fire Damage: 50
- Increased Fire Damage: 30% (summed across items)
- Target Fire Resistance: 20%
- CriticalStrikeChance: 30%, CriticalStrikeDamage: 50% (bonus)
- Target Armor: 30
- Source Armor Penetration: 25%
- DamageDealtMultiplier: 1.0, DamageTakenMultiplier: 1.15 (area debuff)
- Attacker level such that K(AttackerLevel) = 100 (K is level-scaled; see step 3 above)

Step 1: Base = 100 (physical), Fire = 50
Step 2: Effective Armor = 30 * (100 - 25) / 100 = 22.5
Step 3: Reduction = 22.5 / (22.5 + 100) * 100 = 18.4%; Physical after armor = 100 * (100 - 18.4) / 100 = 81.6
Step 4: Fire after Increased = 50 * (1 + 30/100) = 65
Step 5: Fire after resistance = 65 * (100 - 20) / 100 = 52
Step 6: Total pre-crit = 81.6 + 52 = 133.6
Step 7: (If crit) 133.6 * 1.5 = 200.4
Step 8: Final = 200.4 * 1.0 * 1.15 = 230.5

Captured Attributes

Target (Defensive)

Attribute Purpose Range
Armor Physical damage reduction (physical component only — never elemental), diminishing returns A/(A+K(attackerLevel)) 0+ (never reaches 100%)
ResistanceFire Fire damage reduction 0-100%
ResistanceCorruption Corruption damage reduction 0-100%
ResistanceElectric Electric damage reduction 0-100%
DamageTakenMultiplier Incoming damage scale — area debuffs and the poise break (Sunder) window 0+ (1.0 = normal)
BleedDamageTaken Ailment taken-line: % bleed damage taken (additive, negative = less) -100%+
IgniteDamageTaken Ailment taken-line: % ignite damage taken (additive, negative = less) -100%+
PoisonDamageTaken Ailment taken-line: % poison damage taken (additive, negative = less) -100%+

Source (Offensive)

Attribute Purpose Range
ArmorPenetration Reduces target's effective armor 0-100%
CriticalStrikeChance Chance to critically strike 0-100%
CriticalStrikeDamage Bonus damage on crit (50 = +50%) 0+
DamageDealtMultiplier Area modifier: outgoing damage scale 0+ (1.0 = normal)

Source (Offensive) — Increased Damage Buckets

Additive-summed percentages (not multipliers). The attribute value is the summed %; negative = Reduced (floored at -100%). Applied per damage type after flat aggregation, before resistance.

Attribute Purpose
IncreasedPhysicalDamage % boost to physical damage
IncreasedFireDamage % boost to fire damage
IncreasedCorruptionDamage % boost to corruption damage
IncreasedElectricDamage % boost to electric damage
IncreasedDamageOverTime % boost to all DoT tick damage

Source (Offensive) — Conditional Damage vs Target Status

Join the per-type Increased line only if the target carries the matching status tag. See Conditional Modifiers.

Attribute Joins when target has
ConditionalDamageVsBleeding Status.Bleed
ConditionalDamageVsIgnited Status.Ignite
ConditionalDamageVsPoisoned Status.Poison
ConditionalDamageVsShocked Status.Shock

Increased Damage Buckets

Each damage type has a paired Increased attribute summed across all sources and applied as a single multiplier before resistance:

TypeDamage *= max(0, 1 + (IncreasedType% + ConditionalVsStatusSum%) / 100)

Authoring: modifiers target Stats.Offensive.Increased.<Type> (or ...ConditionalDamage.Vs*) with operation Flat — the attribute holds the final summed %, so multiple items stack additively. A parallel map FEternalGameplayTags::DamageTypesToIncreasedDamage maps each damage type (including Physical, unlike the resistance map) to its Increased attribute. Capture defs are built lazily (first-call) to avoid the native-tag CDO timing pitfall.


Damage Types

Tag Structure

DamageType
    |
    +-- Physical    (reduced by Armor)
    +-- Fire        (reduced by ResistanceFire)
    +-- Corruption  (reduced by ResistanceCorruption)
    +-- Electric    (reduced by ResistanceElectric)

Mapping

Damage Tag Resistance Attribute
DamageType.Physical Armor
DamageType.Fire ResistanceFire
DamageType.Corruption ResistanceCorruption
DamageType.Electric ResistanceElectric

The mapping is defined in FEternalGameplayTags::DamageTypesToResistances.


Player vs NPC Damage Sources

Player Damage Path

+-------------------+     +-------------------+     +-------------------+
| UPlayerCombat     | --> | PlayerCombat      | --> | ExecCalc reads    |
| Ability           |     | Component         |     | final damage      |
+-------------------+     +-------------------+     +-------------------+
                                 |
                    CalculateFinalDamage()
                                 |
                    +------------+------------+
                    |                         |
               Weapon Stats              Combo/Charge
               (Base damage,             Multipliers
                random range)

NPC Damage Path

+-------------------+     +-------------------+     +-------------------+
| UEnemyAbility     | --> | GE Spec with      | --> | ExecCalc sums     |
|                   |     | SetByCaller values|     | damage types      |
+-------------------+     +-------------------+     +-------------------+
                                |
                   BaseDamage attribute
                   (scaled by UBalanceSubsystem)

NPCs use BaseDamage attribute scaled by the Balance System. The ability passes this via SetByCaller: - UBalanceSubsystem calculates BaseDamage from Area Level, Threat Tier, and Archetype - Enemy abilities read BaseDamage from the AttributeSet - SetByCaller tags pass the value to the damage GE

Why the difference? Player damage involves weapon stats, combo multipliers, and attribute scaling that only the CombatComponent understands. NPCs use attribute-driven damage scaled by the Balance System.


Player Damage Calculation

Factors

Factor Source Effect
Base Weapon Damage FWeaponFragment::GetBaseDamage() × quality roll × item-level curve, via UWeaponPropertiesLibrary::CalculateWeaponDamage Starting value. Manifest damage is the on-curve value at the anchor item level; the curve (Config/Balance/ItemScaling.json) and the per-drop DamageQualityRoll are applied in the one builder every consumer reads — see Item Generation
Random Range UWeaponPropertiesLibrary::GetRandomDamage() Min-max variance
Attribute Scaling Ferocity, Grace, Feral (unarmed) Additive bonus
Combo Multiplier FComboState::DamageMultiplier Per-hit scaling in chain
Charge Multiplier FChargeLevel::DamageMultiplier Based on charge duration

Unarmed Scaling

Unarmed Bonus = (Ferocity * 0.4) + (Grace * 0.2) + (Feral * 0.3)

Damage Number Display

Flow

HandleIncomingDamage()
         |
         | Creates FGameplayCueParameters
         |   - RawMagnitude = damage value
         |   - Location = target position
         |   - SourceObject = attacker
         v
ExecuteGameplayCue("GameplayCue.DamageNumber")
         |
         v
AGameplayCueNotify_DamageNumber::OnExecute_Implementation()
         |
         v
UHUDController::SpawnStatus(Target, Value, Color)

Cue Parameters

Parameter Value Purpose
RawMagnitude Damage dealt Number to display
Location Target actor location Spawn position
TargetAttachComponent Target root World-space anchor
SourceObject Attacking actor Directional context

Damage Over Time Pipeline

Status effects (bleed, ignite, poison) use a separate ExecCalc that differs from the main damage pipeline:

ExecCalc_DamageOverTime vs ExecCalc_Damage

Step ExecCalc_Damage ExecCalc_DamageOverTime
Elemental Resistances Yes Yes
Armor Reduction Yes (physical component only) No (DoTs bypass armor)
Critical Strike Yes No (DoTs don't crit)
Per-type Increased buckets Yes No (ticks seed from post-Increased hit damage)
DoT dealt-line (IncreasedDamageOverTime + *DamageDealt) No Yes (selected by Status.* asset tag)
Ailment taken-line (*DamageTaken) No Yes (selected by Status.* asset tag)
Area Multipliers Yes Yes

Why skip armor? DoTs represent ongoing wounds or elemental burning — they're not physical impacts. Elemental resistances still reduce elemental DoTs (fire resist reduces ignite). This deliberately includes Bleed: armor defends against hits only, so a heavy-armor character is not automatically safe from bleed — bleed defense is its own gear decision via the BleedDamageTaken taken-line. Ailments inherit their damage type's resistance and never get resistances of their own; see Ailment Model.

Ailment Types

Ailment Damage Type Behavior DoT?
Bleed Physical Ticks damage over duration Yes
Ignite Fire Ticks damage over duration Yes
Poison Corruption Ticks damage over duration Yes
Shock Electric Reduces all elemental resistances No (debuff)

DoT Damage Scaling

DoT tick damage scales from the hit that triggered it, not a fixed value:

Tick Damage = HitDamage × HitDamageMultiplier
  → Passed via SetByCaller to GE
  → ExecCalc_DamageOverTime applies resistance, then the source's DoT dealt-line and
    the target's ailment taken-line (both selected by the Status.* asset tag on the GE):
       Tick *= (100 - Resistance%) / 100
       Tick *= max(0, 1 + (IncreasedDamageOverTime% + AilmentDealt%) / 100)   // source side
       Tick *= max(0, 1 + AilmentDamageTaken% / 100)     // target side, Bleed/Ignite/Poison only

Per-type Increased* buckets deliberately do not apply to ticks: procs seed their tick damage from the post-Increased hit, so re-applying the per-type bucket double-dipped ("+50% Physical" was worth ~+125% on bleed ticks). DoT scaling is its own vocabulary — IncreasedDamageOverTime (all DoTs) plus the per-ailment dealt-line (BleedDamageDealt/IgniteDamageDealt/PoisonDamageDealt, tags Stats.Offensive.AilmentDealt.*); see Ailment Model.

Each independent proc instance tracks its own damage and duration (PoE2-style stacking).


Death-Reaction Abilities & Shared Hazard Plumbing

Some enemies deal damage as a reaction to dying (e.g. the larva's death explosion). This is a GAS-driven pattern layered on top of the death lifecycle rather than a special case inside the damage pipeline, so the same hazard/event plumbing is reusable by any ability or projectile.

Death Event Flow

Fatal hit detected in PostGameplayEffectExecute
         |
         | death broadcast deferred one tick (SetTimerForNextTick)
         v
AEternalCharacter::Die()
         |
         | SendGameplayEventToActor(Event.Death)
         v
+------------------------------------------+
| UEnemyDeathExplosionAbility              |  (a UEnemyAoeAbility)
| - Triggered by Event.Death               |
| - Tagged Ability.Death so death cleanup  |
|   does NOT cancel it                      |
| - Plays death montage (no ragdoll)       |
+------------------------------------------+
         |
         | montage burst-frame notify
         | (UAnimNotify_SendGameplayEvent -> Event.Combat.Hit)
         v
+------------------------------------------+
| OnHitEvent():                            |
| 1. Radial AOE burst (one-shot damage)    |
| 2. Drop lingering ground hazard          |
| 3. Vanish mesh via State.Hidden          |
+------------------------------------------+

The first step is not cosmetic. UCombatComponent never broadcasts death on the hot stack — it defers one tick. The fatal handler runs inside PostGameplayEffectExecute, and everything downstream of the broadcast (effect removals, ability cancels, collision/ragdoll flips, hazard overlap-ends) mutates the very effect container still executing — sometimes the very periodic effect that landed the killing tick.

The enemy opts out of ragdolling so the death montage can play: AEternalEnemy overrides ShouldRagdollOnDeath() to return !bSuppressRagdollOnDeath. Once dead, State.Dead blocks all incoming damage, so invincibility during the animation is automatic.

Players are the caveat. State.Dead lives on the persistent PlayerState ASC, not on the pawn, so it does not clear when the dead pawn is destroyed. Only UEternalAbilitySystemComponent::ResetForRespawn() removes it — without that call the respawned player is permanently damage-immune. See Spawn & Transition Ordering.

Why Ability.Death? Death cleanup cancels in-flight abilities. The death-reaction ability must be exempt, so it carries the Ability.Death asset tag (the ASC's death cleanup skips abilities with that tag). Per project convention, ability/asset/trigger tags are wired in the Blueprint, not the C++ constructor (see CLAUDE.md — native tags are empty during CDO construction).

Reusable Infrastructure

Class Role Authority
UCombatHazardLibrary::SpawnGroundHazard Single entry point for spawning a ground hazard; shared by projectiles (on impact) and abilities Server-only (client calls ignored)
UAnimNotify_SendGameplayEvent Generic montage-side cue that sends an arbitrary gameplay event to the montage owner; lets GAS logic wait on a montage frame Server-only by default
UVisibilityStateComponent Hides a character's mesh (and optionally collision) purely from the presence of State.Hidden on the owner ASC Driven by replicated ASC tag

Damage survives the instigator's death. SpawnGroundHazard snapshots its damage specs from the instigator ASC's BaseDamage at spawn time, so a hazard dropped by a dying enemy keeps dealing damage after that enemy is gone.

Decoupled hiding. UVisibilityStateComponent reacts only to the State.Hidden tag count on the ASC — the system that hides a character (here, the death-explosion ability) never references the character class directly.

Native Tags

Tag Purpose
Event.Death Sent from Die(); triggers death-reaction abilities
Ability.Death Asset tag exempting an ability from death cleanup cancellation
State.Hidden Owner-ASC tag that drives UVisibilityStateComponent to hide the mesh

Source Reference

Component Location
UExecCalc_Damage Source/ProjectEternal/Public/AbilitySystem/ExecCalc/ExecCalc_Damage.h
UExecCalc_DamageOverTime Source/ProjectEternal/Public/AbilitySystem/ExecCalc/ExecCalc_DamageOverTime.h
UOnHitAbility Source/ProjectEternal/Public/Abilities/OnHitAbility.h
UPlayerCombatComponent::CalculateFinalDamage Source/ProjectEternal/Private/Combat/Components/PlayerCombatComponent.cpp
UEternalAttributeSet::HandleIncomingDamage Source/ProjectEternal/Private/AbilitySystem/EternalAttributeSet.cpp
AGameplayCueNotify_DamageNumber Source/ProjectEternal/Public/AbilitySystem/GameplayCues/GameplayCueNotify_DamageNumber.h
FEternalGameplayTags::DamageTypesToResistances Source/ProjectEternal/Private/EternalGameplayTags.cpp
FEternalGameplayTags::DamageTypesToIncreasedDamage Source/ProjectEternal/Private/EternalGameplayTags.cpp
Increased/Conditional capture + helpers Source/ProjectEternal/Private/AbilitySystem/ExecCalc/ExecCalc_Damage.cppGetHitIncreasedMultiplier() / GetHitConditionalVsStatusSum()
UEnemyDeathExplosionAbility Source/ProjectEternal/Public/Abilities/EnemyAbilities/EnemyDeathExplosionAbility.h
UCombatHazardLibrary::SpawnGroundHazard Source/ProjectEternal/Public/Combat/Hazards/CombatHazardLibrary.h
UAnimNotify_SendGameplayEvent Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_SendGameplayEvent.h
UVisibilityStateComponent Source/ProjectEternal/Public/Character/Components/VisibilityStateComponent.h
AEternalCharacter::Die Source/ProjectEternal/Private/Character/EternalCharacter.cpp
AEternalEnemy::ShouldRagdollOnDeath Source/ProjectEternal/Public/AI/Enemy/EternalEnemy.h


Refactoring Considerations

Issue Current State Recommendation
Player vs NPC source Split happens upstream, not in the ExecCalc: player damage is computed by UPlayerCombatComponent (implements ICombatDataProvider, player-side only) and fed via SetByCaller; NPCs feed BaseDamage. The ExecCalc no longer branches on an actor tag Resolved — the player-side combat-data interface is ICombatDataProvider (a hypothetical IPlayerDamageSource was never built)
Damage colors All numbers use white Map damage types to colors via TMap<FGameplayTag, FLinearColor>
Resistance lookup Repeated per-type code Create GetResistanceCaptureDef(FGameplayTag) helper

Recent Changes

Date Change Impact
2026-08-06 Death broadcast deferred one tick out of PostGameplayEffectExecute; documented that State.Dead on a player clears only via ResetForRespawn() The death cascade mutates the effect container still executing — a DoT tick killing mid-montage was the crashing case. On players the tag outlives the pawn, so a respawn that skips the reset is permanently damage-immune
2026-07-27 The poise break window rides DamageTakenMultiplier (a flat modifier for the break duration, enemies only) rather than a new attribute or an execution change Both damage executions already capture the attribute, so DoTs and every other damage source are amplified for free. Anything else tuning DamageTakenMultiplier now shares it with the break window, and stacks with it additively. See Sunder
2026-07-24 Armor K scales with attacker level (FSH-436, feature/balance-curve-retune) ArmorToReductionPercent takes (EffectiveArmor, AttackerLevel); K from FCombatBalanceConfig::GetArmorK (Config/Balance/CombatScaling.json, anchor K(40)=250, same curve shape as enemy scaling → on-curve gear ≈ constant mitigation, stale gear decays). FHitInputs.AttackerLevel stamped at all emit sites (player = weapon ilvl, enemy = EnemyLevel, unarmed/hazard = area level; chains inherit via seed payload). Fixed-K CVar eternal.Combat.ArmorK deleted; eternal.Combat.ArmorKMultiplier multiplies curve K. Character sheet shows mitigation vs current area depth. Enemy armor retuned as per-archetype identity (EnemyScaling.json)
2026-07-24 Souls lethality pass + weapon item-level scaling (feature/itemization-fsh-369-364) Weapon damage = manifest base × item-level curve (Config/Balance/ItemScaling.json, anchor iLvl 5, mirrors the enemy MaxHealth curve) × per-drop ±15% DamageQualityRoll, applied in CalculateWeaponDamage. Enemy BaseDamage 4→16, eternal.Combat.ArmorK 100→250 (interim static; K-by-enemy-level is the follow-up), threat-tier HP elite 4x / champion 6x / boss 20x. Tuning target: 4-6 normal enemy hits kill the player; level-40 sweep TTK in band
2026-07-02 Blocked-hit pre-block mirror (commit 0c52bcf5c) On blocked hits the Event.Combat.DamageReceived mirror's EventMagnitude is now the pre-block damage (attacker-side DamageDealt unchanged); full blocks (chip = 0) still fire on-damaged / retaliation procs and their DoT ticks scale off the swing received, not the chip. See Block System
2026-07-01 Per-hit proc tags (Event.Hit.Critical / Event.Hit.Blocked) HandleIncomingDamage packs transient hit tags into the DamageDealt/DamageReceived event's InstigatorTags; new RequiredHitTags (HasAll) condition on UOnHitAbility unlocks on-crit / on-block proc affixes (GDD "ignite-on-crit" channel). Parried hits fire no event, so no Event.Hit.Parried
2026-07-01 Armor scoped to the physical component only Elemental damage no longer double-mitigated (resist + armor); armor is THE physical axis, resistances THE elemental one — tooltip math now matches the exec (GDD "tooltip math legible")
2026-07-01 DoT scaling vocabulary (dealt-line, double-dip removed) Per-type Increased* no longer applies to ticks (procs seed from post-Increased hits); new source-side Bleed/Ignite/PoisonDamageDealt attributes (Stats.Offensive.AilmentDealt.*) join IncreasedDamageOverTime on one additive line, selected by Status.* asset tag
2026-07-01 Armor diminishing returns (FSH-318) Reduction% = A/(A+K), K=100 via eternal.Combat.ArmorK; shared UEternalCombatStatics::ArmorToReductionPercent drives exec, character sheet (computed %), and Cog balance EHP; no reachable physical immunity
2026-07-01 Ailment taken-line (FSH-307) BleedDamageTaken/IgniteDamageTaken/PoisonDamageTaken target-side additive % attributes applied in ExecCalc_DamageOverTime, selected by Status.* asset tag; modifier tags Stats.Defensive.AilmentTaken.*; see 06_Ailment_Model.md
2026-06-11 Increased damage buckets + conditional damage vs status 5 Increased attributes (Physical/Fire/Corruption/Electric/DamageOverTime) + 4 ConditionalDamageVs* attributes; native tags + DamageTypesToIncreasedDamage map; both ExecCalcs apply the additive Increased line (incl. conditional-vs-status sum) before resistance; existing item-equip path resolves new tags automatically
2026-05-22 Enemy death-reaction abilities + reusable hazard/notify infra UEnemyDeathExplosionAbility triggered by Event.Death (exempt via Ability.Death); UCombatHazardLibrary::SpawnGroundHazard snapshots BaseDamage so hazards outlive the instigator; generic UAnimNotify_SendGameplayEvent; UVisibilityStateComponent hides mesh via State.Hidden
2026-03 On-hit proc system + ExecCalc_DamageOverTime Event.Combat.DamageDealt fires from HandleIncomingDamage; DoT pipeline skips armor/crits
2026-02 Critical strike system CritChance/CritDamage captured in ExecCalc, IncomingCritical meta attribute
2026-02 Area modifier multipliers DamageDealtMultiplier (source) and DamageTakenMultiplier (target) applied after armor
2026-02 NPC BaseDamage via Balance System Enemy damage scales with area/tier/archetype
2025-Q1 Trimmed to 3 resistances Fire, Corruption, Electric
2024-Q4 Player damage via CombatComponent Centralized weapon calculations
2024-Q4 Meta attribute pipeline IncomingDamage now routes all damage
2024-Q3 Armor penetration New offensive stat, captured in ExecCalc