2024-08-04 11:41:23 +00:00
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 < ProbabilityPoint > ,
// 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 < f64 > ,
}
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 < f64 > = 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 < String > ,
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 < u32 > ,
pub rarity : u32 ,
pub category_tags : Vec < String > ,
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 < String , TenPullDiscount > ,
pub must_gain_item_map : HashMap < String , MustGainItem > ,
pub advanced_guarantee_map : HashMap < String , AdvancedGuarantee > ,
pub free_select_map : HashMap < String , FreeSelectItem > ,
}
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}). " ) ;
}
}
}
2024-08-06 09:02:49 +00:00
#[ 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 < Self > {
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 < i32 > for GachaAddedItemType {
fn from ( value : i32 ) -> Self {
match value {
1 = > Self ::Weapon ,
2 = > Self ::Character ,
3 = > Self ::Bangboo ,
_ = > Self ::None
}
}
}
impl Into < i32 > for GachaAddedItemType {
fn into ( self ) -> i32 {
match self {
Self ::Weapon = > 1 ,
Self ::Character = > 2 ,
Self ::Bangboo = > 3 ,
Self ::None = > 0 ,
}
}
}
2024-08-04 11:41:23 +00:00
#[ derive(Debug, Default, Deserialize) ]
pub struct GachaCategoryInfo {
#[ serde(default) ]
pub is_promotional_items : bool ,
pub item_ids : Vec < u32 > ,
pub category_weight : u32 ,
2024-08-06 09:02:49 +00:00
#[ serde(deserialize_with = " from_str " ) ]
2024-08-04 11:41:23 +00:00
pub item_type : GachaAddedItemType ,
}
pub fn from_str < ' de , D > ( deserializer : D ) -> Result < GachaAddedItemType , D ::Error >
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 < String > ,
pub categories : HashMap < String , GachaCategoryInfo > ,
pub probability_model_tag : String ,
#[ serde(default) ]
pub category_guarantee_policy_tags : Vec < String > ,
}
#[ 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 < Local > ,
pub end_time : DateTime < Local > ,
#[ serde(default) ]
pub discount_policy_tags : Vec < String > ,
pub sharing_guarantee_info_category : String ,
pub gacha_items : Vec < GachaAvailableItemsInfo > ,
}
impl CharacterGachaPool {
pub fn is_still_open ( & self , now : & DateTime < Local > ) -> bool {
self . start_time < = * now & & * now < = self . end_time
}
pub fn post_configure ( & mut self , probability_model_map : & HashMap < String , ProbabilityModel > ) {
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 < CharacterGachaPool > ,
pub probability_model_map : HashMap < String , ProbabilityModel > ,
pub category_guarantee_policy_map : HashMap < String , CategoryGuaranteePolicy > ,
pub extra_items_policy_map : HashMap < String , ExtraItemsPolicy > ,
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 ) ;
}
}
}