use std::collections::{HashMap, HashSet}; use chrono::{prelude::Local, DateTime}; use serde::{Deserialize, Deserializer}; use tracing; #[derive(Debug, Default, Deserialize)] pub struct ExtraItemsPolicy { pub id: u32, pub count: u32, #[serde(default)] pub apply_on_owned_count: u32, } #[derive(Debug, Default, Deserialize)] pub struct ProbabilityPoint { pub start_pity: u32, pub start_chance_percent: f64, #[serde(default)] pub increment_percent: f64, } #[derive(Debug, Default, Deserialize)] pub struct ProbabilityModel { #[serde(default)] pub clear_status_on_higher_rarity_pulled: bool, pub points: Vec, // This value is for display only, so it's set when // the maximum guarantee is not equal to the // automatically calculated value (commonly, less than). #[serde(default)] pub maximum_guarantee_pity: u32, #[serde(skip_deserializing)] probability_percents: Vec, } impl ProbabilityModel { fn get_maximum_guarantee(&self) -> u32 { self.probability_percents.len() as u32 - 1 } pub fn post_configure(&mut self, tag: &String) { self.points.sort_by_key(|point| point.start_pity); let mut probability_percents: Vec = vec![0.0]; for (i, point) in self.points.iter().enumerate() { if i > 0 { let last_point = &self.points[i - 1]; let last_stop_percent = last_point.start_chance_percent + last_point.increment_percent * (point.start_pity - last_point.start_pity) as f64; if last_stop_percent > point.start_chance_percent { tracing::warn!("Gacha - ProbabilityModel '{tag}': The start chance of '{point:?}' is less than the value inherited from the previous point."); } } let mut max_pity = 2000; if i < self.points.len() - 1 { let next_point = &self.points[i + 1]; max_pity = next_point.start_pity - 1; let max_probability = point.start_chance_percent + point.increment_percent * (next_point.start_pity - 1 - point.start_pity) as f64; assert!(max_probability < 100.0, "Gacha - ProbabilityModel '{tag}': Probability already reached 100% in '{point:?}' (though points with higher pity left)"); } let mut pity = point.start_pity; let mut percent = point.start_chance_percent; while pity <= max_pity { if max_pity >= 2000 && percent >= 100.0 { probability_percents.push(100.0); break; } probability_percents.push(percent); percent += point.increment_percent; pity += 1; } assert!(pity <= 2000, "Gacha - ProbabilityModel '{tag}' (point {i}): Haven't reached 100% guarantee probability at Pity 2001. The current probability is {percent}%. Crazy."); } self.probability_percents = probability_percents; if self.maximum_guarantee_pity <= 0 { self.maximum_guarantee_pity = self.get_maximum_guarantee(); } } pub fn get_chance_percent(&self, pity: &u32) -> f64 { // The vec length is 1 bigger than the maximum pity (1-based) let guarantee_pity = self.probability_percents.len() - 1; let idx = *pity as usize; if idx > guarantee_pity { return self.probability_percents[guarantee_pity]; } self.probability_percents[idx] } } #[allow(dead_code)] #[derive(Debug, Default, Deserialize)] pub struct CategoryGuaranteePolicy { pub included_category_tags: HashSet, pub trigger_on_failure_times: u32, pub clear_status_on_target_changed: bool, pub chooseable: bool, } #[derive(Debug, Default, Deserialize)] pub struct TenPullDiscount { pub use_limit: u32, pub discounted_prize: u32, } #[derive(Debug, Default, Deserialize)] pub struct AdvancedGuarantee { pub use_limit: u32, pub rarity: u32, pub guarantee_pity: u32, } #[derive(Debug, Default, Deserialize)] pub struct MustGainItem { pub use_limit: u32, pub rarity: u32, pub category_tag: String, } #[derive(Debug, Default, Deserialize)] pub struct FreeSelectItem { pub milestones: Vec, pub rarity: u32, pub category_tags: Vec, pub free_select_progress_record_tag: String, pub free_select_usage_record_tag: String, } #[derive(Debug, Default, Deserialize)] pub struct DiscountPolicyCollection { pub ten_pull_discount_map: HashMap, pub must_gain_item_map: HashMap, pub advanced_guarantee_map: HashMap, pub free_select_map: HashMap, } impl DiscountPolicyCollection { pub fn post_configure(&mut self) { for (tag, ten_pull_discount) in self.ten_pull_discount_map.iter() { let discounted_prize = ten_pull_discount.discounted_prize; assert!(discounted_prize < 10, "Gacha - DiscountPolicy '{tag}': ten_pull_discount's value should be smaller than 10 (read {discounted_prize})."); } } } #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum GachaAddedItemType { #[default] None = 0, Weapon = 1, Character = 2, Bangboo = 3, } impl GachaAddedItemType { pub fn as_str_name(&self) -> &'static str { match self { GachaAddedItemType::None => "GACHA_ADDED_ITEM_TYPE_NONE", GachaAddedItemType::Weapon => "GACHA_ADDED_ITEM_TYPE_WEAPON", GachaAddedItemType::Character => "GACHA_ADDED_ITEM_TYPE_CHARACTER", GachaAddedItemType::Bangboo => "GACHA_ADDED_ITEM_TYPE_BANGBOO", } } pub fn from_str_name(value: &str) -> ::core::option::Option { match value { "GACHA_ADDED_ITEM_TYPE_NONE" => Some(Self::None), "GACHA_ADDED_ITEM_TYPE_WEAPON" => Some(Self::Weapon), "GACHA_ADDED_ITEM_TYPE_CHARACTER" => Some(Self::Character), "GACHA_ADDED_ITEM_TYPE_BANGBOO" => Some(Self::Bangboo), _ => None, } } } impl From for GachaAddedItemType { fn from(value: i32) -> Self { match value { 1 => Self::Weapon, 2 => Self::Character, 3 => Self::Bangboo, _ => Self::None } } } impl Into for GachaAddedItemType { fn into(self) -> i32 { match self { Self::Weapon => 1, Self::Character => 2, Self::Bangboo => 3, Self::None => 0, } } } #[derive(Debug, Default, Deserialize)] pub struct GachaCategoryInfo { #[serde(default)] pub is_promotional_items: bool, pub item_ids: Vec, pub category_weight: u32, #[serde(deserialize_with = "from_str")] pub item_type: GachaAddedItemType, } pub fn from_str<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer)?; let result = GachaAddedItemType::from_str_name(&s); match result { Some(val) => Ok(val), None => Ok(GachaAddedItemType::None) } } #[derive(Debug, Default, Deserialize)] pub struct GachaAvailableItemsInfo { pub rarity: u32, #[serde(default)] pub extra_items_policy_tags: Vec, pub categories: HashMap, pub probability_model_tag: String, #[serde(default)] pub category_guarantee_policy_tags: Vec, } #[allow(dead_code)] #[derive(Debug, Default, Deserialize)] pub struct CharacterGachaPool { pub gacha_schedule_id: u32, pub gacha_parent_schedule_id: u32, pub comment: String, pub gacha_type: u32, pub cost_item_id: u32, pub start_time: DateTime, pub end_time: DateTime, #[serde(default)] pub discount_policy_tags: Vec, pub sharing_guarantee_info_category: String, pub gacha_items: Vec, } impl CharacterGachaPool { pub fn is_still_open(&self, now: &DateTime) -> bool { self.start_time <= *now && *now <= self.end_time } pub fn post_configure(&mut self, probability_model_map: &HashMap) { self.gacha_items .sort_by_key(|item_list| u32::MAX - item_list.rarity); for items_info in self.gacha_items.iter_mut() { assert!(probability_model_map.contains_key(&items_info.probability_model_tag), "Gacha - CharacterGachaPool '{}': Specified ProbabilityModel tag '{}' that does not exist.", self.gacha_schedule_id, items_info.probability_model_tag); } } } #[derive(Debug, Default, Deserialize)] pub struct GachaCommonProperties { pub up_item_category_tag: String, pub s_item_rarity: u32, pub a_item_rarity: u32, // TODO: PostConfigure check pub ten_pull_discount_tag: String, pub newcomer_advanced_s_tag: String, } #[derive(Debug, Default, Deserialize)] pub struct GachaConfiguration { pub character_gacha_pool_list: Vec, pub probability_model_map: HashMap, pub category_guarantee_policy_map: HashMap, pub extra_items_policy_map: HashMap, pub discount_policies: DiscountPolicyCollection, pub common_properties: GachaCommonProperties, } impl GachaConfiguration { pub fn post_configure(&mut self) { assert!( self.category_guarantee_policy_map .contains_key(&self.common_properties.up_item_category_tag), "The UP category should be valid in policy map." ); for (tag, policy) in self.probability_model_map.iter_mut() { policy.post_configure(&tag); } self.discount_policies.post_configure(); for character_pool in self.character_gacha_pool_list.iter_mut() { character_pool.post_configure(&self.probability_model_map); } } }