diff --git a/src/taiko/attributes.rs b/src/taiko/attributes.rs index 5748bdb0..fe1b4be5 100644 --- a/src/taiko/attributes.rs +++ b/src/taiko/attributes.rs @@ -15,6 +15,9 @@ pub struct TaikoDifficultyAttributes { pub great_hit_window: f64, /// The perceived hit window for an n100 inclusive of rate-adjusting mods (DT/HT/etc) pub ok_hit_window: f64, + /// The ratio of stamina difficulty from mono-color (single color) streams to total + /// stamina difficulty. + pub mono_stamina_factor: f64, /// The final star rating. pub stars: f64, /// The maximum combo. diff --git a/src/taiko/difficulty/gradual.rs b/src/taiko/difficulty/gradual.rs index 3ebfb53c..2dd54789 100644 --- a/src/taiko/difficulty/gradual.rs +++ b/src/taiko/difficulty/gradual.rs @@ -153,6 +153,8 @@ impl Iterator for TaikoGradualDifficulty { Skill::new(&mut self.skills.rhythm, &self.diff_objects).process(&borrowed); Skill::new(&mut self.skills.color, &self.diff_objects).process(&borrowed); Skill::new(&mut self.skills.stamina, &self.diff_objects).process(&borrowed); + Skill::new(&mut self.skills.single_color_stamina, &self.diff_objects) + .process(&borrowed); if borrowed.base_hit_type.is_hit() { self.attrs.max_combo += 1; @@ -231,6 +233,8 @@ impl Iterator for TaikoGradualDifficulty { let mut rhythm = Skill::new(&mut self.skills.rhythm, &self.diff_objects); let mut color = Skill::new(&mut self.skills.color, &self.diff_objects); let mut stamina = Skill::new(&mut self.skills.stamina, &self.diff_objects); + let mut single_color_stamina = + Skill::new(&mut self.skills.single_color_stamina, &self.diff_objects); for _ in 0..take { loop { @@ -239,6 +243,7 @@ impl Iterator for TaikoGradualDifficulty { rhythm.process(&borrowed); color.process(&borrowed); stamina.process(&borrowed); + single_color_stamina.process(&borrowed); if borrowed.base_hit_type.is_hit() { self.attrs.max_combo += 1; diff --git a/src/taiko/difficulty/mod.rs b/src/taiko/difficulty/mod.rs index 9395fadf..26299a5e 100644 --- a/src/taiko/difficulty/mod.rs +++ b/src/taiko/difficulty/mod.rs @@ -142,11 +142,14 @@ impl DifficultyValues { let mut rhythm = Skill::new(&mut skills.rhythm, &diff_objects); let mut color = Skill::new(&mut skills.color, &diff_objects); let mut stamina = Skill::new(&mut skills.stamina, &diff_objects); + let mut single_color_stamina = + Skill::new(&mut skills.single_color_stamina, &diff_objects); for hit_object in diff_objects.iter().take(n_diff_objects) { rhythm.process(&hit_object.get()); color.process(&hit_object.get()); stamina.process(&hit_object.get()); + single_color_stamina.process(&hit_object.get()); } } @@ -157,15 +160,32 @@ impl DifficultyValues { let color_rating = skills.color.as_difficulty_value() * COLOR_SKILL_MULTIPLIER; let rhythm_rating = skills.rhythm.as_difficulty_value() * RHYTHM_SKILL_MULTIPLIER; let stamina_rating = skills.stamina.as_difficulty_value() * STAMINA_SKILL_MULTIPLIER; + let mono_stamina_rating = + skills.single_color_stamina.as_difficulty_value() * STAMINA_SKILL_MULTIPLIER; + let mono_stamina_factor = if stamina_rating.abs() >= f64::EPSILON { + (mono_stamina_rating / stamina_rating).powf(5.0) + } else { + 1.0 + }; let combined_rating = combined_difficulty_value(skills.color, skills.rhythm, skills.stamina); - let star_rating = rescale(combined_rating * 1.4); + let mut star_rating = rescale(combined_rating * 1.4); + + // * TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. + if attrs.is_convert { + star_rating *= 0.925; + // * For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused. + if color_rating < 2.0 && stamina_rating > 8.0 { + star_rating *= 0.80; + } + } attrs.stamina = stamina_rating; attrs.rhythm = rhythm_rating; attrs.color = color_rating; attrs.peak = combined_rating; + attrs.mono_stamina_factor = mono_stamina_factor; attrs.stars = star_rating; } diff --git a/src/taiko/difficulty/skills/mod.rs b/src/taiko/difficulty/skills/mod.rs index bf032a84..b6bdf906 100644 --- a/src/taiko/difficulty/skills/mod.rs +++ b/src/taiko/difficulty/skills/mod.rs @@ -9,6 +9,7 @@ pub struct TaikoSkills { pub rhythm: Rhythm, pub color: Color, pub stamina: Stamina, + pub single_color_stamina: Stamina, } impl TaikoSkills { @@ -16,7 +17,8 @@ impl TaikoSkills { Self { rhythm: Rhythm::default(), color: Color::default(), - stamina: Stamina::default(), + stamina: Stamina::new(false), + single_color_stamina: Stamina::new(true), } } } diff --git a/src/taiko/difficulty/skills/stamina.rs b/src/taiko/difficulty/skills/stamina.rs index acbc7ef7..2aaa0f4e 100644 --- a/src/taiko/difficulty/skills/stamina.rs +++ b/src/taiko/difficulty/skills/stamina.rs @@ -1,24 +1,34 @@ use crate::{ any::difficulty::{ object::IDifficultyObject, - skills::{strain_decay, ISkill, Skill, StrainDecaySkill}, + skills::{strain_decay, ISkill, Skill, StrainDecaySkill, StrainSkill}, }, taiko::{ difficulty::object::{TaikoDifficultyObject, TaikoDifficultyObjects}, object::HitType, }, - util::strains_vec::StrainsVec, + util::{strains_vec::StrainsVec, sync::Weak}, }; const SKILL_MULTIPLIER: f64 = 1.1; const STRAIN_DECAY_BASE: f64 = 0.4; -#[derive(Clone, Default)] +#[derive(Clone)] pub struct Stamina { - inner: StrainDecaySkill, + inner: StrainSkill, + single_color: bool, + curr_strain: f64, } impl Stamina { + pub fn new(single_color: bool) -> Self { + Self { + inner: StrainSkill::default(), + single_color, + curr_strain: 0.0, + } + } + pub fn get_curr_strain_peaks(self) -> StrainsVec { self.inner.get_curr_strain_peaks() } @@ -36,6 +46,10 @@ impl ISkill for Stamina { impl Skill<'_, Stamina> { fn calculate_initial_strain(&mut self, time: f64, curr: &TaikoDifficultyObject) -> f64 { + if self.inner.single_color { + return 0.0; + } + let prev_start_time = curr .previous(0, &self.diff_objects.objects) .map_or(0.0, |prev| prev.get().start_time); @@ -44,27 +58,27 @@ impl Skill<'_, Stamina> { } const fn curr_strain(&self) -> f64 { - self.inner.inner.curr_strain + self.inner.curr_strain } fn curr_strain_mut(&mut self) -> &mut f64 { - &mut self.inner.inner.curr_strain + &mut self.inner.curr_strain } const fn curr_section_peak(&self) -> f64 { - self.inner.inner.inner.curr_section_peak + self.inner.inner.curr_section_peak } fn curr_section_peak_mut(&mut self) -> &mut f64 { - &mut self.inner.inner.inner.curr_section_peak + &mut self.inner.inner.curr_section_peak } const fn curr_section_end(&self) -> f64 { - self.inner.inner.inner.curr_section_end + self.inner.inner.curr_section_end } fn curr_section_end_mut(&mut self) -> &mut f64 { - &mut self.inner.inner.inner.curr_section_end + &mut self.inner.inner.curr_section_end } pub fn process(&mut self, curr: &TaikoDifficultyObject) { @@ -86,13 +100,30 @@ impl Skill<'_, Stamina> { fn strain_value_at(&mut self, curr: &TaikoDifficultyObject) -> f64 { *self.curr_strain_mut() *= strain_decay(curr.delta_time, STRAIN_DECAY_BASE); - *self.curr_strain_mut() += self.strain_value_of(curr) * SKILL_MULTIPLIER; - - self.curr_strain() - } - - fn strain_value_of(&self, curr: &TaikoDifficultyObject) -> f64 { - StaminaEvaluator::evaluate_diff_of(curr, self.diff_objects) + *self.curr_strain_mut() += + StaminaEvaluator::evaluate_diff_of(curr, self.diff_objects) * SKILL_MULTIPLIER; + + // Safely prevents previous strains from shifting as new notes are added. + let index = curr + .color + .mono_streak + .as_ref() + .and_then(Weak::upgrade) + .and_then(|mono| { + mono.get().hit_objects.iter().position(|h| { + let Some(h) = h.upgrade() else { return false }; + let h = h.get(); + + h.idx == curr.idx + }) + }) + .unwrap_or(0); + + if self.inner.single_color { + self.curr_strain() / (1.0 + ((-(index as isize - 10)) as f64 / 2.0).exp()) + } else { + self.curr_strain() + } } } diff --git a/src/taiko/performance/mod.rs b/src/taiko/performance/mod.rs index a5067dd4..bd422df8 100644 --- a/src/taiko/performance/mod.rs +++ b/src/taiko/performance/mod.rs @@ -416,12 +416,12 @@ impl TaikoPerformanceInner<'_> { let mut multiplier = 1.13; - if self.mods.hd() { + if self.mods.hd() && !self.attrs.is_convert { multiplier *= 1.075; } if self.mods.ez() { - multiplier *= 0.975; + multiplier *= 0.95; } let diff_value = @@ -459,10 +459,10 @@ impl TaikoPerformanceInner<'_> { diff_value *= 0.986_f64.powf(effective_miss_count); if self.mods.ez() { - diff_value *= 0.985; + diff_value *= 0.9; } - if self.mods.hd() && !self.attrs.is_convert { + if self.mods.hd() { diff_value *= 1.025; } @@ -471,11 +471,19 @@ impl TaikoPerformanceInner<'_> { } if self.mods.fl() { - diff_value *= 1.05 * len_bonus; + diff_value *= + (1.05 - (self.attrs.mono_stamina_factor / 50.0).min(1.0) * len_bonus).max(1.0); } + // * Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. + let acc_scaling_exp = f64::from(2) + self.attrs.mono_stamina_factor; + let acc_scaling_shift = f64::from(300) - f64::from(100) * self.attrs.mono_stamina_factor; + diff_value - * (special_functions::erf(400.0 / (2.0_f64.sqrt() * estimated_unstable_rate))).powf(2.0) + * (special_functions::erf( + acc_scaling_shift / (2.0_f64.sqrt() * estimated_unstable_rate), + )) + .powf(acc_scaling_exp) } fn compute_accuracy_value(&self, estimated_unstable_rate: Option) -> f64 { diff --git a/tests/difficulty.rs b/tests/difficulty.rs index 80fac8e2..1be41638 100644 --- a/tests/difficulty.rs +++ b/tests/difficulty.rs @@ -71,6 +71,7 @@ macro_rules! test_cases { rhythm: $rhythm:literal, color: $color:literal, peak: $peak:literal, + mono_stamina_factor: $mono_stamina_factor:literal, great_hit_window: $great_hit_window:literal, ok_hit_window: $ok_hit_window:literal, stars: $stars:literal, @@ -82,6 +83,7 @@ macro_rules! test_cases { rhythm: $rhythm, color: $color, peak: $peak, + mono_stamina_factor: $mono_stamina_factor, great_hit_window: $great_hit_window, ok_hit_window: $ok_hit_window, stars: $stars, @@ -362,6 +364,7 @@ fn basic_taiko() { rhythm: 0.20130047251681948, color: 1.0487315549761433, peak: 1.8422453377400778, + mono_stamina_factor: 2.66403971858592e-07, great_hit_window: 35.0, ok_hit_window: 80.0, stars: 2.914589700180437, @@ -373,6 +376,7 @@ fn basic_taiko() { rhythm: 0.20130047251681948, color: 1.0487315549761433, peak: 1.8422453377400778, + mono_stamina_factor: 2.66403971858592e-07, great_hit_window: 29.0, ok_hit_window: 68.0, stars: 2.914589700180437, @@ -384,6 +388,7 @@ fn basic_taiko() { rhythm: 0.4448175371191029, color: 1.363762496098889, peak: 2.625066421324458, + mono_stamina_factor: 2.515617502055679e-07, great_hit_window: 23.333333333333332, ok_hit_window: 53.333333333333336, stars: 3.942709244618132, @@ -403,9 +408,10 @@ fn convert_taiko() { rhythm: 1.4696991260446617, color: 2.303228172964907, peak: 4.117779264387738, + mono_stamina_factor: 0.0016957378202796742, great_hit_window: 23.59999942779541, ok_hit_window: 57.19999885559082, - stars: 5.660149021515273, + stars: 5.235637844901627, max_combo: 908, is_convert: true, }; @@ -414,9 +420,10 @@ fn convert_taiko() { rhythm: 1.4696991260446617, color: 2.303228172964907, peak: 4.117779264387738, + mono_stamina_factor: 0.0016957378202796742, great_hit_window: 20.0, - ok_hit_window: 50.0, - stars: 5.660149021515273, + ok_hit_window: 50.0 + stars: 5.235637844901627, max_combo: 908, is_convert: true, }; @@ -425,9 +432,10 @@ fn convert_taiko() { rhythm: 2.002843919169095, color: 3.1864894777399986, peak: 6.103209631166694, + mono_stamina_factor: 0.0017075184344987763, great_hit_window: 15.733332951863607, ok_hit_window: 38.13333257039388, - stars: 7.578560915020682, + stars: 7.010168846394131, max_combo: 908, is_convert: true, }; diff --git a/tests/performance.rs b/tests/performance.rs index 4e729051..13726b3e 100644 --- a/tests/performance.rs +++ b/tests/performance.rs @@ -230,30 +230,30 @@ fn basic_taiko() { test_cases! { Taiko: TAIKO { NM => { - pp: 117.93083232512124, + pp: 114.68651694107942, pp_acc: 67.10083752258917, - pp_difficulty: 43.804435430934774, + pp_difficulty: 40.6658183165898, effective_miss_count: 0.0, estimated_unstable_rate: Some(148.44150180469418), }; HD => { - pp: 127.99624094636974, + pp: 124.41592086295445, pp_acc: 67.10083752258917, - pp_difficulty: 44.89954631670814, + pp_difficulty: 41.68246377450454, effective_miss_count: 0.0, estimated_unstable_rate: Some(148.44150180469418), }; HR => { - pp: 139.75239372681187, + pp: 138.3981102935321, pp_acc: 82.52109686788792, - pp_difficulty: 48.75926757049594, + pp_difficulty: 47.44272798866182, effective_miss_count: 0.0, estimated_unstable_rate: Some(122.99438720960376), }; DT => { - pp: 220.51543873147975, + pp: 220.07140899937482, pp_acc: 118.28107309573312, - pp_difficulty: 89.35584221033577, + pp_difficulty: 88.93091255724303, effective_miss_count: 0.0, estimated_unstable_rate: Some(98.96100120312946), }; @@ -266,30 +266,30 @@ fn convert_taiko() { test_cases! { Taiko: OSU { NM => { - pp: 396.36982258196866, - pp_acc: 160.00481201044695, - pp_difficulty: 213.19920144243838, + pp: 353.6961706002712, + pp_acc: 155.09212159726567, + pp_difficulty: 178.19145253120928, effective_miss_count: 0.0, estimated_unstable_rate: Some(85.75868894575865), }; HD => { - pp: 426.0975592756163, - pp_acc: 160.00481201044695, - pp_difficulty: 213.19920144243838, + pp: 358.45704044422996, + pp_acc: 155.09212159726567, + pp_difficulty: 182.6462388444895, effective_miss_count: 0.0, estimated_unstable_rate: Some(85.75868894575865), }; HR => { - pp: 452.71458235192836, - pp_acc: 191.95668459371925, - pp_difficulty: 234.5205569790155, + pp: 405.57235351353773, + pp_acc: 186.06296332183615, + pp_difficulty: 196.1813610529617, effective_miss_count: 0.0, estimated_unstable_rate: Some(72.67685680089848), }; DT => { - pp: 739.7393581199891, - pp_acc: 280.8904545747157, - pp_difficulty: 415.0249135067657, + pp: 658.0214875413873, + pp_acc: 272.26616492989393, + pp_difficulty: 347.4712042359611, effective_miss_count: 0.0, estimated_unstable_rate: Some(57.17245929717244), };