wicked-waifus-rs/wicked-waifus-game-server/src/logic/gacha/gacha_pool.rs
2025-05-16 11:02:03 +00:00

267 lines
8.8 KiB
Rust

use rand::prelude::IndexedRandom;
use rand::Rng;
use wicked_waifus_protocol::{ErrorCode, GachaResult, GachaReward};
use wicked_waifus_data::GachaViewTypeInfoId::{
BeginnersChoiceConvene, FeaturedResonatorConvene, FeaturedWeaponConvene,
MultipleChoiceResonatorConvene, MultipleChoiceWeaponConvene, NoviceConvene,
StandardResonatorConvene, StandardWeaponConvene,
};
use crate::logic::gacha::pool_info::PoolInfo;
use crate::logic::player::Player;
use crate::logic::role::Role;
pub struct PoolRates {
pub three_star: f32,
pub four_star: f32,
pub five_star: f32,
}
impl Default for PoolRates {
fn default() -> Self {
Self {
three_star: 93.2,
four_star: 20.0, // 6.0
five_star: 30.0, // 0.8
}
}
}
pub struct GachaPool {
pub info: PoolInfo,
pub rates: PoolRates,
pub pull_count: i32,
pub pity_four: i32,
pub daily_pulls: i32,
pub total_pulls: i32,
pub rate_up: bool, // TODO: persistence state within player data
}
impl GachaPool {
pub(crate) fn new(info: PoolInfo) -> Self {
Self {
info,
rates: PoolRates::default(),
pull_count: 0,
pity_four: 0,
daily_pulls: 0,
total_pulls: 0,
rate_up: false,
}
}
pub fn pull<T: Rng>(
&mut self,
rng: &mut T,
player: &mut Player,
) -> Result<GachaResult, ErrorCode> {
self.check_limits()?;
let result = if (self.info.pool_type == BeginnersChoiceConvene)
&& (self.info.pool_id > 50)
&& (self.info.pool_id < 60)
{
let item_id = self.info.guaranteed_character_id.unwrap();
GachaResult {
gacha_reward: Some(GachaReward {
item_id,
item_count: 1,
}),
extra_rewards: self.calculate_extra_rewards(2),
transform_rewards: Self::get_transform_rewards(player, item_id),
bottom: None,
}
} else {
let rarity = self.determine_rarity(&self.calculate_probabilities(), rng);
let item_id = match self.info.pool_type {
FeaturedResonatorConvene => {
let item_id = if rarity == 2 {
if self.rate_up || rng.random_bool(0.5) {
self.rate_up = false;
self.info.guaranteed_character_id.unwrap_or(0)
} else {
self.rate_up = true;
*[1203, 1405, 1104, 1503, 1301].choose(rng).unwrap()
}
} else {
self.get_random_item(rarity, rng)
};
item_id
}
_ => self.get_random_item(rarity, rng),
};
self.update_pity(rarity);
GachaResult {
gacha_reward: Some(GachaReward {
item_id,
item_count: 1,
}),
extra_rewards: self.calculate_extra_rewards(rarity),
transform_rewards: Self::get_transform_rewards(player, item_id),
bottom: None,
}
};
self.update_limits();
Ok(result)
}
fn get_transform_rewards(player: &mut Player, item_id: i32) -> Vec<GachaReward> {
let mut transform_rewards = Vec::new();
let required_role_ids: Vec<i32> = Role::get_all_roles_except_mc();
match player.role_list.get(&item_id) {
None => {
if required_role_ids.contains(&item_id) {
player.role_list.insert(item_id, Role::new(item_id));
}
}
Some(role) => {
// TODO: Even if we have, we can't get more than six wavebands, make a check
transform_rewards.push(GachaReward {
item_id: 10000000 + role.role_id,
item_count: 1,
}) // TODO: get from role data
}
}
transform_rewards
}
fn get_random_item(&self, rarity: usize, rng: &mut impl Rng) -> i32 {
let items: &[i32] = match rarity {
0 => &[
21010013, 21020013, 21030013, 21040013, 21050013, 21010023, 21020023, 21030023,
21040023, 21050023, 21010043, 21020043, 21030043, 21040043, 21050043,
],
1 => match self.info.pool_type {
StandardWeaponConvene => &[
21010024, 21020024, 21030024, 21040024, 21050024, 21010044, 21020044, 21030044,
21040044, 21050044, 21010064, 21020064, 21030064, 21040064, 21050064,
],
FeaturedResonatorConvene | FeaturedWeaponConvene => {
&self.info.rate_up_four_star[..]
}
_ => &[1303, 1602, 1102, 1204, 1403, 1103, 1402, 1202, 1601],
},
2 => match self.info.pool_type {
NoviceConvene | StandardResonatorConvene => &[1405, 1301, 1503, 1104, 1203],
// TODO: Review MultipleChoiceConvene
FeaturedResonatorConvene
| FeaturedWeaponConvene
| StandardWeaponConvene
| BeginnersChoiceConvene
| MultipleChoiceResonatorConvene
| MultipleChoiceWeaponConvene => &self.info.rate_up_five_star[..],
},
_ => unreachable!(),
};
*items.choose(rng).unwrap_or(&0)
}
fn check_limits(&self) -> Result<(), ErrorCode> {
if self.daily_pulls >= self.info.daily_limit && self.info.daily_limit != 0 {
return Err(ErrorCode::ErrGachaDailyTimesLimit);
}
if self.total_pulls >= self.info.total_limit && self.info.total_limit != 0 {
return Err(ErrorCode::ErrGachaTotalTimesLimit);
}
Ok(())
}
fn update_limits(&mut self) {
self.daily_pulls = 1;
self.total_pulls = 1;
}
fn calculate_probabilities(&self) -> [f32; 3] {
let mut prob = [
self.rates.three_star,
self.rates.four_star,
self.rates.five_star,
];
if self.pull_count >= self.info.pity_system.soft_pity_start {
let extra_prob =
0.8 + 8.0 * (self.pull_count - self.info.pity_system.soft_pity_start - 1) as f32;
prob[0] -= extra_prob;
prob[2] = extra_prob;
}
match (
self.pity_four + 1 >= self.info.pity_system.hard_pity_four,
self.pull_count + 1 >= self.info.pity_system.hard_pity_five,
) {
(true, _) => [0.0, 100.0, 0.0],
(_, true) => [0.0, 0.0, 100.0],
_ => prob,
}
}
fn determine_rarity(&self, prob: &[f32; 3], rng: &mut impl Rng) -> usize {
let roll: f32 = rng.random_range(0.0..100.0);
match (roll < prob[2], roll < prob[2] + prob[1]) {
(true, _) => 2, // 5-star
(_, true) => 1, // 4-star
_ => 0, // 3-star
}
}
fn update_pity(&mut self, rarity: usize) {
self.pity_four = match rarity {
1 => 0,
_ => self.pity_four + 1,
};
self.pull_count = match rarity {
2 => 0,
_ => self.pull_count + 1,
};
// reset rate_up if a 5-star was pulled
if rarity == 2 {
self.rate_up = false;
}
}
fn calculate_extra_rewards(&self, rarity: usize) -> Vec<GachaReward> {
let mut rewards = Vec::new();
if rarity == 0 {
rewards.push(GachaReward {
item_id: 50003, // oscillate corals
item_count: 15,
});
}
/*
TODO: update rewards for duplicates
4-star duplicate:
1st to 6th duplicate: 3 afterglow corals and 1 waveband of that char
7th duplicate onwards: 8 afterglow corals
will not receive any afterglow corals when you pull a 4-star that you do not already own for the first time.
5-star duplicate:
1st to 6th duplicate: 15 afterglow corals and 1 waveband of that char
7th duplicate onwards: 40 afterglow corals
will not receive any afterglow corals when you pull a 5-star that you do not already own for the first time.
*/
match rarity {
2 => rewards.push(GachaReward {
item_id: 50004, // afterglow corals
item_count: 15,
}),
1 => rewards.push(GachaReward {
item_id: 50004, // afterglow corals
item_count: 3,
}),
_ => {}
}
rewards
}
pub fn is_active(&self) -> bool {
self.info.is_active()
}
}