Skip to main content

wowlab_engine_domain/dbc/game_data/
scaling.rs

1//! Level, item-level, and enchantment scaling policy over resolved tables.
2
3use wowlab_engine_gamedata::{RatingMultiplierSlot, ResolvedGameTables};
4use wowlab_engine_ports::EnchantmentEffect;
5use wowlab_types::data::SpellScalingFlat;
6
7use super::super::EffectLookup;
8
9/// Continuous level-scaling rows use `ExpectedStat.ExpansionID == -2`.
10pub const EXPANSION_LEVEL_SCALING: i32 = -2;
11/// `scaling_class -1`: budget reads the primary-stat item column without an armor multiplier.
12pub const SCALING_CLASS_PRIMARY_STAT: i32 = -1;
13/// `scaling_class -8` (`DAMAGE_REPLACE_STAT`): budget reads the `damage_replace_stat` column.
14pub const SCALING_CLASS_DAMAGE_REPLACE_STAT: i32 = -8;
15/// `scaling_class -7`: budget reads the `item` column scaled by the ilvl-1 armor CR multiplier.
16pub const SCALING_CLASS_ARMOR_MULTIPLIED: i32 = -7;
17/// `scaling_class -9` (`DAMAGE_SECONDARY`): budget reads the `damage_secondary` column.
18pub const SCALING_CLASS_DAMAGE_SECONDARY: i32 = -9;
19/// Item level at which the armor combat-rating multiplier is sampled for `-7` scaling.
20pub const CR_MULTIPLIER_BASE_ILVL: i32 = 1;
21
22/// Spell-scaling budget for `scaling_class` at `level`; `None` when the level row is absent.
23#[must_use]
24pub fn spell_scaling_budget(
25    tables: &ResolvedGameTables,
26    scaling_class: i32,
27    level: u32,
28) -> Option<f64> {
29    let row = tables.spell_scaling_row(level)?;
30    let budget = if scaling_class == SCALING_CLASS_DAMAGE_REPLACE_STAT {
31        row.damage_replace_stat
32    } else {
33        row.item
34    };
35
36    if scaling_class == SCALING_CLASS_ARMOR_MULTIPLIED {
37        let mult = crate::stats::rating_multiplier_at_ilvl(
38            tables,
39            RatingMultiplierSlot::Armor,
40            CR_MULTIPLIER_BASE_ILVL,
41        );
42
43        Some(budget * mult)
44    } else {
45        Some(budget)
46    }
47}
48
49/// Item-level budget used by a DBC spell effect with no backing item instance.
50#[must_use]
51pub fn item_effect_scaling_budget(
52    tables: &ResolvedGameTables,
53    scaling_class: i32,
54    item_level: i32,
55) -> Option<f64> {
56    let row = tables.rand_prop_points_row(item_level)?;
57
58    match scaling_class {
59        SCALING_CLASS_DAMAGE_REPLACE_STAT => Some(row.damage_replace_stat_f),
60        SCALING_CLASS_DAMAGE_SECONDARY => Some(row.damage_secondary_f),
61        SCALING_CLASS_ARMOR_MULTIPLIED => Some(
62            row.good_f_0
63                * crate::stats::rating_multiplier_at_ilvl(
64                    tables,
65                    RatingMultiplierSlot::Trinket,
66                    item_level,
67                ),
68        ),
69        _ => Some(row.good_f_0),
70    }
71}
72
73/// Resolves one effect's coefficient against the item-level budget selected by its scaling class.
74#[must_use]
75pub fn item_scaled_effect_value(lookup: EffectLookup<'_>, item_level: i32) -> Option<f64> {
76    let coefficient = lookup.coefficient();
77    let scaling_class = lookup.scaling_class();
78    let budget = item_effect_scaling_budget(lookup.data.game_tables(), scaling_class, item_level)?;
79
80    Some(coefficient * budget)
81}
82
83// #t(fn: rust_cyclomatic_complexity) 1:1 scaling-class-to-column match table
84fn enchant_scaling_column(row: &SpellScalingFlat, class: i32) -> Option<f64> {
85    match class {
86        // tidy-alphabetical-start
87        -10 => Some(row.mana_consumable),
88        -2 => Some(row.consumable),
89        -3 => Some(row.gem1),
90        -4 => Some(row.gem2),
91        -5 => Some(row.gem3),
92        -6 => Some(row.health),
93        -9 => Some(row.damage_secondary),
94        1 => Some(row.warrior),
95        10 => Some(row.monk),
96        11 => Some(row.druid),
97        12 => Some(row.demon_hunter),
98        13 => Some(row.evoker),
99        2 => Some(row.paladin),
100        3 => Some(row.hunter),
101        4 => Some(row.rogue),
102        5 => Some(row.priest),
103        6 => Some(row.death_knight),
104        7 => Some(row.shaman),
105        8 => Some(row.mage),
106        9 => Some(row.warlock),
107        SCALING_CLASS_DAMAGE_REPLACE_STAT => Some(row.damage_replace_stat),
108        SCALING_CLASS_PRIMARY_STAT | SCALING_CLASS_ARMOR_MULTIPLIED => Some(row.item),
109        // tidy-alphabetical-end
110        _ => None,
111    }
112}
113
114/// Returns a flat stat amount for an enchantment effect.
115#[must_use]
116pub fn enchantment_stat_value(
117    effect: &EnchantmentEffect,
118    tables: &ResolvedGameTables,
119    scaling_class: i32,
120    max_level: i32,
121    player_level: u32,
122) -> Option<f64> {
123    let scaling_points = effect.scaling_points.unwrap_or(0.0);
124
125    if scaling_class == 0 || scaling_points.abs() < f64::EPSILON {
126        return effect.points_min.filter(|p| p.abs() >= f64::EPSILON);
127    }
128
129    let level = if max_level > 0 {
130        player_level.min(u32::try_from(max_level).unwrap_or(0))
131    } else {
132        player_level
133    };
134    let row = tables.spell_scaling_row(level)?;
135    let budget = enchant_scaling_column(row, scaling_class)?;
136
137    Some((budget * scaling_points).round())
138}