JaneDoe-ZS/nap_data/src/gacha/gacha_config.rs
2024-08-06 22:58:24 +08:00

305 lines
10 KiB
Rust

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}).");
}
}
}
#[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,
}
}
}
#[derive(Debug, Default, Deserialize)]
pub struct GachaCategoryInfo {
#[serde(default)]
pub is_promotional_items: bool,
pub item_ids: Vec<u32>,
pub category_weight: u32,
#[serde(deserialize_with = "from_str")]
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);
}
}
}