1use num_format::{Locale, ToFormattedString};
2
3#[cfg(feature = "python")]
4use crate::api::calculator::DynStatisticAnalyzerPaths;
5use crate::{
6 api::{
7 calculator::{self, Calculator, GroupRoute, ItemRoute, StatisticAnalyzerPaths},
8 currency::CraftCurrencyList,
9 provider::{
10 item_info::ItemInfoProvider,
11 market_prices::{MarketPriceProvider, PriceInDivines, PriceKind},
12 },
13 types::{
14 AffixClassEnum, AffixLocationEnum, AffixSpecifier, AffixTierLevelBoundsEnum,
15 BaseItemId, THashSet,
16 },
17 },
18 calc::statistics::presets::statistic_analyzer_currency_group_presets::StatisticAnalyzerCurrencyGroupPreset,
19 utils::fraction_utils::Fraction,
20};
21use std::fmt::Write;
22
23impl ItemRoute {
24 pub fn locate_group<'a>(
25 &self,
26 calculated_groups: &'a Vec<GroupRoute>,
27 ) -> Option<&'a GroupRoute> {
28 let curr = self
29 .route
30 .iter()
31 .map(|e| e.currency_list.clone())
32 .collect::<Vec<CraftCurrencyList>>();
33
34 let found = calculated_groups
35 .iter()
36 .find(|test| test.group.as_slice() == curr.as_slice());
37
38 found
39 }
40
41 pub fn to_pretty_string(
42 &self,
43 item_provider: &ItemInfoProvider,
44 market_provider: &MarketPriceProvider,
45 unique_path_statistic_analyzer: &dyn StatisticAnalyzerPaths,
46 calculator: &Calculator,
47 calculated_groups: Option<&Vec<GroupRoute>>,
48 ) -> String {
49 let mut out = String::new();
50
51 if let Some(group) = calculated_groups {
52 let found = self.locate_group(&group);
53
54 match found {
55 Some(e) => writeln!(
56 &mut out,
57 "{}",
58 e.to_pretty_string(
59 &item_provider,
60 &market_provider,
61 StatisticAnalyzerCurrencyGroupPreset::CurrencyGroupChance
62 .get_instance()
63 .0
64 .as_ref()
65 )
66 )
67 .unwrap(),
68 None => writeln!(&mut out, "Group info could not be parsed.").unwrap(),
69 };
70 }
71
72 let start_item = &calculator.starting_item;
73
74 write!(
75 &mut out,
76 "{}",
77 start_item.to_pretty_string(&item_provider, false)
78 )
79 .unwrap();
80
81 let cost_per_1 = unique_path_statistic_analyzer.calculate_cost_per_craft(
82 &self
83 .route
84 .iter()
85 .map(|e| e.currency_list.clone())
86 .collect::<Vec<CraftCurrencyList>>(),
87 &item_provider,
88 &market_provider,
89 );
90
91 let tries_for_60 =
92 unique_path_statistic_analyzer.calculate_tries_needed_for_60_percent(&self);
93 let cost_per_60 =
94 PriceInDivines::new((tries_for_60 as f64) * cost_per_1.get_divine_value());
95
96 writeln!(
97 &mut out,
98 "Exact Chance: {:.5}% | Tries needed for 60%: {} | Cost per Craft: {} | Cost for 60%: {}{}",
99 (*self.chance.get_raw_value()) * 100_f64,
100 tries_for_60.to_formatted_string(&Locale::en),
101 format!(
102 "{} EX",
103 (market_provider
104 .currency_convert(&cost_per_1, &PriceKind::Exalted)
105 .ceil() as u64)
106 .to_formatted_string(&Locale::en)
107 ),
108 format!(
109 "{} EX",
110 (market_provider
111 .currency_convert(&cost_per_60, &PriceKind::Exalted)
112 .ceil() as u64)
113 .to_formatted_string(&Locale::en)
114 ),
115 match unique_path_statistic_analyzer.format_display_more_info(
116 &self,
117 &item_provider,
118 &market_provider
119 ) {
120 Some(e) => e,
121 None => "".to_string(),
122 }
123 )
124 .unwrap();
125
126 writeln!(
127 out,
128 "0. Starting with ...{}",
129 if start_item.affixes.is_empty() {
130 " nothing :3".to_string()
131 } else {
132 "".to_string()
133 }
134 )
135 .unwrap();
136
137 for affix in &start_item.affixes {
138 print_affix(
139 &mut out,
140 Some(0),
141 affix,
142 None,
143 item_provider,
144 true,
145 &calculator.starting_item.base_id,
146 false,
147 );
148 }
149
150 let mut prev_affixes = start_item.affixes.clone();
151 let mut prev_rarity = start_item.rarity.clone();
152 let mut temporary_affixes: THashSet<AffixSpecifier> = THashSet::default();
153
154 for (i, path) in self.route.iter().enumerate() {
155 let item: &calculator::ItemMatrixNode =
156 calculator.matrix.get(&path.item_matrix_id).unwrap();
157 let new_affixes = &item.item.snapshot.affixes;
158 let new_rarity = &item.item.snapshot.rarity;
159
160 let added: THashSet<_> = new_affixes.difference(&prev_affixes).collect();
161 let removed: THashSet<_> = prev_affixes.difference(&new_affixes).collect();
162 let temporary = item.item.meta.mark_for_essence_only;
163
164 writeln!(
165 out,
166 "{}. Apply {}{}",
167 i + 1,
168 path.currency_list
169 .list
170 .iter()
171 .map(|e| {
172 let currency_value = market_provider
173 .try_lookup_currency_in_divines_default_if_fail(&e, &item_provider);
174 let currency_value_ex = market_provider
175 .currency_convert(¤cy_value, &PriceKind::Exalted)
176 .ceil() as u32;
177
178 format!(
179 "{} ({} EX)",
180 e.get_item_name(&item_provider),
181 currency_value_ex.to_formatted_string(&Locale::en)
182 )
183 })
184 .collect::<Vec<String>>()
185 .join(" + "),
186 match temporary {
187 true => " [TEMP]",
188 false => "",
189 }
190 )
191 .unwrap();
192
193 for affix in removed.iter() {
194 print_affix(
195 &mut out,
196 Some(i + 1),
197 affix,
198 Some(path.chance),
199 item_provider,
200 false,
201 &calculator.starting_item.base_id,
202 temporary_affixes.contains(&affix),
203 );
204
205 temporary_affixes.remove(affix);
206 }
207
208 for affix in added.iter() {
209 if temporary {
210 temporary_affixes.insert((*affix).clone());
211 }
212
213 print_affix(
214 &mut out,
215 Some(i + 1),
216 affix,
217 Some(path.chance),
218 item_provider,
219 true,
220 &calculator.starting_item.base_id,
221 temporary,
222 );
223 }
224
225 if start_item.allowed_sockets != item.item.snapshot.allowed_sockets {
226 print_socket_change(
227 &mut out,
228 i + 1,
229 start_item
230 .allowed_sockets
231 .abs_diff(item.item.snapshot.allowed_sockets),
232 Some(path.chance),
233 item_provider,
234 &calculator.starting_item.base_id,
235 );
236 }
237
238 if new_rarity != &prev_rarity {
239 writeln!(
240 &mut out,
241 "{}. \t! Rarity {:?} -> {:?}",
242 i + 1,
243 prev_rarity,
244 new_rarity
245 )
246 .unwrap();
247 }
248
249 if item.item.snapshot.corrupted {
250 writeln!(
251 &mut out,
252 "{}. \t! Corrupted - ensure maximum quality and wanted affixes prior to applying a Vaal Orb, since corrupted items CAN NOT be modified further.",
253 i + 1
254 )
255 .unwrap();
256 }
257
258 prev_affixes = new_affixes.clone();
259 prev_rarity = new_rarity.clone();
260 }
261
262 out
263 }
264}
265
266pub fn print_socket_change(
267 out: &mut String,
268 index: usize,
269 current_sockets: u8,
270 chance: Option<Fraction>,
271 item_provider: &ItemInfoProvider,
272 base_id: &BaseItemId,
273) {
274 let bg = item_provider.lookup_base_group(base_id).unwrap();
275 let bg = item_provider.lookup_base_group_definition(&bg).unwrap();
276
277 writeln!(
278 out,
279 "{}.\t{}{} Socket ({}/{})",
280 index,
281 if index == 0 { "" } else { "+ " },
282 match chance {
283 Some(c) => format!("[{} (~{:.3}%)]", c, c.to_f64() * 100_f64).to_string(),
284 None => "".to_string(),
285 },
286 current_sockets,
287 bg.max_sockets
288 )
289 .unwrap();
290}
291
292pub fn print_affix(
293 out: &mut String,
294 index: Option<usize>,
295 affix: &AffixSpecifier,
296 chance: Option<Fraction>,
297 item_provider: &ItemInfoProvider,
298 is_added: bool,
299 base_id: &BaseItemId,
300 is_temporary: bool,
301) {
302 let affix_def = item_provider.lookup_affix_definition(&affix.affix).unwrap();
303
304 let name = &affix_def.description_template;
305
306 let min_ivl = match affix_def.affix_class {
307 AffixClassEnum::Base | AffixClassEnum::Desecrated | AffixClassEnum::Essence => {
308 let ilvl = &item_provider
309 .lookup_base_item_mods(&base_id)
310 .unwrap()
311 .get(&affix.affix)
312 .unwrap()
313 .iter()
314 .find(|e| e.0 == &affix.tier.tier)
315 .unwrap()
316 .1
317 .min_item_level;
318
319 format!("ilvl {}", ilvl.get_raw_value()).to_string()
320 }
321 };
322
323 let more_meta: Vec<Option<String>> = vec![
324 match is_temporary {
325 true => None,
326 false => Some(
327 format!(
328 "Tier {}{}",
329 affix.tier.tier.get_raw_value(),
330 match affix.tier.bounds {
331 AffixTierLevelBoundsEnum::Exact => "=",
332 AffixTierLevelBoundsEnum::Minimum => "+",
333 }
334 )
335 .to_string(),
336 ),
337 },
338 match is_temporary {
339 true => None,
340 false => Some(min_ivl),
341 },
342 match affix_def.affix_location {
343 AffixLocationEnum::Prefix => Some("Prefix".to_string()),
344 AffixLocationEnum::Suffix => Some("Suffix".to_string()),
345 _ => None,
346 },
347 match affix.fractured {
348 true => Some("FRAC".to_string()),
349 false => None,
350 },
351 match affix_def.affix_class {
352 AffixClassEnum::Base => None,
353 AffixClassEnum::Desecrated => Some("Des.".to_string()),
354 AffixClassEnum::Essence => Some("Ess.".to_string()),
355 },
356 ];
357
358 let more_meta = more_meta
359 .iter()
360 .filter_map(|test| test.clone())
361 .collect::<Vec<String>>();
362
363 writeln!(
364 out,
365 "{}{}[{}{}] '{}'",
366 match index {
367 Some(i) => format!("{}.\t", i),
368 None => "".to_string(),
369 },
370 match index {
371 Some(i) if i != 0 && is_added => "+ ",
372 Some(i) if i != 0 && !is_added => "- ",
373 _ => "",
374 },
375 match chance {
376 Some(c) => format!("{} (~{:.3}%), ", c, c.to_f64() * 100_f64).to_string(),
377 None => "".to_string(),
378 },
379 match more_meta.is_empty() {
380 true => "".to_string(),
381 false => format!("{}", more_meta.join(", ")).as_str().to_string(),
382 },
383 match is_temporary {
384 true => format!(
385 "any {} (temporary)",
386 match affix_def.affix_location {
387 AffixLocationEnum::Prefix => "prefix",
388 AffixLocationEnum::Suffix => "suffix",
389 _ => "???",
390 }
391 )
392 .to_string(),
393 false => name.to_string(),
394 }
395 )
396 .unwrap();
397}
398
399#[cfg(feature = "python")]
400#[cfg_attr(feature = "python", pyo3_stub_gen::derive::gen_stub_pymethods)]
401#[cfg_attr(feature = "python", pyo3::prelude::pymethods)]
402impl ItemRoute {
403 #[pyo3(name = "to_pretty_string")]
404 pub fn to_pretty_string_py(
405 &self,
406 item_provider: &ItemInfoProvider,
407 market_provider: &MarketPriceProvider,
408 statistic_analyzer: &DynStatisticAnalyzerPaths,
409 calculator: &Calculator,
410 groups: Option<Vec<GroupRoute>>,
411 ) -> String {
412 self.to_pretty_string(
413 item_provider,
414 market_provider,
415 statistic_analyzer.0.as_ref(),
416 calculator,
417 groups.as_ref(),
418 )
419 }
420
421 #[pyo3(name = "locate_group")]
422 pub fn locate_group_py(&self, calculated_groups: Vec<GroupRoute>) -> Option<GroupRoute> {
423 self.locate_group(calculated_groups.as_ref()).cloned()
424 }
425}