added TaikoGradualDifficultyAttributes

This commit is contained in:
MaxOhn
2021-11-24 02:21:48 +01:00
parent e0521ba03e
commit 9d02a5cc48
7 changed files with 515 additions and 86 deletions
+1
View File
@@ -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
+7 -11
View File
@@ -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))
}
+392
View File
@@ -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);
}
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+3 -2
View File
@@ -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;