osu: updated up to commit from 2021-10-27
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
## Upcoming
|
||||
|
||||
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
|
||||
|
||||
# v0.2.3
|
||||
|
||||
- Reduced amount of required features of `async_std` and `async_tokio`
|
||||
|
||||
@@ -14,6 +14,7 @@ pub trait Mods: Copy {
|
||||
const HD: u32 = 1 << 3;
|
||||
const HR: u32 = 1 << 4;
|
||||
const DT: u32 = 1 << 6;
|
||||
const RX: u32 = 1 << 7;
|
||||
const HT: u32 = 1 << 8;
|
||||
const FL: u32 = 1 << 10;
|
||||
const SO: u32 = 1 << 12;
|
||||
@@ -28,6 +29,7 @@ pub trait Mods: Copy {
|
||||
fn hd(self) -> bool;
|
||||
fn hr(self) -> bool;
|
||||
fn dt(self) -> bool;
|
||||
fn rx(self) -> bool;
|
||||
fn ht(self) -> bool;
|
||||
fn fl(self) -> bool;
|
||||
fn so(self) -> bool;
|
||||
@@ -72,6 +74,7 @@ impl Mods for u32 {
|
||||
impl_mods!(hd, HD);
|
||||
impl_mods!(hr, HR);
|
||||
impl_mods!(dt, DT);
|
||||
impl_mods!(rx, RX);
|
||||
impl_mods!(ht, HT);
|
||||
impl_mods!(fl, FL);
|
||||
impl_mods!(so, SO);
|
||||
|
||||
+7
-5
@@ -10,13 +10,15 @@ pub use versions::*;
|
||||
/// This data is necessary to calculate PP.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DifficultyAttributes {
|
||||
pub stars: f32,
|
||||
pub aim_strain: f32,
|
||||
pub speed_strain: f32,
|
||||
pub flashlight_rating: f32,
|
||||
pub ar: f32,
|
||||
pub od: f32,
|
||||
pub speed_strain: f32,
|
||||
pub aim_strain: f32,
|
||||
pub flashlight_strain: f32,
|
||||
pub max_combo: usize,
|
||||
pub hp: f32,
|
||||
pub n_circles: usize,
|
||||
pub n_sliders: usize,
|
||||
pub n_spinners: usize,
|
||||
pub stars: f32,
|
||||
pub max_combo: usize,
|
||||
}
|
||||
|
||||
+98
-56
@@ -42,6 +42,8 @@ pub struct OsuPP<'m> {
|
||||
n50: Option<usize>,
|
||||
n_misses: usize,
|
||||
passed_objects: Option<usize>,
|
||||
|
||||
effective_misses: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'m> OsuPP<'m> {
|
||||
@@ -59,6 +61,8 @@ impl<'m> OsuPP<'m> {
|
||||
n50: None,
|
||||
n_misses: 0,
|
||||
passed_objects: None,
|
||||
|
||||
effective_misses: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,9 +283,11 @@ impl<'m> OsuPP<'m> {
|
||||
let total_hits = self.total_hits() as f32;
|
||||
let mut multiplier = 1.12;
|
||||
|
||||
self.calculate_effective_misses(total_hits);
|
||||
|
||||
// NF penalty
|
||||
if self.mods.nf() {
|
||||
multiplier *= (1.0 - 0.02 * self.n_misses as f32).max(0.9);
|
||||
multiplier *= (1.0 - 0.02 * self.effective_misses.map_or(0.0, |m| m as f32)).max(0.9);
|
||||
}
|
||||
|
||||
// SO penalty
|
||||
@@ -290,6 +296,14 @@ impl<'m> OsuPP<'m> {
|
||||
multiplier *= 1.0 - (n_spinners as f32 / total_hits).powf(0.85);
|
||||
}
|
||||
|
||||
// Relax penalty
|
||||
if self.mods.rx() {
|
||||
*self.effective_misses.as_mut().unwrap() +=
|
||||
self.n100.unwrap_or(0) + self.n50.unwrap_or(0);
|
||||
|
||||
multiplier *= 0.6;
|
||||
}
|
||||
|
||||
let aim_value = self.compute_aim_value(total_hits);
|
||||
let speed_value = self.compute_speed_value(total_hits);
|
||||
let acc_value = self.compute_accuracy_value(total_hits);
|
||||
@@ -326,10 +340,10 @@ impl<'m> OsuPP<'m> {
|
||||
aim_value *= len_bonus;
|
||||
|
||||
// Penalize misses
|
||||
if self.n_misses > 0 {
|
||||
let effective_misses = self.effective_misses.map_or(0, |m| m as i32);
|
||||
if effective_misses > 0 {
|
||||
aim_value *= 0.97
|
||||
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
|
||||
.powi(self.n_misses as i32);
|
||||
* (1.0 - (effective_misses as f32 / total_hits).powf(0.775)).powi(effective_misses);
|
||||
}
|
||||
|
||||
// Combo scaling
|
||||
@@ -349,7 +363,7 @@ impl<'m> OsuPP<'m> {
|
||||
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip();
|
||||
let ar_bonus = 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
|
||||
|
||||
// HD bonus
|
||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||
if self.mods.hd() {
|
||||
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||
}
|
||||
@@ -376,10 +390,11 @@ impl<'m> OsuPP<'m> {
|
||||
speed_value *= len_bonus;
|
||||
|
||||
// Penalize misses
|
||||
if self.n_misses > 0 {
|
||||
let effective_misses = self.effective_misses.map_or(0.0, |m| m as f32);
|
||||
if effective_misses > 0.0 {
|
||||
speed_value *= 0.97
|
||||
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
|
||||
.powf((self.n_misses as f32).powf(0.875));
|
||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||
.powf(effective_misses.powf(0.875));
|
||||
}
|
||||
|
||||
// Combo scaling
|
||||
@@ -398,7 +413,7 @@ impl<'m> OsuPP<'m> {
|
||||
|
||||
speed_value *= 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
|
||||
|
||||
// HD bonus
|
||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||
if self.mods.hd() {
|
||||
speed_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||
}
|
||||
@@ -421,6 +436,10 @@ impl<'m> OsuPP<'m> {
|
||||
}
|
||||
|
||||
fn compute_accuracy_value(&self, total_hits: f32) -> f32 {
|
||||
if self.mods.rx() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
let n_circles = attributes.n_circles as f32;
|
||||
let n300 = self.n300.unwrap_or(0) as f32;
|
||||
@@ -436,7 +455,7 @@ impl<'m> OsuPP<'m> {
|
||||
// Bonus for many hitcircles
|
||||
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
|
||||
|
||||
// HD bonus
|
||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||
if self.mods.hd() {
|
||||
acc_value *= 1.08;
|
||||
}
|
||||
@@ -450,53 +469,76 @@ impl<'m> OsuPP<'m> {
|
||||
}
|
||||
|
||||
fn compute_flashlight_value(&self, total_hits: f32) -> f32 {
|
||||
if self.mods.fl() {
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
|
||||
// TD penalty
|
||||
let raw_flashlight = if self.mods.td() {
|
||||
attributes.flashlight_strain.powf(0.8)
|
||||
} else {
|
||||
attributes.flashlight_strain
|
||||
};
|
||||
|
||||
let mut flashlight_value = raw_flashlight * raw_flashlight * 25.0;
|
||||
|
||||
// Add an additional bonus for HDFL
|
||||
if self.mods.hd() {
|
||||
flashlight_value *= 1.3;
|
||||
}
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// Default a 3% reduction for any # of misses
|
||||
if self.n_misses > 0 {
|
||||
flashlight_value *= 0.97
|
||||
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
|
||||
.powf((self.n_misses as f32).powf(0.875));
|
||||
}
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
flashlight_value *=
|
||||
((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||
}
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
|
||||
flashlight_value *= 0.7
|
||||
+ 0.1 * (total_hits / 200.0).min(1.0)
|
||||
+ (total_hits > 200.0) as u8 as f32
|
||||
* (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
||||
|
||||
// Scale the aim value with accuracy _slightly_
|
||||
flashlight_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||
|
||||
// It is important to also consider accuracy difficulty when doing that
|
||||
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||
|
||||
flashlight_value
|
||||
} else {
|
||||
0.0
|
||||
if !self.mods.fl() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
|
||||
// TD penalty
|
||||
let raw_flashlight = if self.mods.td() {
|
||||
attributes.flashlight_rating.powf(0.8)
|
||||
} else {
|
||||
attributes.flashlight_rating
|
||||
};
|
||||
|
||||
let mut flashlight_value = raw_flashlight * raw_flashlight * 25.0;
|
||||
|
||||
// Add an additional bonus for HDFL
|
||||
if self.mods.hd() {
|
||||
flashlight_value *= 1.3;
|
||||
}
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// Default a 3% reduction for any # of misses
|
||||
let effective_misses = self.effective_misses.map_or(0.0, |m| m as f32);
|
||||
if effective_misses > 0.0 {
|
||||
flashlight_value *= 0.97
|
||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||
.powf(effective_misses.powf(0.875));
|
||||
}
|
||||
|
||||
// Combo scaling
|
||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||
flashlight_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||
}
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
|
||||
flashlight_value *= 0.7
|
||||
+ 0.1 * (total_hits / 200.0).min(1.0)
|
||||
+ (total_hits > 200.0) as u8 as f32 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
||||
|
||||
// Scale the aim value with accuracy _slightly_
|
||||
flashlight_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||
|
||||
// It is important to also consider accuracy difficulty when doing that
|
||||
flashlight_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||
|
||||
flashlight_value
|
||||
}
|
||||
|
||||
fn calculate_effective_misses(&mut self, total_hits: f32) {
|
||||
// Guess the number of misses + slider breaks from combo
|
||||
let mut combo_based_misses: f32 = 0.0;
|
||||
|
||||
let attributes = self.attributes.as_ref().unwrap();
|
||||
|
||||
if attributes.n_sliders > 0 {
|
||||
let full_combo_threshold =
|
||||
attributes.max_combo as f32 - 0.1 * attributes.n_sliders as f32;
|
||||
|
||||
let f32_combo = self.combo.map(|c| c as f32);
|
||||
|
||||
if let Some(combo) = f32_combo.filter(|&c| c < full_combo_threshold) {
|
||||
combo_based_misses = full_combo_threshold / combo.max(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// We're clamping misses because since it's derived from combo it
|
||||
// can be higher than total hits and that breaks some calculations
|
||||
combo_based_misses = combo_based_misses.min(total_hits);
|
||||
|
||||
self.effective_misses = Some(self.n_misses.max(combo_based_misses.floor() as usize))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -10,11 +10,7 @@ pub mod no_leniency;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "no_sliders_no_leniency")))]
|
||||
pub mod no_sliders_no_leniency;
|
||||
|
||||
const OSU_OD_MAX: f32 = 20.0;
|
||||
const OSU_OD_AVG: f32 = 50.0;
|
||||
const OSU_OD_MIN: f32 = 80.0;
|
||||
|
||||
#[inline]
|
||||
fn difficulty_range(od: f32) -> f32 {
|
||||
super::super::difficulty_range(od, OSU_OD_MAX, OSU_OD_AVG, OSU_OD_MIN)
|
||||
fn difficulty_range_od(od: f32) -> f32 {
|
||||
super::super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||
}
|
||||
|
||||
@@ -21,30 +21,36 @@ impl<'h> DifficultyObject<'h> {
|
||||
scaling_factor: f32,
|
||||
) -> Self {
|
||||
let delta = base.time - prev.time;
|
||||
let strain_time = delta.max(50.0);
|
||||
|
||||
// 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();
|
||||
|
||||
let jump_dist = if base.is_spinner() {
|
||||
0.0
|
||||
// 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 {
|
||||
((pos - prev_cursor_pos) * scaling_factor).length()
|
||||
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)
|
||||
};
|
||||
|
||||
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()
|
||||
});
|
||||
|
||||
Self {
|
||||
base,
|
||||
prev: prev_vals,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#![cfg(feature = "no_leniency")]
|
||||
|
||||
use std::mem;
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
|
||||
mod difficulty_object;
|
||||
@@ -39,11 +41,12 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
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 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()
|
||||
};
|
||||
@@ -88,7 +91,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::new(SkillKind::Aim));
|
||||
skills.push(Skill::new(SkillKind::speed()));
|
||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
||||
|
||||
if fl {
|
||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
||||
@@ -106,6 +109,10 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -144,7 +151,12 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
}
|
||||
|
||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let speed_rating = if mods.rx() {
|
||||
0.0
|
||||
} else {
|
||||
skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
};
|
||||
|
||||
let flashlight_rating = if let Some(skill) = skills.get_mut(2) {
|
||||
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
@@ -152,30 +164,61 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
0.0
|
||||
};
|
||||
|
||||
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
|
||||
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.n_circles = map.n_circles as usize;
|
||||
diff_attributes.n_spinners = map.n_spinners as usize;
|
||||
diff_attributes.stars = stars;
|
||||
diff_attributes.speed_strain = speed_rating;
|
||||
diff_attributes.aim_strain = aim_rating;
|
||||
diff_attributes.flashlight_strain = flashlight_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;
|
||||
|
||||
StarResult::Osu(diff_attributes)
|
||||
}
|
||||
|
||||
// TODO: Add flashlight strains
|
||||
/// 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 hit_window =
|
||||
super::difficulty_range_od(map_attributes.od).floor() / 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()
|
||||
};
|
||||
@@ -207,8 +250,15 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
)
|
||||
});
|
||||
|
||||
let mut aim = Skill::new(SkillKind::Aim);
|
||||
let mut speed = Skill::new(SkillKind::speed());
|
||||
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();
|
||||
@@ -222,11 +272,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
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;
|
||||
}
|
||||
|
||||
aim.process(&h);
|
||||
speed.process(&h);
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
||||
@@ -237,31 +292,43 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let h = DifficultyObject::new(&curr, &prev, prev_vals, prev_prev, 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);
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.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();
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let strains = aim
|
||||
.strain_peaks
|
||||
.into_iter()
|
||||
.zip(speed.strain_peaks.into_iter())
|
||||
.map(|(aim, speed)| aim + speed)
|
||||
.collect();
|
||||
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,
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
use crate::math_util;
|
||||
|
||||
use super::{DifficultyObject, SkillKind};
|
||||
use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
|
||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
||||
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
||||
|
||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const AIM_DECAY_WEIGHT: f32 = 0.9;
|
||||
|
||||
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.13;
|
||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
|
||||
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
|
||||
|
||||
pub(crate) struct Skill {
|
||||
current_strain: f32,
|
||||
current_section_peak: f32,
|
||||
curr_strain: f32,
|
||||
curr_section_peak: f32,
|
||||
|
||||
kind: SkillKind,
|
||||
pub(crate) strain_peaks: Vec<f32>,
|
||||
@@ -31,8 +20,8 @@ impl Skill {
|
||||
#[inline]
|
||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
||||
Self {
|
||||
current_strain: 1.0,
|
||||
current_section_peak: 1.0,
|
||||
curr_strain: 1.0,
|
||||
curr_section_peak: 0.0,
|
||||
|
||||
kind,
|
||||
strain_peaks: Vec::with_capacity(128),
|
||||
@@ -41,39 +30,33 @@ impl Skill {
|
||||
}
|
||||
}
|
||||
|
||||
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.current_section_peak);
|
||||
self.strain_peaks.push(self.curr_section_peak);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
||||
self.kind.pre_process();
|
||||
self.current_strain *= self.strain_decay(current.delta);
|
||||
self.current_strain += self.kind.strain_value_of(¤t) * self.skill_multiplier();
|
||||
|
||||
self.current_section_peak = self
|
||||
.current_section_peak
|
||||
.max(self.total_current_strain(current));
|
||||
|
||||
self.prev_time.replace(current.base.time);
|
||||
self.kind.post_process(current);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_current_strain(&self, current: &DifficultyObject) -> f32 {
|
||||
self.kind.total_current_strain(self.current_strain, current)
|
||||
// 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 {
|
||||
// TODO: Remove
|
||||
// for (i, strain) in self.strain_peaks.iter().enumerate() {
|
||||
// println!("[{}] {}", i, strain);
|
||||
// }
|
||||
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
let decay_weight = self.decay_weight();
|
||||
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;
|
||||
@@ -81,12 +64,9 @@ impl Skill {
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
for (i, strain) in self
|
||||
.strain_peaks
|
||||
.iter_mut()
|
||||
.take(reduced_section_count)
|
||||
.enumerate()
|
||||
{
|
||||
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);
|
||||
@@ -95,7 +75,7 @@ impl Skill {
|
||||
self.strain_peaks
|
||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||
|
||||
for &strain in self.strain_peaks.iter() {
|
||||
for &strain in &self.strain_peaks {
|
||||
difficulty += strain * weight;
|
||||
weight *= decay_weight;
|
||||
}
|
||||
@@ -103,40 +83,31 @@ impl Skill {
|
||||
difficulty * difficulty_multiplier
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn skill_multiplier(&self) -> f32 {
|
||||
match self.kind {
|
||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||
SkillKind::Speed { .. } => SPEED_SKILL_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,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay_base(&self) -> f32 {
|
||||
match self.kind {
|
||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decay_weight(&self) -> f32 {
|
||||
match self.kind {
|
||||
SkillKind::Aim => AIM_DECAY_WEIGHT,
|
||||
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
||||
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
||||
self.current_strain * self.strain_decay(delta_time)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strain_decay(&self, ms: f32) -> f32 {
|
||||
self.strain_decay_base().powf(ms / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,76 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::{collections::VecDeque, f32::consts::PI, iter};
|
||||
|
||||
use crate::parse::Pos2;
|
||||
use crate::{math_util, parse::Pos2};
|
||||
|
||||
use super::DifficultyObject;
|
||||
|
||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
||||
const SPEED_ANGLE_BONUS_BEGIN: f32 = 5.0 * std::f32::consts::FRAC_PI_6;
|
||||
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
|
||||
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
|
||||
|
||||
const MIN_SPEED_BONUS: f32 = 75.0;
|
||||
const MAX_SPEED_BONUS: f32 = 45.0;
|
||||
const SPEED_BALANCING_FACTOR: f32 = 40.0;
|
||||
|
||||
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
|
||||
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 FLASHLIGHT_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 FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
||||
const SPEED_HISTORY_LENGTH: usize = 32;
|
||||
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0;
|
||||
const SPEED_RHYTHM_MULTIPLIER: f32 = 0.75;
|
||||
|
||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
||||
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 SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
||||
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||
|
||||
const SPEED_HISTORY_TIME_MAX: f32 = 3000.0;
|
||||
const SPEED_RHYTHM_MULTIPLIER: f32 = 1.5;
|
||||
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
||||
|
||||
pub(crate) struct FlashlightHistoryEntry {
|
||||
is_spinner: bool,
|
||||
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 {
|
||||
@@ -49,7 +78,9 @@ pub(crate) enum SkillKind {
|
||||
scaling_factor: f32,
|
||||
},
|
||||
Speed {
|
||||
curr_rhythm: f32,
|
||||
history: VecDeque<SpeedHistoryEntry>,
|
||||
hit_window: f32,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -61,9 +92,11 @@ impl SkillKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn speed() -> Self {
|
||||
pub(crate) fn speed(hit_window: f32) -> Self {
|
||||
Self::Speed {
|
||||
curr_rhythm: 1.0,
|
||||
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH),
|
||||
hit_window,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,99 +104,92 @@ impl SkillKind {
|
||||
match self {
|
||||
Self::Aim => {}
|
||||
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
|
||||
Self::Speed { history } => history.truncate(SPEED_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, .. } => {
|
||||
let entry = FlashlightHistoryEntry {
|
||||
is_spinner: current.base.is_spinner(),
|
||||
end_pos: current.base.end_pos(),
|
||||
strain_time: current.strain_time,
|
||||
};
|
||||
|
||||
history.push_front(entry);
|
||||
}
|
||||
Self::Speed { history } => {
|
||||
let entry = SpeedHistoryEntry {
|
||||
is_slider: current.base.is_slider(),
|
||||
start_time: current.base.time,
|
||||
strain_time: current.strain_time,
|
||||
};
|
||||
|
||||
history.push_front(entry);
|
||||
}
|
||||
Self::Flashlight { history, .. } => history.push_front(current.into()),
|
||||
Self::Speed { history, .. } => history.push_front(current.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn strain_value_of(&self, current: &DifficultyObject) -> f32 {
|
||||
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject) -> f32 {
|
||||
match self {
|
||||
Self::Aim => {
|
||||
if current.base.is_spinner() {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut result = 0.0;
|
||||
let mut aim_strain = 0.0;
|
||||
|
||||
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
|
||||
if let Some(angle) = current.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
|
||||
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)
|
||||
* (current.jump_dist - scale).max(0.0))
|
||||
* (curr.jump_dist - scale).max(0.0))
|
||||
.sqrt();
|
||||
|
||||
result = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
|
||||
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(current.jump_dist);
|
||||
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
|
||||
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();
|
||||
|
||||
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
|
||||
.max(dist_exp / current.strain_time)
|
||||
let res = (aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
|
||||
.max(dist_exp / curr.strain_time);
|
||||
|
||||
res
|
||||
}
|
||||
Self::Flashlight {
|
||||
history,
|
||||
scaling_factor,
|
||||
} => {
|
||||
if current.base.is_spinner() {
|
||||
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 = history.iter();
|
||||
|
||||
if let Some(entry) = history.next() {
|
||||
if !entry.is_spinner {
|
||||
let jump_dist = (current.base.pos - entry.end_pos).length();
|
||||
cumulative_strain_time += entry.strain_time;
|
||||
if let Some(prev) = history.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
|
||||
if jump_dist < 50.0 {
|
||||
small_dist_nerf = jump_dist / 50.0;
|
||||
}
|
||||
small_dist_nerf = (jump_dist / 75.0).min(1.0);
|
||||
|
||||
result += scaling_factor * jump_dist / cumulative_strain_time;
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (i, entry) in (1..).zip(history) {
|
||||
if !entry.is_spinner {
|
||||
let jump_dist = (current.base.pos - entry.end_pos).length();
|
||||
cumulative_strain_time += entry.strain_time;
|
||||
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
|
||||
|
||||
result += 0.8_f32.powi(i) * scaling_factor * jump_dist
|
||||
for (factor, prev) in factors.zip(history) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -173,57 +199,45 @@ impl SkillKind {
|
||||
|
||||
result * result
|
||||
}
|
||||
Self::Speed { .. } => {
|
||||
if current.base.is_spinner() {
|
||||
Self::Speed {
|
||||
history,
|
||||
hit_window,
|
||||
..
|
||||
} => {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
|
||||
let delta_time = MAX_SPEED_BONUS.max(current.delta);
|
||||
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 delta_time < MIN_SPEED_BONUS {
|
||||
let exp_base = (MIN_SPEED_BONUS - delta_time) / SPEED_BALANCING_FACTOR;
|
||||
speed_bonus = 1.0 + exp_base * exp_base;
|
||||
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 mut angle_bonus = 1.0;
|
||||
let dist = SINGLE_SPACING_TRESHOLD.min(curr.travel_dist + curr.jump_dist);
|
||||
|
||||
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
|
||||
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
|
||||
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
|
||||
|
||||
if angle < PI_OVER_2 {
|
||||
angle_bonus = 1.28;
|
||||
|
||||
if dist < 90.0 && angle < PI_OVER_4 {
|
||||
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
|
||||
} else if dist < 90.0 {
|
||||
angle_bonus += (1.0 - angle_bonus)
|
||||
* ((90.0 - dist) / 10.0).min(1.0)
|
||||
* ((PI_OVER_2 - angle) / PI_OVER_4).sin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(1.0 + (speed_bonus - 1.0) * 0.75)
|
||||
* angle_bonus
|
||||
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
|
||||
/ current.strain_time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn total_current_strain(
|
||||
&self,
|
||||
current_strain: f32,
|
||||
current: &DifficultyObject,
|
||||
) -> f32 {
|
||||
match self {
|
||||
SkillKind::Aim | SkillKind::Flashlight { .. } => current_strain,
|
||||
SkillKind::Speed { history } => {
|
||||
current_strain * calculate_speed_rhythm_bonus(history, current.base.time)
|
||||
(speed_bonus + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
|
||||
/ strain_time
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,65 +253,143 @@ impl SkillKind {
|
||||
Self::Speed { .. } => (SPEED_REDUCED_SECTION_COUNT, SPEED_DIFFICULTY_MULTIPLIER),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_speed_rhythm_bonus(history: &VecDeque<SpeedHistoryEntry>, start_time: f32) -> f32 {
|
||||
let mut previous_island_size = usize::MAX;
|
||||
let mut island_times = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
let mut island_size = 0;
|
||||
let mut first_delta_switch = false;
|
||||
|
||||
for (prev, curr) in history.iter().skip(1).zip(history).rev() {
|
||||
let prev_delta = prev.strain_time;
|
||||
let curr_delta = curr.strain_time;
|
||||
|
||||
let mut effective_ratio = prev_delta.min(curr_delta) / prev_delta.max(curr_delta);
|
||||
|
||||
if effective_ratio > 0.5 {
|
||||
effective_ratio = 0.5 + (effective_ratio - 0.5) * 5.0;
|
||||
}
|
||||
|
||||
let curr_historical_decay = (SPEED_HISTORY_TIME_MAX - (start_time - curr.start_time))
|
||||
.max(0.0)
|
||||
/ SPEED_HISTORY_TIME_MAX;
|
||||
|
||||
if first_delta_switch {
|
||||
if is_ratio_equal(1.0, prev_delta, curr_delta) {
|
||||
island_size += 1;
|
||||
} else {
|
||||
if island_size > 6 {
|
||||
island_size = 6;
|
||||
}
|
||||
|
||||
if curr.is_slider {
|
||||
effective_ratio /= 2.0;
|
||||
}
|
||||
|
||||
if prev.is_slider {
|
||||
effective_ratio *= 0.75;
|
||||
}
|
||||
|
||||
if previous_island_size == island_size {
|
||||
effective_ratio /= 0.5;
|
||||
}
|
||||
|
||||
island_times[island_size] += effective_ratio * curr_historical_decay;
|
||||
previous_island_size = island_size;
|
||||
|
||||
if prev_delta * 1.25 < curr_delta {
|
||||
first_delta_switch = false;
|
||||
}
|
||||
|
||||
island_size = 6;
|
||||
}
|
||||
} else if prev_delta > 1.25 * curr_delta {
|
||||
first_delta_switch = true;
|
||||
island_size = 0;
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
||||
let rhythm_complexity_sum: f32 = island_times.iter().sum();
|
||||
#[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
|
||||
}
|
||||
|
||||
@@ -305,8 +397,3 @@ fn calculate_speed_rhythm_bonus(history: &VecDeque<SpeedHistoryEntry>, start_tim
|
||||
fn apply_diminishing_exp(val: f32) -> f32 {
|
||||
val.powf(0.99)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_ratio_equal(ratio: f32, a: f32, b: f32) -> bool {
|
||||
a + 15.0 > ratio * b && a - 15.0 < ratio * b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user