Combat Formulas
The DamageCalc multiplier chain: how a cast turns into a damage number, in order.
Every damage number in the engine comes out of one function: DamageCalc::calculate. It takes the spell's base amount and a handful of stat inputs and walks them through a fixed multiplier chain: weapon roll, raw damage, crit, versatility, armor, then the situational multipliers. The order matters, and the engine commits to one.
This is the cast pipeline's damage step, zoomed in. The figure below expands the SimEngine.run box of the simulation pipeline (the Zoom-1 sim-pipeline figure); concretely it is what process_cast reaches when it hits the damage branch.
The chain, step by step
DamageCalc is a plain struct of inputs. Everything the formula needs is a field on it:
calculate(rng) consumes them in this exact order, drawing from the RNG at most twice:
- Weapon roll.
weapon_roll = weapon_min + rng()*(weapon_max - weapon_min), but only whenweapon_max > 0. For a spell with no weapon component the roll is skipped entirely, including the RNG call, so the first random draw belongs to crit instead. - Raw.
raw = base + weapon_roll*weapon_multiplier + coefficient*attack_power. This folds the flat base, the rolled weapon contribution, and the attack-power-scaled portion into one number before any multiplier touches it. - Crit.
is_crit = rng() < crit_chance.clamp(0,1); the factor iscrit_multiplieron a crit, else1.0. The default crit multiplier is2.0. - Crit applied.
after_crit = raw * crit_factor. - Versatility.
after_vers = after_crit * (1 + versatility/100). - Armor.
after_armor = after_vers * armor_mitigation(target_armor, armor_k). - Multipliers.
final_amount = (after_armor * damage_multiplier * mastery_mult).max(0), where the floor at zero is the only clamp on the result.
The output is a DamageResult { raw, is_crit, final_amount }. Note that crit, versatility, and the late multipliers are all multiplicative against the same raw; there is no additive bucketing here. That is a simplification, since real WoW splits modifiers into additive and multiplicative buckets, but for the spells the engine models it keeps the formula auditable.
Armor mitigation
Armor only applies to physical damage, and it uses the standard ratio:
armor_mitigation(armor, K) = 1 - armor / (armor + K)
In code that ratio is clamped to [0, 1] and short-circuits to 1.0, meaning no mitigation, when armor is non-positive:
The constant K is the armor coefficient for the target's level, supplied as armor_k = game_data.armor_k(), which is armor_constant * armor_constant_mod resolved from the expected-stats table. Non-physical schools skip the armor term: prepare_damage_setup only reads target armor for physical hits.
Where the inputs come from
DamageCalc is assembled in prepare_damage_setup, which reads the player's crit and versatility, the target armor for physical hits, the spell's base points, and then two aggregates: the buff totals and the mastery multiplier.
Buff totals
Active auras contribute their stat and damage modifiers through accumulate_buffs, which sums every active aura's BuffEffects, each scaled by its current stack count, into a single BuffTotals. The BuffEffect enum is the vocabulary of what an aura can change:
BuffEffect is #[non_exhaustive], and its scaled(stacks) method is where the stack count actually applies. Additive stats scale linearly; the DamageMult* family compounds via powf(stacks) instead:
BuffTotals also keeps a per-school multiplier array school_damage_mult: [f64; 8] so school-scoped buffs land on the right hits.
Mastery
Mastery is not one formula. It is per-spec, so the engine carries a mastery_category on CombatState and applies it in two shapes:
MasteryCategory::UniformMult: mastery multiplies all of the spec's damage equally.MasteryCategory::SchoolMult: mastery multiplies only a specific school.
The category is set at build time from the manifest (mastery_category(params.mastery.category)), and the resolved mastery percent comes from the stat recompute, scaled by the spec's mastery coefficient. This is deliberately coarse: most specs in WoW have a bespoke mastery, and the engine only models the two that fit a multiplier. Specs whose mastery does something structurally different (e.g. adds a proc, changes resource generation) need a hook, not a category.
Dealing the hit
DamageCalc::calculate is the arithmetic; deal_damage is the orchestration around it. For a single-target cast it builds one DamageCalc, calls .calculate(rng), adds the result to state.total_damage, emits a DamageEvent to the telemetry sink, and fires impact procs via fire_impact_procs. When the spell is flagged AoE it loops run_single_hit per target with a per-target multiplier (split, chain, or square-root falloff), and a single-target physical hit can still cleave extra hits when a Cleave buff is active.
Snapshot DoTs are the exception: deal_damage_with_snapshot feeds DamageCalc the AP/SP/crit/vers/mastery captured when the DoT was applied instead of the live stats. That mechanism is the subject of the auras page.
Next steps
