fruits: FruitsDifficultyAttributes::max_combo is now a method + fixed attributes on very short maps
This commit is contained in:
+2
-1
@@ -11,7 +11,8 @@
|
|||||||
- [BREAKING] Renamed the `attributes` field to `difficulty` for all `{Mode}PerformanceAttributes` structs
|
- [BREAKING] Renamed the `attributes` field to `difficulty` for all `{Mode}PerformanceAttributes` structs
|
||||||
- Added `OsuGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
- Added `OsuGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
||||||
- Added `OsuGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `OsuPP` over and over.
|
- Added `OsuGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `OsuPP` over and over.
|
||||||
- osu: Fixed incorrect attributes on maps with only 1 or 2 hit objects
|
- Fixed incorrect attributes on maps with only 1 or 2 hit objects for all modes
|
||||||
|
- [BREAKING] Replaced field `FruitsDifficultyAttributes::max_combo` by a method with the same name
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
|
|||||||
+31
-31
@@ -35,28 +35,22 @@ pub fn stars(
|
|||||||
mods: impl Mods,
|
mods: impl Mods,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
) -> FruitsDifficultyAttributes {
|
) -> FruitsDifficultyAttributes {
|
||||||
match calculate_movement(map, mods, passed_objects) {
|
let (mut movement, mut attributes) = calculate_movement(map, mods, passed_objects);
|
||||||
Some((mut movement, mut attributes)) => {
|
|
||||||
attributes.stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
attributes.stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
||||||
attributes.max_combo = attributes.n_fruits + attributes.n_droplets;
|
|
||||||
|
|
||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
None => FruitsDifficultyAttributes::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Essentially the same as the [`stars`] function but instead of
|
/// Essentially the same as the [`stars`] function but instead of
|
||||||
/// evaluating the final strains, it just returns them as is.
|
/// evaluating the final strains, it just returns them as is.
|
||||||
///
|
///
|
||||||
/// Suitable to plot the difficulty of a map over time.
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
match calculate_movement(map, mods, None) {
|
let (movement, _) = calculate_movement(map, mods, None);
|
||||||
Some((movement, _)) => Strains {
|
|
||||||
|
Strains {
|
||||||
section_length: SECTION_LENGTH * mods.speed(),
|
section_length: SECTION_LENGTH * mods.speed(),
|
||||||
strains: movement.strain_peaks,
|
strains: movement.strain_peaks,
|
||||||
},
|
|
||||||
None => Strains::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,11 +58,7 @@ fn calculate_movement(
|
|||||||
map: &Beatmap,
|
map: &Beatmap,
|
||||||
mods: impl Mods,
|
mods: impl Mods,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
) -> Option<(Movement, FruitsDifficultyAttributes)> {
|
) -> (Movement, FruitsDifficultyAttributes) {
|
||||||
if map.hit_objects.len() < 2 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let take = passed_objects.unwrap_or(usize::MAX);
|
let take = passed_objects.unwrap_or(usize::MAX);
|
||||||
|
|
||||||
let map_attributes = map.attributes().mods(mods);
|
let map_attributes = map.attributes().mods(mods);
|
||||||
@@ -224,11 +214,15 @@ fn calculate_movement(
|
|||||||
// Strain business
|
// Strain business
|
||||||
let mut movement = Movement::new(map_attributes.cs as f32);
|
let mut movement = Movement::new(map_attributes.cs as f32);
|
||||||
let section_len = SECTION_LENGTH * map_attributes.clock_rate;
|
let section_len = SECTION_LENGTH * map_attributes.clock_rate;
|
||||||
let mut current_section_end =
|
|
||||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
|
||||||
|
|
||||||
let mut prev = hit_objects.next().unwrap();
|
let (mut prev, mut curr) = match (hit_objects.next(), hit_objects.next()) {
|
||||||
let mut curr = hit_objects.next().unwrap();
|
(Some(prev), Some(curr)) => (prev, curr),
|
||||||
|
(Some(_), None) | (None, None) => return (movement, attributes),
|
||||||
|
(None, Some(_)) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: time of second second object instead?
|
||||||
|
let mut curr_section_end = (prev.time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
prev.init_hyper_dash(
|
prev.init_hyper_dash(
|
||||||
half_catcher_width,
|
half_catcher_width,
|
||||||
@@ -253,8 +247,8 @@ fn calculate_movement(
|
|||||||
map_attributes.clock_rate,
|
map_attributes.clock_rate,
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > curr_section_end {
|
||||||
current_section_end += section_len;
|
curr_section_end += section_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
movement.process(&h);
|
movement.process(&h);
|
||||||
@@ -278,10 +272,10 @@ fn calculate_movement(
|
|||||||
map_attributes.clock_rate,
|
map_attributes.clock_rate,
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > curr_section_end {
|
||||||
movement.save_current_peak();
|
movement.save_current_peak();
|
||||||
movement.start_new_section_from(current_section_end / map_attributes.clock_rate);
|
movement.start_new_section_from(curr_section_end / map_attributes.clock_rate);
|
||||||
current_section_end += section_len;
|
curr_section_end += section_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
movement.process(&h);
|
movement.process(&h);
|
||||||
@@ -298,17 +292,17 @@ fn calculate_movement(
|
|||||||
map_attributes.clock_rate,
|
map_attributes.clock_rate,
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > curr_section_end {
|
||||||
movement.save_current_peak();
|
movement.save_current_peak();
|
||||||
movement.start_new_section_from(current_section_end / map_attributes.clock_rate);
|
movement.start_new_section_from(curr_section_end / map_attributes.clock_rate);
|
||||||
|
|
||||||
current_section_end += section_len;
|
curr_section_end += section_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
movement.process(&h);
|
movement.process(&h);
|
||||||
movement.save_current_peak();
|
movement.save_current_peak();
|
||||||
|
|
||||||
Some((movement, attributes))
|
(movement, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG: Sometimes there are off-by-one errors,
|
// BUG: Sometimes there are off-by-one errors,
|
||||||
@@ -424,8 +418,6 @@ impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
|
|||||||
pub struct FruitsDifficultyAttributes {
|
pub struct FruitsDifficultyAttributes {
|
||||||
/// The final star rating
|
/// The final star rating
|
||||||
pub stars: f64,
|
pub stars: f64,
|
||||||
/// The maximum combo.
|
|
||||||
pub max_combo: usize,
|
|
||||||
/// The approach rate.
|
/// The approach rate.
|
||||||
pub ar: f64,
|
pub ar: f64,
|
||||||
/// The amount of fruits.
|
/// The amount of fruits.
|
||||||
@@ -436,6 +428,14 @@ pub struct FruitsDifficultyAttributes {
|
|||||||
pub n_tiny_droplets: usize,
|
pub n_tiny_droplets: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FruitsDifficultyAttributes {
|
||||||
|
/// Return the maximum combo.
|
||||||
|
#[inline]
|
||||||
|
pub fn max_combo(&self) -> usize {
|
||||||
|
self.n_fruits + self.n_droplets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The result of a performance calculation on an osu!ctb map.
|
/// The result of a performance calculation on an osu!ctb map.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct FruitsPerformanceAttributes {
|
pub struct FruitsPerformanceAttributes {
|
||||||
@@ -461,7 +461,7 @@ impl FruitsPerformanceAttributes {
|
|||||||
/// Return the maximum combo of the map.
|
/// Return the maximum combo of the map.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn max_combo(&self) -> usize {
|
pub fn max_combo(&self) -> usize {
|
||||||
self.difficulty.max_combo
|
self.difficulty.max_combo()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -157,9 +157,10 @@ impl<'map> FruitsPP<'map> {
|
|||||||
.n_droplets
|
.n_droplets
|
||||||
.unwrap_or_else(|| attributes.n_droplets.saturating_sub(self.n_misses));
|
.unwrap_or_else(|| attributes.n_droplets.saturating_sub(self.n_misses));
|
||||||
|
|
||||||
|
let max_combo = attributes.max_combo();
|
||||||
|
|
||||||
let n_fruits = self.n_fruits.unwrap_or_else(|| {
|
let n_fruits = self.n_fruits.unwrap_or_else(|| {
|
||||||
attributes
|
max_combo
|
||||||
.max_combo
|
|
||||||
.saturating_sub(self.n_misses)
|
.saturating_sub(self.n_misses)
|
||||||
.saturating_sub(n_droplets)
|
.saturating_sub(n_droplets)
|
||||||
});
|
});
|
||||||
@@ -168,7 +169,7 @@ impl<'map> FruitsPP<'map> {
|
|||||||
acc /= 100.0;
|
acc /= 100.0;
|
||||||
|
|
||||||
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
||||||
((acc * (attributes.max_combo + max_tiny_droplets) as f64).round() as usize)
|
((acc * (max_combo + max_tiny_droplets) as f64).round() as usize)
|
||||||
.saturating_sub(n_fruits)
|
.saturating_sub(n_fruits)
|
||||||
.saturating_sub(n_droplets)
|
.saturating_sub(n_droplets)
|
||||||
});
|
});
|
||||||
@@ -184,10 +185,12 @@ impl<'map> FruitsPP<'map> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn assert_hitresults(self, attributes: FruitsDifficultyAttributes) -> FruitsPPInner {
|
fn assert_hitresults(self, attributes: FruitsDifficultyAttributes) -> FruitsPPInner {
|
||||||
|
let max_combo = attributes.max_combo();
|
||||||
|
|
||||||
let correct_combo_hits = self
|
let correct_combo_hits = self
|
||||||
.n_fruits
|
.n_fruits
|
||||||
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses))
|
.and_then(|f| self.n_droplets.map(|d| f + d + self.n_misses))
|
||||||
.filter(|h| *h == attributes.max_combo);
|
.filter(|h| *h == max_combo);
|
||||||
|
|
||||||
let correct_fruits = self
|
let correct_fruits = self
|
||||||
.n_fruits
|
.n_fruits
|
||||||
@@ -213,8 +216,7 @@ impl<'map> FruitsPP<'map> {
|
|||||||
let mut n_tiny_droplets = self.n_tiny_droplets.unwrap_or(0);
|
let mut n_tiny_droplets = self.n_tiny_droplets.unwrap_or(0);
|
||||||
let n_tiny_droplet_misses = self.n_tiny_droplet_misses.unwrap_or(0);
|
let n_tiny_droplet_misses = self.n_tiny_droplet_misses.unwrap_or(0);
|
||||||
|
|
||||||
let missing = attributes
|
let missing = max_combo
|
||||||
.max_combo
|
|
||||||
.saturating_sub(n_fruits)
|
.saturating_sub(n_fruits)
|
||||||
.saturating_sub(n_droplets)
|
.saturating_sub(n_droplets)
|
||||||
.saturating_sub(self.n_misses);
|
.saturating_sub(self.n_misses);
|
||||||
@@ -279,6 +281,7 @@ impl FruitsPPInner {
|
|||||||
fn calculate(self) -> FruitsPerformanceAttributes {
|
fn calculate(self) -> FruitsPerformanceAttributes {
|
||||||
let attributes = &self.attributes;
|
let attributes = &self.attributes;
|
||||||
let stars = attributes.stars;
|
let stars = attributes.stars;
|
||||||
|
let max_combo = attributes.max_combo();
|
||||||
|
|
||||||
// Relying heavily on aim
|
// Relying heavily on aim
|
||||||
let mut pp = (5.0 * (stars / 0.0049).max(1.0) - 4.0).powi(2) / 100_000.0;
|
let mut pp = (5.0 * (stars / 0.0049).max(1.0) - 4.0).powi(2) / 100_000.0;
|
||||||
@@ -286,7 +289,7 @@ impl FruitsPPInner {
|
|||||||
let mut combo_hits = self.combo_hits();
|
let mut combo_hits = self.combo_hits();
|
||||||
|
|
||||||
if combo_hits == 0 {
|
if combo_hits == 0 {
|
||||||
combo_hits = attributes.max_combo;
|
combo_hits = max_combo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
@@ -300,10 +303,8 @@ impl FruitsPPInner {
|
|||||||
pp *= 0.97_f64.powi(self.n_misses as i32);
|
pp *= 0.97_f64.powi(self.n_misses as i32);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
if let Some(combo) = self.combo.filter(|_| max_combo > 0) {
|
||||||
pp *= (combo as f64 / attributes.max_combo as f64)
|
pp *= (combo as f64 / max_combo as f64).powf(0.8).min(1.0);
|
||||||
.powf(0.8)
|
|
||||||
.min(1.0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AR scaling
|
// AR scaling
|
||||||
@@ -427,7 +428,6 @@ mod test {
|
|||||||
n_fruits: 1234,
|
n_fruits: 1234,
|
||||||
n_droplets: 567,
|
n_droplets: 567,
|
||||||
n_tiny_droplets: 2345,
|
n_tiny_droplets: 2345,
|
||||||
max_combo: 1234 + 567,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -327,7 +327,7 @@ impl DifficultyAttributes {
|
|||||||
pub fn max_combo(&self) -> Option<usize> {
|
pub fn max_combo(&self) -> Option<usize> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(attributes) => Some(attributes.max_combo),
|
Self::Fruits(attributes) => Some(attributes.max_combo()),
|
||||||
Self::Mania(_) => None,
|
Self::Mania(_) => None,
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
Self::Osu(attributes) => Some(attributes.max_combo),
|
Self::Osu(attributes) => Some(attributes.max_combo),
|
||||||
@@ -450,7 +450,7 @@ impl PerformanceAttributes {
|
|||||||
pub fn max_combo(&self) -> Option<usize> {
|
pub fn max_combo(&self) -> Option<usize> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(f) => Some(f.difficulty.max_combo),
|
Self::Fruits(f) => Some(f.difficulty.max_combo()),
|
||||||
Self::Mania(_) => None,
|
Self::Mania(_) => None,
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
Self::Osu(o) => Some(o.difficulty.max_combo),
|
Self::Osu(o) => Some(o.difficulty.max_combo),
|
||||||
|
|||||||
Reference in New Issue
Block a user