overlord_event_system/gacha/
item_case.rs

1use configs::game_config::GameConfig;
2use essences::character_state::CharacterState;
3use essences::item_case::ItemCasesSettingsByLevel;
4use essences::items::{Item, ItemAttribute, ItemRarity, ItemRarityId, ItemTemplate};
5
6use crate::ScriptRunner;
7use crate::event::OverlordEvent;
8use crate::game_config_helpers::GameConfigLookup;
9use crate::state::OverlordState;
10use rand::TryRngCore;
11use rand::seq::IndexedRandom;
12use rand::seq::SliceRandom;
13use rand::{
14    Rng, SeedableRng,
15    rngs::{OsRng, StdRng},
16};
17
18fn get_item_rarity_id(rng: &mut StdRng, level_settings: &ItemCasesSettingsByLevel) -> ItemRarityId {
19    let total_weight: f64 = level_settings
20        .rarity_weights
21        .iter()
22        .map(|rarity_weight| rarity_weight.weight)
23        .sum();
24
25    if total_weight < 1e-10 {
26        panic!("Sum of weights is too low: {total_weight}");
27    }
28
29    let rnd_weight = rng.random_range(0.0..total_weight);
30
31    let mut cumulative_weight = 0.0;
32    for rarity_weight in &level_settings.rarity_weights {
33        cumulative_weight += rarity_weight.weight;
34        if rnd_weight < cumulative_weight {
35            return rarity_weight.rarity_id;
36        }
37    }
38
39    panic!("Failed to get item rarity id for weight {rnd_weight}");
40}
41
42pub fn try_open_item_case(
43    character_state: &CharacterState,
44    config: &GameConfig,
45    seed: Option<u64>,
46    script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
47) -> anyhow::Result<Item> {
48    let mut rng = StdRng::seed_from_u64(seed.unwrap_or(OsRng.try_next_u64()?));
49
50    if let Some(item_template_id) =
51        script_runner.try_get_item_template_id(character_state, config)?
52    {
53        tracing::debug!("Got item_template_id from script: {item_template_id}");
54        let Some(item_template) = config.item_template(item_template_id) else {
55            anyhow::bail!("Failed to get item_template with id={}", item_template_id);
56        };
57
58        let Some(rarity) = config.item_rarity(item_template.rarity_id) else {
59            anyhow::bail!(
60                "Failed to get rarity with rarity_id={}",
61                item_template.rarity_id
62            );
63        };
64
65        let item = generate_item_from_template(
66            item_template,
67            rarity.clone(),
68            character_state.character.character_level,
69            config,
70            &mut rng,
71        );
72
73        return Ok(item);
74    };
75
76    open_item_case(character_state, config, &mut rng)
77}
78
79pub fn open_item_case(
80    character_state: &CharacterState,
81    config: &GameConfig,
82    rng: &mut rand::rngs::StdRng,
83) -> anyhow::Result<Item> {
84    let Some(level_settings) =
85        config.item_case_settings_by_level(character_state.character.item_case_level)
86    else {
87        anyhow::bail!(
88            "Failed to get case settings for item_case_level={}",
89            character_state.character.item_case_level
90        );
91    };
92
93    let rarity_id = get_item_rarity_id(rng, level_settings);
94
95    let Some(rarity) = config.item_rarity(rarity_id) else {
96        anyhow::bail!("Failed to get rarity with rarity_id={}", rarity_id);
97    };
98
99    let Some(inventory_level) = config
100        .inventory_levels
101        .iter()
102        .rev()
103        .find(|l| l.from_chapter_level <= character_state.character.current_chapter_level)
104    else {
105        anyhow::bail!(
106            "Failed to get inventory level for current_chapter_level={}",
107            character_state.character.current_chapter_level
108        );
109    };
110
111    let item_type = *inventory_level
112        .item_types
113        .choose(rng)
114        .ok_or(anyhow::anyhow!(
115            "No item slots specified for inventory_level={:?}",
116            inventory_level
117        ))?;
118
119    let items_pool: Vec<&ItemTemplate> = {
120        let result = config
121            .items
122            .iter()
123            .filter(|item| {
124                item.rarity_id == rarity.id
125                    && item.item_type == item_type
126                    && !item.exclude_from_mimic
127            })
128            .collect::<Vec<_>>();
129
130        if result.is_empty() {
131            tracing::error!(
132                "No items found for rarity_id={:?} and item_type={:?}",
133                rarity.id,
134                item_type
135            );
136            let result = config
137                .items
138                .iter()
139                .filter(|item| item.rarity_id == rarity.id)
140                .collect::<Vec<_>>();
141            if result.is_empty() {
142                anyhow::bail!("No items found for rariy_id={:?}", rarity.id);
143            }
144            result
145        } else {
146            result
147        }
148    };
149
150    let Some(&item_template) = items_pool.choose(rng) else {
151        anyhow::bail!("Failed to choose random element from items");
152    };
153
154    let item = generate_item_from_template(
155        item_template,
156        rarity.clone(),
157        character_state.character.character_level,
158        config,
159        rng,
160    );
161
162    Ok(item)
163}
164
165pub fn generate_item_from_template(
166    template: &ItemTemplate,
167    rarity: ItemRarity,
168    level: i64,
169    game_config: &GameConfig,
170    rng: &mut rand::rngs::StdRng,
171) -> Item {
172    let mut attributes: Vec<ItemAttribute> = Vec::new();
173
174    let mut optional_attribute_ids = template.attributes_settings.optional_attributes_ids.clone();
175    optional_attribute_ids.shuffle(rng);
176    optional_attribute_ids = optional_attribute_ids
177        .into_iter()
178        .take(template.attributes_settings.optional_attributes_count as usize)
179        .collect();
180
181    for attribute in &game_config.attributes {
182        if game_config
183            .game_settings
184            .required_attributes
185            .contains(&attribute.id)
186            || optional_attribute_ids.contains(&attribute.id)
187        {
188            attributes.push(ItemAttribute {
189                attr_id: attribute.id,
190                value: 0,
191            });
192        }
193    }
194
195    Item {
196        id: uuid::Uuid::now_v7(),
197        item_template_id: template.id,
198        item_type: template.item_type,
199        rarity,
200        level,
201        name: template.name.clone(),
202        icon_url: template.icon_url.clone(),
203        icon_path: template.icon_path.clone(),
204        is_equipped: false,
205        price: vec![],
206        experience: 0,
207        attributes,
208    }
209}