Skip to main content

wowlab_engine_domain/dbc/
mod.rs

1//! DBC effect/aura constants and pure classifiers shared by data resolution.
2
3mod decode;
4mod effect_lookup;
5mod game_data;
6mod live_modifier;
7mod passives;
8mod semantics;
9mod spell_props;
10#[cfg(any(test, feature = "test-support"))]
11pub mod test_support;
12
13mod weapon;
14
15use wowlab_engine_ports::WeaponEnchantStat;
16#[cfg(test)]
17use wowlab_types::data::SpellResources;
18use wowlab_types::data::{SpellDataFlat, SpellEffect};
19
20#[rustfmt::skip]
21pub use decode::{
22    HealthCosts, PowerModifiers, cost_entry_index, health_cost, primary_cost, primary_cost_pct,
23    primary_max_cost_pct, primary_optional_cost, primary_optional_cost_pct,
24    primary_power_burn_cost, primary_resource_gain, scale_power_amount, secondary_cost,
25    secondary_optional_cost, secondary_resource_gain,
26};
27pub use effect_lookup::{EffectLookup, ResolvedGameDataEffectExt};
28#[rustfmt::skip]
29pub use game_data::{
30    CR_MULTIPLIER_BASE_ILVL, CooldownMutation, EXPANSION_LEVEL_SCALING,
31    SCALING_CLASS_ARMOR_MULTIPLIED, SCALING_CLASS_DAMAGE_REPLACE_STAT,
32    SCALING_CLASS_DAMAGE_SECONDARY, SCALING_CLASS_PRIMARY_STAT, TriggerCooldownTarget,
33    TriggerOperation, TriggerProgram,
34    TriggerResolveError, TriggeredDamage, damage_def, damage_school, enchantment_stat_value,
35    equipped_weapon_matches, is_physical, item_effect_scaling_budget, item_scaled_effect_value,
36    resolve_effect_program, resolve_trigger_program, resolve_triggered_damage,
37    spell_scaling_budget,
38};
39pub use live_modifier::{LiveDamageModifierKind, live_damage_modifier};
40pub use spell_props::spell_props_from;
41#[rustfmt::skip]
42pub use passives::{
43    CritChanceScaledAxis, CritChanceScaledCritDamage,
44    ModifiedPowerCoefficients, POWER_REGEN_PERIOD_SECONDS, PassiveDamageMods,
45    PassiveModifierComponents, PassivePetStatMods, PassiveProcChanceMods, PassiveQuery,
46    PassiveStatMods, RankedPassive, ResourcePassive,
47    apply_passive_cooldowns, class_passive_spell_ids, crit_chance_scaled_crit_damage,
48    crit_chance_scaled_spell_percent, is_damage_modifier_passive,
49    is_stat_modifier_passive, is_target_damage_taken_debuff, modified_effect_amplitude,
50    modified_effect_base_points,
51    modified_effect_coefficient, modified_power_coefficients, passive_auto_attack_mult,
52    passive_cast_time_ms, passive_chain_multiplier, passive_chain_target_range,
53    passive_chain_targets, passive_cooldown_ms, passive_damage_mods, passive_doses,
54    passive_duration_ms, passive_gcd_ms, passive_guardian_damage_mult, passive_hasted_cooldown,
55    passive_hasted_gcd, passive_hostile_max_range, passive_mana_regen_multiplier,
56    passive_max_charges, passive_max_stacks, passive_max_targets, passive_period_ms,
57    passive_modifier_components, passive_pet_damage_mult, passive_pet_stat_mods,
58    passive_proc_chance_mods, passive_radius, passive_resource_cost, passive_stat_mods,
59    passive_target_armor_mult, talent_resource_passives,
60};
61#[rustfmt::skip]
62pub use semantics::{
63    AURA_SUBTYPE_SEMANTICS, AttributeMask, AuraState, AuraSubtypeKind, AuraSubtypeSemantic,
64    CombatRating,
65    CombatRatingMask,
66    DefenseType,
67    CrowdControlKind, CrowdControlSemantic, DiminishingGroup, crowd_control_semantic,
68    DifficultyId, EffectSemanticCoverage, EnchantEffectKind, EnchantTriggerSemantic, IMPLICIT_TARGET_SEMANTICS, ImplicitTargetCheck,
69    ImplicitTargetDirection, ImplicitTargetObject, ImplicitTargetReference,
70    ImplicitTargetSelection, ImplicitTargetSemantic, ImplicitTargetSelector, InventoryType, ItemClass, ItemEffectTrigger,
71    GemSubclass, DungeonDifficultyFlags, ItemBonusListGroupEntryFlags, ItemBonusType, ItemContext,
72    RaidDifficultyFlags,
73    MODIFIER_PROPERTY_SEMANTICS, ModifierFilter, ModifierOperation, ModifierOperations,
74    DispelType, Mechanic, MechanicMask, ModifierPropertyKind, ModifierPropertySemantic, PetStatKind, SPELL_ATTRIBUTE_SEMANTICS,
75    SPELL_CAST_TARGET_FLAG_SEMANTICS,
76    SPELL_EFFECT_SEMANTICS, CONSUMED_PROC_ATTRIBUTES, PROC_ATTRIBUTE_SEMANTICS, ProcAttributeKind,
77    PowerType, ProcAttributeSemantic, ProcAttributes, ProcTypeMask, RatingMultiplierMask,
78    ResolvedGameDataSemanticExt, SemanticSupport, ShapeshiftForm, SpellAttributeKind,
79    SpellAttributeSemantic,
80    SpellAuraInterruptFlags, SpellCastTargetFlagSemantic, SpellInterruptFlags,
81    SpellCastTargetFlags, SpellEffectAttributes, SpellEffectImplicitTarget, SpellEffectKind,
82    SpellEffectSemantic, SpellEffectSemanticExt, SpellEffectTargetSemantic, SpellSchoolMask,
83    SpellShapeshiftFormFlags, SpellTargetMasks, StatModifierKind, TraitConditionType, TraitCurrencyFlags,
84    TraitEffectOperation, TraitNodeType, TraitTreeIndex, WeaponSubclass, ArmorSubclass,
85    ConsumableSubclass,
86    aura_subtype_is, aura_subtype_is_any, aura_subtype_kind, aura_subtype_modifier,
87    aura_subtype_raw, aura_subtype_semantic, effect_semantic_coverage,
88    enchant_effect_is, enchant_effect_kind, enchant_trigger_semantic, implicit_target_semantic, is_primary_stat_misc,
89    modifier_combination_supported, modifier_property_for_effect,
90    modifier_property_is, modifier_property_is_any,
91    modifier_property_kind, modifier_property_raw, modifier_property_semantic, pet_stat_kind,
92    proc_attribute_semantic,
93    item_matches_consumable_query, resolve_item_budget, rppm_crit_scaled, rppm_haste_scaled, spell_attribute_is, spell_attribute_is_any,
94    spell_attribute_semantic, spell_cast_target_flag_semantic, spell_effect_is,
95    spell_effect_is_any, spell_effect_kind, spell_effect_raw, spell_effect_semantic,
96    spell_effect_target_semantic, spell_target_masks, stance_mask_allows,
97};
98#[rustfmt::skip]
99pub use weapon::{WeaponClass, classify_weapon, normalized_speed_s, resolve_weapon_stats};
100
101/// Spell-power/attack-power coefficient bundle extracted from a damage effect.
102#[derive(Clone, Copy, Debug, Default)]
103pub struct TriggerPayload {
104    pub sp_coef: f64,
105    pub ap_coef: f64,
106    pub base_points: f64,
107    pub amplitude: f64,
108    pub coefficient: f64,
109    pub power_coefficient_mastery_add: f64,
110}
111
112impl TriggerPayload {
113    #[must_use]
114    pub fn from_effect(effect: &SpellEffect) -> Self {
115        Self {
116            sp_coef: effect.bonus_coefficient,
117            ap_coef: effect.bonus_coefficient_from_ap,
118            base_points: effect.base_points,
119            amplitude: f64::from(effect.amplitude),
120            coefficient: f64::from(effect.coefficient),
121            power_coefficient_mastery_add: 0.0,
122        }
123    }
124
125    /// Whether neither coefficient carries a value (DBC zero-as-absent).
126    #[must_use]
127    pub fn has_no_coef(&self) -> bool {
128        // #t(rust_floating_point_eq) presence check vs DBC zero-as-absent.
129        self.sp_coef == 0.0 && self.ap_coef == 0.0
130    }
131}
132
133/// True if an effect forwards to its `trigger_spell` for the real damage payload.
134#[must_use]
135pub fn effect_redirects_to_trigger_spell(effect: &SpellEffect) -> bool {
136    if effect.trigger_spell == 0 {
137        return false;
138    }
139
140    match spell_effect_kind(effect.effect) {
141        Some(
142            SpellEffectKind::TriggerMissile
143            | SpellEffectKind::TriggerSpell
144            | SpellEffectKind::TriggerSpellWithValue
145            | SpellEffectKind::TriggerSpell2,
146        ) => true,
147        Some(SpellEffectKind::ApplyAura) => effect.aura_is(AuraSubtypeKind::PeriodicTriggerSpell),
148        _ => false,
149    }
150}
151
152/// Extract the damage coefficient payload from a spell, preferring direct school damage over the first periodic-damage aura.
153#[must_use]
154pub fn extract_damage_payload(spell: &SpellDataFlat) -> Option<TriggerPayload> {
155    let mut periodic: Option<TriggerPayload> = None;
156
157    for e in &spell.effects {
158        // #t(rust_floating_point_eq) presence check: DBC stores zero for "absent", non-zero for any real coefficient.
159        let has_coef = e.bonus_coefficient != 0.0 || e.bonus_coefficient_from_ap != 0.0;
160
161        if !has_coef {
162            continue;
163        }
164
165        let payload = TriggerPayload::from_effect(e);
166        let effect_kind = spell_effect_kind(e.effect);
167
168        if matches!(
169            effect_kind,
170            Some(SpellEffectKind::SchoolDamage | SpellEffectKind::HealthLeech)
171        ) {
172            return Some(payload);
173        }
174
175        if effect_kind == Some(SpellEffectKind::ApplyAura)
176            && e.aura_is_any(&[
177                AuraSubtypeKind::PeriodicDamage,
178                AuraSubtypeKind::PeriodicLeech,
179            ])
180            && periodic.is_none()
181        {
182            periodic = Some(payload);
183        }
184    }
185
186    periodic
187}
188
189/// Classify a weapon-enchant buff effect into its stat bucket and amount.
190#[must_use]
191pub fn classify_enchant_buff_effect(
192    aura_kind: i32,
193    misc_value_0: i32,
194    base_points: f64,
195) -> (WeaponEnchantStat, f64) {
196    match aura_subtype_kind(aura_kind) {
197        Some(AuraSubtypeKind::ModStat) if is_primary_stat_misc(misc_value_0) => {
198            (WeaponEnchantStat::PrimaryStat, base_points)
199        }
200        Some(AuraSubtypeKind::ModTotalStatPercentage) => {
201            (WeaponEnchantStat::PrimaryStatPercent, base_points)
202        }
203        Some(AuraSubtypeKind::ModRating) => {
204            match CombatRatingMask::from_dbc(misc_value_0).primary_rating() {
205                Some(CombatRating::Crit) => (WeaponEnchantStat::Crit, base_points),
206                Some(CombatRating::Haste) => (WeaponEnchantStat::Haste, base_points),
207                Some(CombatRating::Versatility) => (WeaponEnchantStat::Versatility, base_points),
208                Some(CombatRating::Mastery) => (WeaponEnchantStat::Mastery, base_points),
209                None => (WeaponEnchantStat::None, 0.0),
210            }
211        }
212        _ => (WeaponEnchantStat::None, 0.0),
213    }
214}
215
216/// Cast time in ms; channeled spells store their duration in the duration slot.
217#[must_use]
218pub fn cast_time_ms(spell: &SpellDataFlat) -> i32 {
219    if is_channeled_spell(spell) {
220        spell.duration
221    } else {
222        spell.cast_time
223    }
224}
225
226/// Whether the spell's cast-time slot represents a channeled duration.
227#[must_use]
228pub fn is_channeled_spell(spell: &SpellDataFlat) -> bool {
229    spell_attribute_is_any(
230        &spell.attributes,
231        &[
232            SpellAttributeKind::Channeled,
233            SpellAttributeKind::SelfChanneled,
234        ],
235    )
236}
237
238#[cfg(test)]
239#[path = "dbc_tests.rs"]
240mod tests;