added functions to calculate only the strains
This commit is contained in:
+231
-1
@@ -11,7 +11,7 @@ use movement::Movement;
|
||||
pub use pp::*;
|
||||
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;
|
||||
|
||||
@@ -278,6 +278,236 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
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(
|
||||
start_time: f32,
|
||||
time_between_ticks: f32,
|
||||
|
||||
@@ -20,7 +20,7 @@ pub(crate) struct Movement {
|
||||
current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
|
||||
strain_peaks: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
prev_time: Option<f32>,
|
||||
}
|
||||
|
||||
|
||||
+32
-1
@@ -114,7 +114,7 @@ pub use parse::{
|
||||
};
|
||||
|
||||
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.
|
||||
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
|
||||
/// mode and use the mode's corresponding calculator, e.g. [`TaikoPP`](crate::TaikoPP) for taiko.
|
||||
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 {
|
||||
@@ -147,9 +156,30 @@ impl BeatmapExt for Beatmap {
|
||||
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.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum StarResult {
|
||||
Fruits {
|
||||
attributes: fruits::DifficultyAttributes,
|
||||
@@ -179,6 +209,7 @@ impl StarResult {
|
||||
}
|
||||
|
||||
/// Basic struct containing the result of a PP calculation.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PpResult {
|
||||
pub pp: f32,
|
||||
pub attributes: StarResult,
|
||||
|
||||
+54
-1
@@ -4,7 +4,7 @@ mod strain;
|
||||
pub use pp::*;
|
||||
use strain::Strain;
|
||||
|
||||
use crate::{Beatmap, HitObject, Mods, StarResult};
|
||||
use crate::{Beatmap, HitObject, Mods, StarResult, Strains};
|
||||
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
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 }
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub(crate) struct DifficultyHitObject<'o> {
|
||||
base: &'o HitObject,
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ pub(crate) struct Strain {
|
||||
|
||||
hold_end_times: Vec<f32>,
|
||||
individual_strains: Vec<f32>,
|
||||
pub strain_peaks: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<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 }
|
||||
}
|
||||
|
||||
// TODO: strains function
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::super::OsuPP;
|
||||
|
||||
@@ -17,7 +17,7 @@ use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{Beatmap, Mods, StarResult};
|
||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.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)]
|
||||
mod tests {
|
||||
use super::super::super::OsuPP;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
|
||||
current_section_peak: f32,
|
||||
|
||||
kind: SkillKind,
|
||||
strain_peaks: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult};
|
||||
use crate::{Beatmap, HitObject, HitObjectKind, Mods, StarResult, Strains};
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::super::super::OsuPP;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
|
||||
current_section_peak: f32,
|
||||
|
||||
kind: SkillKind,
|
||||
strain_peaks: Vec<f32>,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
}
|
||||
|
||||
+86
-1
@@ -16,7 +16,7 @@ use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use stamina_cheese::StaminaCheeseDetector;
|
||||
|
||||
use crate::{Beatmap, Mods, StarResult};
|
||||
use crate::{Beatmap, Mods, StarResult, Strains};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
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 }
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn rescale(stars: f32) -> f32 {
|
||||
if stars < 0.0 {
|
||||
|
||||
Reference in New Issue
Block a user