gradual calc for taiko
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
use std::{cell::RefCell, mem, rc::Rc, slice::Iter};
|
||||
|
||||
use crate::{
|
||||
model::{beatmap::HitWindows, hit_object::HitObject},
|
||||
taiko::TaikoBeatmap,
|
||||
ModeDifficulty,
|
||||
};
|
||||
|
||||
use super::{
|
||||
object::{TaikoDifficultyObject, TaikoDifficultyObjects},
|
||||
skills::peaks::{Peaks, PeaksSkill},
|
||||
DifficultyValues, TaikoDifficultyAttributes,
|
||||
};
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!taiko map.
|
||||
///
|
||||
/// Note that this struct implements [`Iterator`]. On every call of
|
||||
/// [`Iterator::next`], the map's next hit object will be processed and the
|
||||
/// [`TaikoDifficultyAttributes`] will be updated and returned.
|
||||
///
|
||||
/// If you want to calculate performance attributes, use
|
||||
/// [`TaikoGradualPerformance`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, ModeDifficulty};
|
||||
/// use rosu_pp::taiko::{Taiko, TaikoGradualDifficulty};
|
||||
///
|
||||
/// let map = Beatmap::from_path("./resources/1028484.osu")
|
||||
/// .unwrap()
|
||||
/// .unchecked_into_converted::<Taiko>();
|
||||
///
|
||||
/// let difficulty = ModeDifficulty::new().mods(64); // DT
|
||||
/// let mut iter = TaikoGradualDifficulty::new(&difficulty, &converted);
|
||||
///
|
||||
/// // the difficulty of the map after the first hit object
|
||||
/// let attrs1 = iter.next();
|
||||
/// // ... after the second hit object
|
||||
/// let attrs2 = iter.next();
|
||||
///
|
||||
/// // Remaining hit objects
|
||||
/// for difficulty in iter {
|
||||
/// // ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`TaikoGradualPerformance`]: crate::taiko::TaikoGradualPerformance
|
||||
pub struct TaikoGradualDifficulty {
|
||||
pub(crate) idx: usize,
|
||||
pub(crate) mods: u32,
|
||||
pub(crate) clock_rate: f64,
|
||||
attrs: TaikoDifficultyAttributes,
|
||||
diff_objects: TaikoDifficultyObjects,
|
||||
diff_objects_iter: Iter<'static, Rc<RefCell<TaikoDifficultyObject>>>,
|
||||
peaks: Peaks,
|
||||
total_hits: usize,
|
||||
first_combos: FirstTwoCombos,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum FirstTwoCombos {
|
||||
None,
|
||||
OnlyFirst,
|
||||
OnlySecond,
|
||||
Both,
|
||||
}
|
||||
|
||||
impl TaikoGradualDifficulty {
|
||||
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: &TaikoBeatmap<'_>) -> Self {
|
||||
let take = difficulty.get_passed_objects();
|
||||
let mods = difficulty.get_mods();
|
||||
let clock_rate = difficulty.get_clock_rate();
|
||||
|
||||
let first_combos = match (
|
||||
converted.map.hit_objects.first().map(HitObject::is_circle),
|
||||
converted.map.hit_objects.get(1).map(HitObject::is_circle),
|
||||
) {
|
||||
(None, _) | (Some(false), Some(false) | None) => FirstTwoCombos::None,
|
||||
(Some(true), Some(false) | None) => FirstTwoCombos::OnlyFirst,
|
||||
(Some(false), Some(true)) => FirstTwoCombos::OnlySecond,
|
||||
(Some(true), Some(true)) => FirstTwoCombos::Both,
|
||||
};
|
||||
|
||||
let HitWindows { od: hit_window, .. } = converted
|
||||
.attributes()
|
||||
.mods(mods)
|
||||
.clock_rate(clock_rate)
|
||||
.hit_windows();
|
||||
|
||||
let mut n_diff_objects = 0;
|
||||
let mut max_combo = 0;
|
||||
|
||||
let diff_objects = DifficultyValues::create_difficulty_objects(
|
||||
converted,
|
||||
take as u32,
|
||||
clock_rate,
|
||||
&mut max_combo,
|
||||
&mut n_diff_objects,
|
||||
);
|
||||
|
||||
let peaks = Peaks::new();
|
||||
|
||||
let attrs = TaikoDifficultyAttributes {
|
||||
hit_window,
|
||||
is_convert: converted.is_convert,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let total_hits = converted
|
||||
.map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| h.is_circle())
|
||||
.count();
|
||||
|
||||
let diff_objects_iter = extend_lifetime(diff_objects.iter());
|
||||
|
||||
Self {
|
||||
idx: 0,
|
||||
mods,
|
||||
clock_rate,
|
||||
diff_objects,
|
||||
diff_objects_iter,
|
||||
peaks,
|
||||
attrs,
|
||||
total_hits,
|
||||
first_combos,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_lifetime(
|
||||
iter: Iter<'_, Rc<RefCell<TaikoDifficultyObject>>>,
|
||||
) -> Iter<'static, Rc<RefCell<TaikoDifficultyObject>>> {
|
||||
// SAFETY: The underlying data will never be moved.
|
||||
unsafe { mem::transmute(iter) }
|
||||
}
|
||||
|
||||
impl Iterator for TaikoGradualDifficulty {
|
||||
type Item = TaikoDifficultyAttributes;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// The first difficulty object belongs to the third note since each
|
||||
// difficulty object requires the current, the last, and the second to
|
||||
// last note. Hence, if we're still on the first or second object, we
|
||||
// don't have a difficulty object yet and just skip processing.
|
||||
if self.idx >= 2 {
|
||||
loop {
|
||||
let curr = self.diff_objects_iter.next()?;
|
||||
let borrowed = curr.borrow();
|
||||
PeaksSkill::new(&mut self.peaks, &self.diff_objects).process(&borrowed);
|
||||
|
||||
if borrowed.base_hit_type.is_hit() {
|
||||
self.attrs.max_combo += 1;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if self.diff_objects.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
match self.first_combos {
|
||||
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::OnlySecond if self.idx == 1 => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::Both if self.idx == 0 => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::Both if self.idx == 1 => self.attrs.max_combo = 2,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.idx += 1;
|
||||
|
||||
let color = self.peaks.color_difficulty_value();
|
||||
let rhythm = self.peaks.rhythm_difficulty_value();
|
||||
let stamina = self.peaks.stamina_difficulty_value();
|
||||
let combined = self.peaks.clone().difficulty_value();
|
||||
|
||||
let mut attrs = self.attrs.clone();
|
||||
|
||||
DifficultyValues::eval(&mut attrs, color, rhythm, stamina, combined);
|
||||
|
||||
Some(attrs)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = self.len();
|
||||
|
||||
(len, Some(len))
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
let mut take = n.min(self.len().saturating_sub(1));
|
||||
|
||||
// The first two notes have no difficulty object but might add to combo
|
||||
match (take, self.idx) {
|
||||
(_, 2..) | (0, _) => {}
|
||||
(1, 0) => {
|
||||
take -= 1;
|
||||
self.idx += 1;
|
||||
|
||||
match self.first_combos {
|
||||
FirstTwoCombos::None => {}
|
||||
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::OnlySecond => {}
|
||||
FirstTwoCombos::Both => self.attrs.max_combo = 1,
|
||||
}
|
||||
}
|
||||
(_, 0) => {
|
||||
take -= 2;
|
||||
self.idx += 2;
|
||||
|
||||
match self.first_combos {
|
||||
FirstTwoCombos::None => {}
|
||||
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::OnlySecond => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::Both => self.attrs.max_combo = 2,
|
||||
}
|
||||
}
|
||||
(_, 1) => {
|
||||
take -= 1;
|
||||
self.idx += 1;
|
||||
|
||||
match self.first_combos {
|
||||
FirstTwoCombos::None => {}
|
||||
FirstTwoCombos::OnlyFirst => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::OnlySecond => self.attrs.max_combo = 1,
|
||||
FirstTwoCombos::Both => self.attrs.max_combo = 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut peaks = PeaksSkill::new(&mut self.peaks, &self.diff_objects);
|
||||
|
||||
for _ in 0..take {
|
||||
loop {
|
||||
let curr = self.diff_objects_iter.next()?;
|
||||
let borrowed = curr.borrow();
|
||||
peaks.process(&borrowed);
|
||||
|
||||
if borrowed.base_hit_type.is_hit() {
|
||||
self.attrs.max_combo += 1;
|
||||
self.idx += 1;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for TaikoGradualDifficulty {
|
||||
fn len(&self) -> usize {
|
||||
self.total_hits - self.idx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::Beatmap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let converted = Beatmap::from_bytes(&[]).unwrap().unchecked_into_converted();
|
||||
|
||||
let difficulty = ModeDifficulty::new();
|
||||
let mut gradual = TaikoGradualDifficulty::new(&difficulty, &converted);
|
||||
|
||||
assert!(gradual.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1028484.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let difficulty = ModeDifficulty::new();
|
||||
|
||||
let mut gradual = TaikoGradualDifficulty::new(&difficulty, &converted);
|
||||
let mut gradual_2nd = TaikoGradualDifficulty::new(&difficulty, &converted);
|
||||
let mut gradual_3rd = TaikoGradualDifficulty::new(&difficulty, &converted);
|
||||
|
||||
let hit_objects_len = converted.map.hit_objects.len();
|
||||
|
||||
let n_hits = converted
|
||||
.map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| h.is_circle())
|
||||
.count();
|
||||
|
||||
for i in 1.. {
|
||||
let Some(next_gradual) = gradual.next() else {
|
||||
assert_eq!(i, n_hits + 1);
|
||||
assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let expected = ModeDifficulty::new()
|
||||
.passed_objects(i as u32)
|
||||
.calculate(&converted);
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
-50
@@ -15,6 +15,7 @@ use self::skills::peaks::Peaks;
|
||||
use super::{attributes::TaikoDifficultyAttributes, convert::TaikoBeatmap};
|
||||
|
||||
mod color;
|
||||
pub mod gradual;
|
||||
mod object;
|
||||
mod rhythm;
|
||||
mod skills;
|
||||
@@ -36,35 +37,27 @@ pub fn difficulty(
|
||||
|
||||
let DifficultyValues { peaks, max_combo } = DifficultyValues::calculate(difficulty, converted);
|
||||
|
||||
let color_rating = peaks.color_difficulty_value() * DIFFICULTY_MULTIPLIER;
|
||||
let rhythm_rating = peaks.rhythm_difficulty_value() * DIFFICULTY_MULTIPLIER;
|
||||
let stamina_rating = peaks.stamina_difficulty_value() * DIFFICULTY_MULTIPLIER;
|
||||
let combined_rating = peaks.difficulty_value() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
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 converted.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.8;
|
||||
}
|
||||
}
|
||||
|
||||
TaikoDifficultyAttributes {
|
||||
stamina: stamina_rating,
|
||||
rhythm: rhythm_rating,
|
||||
color: color_rating,
|
||||
peak: combined_rating,
|
||||
let mut attrs = TaikoDifficultyAttributes {
|
||||
hit_window,
|
||||
stars: star_rating,
|
||||
max_combo,
|
||||
is_convert: converted.is_convert,
|
||||
}
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let color_rating = peaks.color_difficulty_value();
|
||||
let rhythm_rating = peaks.rhythm_difficulty_value();
|
||||
let stamina_rating = peaks.stamina_difficulty_value();
|
||||
let combined_rating = peaks.difficulty_value();
|
||||
|
||||
DifficultyValues::eval(
|
||||
&mut attrs,
|
||||
color_rating,
|
||||
rhythm_rating,
|
||||
stamina_rating,
|
||||
combined_rating,
|
||||
);
|
||||
|
||||
attrs
|
||||
}
|
||||
|
||||
fn rescale(stars: f64) -> f64 {
|
||||
@@ -88,6 +81,73 @@ impl DifficultyValues {
|
||||
let mut n_diff_objects = 0;
|
||||
let mut max_combo = 0;
|
||||
|
||||
let diff_objects = Self::create_difficulty_objects(
|
||||
converted,
|
||||
take as u32,
|
||||
clock_rate,
|
||||
&mut max_combo,
|
||||
&mut n_diff_objects,
|
||||
);
|
||||
|
||||
// The first two hit objects have no difficulty object
|
||||
n_diff_objects = n_diff_objects.saturating_sub(2);
|
||||
|
||||
let mut peaks = Peaks::new();
|
||||
|
||||
{
|
||||
let mut peaks = PeaksSkill::new(&mut peaks, &diff_objects);
|
||||
|
||||
for hit_object in diff_objects.iter().take(n_diff_objects) {
|
||||
peaks.process(&hit_object.borrow());
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
peaks,
|
||||
max_combo: max_combo as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval(
|
||||
attrs: &mut TaikoDifficultyAttributes,
|
||||
color_difficulty_value: f64,
|
||||
rhythm_difficulty_value: f64,
|
||||
stamina_difficulty_value: f64,
|
||||
peaks_difficulty_value: f64,
|
||||
) {
|
||||
let color_rating = color_difficulty_value * DIFFICULTY_MULTIPLIER;
|
||||
let rhythm_rating = rhythm_difficulty_value * DIFFICULTY_MULTIPLIER;
|
||||
let stamina_rating = stamina_difficulty_value * DIFFICULTY_MULTIPLIER;
|
||||
let combined_rating = peaks_difficulty_value * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
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.8;
|
||||
}
|
||||
}
|
||||
|
||||
attrs.stamina = stamina_rating;
|
||||
attrs.rhythm = rhythm_rating;
|
||||
attrs.color = color_rating;
|
||||
attrs.peak = combined_rating;
|
||||
attrs.stars = star_rating;
|
||||
}
|
||||
|
||||
pub fn create_difficulty_objects(
|
||||
converted: &TaikoBeatmap<'_>,
|
||||
take: u32,
|
||||
clock_rate: f64,
|
||||
max_combo: &mut u32,
|
||||
n_diff_objects: &mut usize,
|
||||
) -> TaikoDifficultyObjects {
|
||||
let mut hit_objects_iter = converted
|
||||
.map
|
||||
.hit_objects
|
||||
@@ -95,18 +155,15 @@ impl DifficultyValues {
|
||||
.zip(converted.map.hit_sounds.iter())
|
||||
.map(|(h, s)| TaikoObject::new(h, *s))
|
||||
.inspect(|h| {
|
||||
if max_combo < take {
|
||||
n_diff_objects += 1;
|
||||
max_combo += usize::from(h.is_hit());
|
||||
if *max_combo < take {
|
||||
*n_diff_objects += 1;
|
||||
*max_combo += u32::from(h.is_hit());
|
||||
}
|
||||
});
|
||||
|
||||
let Some((mut last_last, mut last)) = hit_objects_iter.next().zip(hit_objects_iter.next())
|
||||
else {
|
||||
return Self {
|
||||
peaks: Peaks::new(),
|
||||
max_combo: max_combo as u32,
|
||||
};
|
||||
return TaikoDifficultyObjects::with_capacity(0);
|
||||
};
|
||||
|
||||
let mut diff_objects =
|
||||
@@ -130,22 +187,6 @@ impl DifficultyValues {
|
||||
|
||||
ColorDifficultyPreprocessor::process_and_assign(&diff_objects);
|
||||
|
||||
// The first two hit objects have no difficulty object
|
||||
n_diff_objects -= 2;
|
||||
|
||||
let mut peaks = Peaks::new();
|
||||
|
||||
{
|
||||
let mut peaks = PeaksSkill::new(&mut peaks, &diff_objects);
|
||||
|
||||
for hit_object in diff_objects.iter().take(n_diff_objects) {
|
||||
peaks.process(&hit_object.borrow());
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
peaks,
|
||||
max_combo: max_combo as u32,
|
||||
}
|
||||
diff_objects
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@ impl TaikoDifficultyObjects {
|
||||
self.objects.push(hit_object);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.objects.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter<'_, Rc<RefCell<TaikoDifficultyObject>>> {
|
||||
self.objects.iter()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
const SKILL_MULTIPLIER: f64 = 0.12;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.8;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Color {
|
||||
inner: StrainDecaySkill,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const STAMINA_SKILL_MULTIPLIER: f64 = 0.375 * FINAL_MULTIPLIER;
|
||||
|
||||
const FINAL_MULTIPLIER: f64 = 0.0625;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Peaks {
|
||||
pub color: Color,
|
||||
pub rhythm: Rhythm,
|
||||
|
||||
@@ -20,7 +20,7 @@ const STRAIN_DECAY: f64 = 0.96;
|
||||
const RHYTHM_HISTORY_MAX_LEN: usize = 8;
|
||||
|
||||
#[allow(clippy::struct_field_names)]
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Rhythm {
|
||||
inner: StrainDecaySkill,
|
||||
rhythm_history: LimitedQueue<RhythmHistoryElement, RHYTHM_HISTORY_MAX_LEN>,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
const SKILL_MULTIPLIER: f64 = 1.1;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.4;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Stamina {
|
||||
inner: StrainDecaySkill,
|
||||
}
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ use crate::{
|
||||
pub use self::{
|
||||
attributes::{TaikoDifficultyAttributes, TaikoPerformanceAttributes},
|
||||
convert::TaikoBeatmap,
|
||||
performance::TaikoPerformance,
|
||||
difficulty::gradual::TaikoGradualDifficulty,
|
||||
performance::{gradual::TaikoGradualPerformance, TaikoPerformance},
|
||||
score_state::TaikoScoreState,
|
||||
strains::TaikoStrains,
|
||||
};
|
||||
|
||||
+7
-1
@@ -22,7 +22,7 @@ impl TaikoObject {
|
||||
}
|
||||
|
||||
pub const fn is_hit(&self) -> bool {
|
||||
!matches!(self.hit_type, HitType::NonHit)
|
||||
self.hit_type.is_hit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,3 +32,9 @@ pub enum HitType {
|
||||
Rim,
|
||||
NonHit,
|
||||
}
|
||||
|
||||
impl HitType {
|
||||
pub const fn is_hit(self) -> bool {
|
||||
!matches!(self, Self::NonHit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
use crate::{
|
||||
taiko::{difficulty::gradual::TaikoGradualDifficulty, TaikoBeatmap, TaikoScoreState},
|
||||
ModeDifficulty,
|
||||
};
|
||||
|
||||
use super::TaikoPerformanceAttributes;
|
||||
|
||||
/// Gradually calculate the performance attributes of an osu!taiko map.
|
||||
///
|
||||
/// After each hit object you can call [`next`] and it will return the
|
||||
/// resulting current [`TaikoPerformanceAttributes`]. To process multiple
|
||||
/// objects at once, use [`nth`] instead.
|
||||
///
|
||||
/// Both methods require a [`TaikoScoreState`] that contains the current
|
||||
/// hitresults as well as the maximum combo so far.
|
||||
///
|
||||
/// If you only want to calculate difficulty attributes use
|
||||
/// [`TaikoGradualDifficulty`] instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::{Beatmap, ModeDifficulty};
|
||||
/// use rosu_pp::taiko::{Taiko, TaikoGradualPerformance, TaikoScoreState};
|
||||
///
|
||||
/// let map = Beatmap::from_path("./resources/1028484.osu")
|
||||
/// .unwrap()
|
||||
/// .unchecked_into_converted::<Taiko>();
|
||||
///
|
||||
/// let difficulty = ModeDifficulty::new().mods(64); // DT
|
||||
/// let mut gradual_perf = TaikoGradualPerformance::new(&difficulty, &converted);
|
||||
/// let mut state = TaikoScoreState::new(); // empty state, everything is on 0.
|
||||
///
|
||||
/// // The first 10 hitresults are 300s
|
||||
/// for _ in 0..10 {
|
||||
/// state.n300 += 1;
|
||||
/// state.max_combo += 1;
|
||||
///
|
||||
/// let performance = gradual_perf.next(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
/// }
|
||||
///
|
||||
/// // Then comes a miss.
|
||||
/// // Note that state's max combo won't be incremented for
|
||||
/// // the next few objects because the combo is reset.
|
||||
/// state.n_misses += 1;
|
||||
/// let performance = gradual_perf.next(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // The next 10 objects will be a mixture of 300s and 100s.
|
||||
/// // Notice how all 10 objects will be processed in one go.
|
||||
/// state.n300 += 3;
|
||||
/// state.n100 += 7;
|
||||
/// // The `nth` method takes a zero-based value.
|
||||
/// let performance = gradual_perf.nth(state.clone(), 9).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Now comes another 300. Note that the max combo gets incremented again.
|
||||
/// state.n300 += 1;
|
||||
/// state.max_combo += 1;
|
||||
/// let performance = gradual_perf.next(state.clone()).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Skip to the end
|
||||
/// # /*
|
||||
/// state.max_combo = ...
|
||||
/// state.n300 = ...
|
||||
/// state.n100 = ...
|
||||
/// state.n_misses = ...
|
||||
/// # */
|
||||
/// let final_performance = gradual_perf.nth(state.clone(), usize::MAX).unwrap();
|
||||
/// println!("PP: {}", performance.pp);
|
||||
///
|
||||
/// // Once the final performance was calculated,
|
||||
/// // attempting to process further objects will return `None`.
|
||||
/// assert!(gradual_perf.next(state).is_none());
|
||||
/// ```
|
||||
///
|
||||
/// [`next`]: TaikoGradualPerformance::next
|
||||
/// [`nth`]: TaikoGradualPerformance::nth
|
||||
pub struct TaikoGradualPerformance {
|
||||
difficulty: TaikoGradualDifficulty,
|
||||
}
|
||||
|
||||
impl TaikoGradualPerformance {
|
||||
/// Create a new gradual performance calculator for osu!taiko maps.
|
||||
pub fn new(difficulty: &ModeDifficulty, converted: &TaikoBeatmap<'_>) -> Self {
|
||||
let difficulty = TaikoGradualDifficulty::new(difficulty, converted);
|
||||
|
||||
Self { difficulty }
|
||||
}
|
||||
|
||||
/// Process the next hit object and calculate the performance attributes
|
||||
/// for the resulting score.
|
||||
pub fn next(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
|
||||
self.nth(state, 0)
|
||||
}
|
||||
|
||||
/// Process all remaining hit objects and calculate the final performance
|
||||
/// attributes.
|
||||
pub fn last(&mut self, state: TaikoScoreState) -> Option<TaikoPerformanceAttributes> {
|
||||
self.nth(state, usize::MAX)
|
||||
}
|
||||
|
||||
/// Process everything up the the next `n`th hit object and calculate the
|
||||
/// performance attributes for the resulting score state.
|
||||
///
|
||||
/// Note that the count is zero-indexed, so `n=0` will process 1 object,
|
||||
/// `n=1` will process 2, and so on.
|
||||
pub fn nth(&mut self, state: TaikoScoreState, n: usize) -> Option<TaikoPerformanceAttributes> {
|
||||
let performance = self
|
||||
.difficulty
|
||||
.nth(n)?
|
||||
.performance()
|
||||
.state(state)
|
||||
.mods(self.difficulty.mods)
|
||||
.clock_rate(self.difficulty.clock_rate)
|
||||
.passed_objects(self.difficulty.idx as u32)
|
||||
.calculate();
|
||||
|
||||
Some(performance)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{taiko::TaikoPerformance, Beatmap};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_and_nth() {
|
||||
let converted = Beatmap::from_path("./resources/1028484.osu")
|
||||
.unwrap()
|
||||
.unchecked_into_converted();
|
||||
|
||||
let mods = 88; // HDHRDT
|
||||
let difficulty = ModeDifficulty::new().mods(88);
|
||||
|
||||
let mut gradual = TaikoGradualPerformance::new(&difficulty, &converted);
|
||||
let mut gradual_2nd = TaikoGradualPerformance::new(&difficulty, &converted);
|
||||
let mut gradual_3rd = TaikoGradualPerformance::new(&difficulty, &converted);
|
||||
|
||||
let mut state = TaikoScoreState::default();
|
||||
|
||||
let hit_objects_len = converted.map.hit_objects.len();
|
||||
|
||||
let n_hits = converted
|
||||
.map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter(|h| h.is_circle())
|
||||
.count();
|
||||
|
||||
for i in 1.. {
|
||||
state.n_misses += 1;
|
||||
|
||||
let Some(next_gradual) = gradual.next(state.clone()) else {
|
||||
assert_eq!(i, n_hits + 1);
|
||||
assert!(gradual_2nd.last(state.clone()).is_some() || hit_objects_len % 2 == 0);
|
||||
assert!(gradual_3rd.last(state.clone()).is_some() || hit_objects_len % 3 == 0);
|
||||
break;
|
||||
};
|
||||
|
||||
if i % 2 == 0 {
|
||||
let next_gradual_2nd = gradual_2nd.nth(state.clone(), 1).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_2nd);
|
||||
}
|
||||
|
||||
if i % 3 == 0 {
|
||||
let next_gradual_3rd = gradual_3rd.nth(state.clone(), 2).unwrap();
|
||||
assert_eq!(next_gradual, next_gradual_3rd);
|
||||
}
|
||||
|
||||
let mut regular_calc = TaikoPerformance::new(converted.as_owned())
|
||||
.mods(mods)
|
||||
.passed_objects(i as u32)
|
||||
.state(state.clone());
|
||||
|
||||
let regular_state = regular_calc.generate_state();
|
||||
assert_eq!(state, regular_state);
|
||||
|
||||
let expected = regular_calc.calculate();
|
||||
|
||||
assert_eq!(next_gradual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ use super::{
|
||||
Taiko,
|
||||
};
|
||||
|
||||
pub mod gradual;
|
||||
|
||||
/// Performance calculator on osu!taiko maps.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[must_use]
|
||||
Reference in New Issue
Block a user