start base
This commit is contained in:
+12
-1
@@ -138,13 +138,18 @@ impl<'map> OsuStars<'map> {
|
||||
1.0
|
||||
};
|
||||
|
||||
let aim_difficult_strain_count = skills::OsuStrainSkill::count_difficult_strains(&mut aim);
|
||||
|
||||
// currently unused as relax pp doesn't include speed
|
||||
let speed_difficult_strain_count =
|
||||
skills::OsuStrainSkill::count_difficult_strains(&mut speed);
|
||||
|
||||
if mods.td() {
|
||||
aim_rating = aim_rating.powf(0.8);
|
||||
flashlight_rating = flashlight_rating.powf(0.8);
|
||||
}
|
||||
|
||||
if mods.rx() {
|
||||
aim_rating *= 0.9;
|
||||
speed_rating = 0.0;
|
||||
flashlight_rating *= 0.7;
|
||||
}
|
||||
@@ -176,6 +181,8 @@ impl<'map> OsuStars<'map> {
|
||||
attrs.speed = speed_rating;
|
||||
attrs.flashlight = flashlight_rating;
|
||||
attrs.slider_factor = slider_factor;
|
||||
attrs.aim_difficult_strain_count = aim_difficult_strain_count;
|
||||
attrs.speed_difficult_strain_count = speed_difficult_strain_count;
|
||||
attrs.stars = star_rating;
|
||||
attrs.speed_note_count = speed_notes;
|
||||
|
||||
@@ -508,6 +515,10 @@ pub struct OsuDifficultyAttributes {
|
||||
pub flashlight: f64,
|
||||
/// The ratio of the aim strain with and without considering sliders
|
||||
pub slider_factor: f64,
|
||||
/// The count of difficult aim strains.
|
||||
pub aim_difficult_strain_count: f64,
|
||||
/// The count of difficult speed strains.
|
||||
pub speed_difficult_strain_count: f64,
|
||||
/// The number of clickable objects weighted by difficulty.
|
||||
pub speed_note_count: f64,
|
||||
/// The approach rate.
|
||||
|
||||
+25
-318
@@ -388,7 +388,7 @@ struct OsuPpInner {
|
||||
}
|
||||
|
||||
impl OsuPpInner {
|
||||
fn calculate(mut self) -> OsuPerformanceAttributes {
|
||||
fn calculate(self) -> OsuPerformanceAttributes {
|
||||
let total_hits = self.state.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
@@ -410,28 +410,6 @@ impl OsuPpInner {
|
||||
multiplier *= 1.0 - (self.attrs.n_spinners as f64 / total_hits).powf(0.85);
|
||||
}
|
||||
|
||||
if self.mods.rx() {
|
||||
// * https://www.desmos.com/calculator/bc9eybdthb
|
||||
// * we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
||||
// * this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
||||
let (n100_mult, n50_mult) = if self.attrs.od > 0.0 {
|
||||
(
|
||||
1.0 - (self.attrs.od / 13.33).powf(1.8),
|
||||
1.0 - (self.attrs.od / 13.33).powi(5),
|
||||
)
|
||||
} else {
|
||||
(1.0, 1.0)
|
||||
};
|
||||
|
||||
// * As we're adding Oks and Mehs to an approximated number of combo breaks the result can be
|
||||
// * higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
self.effective_miss_count = (self.effective_miss_count
|
||||
+ self.state.n100 as f64
|
||||
+ n100_mult
|
||||
+ self.state.n50 as f64 * n50_mult)
|
||||
.min(total_hits);
|
||||
}
|
||||
|
||||
let aim_value = self.compute_aim_value();
|
||||
let speed_value = self.compute_speed_value();
|
||||
let acc_value = self.compute_accuracy_value();
|
||||
@@ -460,25 +438,30 @@ impl OsuPpInner {
|
||||
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
let len_bonus = 0.95
|
||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||
+ (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
|
||||
let mut len_bonus = 1.0;
|
||||
if self.mods.rx() {
|
||||
if self.effective_miss_count > 0.0 {
|
||||
aim_value *= self.calculate_miss_penalty(self.attrs.aim_difficult_strain_count);
|
||||
}
|
||||
} else {
|
||||
len_bonus = 0.95
|
||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||
+ (total_hits > 2000.0) as u8 as f64 * (total_hits / 2000.0).log10() * 0.5;
|
||||
|
||||
aim_value *= len_bonus;
|
||||
aim_value *= len_bonus;
|
||||
|
||||
// * Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// * Default a 3% reduction for any # of misses.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
aim_value *= 0.97
|
||||
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
|
||||
.powf(self.effective_miss_count);
|
||||
// * Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// * Default a 3% reduction for any # of misses.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
aim_value *= 0.97
|
||||
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
|
||||
.powf(self.effective_miss_count);
|
||||
}
|
||||
|
||||
aim_value *= self.get_combo_scaling_factor();
|
||||
}
|
||||
|
||||
aim_value *= self.get_combo_scaling_factor();
|
||||
|
||||
let ar_factor = if self.mods.rx() {
|
||||
0.0
|
||||
} else if self.attrs.ar > 10.33 {
|
||||
let ar_factor = if self.attrs.ar > 10.33 {
|
||||
0.3 * (self.attrs.ar - 10.33)
|
||||
} else if self.attrs.ar < 8.0 {
|
||||
0.05 * (8.0 - self.attrs.ar)
|
||||
@@ -671,6 +654,10 @@ impl OsuPpInner {
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_miss_penalty(&self, strain_count: f64) -> f64 {
|
||||
0.94 / ((self.effective_miss_count / (2.0 * strain_count.sqrt())) + 1.0)
|
||||
}
|
||||
|
||||
fn total_hits(&self) -> f64 {
|
||||
self.state.total_hits() as f64
|
||||
}
|
||||
@@ -738,283 +725,3 @@ impl OsuAttributeProvider for PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_tokio", feature = "async_str")))]
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Beatmap;
|
||||
|
||||
fn test_data() -> (Beatmap, OsuDifficultyAttributes) {
|
||||
let path = "./maps/2785319.osu";
|
||||
let map = Beatmap::from_path(path).unwrap();
|
||||
|
||||
let attrs = OsuDifficultyAttributes {
|
||||
aim: 2.8693628443424104,
|
||||
speed: 2.533869745015772,
|
||||
flashlight: 2.288770487900865,
|
||||
slider_factor: 0.9803052946037858,
|
||||
speed_note_count: 210.36373973116545,
|
||||
ar: 9.300000190734863,
|
||||
od: 8.800000190734863,
|
||||
hp: 5.0,
|
||||
n_circles: 307,
|
||||
n_sliders: 293,
|
||||
n_spinners: 1,
|
||||
stars: 5.669858729379631,
|
||||
max_combo: 909,
|
||||
};
|
||||
|
||||
(map, attrs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n100(20)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 20,
|
||||
n50: 279,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 289,
|
||||
n50: 10,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 0,
|
||||
n100: 589,
|
||||
n50: 10,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_n300_n100_n50_n_misses_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.n300(300)
|
||||
.n100(50)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 300,
|
||||
n100: 50,
|
||||
n50: 249,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(state, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(98.0)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 584,
|
||||
n100: 15,
|
||||
n50: 0,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n100_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(95.0)
|
||||
.n100(15)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 562,
|
||||
n100: 15,
|
||||
n50: 22,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_n50_n_misses_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(95.0)
|
||||
.n50(10)
|
||||
.n_misses(2)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 560,
|
||||
n100: 29,
|
||||
n50: 10,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_best() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(90.0)
|
||||
.hitresult_priority(HitResultPriority::BestCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 511,
|
||||
n100: 89,
|
||||
n50: 1,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hitresults_acc_worst() {
|
||||
let (map, attrs) = test_data();
|
||||
let max_combo = attrs.max_combo();
|
||||
|
||||
let state = OsuPP::new(&map)
|
||||
.attributes(attrs)
|
||||
.combo(500)
|
||||
.accuracy(90.0)
|
||||
.hitresult_priority(HitResultPriority::WorstCase)
|
||||
.generate_hitresults(max_combo);
|
||||
|
||||
let expected = OsuScoreState {
|
||||
max_combo: 500,
|
||||
n300: 528,
|
||||
n100: 4,
|
||||
n50: 69,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state,
|
||||
expected,
|
||||
"{}% vs {}%",
|
||||
state.accuracy(),
|
||||
expected.accuracy()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -11,6 +11,7 @@ pub(crate) struct Aim {
|
||||
curr_section_end: f64,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
with_sliders: bool,
|
||||
pub(crate) object_strains: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Aim {
|
||||
@@ -24,6 +25,7 @@ impl Aim {
|
||||
curr_section_end: 0.0,
|
||||
strain_peaks: Vec::new(),
|
||||
with_sliders,
|
||||
object_strains: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +76,8 @@ impl StrainSkill for Aim {
|
||||
self.curr_strain += AimEvaluator::evaluate_diff_of(curr, diff_objects, self.with_sliders)
|
||||
* Self::SKILL_MULTIPLIER;
|
||||
|
||||
self.object_strains.push(self.curr_strain);
|
||||
|
||||
self.curr_strain
|
||||
}
|
||||
|
||||
@@ -93,7 +97,19 @@ impl StrainSkill for Aim {
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuStrainSkill for Aim {}
|
||||
impl OsuStrainSkill for Aim {
|
||||
fn count_difficult_strains(&mut self) -> f64 {
|
||||
let top_strain = self
|
||||
.object_strains
|
||||
.iter()
|
||||
.fold(f64::NEG_INFINITY, |prev, curr| prev.max(*curr));
|
||||
|
||||
self.object_strains
|
||||
.iter()
|
||||
.map(|strain| (strain / top_strain).powi(4))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
struct AimEvaluator;
|
||||
|
||||
|
||||
@@ -108,7 +108,11 @@ impl StrainSkill for Flashlight {
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuStrainSkill for Flashlight {}
|
||||
impl OsuStrainSkill for Flashlight {
|
||||
fn count_difficult_strains(&mut self) -> f64 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
struct FlashlightEvaluator;
|
||||
|
||||
|
||||
@@ -118,6 +118,18 @@ impl StrainSkill for Speed {
|
||||
impl OsuStrainSkill for Speed {
|
||||
const REDUCED_SECTION_COUNT: usize = 5;
|
||||
const DIFFICULTY_MULTIPLER: f64 = 1.04;
|
||||
|
||||
fn count_difficult_strains(&mut self) -> f64 {
|
||||
let top_strain = self
|
||||
.object_strains
|
||||
.iter()
|
||||
.fold(f64::NEG_INFINITY, |prev, curr| prev.max(*curr));
|
||||
|
||||
self.object_strains
|
||||
.iter()
|
||||
.map(|strain| (strain / top_strain).powi(4))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
struct SpeedEvaluator;
|
||||
|
||||
@@ -90,6 +90,8 @@ pub(crate) trait OsuStrainSkill: StrainSkill + Sized {
|
||||
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||
const DIFFICULTY_MULTIPLER: f64 = 1.06;
|
||||
|
||||
fn count_difficult_strains(&mut self) -> f64;
|
||||
|
||||
fn difficulty_value(&mut self) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
Reference in New Issue
Block a user