pyoe2_craftpath/utils/
pretty_print_unique_utils.rs

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(&currency_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}