use super::record::*; use super::stat::*; use data::gacha; use data::gacha::gacha_config::*; use data::tables::ItemID; use chrono::{DateTime, Local}; use proto::GachaModelBin; use rand::{thread_rng, Rng}; use std::collections::{HashMap, HashSet}; use std::hash::{BuildHasher, Hash}; pub struct GachaModel { pub gacha_status_map: HashMap, pub gacha_records: Vec, } impl Default for GachaModel { fn default() -> GachaModel { let result = GachaModel { gacha_status_map: HashMap::new(), gacha_records: vec![], }; result.post_deserialize() } } impl GachaModel { pub fn from_bin(gacha_bin: GachaModelBin) -> Self { let result = Self { gacha_status_map: gacha_bin .gacha_status_map .into_iter() .map(|(k, v)| (k, GachaStatus::from_bin(v))) .collect(), gacha_records: gacha_bin .gacha_records .into_iter() .map(|x| GachaRecord::from_bin(x)) .collect(), }; result.post_deserialize() } pub fn to_bin(&self) -> GachaModelBin { GachaModelBin { gacha_status_map: self .gacha_status_map .iter() .map(|(k, v)| (k.clone(), v.to_bin())) .collect(), gacha_records: self.gacha_records.iter().map(|x| x.to_bin()).collect(), ..Default::default() } } pub fn post_deserialize(mut self) -> GachaModel { let gachaconf = gacha::global_gacha_config(); for gacha_pool in gachaconf.character_gacha_pool_list.iter() { let mut gacha_status_map = &mut self.gacha_status_map; let status_bin = get_or_add( &mut gacha_status_map, &gacha_pool.sharing_guarantee_info_category, ); for rarity_items in gacha_pool.gacha_items.iter() { let progress_bin = get_or_add(&mut status_bin.rarity_status_map, &rarity_items.rarity); if progress_bin.pity <= 0 { progress_bin.pity = 1; } for category_guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { get_or_add( &mut progress_bin.categories_progress_map, &category_guarantee_policy_tag, ); let guarantee_policy = gachaconf .category_guarantee_policy_map .get(category_guarantee_policy_tag) .unwrap(); if !guarantee_policy.chooseable { continue; } get_or_add( &mut progress_bin.categories_chosen_guarantee_progress_map, &category_guarantee_policy_tag, ); } } for discount_policy_tag in gacha_pool.discount_policy_tags.iter() { if gachaconf .discount_policies .free_select_map .contains_key(discount_policy_tag) { let policy = gachaconf .discount_policies .free_select_map .get(discount_policy_tag) .unwrap(); get_or_add( &mut status_bin.discount_usage_map, &policy.free_select_progress_record_tag, ); get_or_add( &mut status_bin.discount_usage_map, &policy.free_select_usage_record_tag, ); } else { get_or_add(&mut status_bin.discount_usage_map, &discount_policy_tag); } } } self } pub fn perform_pull_pool<'bin, 'conf>( &'bin mut self, pull_time: &DateTime, target_pool: &'conf CharacterGachaPool, ) -> GachaRecord { let (rarity_items, progress_bin, status_bin, probability_model) = self.determine_rarity(target_pool); let (category_tag, category) = self.determine_category(rarity_items, progress_bin, target_pool); let result = determine_gacha_result( pull_time, category, target_pool, status_bin, progress_bin, rarity_items, ); self.update_pity(rarity_items, probability_model, target_pool); self.update_category_guarantee_info(rarity_items, &category_tag, target_pool); self.update_discount(target_pool, &category_tag, rarity_items); result } fn rand_rarity<'bin, 'conf>( &'bin self, target_pool: &'conf CharacterGachaPool, status_bin: &'bin GachaStatus, ) -> ( &'conf GachaAvailableItemsInfo, &'bin GachaProgress, &'conf ProbabilityModel, ) { let gachaconf = gacha::global_gacha_config(); let mut rng = thread_rng(); let rarity_status_map = &status_bin.rarity_status_map; // gacha_items is already sorted by rarity descendingly in its post_configure. for rarity_items in target_pool.gacha_items.iter() { // Surely any judgement should be made on the current pity. let progress_bin = rarity_status_map.get(&rarity_items.rarity).unwrap(); let pity = progress_bin.pity; let probability_model = gachaconf .probability_model_map .get(&rarity_items.probability_model_tag) .unwrap(); if rng.gen_range(0.0..100.0) <= probability_model.get_chance_percent(&pity) { return (rarity_items, progress_bin, probability_model); } } panic!("The user failed to get any items."); } fn determine_rarity<'bin, 'conf>( &'bin self, target_pool: &'conf CharacterGachaPool, ) -> ( &'conf GachaAvailableItemsInfo, &'bin GachaProgress, &'bin GachaStatus, &'conf ProbabilityModel, ) { let gachaconf = gacha::global_gacha_config(); let status_bin = self .gacha_status_map .get(&target_pool.sharing_guarantee_info_category) .expect(&format!( "post_deserialize forgot StatusBin/sharing_guarantee_info_category: {}", target_pool.sharing_guarantee_info_category )); let (mut rarity_items, mut progress_bin, mut probability_model) = self.rand_rarity(target_pool, &status_bin); // We should take AdvancedGuarantee discount into consideration. for discount_tag in target_pool.discount_policy_tags.iter() { if let Some(discount) = gachaconf .discount_policies .advanced_guarantee_map .get(discount_tag) { if discount.rarity <= rarity_items.rarity { continue; } if status_bin .discount_usage_map .get(discount_tag) .expect(&format!( "post_deserialize forgot StatusBin/discount_usage_map: {}", discount_tag )) >= &discount.use_limit { continue; } let higher_progress_bin = status_bin .rarity_status_map .get(&discount.rarity) .expect(&format!( "post_deserialize forgot StatusBin/rarity_status_map: {}", &discount.rarity )); if higher_progress_bin.pity >= discount.guarantee_pity { let mut found_rarity_items = false; for gacha_items in target_pool.gacha_items.iter() { if gacha_items.rarity == discount.rarity { rarity_items = gacha_items; probability_model = gachaconf .probability_model_map .get(&gacha_items.probability_model_tag) .unwrap(); found_rarity_items = true; break; } } assert!(found_rarity_items, "Handle AdvancedGuarantee Discount ({discount_tag}) error: The target rarity does not exist in this pool."); progress_bin = higher_progress_bin; } } } (rarity_items, progress_bin, status_bin, probability_model) } fn determine_category<'bin, 'conf>( &'bin self, rarity_items: &'conf GachaAvailableItemsInfo, progress_bin: &'bin GachaProgress, target_pool: &'conf CharacterGachaPool, ) -> (String, &'conf GachaCategoryInfo) { let gachaconf = gacha::global_gacha_config(); let mut category_tag_inited = false; let mut category_tag_result: HashSet = HashSet::new(); // First of all, if there's a chooseable category and // it is SELECTED then we MUST give that category's item. for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { let category_guarantee_policy = gachaconf .category_guarantee_policy_map .get(guarantee_policy_tag) .unwrap(); if !category_guarantee_policy.chooseable { continue; } // As we found a policy defined chooseable, we // should head to look whether the user chose // the category he want. if let Some(category_tag) = progress_bin .categories_chosen_guarantee_category_map .get(guarantee_policy_tag) { // User chose a category; our work are done here. category_tag_result.insert(category_tag.clone()); category_tag_inited = true; } } // Then we should take a look at MustGainItem. if !category_tag_inited { for discount_policy_tag in target_pool.discount_policy_tags.iter() { if let Some(discount) = gachaconf .discount_policies .must_gain_item_map .get(discount_policy_tag) { if discount.rarity != rarity_items.rarity { continue; } category_tag_result.insert(discount.category_tag.clone()); category_tag_inited = true; } } } // Otherwise, just select as normal. if !category_tag_inited { for tag in rarity_items.categories.keys() { category_tag_result.insert(tag.clone()); } for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { let category_guarantee_policy = gachaconf .category_guarantee_policy_map .get(guarantee_policy_tag) .unwrap(); let failure_times = progress_bin.categories_progress_map .get(guarantee_policy_tag) .expect(&format!("post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", &rarity_items.rarity, guarantee_policy_tag)); if failure_times >= &category_guarantee_policy.trigger_on_failure_times { category_tag_result = category_tag_result .intersection(&category_guarantee_policy.included_category_tags) .cloned() .collect(); } } // category_tag_inited = true; } let mut categories: Vec<(String, &GachaCategoryInfo)> = vec![]; let mut weight_sum = 0; for result_tag in category_tag_result { let category = rarity_items.categories.get(&result_tag).unwrap(); categories.push((result_tag, category)); weight_sum += category.category_weight; } let randomnum = rand::thread_rng().gen_range(0..weight_sum); let mut enumerated_ranges_end = 0; for category in categories.into_iter() { if randomnum <= enumerated_ranges_end + category.1.category_weight { return (category.0, category.1); } enumerated_ranges_end += category.1.category_weight; } panic!("No category is chosen."); } fn update_pity<'bin, 'conf>( &'bin mut self, rarity_items: &'conf GachaAvailableItemsInfo, probability_model: &'conf ProbabilityModel, target_pool: &'conf CharacterGachaPool, ) { let status_bin = self .gacha_status_map .get_mut(&target_pool.sharing_guarantee_info_category) .unwrap(); for (rarity, rarity_status) in status_bin.rarity_status_map.iter_mut() { if (rarity == &rarity_items.rarity) || (probability_model.clear_status_on_higher_rarity_pulled && rarity < &rarity_items.rarity) { rarity_status.pity = 1; } else { rarity_status.pity += 1; } } } fn update_category_guarantee_info<'bin, 'conf>( &'bin mut self, rarity_items: &'conf GachaAvailableItemsInfo, category_tag: &String, target_pool: &'conf CharacterGachaPool, ) { let gachaconf = gacha::global_gacha_config(); let status_bin = self .gacha_status_map .get_mut(&target_pool.sharing_guarantee_info_category) .unwrap(); let progress_bin = status_bin .rarity_status_map .get_mut(&rarity_items.rarity) .unwrap(); for policy_tag in rarity_items.category_guarantee_policy_tags.iter() { let policy = gachaconf .category_guarantee_policy_map .get(policy_tag) .unwrap(); // TODO: Chooseable guarantee not implemented let prev_failure = progress_bin .categories_progress_map .get_mut(policy_tag) .expect(&format!( "post_deserialize forgot StatusBin/rarity_status_map[{}]/categories_progress_map: {}", rarity_items.rarity, policy_tag )); if policy.included_category_tags.contains(category_tag) { *prev_failure = 0; } else { *prev_failure += 1; } } } fn update_discount<'bin, 'conf>( &'bin mut self, target_pool: &'conf CharacterGachaPool, category_tag: &String, rarity_items: &GachaAvailableItemsInfo, ) { let gachaconf = gacha::global_gacha_config(); for (policy_tag, policy) in gachaconf.discount_policies.must_gain_item_map.iter() { if *category_tag != policy.category_tag { continue; } if !target_pool.discount_policy_tags.contains(policy_tag) { continue; } let status_bin = self .gacha_status_map .get_mut(&target_pool.sharing_guarantee_info_category) .unwrap(); let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap(); if *usage < policy.use_limit { *usage += 1; } } for (policy_tag, policy) in gachaconf.discount_policies.advanced_guarantee_map.iter() { if rarity_items.rarity != policy.rarity { continue; } if !target_pool.discount_policy_tags.contains(policy_tag) { continue; } let status_bin = self .gacha_status_map .get_mut(&target_pool.sharing_guarantee_info_category) .unwrap(); let usage = status_bin.discount_usage_map.get_mut(policy_tag).unwrap(); if *usage < policy.use_limit { *usage += 1; } } for (policy_tag, policy) in gachaconf.discount_policies.free_select_map.iter() { if !target_pool.discount_policy_tags.contains(policy_tag) { continue; } let status_bin = self .gacha_status_map .get_mut(&target_pool.sharing_guarantee_info_category) .unwrap(); let progress = status_bin .discount_usage_map .get_mut(&policy.free_select_progress_record_tag) .unwrap(); *progress += 1; } } } fn get_or_add<'a, K: Eq + PartialEq + Hash + Clone, V: Default, S: BuildHasher>( map: &'a mut HashMap, key: &K, ) -> &'a mut V { if !map.contains_key(key) { map.insert(key.clone(), V::default()); } map.get_mut(key).unwrap() } fn determine_gacha_result<'bin, 'conf>( pull_time: &DateTime, category: &'conf GachaCategoryInfo, target_pool: &'conf CharacterGachaPool, status_bin: &'bin GachaStatus, progress_bin: &'bin GachaProgress, rarity_items: &'conf GachaAvailableItemsInfo, ) -> GachaRecord { let gachaconf = gacha::global_gacha_config(); let item_pool_len = category.item_ids.len() as u32; let mut item_id: Option<&u32> = None; // We should see whether user's search priority exists. for guarantee_policy_tag in rarity_items.category_guarantee_policy_tags.iter() { let category_guarantee_policy = gachaconf .category_guarantee_policy_map .get(guarantee_policy_tag) .unwrap(); if !category_guarantee_policy.chooseable { continue; } // Firstly, judge whether the user failed enough times. // The user is limited to get only this category's item, // so we should record the user's failure to get his // selected item elsewhere. if progress_bin .categories_chosen_guarantee_progress_map .get(guarantee_policy_tag) .unwrap() < &category_guarantee_policy.trigger_on_failure_times { continue; } // We directly look whether user chose an UP item. if let Some(item) = progress_bin .categories_chosen_guarantee_item_map .get(guarantee_policy_tag) { item_id = Some(item); } } let item_id = match item_id { Some(val) => val, None => category .item_ids .get(rand::thread_rng().gen_range(0..item_pool_len) as usize) .unwrap(), }; let mut extra_item_id: Option = None; let mut extra_item_count: u32 = 0; for extra_items_policy_tag in rarity_items.extra_items_policy_tags.iter() { let extra_items_policy = gachaconf .extra_items_policy_map .get(extra_items_policy_tag) .unwrap(); // TODO: apply_on_owned_count in a context with bag // TODO: That's what RoleModel should do, not me. if extra_items_policy.apply_on_owned_count == 0 { extra_item_id = ItemID::new(extra_items_policy.id); extra_item_count = extra_items_policy.count; } } let extra_resources = match extra_item_id { Some(item_id) => Some(GachaExtraResources { extra_item_id: item_id, extra_item_count, }), None => None, }; GachaRecord { pull_timestamp: pull_time.timestamp(), obtained_item_id: ItemID::new_unchecked(item_id.clone()), gacha_id: target_pool.gacha_schedule_id.clone(), progress_map: status_bin.rarity_status_map.clone(), extra_resources, item_type: category.item_type.clone(), } }