Command Palette

Search for a command to run...

Anmelden

Combat Formulas

The DamageCalc multiplier chain: how a cast turns into a damage number, in order.

4 Min. Lesezeit

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.

Abbildung 17
Damage Calculation Pipeline
Expands the deal_damage step of process_cast: DamageCalc::calculate in order — weapon roll, raw (base plus coefficient times attack power), crit, versatility, armor mitigation, damage multiplier, mastery.

The chain, step by step

DamageCalc is a plain struct of inputs. Everything the formula needs is a field on it:

rust
#[derive(Debug, Clone)]
pub struct DamageCalc {
    pub base: f64,
    pub coefficient: f64,
    pub attack_power: f64,
    pub crit_chance: f64,
    pub crit_multiplier: f64,
    pub versatility: f64,
    pub target_armor: f64,
    pub armor_k: f64,
    pub damage_multiplier: f64,
    pub mastery_mult: f64,
    pub weapon_min: f64,
    pub weapon_max: f64,
    pub weapon_multiplier: f64,
    pub school: DamageSchool,
}

calculate(rng) consumes them in this exact order, drawing from the RNG at most twice:

  1. Weapon roll. weapon_roll = weapon_min + rng()*(weapon_max - weapon_min), but only when weapon_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.
  2. 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.
  3. Crit. is_crit = rng() < crit_chance.clamp(0,1); the factor is crit_multiplier on a crit, else 1.0. The default crit multiplier is 2.0.
  4. Crit applied. after_crit = raw * crit_factor.
  5. Versatility. after_vers = after_crit * (1 + versatility/100).
  6. Armor. after_armor = after_vers * armor_mitigation(target_armor, armor_k).
  7. 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:

rust
pub fn armor_mitigation(armor: f64, armor_k: f64) -> f64 {
    if armor <= 0.0 {
        return 1.0;
    }
    let mitigated = armor / (armor + armor_k);
    (1.0 - mitigated.clamp(0.0, 1.0)).max(0.0)
}

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:

Tabelle 9
BuffEffect Variants
VariantEffect
Haste(f64)additive haste percent
Crit(f64)additive crit percent
Mastery(f64)additive mastery percent
Versatility(f64)additive versatility percent
PrimaryStat(f64)additive primary stat
DamageMult(f64)flat damage multiplier on all damage
DamageMultSchool(f64, DamageSchool)damage multiplier scoped to one school
DamageMultSpells(f64, &[u32])damage multiplier scoped to a spell list
Cleave(f64, u8)extra cleave hits at a fraction of damage
DamageMultStacking{initial, per_stack}multiplier that grows per stack
The ten BuffEffect variants an aura can contribute (Haste, Crit, Mastery, Versatility, PrimaryStat, DamageMult, DamageMultSchool, DamageMultSpells, Cleave, DamageMultStacking) summed by accumulate_buffs.

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:

rust
#[inline]
pub fn scaled(self, stacks: f64) -> Self {
    match self {
        Self::Haste(v) => Self::Haste(v * stacks),
        Self::Crit(v) => Self::Crit(v * stacks),
        Self::Mastery(v) => Self::Mastery(v * stacks),
        Self::Versatility(v) => Self::Versatility(v * stacks),
        Self::PrimaryStat(v) => Self::PrimaryStat(v * stacks),
        Self::DamageMult(v) => Self::DamageMult(v.powf(stacks)),
        Self::DamageMultSchool(v, school) => Self::DamageMultSchool(v.powf(stacks), school),
        Self::DamageMultSpells(v, spells) => Self::DamageMultSpells(v.powf(stacks), spells),
        Self::Cleave(frac, targets) => Self::Cleave(frac, targets),
        Self::DamageMultStacking { initial, per_stack } => {
            Self::DamageMult(1.0 + initial + per_stack * (stacks - 1.0))
        }
    }
}

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.

Nächste Schritte