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 ) {
114 if let Some(bonuses) = data.bonuses.get(&bonus.value_0) {
115 expanded.extend(bonuses.iter());
116 }
117 }
118 }
119
120 all_bonuses.extend(expanded);
121
122 all_bonuses.sort_by_key(|b| b.order_index);
123
124 all_bonuses
125}
126
127pub(crate) fn resolve_item_level(
129 bonuses: &[&ItemBonusFlat],
130 base: i32,
131 item_bonus_ids: &[i32],
132 data: &ItemScalingData,
133 drop_level: Option<i32>,
134) -> ItemLevelResolution {
135 let mut item_level = base;
136 let mut has_midnight_scaling = false;
137 let mut applied = Vec::new();
138
139 for bonus in bonuses {
140 match BonusType::try_from(bonus.bonus_type) {
141 Ok(BonusType::SquishCurve) => {
142 let Some(curve_value) =
143 interpolate_curve(data, bonus.value_0, f64::from(bonus.value_1))
144 else {
145 continue;
146 };
147 let mut level = f64_to_i32_saturating_round(curve_value);
148
149 if bonus.value_2 == 1 {
150 if let Some(curve) = latest_squish_curve(data) {
151 if let Some(squished) = interpolate_curve(data, curve, f64::from(level)) {
152 level = f64_to_i32_saturating_round(squished);
153 }
154 }
155 }
156
157 item_level = level;
158 has_midnight_scaling = true;
159 applied.push(AppliedBonus {
160 bonus_list_id: bonus.parent_item_bonus_list_id,
161 bonus_type: bonus.bonus_type,
162 description: format!("Item level {item_level} (squish curve)"),
163 });
164 }
165 Ok(BonusType::ScaleConfig) => {
166 let Some(cfg) = data.item_scaling_configs.get(&bonus.value_0) else {
167 continue;
168 };
169 let Some(offset) = data.item_offset_curves.get(&cfg.item_offset_curve_id) else {
170 continue;
171 };
172 let squish_curve = (bonus.value_1 != 0
173 && cfg.required_level <= MAX_SQUISH_REQUIRED_LEVEL)
174 .then(|| squish_curve_for_era(data, cfg.item_squish_era_id))
175 .flatten();
176 let curve_id = squish_curve.unwrap_or(offset.curve_id);
177 let Some(curve_value) =
178 interpolate_curve(data, curve_id, f64::from(cfg.item_level))
179 else {
180 continue;
181 };
182
183 item_level = f64_to_i32_saturating_round(curve_value) + offset.offset;
184 has_midnight_scaling = true;
185 applied.push(AppliedBonus {
186 bonus_list_id: bonus.parent_item_bonus_list_id,
187 bonus_type: bonus.bonus_type,
188 description: format!(
189 "Item level {} (scale config {})",
190 item_level, bonus.value_0
191 ),
192 });
193 }
194 Ok(BonusType::ScaleConfig2) => {
195 let Some(cfg) = data.item_scaling_configs.get(&bonus.value_0) else {
196 continue;
197 };
198 let Some(offset) = data.item_offset_curves.get(&cfg.item_offset_curve_id) else {
199 continue;
200 };
201 let x = drop_level.unwrap_or(cfg.item_level);
202 let Some(curve_value) = interpolate_curve(data, offset.curve_id, f64::from(x))
203 else {
204 continue;
205 };
206
207 item_level = f64_to_i32_saturating_round(curve_value) + offset.offset;
208 has_midnight_scaling = true;
209 applied.push(AppliedBonus {
210 bonus_list_id: bonus.parent_item_bonus_list_id,
211 bonus_type: bonus.bonus_type,
212 description: format!(
213 "Item level {} (scale config 2 {})",
214 item_level, bonus.value_0
215 ),
216 });
217 }
218 Ok(BonusType::CraftingQuality) => {
219 item_level += bonus.value_0;
220 applied.push(AppliedBonus {
221 bonus_list_id: bonus.parent_item_bonus_list_id,
222 bonus_type: bonus.bonus_type,
223 description: format!("Item level {:+} (crafting quality)", bonus.value_0),
224 });
225 }
226 Ok(BonusType::PostSquishItemLevel) => {
227 if has_midnight_scaling {
228 item_level += bonus.value_0;
229 applied.push(AppliedBonus {
230 bonus_list_id: bonus.parent_item_bonus_list_id,
231 bonus_type: bonus.bonus_type,
232 description: format!("Item level {:+} (post squish)", bonus.value_0),
233 });
234 }
235 }
236 Ok(
237 BonusType::Ilevel
238 | BonusType::SetIlevel2
239 | BonusType::Mod
240 | BonusType::Scaling
241 | BonusType::Scaling2
242 | BonusType::ApplyBonus
243 | BonusType::Quality
244 | BonusType::Desc
245 | BonusType::Suffix
246 | BonusType::Socket
247 | BonusType::ReqLevel
248 | BonusType::SetIlevel
249 | BonusType::AddRank
250 | BonusType::AddItemEffect
251 | BonusType::ModItemStat
252 | BonusType::IlevelInPvp,
253 )
254 | Err(_) => {}
255 }
256 }
257
258 let ilevel_disabled = has_midnight_scaling
259 || item_bonus_ids
260 .iter()
261 .any(|id| ILEVEL_DISABLE_BONUS_IDS.contains(id));
262
263 if !ilevel_disabled {
264 for bonus in bonuses {
265 match BonusType::try_from(bonus.bonus_type) {
266 Ok(BonusType::Ilevel) => {
267 item_level += bonus.value_0;
268 applied.push(AppliedBonus {
269 bonus_list_id: bonus.parent_item_bonus_list_id,
270 bonus_type: bonus.bonus_type,
271 description: format!("Item level {:+}", bonus.value_0),
272 });
273 }
274 Ok(BonusType::SetIlevel2) => {
275 item_level = bonus.value_0;
276 applied.push(AppliedBonus {
277 bonus_list_id: bonus.parent_item_bonus_list_id,
278 bonus_type: bonus.bonus_type,
279 description: format!("Item level set to {}", bonus.value_0),
280 });
281 }
282 Ok(
283 BonusType::Mod
284 | BonusType::Quality
285 | BonusType::Desc
286 | BonusType::Suffix
287 | BonusType::Socket
288 | BonusType::ReqLevel
289 | BonusType::Scaling
290 | BonusType::Scaling2
291 | BonusType::SetIlevel
292 | BonusType::AddRank
293 | BonusType::AddItemEffect
294 | BonusType::ModItemStat
295 | BonusType::IlevelInPvp
296 | BonusType::SquishCurve
297 | BonusType::ScaleConfig
298 | BonusType::ApplyBonus
299 | BonusType::ScaleConfig2
300 | BonusType::CraftingQuality
301 | BonusType::PostSquishItemLevel,
302 )
303 | Err(_) => {}
304 }
305 }
306 }
307
308 ItemLevelResolution {
309 item_level,
310 applied,
311 }
312}
313
314pub fn get_bonus_description(bonus_type: i32) -> &'static str {
316 BonusType::try_from(bonus_type).map_or("Unknown", BonusType::name)
317}
318
319#[cfg(test)]
320mod tests;