essences/
autochest.rs

1use crate::gatings::AutoChestGatings;
2use crate::prelude::*;
3
4use crate::items::Attribute;
5use crate::items::AttributeId;
6use crate::items::Item;
7use crate::items::ItemRarity;
8use crate::items::ItemRarityId;
9
10#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
11pub struct AutoChest {
12    pub active_filter: Option<AutoChestFilter>,
13    pub filters: Vec<AutoChestFilter>,
14    pub power_compare_enabled: bool,
15    pub batch_size: i64,
16}
17
18impl Default for AutoChest {
19    fn default() -> Self {
20        Self {
21            active_filter: None,
22            filters: Vec::new(),
23            power_compare_enabled: true,
24            batch_size: 0,
25        }
26    }
27}
28
29#[declare]
30pub type AutoChestFilterId = Uuid;
31
32#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
33pub struct AutoChestFilter {
34    pub id: AutoChestFilterId,
35    pub name: String,
36    pub rarity_id: Option<ItemRarityId>,
37    pub guaranteed_attribute_enabled: bool,
38    pub guaranteed_attribute_id: Option<AttributeId>,
39    pub additional_attribute_enabled: bool,
40    pub additional_attribute_ids: Vec<AttributeId>,
41}
42
43impl AutoChestFilter {
44    pub fn validate(
45        &self,
46        cur_chapter_level: i64,
47        character_level: i64,
48        attributes: &[Attribute],
49        autochest_gatings: &AutoChestGatings,
50    ) -> anyhow::Result<()> {
51        // Auto-chest unlock gates on CHARACTER LEVEL (rewards manual chest-opening
52        // → faster leveling → earlier auto-open). The filter sub-capabilities
53        // (rarity / guaranteed-stat) below stay chapter-gated — they have their
54        // own thresholds.
55        if character_level < autochest_gatings.autochest_button_unlock_character_level {
56            anyhow::bail!(
57                "Auto chest is not enabled at character_level = {}",
58                character_level
59            )
60        }
61
62        if self.rarity_id.is_some() && cur_chapter_level < autochest_gatings.rarity_filter_unlock {
63            anyhow::bail!(
64                "Rarity filter is not available on cur_chapter_level = {}",
65                cur_chapter_level
66            )
67        }
68
69        if let Some(guaranteed_attribute_id) = self.guaranteed_attribute_id {
70            if cur_chapter_level < autochest_gatings.guaranteed_stat_unlock {
71                anyhow::bail!(
72                    "Guaranteed stat filter is not available on cur_chapter_level = {}",
73                    cur_chapter_level
74                )
75            }
76
77            let Some(guaranteed_attr) = attributes
78                .iter()
79                .find(|attr| attr.id == guaranteed_attribute_id)
80            else {
81                anyhow::bail!(
82                    "Tried finding guaranteed_attribute with id = {}, but didn't find it in config",
83                    guaranteed_attribute_id
84                )
85            };
86
87            if !guaranteed_attr.is_ui_visible {
88                anyhow::bail!(
89                    "Provided guaranteed attribute = {:?}, shouldn't be in auto chest",
90                    guaranteed_attr
91                )
92            }
93        }
94
95        if !self.additional_attribute_ids.is_empty() {
96            if self.additional_attribute_ids.len() > 3 {
97                anyhow::bail!(
98                    "More additional attributes provided: {}, than allowed: 3",
99                    self.additional_attribute_ids.len(),
100                )
101            }
102
103            let required_chapter = match self.additional_attribute_ids.len() {
104                1 => autochest_gatings.first_additional_stat_unlock,
105                2 => autochest_gatings.second_additional_stat_unlock,
106                3 => autochest_gatings.third_additional_stat_unlock,
107                _ => unreachable!(),
108            };
109
110            if cur_chapter_level < required_chapter {
111                anyhow::bail!(
112                    "Additional stats filter with {} attribute(s) is not available on cur_chapter_level = {}",
113                    self.additional_attribute_ids.len(),
114                    cur_chapter_level
115                )
116            }
117
118            for additional_attr_id in &self.additional_attribute_ids {
119                let Some(additional_attr) = attributes
120                    .iter()
121                    .find(|attr| attr.id == *additional_attr_id)
122                else {
123                    anyhow::bail!(
124                        "Tried finding additional_attribute with id = {}, but didn't find it in config",
125                        additional_attr_id
126                    )
127                };
128
129                if !additional_attr.is_ui_visible {
130                    anyhow::bail!(
131                        "Provided additional attribute = {:?}, shouldn't be in auto chest",
132                        additional_attr
133                    )
134                }
135            }
136        }
137
138        Ok(())
139    }
140}
141
142pub fn filter_items(
143    items: &[Item],
144    filter: &Option<AutoChestFilter>,
145    power_compare_enabled: bool,
146    item_rarities: &Vec<ItemRarity>,
147    item_powers: &std::collections::HashMap<uuid::Uuid, i64>,
148    equipped_item_powers: &std::collections::HashMap<crate::items::ItemType, i64>,
149) -> anyhow::Result<(Vec<Item>, Vec<Item>)> {
150    let mut items_to_sell = vec![];
151    let mut items_to_equip = vec![];
152
153    let filter_rarity = if let Some(filter) = filter
154        && let Some(rarity_id) = filter.rarity_id
155    {
156        let Some(rarity) = item_rarities.iter().find(|r| r.id == rarity_id) else {
157            anyhow::bail!(
158                "Couldn't find filter rarity_id = {}, in provided item_rarities = {:?}",
159                rarity_id,
160                item_rarities
161            );
162        };
163        Some(rarity)
164    } else {
165        None
166    };
167
168    for item in items {
169        if let Some(filter_rarity) = filter_rarity
170            && item.rarity.order < filter_rarity.order
171        {
172            items_to_sell.push(item.clone());
173            continue;
174        }
175
176        if let Some(filter) = filter {
177            if filter.guaranteed_attribute_enabled
178                && let Some(guaranteed_attribute_id) = filter.guaranteed_attribute_id
179                && !item
180                    .attributes
181                    .iter()
182                    .any(|item_attr| item_attr.attr_id == guaranteed_attribute_id)
183            {
184                items_to_sell.push(item.clone());
185                continue;
186            }
187
188            if filter.additional_attribute_enabled
189                && !filter.additional_attribute_ids.is_empty()
190                && !filter.additional_attribute_ids.iter().any(|id| {
191                    item.attributes
192                        .iter()
193                        .any(|item_attr| *id == item_attr.attr_id)
194                })
195            {
196                items_to_sell.push(item.clone());
197                continue;
198            }
199        }
200
201        // Compare power if enabled and there's an equipped item of the same type
202        if power_compare_enabled
203            && let Some(&equipped_power) = equipped_item_powers.get(&item.item_type)
204            && let Some(&item_power) = item_powers.get(&item.id)
205            && item_power < equipped_power
206        {
207            items_to_sell.push(item.clone());
208            continue;
209        }
210
211        items_to_equip.push(item.clone());
212    }
213
214    Ok((items_to_sell, items_to_equip))
215}