Combat & Damage Calculation
The full damage pipeline — hit chance, damage, mitigation order, block, resistances, crushing blow, life leech, and attack speed — with the real formulas.
On this page
- Overview — the damage pipeline
- Hit chance — Attack Rating vs Defense Rating
- Derived combat stats
- Outgoing physical damage at swing time
- Incoming mitigation (defending hero)
- Worked examples
- Resistances & elemental damage
- Block
- Crushing Blow
- Life Leech
- HP regeneration
- Thorns
- Rage (feeds the outgoing multiplier)
- Order-of-operations gotchas
- Caps & constants quick reference
This page is the exact reference for how a swing turns into damage in Echoes of Theron. Every
number below is read straight from the engine (src/core/formulas.ts, src/core/simulation.ts) and
its hand-tunable constants (src/data/balance/). For a visual box-by-box version, see the
Damage Flow charts.
Overview — the damage pipeline
Each landed swing resolves in a strict order. Physical and elemental damage are separate hits that ride the same swing and obey opposite rules.
- Roll to hit — compare the attacker’s Attack Rating (AR) to the defender’s Defense Rating (DR)
on a
tanhcurve, then apply a small level-difference shift. Miss → 0 damage. - Roll raw damage — a uniform integer between the fighter’s Min and Max Damage.
- Outgoing multipliers — skill damage multiplier, then the passive/rage multiplier
(Wrath + Apex + Rampage). Round →
scaledRaw. - Mitigation (only when the hero is the defender) — block first, then the capped percentage-reduction bucket, then flat reduction.
- On-hit effects — life leech, crushing blow, thorns, elemental riders, bleed/DoTs, procs.
Two asymmetries are critical and never change:
- Physical damage to enemies ignores resistance, AR and DR, and enemies never block. AR/DR only decide whether a swing lands, never how hard.
- Elemental damage is a separate flat-per-hit rider. It bypasses the skill multiplier and Wrath, and it is the only player damage reduced by the target’s resistance.
Hit chance — Attack Rating vs Defense Rating
AR and DR feed a smooth log-ratio sigmoid centred on 70% at AR = DR. This is the whole of what
AR/DR do to offense (Defense Rating additionally grants the defender a small capped physical
reduction — see Incoming mitigation).
From hitChanceFromRatings(AR, DR):
logRatio = ln( AR / DR )
curve = tanh( logRatio × 1.1 )
chance = 0.70 + curve × (0.95 − 0.70) if curve ≥ 0 (toward the ceiling)
= 0.70 + curve × (0.70 − 0.05) if curve < 0 (toward the floor)
chance = clamp(chance, 0.05, 0.95)
AR and DR are each floored at 1 before the ratio. The constants (FORMULA_CONFIG):
| Constant | Value | Meaning |
|---|---|---|
hitChanceAtEqualRatings | 0.70 | Hit chance when AR = DR |
hitChanceCurveSteepness | 1.1 | Multiplier on ln(AR/DR) inside tanh |
maxHitChance | 0.95 | Ceiling, even with enormous AR |
minHitChance | 0.05 | Floor, even against overwhelming DR |
The range is asymmetric: an AR advantage climbs from 70% toward the 95% ceiling (a 25-point band), while an AR deficit falls from 70% toward the 5% floor (a 65-point band), so being out-rated costs more than being over-rated gains.
Worked hit-chance values
| AR | DR | ln(AR/DR) | tanh(×1.1) | Hit chance |
|---|---|---|---|---|
| 100 | 100 | 0 | 0 | 70.0% |
| 200 | 100 | 0.693 | +0.642 | 86.1% |
| 400 | 100 | 1.386 | +0.910 | 92.7% |
| 50 | 100 | −0.693 | −0.642 | 28.2% |
| 25 | 100 | −1.386 | −0.910 | 10.9% |
Flat hit-chance bonus
Some affixes and passives add a flat bonus on top of the sigmoid via applyFlatHitChanceBonus,
which re-clamps to [0.05, 0.95]. Gear flat hit-chance is capped at +30%
(MAX_HIT_CHANCE_FLAT_BONUS = 0.3); the combined total (gear + Precision + tavern drinks) is capped
at +50% in aggregate before the clamp.
Level difference
The level gap between attacker and defender changes the chance once more, at every level.
Enemies attacking the hero get a flat shift through the same applyFlatHitChanceBonus path
(levelDeltaHitChanceShift), so it obeys the same [0.05, 0.95] clamp:
shift = clamp( (enemyLevel − heroLevel) × 0.01, −0.10, +0.10 )
The hero attacking (playerLevelHitChanceRule):
- Out-levelling the enemy adds the same points: +1% per level, at most +10%.
- An enemy above the hero takes a share of the hero’s hit chance instead of points: 1% of
it per level, at most 10% (10 levels and more). It applies after the flat bonus and is
clamped into
[0.05, 0.95]again:
chance = clamp( chanceWithFlatBonus × (1 − min(levelsAbove × 0.01, 0.10)), 0.05, 0.95 )
| Hit chance before | Enemy 10+ levels above | Lost |
|---|---|---|
| 80% | 72% | a tenth |
| 40% | 36% | a tenth |
| 20% | 18% | a tenth |
Every hero loses the same part of their hits, however much Attack Rating they have (a flat 10 points
would have cost the 20% hero half of them). The rule is deliberately ungated: in the Endless
Delve, where enemy level climbs past the hero cap of 99, it settles at a constant tenth. Constants
(FORMULA_CONFIG): levelDeltaHitChancePerLevel = 0.01, levelDeltaHitChanceMaxShift = 0.10,
playerLevelPenaltySharePerLevel = 0.01, playerLevelPenaltyMaxShare = 0.10.
Derived combat stats
These are built once per stat-panel rebuild in computePlayerStats. Attributes are floored at 0;
each (1 + percent) multiplier is floored at 0.2 (MIN_STAT_MULTIPLIER) so a −100% roll can
never fully zero a stat. Per-point coefficients (src/data/balance/player.ts):
| Attribute | Per-point effect |
|---|---|
| Vitality | +5 Max HP (HERO_HP_PER_VITALITY) |
| Strength | +0.25% physical damage (STR_DAMAGE_PERCENT_PER_POINT = 0.0025) and +3 Attack Rating (HERO_AR_PER_STRENGTH) |
| Dexterity | +4 Attack Rating (HERO_AR_PER_DEXTERITY) and +0.05% attack speed (DEX_ATTACK_SPEED_PER_POINT = 0.0005) |
| Constitution | +5 Defense Rating (HERO_DR_PER_CONSTITUTION), +0.06% physical reduction (CON_PHYS_REDUCTION_PER_POINT = 0.0006), and flat per-hit reduction of 0.0025 × level (CON_FLAT_REDUCTION_PER_POINT_PER_LEVEL) |
Per level (independent of attributes): +3 Max HP, +4 Attack Rating, +2 Defense Rating
(HERO_*_PER_LEVEL). A fresh hero starts at base 70 HP, 50 AR, 1.12 s interval.
Max HP
MaxHP = max( 12, (baseHp + flatHpAffix + 5×VIT + 3×level) × (1 + hp%) )
Attack Rating
AR = max( 1, baseAR + flatARaffix + 4×DEX + 3×STR + 4×level )
AR × (1 + gearAR% + trainingAR% + drinkAR% + Σ buffAR%) ← one pooled multiplier
Note the class-rebalance coefficients: DEX gives 4 AR/pt (not 5) and STR gives 3 AR/pt,
so Strength buys accuracy and DEX no longer monopolises it; 4 AR per level keeps every hero’s
hit chance from sliding as enemy Defense Rating climbs. All AR% sources — unique-item
affixes, Leather armor / Blades weapon training, the tavern AR drink and active buff skills — are
summed into a single additive pool and applied once on top of the flat AR (in
applyPlayerDerivedStats); Strike’s AR% is a separate multiplier after that. Rank 1 is the bare
0.125% base and S(rank) is the skill’s accumulated effect steps — 27 at
rank 20, so a maxed Strike is AR × 1.035.
Magic/Rare gear only rolls flat AR.
Defense Rating
DR = max( 1, (baseDR + flatDRaffix + 5×CON + 2×level) × (1 + DR%) )
Min / Max damage
Flat-damage affixes are speed-normalised by the weapon’s own interval so they yield equal DPS on fast and slow weapons; the weapon’s own min/max (and the enhanced-damage % that scales it) are already DPS-normalised and left untouched. Strength is a speed-neutral multiplier, not flat per hit.
Scope: Flat Damage and Enhanced Damage rolled on a weapon boost that weapon only, and the same affixes on a shield boost only its Shield-Strike damage — both are already folded into the range printed on the item’s own card (class passive included). From every other slot (ring, amulet, armour) they are character-wide and add on top of whichever damage source you currently swing.
An unrevealed unique contributes its base item stats in full but none of its affixes — an Enhanced Damage or Flat Damage roll adds nothing until the Trader reveals the item.
normFlat = round( (flatDamageAffix_weapon + flatDamageAffix_otherSlots) × weaponBaseInterval / 1.0 )
# RAGE_REFERENCE_INTERVAL = 1.0
edOther = max( 0.2, 1 + enhancedDamage% from non-weapon, non-shield slots )
strMult = max( 0.2, 1 + 0.0025 × STR )
MinDamage = max( 1, (weaponMin + normFlat) × edOther × strMult )
MaxDamage = max( 2, (weaponMax + normFlat) × edOther × strMult )
weaponMin/weaponMax already include the weapon’s own enhanced-damage % (which scales weapon base
only, before flat and before Strength) plus any smith-temper / witch-enchant. After the stored stat is built, the
flow chart’s later layers — the class weapon-class multiplier and War Cry — are applied to the
stored damage stat before the swing. There is no critical-hit chance;
variance comes only from the Min–Max roll and elemental rolls.
Two-handed weapons
Every one-handed family sits on the same base DPS per tier — a gilded knife at 1.0 s and a gilded hammer at 3.5 s roll very different dice but land the same damage per second, so the choice between them is speed class, hit size and weight, never raw throughput. A two-handed weapon’s dice are that same baseline scaled by one constant:
perHitDamage = oneHandedBaseDPS × TWO_HANDED_DAMAGE_MULT × weaponBaseInterval # mult = 1.75
Greatswords and greataxes (2.5 s), claymores and halberds (3.0 s) and mauls (3.5 s) — and the staves line, Quarterstaff (1.0 s), Bo (1.5 s), Battlestaff (2.0 s), Ironstaff (2.5 s) and Khakkhara (3.0 s) — therefore all land on 1.75× the one-handed DPS and differ only in how big a single hit is. Everything downstream — flat-damage normalisation, Strength, enhanced damage, the skill multiplier, Wrath — treats a two-hander exactly like any other weapon; the premium is paid for by the empty off-hand (no block, no shield defense, and the shield-strike skills fall back to a plain attack). See Item Base Types.
Dual wield
Two one-handed weapons run two independent swing timers, each at its own interval, and each timer resolves the full selected main skill. Every hand’s swing is scaled by two constants:
handDamage = ownWeaponRoll × DUAL_WIELD_DAMAGE_MULT # mult = 0.65
handEffect = nominalEffect × DUAL_WIELD_EFFECT_MULT # mult = 0.50
Two hands at 0.65 damage add up to 130% of a single weapon’s output — the pair’s gain over one weapon is 30% plus a second affix set, not 100%. Per-hit effects sit lower, at 0.50 per hand: two hands proc at 100% of a single weapon’s rate, so swinging twice as often does not also double your rage, bleeds, stuns, leech or crushing blows.
What scales per hand (the EFFECT multiplier): Rage / Valor per hit and the On Hit affixes,
crushing-blow chance — the gear/training chance and a skill’s built-in rider such as Crush’s —,
Aegis’s and Smite’s stun chance, Rend’s bleed fraction, Shatter’s armor shred per hit, Reaver’s
leech per rank, the monk riders’ elemental share, and Flurry’s attack-speed stack. The elemental status procs
(ignite, freeze, lightning stun) scale their chance the same way; their damage needs no scaling
because each hand carries its own weapon’s flat elemental values at the same 0.65 damage scale.
What applies once, never per weapon: Berserk’s defense penalty, the attack-speed multipliers of Crush / Berserk / Flurry (they scale each timer, they do not stack), and the main skill’s rank.
Per-hand details worth knowing:
- Each hand rolls its own weapon’s dice and its own weapon-scoped affixes (flat damage, enhanced damage, flat elemental, life leech, Rage On Hit, attack speed); everything character-level counts once for both.
- Accuracy is per hand too.
Attack Rating,Attack Rating %andHit Chancerolled on a weapon only sharpen that weapon’s swings, so each hand rolls to hit with its own attack rating and its own flat hit chance. The same affixes on armour, an amulet, a ring or a shield stay character-wide and count for both hands, as do the material-set bonus, passive skills, buffs and Strike’s own Attack Rating perk. While dual wielding the hero box groups its Damage and Hit Chance rows under a Strike (Main Hand) and a Strike (Off-Hand) header — the selected main skill’s name plus the hand — and each hand gets its own swing bar. Active skills, shield strikes and counter-attacks always use the main hand’s rating. - The class weapon multiplier is evaluated per hand, from the speed class of the weapon actually swinging — a Duelist swinging a fast off-hand knife and a slow main-hand hammer gets +20% on one and −20% on the other.
- Zeal keeps its three hits per hand, each at Zeal’s per-hit damage
× 0.65; the Rage On Hit split across the hits works as it always did, then takes the 0.50 effect scale. - Weapon-training bonuses are main-hand only — T1–T5 bonuses and the T6 procs (Riposte, Breach, Concussion, Exploit Opening) never fire from an off-hand swing. Rep is split instead: each hand’s landed swing earns half the normal rep for its own weapon’s class, so the pair earns what one weapon would. Riposte is still primed by any block; only a main-hand swing spends the prime. Bulwark is a shield mechanic and cannot apply at all.
- Every stat outside combat — offline progress, the enemy-threat math behind auto-potion, the tooltip DPS, the character sheet — describes the main hand.
Shield-Strike damage
Shield Strike and Smite swing the shield, so the stored Min/Max damage is replaced by the shield’s own damage — built the same way, but normalised against the shield’s interval. The equipped weapon contributes nothing here.
normFlat = round( (flatDamage_shield + flatDamage_otherSlots) × shieldInterval / 1.0 ) + blessedShield
MinDamage = (shieldMin × (1 + enhancedDamage%_shield) + normFlat) × edOther × strMult
MaxDamage = (shieldMax × (1 + enhancedDamage%_shield) + normFlat) × edOther × strMult
The shield’s crafts count exactly as on a weapon: its smith-temper adds to enhancedDamage%_shield
(it lifts the shield’s dice, alongside its defense), and a witch-enchant scales the shield’s own
Enhanced Damage and Flat Damage affixes.
Blessed Shield sits outside the normalisation: its bonus is already a per-hit value (the skill
tooltip prints it directly), so the shield’s speed never scales it. It grows with your level squared:
(base + steps × perStep) × (min(level, 80) ÷ 80)² (BUFF_SHIELD_DAMAGE_FLAT_FULL_LEVEL,
BUFF_SHIELD_DAMAGE_FLAT_LEVEL_EXPONENT), so rank 20 adds +17.5 at level 40, +39 at level 60 and its
full +70 from level 80 on.
Hand requirements and the plain swing
Every skill node belongs to one class’s trees, and some nodes additionally declare the hands they were built for: a two-handed weapon (Cleave, Executioner), two weapons (Thousand Cuts, Ambidexterity), a shield (Shield Strike, Aegis, Smite, Judgement, Bastion, Shield Charge, Shield Wall) or a staff (Cataclysm, Hundred Palms, Guarded Stance).
The lock is soft and resolved at swing time, not at learn time:
- a gated main skill whose requirement is unmet resolves as the plain swing (Strike, 100% weapon damage) — your selection is remembered and comes back with the right hands. A shield that has broken counts as no shield. A hero who has learned no main skill yet swings the same way.
- a gated active or passive contributes nothing while unmet.
Actives have no cooldowns. Their pacing is the class resource (20 Rage, Valor or Enlightenment, or 40 for a capstone buff), the cast time, and the trigger rule — see Active Skills.
Status procs from the elemental pipeline are capped twice: 20% before speed normalisation and 50% after it and any multiplier (see Elements & Status).
Attack speed & swing interval
Gear IAS and Dexterity are summed and capped first, then the main-skill and buff/Flurry speed multipliers divide the interval one after another.
iasMod = clamp( attackSpeed% + 0.0005×DEX, −0.85, +0.85 ) # ATTACK_SPEED_MODIFIER_CAP
interval = clamp( weaponBaseInterval ÷ (1 + iasMod), 0.38, 3.6 ) # intermediate clamp
# then ÷ main-skill IAS mult, ÷ (1 + buff/Flurry IAS)
interval = clamp( interval, 0.45, 6.0 ) # FINAL clamp
Gear, Dexterity and buffs all divide the same way: +45% attack speed is 1.45× the swings per second, +70% is 1.70×.
The 0.38 … 3.6 clamp inside computePlayerStats is intermediate; the final interval is
re-clamped to 0.45 s (fastest) … 6.0 s (slowest) after the multiplicative speed layers.
Throughput ≈ damage ÷ interval.
No attack-speed breakpoints. Unlike many ARPGs, the swing interval is a continuous float, not a frame-quantised value — every point of IAS shortens the timer smoothly. There is nothing to “round up” to; the only discontinuities are the two clamp edges at 0.45 s and 6.0 s (
MIN_ATTACK_INTERVAL = 0.45insrc/data/balance/player.ts; the final clamps live inapplyPlayerDerivedStats,src/core/simulation-helpers.ts).
Outgoing physical damage at swing time
After the stored Min/Max stat is built, each swing (applyResolvedHit) does:
raw = randomInt(minDamage, maxDamage)
scaledRaw = round( raw × skillDamageMultiplier × outgoingPassiveMult )
outgoingPassiveMult = 1 + Wrath + Apex Predator + Rampage + … (clamped ≥ 0; every contributor is
listed under Damage passives that stack), evaluated
per-hit because rage and the kill stack move mid-fight; Apex Predator’s own condition
is the enemy’s rarity — rare, special or a boss — and is fixed for the fight. Wrath turns the live rage
pool (0–100) into a physical multiplier. The Sentinel’s Unrelenting (+1.5% → +25%) adds only to hits
with the weapon: a hit with the shield (the shield main skills, Bastion’s answer to a block, Shield
Charge, Taunt) leaves it out (shieldHit). These are the last physical multipliers and hit the
rolled value — they do not touch elemental riders.
Incoming mitigation (defending hero)
When the hero is the defender, mitigation runs in a strict order: block → capped percentage bucket → flat reduction. (Enemies get none of this — enemy hits are only the raw roll.)
Order of operations
- Block — with probability
blockChance(cap 0.75), the hit is multiplied by(1 − 0.75). A block removes 75% of the hit (blockDamageReduction); it is partial, never full negation. Applied to the raw hit before the percentage bucket. - Percentage-reduction bucket — a single shared bucket capped at 0.5
(
MAX_PHYSICAL_REDUCTION):totalReduction = min( 0.5, constitution% (0.0006 × CON) + alwaysOnPassives (Iron Resilience — pooled with CON% into damageReductionPercent) + reducedPhysicalAffix + conditionalPassives (e.g. Last Stand) + physReductionFromDR ) - Flat reduction — Constitution’s flat cut plus the Damage-Reduced-Flat affix is subtracted
last. It bypasses the 50% cap, but a single hit can never lose more than half its
remaining damage this way (
FLAT_REDUCTION_MAX_FRACTION_OF_HIT = 0.5):flat = floor( damageReducedFlatAffix + 0.0025 × CON × level ) out = out − min( flat, floor(out × 0.5) )
Every stage floors the result at 1 — a landed hit always deals at least 1.
Defense Rating → physical reduction
Beyond hit-avoidance, DR grants a level-invariant, capped physical reduction that folds into the
0.5 bucket above (physicalReductionFromDefenseRating):
physReductionFromDR = min( 0.25, 0.25 × DR / (DR + 2000) )
Constants: DR_MITIGATION_CAP = 0.25, DR_MITIGATION_HALF_VALUE = 2000 (at 2000 DR you get half
the cap, i.e. 12.5%). It is kept well below the 0.5 shared cap so DR can never fill the bucket
alone: DR plus Constitution reach about a third at level 95, and Reduced-Physical, Iron Resilience,
Vigilance and Last Stand keep headroom. A given DR is always worth the same reduction; armour never
goes obsolete as enemies grow. DR reduces physical only, never elemental.
| Defense Rating | Physical reduction from DR |
|---|---|
| 500 | 5.0% |
| 1000 | 8.3% |
| 2000 | 12.5% |
| 4000 | 16.7% |
| 8000 | 20.0% |
Constitution
Constitution has a dual defensive identity, present in both the % bucket and the flat step:
- % physical reduction:
0.0006 × CON(into the capped 0.5 bucket). - Flat per-hit reduction:
0.0025 × CON × level(level-scaled, applied after the cap, itself limited to half the hit).
It also grants +5 DR/point (which further feeds the DR→reduction curve above). Constitution grants no HP regen.
Worked example
Incoming physical hit scaledRaw = 200; defender at level 50 with CON 100, DR 2000, a
+5% Reduced-Physical affix, no Last Stand.
physReductionFromDR = 0.25 × 2000/(2000+2000) = 0.125constitution% = 0.0006 × 100 = 0.06totalReduction = min(0.5, 0.125 + 0.06 + 0.05) = 0.235flat = floor(0.0025 × 100 × 50) = 12
Not blocked: 200 × (1 − 0.235) = 153, then 153 − min(12, 76) = 141.
Blocked: 200 × (1 − 0.75) = 50 → 50 × (1 − 0.235) = 38 → 38 − min(12, 19) = 26.
Block multiplies before the cap, and flat is always last.
Worked examples
The sections above give each stage in isolation. These two swings walk the whole pipeline end-to-end so you can see the pieces compose.
Example A — outgoing physical swing (hero → enemy)
A weapon whose stored Min/Max damage is 100–150, using Crush at rank 20 (skill multiplier ×2.75 — its 1.40 base plus 27 effect steps × 5% Enhanced Damage per step), at full rage with Wrath at rank 20 (+35%), against an ordinary enemy:
- Roll to hit — assume it lands (AR/DR only decide whether, never how hard).
- Roll raw damage — the uniform roll comes up at the top:
raw = 150. - Skill multiplier — Crush:
150 × 2.75 = 412.5. - Outgoing passive multiplier —
1 + Wrath + Apex + Rampage = 1 + 0.35 = 1.35:scaledRaw = round(412.5 × 1.35) = 557. - Enemy mitigation — none. Physical damage to enemies ignores resistance, AR and DR, and enemies never block. The hit lands for 557.
Any flat elemental rider on the weapon resolves as a separate hit on the same swing — it skips steps 3–4 entirely (no skill multiplier, no Wrath) and is reduced only by the enemy’s matching resistance.
Example B — incoming physical hit (enemy → hero)
A raw enemy hit of 200 against a hero at level 60 with CON 200, DR 4000, a conditional Last Stand +15% active, and a +10% Reduced-Physical affix. Mitigation runs block → %-bucket → flat, each stage floored at 1:
First, the shared values:
physReductionFromDR = min(0.25, 0.25 × 4000/(4000+2000)) = 0.167constitution% = 0.0006 × 200 = 0.12reducedPhysicalAffix = 0.10conditionalPassive (Last Stand) = 0.15totalReduction = min(0.5, 0.167 + 0.12 + 0.10 + 0.15) = min(0.5, 0.537) = 0.50← hits the 0.5 capflat = floor(0.0025 × 200 × 60) = floor(30) = 30
Not blocked:
200 × (1 − 0.50) = 100 # capped %-bucket
100 − min(30, floor(100 × 0.5)=50) = 70 # flat, ≤50% of the hit
→ 70 damage
Blocked (blockDamageReduction = 0.75, applied before the bucket):
200 × (1 − 0.75) = 50 # block first
50 × (1 − 0.50) = 25 # capped %-bucket
25 − min(30, floor(25 × 0.5)=12) = 13 # flat clamped to half the remaining hit
→ 13 damage
Note two caps engaging at once: the % bucket is clamped to 0.5 even though DR + CON + the affix + Last Stand would sum to 0.537, and on the blocked path the 30-point flat cut is clamped down to 12 because a single hit can never lose more than half its remaining damage to flat reduction.
Resistances & elemental damage
Elemental damage (fire, cold, lightning, poison) is a separate hit that resolves against the target’s matching resistance only — never DR, block, or the physical bucket:
elementalDamage = rawElementalDamage × (1 − resistance)
resistance = max(0, baseResistance − resistDebuffs − targetResReduction) on the enemy side.
An enemy’s baseResistance follows its level only, the same in every region:
75% × (level − 1) / 199 (0% at level 1, 75% at level 200 and above, enemyBaseResistanceForLevel),
+3% for rares / +6% for specials, ±2% per enemy type and element, capped at 85%.
Resistance bounds (FORMULA_CONFIG):
| Who | Bounds |
|---|---|
| Hero | −75% … 75% (minResistance / maxResistance) per element |
| Enemy | 0 … 95% (maxEnemyResistance) |
The hero’s floor matters because the mitigation step is × (1 − resistance): a negative
resistance amplifies that element, so a rider against −75% lands for 175% of its roll. Only the
Region Resonance tax pushes a resistance below zero — −10% to all four elements per applied
Resonance level, subtracted from the uncapped gear + skill total and clamped once (so overcapped
resistance is headroom), and only on unlocked overworld combat tiles: dungeons and the Endless Delve
are exempt. The endgame boss fights take their own penalty the same way instead, 1% per fight level
above 100 (−50% echoed dungeons, −75% Brood Matriarch, −100% echoed Dark Citadel finale). See Elements & Status Effects.
Debuff/penetration effectiveness is capped at 90% (MAX_DEBUFF_EFFECTIVENESS). Flat elemental
affixes are speed-normalised per hit exactly like flat physical damage. Elemental hits can also
inflict status effects (values from the Elements help section):
| Element | Status | Proc | Duration | Effect |
|---|---|---|---|---|
| Fire | Burn (DoT) | 10%/hit | 6 s | 25% of the hit’s fire damage over the duration (ticks every 0.5 s) |
| Cold | Freeze | 10%/hit, speed-normalised | 2 s | −30% enemy attack speed |
| Lightning | Stun | 10%/hit, speed-normalised | 2 s | Pauses the enemy attack timer |
| Poison | Poison (DoT) | Always (every hit) | 6 s | Flat poison value over 6 s, reduced by Poison resistance |
DoT instances stack (a new burn does not refresh an old one). Physical bleeds (Rend) are unresisted.
Block
- Chance cap: 75% (
maxBlockChance = 0.75) — the maximum block chance regardless of gear or skills. - Needs a shield. With no shield in either hand your block chance is 0%, whatever gear, passives (Shield Mastery) or buffs (Brace) add. The one exception is the Staves T6 Parry: the staff blocks on its own, and then every block source counts as if a shield were worn. The base block value and the Block Chance affix roll on shields only; the block chance on Oak Touch (gloves) and Pendant of the Sentinel (amulet) works only next to a shield or Parry.
- Damage reduction: 75% (
blockDamageReduction = 0.75) — a blocked hit loses 75% of its damage, applied before the percentage bucket. - Side effects: thorns deal 150% on a blocked hit, the Rage-on-Block affix generates rage on a successful block, the Heal-on-Block shield affix heals its share of the damage the block prevented (the log’s “(N blocked)”), and the Reflect-on-Block shield affix sends its share of that same amount back at the attacker (see Thorns).
Note: the block chance cap and the block damage reduction are both 75% — two different numbers that happen to share a value: one is how often you block, the other how much a block takes off the hit.
Crushing Blow
Some weapon affixes and the Crush main skill grant a Crushing Blow chance. On proc, it deals a fraction of the enemy’s current HP as extra damage.
- Chance is speed-normalised by the weapon’s intrinsic interval (
× weaponBaseInterval / 1.0), so fast weapons don’t proc more per second than slow ones. Gear chance + skill (Crush) chance are both normalised, then capped at 0.75 (MAX_CRUSHING_BLOW_CHANCE). - If a slow weapon’s scaled chance exceeds the cap, the clipped share overflows into magnitude (a bigger crush, up to 2×), preserving expected value per second.
- Block does not reduce Crushing Blow (it is applied with block chance 0).
Fraction of current HP by target rarity:
| Target | Fraction of current HP |
|---|---|
| Common | 12% (CRUSHING_BLOW_HEALTH_FACTOR_BY_RARITY.common) |
| Rare | 9% |
| Special | 6% |
| Boss: regional and echoed dungeon bosses (Echoed Theron included), hunt marks (the Brood Matriarch included) | 4% (CRUSHING_BLOW_BOSS_HEALTH_FACTOR) |
Higher-rarity foes take a smaller chunk so high-HP bosses aren’t trivialised, while it melts trash.
Life Leech
A fraction of your post-mitigation physical damage is returned as healing
(applyLifeLeechFromHit): heal = round(mitigatedDamage × leechFactor × enemyShare).
-
Enemy share (
LIFE_LEECH_FACTOR_BY_ENEMY): the full heal against a common, ¾ against a rare, ½ against a special, ⅓ against a boss. Bosses are the regional and echoed dungeon bosses (Echoed Theron included) and hunt marks (the Brood Matriarch included); every other enemy counts by its rarity, her brood and the Delve’s wardens too. Siphon’s elemental leech uses the same shares. Skill heals that aren’t leech (Grim Harvest’s own heal, Mending Strike, Renewal, potions) are not affected. -
Gear leech cap: 90% (
MAX_LIFE_LEECH). -
Total leech cap: 95% (
MAX_TOTAL_LIFE_LEECH) — gear + Bloodlust + Reaver + buffs; the runtime clamp is 0.95. -
Leech triggers only from the physical weapon hit — never from elemental, crushing blow, or thorns.
HP regeneration
Out of combat, and in the respawn gap between one enemy dying and the next appearing, the hero heals
passively at HERO_OUT_OF_COMBAT_REGEN_RATE = 1/60 of max HP per second, so a full bar takes 60
seconds (≈1.67% per second). It does not tick while an enemy is standing in front of you.
Constitution grants no regeneration. It pays out in Defense Rating, percentage physical reduction, and the level-scaled flat reduction above. The separate Life Regeneration affix is the one that heals a flat amount of HP per second at all times, in combat included.
Thorns
Two thorn types stack, both dealing physical damage that can kill:
- Flat thorns — a fixed value returned to the attacker on every hit you take.
- Reflect thorns — a percentage of the damage that actually landed on you (after block and after the physical-reduction bucket) returned.
Both deal 150% on a blocked hit, and a blocked hit also flips reflect’s base from the landed
amount to the pre-block, pre-reduction raw hit — so reflect = round(rawHit × 1.5 × reflect%).
A block adds a third, separate reflect before those two: the damage the block prevented times the
Reflect on Block shield affix plus Unyielding’s share while that buff is up —
round(blocked × (affix% + Unyielding%)), one hit for both. Thorns then land on top as usual.
Rage (feeds the outgoing multiplier)
Rage powers active skills and drives Wrath. It caps at 100 (BASE_MAX_RAGE, raised by the
unique-only Maximum Rage affix), decays 5/sec out of combat
(RAGE_DECAY_PER_SECOND, a full 100-rage bar in 20 s), and each active-skill cast costs 20
(ACTIVE_SKILL_RAGE_COST) — 40 for the four capstone buffs (CAPSTONE_BUFF_RAGE_COST), which
are the only per-skill override. Plain attacks grant 10 rage/swing, speed-normalised by the weapon’s
own interval (RAGE_REFERENCE_INTERVAL = 1.0) so rage-per-second stays neutral across weapon speeds.
The weapon-only Rage On Hit affix adds its flat value to that swing’s nominal rage and takes the
same interval scaling; on a multi-hit skill (Zeal) it is split across the hits, so a full swing grants it
once. It is never granted by a shield strike (no weapon is swung) or by a skill cast.
A shield skill with no usable shield falls back to a real weapon swing, and that one does grant it.
Reduced-Rage-Decay affixes cut decay by up to 95% (MAX_RAGE_DECAY_REDUCTION).
The Sentinel’s Valor stops fading at its floor, 33% of max Valor (VALOR_FLOOR_SHARE; 33 of 100),
and below the floor it regenerates 5/sec out of combat and in the respawn wait
(VALOR_FLOOR_REGEN_PER_SECOND), never in a fight. Reduced Valor Decay does not change the regen.
Order-of-operations gotchas
- Enhanced-Damage % is scoped by the slot it sits on. On a weapon it scales that weapon’s dice only, folded in before flat-damage and before Strength; on a shield it scales only the shield’s Shield-Strike dice the same way. From any other slot it is a character-level multiplier applied to dice plus the normalised flat add, so there it does scale your flat damage. It never touches elemental affixes.
- Flat-Damage is speed-normalised, then added inside the Strength multiplier, so STR scales your flat adds too — and the skill multiplier and Wrath (applied later, at swing time) scale them again.
- Skill multiplier + Wrath / Apex / Rampage hit the rolled value multiplicatively, after the whole stat panel is built. They are the last physical multipliers.
- Elemental damage bypasses the skill multiplier and Wrath entirely; it is flat-per-hit and is the only player damage reduced by enemy resistance. Physical to enemies ignores resistance, AR and DR, and enemies never block.
- AR / DR are hit-chance only. Stacking DR-reduction on an enemy (Sunder/Shatter) raises your hit-rate; it does not amplify each hit.
- Incoming mitigation order: block (−75%) → %-reduction bucket (DR-curve ≤25% + Reduced-Phys + CON + Last Stand, total capped at 50%) → flat reduction (≤50% of the hit). Order means block multiplies before the cap, and flat is always last.
- The Hero-tab damage number is optimistic: it adds elemental as raw (no resistance), assumes every swing lands, and omits crushing blow, ignite/bleed/poison DoTs, procs, and conditional passives.
Caps & constants quick reference
| Constant | Value | Source |
|---|---|---|
| Hit chance at AR = DR | 0.70 | FORMULA_CONFIG.hitChanceAtEqualRatings |
| Hit chance curve steepness | 1.1 | hitChanceCurveSteepness |
| Hit chance floor / ceiling | 0.05 / 0.95 | minHitChance / maxHitChance |
| Flat hit-chance cap (gear / total) | +30% / +50% | MAX_HIT_CHANCE_FLAT_BONUS |
| Level-difference hit shift, enemy attacks and the hero out-levelling (per level / cap) | ±1% / ±10% | levelDeltaHitChancePerLevel / levelDeltaHitChanceMaxShift |
| Hero vs a higher enemy: share of the hit chance lost (per level / cap) | 1% / 10% | playerLevelPenaltySharePerLevel / playerLevelPenaltyMaxShare |
| Attack-speed modifier cap | ±0.85 | ATTACK_SPEED_MODIFIER_CAP |
| Attack interval final clamp | 0.45 – 6.0 s | MIN_ATTACK_INTERVAL + flow |
| Block chance cap | 0.75 | maxBlockChance |
| Block damage reduction | 0.75 | blockDamageReduction |
| Shared physical-reduction cap | 0.50 | MAX_PHYSICAL_REDUCTION |
| DR reduction cap / half-value | 0.25 / 2000 | DR_MITIGATION_CAP / DR_MITIGATION_HALF_VALUE |
| Flat reduction max per hit | 0.50 of hit | FLAT_REDUCTION_MAX_FRACTION_OF_HIT |
| Resistance cap (hero / enemy) | 0.75 / 0.95 | maxResistance / maxEnemyResistance |
| Resistance floor (hero) | −0.75 | minResistance |
| Resonance resistance tax | −0.10 per applied level | RESONANCE_RES_MALUS_PER_LEVEL |
| Debuff effectiveness cap | 0.90 | MAX_DEBUFF_EFFECTIVENESS |
| Crushing Blow chance cap | 0.75 | MAX_CRUSHING_BLOW_CHANCE |
| Crushing Blow HP% (common/rare/special/boss) | 12 / 9 / 6 / 4% | crushing-blow factors |
| Life leech cap (gear / total) | 0.90 / 0.95 | MAX_LIFE_LEECH / MAX_TOTAL_LIFE_LEECH |
| Leech share vs common / rare / special / boss | 1 / ¾ / ½ / ⅓ | LIFE_LEECH_FACTOR_BY_ENEMY |
| Run-speed cap / respawn factor | 0.80 / 1.65 | maxRunSpeed / runSpeedRespawnFactor |
| Respawn floor | 0.32 s | minRespawnSeconds |
| Min stat multiplier floor | 0.20 | MIN_STAT_MULTIPLIER |
| Rage cap / decay / cast cost | 100 + Maximum Rage / 5·s⁻¹ / 20 | BASE_MAX_RAGE / RAGE_* |
| Valor floor / regen below it | 33% of max Valor / 5·s⁻¹ | VALOR_FLOOR_SHARE / VALOR_FLOOR_REGEN_PER_SECOND |
If a number here ever contradicts what the game actually does, the game wins — this is generated from the engine source.