wip mania pp update

This commit is contained in:
MaxOhn
2022-10-09 16:37:49 +02:00
parent 5eb8dab588
commit 4bb2fca50c
20 changed files with 852 additions and 465 deletions
@@ -3,6 +3,7 @@ use crate::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType,
},
curve::Curve,
mania::ManiaObject,
parse::{HitObject, HitSound},
Beatmap,
};
@@ -99,7 +100,7 @@ impl<'h> DistanceObjectPatternGenerator<'h> {
let mut end_time_pattern = Pattern::default();
for obj in orig_pattern.hit_objects {
let column = obj.column(self.total_columns as f32);
let column = ManiaObject::new(&obj).column(self.total_columns as f32) as u8;
if self.end_time != obj.end_time().round() as i32 {
intermediate_pattern.add_object(obj, column);
@@ -2,6 +2,7 @@ use crate::{
beatmap::converts::mania::{
legacy_random::Random, pattern::Pattern, pattern_type::PatternType, PrevValues,
},
mania::ManiaObject,
parse::{HitObject, HitSound},
Beatmap,
};
@@ -103,7 +104,7 @@ impl<'h> HitObjectPatternGenerator<'h> {
pub(crate) fn generate(&mut self) -> Pattern {
let pattern = self.generate_core();
for obj in pattern.hit_objects.iter() {
for obj in pattern.hit_objects.iter().map(ManiaObject::new) {
if self.convert_type.contains(PatternType::STAIR)
&& obj.column(self.total_columns as f32) as i32 == self.total_columns - 1
{
@@ -129,7 +130,8 @@ impl<'h> HitObjectPatternGenerator<'h> {
.prev_pattern
.hit_objects
.last()
.map_or(0, |h| h.column(self.total_columns as f32));
.map(ManiaObject::new)
.map_or(0, |h| h.column(self.total_columns as f32) as u8);
let random_start = self.random_start() as u8;
@@ -1,4 +1,4 @@
use crate::{parse::HitObject, Beatmap};
use crate::{mania::ManiaObject, parse::HitObject, Beatmap};
use super::{legacy_random::Random, pattern::Pattern};
@@ -26,7 +26,7 @@ trait PatternGenerator {
((self.hit_object().pos.x / LOCAL_X_DIVISOR).floor() as u8).clamp(0, 6) + 1
} else {
self.hit_object().column(self.total_columns() as f32)
ManiaObject::new(self.hit_object()).column(self.total_columns() as f32) as u8
};
res
+1 -1
View File
@@ -22,7 +22,7 @@ pub struct CatchScoreState {
/// Amount of current tiny droplet misses (katus).
pub n_tiny_droplet_misses: usize,
/// Amount of current misses (fruits and droplets).
pub misses: usize,
pub n_misses: usize,
}
impl CatchScoreState {
+2 -2
View File
@@ -168,7 +168,7 @@ impl<'map> CatchPP<'map> {
n_droplets,
n_tiny_droplets,
n_tiny_droplet_misses,
misses,
n_misses,
} = state;
self.combo = Some(max_combo);
@@ -176,7 +176,7 @@ impl<'map> CatchPP<'map> {
self.n_droplets = Some(n_droplets);
self.n_tiny_droplets = Some(n_tiny_droplets);
self.n_tiny_droplet_misses = Some(n_tiny_droplet_misses);
self.n_misses = misses;
self.n_misses = n_misses;
self
}
+22 -20
View File
@@ -1,6 +1,6 @@
use crate::{
catch::{CatchGradualDifficultyAttributes, CatchGradualPerformanceAttributes, CatchScoreState},
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes},
mania::{ManiaGradualDifficultyAttributes, ManiaGradualPerformanceAttributes, ManiaScoreState},
osu::{OsuGradualDifficultyAttributes, OsuGradualPerformanceAttributes, OsuScoreState},
taiko::{TaikoGradualDifficultyAttributes, TaikoGradualPerformanceAttributes, TaikoScoreState},
Beatmap, DifficultyAttributes, GameMode, PerformanceAttributes,
@@ -99,30 +99,18 @@ pub struct ScoreState {
///
/// Irrelevant for osu!mania.
pub max_combo: usize,
/// Amount of current katus (tiny droplet misses for osu!catch).
///
/// Only relevant for osu!catch.
/// Amount of current gekis (n320 for osu!mania).
pub n_geki: usize,
/// Amount of current katus (tiny droplet misses for osu!catch / n200 for osu!mania).
pub n_katu: usize,
/// Amount of current 300s (fruits for osu!catch).
///
/// Irrelevant for osu!mania.
pub n300: usize,
/// Amount of current 100s (droplets for osu!catch).
///
/// Irrelevant for osu!mania.
pub n100: usize,
/// Amount of current 50s (tiny droplets for osu!catch).
///
/// Irrelevant for osu!taiko and osu!mania.
pub n50: usize,
/// Amount of current misses (fruits + droplets for osu!catch).
///
/// Irrelevant for osu!mania.
pub misses: usize,
/// The current score.
///
/// Only relevant for osu!mania.
pub score: u32,
pub n_misses: usize,
}
impl ScoreState {
@@ -141,7 +129,7 @@ impl From<ScoreState> for CatchScoreState {
n_droplets: state.n100,
n_tiny_droplets: state.n50,
n_tiny_droplet_misses: state.n_katu,
misses: state.misses,
n_misses: state.n_misses,
}
}
}
@@ -154,7 +142,7 @@ impl From<ScoreState> for OsuScoreState {
n300: state.n300,
n100: state.n100,
n50: state.n50,
misses: state.misses,
n_misses: state.n_misses,
}
}
}
@@ -166,7 +154,21 @@ impl From<ScoreState> for TaikoScoreState {
max_combo: state.max_combo,
n300: state.n300,
n100: state.n100,
misses: state.misses,
n_misses: state.n_misses,
}
}
}
impl From<ScoreState> for ManiaScoreState {
#[inline]
fn from(state: ScoreState) -> Self {
Self {
n320: state.n_geki,
n300: state.n300,
n200: state.n_katu,
n100: state.n100,
n50: state.n50,
n_misses: state.n_misses,
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use super::mania_object::ManiaObject;
pub(crate) struct ManiaDifficultyObject<'h> {
pub(crate) idx: usize,
pub(crate) base: ManiaObject<'h>,
pub(crate) delta_time: f64,
pub(crate) start_time: f64,
pub(crate) end_time: f64,
}
impl<'h> ManiaDifficultyObject<'h> {
pub(crate) fn new(
base: ManiaObject<'h>,
last: ManiaObject<'h>,
clock_rate: f64,
idx: usize,
) -> Self {
let delta_time = (base.start_time() - last.start_time()) / clock_rate;
let start_time = base.start_time() / clock_rate;
let end_time = base.end_time() / clock_rate;
Self {
idx,
base,
delta_time,
start_time,
end_time,
}
}
}
+30
View File
@@ -2,6 +2,36 @@ use crate::{Beatmap, ManiaPP};
use super::{ManiaGradualDifficultyAttributes, ManiaPerformanceAttributes};
/// TODO: docs
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ManiaScoreState {
/// Amount of current 320s.
pub n320: usize,
/// Amount of current 300s.
pub n300: usize,
/// Amount of current 200s.
pub n200: usize,
/// Amount of current 100s.
pub n100: usize,
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub n_misses: usize,
}
impl ManiaScoreState {
pub fn total_hits(&self) -> usize {
self.n320 + self.n300 + self.n200 + self.n100 + self.n50 + self.n_misses
}
}
impl ManiaScoreState {
/// Create a new empty score state.
pub fn new() -> Self {
Self::default()
}
}
/// Gradually calculate the performance attributes of an osu!mania map.
///
/// After each hit object you can call
+27
View File
@@ -0,0 +1,27 @@
use crate::parse::HitObject;
pub(crate) struct ManiaObject<'h> {
hit_object: &'h HitObject,
}
impl<'h> ManiaObject<'h> {
pub(crate) fn new(hit_object: &'h HitObject) -> Self {
Self { hit_object }
}
pub(crate) fn start_time(&self) -> f64 {
self.hit_object.start_time
}
pub(crate) fn end_time(&self) -> f64 {
self.hit_object.end_time()
}
pub(crate) fn column(&self, total_columns: f32) -> usize {
let x_divisor = 512.0 / total_columns;
(self.hit_object.pos.x / x_divisor)
.floor()
.min(total_columns - 1.0) as usize
}
}
+58 -65
View File
@@ -1,16 +1,22 @@
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod mania_object;
mod pp;
mod strain;
mod skills;
use std::borrow::Cow;
pub use gradual_difficulty::*;
pub use gradual_performance::*;
pub use pp::*;
use strain::Strain;
use crate::{beatmap::BeatmapHitWindows, parse::HitObjectKind, Beatmap, GameMode, Mods, OsuStars};
use crate::{parse::HitObject, Beatmap, GameMode, Mods, OsuStars};
pub use self::{gradual_difficulty::*, gradual_performance::*, pp::*};
pub(crate) use self::mania_object::ManiaObject;
use self::{
difficulty_object::ManiaDifficultyObject,
skills::{Skill, Strain},
};
const SECTION_LEN: f64 = 400.0;
const STAR_SCALING_FACTOR: f64 = 0.018;
@@ -88,11 +94,23 @@ impl<'map> ManiaStars<'map> {
/// Calculate all difficulty related values, including stars.
#[inline]
pub fn calculate(self) -> ManiaDifficultyAttributes {
let mut strain = calculate_strain(self);
let is_convert = matches!(self.map, Cow::Owned(_));
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
ManiaDifficultyAttributes {
stars: Strain::difficulty_value(&mut strain.strain_peaks) * STAR_SCALING_FACTOR,
}
let BeatmapHitWindows { od: hit_window, .. } = self
.map
.attributes()
.mods(self.mods)
.converted(is_convert)
.clock_rate(clock_rate)
.hit_windows();
let (strain, mut attrs) = calculate_strain(self);
attrs.stars = strain.difficulty_value() * STAR_SCALING_FACTOR;
attrs.hit_window = hit_window;
attrs
}
/// Calculate the skill strains.
@@ -101,10 +119,10 @@ impl<'map> ManiaStars<'map> {
#[inline]
pub fn strains(self) -> ManiaStrains {
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
let strain = calculate_strain(self);
let (strain, _) = calculate_strain(self);
ManiaStrains {
section_len: SECTION_LEN * clock_rate,
section_len: SECTION_LEN * clock_rate, // TODO: clock_rate correct here?
strains: strain.strain_peaks,
}
}
@@ -129,7 +147,7 @@ impl ManiaStrains {
}
}
fn calculate_strain(params: ManiaStars<'_>) -> Strain {
fn calculate_strain(params: ManiaStars<'_>) -> (Strain, ManiaDifficultyAttributes) {
let ManiaStars {
map,
mods,
@@ -138,64 +156,37 @@ fn calculate_strain(params: ManiaStars<'_>) -> Strain {
} = params;
let take = passed_objects.unwrap_or(map.hit_objects.len());
let columns = map.cs.round().max(1.0) as u8;
let total_columns = map.cs.round().max(1.0) as usize;
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
let mut strain = Strain::new(columns);
let columns = columns as f32;
let mut strain = Strain::new(total_columns);
let mut hit_objects = map
let mut attrs = ManiaDifficultyAttributes::default();
let diff_objects_iter = map
.hit_objects
.iter()
.take(take)
.inspect(|h| match &h.kind {
HitObjectKind::Hold { end_time } => {
attrs.max_combo += 1 + ((*end_time - h.start_time) / 100.0) as usize
}
_ => attrs.max_combo += 1,
})
.skip(1)
.zip(map.hit_objects.iter())
.map(|(base, prev)| DifficultyHitObject::new(base, prev, columns, clock_rate));
.map(ManiaObject::new)
.enumerate()
.zip(map.hit_objects.iter().map(ManiaObject::new))
.map(|((i, base), prev)| ManiaDifficultyObject::new(base, prev, clock_rate, i));
// Handle first object distinctly
let h = match hit_objects.next() {
Some(h) => h,
None => return strain,
};
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().min(take).saturating_sub(1));
diff_objects.extend(diff_objects_iter);
// No strain for first object
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
strain.process(&h);
// Handle all other objects
for h in hit_objects {
while h.start_time > curr_section_end {
strain.save_current_peak();
strain.start_new_section_from(curr_section_end);
curr_section_end += SECTION_LEN;
}
strain.process(&h);
for curr in diff_objects.iter() {
strain.process(curr, &diff_objects);
}
strain.save_current_peak();
strain
}
#[derive(Debug)]
pub(crate) struct DifficultyHitObject<'o> {
base: &'o HitObject,
column: usize,
delta: f64,
start_time: f64,
}
impl<'o> DifficultyHitObject<'o> {
#[inline]
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f64) -> Self {
Self {
base,
column: base.column(columns) as usize,
delta: (base.start_time - prev.start_time) / clock_rate,
start_time: base.start_time / clock_rate,
}
}
(strain, attrs)
}
/// The result of a difficulty calculation on an osu!mania map.
@@ -203,19 +194,21 @@ impl<'o> DifficultyHitObject<'o> {
pub struct ManiaDifficultyAttributes {
/// The final star rating.
pub stars: f64,
/// The maximum achievable combo.
pub max_combo: usize,
/// The perceived hit window for an n300 inclusive of rate-adjusting mods (DT/HT/etc).
pub hit_window: f64,
}
/// The result of a performance calculation on an osu!mania map.
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct ManiaPerformanceAttributes {
/// The difficulty attributes that were used for the performance calculation
/// The difficulty attributes that were used for the performance calculation.
pub difficulty: ManiaDifficultyAttributes,
/// The final performance points.
pub pp: f64,
/// The accuracy portion of the final pp.
pub pp_acc: f64,
/// The strain portion of the final pp.
pub pp_strain: f64,
/// The difficulty portion of the final pp.
pub pp_difficulty: f64,
}
impl ManiaPerformanceAttributes {
+300 -99
View File
@@ -1,11 +1,9 @@
use std::borrow::Cow;
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaStars};
use crate::{
beatmap::BeatmapHitWindows, Beatmap, DifficultyAttributes, GameMode, Mods, OsuPP,
PerformanceAttributes,
};
use super::{ManiaDifficultyAttributes, ManiaPerformanceAttributes, ManiaScoreState, ManiaStars};
use crate::{Beatmap, DifficultyAttributes, GameMode, Mods, OsuPP, PerformanceAttributes};
// TODO: update
/// Performance calculator on osu!mania maps.
///
/// # Example
@@ -37,11 +35,20 @@ use crate::{
#[allow(clippy::upper_case_acronyms)]
pub struct ManiaPP<'map> {
map: Cow<'map, Beatmap>,
stars: Option<f64>,
attributes: Option<ManiaDifficultyAttributes>,
mods: u32,
pub(crate) score: Option<f64>,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
n320: Option<usize>,
n300: Option<usize>,
n200: Option<usize>,
n100: Option<usize>,
n50: Option<usize>,
n_misses: Option<usize>,
acc: Option<f64>,
hitresult_priority: Option<ManiaHitResultPriority>,
}
impl<'map> ManiaPP<'map> {
@@ -50,11 +57,18 @@ impl<'map> ManiaPP<'map> {
pub fn new(map: &'map Beatmap) -> Self {
Self {
map: Cow::Borrowed(map),
stars: None,
attributes: None,
mods: 0,
score: None,
passed_objects: None,
clock_rate: None,
n320: None,
n300: None,
n200: None,
n100: None,
n50: None,
n_misses: None,
acc: None,
hitresult_priority: None,
}
}
@@ -62,9 +76,9 @@ impl<'map> ManiaPP<'map> {
/// If you already calculated the attributes for the current map-mod combination,
/// be sure to put them in here so that they don't have to be recalculated.
#[inline]
pub fn attributes(mut self, attributes: impl ManiaAttributeProvider) -> Self {
if let Some(stars) = attributes.attributes() {
self.stars = Some(stars);
pub fn attributes(mut self, attrs: impl ManiaAttributeProvider) -> Self {
if let Some(attrs) = attrs.attributes() {
self.attributes = Some(attrs);
}
self
@@ -80,15 +94,7 @@ impl<'map> ManiaPP<'map> {
self
}
/// Specify the score of a play.
/// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
#[inline]
pub fn score(mut self, score: u32) -> Self {
self.score = Some(score as f64);
self
}
// TODO: update
/// Amount of passed objects for partial plays, e.g. a fail.
///
/// Be sure you also set [`score`](ManiaPP::score) or the final values
@@ -99,7 +105,7 @@ impl<'map> ManiaPP<'map> {
/// [`ManiaGradualPerformanceAttributes`](crate::mania::ManiaGradualPerformanceAttributes).
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects.replace(passed_objects);
self.passed_objects = Some(passed_objects);
self
}
@@ -114,9 +120,87 @@ impl<'map> ManiaPP<'map> {
self
}
#[inline]
pub fn accuracy(mut self, acc: f64) -> Self {
self.acc = Some(acc);
self
}
#[inline]
pub fn hitresult_priority(mut self, priority: ManiaHitResultPriority) -> Self {
self.hitresult_priority = Some(priority);
self
}
#[inline]
pub fn n320(mut self, n320: usize) -> Self {
self.n320 = Some(n320);
self
}
#[inline]
pub fn n300(mut self, n300: usize) -> Self {
self.n300 = Some(n300);
self
}
#[inline]
pub fn n200(mut self, n200: usize) -> Self {
self.n200 = Some(n200);
self
}
#[inline]
pub fn n100(mut self, n100: usize) -> Self {
self.n100 = Some(n100);
self
}
#[inline]
pub fn n50(mut self, n50: usize) -> Self {
self.n50 = Some(n50);
self
}
#[inline]
pub fn n_misses(mut self, n_misses: usize) -> Self {
self.n_misses = Some(n_misses);
self
}
#[inline]
pub fn state(mut self, state: ManiaScoreState) -> Self {
let ManiaScoreState {
n320,
n300,
n200,
n100,
n50,
n_misses,
} = state;
self.n320 = Some(n320);
self.n300 = Some(n300);
self.n200 = Some(n200);
self.n100 = Some(n100);
self.n50 = Some(n50);
self.n_misses = Some(n_misses);
self
}
/// Calculate all performance related values, including pp and stars.
pub fn calculate(self) -> ManiaPerformanceAttributes {
let stars = self.stars.unwrap_or_else(|| {
let attrs = self.attributes.unwrap_or_else(|| {
// TODO: handle converts
let mut calculator = ManiaStars::new(self.map.as_ref()).mods(self.mods);
if let Some(passed_objects) = self.passed_objects {
@@ -127,84 +211,203 @@ impl<'map> ManiaPP<'map> {
calculator = calculator.clock_rate(clock_rate);
}
calculator.calculate().stars
calculator.calculate()
});
let ez = self.mods.ez();
let nf = self.mods.nf();
let ht = self.mods.ht();
let inner = ManiaPpInner {
attrs,
mods: self.mods,
clock_rate: self.clock_rate.unwrap_or_else(|| self.mods.clock_rate()),
state: self.generate_hitresults(),
};
let mut scaled_score = self.score.map_or(1_000_000.0, |score| {
score / 0.5_f64.powi(ez as i32 + nf as i32 + ht as i32)
});
inner.calculate()
}
if let Some(passed_objects) = self.passed_objects {
let percent_passed =
passed_objects as f64 / (self.map.n_circles + self.map.n_sliders) as f64;
fn generate_hitresults(&self) -> ManiaScoreState {
let n_objects = self.map.hit_objects.len();
let priority = self.hitresult_priority.unwrap_or_default();
scaled_score /= percent_passed;
let mut state = ManiaScoreState {
n320: self.n320.unwrap_or(0),
n300: self.n300.unwrap_or(0),
n200: self.n200.unwrap_or(0),
n100: self.n100.unwrap_or(0),
n50: self.n50.unwrap_or(0),
n_misses: self.n_misses.unwrap_or(0),
};
if let Some(acc) = self.acc {
let target_total = (acc * (n_objects * 6) as f64).round() as usize;
let mut delta = target_total.saturating_sub(n_objects.saturating_sub(state.n_misses));
if self.n50.is_some() {
delta /= 2;
}
if self.n100.is_some() {
delta /= 2;
}
if let Some(n320) = self.n320 {
delta = delta.saturating_sub(n320 * 6);
} else {
state.n320 = delta / 5;
}
if self.n100.is_none() {
state.n100 = delta % 5;
}
state.n50 += n_objects.saturating_sub(state.total_hits() - state.n50);
if let ManiaHitResultPriority::BestCase = priority {
// Shift n50 to n200
if self.n320.or(self.n300).or(self.n200).or(self.n50).is_none() {
let n = (state.n320 + state.n300).min(state.n50 / 2);
if n <= state.n300 {
state.n300 -= n;
} else {
state.n320 -= n - state.n300;
state.n300 = 0;
};
state.n200 += 2 * n;
state.n50 -= n;
}
// Shift n50 to n100
if self.n320.or(self.n300).or(self.n100).or(self.n50).is_none() {
let n = (state.n320 + state.n300).min(state.n50 / 4);
if n <= state.n300 {
state.n300 -= n;
} else {
state.n320 -= n - state.n300;
state.n300 = 0;
};
state.n100 += 5 * n;
state.n50 -= 4 * n;
}
}
} else {
let remaining = n_objects.saturating_sub(state.total_hits());
match priority {
ManiaHitResultPriority::BestCase => {
if self.n320.is_none() {
state.n320 = remaining;
} else if self.n300.is_none() {
state.n300 = remaining;
} else if self.n200.is_none() {
state.n200 = remaining;
} else if self.n100.is_none() {
state.n100 = remaining;
} else if self.n50.is_none() {
state.n50 = remaining;
} else {
state.n320 = remaining;
}
}
ManiaHitResultPriority::WorstCase => {
if self.n50.is_none() {
state.n50 = remaining;
} else if self.n100.is_none() {
state.n100 = remaining;
} else if self.n200.is_none() {
state.n200 = remaining;
} else if self.n300.is_none() {
state.n300 = remaining;
} else if self.n320.is_none() {
state.n320 = remaining;
} else {
state.n50 = remaining;
}
}
}
}
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
state
}
}
let BeatmapHitWindows { od: hit_window, .. } = self
.map
.attributes()
.mods(self.mods)
.clock_rate(clock_rate)
.converted(matches!(self.map, Cow::Owned(_)))
.hit_windows();
struct ManiaPpInner {
attrs: ManiaDifficultyAttributes,
mods: u32,
clock_rate: f64,
state: ManiaScoreState,
}
impl ManiaPpInner {
fn calculate(self) -> ManiaPerformanceAttributes {
// * Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// * The specific number has no intrinsic meaning and can be adjusted as needed.
let mut multiplier = 0.8;
if nf {
multiplier *= 0.9;
if self.mods.nf() {
multiplier *= 0.75;
}
if ez {
if self.mods.ez() {
multiplier *= 0.5;
}
let strain_value = self.compute_strain(scaled_score, stars);
let acc_value = self.compute_accuracy_value(scaled_score, strain_value, hit_window);
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
let difficulty_value = self.compute_difficulty_value();
let pp = difficulty_value * multiplier;
ManiaPerformanceAttributes {
difficulty: ManiaDifficultyAttributes { stars },
pp_acc: acc_value,
pp_strain: strain_value,
difficulty: self.attrs,
pp,
pp_difficulty: difficulty_value,
}
}
fn compute_strain(&self, score: f64, stars: f64) -> f64 {
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f64 / 1500.0).min(1.0);
if score <= 500_000.0 {
strain_value = 0.0;
} else if score <= 600_000.0 {
strain_value *= (score - 500_000.0) / 100_000.0 * 0.3;
} else if score <= 700_000.0 {
strain_value *= 0.3 + (score - 600_000.0) / 100_000.0 * 0.25;
} else if score <= 800_000.0 {
strain_value *= 0.55 + (score - 700_000.0) / 100_000.0 * 0.2;
} else if score <= 900_000.0 {
strain_value *= 0.75 + (score - 800_000.0) / 100_000.0 * 0.15;
} else {
strain_value *= 0.9 + (score - 900_000.0) / 100_000.0 * 0.1;
}
strain_value
fn compute_difficulty_value(&self) -> f64 {
// Star rating to pp curve
(self.attrs.stars - 0.15).max(0.05).powf(2.2)
// From 80% accuracy, 1/20th of total pp is awarded per additional 1% accuracy
* (5.0 * self.custom_accuracy() - 4.0).max(0.0)
// Length bonus, capped at 1500 notes
* (1.0 + 0.1 * (self.total_hits() / 1500.0).min(1.0))
}
fn total_hits(&self) -> f64 {
self.state.total_hits() as f64
}
fn custom_accuracy(&self) -> f64 {
let ManiaScoreState {
n320,
n300,
n200,
n100,
n50,
n_misses,
} = &self.state;
let numerator = *n320 * 320 + *n300 * 300 + *n200 * 200 + *n100 * 100 + *n50 * 50;
let denominator = self.total_hits() * 320.0;
numerator as f64 / denominator
}
}
/// While generating hitresults that weren't specific, decide how they should be distributed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ManiaHitResultPriority {
/// Prioritize good hitresults over bad ones
BestCase,
/// Prioritize bad hitresults over good ones
WorstCase,
}
impl Default for ManiaHitResultPriority {
#[inline]
fn compute_accuracy_value(&self, score: f64, strain: f64, hit_window: f64) -> f64 {
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
* strain
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
fn default() -> Self {
Self::BestCase
}
}
@@ -221,48 +424,47 @@ impl<'map> From<OsuPP<'map>> for ManiaPP<'map> {
Self {
map: map.convert_mode(GameMode::Mania),
stars: None,
attributes: None,
mods,
score: None,
passed_objects,
clock_rate,
n320: None,
n300: None,
n200: None,
n100: None,
n50: None,
n_misses: None,
acc: None,
hitresult_priority: None,
}
}
}
/// Abstract type to provide flexibility when passing difficulty attributes to a performance calculation.
pub trait ManiaAttributeProvider {
/// Provide the star rating (only difficulty attribute for osu!mania).
fn attributes(self) -> Option<f64>;
}
impl ManiaAttributeProvider for f64 {
#[inline]
fn attributes(self) -> Option<f64> {
Some(self)
}
/// Provide the actual difficulty attributes.
fn attributes(self) -> Option<ManiaDifficultyAttributes>;
}
impl ManiaAttributeProvider for ManiaDifficultyAttributes {
#[inline]
fn attributes(self) -> Option<f64> {
Some(self.stars)
fn attributes(self) -> Option<ManiaDifficultyAttributes> {
Some(self)
}
}
impl ManiaAttributeProvider for ManiaPerformanceAttributes {
#[inline]
fn attributes(self) -> Option<f64> {
Some(self.difficulty.stars)
fn attributes(self) -> Option<ManiaDifficultyAttributes> {
Some(self.difficulty)
}
}
impl ManiaAttributeProvider for DifficultyAttributes {
#[inline]
fn attributes(self) -> Option<f64> {
#[allow(irrefutable_let_patterns)]
if let Self::Mania(attributes) = self {
Some(attributes.stars)
fn attributes(self) -> Option<ManiaDifficultyAttributes> {
if let Self::Mania(attrs) = self {
Some(attrs)
} else {
None
}
@@ -271,10 +473,9 @@ impl ManiaAttributeProvider for DifficultyAttributes {
impl ManiaAttributeProvider for PerformanceAttributes {
#[inline]
fn attributes(self) -> Option<f64> {
#[allow(irrefutable_let_patterns)]
if let Self::Mania(attributes) = self {
Some(attributes.difficulty.stars)
fn attributes(self) -> Option<ManiaDifficultyAttributes> {
if let Self::Mania(attrs) = self {
Some(attrs.difficulty)
} else {
None
}
+135
View File
@@ -0,0 +1,135 @@
use std::{cmp::Ordering, mem};
use super::{difficulty_object::ManiaDifficultyObject, SECTION_LEN};
pub(crate) use self::strain::Strain;
mod strain;
pub(crate) trait Skill {
fn process(
&mut self,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
);
fn difficulty_value(self) -> f64;
}
pub(crate) trait StrainSkill: Sized + Skill {
const DECAY_WEIGHT: f64 = 0.9;
fn curr_section_end(&self) -> f64;
fn curr_section_end_mut(&mut self) -> &mut f64;
fn curr_section_peak(&self) -> f64;
fn curr_section_peak_mut(&mut self) -> &mut f64;
fn strain_peaks_mut(&mut self) -> &mut Vec<f64>;
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64;
fn process(
&mut self,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) {
// The first object doesn't generate a strain, so we begin with an incremented section end
if curr.idx == 0 {
// currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
*self.curr_section_end_mut() = (curr.start_time / SECTION_LEN).ceil() * SECTION_LEN;
}
while curr.start_time > self.curr_section_end() {
self.save_curr_peak();
self.start_new_section_from(self.curr_section_end(), curr, diff_objects);
*self.curr_section_end_mut() += SECTION_LEN;
}
*self.curr_section_peak_mut() = self.strain_value_at(curr).max(self.curr_section_peak());
}
fn save_curr_peak(&mut self) {
let curr_section_peak = self.curr_section_peak();
self.strain_peaks_mut().push(curr_section_peak);
}
fn start_new_section_from(
&mut self,
time: f64,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) {
*self.curr_section_peak_mut() = self.calculate_initial_strain(time, curr, diff_objects);
}
fn calculate_initial_strain(
&self,
time: f64,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) -> f64;
fn get_curr_strain_peaks(mut self) -> Vec<f64> {
let mut peaks = mem::take(self.strain_peaks_mut());
peaks.push(self.curr_section_peak());
peaks
}
fn difficulty_value(self) -> f64 {
let mut difficulty = 0.0;
let mut weight = 1.0;
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
let mut peaks = self.get_curr_strain_peaks();
peaks.retain(|&peak| peak > 0.0);
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
for strain in peaks {
difficulty += strain * weight;
weight *= Self::DECAY_WEIGHT;
}
difficulty
}
}
pub(crate) trait StrainDecaySkill: StrainSkill {
const SKILL_MULTIPLIER: f64;
const STRAIN_DECAY_BASE: f64;
fn curr_strain(&self) -> f64;
fn curr_strain_mut(&mut self) -> &mut f64;
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64;
fn calculate_initial_strain(
&self,
time: f64,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) -> f64;
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64 {
*self.curr_strain_mut() *= self.strain_decay(curr.delta_time);
*self.curr_strain_mut() += self.strain_value_of(curr) * Self::SKILL_MULTIPLIER;
self.curr_strain()
}
fn strain_decay(&self, ms: f64) -> f64 {
Self::STRAIN_DECAY_BASE.powf(ms / 1000.0)
}
}
fn previous<'map, 'objects>(
diff_objects: &'objects [ManiaDifficultyObject<'map>],
curr: usize,
backwards_idx: usize,
) -> Option<&'objects ManiaDifficultyObject<'map>> {
curr.checked_sub(backwards_idx + 1)
.and_then(|idx| diff_objects.get(idx))
}
+205
View File
@@ -0,0 +1,205 @@
use crate::mania::difficulty_object::ManiaDifficultyObject;
use super::{previous, Skill, StrainDecaySkill, StrainSkill};
pub(crate) struct Strain {
start_times: Vec<f64>,
end_times: Vec<f64>,
individual_strains: Vec<f64>,
individual_strain: f64,
overall_strain: f64,
curr_strain: f64,
curr_section_peak: f64,
curr_section_end: f64,
pub(crate) strain_peaks: Vec<f64>,
total_columns: f32,
}
impl Strain {
const INDIVIDUAL_DECAY_BASE: f64 = 0.125;
const OVERALL_DECAY_BASE: f64 = 0.3;
const RELEASE_THRESHOLD: f64 = 24.0;
pub(crate) fn new(total_columns: usize) -> Self {
Self {
start_times: Vec::with_capacity(total_columns),
end_times: Vec::with_capacity(total_columns),
individual_strains: Vec::with_capacity(total_columns),
individual_strain: 0.0,
overall_strain: 1.0,
curr_strain: 0.0,
curr_section_peak: 0.0,
curr_section_end: 0.0,
strain_peaks: Vec::new(),
total_columns: total_columns as f32,
}
}
fn apply_decay(value: f64, delta_time: f64, decay_base: f64) -> f64 {
value * decay_base.powf(delta_time / 1000.0)
}
}
impl Skill for Strain {
fn process(
&mut self,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) {
<Self as StrainSkill>::process(self, curr, diff_objects)
}
fn difficulty_value(self) -> f64 {
<Self as StrainSkill>::difficulty_value(self)
}
}
impl StrainSkill for Strain {
const DECAY_WEIGHT: f64 = 0.9;
fn curr_section_end(&self) -> f64 {
self.curr_section_end
}
fn curr_section_end_mut(&mut self) -> &mut f64 {
&mut self.curr_section_end
}
fn curr_section_peak(&self) -> f64 {
self.curr_section_peak
}
fn curr_section_peak_mut(&mut self) -> &mut f64 {
&mut self.curr_section_peak
}
fn strain_peaks_mut(&mut self) -> &mut Vec<f64> {
&mut self.strain_peaks
}
fn strain_value_at(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64 {
<Self as StrainDecaySkill>::strain_value_at(self, curr)
}
fn calculate_initial_strain(
&self,
time: f64,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) -> f64 {
<Self as StrainDecaySkill>::calculate_initial_strain(self, time, curr, diff_objects)
}
}
impl StrainDecaySkill for Strain {
const SKILL_MULTIPLIER: f64 = 1.0;
const STRAIN_DECAY_BASE: f64 = 1.0;
fn curr_strain(&self) -> f64 {
self.curr_strain
}
fn curr_strain_mut(&mut self) -> &mut f64 {
&mut self.curr_strain
}
fn strain_value_of(&mut self, curr: &ManiaDifficultyObject<'_>) -> f64 {
let mania_curr = curr;
let start_time = mania_curr.start_time;
let end_time = mania_curr.end_time;
let col = mania_curr.base.column(self.total_columns);
let mut is_overlapping = false;
// Lowest value we can assume with the current information
let mut closest_end_time = (end_time - start_time).abs();
// Factor to all additional strains in case something else is held
let mut hold_factor = 1.0;
// Addition to the current note in case it's a hold and has to be released awkwardly
let mut hold_addition = 0.0;
for i in 0..self.end_times.len() {
// The current note is overlapped if a previous note or end is overlapping the current note body
is_overlapping |=
self.end_times[i] > start_time + 1.0 && end_time > self.end_times[i] + 1.0;
// We give a slight bonus to everything if something is held meanwhile
if self.end_times[i] > end_time + 1.0 {
hold_factor = 1.25;
}
closest_end_time = (end_time - self.end_times[i]).abs().min(closest_end_time);
}
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
// ^
// 1.0 + - - - - - -+-----------
// | /
// 0.5 + - - - - -/ Sigmoid Curve
// | /|
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if is_overlapping {
hold_addition =
(1.0 + (0.5 * (Self::RELEASE_THRESHOLD - closest_end_time)).exp()).recip();
}
// Decay and increase individualStrains in own column
self.individual_strains[col] = Self::apply_decay(
self.individual_strains[col],
start_time - self.start_times[col],
Self::INDIVIDUAL_DECAY_BASE,
);
self.individual_strains[col] += 2.0 * hold_factor;
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
self.individual_strain = if mania_curr.delta_time <= 1.0 {
self.individual_strain.max(self.individual_strains[col])
} else {
self.individual_strains[col]
};
// Decay and increase overallStrain
self.overall_strain = Self::apply_decay(
self.overall_strain,
curr.delta_time,
Self::OVERALL_DECAY_BASE,
);
self.overall_strain += (1.0 + hold_addition) * hold_factor;
// Update startTimes and endTimes arrays
self.start_times[col] = start_time;
self.end_times[col] = end_time;
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
self.individual_strain + self.overall_strain - self.curr_strain
}
fn calculate_initial_strain(
&self,
offset: f64,
curr: &ManiaDifficultyObject<'_>,
diff_objects: &[ManiaDifficultyObject<'_>],
) -> f64 {
let prev_start = previous(diff_objects, curr.idx, 0).map_or(0.0, |h| h.start_time);
let individual_decay = Self::apply_decay(
self.individual_strain,
offset - prev_start,
Self::INDIVIDUAL_DECAY_BASE,
);
let overall_decay = Self::apply_decay(
self.overall_strain,
offset - prev_start,
Self::OVERALL_DECAY_BASE,
);
individual_decay * overall_decay
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ pub struct OsuScoreState {
/// Amount of current 50s.
pub n50: usize,
/// Amount of current misses.
pub misses: usize,
pub n_misses: usize,
}
impl OsuScoreState {
+3 -3
View File
@@ -138,7 +138,7 @@ impl<'map> OsuPP<'map> {
/// Specify the amount of misses of a play.
#[inline]
pub fn misses(mut self, n_misses: usize) -> Self {
pub fn n_misses(mut self, n_misses: usize) -> Self {
self.n_misses = n_misses;
self
@@ -174,14 +174,14 @@ impl<'map> OsuPP<'map> {
n300,
n100,
n50,
misses,
n_misses,
} = state;
self.combo = Some(max_combo);
self.n300 = Some(n300);
self.n100 = Some(n100);
self.n50 = Some(n50);
self.n_misses = misses;
self.n_misses = n_misses;
self
}
-223
View File
@@ -1,223 +0,0 @@
use super::{lerp, skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
use std::{cmp::Ordering, fmt};
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
#[derive(Clone, Debug)]
pub(crate) struct Skills {
skills: Box<[Skill]>,
mask: u8,
}
impl Skills {
const RX: u8 = 1 << 0;
const FL: u8 = 1 << 1;
pub(crate) fn new(hit_window: f64, rx: bool, radius: f32, fl: bool) -> Self {
let mut skills = Vec::with_capacity(2 + !rx as usize + fl as usize);
skills.push(Skill::aim(true));
skills.push(Skill::aim(false));
if !rx {
skills.push(Skill::speed(hit_window));
}
if fl {
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
let scaling_factor = 52.0 / radius as f64;
skills.push(Skill::flashlight(scaling_factor));
}
let mask = rx as u8 * Self::RX + fl as u8 * Self::FL;
let skills = skills.into_boxed_slice();
Self { skills, mask }
}
pub(crate) fn start_new_section_from(&mut self, curr_section_end: f64) {
for skill in self.skills.iter_mut() {
skill.start_new_section_from(curr_section_end);
}
}
pub(crate) fn save_peak_and_start_new_section(&mut self, curr_section_end: f64) {
for skill in self.skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(curr_section_end);
}
}
pub(crate) fn save_current_peak(&mut self) {
for skill in self.skills.iter_mut() {
skill.save_current_peak();
}
}
pub(crate) fn process(&mut self, h: &DifficultyObject<'_>) {
for skill in self.skills.iter_mut() {
skill.process(h);
}
}
pub(crate) fn aim(&mut self) -> &mut Skill {
&mut self.skills[0]
}
pub(crate) fn aim_no_sliders(&mut self) -> &mut Skill {
&mut self.skills[1]
}
pub(crate) fn speed_flashlight(&mut self) -> (Option<&mut Skill>, Option<&mut Skill>) {
match (self.mask & Self::RX, self.mask & Self::FL) {
// only speed
(0, 0) => (Some(&mut self.skills[2]), None),
// both speed and flashlight
(0, _) => {
let (left, right) = self.skills.split_at_mut(3);
(Some(&mut left[2]), Some(&mut right[0]))
}
// neither
(_, 0) => (None, None),
// only flashlight
(_, _) => (None, Some(&mut self.skills[2])),
}
}
}
#[derive(Clone)]
pub(crate) struct Skill {
curr_strain: f64,
pub(crate) curr_section_peak: f64,
kind: SkillKind,
pub(crate) strain_peaks: Vec<f64>,
prev_time: Option<f64>,
}
impl Skill {
#[inline]
pub(crate) fn aim(with_sliders: bool) -> Self {
Self::new(SkillKind::aim(with_sliders))
}
#[inline]
pub(crate) fn flashlight(scaling_factor: f64) -> Self {
Self::new(SkillKind::flashlight(scaling_factor))
}
#[inline]
pub(crate) fn speed(hit_window: f64) -> Self {
Self::new(SkillKind::speed(hit_window))
}
#[inline]
fn new(kind: SkillKind) -> Self {
Self {
curr_strain: 0.0,
curr_section_peak: 0.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>) {
self.kind.pre_process();
self.curr_section_peak = self.strain_value_at(curr).max(self.curr_section_peak);
self.prev_time = Some(curr.base.start_time / curr.clock_rate);
self.kind.post_process(curr);
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
self.strain_peaks.push(self.curr_section_peak);
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f64) {
// The maximum strain of the new section is not zero by default
self.curr_section_peak = self.calculate_initial_strain(time);
}
pub(crate) fn difficulty_value(strain_peaks: &mut [f64], this: &Self) -> f64 {
// ? Common values to debug
// println!("---");
// for (i, strain) in self.strain_peaks.iter().enumerate() {
// println!("[{}] {}", i, strain);
// }
let mut difficulty = 0.0;
let mut weight = 1.0;
let decay_weight = this.kind.decay_weight();
let (reduced_section_count, difficulty_multiplier) = this.kind.difficulty_values();
let reduced_section_count_f64 = reduced_section_count as f64;
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
let peaks = strain_peaks.iter_mut();
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
let clamped = (i as f64 / reduced_section_count_f64).clamp(0.0, 1.0);
let scale = (lerp(1.0, 10.0, clamped)).log10();
*strain *= lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
}
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in strain_peaks.iter() {
difficulty += strain * weight;
weight *= decay_weight;
}
difficulty * difficulty_multiplier
}
pub(crate) fn calculate_initial_strain(&self, time: f64) -> f64 {
let prev_time = self.prev_time.unwrap_or(0.0);
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
match &self.kind {
SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => decayed_strain,
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
}
}
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f64 {
self.curr_strain *= self.kind.strain_decay(curr.delta);
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
match &mut self.kind {
SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => self.curr_strain,
SkillKind::Speed {
curr_rhythm,
history,
hit_window,
} => {
*curr_rhythm = calculate_speed_rhythm_bonus(curr, history, *hit_window);
self.curr_strain * *curr_rhythm
}
}
}
}
impl fmt::Debug for Skill {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Skill")
.field("curr_strain", &self.curr_strain)
.field("curr_section_peak", &self.curr_section_peak)
.field("kind", &self.kind)
.field("strain_peaks_len", &self.strain_peaks.len())
.field("prev_time", &self.prev_time)
.finish()
}
}
-8
View File
@@ -44,14 +44,6 @@ impl HitObject {
pub fn is_spinner(&self) -> bool {
matches!(self.kind, HitObjectKind::Spinner { .. })
}
/// The column of this node for osu!mania
#[inline]
pub fn column(&self, total_columns: f32) -> u8 {
let x_divisor = 512.0 / total_columns;
(self.pos.x / x_divisor).floor().min(total_columns - 1.0) as u8
}
}
impl PartialOrd for HitObject {
+20 -28
View File
@@ -145,7 +145,7 @@ impl<'map> AnyPP<'map> {
pub fn state(self, state: ScoreState) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.state(state.into())),
Self::Mania(m) => Self::Mania(m.score(state.score)),
Self::Mania(m) => Self::Mania(m.state(state.into())),
Self::Osu(o) => Self::Osu(o.state(state.into())),
Self::Taiko(t) => Self::Taiko(t.state(state.into())),
}
@@ -155,28 +155,24 @@ impl<'map> AnyPP<'map> {
///
/// For some modes this method depends on previously set values.
/// Be sure to call this last before calling `calculate`.
///
/// Irrelevant for osu!mania.
#[inline]
pub fn accuracy(self, acc: f64) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.accuracy(acc)),
Self::Mania(_) => self,
Self::Mania(m) => Self::Mania(m.accuracy(acc)),
Self::Osu(o) => Self::Osu(o.accuracy(acc)),
Self::Taiko(t) => Self::Taiko(t.accuracy(acc)),
}
}
/// Specify the amount of misses of a play.
///
/// Irrelevant for osu!mania.
#[inline]
pub fn misses(self, misses: usize) -> Self {
pub fn n_misses(self, n_misses: usize) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.misses(misses)),
Self::Mania(_) => self,
Self::Osu(o) => Self::Osu(o.misses(misses)),
Self::Taiko(t) => Self::Taiko(t.misses(misses)),
Self::Catch(f) => Self::Catch(f.misses(n_misses)),
Self::Mania(m) => Self::Mania(m.n_misses(n_misses)),
Self::Osu(o) => Self::Osu(o.n_misses(n_misses)),
Self::Taiko(t) => Self::Taiko(t.n_misses(n_misses)),
}
}
@@ -194,26 +190,22 @@ impl<'map> AnyPP<'map> {
}
/// Specify the amount of 300s of a play.
///
/// Irrelevant for osu!mania.
#[inline]
pub fn n300(self, n300: usize) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.fruits(n300)),
Self::Mania(_) => self,
Self::Mania(m) => Self::Mania(m.n300(n300)),
Self::Osu(o) => Self::Osu(o.n300(n300)),
Self::Taiko(t) => Self::Taiko(t.n300(n300)),
}
}
/// Specify the amount of 100s of a play.
///
/// Irrelevant for osu!mania.
#[inline]
pub fn n100(self, n100: usize) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.droplets(n100)),
Self::Mania(_) => self,
Self::Mania(m) => Self::Mania(m.n100(n100)),
Self::Osu(o) => Self::Osu(o.n100(n100)),
Self::Taiko(t) => Self::Taiko(t.n100(n100)),
}
@@ -221,12 +213,12 @@ impl<'map> AnyPP<'map> {
/// Specify the amount of 50s of a play.
///
/// Irrelevant for osu!mania and osu!taiko.
/// Irrelevant for osu!taiko.
#[inline]
pub fn n50(self, n50: usize) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.tiny_droplets(n50)),
Self::Mania(_) => self,
Self::Mania(m) => Self::Mania(m.n50(n50)),
Self::Osu(o) => Self::Osu(o.n50(n50)),
Self::Taiko(_) => self,
}
@@ -235,29 +227,29 @@ impl<'map> AnyPP<'map> {
/// Specify the amount of katus of a play.
///
/// This value is only relevant for osu!catch for which it represents
/// the amount of tiny droplet misses.
/// the amount of tiny droplet misses and osu!mania for which it.
/// repesents the amount of n200.
#[inline]
pub fn n_katu(self, n_katu: usize) -> Self {
match self {
Self::Catch(f) => Self::Catch(f.tiny_droplet_misses(n_katu)),
Self::Mania(_) => self,
Self::Mania(m) => Self::Mania(m.n200(n_katu)),
Self::Osu(_) => self,
Self::Taiko(_) => self,
}
}
/// Specify the score of a play.
/// Specify the amount of gekis of a play.
///
/// This value is only relevant for osu!mania.
///
/// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
/// This value is only relevant for osu!mania for which it.
/// repesents the amount of n320.
#[inline]
pub fn score(self, score: u32) -> Self {
pub fn n_geki(self, n_geki: usize) -> Self {
match self {
Self::Catch(_) => self,
Self::Mania(m) => Self::Mania(m.score(score)),
Self::Mania(m) => Self::Mania(m.n320(n_geki)),
Self::Osu(_) => self,
Self::Taiko(_) => self,
Self::Catch(_) => self,
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ pub struct TaikoScoreState {
/// Amount of current 100s.
pub n100: usize,
/// Amount of current misses.
pub misses: usize,
pub n_misses: usize,
}
impl TaikoScoreState {
+9 -9
View File
@@ -73,9 +73,9 @@ impl<'map> TaikoPP<'map> {
/// If you already calculated the attributes for the current map-mod combination,
/// be sure to put them in here so that they don't have to be recalculated.
#[inline]
pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self {
if let Some(attributes) = attributes.attributes() {
self.attributes.replace(attributes);
pub fn attributes(mut self, attrs: impl TaikoAttributeProvider) -> Self {
if let Some(attrs) = attrs.attributes() {
self.attributes = Some(attrs);
}
self
@@ -94,7 +94,7 @@ impl<'map> TaikoPP<'map> {
/// Specify the max combo of the play.
#[inline]
pub fn combo(mut self, combo: usize) -> Self {
self.combo.replace(combo);
self.combo = Some(combo);
self
}
@@ -102,7 +102,7 @@ impl<'map> TaikoPP<'map> {
/// Specify the amount of 300s of a play.
#[inline]
pub fn n300(mut self, n300: usize) -> Self {
self.n300.replace(n300);
self.n300 = Some(n300);
self
}
@@ -110,14 +110,14 @@ impl<'map> TaikoPP<'map> {
/// Specify the amount of 100s of a play.
#[inline]
pub fn n100(mut self, n100: usize) -> Self {
self.n100.replace(n100);
self.n100 = Some(n100);
self
}
/// Specify the amount of misses of the play.
#[inline]
pub fn misses(mut self, n_misses: usize) -> Self {
pub fn n_misses(mut self, n_misses: usize) -> Self {
self.n_misses = n_misses.min(self.map.n_circles as usize);
self
@@ -162,13 +162,13 @@ impl<'map> TaikoPP<'map> {
max_combo,
n300,
n100,
misses,
n_misses,
} = state;
self.combo = Some(max_combo);
self.n300 = Some(n300);
self.n100 = Some(n100);
self.n_misses = misses;
self.n_misses = n_misses;
self
}