overlord_event_system/gacha/
item_case.rs1use 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}