JaneDoe-ZS/nap_data/src/gacha/gacha_config.rs

306 lines
10 KiB
Rust
Raw Normal View History

Gacha System implementation ## Abstract This PR implements - The Gacha System for client. Player can pull in defined pools with a similar experience to zenless & mihoyo gacha, and these status can be saved through player save and recovered. - `player kick` command in extra. Command `player kick [player_uid] (optional: [reason_id])` can be used to kick a player to offline. ## Support list - Similar probability to mihoyo gacha - QingYi & Jane Doe's Agent & W-Engine banner - Standard Banner: - Get a S Agent in the first 50 pulls - Get a 20% discount for the first 5 10-pulls - Choose a S Agent once you pulled for 300 - ~~Bangboo Banner~~ (not working due to other reasons) - Choosing your demanded S bangboo for 100% UP ## Principle - A complex configuration file `gacha.jsonc` is introduced to define all behaviours. - Gacha status is saved via `GachaModelBin`. For more informations about the config and save, an article is available [here](https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/). ## Known issues - You can not see any bangboos in the collection when choosing Bangboo. - **Specifically for 1.1 Beta**, performing gacha may lead to random client issues, including: - The TVs showing rarity ending up in blank after its flash. - Game become black screen. - If clicking 'Skip' but not fast enough, game'll stuck and not able to do anything. You may try to click 'Skip' scarcely when 'REC' shows, or after all animations has stopped. Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Reviewed-on: https://git.xeondev.com/NewEriduPubSec/JaneDoe-ZS/pulls/1 Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com> Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
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}).");
}
}
}
#[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,
}
}
}
Gacha System implementation ## Abstract This PR implements - The Gacha System for client. Player can pull in defined pools with a similar experience to zenless & mihoyo gacha, and these status can be saved through player save and recovered. - `player kick` command in extra. Command `player kick [player_uid] (optional: [reason_id])` can be used to kick a player to offline. ## Support list - Similar probability to mihoyo gacha - QingYi & Jane Doe's Agent & W-Engine banner - Standard Banner: - Get a S Agent in the first 50 pulls - Get a 20% discount for the first 5 10-pulls - Choose a S Agent once you pulled for 300 - ~~Bangboo Banner~~ (not working due to other reasons) - Choosing your demanded S bangboo for 100% UP ## Principle - A complex configuration file `gacha.jsonc` is introduced to define all behaviours. - Gacha status is saved via `GachaModelBin`. For more informations about the config and save, an article is available [here](https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/). ## Known issues - You can not see any bangboos in the collection when choosing Bangboo. - **Specifically for 1.1 Beta**, performing gacha may lead to random client issues, including: - The TVs showing rarity ending up in blank after its flash. - Game become black screen. - If clicking 'Skip' but not fast enough, game'll stuck and not able to do anything. You may try to click 'Skip' scarcely when 'REC' shows, or after all animations has stopped. Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Reviewed-on: https://git.xeondev.com/NewEriduPubSec/JaneDoe-ZS/pulls/1 Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com> Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
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,
#[serde(deserialize_with = "from_str")]
Gacha System implementation ## Abstract This PR implements - The Gacha System for client. Player can pull in defined pools with a similar experience to zenless & mihoyo gacha, and these status can be saved through player save and recovered. - `player kick` command in extra. Command `player kick [player_uid] (optional: [reason_id])` can be used to kick a player to offline. ## Support list - Similar probability to mihoyo gacha - QingYi & Jane Doe's Agent & W-Engine banner - Standard Banner: - Get a S Agent in the first 50 pulls - Get a 20% discount for the first 5 10-pulls - Choose a S Agent once you pulled for 300 - ~~Bangboo Banner~~ (not working due to other reasons) - Choosing your demanded S bangboo for 100% UP ## Principle - A complex configuration file `gacha.jsonc` is introduced to define all behaviours. - Gacha status is saved via `GachaModelBin`. For more informations about the config and save, an article is available [here](https://yyheggegg.github.io/mihomo-gio-blogs/zzz-gacha-impl-en/). ## Known issues - You can not see any bangboos in the collection when choosing Bangboo. - **Specifically for 1.1 Beta**, performing gacha may lead to random client issues, including: - The TVs showing rarity ending up in blank after its flash. - Game become black screen. - If clicking 'Skip' but not fast enough, game'll stuck and not able to do anything. You may try to click 'Skip' scarcely when 'REC' shows, or after all animations has stopped. Co-authored-by: YYHEggEgg <53960525+YYHEggEgg@users.noreply.github.com> Reviewed-on: https://git.xeondev.com/NewEriduPubSec/JaneDoe-ZS/pulls/1 Co-authored-by: YYHEggEgg <yyheggegg@xeondev.com> Co-committed-by: YYHEggEgg <yyheggegg@xeondev.com>
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);
}
}
}