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
- 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 in-game
Damage Flow Chart.
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 − Last Stand). 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.895 | 92.4% |
| 50 | 100 | −0.693 | −0.642 | 28.3% |
| 25 | 100 | −1.386 | −0.895 | 11.8% |
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 + Keen Eye + tavern drinks) is capped
at +50% in aggregate before the clamp.
Level-difference shift
The level gap between attacker and defender adds a further flat shift through the same
applyFlatHitChanceBonus path (levelDeltaHitChanceShift), so it obeys the same [0.05, 0.95]
clamp. It applies symmetrically — to the hero attacking and to enemies attacking the hero —
and at every level:
shift = clamp( (attackerLevel − defenderLevel) × 0.01, −0.10, +0.10 )
An attacker gains +1% hit chance per level it out-levels the defender, loses 1%/level when
under-levelled, and the shift saturates at ±10% (±10 levels). It is deliberately ungated: in
the Endless Delve, where enemy level climbs past the hero cap of 99, this settles into a constant
−10% for the hero (and +10% for the enemy) rather than a runaway penalty. Constants
(FORMULA_CONFIG): levelDeltaHitChancePerLevel = 0.01, levelDeltaHitChanceMaxShift = 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 +2 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.1% physical reduction (CON_PHYS_REDUCTION_PER_POINT = 0.001), and flat per-hit reduction of 0.0025 × level (CON_FLAT_REDUCTION_PER_POINT_PER_LEVEL) |
Per level (independent of attributes): +3 Max HP, +2 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 + 2×STR + 2×level )
AR × (1 + gearAR% + trainingAR% + drinkAR% + Σ buffAR%) ← one pooled multiplier
AR × (1 + S(Strike rank) × 2.5%) ← main-skill perk, separate
Note the archetype-rebalance coefficients: DEX gives 4 AR/pt (not 5) and STR gives 2 AR/pt,
so Strength now buys accuracy and DEX no longer monopolises it. 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. S(rank) is the skill’s
accumulated effect steps — 28 at rank 20, so a maxed Strike is AR × 1.70.
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 (archetype passive included). From every other slot (ring, amulet, armour) they are character-wide and add on top of whichever damage source you currently swing.
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 — archetype weapon-class multiplier, main-passive enhanced-damage, 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.
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
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.
Attack speed & swing interval
Gear IAS and Dexterity are summed and capped first, then skill / passive / buff 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 + main-passive IAS), ÷ (1 + buff/Flurry IAS)
interval = clamp( interval, 0.45, 6.0 ) # FINAL clamp
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 insrc/core/simulation-helpers.ts:1480-1656).
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 − Last Stand (clamped ≥ 0), evaluated
per-hit because rage, defender HP and player HP all move mid-fight. Wrath turns the live rage
pool (0–100) into a physical multiplier. 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.60). A block removes 60% 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.001 × CON) + 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 0.5% 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.40, 0.40 × DR / (DR + 2000) )
Constants: DR_MITIGATION_CAP = 0.40, DR_MITIGATION_HALF_VALUE = 2000 (at 2000 DR you get half
the cap, i.e. 20%). It is kept below the 0.5 shared cap so DR can never fill the bucket alone —
Constitution, Reduced-Physical 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 | 8.0% |
| 1000 | 13.3% |
| 2000 | 20.0% |
| 4000 | 26.7% |
| 8000 | 32.0% |
Constitution
Constitution has a dual defensive identity, present in both the % bucket and the flat step:
- % physical reduction:
0.001 × 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.40 × 2000/(2000+2000) = 0.20constitution% = 0.001 × 100 = 0.10totalReduction = min(0.5, 0.20 + 0.10 + 0.05) = 0.35flat = floor(0.0025 × 100 × 50) = 12
Not blocked: 200 × (1 − 0.35) = 130, then 130 − min(12, 65) = 118.
Blocked: 200 × (1 − 0.60) = 80 → 80 × (1 − 0.35) = 52 → 52 − min(12, 26) = 40.
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 (skill multiplier ×1.50), at full rage (Wrath = +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 × 1.50 = 225. - Outgoing passive multiplier —
1 + Wrath + Apex + Rampage − Last Stand = 1 + 0.35 = 1.35:scaledRaw = round(225 × 1.35) = round(303.75) = 304. - Enemy mitigation — none. Physical damage to enemies ignores resistance, AR and DR, and enemies never block. The hit lands for 304.
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 no Reduced-Physical affix. Mitigation runs block → %-bucket → flat, each stage floored at 1:
First, the shared values:
physReductionFromDR = min(0.40, 0.40 × 4000/(4000+2000)) = 0.267constitution% = 0.001 × 200 = 0.20conditionalPassive (Last Stand) = 0.15totalReduction = min(0.5, 0.267 + 0.20 + 0.15) = min(0.5, 0.617) = 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.60, applied before the bucket):
200 × (1 − 0.60) = 80 # block first
80 × (1 − 0.50) = 40 # capped %-bucket
40 − min(30, floor(40 × 0.5)=20) = 20 # flat clamped to half the remaining hit
→ 20 damage
Note two caps engaging at once: the % bucket is clamped to 0.5 even though DR + CON + Last Stand would sum to 0.617, and on the blocked path the 30-point flat cut is clamped down to 20 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.
Resistance caps (FORMULA_CONFIG):
| Who | Cap |
|---|---|
| Hero | 75% (maxResistance) per element |
| Enemy | 95% (maxEnemyResistance) |
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. Requires a shield. - Damage reduction: 60% (
blockDamageReduction = 0.6) — a blocked hit loses 60% of its damage, applied before the percentage bucket. - Side effects: thorns deal 150% on a blocked hit, and the Rage-on-Block affix generates rage on a successful block.
Note: the block chance cap is 75% and the block damage reduction is 60% — two different numbers that are easy to conflate.
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% |
| Dungeon / echo boss | 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).
- 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.
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 incoming raw damage returned.
Both deal 150% on a blocked hit (reflect uses the pre-block raw × 1.5).
Rage (feeds the outgoing multiplier)
Rage powers active skills and drives Wrath. It caps at 100, 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). 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.
Reduced-Rage-Decay affixes cut decay by up to 95% (MAX_RAGE_DECAY_REDUCTION).
Order-of-operations gotchas
- Enhanced-Damage % scales weapon base only — applied before flat-damage and before Strength. It does nothing for flat or 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 (−60%) → %-reduction bucket (DR-curve ≤40% + 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 (per level / cap) | ±1% / ±10% | levelDeltaHitChancePerLevel / levelDeltaHitChanceMaxShift |
| 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.60 | blockDamageReduction |
| Shared physical-reduction cap | 0.50 | MAX_PHYSICAL_REDUCTION |
| DR reduction cap / half-value | 0.40 / 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 |
| 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 |
| Min stat multiplier floor | 0.20 | MIN_STAT_MULTIPLIER |
| Rage cap / decay / cast cost | 100 / 5·s⁻¹ / 20 | RAGE_* |
If a number here ever contradicts what the game actually does, the game wins — this is generated from the engine source.