added TaikoGradualDifficultyAttributes
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
|
- Added methods `TaikoDifficultyAttributes::max_combo` and `OsuDifficultyAttributes::max_combo`
|
||||||
- Added `ManiaGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
- Added `ManiaGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
||||||
- Added `ManiaGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `ManiaPP` over and over.
|
- Added `ManiaGradualPerformanceAttributes`. Suitable to calculate the performance on a map after every or every few objects instead of using `ManiaPP` over and over.
|
||||||
|
- Added `TaikoGradualDifficultyAttributes`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
|
||||||
|
|
||||||
# v0.3.0
|
# v0.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -105,16 +105,13 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
|||||||
|
|
||||||
if self.idx == 2 {
|
if self.idx == 2 {
|
||||||
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
self.strain.process(&h);
|
} else {
|
||||||
|
|
||||||
return Some(ManiaDifficultyAttributes::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
while h.start_time > self.curr_section_end {
|
while h.start_time > self.curr_section_end {
|
||||||
self.strain.save_current_peak();
|
self.strain.save_current_peak();
|
||||||
self.strain.start_new_section_from(self.curr_section_end);
|
self.strain.start_new_section_from(self.curr_section_end);
|
||||||
self.curr_section_end += SECTION_LEN;
|
self.curr_section_end += SECTION_LEN;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.strain.process(&h);
|
self.strain.process(&h);
|
||||||
|
|
||||||
@@ -135,8 +132,7 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
let (mut len, _) = self.difficulty_objects.size_hint();
|
let len = self.len();
|
||||||
len += (self.idx == 0) as usize;
|
|
||||||
|
|
||||||
(len, Some(len))
|
(len, Some(len))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
iter::{self, Enumerate, Skip, Zip},
|
||||||
|
slice::Iter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
parse::{HitObject, HitObjectKind},
|
||||||
|
taiko::{
|
||||||
|
difficulty_object::DifficultyObject, norm, rescale, simple_color_penalty,
|
||||||
|
stamina_cheese::StaminaCheeseDetector, COLOR_SKILL_MULTIPLIER, RHYTHM_SKILL_MULTIPLIER,
|
||||||
|
SECTION_LEN, STAMINA_SKILL_MULTIPLIER,
|
||||||
|
},
|
||||||
|
Beatmap, Mods,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{skill::Skills, TaikoDifficultyAttributes};
|
||||||
|
|
||||||
|
/// Gradually calculate the difficulty attributes of an osu!taiko map.
|
||||||
|
///
|
||||||
|
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
|
||||||
|
/// On every call of [`Iterator::next`](std::iter::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
|
||||||
|
/// [`TaikoGradualPerformanceAttributes`](crate::taiko::TaikoGradualPerformanceAttributes) instead.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use rosu_pp::{Beatmap, taiko::TaikoGradualDifficultyAttributes};
|
||||||
|
///
|
||||||
|
/// # /*
|
||||||
|
/// let map: Beatmap = ...
|
||||||
|
/// # */
|
||||||
|
/// # let map = Beatmap::default();
|
||||||
|
///
|
||||||
|
/// let mods = 64; // DT
|
||||||
|
/// let mut iter = TaikoGradualDifficultyAttributes::new(&map, mods);
|
||||||
|
///
|
||||||
|
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
|
||||||
|
/// let attrs2 = iter.next(); // after the second hit object
|
||||||
|
///
|
||||||
|
/// // Remaining hit objects
|
||||||
|
/// for difficulty in iter {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TaikoGradualDifficultyAttributes<'map> {
|
||||||
|
pub(crate) idx: usize,
|
||||||
|
difficulty_objects: TaikoObjectIter<'map>,
|
||||||
|
cheese: Vec<bool>,
|
||||||
|
skills: Skills,
|
||||||
|
curr_section_end: f64,
|
||||||
|
strain_peak_buf: Vec<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> TaikoGradualDifficultyAttributes<'map> {
|
||||||
|
/// Create a new difficulty attributes iterator for osu!taiko maps.
|
||||||
|
pub fn new(map: &'map Beatmap, mods: impl Mods) -> Self {
|
||||||
|
// True if the object at that index is stamina cheese
|
||||||
|
let cheese = map.find_cheese();
|
||||||
|
|
||||||
|
let skills = Skills::new();
|
||||||
|
let clock_rate = mods.speed();
|
||||||
|
let difficulty_objects = TaikoObjectIter::new(&map.hit_objects, clock_rate);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
idx: 0,
|
||||||
|
difficulty_objects,
|
||||||
|
cheese,
|
||||||
|
skills,
|
||||||
|
curr_section_end: 0.0,
|
||||||
|
strain_peak_buf: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn locally_combined_difficulty(&mut self, stamina_penalty: f64) -> f64 {
|
||||||
|
let iter = self
|
||||||
|
.skills
|
||||||
|
.color
|
||||||
|
.strain_peaks
|
||||||
|
.iter()
|
||||||
|
.zip(self.skills.rhythm.strain_peaks.iter())
|
||||||
|
.zip(self.skills.stamina_right.strain_peaks.iter())
|
||||||
|
.zip(self.skills.stamina_left.strain_peaks.iter())
|
||||||
|
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
|
||||||
|
norm(
|
||||||
|
2.0,
|
||||||
|
color * COLOR_SKILL_MULTIPLIER,
|
||||||
|
rhythm * RHYTHM_SKILL_MULTIPLIER,
|
||||||
|
(stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER * stamina_penalty,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.strain_peak_buf.clear();
|
||||||
|
self.strain_peak_buf.extend(iter);
|
||||||
|
|
||||||
|
let last = norm(
|
||||||
|
2.0,
|
||||||
|
self.skills.color.curr_section_peak * COLOR_SKILL_MULTIPLIER,
|
||||||
|
self.skills.rhythm.curr_section_peak * RHYTHM_SKILL_MULTIPLIER,
|
||||||
|
(self.skills.stamina_right.curr_section_peak
|
||||||
|
+ self.skills.stamina_left.curr_section_peak)
|
||||||
|
* STAMINA_SKILL_MULTIPLIER
|
||||||
|
* stamina_penalty,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.strain_peak_buf.push(last);
|
||||||
|
|
||||||
|
self.strain_peak_buf
|
||||||
|
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
|
|
||||||
|
let mut difficulty = 0.0;
|
||||||
|
let mut weight = 1.0;
|
||||||
|
|
||||||
|
for strain in &self.strain_peak_buf {
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
difficulty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for TaikoGradualDifficultyAttributes<'_> {
|
||||||
|
type Item = TaikoDifficultyAttributes;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.idx = self.idx.saturating_add(1);
|
||||||
|
|
||||||
|
if self.idx == 1 {
|
||||||
|
if self.difficulty_objects.first_object.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.difficulty_objects.max_combo +=
|
||||||
|
self.difficulty_objects.first_object.is_circle() as usize;
|
||||||
|
|
||||||
|
let attributes = TaikoDifficultyAttributes {
|
||||||
|
stars: 0.0,
|
||||||
|
max_combo: self.difficulty_objects.max_combo,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Some(attributes);
|
||||||
|
} else if self.idx == 2 {
|
||||||
|
if self.difficulty_objects.second_object.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.difficulty_objects.max_combo +=
|
||||||
|
self.difficulty_objects.second_object.is_circle() as usize;
|
||||||
|
|
||||||
|
let attributes = TaikoDifficultyAttributes {
|
||||||
|
stars: 0.0,
|
||||||
|
max_combo: self.difficulty_objects.max_combo,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Some(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let h = self.difficulty_objects.next()?;
|
||||||
|
|
||||||
|
if self.idx == 3 {
|
||||||
|
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
|
} else {
|
||||||
|
while h.start_time > self.curr_section_end {
|
||||||
|
self.skills
|
||||||
|
.save_peak_and_start_new_section(self.curr_section_end);
|
||||||
|
self.curr_section_end += SECTION_LEN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.skills.process(&h, &self.cheese);
|
||||||
|
|
||||||
|
let len = self.skills.strain_peaks_len();
|
||||||
|
let missing = len + 1 - self.strain_peak_buf.len();
|
||||||
|
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
|
||||||
|
|
||||||
|
self.skills
|
||||||
|
.color
|
||||||
|
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
||||||
|
|
||||||
|
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||||
|
*last = self.skills.color.curr_section_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color_rating = self
|
||||||
|
.skills
|
||||||
|
.color
|
||||||
|
.difficulty_value(&mut self.strain_peak_buf)
|
||||||
|
* COLOR_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
|
self.skills
|
||||||
|
.rhythm
|
||||||
|
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
||||||
|
|
||||||
|
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||||
|
*last = self.skills.rhythm.curr_section_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rhythm_rating = self
|
||||||
|
.skills
|
||||||
|
.rhythm
|
||||||
|
.difficulty_value(&mut self.strain_peak_buf)
|
||||||
|
* RHYTHM_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
|
self.skills
|
||||||
|
.stamina_right
|
||||||
|
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
||||||
|
|
||||||
|
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||||
|
*last = self.skills.stamina_right.curr_section_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stamina_right = self
|
||||||
|
.skills
|
||||||
|
.stamina_right
|
||||||
|
.difficulty_value(&mut self.strain_peak_buf);
|
||||||
|
|
||||||
|
self.skills
|
||||||
|
.stamina_left
|
||||||
|
.copy_strain_peaks(&mut self.strain_peak_buf[..len]);
|
||||||
|
|
||||||
|
if let Some(last) = self.strain_peak_buf.last_mut() {
|
||||||
|
*last = self.skills.stamina_left.curr_section_peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stamina_left = self
|
||||||
|
.skills
|
||||||
|
.stamina_left
|
||||||
|
.difficulty_value(&mut self.strain_peak_buf);
|
||||||
|
|
||||||
|
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
|
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
||||||
|
stamina_rating *= stamina_penalty;
|
||||||
|
|
||||||
|
let combined_rating = self.locally_combined_difficulty(stamina_penalty);
|
||||||
|
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
||||||
|
|
||||||
|
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
||||||
|
|
||||||
|
let attributes = TaikoDifficultyAttributes {
|
||||||
|
stars,
|
||||||
|
max_combo: self.difficulty_objects.max_combo,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(attributes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
let len = self.len();
|
||||||
|
|
||||||
|
(len, Some(len))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for TaikoGradualDifficultyAttributes<'_> {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
let mut len = self.difficulty_objects.len();
|
||||||
|
|
||||||
|
if self.idx == 0 && !self.difficulty_objects.first_object.is_empty() {
|
||||||
|
len += 1 + !self.difficulty_objects.second_object.is_empty() as usize;
|
||||||
|
} else if self.idx == 1 && !self.difficulty_objects.second_object.is_empty() {
|
||||||
|
len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InnerIter<'map> = Zip<
|
||||||
|
Zip<Skip<Enumerate<Iter<'map, HitObject>>>, Skip<Iter<'map, HitObject>>>,
|
||||||
|
Iter<'map, HitObject>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
enum SimpleObject {
|
||||||
|
Circle,
|
||||||
|
Empty,
|
||||||
|
NonCircle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&HitObject> for SimpleObject {
|
||||||
|
fn from(h: &HitObject) -> Self {
|
||||||
|
match h.kind {
|
||||||
|
HitObjectKind::Circle => Self::Circle,
|
||||||
|
_ => Self::NonCircle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleObject {
|
||||||
|
fn is_empty(self) -> bool {
|
||||||
|
self == Self::Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_circle(self) -> bool {
|
||||||
|
self == Self::Circle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct TaikoObjectIter<'map> {
|
||||||
|
hit_objects: InnerIter<'map>,
|
||||||
|
max_combo: usize,
|
||||||
|
clock_rate: f64,
|
||||||
|
first_object: SimpleObject,
|
||||||
|
second_object: SimpleObject,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> TaikoObjectIter<'map> {
|
||||||
|
fn new(hit_objects: &'map [HitObject], clock_rate: f64) -> Self {
|
||||||
|
let first_object = hit_objects.get(0).map_or(SimpleObject::Empty, From::from);
|
||||||
|
let second_object = hit_objects.get(1).map_or(SimpleObject::Empty, From::from);
|
||||||
|
|
||||||
|
let hit_objects = hit_objects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(2)
|
||||||
|
.zip(hit_objects.iter().skip(1))
|
||||||
|
.zip(hit_objects.iter());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
hit_objects,
|
||||||
|
max_combo: 0,
|
||||||
|
clock_rate,
|
||||||
|
first_object,
|
||||||
|
second_object,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'map> Iterator for TaikoObjectIter<'map> {
|
||||||
|
type Item = DifficultyObject<'map>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let (((idx, base), prev), prev_prev) = self.hit_objects.next()?;
|
||||||
|
self.max_combo += base.is_circle() as usize;
|
||||||
|
|
||||||
|
Some(DifficultyObject::new(
|
||||||
|
idx,
|
||||||
|
base,
|
||||||
|
prev,
|
||||||
|
prev_prev,
|
||||||
|
self.clock_rate,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.hit_objects.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExactSizeIterator for TaikoObjectIter<'_> {
|
||||||
|
#[inline]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.hit_objects.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_map() {
|
||||||
|
let map = Beatmap::default();
|
||||||
|
let mut attributes = TaikoGradualDifficultyAttributes::new(&map, 0);
|
||||||
|
assert!(attributes.next().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
|
||||||
|
#[test]
|
||||||
|
fn iter_end_eq_regular() {
|
||||||
|
let map = Beatmap::from_path("./maps/222766.osu").expect("failed to parse map");
|
||||||
|
let mods = 64;
|
||||||
|
let regular = crate::taiko::stars(&map, mods, None);
|
||||||
|
|
||||||
|
let iter_end = TaikoGradualDifficultyAttributes::new(&map, mods)
|
||||||
|
.last()
|
||||||
|
.expect("empty iter");
|
||||||
|
|
||||||
|
assert_eq!(regular, iter_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ use std::iter::{Cycle, Skip, Take};
|
|||||||
use std::ops::Index;
|
use std::ops::Index;
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct LimitedQueue<T> {
|
pub(crate) struct LimitedQueue<T> {
|
||||||
queue: Vec<T>,
|
queue: Vec<T>,
|
||||||
start: usize,
|
start: usize,
|
||||||
|
|||||||
+45
-59
@@ -1,6 +1,7 @@
|
|||||||
#![cfg(feature = "taiko")]
|
#![cfg(feature = "taiko")]
|
||||||
|
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
|
mod gradual_difficulty;
|
||||||
mod hitobject_rhythm;
|
mod hitobject_rhythm;
|
||||||
mod limited_queue;
|
mod limited_queue;
|
||||||
mod pp;
|
mod pp;
|
||||||
@@ -10,14 +11,15 @@ mod skill_kind;
|
|||||||
mod stamina_cheese;
|
mod stamina_cheese;
|
||||||
|
|
||||||
use difficulty_object::DifficultyObject;
|
use difficulty_object::DifficultyObject;
|
||||||
|
pub use gradual_difficulty::*;
|
||||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
||||||
use limited_queue::LimitedQueue;
|
use limited_queue::LimitedQueue;
|
||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use rim::Rim;
|
use rim::Rim;
|
||||||
use skill::Skill;
|
|
||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use stamina_cheese::StaminaCheeseDetector;
|
use stamina_cheese::StaminaCheeseDetector;
|
||||||
|
|
||||||
|
use crate::taiko::skill::Skills;
|
||||||
use crate::{Beatmap, Mods, Strains};
|
use crate::{Beatmap, Mods, Strains};
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
@@ -38,19 +40,26 @@ pub fn stars(
|
|||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
) -> TaikoDifficultyAttributes {
|
) -> TaikoDifficultyAttributes {
|
||||||
let (skills, max_combo) = calculate_skills(map, mods, passed_objects);
|
let (skills, max_combo) = calculate_skills(map, mods, passed_objects);
|
||||||
let mut buf = vec![0.0; skills[0].strain_peaks.len()];
|
let mut buf = vec![0.0; skills.strain_peaks_len()];
|
||||||
|
|
||||||
let color_rating = skills[0].difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
skills.color.copy_strain_peaks(&mut buf);
|
||||||
let rhythm_rating = skills[1].difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
let mut stamina_rating = (skills[2].difficulty_value(&mut buf)
|
skills.rhythm.copy_strain_peaks(&mut buf);
|
||||||
+ skills[3].difficulty_value(&mut buf))
|
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
||||||
* STAMINA_SKILL_MULTIPLIER;
|
|
||||||
|
skills.stamina_right.copy_strain_peaks(&mut buf);
|
||||||
|
let stamina_right = skills.stamina_right.difficulty_value(&mut buf);
|
||||||
|
|
||||||
|
skills.stamina_left.copy_strain_peaks(&mut buf);
|
||||||
|
let stamina_left = skills.stamina_left.difficulty_value(&mut buf);
|
||||||
|
|
||||||
|
let mut stamina_rating = (stamina_right + stamina_left) * STAMINA_SKILL_MULTIPLIER;
|
||||||
|
|
||||||
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
||||||
stamina_rating *= stamina_penalty;
|
stamina_rating *= stamina_penalty;
|
||||||
|
|
||||||
let combined_rating = locally_combined_difficulty(&skills, stamina_penalty);
|
let combined_rating = locally_combined_difficulty(&mut buf, &skills, stamina_penalty);
|
||||||
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
||||||
|
|
||||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
||||||
@@ -65,12 +74,13 @@ pub fn stars(
|
|||||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
let (skills, _) = calculate_skills(map, mods, None);
|
let (skills, _) = calculate_skills(map, mods, None);
|
||||||
|
|
||||||
let strains = skills[0]
|
let strains = skills
|
||||||
|
.color
|
||||||
.strain_peaks
|
.strain_peaks
|
||||||
.iter()
|
.iter()
|
||||||
.zip(skills[1].strain_peaks.iter())
|
.zip(skills.rhythm.strain_peaks.iter())
|
||||||
.zip(skills[2].strain_peaks.iter())
|
.zip(skills.stamina_right.strain_peaks.iter())
|
||||||
.zip(skills[3].strain_peaks.iter())
|
.zip(skills.stamina_left.strain_peaks.iter())
|
||||||
.map(|(((color, rhythm), stamina_right), stamina_left)| {
|
.map(|(((color, rhythm), stamina_right), stamina_left)| {
|
||||||
color + rhythm + stamina_right + stamina_left
|
color + rhythm + stamina_right + stamina_left
|
||||||
})
|
})
|
||||||
@@ -86,32 +96,19 @@ fn calculate_skills(
|
|||||||
map: &Beatmap,
|
map: &Beatmap,
|
||||||
mods: impl Mods,
|
mods: impl Mods,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
) -> (Vec<Skill>, usize) {
|
) -> (Skills, usize) {
|
||||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||||
|
|
||||||
// True if the object at that index is stamina cheese
|
// True if the object at that index is stamina cheese
|
||||||
let cheese = map.find_cheese();
|
let cheese = map.find_cheese();
|
||||||
|
let mut skills = Skills::new();
|
||||||
let mut skills = vec![
|
|
||||||
Skill::new(SkillKind::color()),
|
|
||||||
Skill::new(SkillKind::rhythm()),
|
|
||||||
Skill::new(SkillKind::stamina(true)),
|
|
||||||
Skill::new(SkillKind::stamina(false)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let clock_rate = mods.speed();
|
let clock_rate = mods.speed();
|
||||||
let section_len = SECTION_LEN * clock_rate;
|
|
||||||
let mut max_combo = 0;
|
let mut max_combo = 0;
|
||||||
|
|
||||||
// No strain for first object
|
match map.hit_objects.get(0) {
|
||||||
let mut curr_section_end = match map.hit_objects.first() {
|
Some(h) => max_combo += h.is_circle() as usize,
|
||||||
Some(h) => {
|
|
||||||
max_combo += h.is_circle() as usize;
|
|
||||||
|
|
||||||
(h.start_time / section_len).ceil() * section_len
|
|
||||||
}
|
|
||||||
None => return (skills, max_combo),
|
None => return (skills, max_combo),
|
||||||
};
|
}
|
||||||
|
|
||||||
match map.hit_objects.get(1) {
|
match map.hit_objects.get(1) {
|
||||||
Some(h) => max_combo += h.is_circle() as usize,
|
Some(h) => max_combo += h.is_circle() as usize,
|
||||||
@@ -131,39 +128,27 @@ fn calculate_skills(
|
|||||||
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle second object separately to remove later if-branching
|
// Handle first element distinctly
|
||||||
let h = match hit_objects.next() {
|
let h = match hit_objects.next() {
|
||||||
Some(h) => h,
|
Some(h) => h,
|
||||||
None => return (skills, max_combo),
|
None => return (skills, max_combo),
|
||||||
};
|
};
|
||||||
|
|
||||||
while h.base.start_time > curr_section_end {
|
// No strain for first object
|
||||||
curr_section_end += section_len;
|
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
}
|
skills.process(&h, &cheese);
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
|
||||||
skill.process(&h, &cheese);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle all other objects
|
// Handle all other objects
|
||||||
for h in hit_objects {
|
for h in hit_objects {
|
||||||
while h.base.start_time > curr_section_end {
|
while h.start_time > curr_section_end {
|
||||||
for skill in skills.iter_mut() {
|
skills.save_peak_and_start_new_section(curr_section_end);
|
||||||
skill.save_current_peak();
|
curr_section_end += SECTION_LEN;
|
||||||
skill.start_new_section_from(curr_section_end / clock_rate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
curr_section_end += section_len;
|
skills.process(&h, &cheese);
|
||||||
}
|
}
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
skills.save_current_peak();
|
||||||
skill.process(&h, &cheese);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for skill in skills.iter_mut() {
|
|
||||||
skill.save_current_peak();
|
|
||||||
}
|
|
||||||
|
|
||||||
(skills, max_combo)
|
(skills, max_combo)
|
||||||
}
|
}
|
||||||
@@ -186,15 +171,16 @@ fn simple_color_penalty(stamina: f64, color: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f64) -> f64 {
|
fn locally_combined_difficulty(peaks: &mut Vec<f64>, skills: &Skills, stamina_penalty: f64) -> f64 {
|
||||||
let mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
|
peaks.clear();
|
||||||
|
|
||||||
let iter = skills[0]
|
let iter = skills
|
||||||
|
.color
|
||||||
.strain_peaks
|
.strain_peaks
|
||||||
.iter()
|
.iter()
|
||||||
.zip(skills[1].strain_peaks.iter())
|
.zip(skills.rhythm.strain_peaks.iter())
|
||||||
.zip(skills[2].strain_peaks.iter())
|
.zip(skills.stamina_right.strain_peaks.iter())
|
||||||
.zip(skills[3].strain_peaks.iter())
|
.zip(skills.stamina_left.strain_peaks.iter())
|
||||||
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
|
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
|
||||||
norm(
|
norm(
|
||||||
2.0,
|
2.0,
|
||||||
@@ -211,7 +197,7 @@ fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f64) -> f64 {
|
|||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
for strain in peaks {
|
for strain in peaks {
|
||||||
difficulty += strain * weight;
|
difficulty += *strain * weight;
|
||||||
weight *= 0.9;
|
weight *= 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +210,7 @@ fn norm(p: f64, a: f64, b: f64, c: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a difficulty calculation on an osu!taiko map.
|
/// The result of a difficulty calculation on an osu!taiko map.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
pub struct TaikoDifficultyAttributes {
|
pub struct TaikoDifficultyAttributes {
|
||||||
/// The final star rating.
|
/// The final star rating.
|
||||||
pub stars: f64,
|
pub stars: f64,
|
||||||
|
|||||||
+66
-14
@@ -13,9 +13,58 @@ const RHYTHM_STRAIN_DECAY_BASE: f64 = 0.0;
|
|||||||
const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
|
const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
|
||||||
const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
|
const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Skills {
|
||||||
|
pub(crate) color: Skill,
|
||||||
|
pub(crate) rhythm: Skill,
|
||||||
|
pub(crate) stamina_right: Skill,
|
||||||
|
pub(crate) stamina_left: Skill,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Skills {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
color: Skill::new(SkillKind::color()),
|
||||||
|
rhythm: Skill::new(SkillKind::rhythm()),
|
||||||
|
stamina_right: Skill::new(SkillKind::stamina(true)),
|
||||||
|
stamina_left: Skill::new(SkillKind::stamina(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_peak_and_start_new_section(&mut self, time: f64) {
|
||||||
|
self.color.save_current_peak();
|
||||||
|
self.color.start_new_section_from(time);
|
||||||
|
self.rhythm.save_current_peak();
|
||||||
|
self.rhythm.start_new_section_from(time);
|
||||||
|
self.stamina_right.save_current_peak();
|
||||||
|
self.stamina_right.start_new_section_from(time);
|
||||||
|
self.stamina_left.save_current_peak();
|
||||||
|
self.stamina_left.start_new_section_from(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
|
self.color.save_current_peak();
|
||||||
|
self.rhythm.save_current_peak();
|
||||||
|
self.stamina_right.save_current_peak();
|
||||||
|
self.stamina_left.save_current_peak();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
|
||||||
|
self.color.process(curr, cheese);
|
||||||
|
self.rhythm.process(curr, cheese);
|
||||||
|
self.stamina_right.process(curr, cheese);
|
||||||
|
self.stamina_left.process(curr, cheese);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn strain_peaks_len(&self) -> usize {
|
||||||
|
self.color.strain_peaks.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Skill {
|
pub(crate) struct Skill {
|
||||||
pub current_strain: f64,
|
pub(crate) current_strain: f64,
|
||||||
current_section_peak: f64,
|
pub(crate) curr_section_peak: f64,
|
||||||
|
|
||||||
kind: SkillKind,
|
kind: SkillKind,
|
||||||
pub(crate) strain_peaks: Vec<f64>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
@@ -28,7 +77,7 @@ impl Skill {
|
|||||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
pub(crate) fn new(kind: SkillKind) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current_strain: 1.0,
|
current_strain: 1.0,
|
||||||
current_section_peak: 1.0,
|
curr_section_peak: 1.0,
|
||||||
|
|
||||||
kind,
|
kind,
|
||||||
strain_peaks: Vec::with_capacity(128),
|
strain_peaks: Vec::with_capacity(128),
|
||||||
@@ -39,31 +88,34 @@ impl Skill {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
self.strain_peaks.push(self.current_section_peak);
|
self.strain_peaks.push(self.curr_section_peak);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
self.curr_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn process(&mut self, current: &DifficultyObject<'_>, cheese: &[bool]) {
|
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
|
||||||
self.current_strain *= self.strain_decay(current.delta);
|
self.current_strain *= self.strain_decay(curr.delta);
|
||||||
self.current_strain += self.kind.strain_value_of(current, cheese) * self.skill_multiplier();
|
self.current_strain += self.kind.strain_value_of(curr, cheese) * self.skill_multiplier();
|
||||||
self.current_section_peak = self.current_section_peak.max(self.current_strain);
|
self.curr_section_peak = self.curr_section_peak.max(self.current_strain);
|
||||||
self.prev_time.replace(current.start_time);
|
self.prev_time.replace(curr.start_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn copy_strain_peaks(&self, buf: &mut [f64]) {
|
||||||
|
buf.copy_from_slice(&self.strain_peaks);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn difficulty_value(&self, buf: &mut [f64]) -> f64 {
|
pub(crate) fn difficulty_value(&self, peaks: &mut [f64]) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
buf.copy_from_slice(&self.strain_peaks);
|
peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
buf.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
|
||||||
|
|
||||||
for &strain in buf.iter() {
|
for &strain in peaks.iter() {
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= DECAY_WEIGHT;
|
weight *= DECAY_WEIGHT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const MONO_HISTORY_MAX_LEN: usize = 5;
|
|||||||
const RHYTHM_HISTORY_MAX_LEN: usize = 8;
|
const RHYTHM_HISTORY_MAX_LEN: usize = 8;
|
||||||
const STAMINA_HISTORY_MAX_LEN: usize = 2;
|
const STAMINA_HISTORY_MAX_LEN: usize = 2;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) enum SkillKind {
|
pub(crate) enum SkillKind {
|
||||||
Color {
|
Color {
|
||||||
mono_history: LimitedQueue<usize>,
|
mono_history: LimitedQueue<usize>,
|
||||||
@@ -16,7 +17,7 @@ pub(crate) enum SkillKind {
|
|||||||
current_mono_len: usize,
|
current_mono_len: usize,
|
||||||
},
|
},
|
||||||
Rhythm {
|
Rhythm {
|
||||||
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
|
rhythm_history: LimitedQueue<(usize, &'static HitObjectRhythm)>, // (idx, rhythm)
|
||||||
notes_since_rhythm_change: usize,
|
notes_since_rhythm_change: usize,
|
||||||
current_strain: f64,
|
current_strain: f64,
|
||||||
},
|
},
|
||||||
@@ -163,7 +164,7 @@ impl SkillKind {
|
|||||||
|
|
||||||
let mut strain = current.rhythm.difficulty;
|
let mut strain = current.rhythm.difficulty;
|
||||||
|
|
||||||
rhythm_history.push((current.idx, *current.rhythm));
|
rhythm_history.push((current.idx, current.rhythm));
|
||||||
|
|
||||||
let mut reps_penalty = 1.0;
|
let mut reps_penalty = 1.0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user