added TaikoGradualDifficultyAttributes
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
- 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 `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
|
||||
|
||||
|
||||
@@ -105,15 +105,12 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
|
||||
if self.idx == 2 {
|
||||
self.curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
self.strain.process(&h);
|
||||
|
||||
return Some(ManiaDifficultyAttributes::default());
|
||||
}
|
||||
|
||||
while h.start_time > self.curr_section_end {
|
||||
self.strain.save_current_peak();
|
||||
self.strain.start_new_section_from(self.curr_section_end);
|
||||
self.curr_section_end += SECTION_LEN;
|
||||
} else {
|
||||
while h.start_time > self.curr_section_end {
|
||||
self.strain.save_current_peak();
|
||||
self.strain.start_new_section_from(self.curr_section_end);
|
||||
self.curr_section_end += SECTION_LEN;
|
||||
}
|
||||
}
|
||||
|
||||
self.strain.process(&h);
|
||||
@@ -135,8 +132,7 @@ impl Iterator for ManiaGradualDifficultyAttributes<'_> {
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let (mut len, _) = self.difficulty_objects.size_hint();
|
||||
len += (self.idx == 0) as usize;
|
||||
let len = self.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::slice::Iter;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct LimitedQueue<T> {
|
||||
queue: Vec<T>,
|
||||
start: usize,
|
||||
|
||||
+45
-59
@@ -1,6 +1,7 @@
|
||||
#![cfg(feature = "taiko")]
|
||||
|
||||
mod difficulty_object;
|
||||
mod gradual_difficulty;
|
||||
mod hitobject_rhythm;
|
||||
mod limited_queue;
|
||||
mod pp;
|
||||
@@ -10,14 +11,15 @@ mod skill_kind;
|
||||
mod stamina_cheese;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
pub use gradual_difficulty::*;
|
||||
use hitobject_rhythm::{closest_rhythm, HitObjectRhythm};
|
||||
use limited_queue::LimitedQueue;
|
||||
pub use pp::*;
|
||||
use rim::Rim;
|
||||
use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use stamina_cheese::StaminaCheeseDetector;
|
||||
|
||||
use crate::taiko::skill::Skills;
|
||||
use crate::{Beatmap, Mods, Strains};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
@@ -38,19 +40,26 @@ pub fn stars(
|
||||
passed_objects: Option<usize>,
|
||||
) -> TaikoDifficultyAttributes {
|
||||
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;
|
||||
let rhythm_rating = skills[1].difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
||||
skills.color.copy_strain_peaks(&mut buf);
|
||||
let color_rating = skills.color.difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
||||
|
||||
let mut stamina_rating = (skills[2].difficulty_value(&mut buf)
|
||||
+ skills[3].difficulty_value(&mut buf))
|
||||
* STAMINA_SKILL_MULTIPLIER;
|
||||
skills.rhythm.copy_strain_peaks(&mut buf);
|
||||
let rhythm_rating = skills.rhythm.difficulty_value(&mut buf) * RHYTHM_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);
|
||||
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 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 {
|
||||
let (skills, _) = calculate_skills(map, mods, None);
|
||||
|
||||
let strains = skills[0]
|
||||
let strains = skills
|
||||
.color
|
||||
.strain_peaks
|
||||
.iter()
|
||||
.zip(skills[1].strain_peaks.iter())
|
||||
.zip(skills[2].strain_peaks.iter())
|
||||
.zip(skills[3].strain_peaks.iter())
|
||||
.zip(skills.rhythm.strain_peaks.iter())
|
||||
.zip(skills.stamina_right.strain_peaks.iter())
|
||||
.zip(skills.stamina_left.strain_peaks.iter())
|
||||
.map(|(((color, rhythm), stamina_right), stamina_left)| {
|
||||
color + rhythm + stamina_right + stamina_left
|
||||
})
|
||||
@@ -86,32 +96,19 @@ fn calculate_skills(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> (Vec<Skill>, usize) {
|
||||
) -> (Skills, usize) {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
// True if the object at that index is stamina cheese
|
||||
let cheese = map.find_cheese();
|
||||
|
||||
let mut skills = vec![
|
||||
Skill::new(SkillKind::color()),
|
||||
Skill::new(SkillKind::rhythm()),
|
||||
Skill::new(SkillKind::stamina(true)),
|
||||
Skill::new(SkillKind::stamina(false)),
|
||||
];
|
||||
|
||||
let mut skills = Skills::new();
|
||||
let clock_rate = mods.speed();
|
||||
let section_len = SECTION_LEN * clock_rate;
|
||||
let mut max_combo = 0;
|
||||
|
||||
// No strain for first object
|
||||
let mut curr_section_end = match map.hit_objects.first() {
|
||||
Some(h) => {
|
||||
max_combo += h.is_circle() as usize;
|
||||
|
||||
(h.start_time / section_len).ceil() * section_len
|
||||
}
|
||||
match map.hit_objects.get(0) {
|
||||
Some(h) => max_combo += h.is_circle() as usize,
|
||||
None => return (skills, max_combo),
|
||||
};
|
||||
}
|
||||
|
||||
match map.hit_objects.get(1) {
|
||||
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)
|
||||
});
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
// Handle first element distinctly
|
||||
let h = match hit_objects.next() {
|
||||
Some(h) => h,
|
||||
None => return (skills, max_combo),
|
||||
};
|
||||
|
||||
while h.base.start_time > curr_section_end {
|
||||
curr_section_end += section_len;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h, &cheese);
|
||||
}
|
||||
// No strain for first object
|
||||
let mut curr_section_end = (h.start_time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
skills.process(&h, &cheese);
|
||||
|
||||
// Handle all other objects
|
||||
for h in hit_objects {
|
||||
while h.base.start_time > curr_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.start_new_section_from(curr_section_end / clock_rate);
|
||||
}
|
||||
|
||||
curr_section_end += section_len;
|
||||
while h.start_time > curr_section_end {
|
||||
skills.save_peak_and_start_new_section(curr_section_end);
|
||||
curr_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h, &cheese);
|
||||
}
|
||||
skills.process(&h, &cheese);
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
skills.save_current_peak();
|
||||
|
||||
(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 {
|
||||
let mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
|
||||
fn locally_combined_difficulty(peaks: &mut Vec<f64>, skills: &Skills, stamina_penalty: f64) -> f64 {
|
||||
peaks.clear();
|
||||
|
||||
let iter = skills[0]
|
||||
let iter = skills
|
||||
.color
|
||||
.strain_peaks
|
||||
.iter()
|
||||
.zip(skills[1].strain_peaks.iter())
|
||||
.zip(skills[2].strain_peaks.iter())
|
||||
.zip(skills[3].strain_peaks.iter())
|
||||
.zip(skills.rhythm.strain_peaks.iter())
|
||||
.zip(skills.stamina_right.strain_peaks.iter())
|
||||
.zip(skills.stamina_left.strain_peaks.iter())
|
||||
.map(|(((&color, &rhythm), &stamina_right), &stamina_left)| {
|
||||
norm(
|
||||
2.0,
|
||||
@@ -211,7 +197,7 @@ fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f64) -> f64 {
|
||||
let mut weight = 1.0;
|
||||
|
||||
for strain in peaks {
|
||||
difficulty += strain * weight;
|
||||
difficulty += *strain * weight;
|
||||
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.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub struct TaikoDifficultyAttributes {
|
||||
/// The final star rating.
|
||||
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_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 current_strain: f64,
|
||||
current_section_peak: f64,
|
||||
pub(crate) current_strain: f64,
|
||||
pub(crate) curr_section_peak: f64,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f64>,
|
||||
@@ -28,7 +77,7 @@ impl Skill {
|
||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
||||
Self {
|
||||
current_strain: 1.0,
|
||||
current_section_peak: 1.0,
|
||||
curr_section_peak: 1.0,
|
||||
|
||||
kind,
|
||||
strain_peaks: Vec::with_capacity(128),
|
||||
@@ -39,31 +88,34 @@ impl Skill {
|
||||
|
||||
#[inline]
|
||||
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]
|
||||
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]
|
||||
pub(crate) fn process(&mut self, current: &DifficultyObject<'_>, cheese: &[bool]) {
|
||||
self.current_strain *= self.strain_decay(current.delta);
|
||||
self.current_strain += self.kind.strain_value_of(current, cheese) * self.skill_multiplier();
|
||||
self.current_section_peak = self.current_section_peak.max(self.current_strain);
|
||||
self.prev_time.replace(current.start_time);
|
||||
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>, cheese: &[bool]) {
|
||||
self.current_strain *= self.strain_decay(curr.delta);
|
||||
self.current_strain += self.kind.strain_value_of(curr, cheese) * self.skill_multiplier();
|
||||
self.curr_section_peak = self.curr_section_peak.max(self.current_strain);
|
||||
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]
|
||||
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 weight = 1.0;
|
||||
|
||||
buf.copy_from_slice(&self.strain_peaks);
|
||||
buf.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
peaks.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;
|
||||
weight *= DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const MONO_HISTORY_MAX_LEN: usize = 5;
|
||||
const RHYTHM_HISTORY_MAX_LEN: usize = 8;
|
||||
const STAMINA_HISTORY_MAX_LEN: usize = 2;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum SkillKind {
|
||||
Color {
|
||||
mono_history: LimitedQueue<usize>,
|
||||
@@ -16,7 +17,7 @@ pub(crate) enum SkillKind {
|
||||
current_mono_len: usize,
|
||||
},
|
||||
Rhythm {
|
||||
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
|
||||
rhythm_history: LimitedQueue<(usize, &'static HitObjectRhythm)>, // (idx, rhythm)
|
||||
notes_since_rhythm_change: usize,
|
||||
current_strain: f64,
|
||||
},
|
||||
@@ -163,7 +164,7 @@ impl SkillKind {
|
||||
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user