restructured osu!standard feature flags
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
//! In addtion to not considering the positional offset caused by stack leniency, slider paths are also ignored.
|
||||
//! This means the travel distance of notes is completely omitted which may cause further inaccuracies.
|
||||
//! Since the slider paths don't have to be computed though, it is generally faster than `no_leniency`.
|
||||
//! This means stacked positions aswell as the travel distance of notes is completely omitted which
|
||||
//! will cause notable inaccuracies.
|
||||
//! The advantage is that it's considerably faster than `osu_precise`.
|
||||
|
||||
#![cfg(feature = "no_sliders_no_leniency")]
|
||||
#![cfg(feature = "osu_fast")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
use super::DifficultyAttributes;
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
@@ -30,8 +31,8 @@ const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
/// Sliders are considered as regular hitcircles and stack leniency is ignored.
|
||||
/// Still very good results but the least precise version in general.
|
||||
/// However, this is the most efficient one.
|
||||
/// Still decently accurate results but definitely less precise than `osu_precise`.
|
||||
/// However, this version is considerably faster.
|
||||
///
|
||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||
pub fn stars(
|
||||
+20
-2
@@ -1,10 +1,22 @@
|
||||
#![cfg(feature = "osu")]
|
||||
|
||||
mod pp;
|
||||
mod versions;
|
||||
|
||||
pub use pp::*;
|
||||
pub use versions::*;
|
||||
|
||||
#[cfg(feature = "osu_precise")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "osu_precise")))]
|
||||
mod precise;
|
||||
|
||||
#[cfg(feature = "osu_precise")]
|
||||
pub use precise::*;
|
||||
|
||||
#[cfg(feature = "osu_fast")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "osu_fast")))]
|
||||
mod fast;
|
||||
|
||||
#[cfg(feature = "osu_fast")]
|
||||
pub use fast::*;
|
||||
|
||||
/// Various data created through the star calculation.
|
||||
/// This data is necessary to calculate PP.
|
||||
@@ -48,7 +60,13 @@ impl PerformanceAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn custom_osu() {
|
||||
use std::{fs::File, time::Instant};
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
//! Every aspect of osu!'s pp calculation is being used.
|
||||
//! This should result in the most accurate values but with
|
||||
//! drawback of being slower than the other versions.
|
||||
//! drawback of being slower than `osu_fast`.
|
||||
|
||||
#![cfg(feature = "all_included")]
|
||||
#![cfg(feature = "osu_precise")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use self::osu_object::ObjectParameters;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
use crate::{curve::CurveBuffers, parse::Pos2};
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
mod skill;
|
||||
@@ -18,12 +13,14 @@ mod skill_kind;
|
||||
mod slider_state;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
use osu_object::OsuObject;
|
||||
use osu_object::{ObjectParameters, OsuObject};
|
||||
use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{Beatmap, Mods, Strains};
|
||||
use crate::{curve::CurveBuffers, parse::Pos2, Beatmap, Mods, Strains};
|
||||
|
||||
use super::DifficultyAttributes;
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
@@ -33,10 +30,8 @@ const STACK_DISTANCE: f32 = 3.0;
|
||||
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
/// Both slider paths and stack leniency are considered.
|
||||
/// Since taking stack leniency into account is fairly expensive,
|
||||
/// this version is slower than the others but in turn gives the
|
||||
/// most precise results.
|
||||
/// Slider paths aswell as stack leniency are considered.
|
||||
/// Both of these drag the performance down but in turn the values are much more accurate
|
||||
///
|
||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||
pub fn stars(
|
||||
@@ -1,25 +0,0 @@
|
||||
#[cfg(feature = "all_included")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "all_included")))]
|
||||
mod all_included;
|
||||
|
||||
#[cfg(feature = "all_included")]
|
||||
pub use all_included::*;
|
||||
|
||||
#[cfg(feature = "no_leniency")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "no_leniency")))]
|
||||
mod no_leniency;
|
||||
|
||||
#[cfg(feature = "no_leniency")]
|
||||
pub use no_leniency::*;
|
||||
|
||||
#[cfg(feature = "no_sliders_no_leniency")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "no_sliders_no_leniency")))]
|
||||
mod no_sliders_no_leniency;
|
||||
|
||||
#[cfg(feature = "no_sliders_no_leniency")]
|
||||
pub use no_sliders_no_leniency::*;
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
super::super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use super::OsuObject;
|
||||
|
||||
pub(crate) struct DifficultyObject<'h> {
|
||||
pub(crate) base: &'h OsuObject,
|
||||
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||
|
||||
pub(crate) jump_dist: f32,
|
||||
pub(crate) travel_dist: f32,
|
||||
pub(crate) angle: Option<f32>,
|
||||
|
||||
pub(crate) delta: f32,
|
||||
pub(crate) strain_time: f32,
|
||||
}
|
||||
|
||||
impl<'h> DifficultyObject<'h> {
|
||||
pub(crate) fn new(
|
||||
base: &'h OsuObject,
|
||||
prev: &OsuObject,
|
||||
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time)
|
||||
prev_prev: Option<OsuObject>,
|
||||
scaling_factor: f32,
|
||||
) -> Self {
|
||||
let delta = base.time - prev.time;
|
||||
|
||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
|
||||
let strain_time = delta.max(25.0);
|
||||
|
||||
let pos = base.pos;
|
||||
let travel_dist = prev.travel_dist();
|
||||
let prev_cursor_pos = prev.lazy_end_pos();
|
||||
|
||||
// We don't need to calculate either angle or distance
|
||||
// when one of the last->curr objects is a spinner
|
||||
let (jump_dist, angle) = if base.is_spinner() {
|
||||
(0.0, None)
|
||||
} else {
|
||||
let jump_dist = ((pos - prev_cursor_pos) * scaling_factor).length();
|
||||
|
||||
let angle = prev_prev.map(|prev_prev| {
|
||||
let prev_prev_cursor_pos = prev_prev.lazy_end_pos();
|
||||
|
||||
let v1 = prev_prev_cursor_pos - prev.pos;
|
||||
let v2 = pos - prev_cursor_pos;
|
||||
|
||||
let dot = v1.dot(v2);
|
||||
let det = v1.x * v2.y - v1.y * v2.x;
|
||||
|
||||
det.atan2(dot).abs()
|
||||
});
|
||||
|
||||
(jump_dist, angle)
|
||||
};
|
||||
|
||||
Self {
|
||||
base,
|
||||
prev: prev_vals,
|
||||
|
||||
jump_dist,
|
||||
travel_dist,
|
||||
angle,
|
||||
|
||||
delta,
|
||||
strain_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
//! The positional offset of notes created by stack leniency is not considered.
|
||||
//! This means the jump distance inbetween notes might be slightly off, resulting in small inaccuracies.
|
||||
//! Since calculating these offsets is relatively expensive though,
|
||||
//! this version is generally faster than `all_included`.
|
||||
|
||||
#![cfg(feature = "no_leniency")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use self::osu_object::ObjectParameters;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
mod skill;
|
||||
mod skill_kind;
|
||||
mod slider_state;
|
||||
|
||||
use difficulty_object::DifficultyObject;
|
||||
use osu_object::OsuObject;
|
||||
use skill::Skill;
|
||||
use skill_kind::SkillKind;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||
|
||||
const OBJECT_RADIUS: f32 = 64.0;
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||
const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
/// Slider paths are considered but stack leniency is ignored.
|
||||
/// As most maps don't even make use of leniency and even if,
|
||||
/// it has generally little effect on stars, the results are close to perfect.
|
||||
/// This version is considerably more efficient than `all_included` since
|
||||
/// processing stack leniency is relatively expensive.
|
||||
///
|
||||
/// In case of a partial play, e.g. a fail, one can specify the amount of passed objects.
|
||||
pub fn stars(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> DifficultyAttributes {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = super::difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
let mut diff_attributes = DifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if take < 2 {
|
||||
return diff_attributes;
|
||||
}
|
||||
|
||||
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 params = ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
attributes: &mut diff_attributes,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.filter_map(|h| OsuObject::new(h, &mut params))
|
||||
.map(|mut h| {
|
||||
h.time /= map_attributes.clock_rate;
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
};
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut prev_vals = None;
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
// 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, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.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, scaling_factor);
|
||||
|
||||
while h.base.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);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let speed_rating = if mods.rx() {
|
||||
0.0
|
||||
} else {
|
||||
skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
};
|
||||
|
||||
let flashlight_rating = skills.get_mut(2).map_or(0.0, |skill| {
|
||||
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
});
|
||||
|
||||
let base_aim_performance = {
|
||||
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
|
||||
|
||||
base * base * base / 100_000.0
|
||||
};
|
||||
|
||||
let base_speed_performance = {
|
||||
let base = 5.0 * (speed_rating / 0.0675).max(1.0) - 4.0;
|
||||
|
||||
base * base * base / 100_000.0
|
||||
};
|
||||
|
||||
let base_flashlight_performance = if fl {
|
||||
flashlight_rating * flashlight_rating * 25.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let base_performance = (base_aim_performance.powf(1.1)
|
||||
+ base_speed_performance.powf(1.1)
|
||||
+ base_flashlight_performance.powf(1.1))
|
||||
.powf(1.0 / 1.1);
|
||||
|
||||
let star_rating = if base_performance > 0.00001 {
|
||||
1.12_f32.cbrt()
|
||||
* 0.027
|
||||
* ((100_000.0 / (1.0_f32 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
diff_attributes.aim_strain = aim_rating;
|
||||
diff_attributes.speed_strain = speed_rating;
|
||||
diff_attributes.flashlight_rating = flashlight_rating;
|
||||
diff_attributes.n_circles = map.n_circles as usize;
|
||||
diff_attributes.n_sliders = map.n_sliders as usize;
|
||||
diff_attributes.n_spinners = map.n_spinners as usize;
|
||||
diff_attributes.stars = star_rating;
|
||||
|
||||
diff_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 {
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = super::difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
let mut diff_attributes = DifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if map.hit_objects.len() < 2 {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
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 params = ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
attributes: &mut diff_attributes,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
};
|
||||
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter_map(|h| OsuObject::new(h, &mut params));
|
||||
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
};
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut prev_vals = None;
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
// 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, scaling_factor);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.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, scaling_factor);
|
||||
|
||||
while h.base.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;
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let mut speed_strains = skills.pop().unwrap().strain_peaks;
|
||||
let mut aim_strains = skills.pop().unwrap().strain_peaks;
|
||||
|
||||
let strains = if let Some(mut flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
|
||||
mem::swap(&mut speed_strains, &mut aim_strains);
|
||||
mem::swap(&mut aim_strains, &mut flashlight_strains);
|
||||
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.zip(flashlight_strains)
|
||||
.map(|((aim, speed), flashlight)| aim + speed + flashlight)
|
||||
.collect()
|
||||
} else {
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.map(|(aim, speed)| aim + speed)
|
||||
.collect()
|
||||
};
|
||||
|
||||
Strains {
|
||||
section_length: SECTION_LEN,
|
||||
strains,
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
use super::super::super::DifficultyAttributes;
|
||||
use super::slider_state::SliderState;
|
||||
|
||||
use crate::{
|
||||
curve::{Curve, CurveBuffers},
|
||||
parse::{HitObject, HitObjectKind, Pos2},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
||||
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) kind: OsuObjectKind,
|
||||
}
|
||||
|
||||
pub(crate) enum OsuObjectKind {
|
||||
Circle,
|
||||
Slider {
|
||||
end_pos: Pos2,
|
||||
lazy_end_pos: Pos2,
|
||||
travel_dist: f32,
|
||||
},
|
||||
Spinner,
|
||||
}
|
||||
|
||||
pub(crate) struct ObjectParameters<'a> {
|
||||
pub(crate) map: &'a Beatmap,
|
||||
pub(crate) radius: f32,
|
||||
pub(crate) scaling_factor: f32,
|
||||
pub(crate) attributes: &'a mut DifficultyAttributes,
|
||||
pub(crate) ticks: Vec<f32>,
|
||||
pub(crate) slider_state: SliderState<'a>,
|
||||
pub(crate) curve_bufs: CurveBuffers,
|
||||
}
|
||||
|
||||
impl OsuObject {
|
||||
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
radius,
|
||||
scaling_factor,
|
||||
attributes,
|
||||
ticks,
|
||||
slider_state,
|
||||
curve_bufs,
|
||||
} = params;
|
||||
|
||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
|
||||
let obj = match &h.kind {
|
||||
HitObjectKind::Circle => Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
kind: OsuObjectKind::Circle,
|
||||
},
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
} => {
|
||||
// Key values which are computed here
|
||||
let mut lazy_end_pos = h.pos;
|
||||
let mut travel_dist = 0.0;
|
||||
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f32;
|
||||
|
||||
let approx_follow_circle_radius = *radius * 3.0;
|
||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||
|
||||
if map.version >= 8 {
|
||||
tick_dist /=
|
||||
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
||||
}
|
||||
|
||||
// Build the curve w.r.t. the curve points
|
||||
let curve = Curve::new(control_points, *pixel_len, curve_bufs);
|
||||
|
||||
let velocity =
|
||||
(BASE_SCORING_DISTANCE * map.slider_mult * slider_state.slider_velocity)
|
||||
/ slider_state.beat_len;
|
||||
|
||||
let end_time = h.start_time + span_count * curve.dist() / velocity;
|
||||
let duration = end_time - h.start_time;
|
||||
let span_duration = duration / span_count;
|
||||
|
||||
// Called on each slider object except for the head.
|
||||
// Increases combo and adjusts `end_pos` and `travel_dist`
|
||||
// w.r.t. the object position at the given time on the slider curve.
|
||||
let mut compute_vertex = |time: f32| {
|
||||
attributes.max_combo += 1;
|
||||
|
||||
let mut progress = (time - h.start_time) / span_duration;
|
||||
|
||||
if progress % 2.0 >= 1.0 {
|
||||
progress = 1.0 - progress % 1.0;
|
||||
} else {
|
||||
progress %= 1.0;
|
||||
}
|
||||
|
||||
let curr_pos = curve.position_at(progress);
|
||||
let diff = h.pos + curr_pos - lazy_end_pos;
|
||||
let mut dist = diff.length();
|
||||
|
||||
if dist > approx_follow_circle_radius {
|
||||
// * The cursor would be outside the follow circle, we need to move it
|
||||
dist -= approx_follow_circle_radius;
|
||||
lazy_end_pos += diff.normalize() * dist;
|
||||
travel_dist += dist;
|
||||
}
|
||||
};
|
||||
|
||||
let mut current_distance = tick_dist;
|
||||
let time_add = duration * (tick_dist / (pixel_len * span_count));
|
||||
|
||||
let target = pixel_len - tick_dist / 8.0;
|
||||
ticks.reserve((target / tick_dist) as usize);
|
||||
|
||||
// Tick of the first span
|
||||
if current_distance < target {
|
||||
for tick_idx in 1.. {
|
||||
let time = h.start_time + time_add * tick_idx as f32;
|
||||
compute_vertex(time);
|
||||
ticks.push(time);
|
||||
current_distance += tick_dist;
|
||||
|
||||
if current_distance >= target {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Other spans
|
||||
if *repeats > 1 {
|
||||
for repeat_id in 1..*repeats {
|
||||
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||
|
||||
// Reverse tick
|
||||
compute_vertex(h.start_time + time_offset);
|
||||
|
||||
// Actual ticks
|
||||
if repeat_id & 1 == 1 {
|
||||
ticks.iter().rev().for_each(|&time| compute_vertex(time));
|
||||
} else {
|
||||
ticks.iter().for_each(|&time| compute_vertex(time));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let final_span_idx = repeats.saturating_sub(1);
|
||||
let final_span_start_time = h.start_time + final_span_idx as f32 * span_duration;
|
||||
let final_span_end_time = (h.start_time + duration / 2.0)
|
||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||
compute_vertex(final_span_end_time);
|
||||
|
||||
ticks.clear();
|
||||
|
||||
let progress = (*repeats % 2 == 0) as u8 as f32;
|
||||
let end_pos = h.pos + curve.position_at(progress);
|
||||
travel_dist *= *scaling_factor;
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
kind: OsuObjectKind::Slider {
|
||||
end_pos,
|
||||
lazy_end_pos,
|
||||
travel_dist,
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { .. } => Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
kind: OsuObjectKind::Spinner,
|
||||
},
|
||||
HitObjectKind::Hold { .. } => return None,
|
||||
};
|
||||
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end_pos(&self) -> Pos2 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
|
||||
OsuObjectKind::Slider { end_pos, .. } => end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn lazy_end_pos(&self) -> Pos2 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => self.pos,
|
||||
OsuObjectKind::Slider { lazy_end_pos, .. } => lazy_end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn travel_dist(&self) -> f32 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner => 0.0,
|
||||
OsuObjectKind::Slider { travel_dist, .. } => travel_dist,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_slider(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Slider { .. })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_spinner(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Spinner)
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use crate::math_util;
|
||||
|
||||
use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
curr_strain: f32,
|
||||
curr_section_peak: f32,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
|
||||
prev_time: Option<f32>,
|
||||
}
|
||||
|
||||
impl Skill {
|
||||
#[inline]
|
||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
||||
Self {
|
||||
curr_strain: 1.0,
|
||||
curr_section_peak: 0.0,
|
||||
|
||||
kind,
|
||||
strain_peaks: Vec::with_capacity(128),
|
||||
|
||||
prev_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process(&mut self, curr: &DifficultyObject<'_>) {
|
||||
self.kind.pre_process();
|
||||
self.curr_section_peak = self.strain_value_at(curr).max(self.curr_section_peak);
|
||||
self.prev_time = Some(curr.base.time);
|
||||
self.kind.post_process(curr);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn save_current_peak(&mut self) {
|
||||
self.strain_peaks.push(self.curr_section_peak);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
// The maximum strain of the new section is not zero by default
|
||||
self.curr_section_peak = self.calculate_initial_strain(time);
|
||||
}
|
||||
|
||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
let decay_weight = self.kind.decay_weight();
|
||||
|
||||
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
||||
let reduced_section_count_f32 = reduced_section_count as f32;
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
let peaks = self.strain_peaks.iter_mut();
|
||||
|
||||
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
||||
let clamped = (i as f32 / reduced_section_count_f32).clamp(0.0, 1.0);
|
||||
let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
|
||||
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||
}
|
||||
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
for &strain in self.strain_peaks.iter() {
|
||||
difficulty += strain * weight;
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty * difficulty_multiplier
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_initial_strain(&self, time: f32) -> f32 {
|
||||
let prev_time = self.prev_time.unwrap_or(0.0);
|
||||
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
|
||||
|
||||
match &self.kind {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => decayed_strain,
|
||||
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f32 {
|
||||
self.curr_strain *= self.kind.strain_decay(curr.delta);
|
||||
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
|
||||
|
||||
match &mut self.kind {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => self.curr_strain,
|
||||
SkillKind::Speed {
|
||||
curr_rhythm,
|
||||
history,
|
||||
hit_window,
|
||||
} => {
|
||||
*curr_rhythm = calculate_speed_rhythm_bonus(curr, history, *hit_window);
|
||||
|
||||
self.curr_strain * *curr_rhythm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
use std::{collections::VecDeque, f32::consts::PI, iter};
|
||||
|
||||
use crate::{math_util, parse::Pos2};
|
||||
|
||||
use super::DifficultyObject;
|
||||
|
||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
||||
|
||||
const MIN_SPEED_BONUS: f32 = 75.0;
|
||||
const SPEED_BALANCING_FACTOR: f32 = 40.0;
|
||||
|
||||
const TIMING_THRESHOLD: f32 = 107.0;
|
||||
|
||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const AIM_DECAY_WEIGHT: f32 = 0.9;
|
||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
|
||||
|
||||
const SPEED_SKILL_MULTIPLIER: f32 = 1375.0;
|
||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
||||
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
||||
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
||||
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
||||
|
||||
const SPEED_HISTORY_LENGTH: usize = 32;
|
||||
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0;
|
||||
const SPEED_RHYTHM_MULTIPLIER: f32 = 0.75;
|
||||
|
||||
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.15;
|
||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
|
||||
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
||||
|
||||
pub(crate) struct FlashlightHistoryEntry {
|
||||
end_pos: Pos2,
|
||||
is_spinner: bool,
|
||||
jump_dist: f32,
|
||||
strain_time: f32,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
||||
fn from(h: &DifficultyObject<'_>) -> Self {
|
||||
Self {
|
||||
end_pos: h.base.end_pos(),
|
||||
is_spinner: h.base.is_spinner(),
|
||||
jump_dist: h.jump_dist,
|
||||
strain_time: h.strain_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SpeedHistoryEntry {
|
||||
is_slider: bool,
|
||||
start_time: f32,
|
||||
strain_time: f32,
|
||||
}
|
||||
|
||||
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
||||
fn from(h: &DifficultyObject<'_>) -> Self {
|
||||
Self {
|
||||
is_slider: h.base.is_slider(),
|
||||
start_time: h.base.time,
|
||||
strain_time: h.strain_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum SkillKind {
|
||||
Aim,
|
||||
Flashlight {
|
||||
history: VecDeque<FlashlightHistoryEntry>,
|
||||
scaling_factor: f32,
|
||||
},
|
||||
Speed {
|
||||
curr_rhythm: f32,
|
||||
history: VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
},
|
||||
}
|
||||
|
||||
impl SkillKind {
|
||||
pub(crate) fn flashlight(scaling_factor: f32) -> Self {
|
||||
Self::Flashlight {
|
||||
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH),
|
||||
scaling_factor,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn speed(hit_window: f32) -> Self {
|
||||
Self::Speed {
|
||||
curr_rhythm: 1.0,
|
||||
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH),
|
||||
hit_window,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pre_process(&mut self) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
|
||||
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => history.push_front(current.into()),
|
||||
Self::Speed { history, .. } => history.push_front(current.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f32 {
|
||||
match self {
|
||||
Self::Aim => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut aim_strain = 0.0;
|
||||
|
||||
if let Some((prev_jump_dist, prev_strain_time)) = curr.prev {
|
||||
if let Some(angle) = curr.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
|
||||
let scale = 90.0;
|
||||
|
||||
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
|
||||
* (prev_jump_dist - scale).max(0.0)
|
||||
* (curr.jump_dist - scale).max(0.0))
|
||||
.sqrt();
|
||||
|
||||
aim_strain = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
|
||||
/ (TIMING_THRESHOLD).max(prev_strain_time)
|
||||
}
|
||||
}
|
||||
|
||||
let jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
|
||||
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist);
|
||||
|
||||
let dist_exp =
|
||||
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
|
||||
|
||||
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
|
||||
.max(dist_exp / curr.strain_time)
|
||||
}
|
||||
Self::Flashlight {
|
||||
history,
|
||||
scaling_factor,
|
||||
} => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut small_dist_nerf = 1.0;
|
||||
let mut result = 0.0;
|
||||
let mut cumulative_strain_time = 0.0;
|
||||
let mut history_iter = history.iter();
|
||||
|
||||
if let Some(prev) = history_iter.next() {
|
||||
// Handle first entry distinctly for slight optimization
|
||||
if !prev.is_spinner {
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
||||
cumulative_strain_time += prev.strain_time;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius
|
||||
small_dist_nerf = (jump_dist / 75.0).min(1.0);
|
||||
|
||||
// We also want to nerf stacks so that only the first object of the stack is accounted for
|
||||
// -- since jump distance is 0 on stacked notes in this version, approximate value as 0.2
|
||||
let stack_nerf =
|
||||
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
|
||||
|
||||
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
|
||||
}
|
||||
|
||||
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
|
||||
|
||||
for (factor, prev) in factors.zip(history_iter) {
|
||||
if !prev.is_spinner {
|
||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
||||
cumulative_strain_time += prev.strain_time;
|
||||
let stack_nerf =
|
||||
((prev.jump_dist / scaling_factor) / 25.0).min(1.0).max(0.2);
|
||||
|
||||
result += factor * stack_nerf * scaling_factor * jump_dist
|
||||
/ cumulative_strain_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result *= small_dist_nerf;
|
||||
|
||||
result * result
|
||||
}
|
||||
Self::Speed {
|
||||
history,
|
||||
hit_window,
|
||||
..
|
||||
} => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut strain_time = curr.strain_time;
|
||||
let hit_window_full = hit_window * 2.0;
|
||||
let speed_window_ratio = strain_time / hit_window_full;
|
||||
let prev = history.front();
|
||||
|
||||
// Aim to nerf cheesy rhythms (very fast consecutive doubles with large delta times between)
|
||||
if let Some(prev) =
|
||||
prev.filter(|p| strain_time < hit_window_full && p.strain_time > strain_time)
|
||||
{
|
||||
strain_time =
|
||||
math_util::lerp(prev.strain_time, strain_time, speed_window_ratio);
|
||||
}
|
||||
|
||||
// Cap delta time to the OD 300 hit window
|
||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly,
|
||||
// whilst 0.92 limits the effect of the cap
|
||||
strain_time /= (strain_time / hit_window_full / 0.93).clamp(0.92, 1.0);
|
||||
|
||||
// Derive speed bonus for calculation
|
||||
let mut speed_bonus = 1.0;
|
||||
|
||||
if strain_time < MIN_SPEED_BONUS {
|
||||
let base = (MIN_SPEED_BONUS - strain_time) / SPEED_BALANCING_FACTOR;
|
||||
speed_bonus = 1.0 + 0.75 * base * base;
|
||||
}
|
||||
|
||||
let dist = SINGLE_SPACING_TRESHOLD.min(curr.travel_dist + curr.jump_dist);
|
||||
|
||||
(speed_bonus + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
|
||||
/ strain_time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
||||
match self {
|
||||
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
||||
Self::Flashlight { .. } => (
|
||||
FLASHLIGHT_REDUCED_SECTION_COUNT,
|
||||
FLASHLIGHT_DIFFICULTY_MULTIPLIER,
|
||||
),
|
||||
Self::Speed { .. } => (SPEED_REDUCED_SECTION_COUNT, SPEED_DIFFICULTY_MULTIPLIER),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn skill_multiplier(&self) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn strain_decay_base(&self) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn decay_weight(&self) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim => AIM_DECAY_WEIGHT,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
||||
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn strain_decay(&self, ms: f32) -> f32 {
|
||||
self.strain_decay_base().powf(ms / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_speed_rhythm_bonus(
|
||||
current: &DifficultyObject<'_>,
|
||||
history: &VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
) -> f32 {
|
||||
if current.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut prev_island_size = 0;
|
||||
let mut rhythm_complexity_sum = 0.0;
|
||||
let mut island_size = 1;
|
||||
let mut first_delta_switch = false;
|
||||
let adjusted_hit_window = hit_window * 0.6;
|
||||
let history_len = history.len() as f32;
|
||||
|
||||
// Store the ratio of the current start of an island to buff for tighter rhythms
|
||||
let mut start_ratio = 0.0;
|
||||
|
||||
let currs = history.iter();
|
||||
let prevs = history.iter().skip(1);
|
||||
let lasts = history.iter().skip(2);
|
||||
|
||||
for (((prev, curr), last), i) in prevs.zip(currs).zip(lasts).rev().zip(2..) {
|
||||
let mut curr_historical_decay =
|
||||
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
|
||||
/ SPEED_HISTORY_TIME_MAX;
|
||||
|
||||
if curr_historical_decay.abs() > f32::EPSILON {
|
||||
// Either we're limited by time or limited by object count
|
||||
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
|
||||
|
||||
let curr_delta = curr.strain_time;
|
||||
let prev_delta = prev.strain_time;
|
||||
let last_delta = last.strain_time;
|
||||
|
||||
// Fancy function to calculate rhythm bonuses
|
||||
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
||||
let curr_ratio = 1.0 + 6.0 * (base * base).min(0.5);
|
||||
|
||||
let lower_penalty = ((prev_delta - curr_delta).abs() - adjusted_hit_window).max(0.0);
|
||||
let window_penalty = (lower_penalty / adjusted_hit_window).min(1.0);
|
||||
|
||||
let mut effective_ratio = window_penalty * curr_ratio;
|
||||
|
||||
if first_delta_switch {
|
||||
if !(prev_delta > 1.25 * curr_delta || prev_delta * 1.25 < curr_delta) {
|
||||
if island_size < 7 {
|
||||
island_size += 1;
|
||||
}
|
||||
} else {
|
||||
if curr.is_slider {
|
||||
// bpm change is into slider, this is easy acc window
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
if prev.is_slider {
|
||||
// bpm change was from a slider, this is easier typically than circle -> circle
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
if prev_island_size == island_size {
|
||||
// repeated island size (ex: triplet -> triplet)
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
if prev_island_size % 2 == island_size % 2 {
|
||||
// repeated island polarity (2 -> 4, 3 -> 5)
|
||||
effective_ratio *= 0.5;
|
||||
}
|
||||
|
||||
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
|
||||
// previous increase happened a note ago, 1/1 -> 1/2-1/4, don't want to buff this
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
||||
* curr_historical_decay
|
||||
* ((4 + island_size) as f32).sqrt()
|
||||
* ((4 + prev_island_size) as f32).sqrt()
|
||||
/ 4.0;
|
||||
|
||||
start_ratio = effective_ratio;
|
||||
prev_island_size = island_size;
|
||||
island_size = 1;
|
||||
|
||||
// we're slowing down, stop counting
|
||||
if prev_delta * 1.25 < curr_delta {
|
||||
// if we're speeding up, this stays true and we keep counting island size
|
||||
first_delta_switch = false;
|
||||
}
|
||||
}
|
||||
} else if prev_delta > 1.25 * curr_delta {
|
||||
// we want to be speeding up
|
||||
// begin counting island until we change speed again
|
||||
first_delta_switch = true;
|
||||
start_ratio = effective_ratio;
|
||||
island_size = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn apply_diminishing_exp(val: f32) -> f32 {
|
||||
val.powf(0.99)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use crate::{Beatmap, ControlPoint, ControlPointIter};
|
||||
|
||||
pub(crate) struct SliderState<'p> {
|
||||
control_points: ControlPointIter<'p>,
|
||||
next: Option<ControlPoint>,
|
||||
pub(crate) beat_len: f32,
|
||||
pub(crate) slider_velocity: f32,
|
||||
}
|
||||
|
||||
impl<'p> SliderState<'p> {
|
||||
#[inline]
|
||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||
let mut control_points = ControlPointIter::new(map);
|
||||
|
||||
let (beat_len, slider_velocity) = match control_points.next() {
|
||||
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
||||
Some(ControlPoint::Difficulty {
|
||||
slider_velocity: speed_mult,
|
||||
..
|
||||
}) => (1000.0, speed_mult),
|
||||
None => (1000.0, 1.0),
|
||||
};
|
||||
|
||||
Self {
|
||||
next: control_points.next(),
|
||||
control_points,
|
||||
beat_len,
|
||||
slider_velocity,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn update(&mut self, time: f32) {
|
||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||
match next {
|
||||
ControlPoint::Timing { beat_len, .. } => {
|
||||
self.beat_len = *beat_len;
|
||||
self.slider_velocity = 1.0;
|
||||
}
|
||||
ControlPoint::Difficulty {
|
||||
slider_velocity: speed_mult,
|
||||
..
|
||||
} => self.slider_velocity = *speed_mult,
|
||||
}
|
||||
|
||||
self.next = self.control_points.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
parse::{DifficultyPoint, TimingPoint},
|
||||
Beatmap,
|
||||
};
|
||||
|
||||
use super::SliderState;
|
||||
|
||||
#[test]
|
||||
fn osu_slider_state() {
|
||||
let map = Beatmap {
|
||||
timing_points: vec![
|
||||
TimingPoint {
|
||||
time: 1.0,
|
||||
beat_len: 10.0,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 3.0,
|
||||
beat_len: 20.0,
|
||||
},
|
||||
TimingPoint {
|
||||
time: 4.0,
|
||||
beat_len: 30.0,
|
||||
},
|
||||
],
|
||||
difficulty_points: vec![
|
||||
DifficultyPoint {
|
||||
time: 2.0,
|
||||
speed_multiplier: 15.0,
|
||||
},
|
||||
DifficultyPoint {
|
||||
time: 5.0,
|
||||
speed_multiplier: 45.0,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut state = SliderState::new(&map);
|
||||
|
||||
state.update(2.0);
|
||||
assert!((state.beat_len - 10.0).abs() <= f32::EPSILON);
|
||||
|
||||
state.update(3.0);
|
||||
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON);
|
||||
|
||||
state.update(5.0);
|
||||
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON);
|
||||
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user