wowlab_parsers/parsers/scaling/
bonus.rs1use wowlab_types::{
6 data::{AppliedBonus, ItemBonusFlat, ItemScalingData},
7 numeric::f64_to_i32_saturating_round,
8};
9
10use super::curve::interpolate_curve;
11
12const MAX_SQUISH_REQUIRED_LEVEL: i32 = 80;
13
14pub(crate) const ALLOC_MULTIPLIER: f64 = 0.0001;
16
17const ILEVEL_DISABLE_BONUS_IDS: [i32; 2] = [7215, 7250];
18
19fn squish_curve_for_era(data: &ItemScalingData, era_id: i32) -> Option<i32> {
20 let curve = data.item_squish_eras.get(&era_id)?.curve_id;
21
22 (curve != 0).then_some(curve)
23}
24
25fn latest_squish_curve(data: &ItemScalingData) -> Option<i32> {
26 let latest = data
27 .item_squish_eras
28 .values()
29 .filter(|e| e.curve_id != 0)
30 .max_by_key(|e| e.patch);
31
32 latest.map(|e| e.curve_id)
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq, num_enum::TryFromPrimitive)]
37#[repr(i32)]
38pub(super) enum BonusType {
39 Ilevel = 1,
40 Mod = 2,
41 Quality = 3,
42 Desc = 4,
43 Suffix = 5,
44 Socket = 6,
45 ReqLevel = 8,
46 Scaling = 11,
47 Scaling2 = 13,
48 SetIlevel = 14,
49 AddRank = 17,
50 AddItemEffect = 23,
51 ModItemStat = 25,
52 IlevelInPvp = 36,
53 SetIlevel2 = 42,
54 SquishCurve = 48,
55 ScaleConfig = 49,
56 ApplyBonus = 50,
57 ScaleConfig2 = 51,
58 CraftingQuality = 52,
59 PostSquishItemLevel = 53,
60}
61
62impl BonusType {
63 pub(super) const fn name(self) -> &'static str {
65 match self {
66 BonusType::Ilevel => "Item Level Modifier",
67 BonusType::Mod => "Stat Modifier",
68 BonusType::Quality => "Quality Modifier",
69 BonusType::Desc => "Description",
70 BonusType::Suffix => "Random Suffix",
71 BonusType::Socket => "Socket",
72 BonusType::ReqLevel => "Required Level",
73 BonusType::Scaling => "Scaling Curve",
74 BonusType::Scaling2 => "Scaling Curve (Alt)",
75 BonusType::SetIlevel | BonusType::SetIlevel2 => "Set Item Level",
76 BonusType::AddRank => "Artifact Rank",
77 BonusType::AddItemEffect => "Item Effect",
78 BonusType::ModItemStat => "Modify Stat",
79 BonusType::IlevelInPvp => "PvP Item Level",
80 BonusType::SquishCurve => "Squish Curve",
81 BonusType::ScaleConfig => "Scale Config",
82 BonusType::ApplyBonus => "Apply Bonus List",
83 BonusType::ScaleConfig2 => "Scale Config (Drop Level)",
84 BonusType::CraftingQuality => "Crafting Quality",
85 BonusType::PostSquishItemLevel => "Post-Squish Item Level",
86 }
87 }
88}
89
90pub(crate) struct ItemLevelResolution {
91 pub(crate) item_level: i32,
92 pub(crate) applied: Vec<AppliedBonus>,
93}
94
95pub(crate) fn collect_bonuses<'a>(
96 bonus_ids: &[i32],
97 data: &'a ItemScalingData,
98) -> Vec<&'a ItemBonusFlat> {
99 let mut all_bonuses: Vec<&ItemBonusFlat> = Vec::new();
100
101 for &bonus_id in bonus_ids {
102 if let Some(bonuses) = data.bonuses.get(&bonus_id) {
103 all_bonuses.extend(bonuses.iter());
104 }
105 }
106
107 let mut expanded: Vec<&ItemBonusFlat> = Vec::new();
108
109 for bonus in &all_bonuses {
110 if matches!(
111 BonusType::try_from(bonus.bonus_type),
112 Ok(BonusType::ApplyBonus)
113 ) && let Some(bonuses) = data.bonuses.get(&bonus.value_0)
114 {
115 expanded.extend(bonuses.iter());
116 }
117 }
118
119 all_bonuses.extend(expanded);
120
121 all_bonuses.sort_by_key(|b| b.order_index);
122
123 all_bonuses
124}
125
126pub(crate) fn resolve_item_level(
128 bonuses: &[&ItemBonusFlat],
129 base: i32,
130 item_bonus_ids: &[i32],
131 data: &ItemScalingData,
132 drop_level: Option<i32>,
133) -> ItemLevelResolution {
134 let mut item_level = base;
135 let mut has_midnight_scaling = false;
136 let mut applied = Vec::new();
137
138 for bonus in bonuses {
139 match BonusType::try_from(bonus.bonus_type) {
140 Ok(BonusType::SquishCurve) => {
141 let Some(curve_value) =
142 interpolate_curve(data, bonus.value_0, f64::from(bonus.value_1))
143 else {
144 continue;
145 };
146 let mut level = f64_to_i32_saturating_round(curve_value);
147
148 if bonus.value_2 == 1
149 && let Some(curve) = latest_squish_curve(data)
150 && let Some(squished) = interpolate_curve(data, curve, f64::from(level))
151 {
152 level = f64_to_i32_saturating_round(squished);
153 }
154
155 item_level = level;
156 has_midnight_scaling = true;
157 applied.push(AppliedBonus {
158 bonus_list_id: bonus.parent_item_bonus_list_id,
159 bonus_type: bonus.bonus_type,
160 description: format!("Item level {item_level} (squish curve)"),
161 });
162 }
163 Ok(BonusType::ScaleConfig) => {
164 let Some(cfg) = data.item_scaling_configs.get(&bonus.value_0) else {
165 continue;
166 };
167 let Some(offset) = data.item_offset_curves.get(&cfg.item_offset_curve_id) else {
168 continue;
169 };
170 let squish_curve = (bonus.value_1 != 0
171 && cfg.required_level <= MAX_SQUISH_REQUIRED_LEVEL)
172 .then(|| squish_curve_for_era(data, cfg.item_squish_era_id))
173 .flatten();
174 let curve_id = squish_curve.unwrap_or(offset.curve_id);
175 let Some(curve_value) =
176 interpolate_curve(data, curve_id, f64::from(cfg.item_level))
177 else {
178 continue;
179 };
180
181 item_level = f64_to_i32_saturating_round(curve_value) + offset.offset;
182 has_midnight_scaling = true;
183 applied.push(AppliedBonus {
184 bonus_list_id: bonus.parent_item_bonus_list_id,
185 bonus_type: bonus.bonus_type,
186 description: format!(
187 "Item level {} (scale config {})",
188 item_level, bonus.value_0
189 ),
190 });
191 }
192 Ok(BonusType::ScaleConfig2) => {
193 let Some(cfg) = data.item_scaling_configs.get(&bonus.value_0) else {
194 continue;
195 };
196 let Some(offset) = data.item_offset_curves.get(&cfg.item_offset_curve_id) else {
197 continue;
198 };
199 let x = drop_level.unwrap_or(cfg.item_level);
200 let Some(curve_value) = interpolate_curve(data, offset.curve_id, f64::from(x))
201 else {
202 continue;
203 };
204
205 item_level = f64_to_i32_saturating_round(curve_value) + offset.offset;
206 has_midnight_scaling = true;
207 applied.push(AppliedBonus {
208 bonus_list_id: bonus.parent_item_bonus_list_id,
209 bonus_type: bonus.bonus_type,
210 description: format!(
211 "Item level {} (scale config 2 {})",
212 item_level, bonus.value_0
213 ),
214 });
215 }
216 Ok(BonusType::CraftingQuality) => {
217 item_level += bonus.value_0;
218 applied.push(AppliedBonus {
219 bonus_list_id: bonus.parent_item_bonus_list_id,
220 bonus_type: bonus.bonus_type,
221 description: format!("Item level {:+} (crafting quality)", bonus.value_0),
222 });
223 }
224 Ok(BonusType::PostSquishItemLevel) => {
225 if has_midnight_scaling {
226 item_level += bonus.value_0;
227 applied.push(AppliedBonus {
228 bonus_list_id: bonus.parent_item_bonus_list_id,
229 bonus_type: bonus.bonus_type,
230 description: format!("Item level {:+} (post squish)", bonus.value_0),
231 });
232 }
233 }
234 Ok(
235 BonusType::Ilevel
236 | BonusType::SetIlevel2
237 | BonusType::Mod
238 | BonusType::Scaling
239 | BonusType::Scaling2
240 | BonusType::ApplyBonus
241 | BonusType::Quality
242 | BonusType::Desc
243 | BonusType::Suffix
244 | BonusType::Socket
245 | BonusType::ReqLevel
246 | BonusType::SetIlevel
247 | BonusType::AddRank
248 | BonusType::AddItemEffect
249 | BonusType::ModItemStat
250 | BonusType::IlevelInPvp,
251 )
252 | Err(_) => {}
253 }
254 }
255
256 let ilevel_disabled = has_midnight_scaling
257 || item_bonus_ids
258 .iter()
259 .any(|id| ILEVEL_DISABLE_BONUS_IDS.contains(id));
260
261 if !ilevel_disabled {
262 for bonus in bonuses {
263 match BonusType::try_from(bonus.bonus_type) {
264 Ok(BonusType::Ilevel) => {
265 item_level += bonus.value_0;
266 applied.push(AppliedBonus {
267 bonus_list_id: bonus.parent_item_bonus_list_id,
268 bonus_type: bonus.bonus_type,
269 description: format!("Item level {:+}", bonus.value_0),
270 });
271 }
272 Ok(BonusType::SetIlevel2) => {
273 item_level = bonus.value_0;
274 applied.push(AppliedBonus {
275 bonus_list_id: bonus.parent_item_bonus_list_id,
276 bonus_type: bonus.bonus_type,
277 description: format!("Item level set to {}", bonus.value_0),
278 });
279 }
280 Ok(
281 BonusType::Mod
282 | BonusType::Quality
283 | BonusType::Desc
284 | BonusType::Suffix
285 | BonusType::Socket
286 | BonusType::ReqLevel
287 | BonusType::Scaling
288 | BonusType::Scaling2
289 | BonusType::SetIlevel
290 | BonusType::AddRank
291 | BonusType::AddItemEffect
292 | BonusType::ModItemStat
293 | BonusType::IlevelInPvp
294 | BonusType::SquishCurve
295 | BonusType::ScaleConfig
296 | BonusType::ApplyBonus
297 | BonusType::ScaleConfig2
298 | BonusType::CraftingQuality
299 | BonusType::PostSquishItemLevel,
300 )
301 | Err(_) => {}
302 }
303 }
304 }
305
306 ItemLevelResolution {
307 item_level,
308 applied,
309 }
310}
311
312pub fn get_bonus_description(bonus_type: i32) -> &'static str {
314 BonusType::try_from(bonus_type).map_or("Unknown", BonusType::name)
315}
316
317#[cfg(test)]
318mod tests;