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:
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¶
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.Deadlives on the persistentPlayerStateASC, not on the pawn, so it does not clear when the dead pawn is destroyed. OnlyUEternalAbilitySystemComponent::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.Deathasset 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.cpp → GetHitIncreasedMultiplier() / 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 |
Related Systems¶
- GAS Overview - Attribute definitions
- Ability Classes - Damage ability implementations, on-hit ability
- Poise System - Poise damage calculation
- Combat Overview - Combat component damage methods
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 |