added functions to calculate only the strains
This commit is contained in:
+231
-1
@@ -11,7 +11,7 @@ use movement::Movement;
|
|||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2, StarResult};
|
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2, StarResult, Strains};
|
||||||
|
|
||||||
use std::convert::identity;
|
use std::convert::identity;
|
||||||
|
|
||||||
@@ -278,6 +278,236 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
StarResult::Fruits { attributes }
|
StarResult::Fruits { attributes }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
/// evaluating the final strains, it just returns them as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
|
if map.hit_objects.len() < 2 {
|
||||||
|
return Strains::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributes = map.attributes().mods(mods);
|
||||||
|
let with_hr = mods.hr();
|
||||||
|
let mut ticks = Vec::new(); // using the same buffer for all sliders
|
||||||
|
let mut slider_state = SliderState::new(map);
|
||||||
|
|
||||||
|
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
||||||
|
let mut hit_objects = map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
|
||||||
|
HitObjectKind::Circle => {
|
||||||
|
let mut h = CatchObject::new((h.pos, h.start_time));
|
||||||
|
|
||||||
|
if with_hr {
|
||||||
|
h = h.with_hr(last_pos, last_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Some(FruitOrJuice::Fruit(Some(h))))
|
||||||
|
}
|
||||||
|
HitObjectKind::Slider {
|
||||||
|
pixel_len,
|
||||||
|
repeats,
|
||||||
|
curve_points,
|
||||||
|
path_type,
|
||||||
|
} => {
|
||||||
|
// HR business
|
||||||
|
last_pos
|
||||||
|
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
|
||||||
|
*last_time = h.start_time;
|
||||||
|
|
||||||
|
// Responsible for timing point values
|
||||||
|
slider_state.update(h.start_time);
|
||||||
|
|
||||||
|
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
||||||
|
|
||||||
|
if map.version >= 8 {
|
||||||
|
tick_distance /=
|
||||||
|
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
||||||
|
/ (map.sv * slider_state.speed_mult)
|
||||||
|
/ 100.0;
|
||||||
|
|
||||||
|
// Ensure path type validity
|
||||||
|
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
|
||||||
|
PathType::Bezier
|
||||||
|
} else if curve_points.len() == 2 {
|
||||||
|
PathType::Linear
|
||||||
|
} else {
|
||||||
|
*path_type
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the curve w.r.t. the curve points
|
||||||
|
let curve = match path_type {
|
||||||
|
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
|
||||||
|
PathType::Bezier => Curve::bezier(curve_points),
|
||||||
|
PathType::Catmull => Curve::catmull(curve_points),
|
||||||
|
PathType::PerfectCurve => Curve::perfect(curve_points),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut current_distance = tick_distance;
|
||||||
|
let time_add = duration * (tick_distance / (*pixel_len * *repeats as f32));
|
||||||
|
|
||||||
|
let target = *pixel_len - tick_distance / 8.0;
|
||||||
|
ticks.reserve((target / tick_distance) as usize);
|
||||||
|
|
||||||
|
// Tick of the first span
|
||||||
|
if current_distance < target {
|
||||||
|
for tick_idx in 1.. {
|
||||||
|
let pos = curve.point_at_distance(current_distance);
|
||||||
|
let time = h.start_time + time_add * tick_idx as f32;
|
||||||
|
ticks.push((pos, time));
|
||||||
|
current_distance += tick_distance;
|
||||||
|
|
||||||
|
if current_distance >= target {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
|
||||||
|
slider_objects.push((h.pos, h.start_time));
|
||||||
|
|
||||||
|
// Other spans
|
||||||
|
if *repeats <= 1 {
|
||||||
|
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
|
||||||
|
} else {
|
||||||
|
slider_objects.append(&mut ticks.clone());
|
||||||
|
|
||||||
|
for repeat_id in 1..*repeats {
|
||||||
|
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
||||||
|
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||||
|
let pos = curve.point_at_distance(dist);
|
||||||
|
|
||||||
|
// Reverse tick
|
||||||
|
slider_objects.push((pos, h.start_time + time_offset));
|
||||||
|
|
||||||
|
// Actual ticks
|
||||||
|
if repeat_id & 1 == 1 {
|
||||||
|
slider_objects.extend(ticks.iter().copied().rev());
|
||||||
|
} else {
|
||||||
|
slider_objects.extend(ticks.iter().copied());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ticks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slider tail
|
||||||
|
let dist_end = (*repeats % 2) as f32 * *pixel_len;
|
||||||
|
let pos = curve.point_at_distance(dist_end);
|
||||||
|
slider_objects.push((pos, h.start_time + duration));
|
||||||
|
|
||||||
|
let iter = slider_objects.into_iter().map(CatchObject::new);
|
||||||
|
|
||||||
|
Some(Some(FruitOrJuice::Juice(iter)))
|
||||||
|
}
|
||||||
|
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => Some(None),
|
||||||
|
})
|
||||||
|
.filter_map(identity)
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
// Hyper dash business
|
||||||
|
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
|
||||||
|
let mut last_direction = 0;
|
||||||
|
let mut last_excess = half_catcher_width;
|
||||||
|
|
||||||
|
// Strain business
|
||||||
|
let mut movement = Movement::new(attributes.cs);
|
||||||
|
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||||
|
let mut current_section_end =
|
||||||
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
let mut prev = hit_objects.next().unwrap();
|
||||||
|
let mut curr = hit_objects.next().unwrap();
|
||||||
|
|
||||||
|
prev.init_hyper_dash(
|
||||||
|
half_catcher_width,
|
||||||
|
&curr,
|
||||||
|
&mut last_direction,
|
||||||
|
&mut last_excess,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let next = hit_objects.next().unwrap();
|
||||||
|
curr.init_hyper_dash(
|
||||||
|
half_catcher_width,
|
||||||
|
&next,
|
||||||
|
&mut last_direction,
|
||||||
|
&mut last_excess,
|
||||||
|
);
|
||||||
|
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
movement.half_catcher_width,
|
||||||
|
attributes.clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement.process(&h);
|
||||||
|
|
||||||
|
prev = curr;
|
||||||
|
curr = next;
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
|
for next in hit_objects {
|
||||||
|
curr.init_hyper_dash(
|
||||||
|
half_catcher_width,
|
||||||
|
&next,
|
||||||
|
&mut last_direction,
|
||||||
|
&mut last_excess,
|
||||||
|
);
|
||||||
|
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
movement.half_catcher_width,
|
||||||
|
attributes.clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
movement.save_current_peak();
|
||||||
|
movement.start_new_section_from(current_section_end);
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement.process(&h);
|
||||||
|
|
||||||
|
prev = curr;
|
||||||
|
curr = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as in loop but without init_hyper_dash because `curr` is the last element
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
movement.half_catcher_width,
|
||||||
|
attributes.clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
movement.save_current_peak();
|
||||||
|
movement.start_new_section_from(current_section_end);
|
||||||
|
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement.process(&h);
|
||||||
|
movement.save_current_peak();
|
||||||
|
|
||||||
|
Strains {
|
||||||
|
section_length: section_len,
|
||||||
|
strains: movement.strain_peaks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn tiny_droplet_count(
|
fn tiny_droplet_count(
|
||||||
start_time: f32,
|
start_time: f32,
|
||||||
time_between_ticks: f32,
|
time_between_ticks: f32,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub(crate) struct Movement {
|
|||||||
current_strain: f32,
|
current_strain: f32,
|
||||||
current_section_peak: f32,
|
current_section_peak: f32,
|
||||||
|
|
||||||
strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f32>,
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-1
@@ -114,7 +114,7 @@ pub use parse::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub trait BeatmapExt {
|
pub trait BeatmapExt {
|
||||||
/// Calculate the stars of a beatmap.
|
/// Calculate the stars and other attributes of a beatmap which are required for pp calculation.
|
||||||
///
|
///
|
||||||
/// For osu!standard maps, the `no_leniency` version will be used.
|
/// For osu!standard maps, the `no_leniency` version will be used.
|
||||||
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult;
|
fn stars(&self, mods: impl Mods, passed_objects: Option<usize>) -> StarResult;
|
||||||
@@ -126,6 +126,15 @@ pub trait BeatmapExt {
|
|||||||
/// If you seek more fine-tuning and options you need to match on the map's
|
/// If you seek more fine-tuning and options you need to match on the map's
|
||||||
/// mode and use the mode's corresponding calculator, e.g. [`TaikoPP`](crate::TaikoPP) for taiko.
|
/// mode and use the mode's corresponding calculator, e.g. [`TaikoPP`](crate::TaikoPP) for taiko.
|
||||||
fn max_pp(&self, mods: u32) -> PpResult;
|
fn max_pp(&self, mods: u32) -> PpResult;
|
||||||
|
|
||||||
|
/// Calculate the strains of a map.
|
||||||
|
/// This essentially performs the same calculation as a `stars` function but
|
||||||
|
/// instead of evaluating the final strains, they are just returned as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
///
|
||||||
|
/// For osu!standard maps, the `no_leniency` version will be used.
|
||||||
|
fn strains(&self, mods: impl Mods) -> Strains;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeatmapExt for Beatmap {
|
impl BeatmapExt for Beatmap {
|
||||||
@@ -147,9 +156,30 @@ impl BeatmapExt for Beatmap {
|
|||||||
GameMode::CTB => FruitsPP::new(self).mods(mods).calculate(),
|
GameMode::CTB => FruitsPP::new(self).mods(mods).calculate(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn strains(&self, mods: impl Mods) -> Strains {
|
||||||
|
match self.mode {
|
||||||
|
GameMode::STD => osu::no_leniency::strains(self, mods),
|
||||||
|
GameMode::MNA => mania::strains(self, mods),
|
||||||
|
GameMode::TKO => taiko::strains(self, mods),
|
||||||
|
GameMode::CTB => fruits::strains(self, mods),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of calculating the strains on a map.
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
///
|
||||||
|
/// `strains` will be the summed strains for each skill of the map's mode.
|
||||||
|
///
|
||||||
|
/// `section_length` is the time in ms inbetween two strains.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct Strains {
|
||||||
|
pub section_length: f32,
|
||||||
|
pub strains: Vec<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic enum containing the result of a star calculation based on the mode.
|
/// Basic enum containing the result of a star calculation based on the mode.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum StarResult {
|
pub enum StarResult {
|
||||||
Fruits {
|
Fruits {
|
||||||
attributes: fruits::DifficultyAttributes,
|
attributes: fruits::DifficultyAttributes,
|
||||||
@@ -179,6 +209,7 @@ impl StarResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Basic struct containing the result of a PP calculation.
|
/// Basic struct containing the result of a PP calculation.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct PpResult {
|
pub struct PpResult {
|
||||||
pub pp: f32,
|
pub pp: f32,
|
||||||
pub attributes: StarResult,
|
pub attributes: StarResult,
|
||||||
|
|||||||
+54
-1
@@ -4,7 +4,7 @@ mod strain;
|
|||||||
pub use pp::*;
|
pub use pp::*;
|
||||||
use strain::Strain;
|
use strain::Strain;
|
||||||
|
|
||||||
use crate::{Beatmap, HitObject, Mods, StarResult};
|
use crate::{Beatmap, HitObject, Mods, StarResult, Strains};
|
||||||
|
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
const STAR_SCALING_FACTOR: f32 = 0.018;
|
const STAR_SCALING_FACTOR: f32 = 0.018;
|
||||||
@@ -63,6 +63,59 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
StarResult::Mania { stars }
|
StarResult::Mania { stars }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
/// evaluating the final strains, it just returns them as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
|
if map.hit_objects.len() < 2 {
|
||||||
|
return Strains::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let clock_rate = mods.speed();
|
||||||
|
let section_len = SECTION_LEN * clock_rate;
|
||||||
|
let mut strain = Strain::new(map.cs as u8);
|
||||||
|
|
||||||
|
let mut hit_objects = map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.zip(map.hit_objects.iter())
|
||||||
|
.map(|(base, prev)| DifficultyHitObject::new(base, prev, map.cs, clock_rate));
|
||||||
|
|
||||||
|
// No strain for first object
|
||||||
|
let mut current_section_end =
|
||||||
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let h = hit_objects.next().unwrap();
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
strain.process(&h);
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
|
for h in hit_objects {
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
strain.save_current_peak();
|
||||||
|
strain.start_new_section_from(current_section_end);
|
||||||
|
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
strain.process(&h);
|
||||||
|
}
|
||||||
|
|
||||||
|
strain.save_current_peak();
|
||||||
|
|
||||||
|
Strains {
|
||||||
|
section_length: section_len,
|
||||||
|
strains: strain.strain_peaks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct DifficultyHitObject<'o> {
|
pub(crate) struct DifficultyHitObject<'o> {
|
||||||
base: &'o HitObject,
|
base: &'o HitObject,
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ pub(crate) struct Strain {
|
|||||||
|
|
||||||
hold_end_times: Vec<f32>,
|
hold_end_times: Vec<f32>,
|
||||||
individual_strains: Vec<f32>,
|
individual_strains: Vec<f32>,
|
||||||
pub strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f32>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
StarResult::Osu { attributes }
|
StarResult::Osu { attributes }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: strains function
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::super::OsuPP;
|
use super::super::super::OsuPP;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{Beatmap, Mods, StarResult};
|
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f32 = 400.0;
|
||||||
@@ -153,6 +153,125 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
/// evaluating the final strains, it just returns them as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
|
let map_attributes = map.attributes().mods(mods);
|
||||||
|
let hitwindow = super::difficulty_range(map_attributes.od).floor() / map_attributes.clock_rate;
|
||||||
|
let od = (80.0 - hitwindow) / 6.0;
|
||||||
|
|
||||||
|
let mut diff_attributes = DifficultyAttributes {
|
||||||
|
ar: map_attributes.ar,
|
||||||
|
od,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if map.hit_objects.len() < 2 {
|
||||||
|
return Strains::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let section_len = SECTION_LEN * map_attributes.clock_rate;
|
||||||
|
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||||
|
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||||
|
|
||||||
|
if radius < 30.0 {
|
||||||
|
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||||
|
scaling_factor *= 1.0 + small_circle_bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slider_state = SliderState::new(map);
|
||||||
|
let mut ticks_buf = Vec::new();
|
||||||
|
|
||||||
|
let mut hit_objects = map.hit_objects.iter().filter_map(|h| {
|
||||||
|
OsuObject::new(
|
||||||
|
h,
|
||||||
|
map,
|
||||||
|
radius,
|
||||||
|
&mut ticks_buf,
|
||||||
|
&mut diff_attributes,
|
||||||
|
&mut slider_state,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut aim = Skill::new(SkillKind::Aim);
|
||||||
|
let mut speed = Skill::new(SkillKind::Speed);
|
||||||
|
|
||||||
|
// First object has no predecessor and thus no strain, handle distinctly
|
||||||
|
let mut current_section_end =
|
||||||
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
let mut prev_prev = None;
|
||||||
|
let mut prev = hit_objects.next().unwrap();
|
||||||
|
let mut prev_vals = None;
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let curr = hit_objects.next().unwrap();
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
map_attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
|
for curr in hit_objects {
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
map_attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
aim.save_current_peak();
|
||||||
|
aim.start_new_section_from(current_section_end);
|
||||||
|
speed.save_current_peak();
|
||||||
|
speed.start_new_section_from(current_section_end);
|
||||||
|
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.save_current_peak();
|
||||||
|
speed.save_current_peak();
|
||||||
|
|
||||||
|
let strains = aim
|
||||||
|
.strain_peaks
|
||||||
|
.into_iter()
|
||||||
|
.zip(speed.strain_peaks.into_iter())
|
||||||
|
.map(|(aim, speed)| aim + speed)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Strains {
|
||||||
|
section_length: section_len,
|
||||||
|
strains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::super::OsuPP;
|
use super::super::super::OsuPP;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
|
|||||||
current_section_peak: f32,
|
current_section_peak: f32,
|
||||||
|
|
||||||
kind: SkillKind,
|
kind: SkillKind,
|
||||||
strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f32>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult};
|
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult, Strains};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
@@ -177,6 +177,115 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
/// evaluating the final strains, it just returns them as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
|
let attributes = map.attributes().mods(mods);
|
||||||
|
|
||||||
|
if map.hit_objects.len() < 2 {
|
||||||
|
return Strains::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
let section_len = SECTION_LEN * attributes.clock_rate;
|
||||||
|
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
|
||||||
|
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||||
|
|
||||||
|
if radius < 30.0 {
|
||||||
|
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
|
||||||
|
scaling_factor *= 1.0 + small_circle_bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hit_objects = map.hit_objects.iter().filter_map(|h| match &h.kind {
|
||||||
|
HitObjectKind::Circle => Some(Cow::Borrowed(h)),
|
||||||
|
HitObjectKind::Slider { .. } => Some(Cow::Owned(HitObject {
|
||||||
|
pos: h.pos,
|
||||||
|
start_time: h.start_time,
|
||||||
|
kind: HitObjectKind::Circle,
|
||||||
|
sound: h.sound,
|
||||||
|
})),
|
||||||
|
HitObjectKind::Spinner { .. } => Some(Cow::Borrowed(h)),
|
||||||
|
HitObjectKind::Hold { .. } => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut aim = Skill::new(SkillKind::Aim);
|
||||||
|
let mut speed = Skill::new(SkillKind::Speed);
|
||||||
|
|
||||||
|
// First object has no predecessor and thus no strain, handle distinctly
|
||||||
|
let mut current_section_end =
|
||||||
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
let mut prev_prev = None;
|
||||||
|
let mut prev = hit_objects.next().unwrap();
|
||||||
|
let mut prev_vals = None;
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let curr = hit_objects.next().unwrap();
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
curr.as_ref(),
|
||||||
|
prev.as_ref(),
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
|
for curr in hit_objects {
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
curr.as_ref(),
|
||||||
|
prev.as_ref(),
|
||||||
|
prev_vals,
|
||||||
|
prev_prev,
|
||||||
|
attributes.clock_rate,
|
||||||
|
scaling_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
aim.save_current_peak();
|
||||||
|
aim.start_new_section_from(current_section_end);
|
||||||
|
speed.save_current_peak();
|
||||||
|
speed.start_new_section_from(current_section_end);
|
||||||
|
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.process(&h);
|
||||||
|
speed.process(&h);
|
||||||
|
|
||||||
|
prev_prev = Some(prev);
|
||||||
|
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||||
|
prev = curr;
|
||||||
|
}
|
||||||
|
|
||||||
|
aim.save_current_peak();
|
||||||
|
speed.save_current_peak();
|
||||||
|
|
||||||
|
let strains = aim
|
||||||
|
.strain_peaks
|
||||||
|
.into_iter()
|
||||||
|
.zip(speed.strain_peaks.into_iter())
|
||||||
|
.map(|(aim, speed)| aim + speed)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Strains {
|
||||||
|
section_length: section_len,
|
||||||
|
strains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::super::super::OsuPP;
|
use super::super::super::OsuPP;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
|
|||||||
current_section_peak: f32,
|
current_section_peak: f32,
|
||||||
|
|
||||||
kind: SkillKind,
|
kind: SkillKind,
|
||||||
strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f32>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-1
@@ -16,7 +16,7 @@ use skill::Skill;
|
|||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use stamina_cheese::StaminaCheeseDetector;
|
use stamina_cheese::StaminaCheeseDetector;
|
||||||
|
|
||||||
use crate::{Beatmap, Mods, StarResult};
|
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
@@ -117,6 +117,91 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
|||||||
StarResult::Taiko { stars }
|
StarResult::Taiko { stars }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Essentially the same as the `stars` function but instead of
|
||||||
|
/// evaluating the final strains, it just returns them as is.
|
||||||
|
///
|
||||||
|
/// Suitable to plot the difficulty of a map over time.
|
||||||
|
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||||
|
if map.hit_objects.len() < 2 {
|
||||||
|
return Strains::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 clock_rate = mods.speed();
|
||||||
|
let section_len = SECTION_LEN * clock_rate;
|
||||||
|
|
||||||
|
// No strain for first object
|
||||||
|
let mut current_section_end =
|
||||||
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
let mut hit_objects = map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.skip(2)
|
||||||
|
.zip(map.hit_objects.iter().skip(1))
|
||||||
|
.zip(map.hit_objects.iter())
|
||||||
|
.map(|(((idx, base), prev), prev_prev)| {
|
||||||
|
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let h = hit_objects.next().unwrap();
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
for skill in skills.iter_mut() {
|
||||||
|
skill.process(&h, &cheese);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
|
for h in hit_objects {
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
for skill in skills.iter_mut() {
|
||||||
|
skill.save_current_peak();
|
||||||
|
skill.start_new_section_from(current_section_end);
|
||||||
|
}
|
||||||
|
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
for skill in skills.iter_mut() {
|
||||||
|
skill.process(&h, &cheese);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for skill in skills.iter_mut() {
|
||||||
|
skill.save_current_peak();
|
||||||
|
}
|
||||||
|
|
||||||
|
let strains = skills[0]
|
||||||
|
.strain_peaks
|
||||||
|
.iter()
|
||||||
|
.zip(skills[1].strain_peaks.iter())
|
||||||
|
.zip(skills[2].strain_peaks.iter())
|
||||||
|
.zip(skills[3].strain_peaks.iter())
|
||||||
|
.map(|(((color, rhythm), stamina_right), stamina_left)| {
|
||||||
|
color + rhythm + stamina_right + stamina_left
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Strains {
|
||||||
|
section_length: section_len,
|
||||||
|
strains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn rescale(stars: f32) -> f32 {
|
fn rescale(stars: f32) -> f32 {
|
||||||
if stars < 0.0 {
|
if stars < 0.0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user