osu: applied update to all_included and no_sliders_no_leniency

This commit is contained in:
MaxOhn
2021-10-30 22:10:24 +02:00
parent d8f7608ad7
commit 62d00d3619
12 changed files with 1071 additions and 327 deletions
@@ -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; // stacked position
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,
+126 -38
View File
@@ -4,6 +4,8 @@
#![cfg(feature = "all_included")]
use std::mem;
use super::super::DifficultyAttributes;
use crate::parse::Pos2;
@@ -39,11 +41,13 @@ 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).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()
};
@@ -112,8 +116,15 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
h
});
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();
@@ -127,11 +138,16 @@ 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;
}
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));
@@ -142,35 +158,77 @@ 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 {
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);
for skill in skills.iter_mut() {
skill.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 aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
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.n_circles = map.n_circles as usize;
diff_attributes.n_spinners = map.n_spinners as usize;
diff_attributes.stars = stars;
diff_attributes.n_sliders = map.n_sliders as usize;
diff_attributes.stars = star_rating;
diff_attributes.speed_strain = speed_rating;
diff_attributes.aim_strain = aim_rating;
diff_attributes.flashlight_rating = flashlight_rating;
StarResult::Osu(diff_attributes)
}
@@ -181,11 +239,13 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
/// 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()
};
@@ -254,8 +314,15 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
h
});
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();
@@ -269,11 +336,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));
@@ -284,31 +356,47 @@ 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);
for skill in skills.iter_mut() {
skill.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,
+43 -49
View File
@@ -1,21 +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 AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const DECAY_WEIGHT: f32 = 0.9;
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>,
@@ -27,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),
@@ -37,27 +30,29 @@ impl Skill {
}
}
#[inline]
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.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.time);
// 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;
@@ -65,12 +60,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);
@@ -81,35 +73,37 @@ impl Skill {
for &strain in self.strain_peaks.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
weight *= decay_weight;
}
difficulty * difficulty_multiplier
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_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::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 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)
}
}
+329 -46
View File
@@ -1,100 +1,241 @@
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 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 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 AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
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,
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum SkillKind {
Aim,
Speed,
Flashlight {
history: VecDeque<FlashlightHistoryEntry>,
scaling_factor: f32,
},
Speed {
curr_rhythm: f32,
history: VecDeque<SpeedHistoryEntry>,
hit_window: f32,
},
}
impl SkillKind {
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
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 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)
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / curr.strain_time)
}
Self::Speed => {
if current.base.is_spinner() {
Self::Flashlight {
history,
scaling_factor,
} => {
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 small_dist_nerf = 1.0;
let mut result = 0.0;
let mut cumulative_strain_time = 0.0;
let mut history = history.iter();
let mut speed_bonus = 1.0;
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;
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;
}
// 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);
let mut angle_bonus = 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);
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;
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
}
if angle < PI_OVER_2 {
angle_bonus = 1.28;
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
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();
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;
}
}
}
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.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
}
}
}
@@ -103,9 +244,151 @@ impl SkillKind {
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
match self {
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
Self::Speed => (SPEED_REDUCED_SECTION_COUNT, SPEED_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]
+2 -4
View File
@@ -158,11 +158,9 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
};
let flashlight_rating = if let Some(skill) = skills.get_mut(2) {
let flashlight_rating = skills.get_mut(2).map_or(0.0, |skill| {
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
} else {
0.0
};
});
let base_aim_performance = {
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
-5
View File
@@ -49,11 +49,6 @@ impl Skill {
}
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.kind.decay_weight();
+2 -4
View File
@@ -145,10 +145,8 @@ impl SkillKind {
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
let res = (aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / curr.strain_time);
res
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / curr.strain_time)
}
Self::Flashlight {
history,
@@ -20,24 +20,30 @@ impl<'h> DifficultyObject<'h> {
scaling_factor: f32,
) -> Self {
let delta = base.time - prev.time;
let strain_time = delta.max(50.0);
let jump_dist = if base.is_spinner {
0.0
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
let strain_time = delta.max(25.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 {
((base.pos - prev.pos) * scaling_factor).length()
let jump_dist = ((base.pos - prev.pos) * scaling_factor).length();
let angle = prev_prev.map(|prev_prev| {
let v1 = prev_prev.pos - prev.pos;
let v2 = base.pos - prev.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 v1 = prev_prev.pos - prev.pos;
let v2 = base.pos - prev.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,
+132 -50
View File
@@ -4,6 +4,8 @@
#![cfg(feature = "no_sliders_no_leniency")]
use std::mem;
use super::super::DifficultyAttributes;
mod difficulty_object;
@@ -36,12 +38,13 @@ 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 attributes = map.attributes().mods(mods);
let hitwindow = super::difficulty_range(attributes.od).floor() / attributes.clock_rate;
let od = (80.0 - hitwindow) / 6.0;
let hit_window = super::difficulty_range_od(attributes.od).floor() / attributes.clock_rate;
let od = (80.0 - hit_window) / 6.0;
if take < 2 {
return StarResult::Osu(DifficultyAttributes {
ar: attributes.ar,
hp: attributes.hp,
od,
..Default::default()
});
@@ -68,25 +71,32 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
HitObjectKind::Circle => {
max_combo += 1;
Some(OsuObject::new(h.pos, h.start_time, false, clock_rate))
OsuObject::from(h, clock_rate)
}
HitObjectKind::Slider {
pixel_len, repeats, ..
} => {
max_combo += state.count_ticks(h.start_time, *pixel_len, *repeats, &map);
Some(OsuObject::new(h.pos, h.start_time, false, clock_rate))
OsuObject::from(h, clock_rate)
}
HitObjectKind::Spinner { .. } => {
max_combo += 1;
Some(OsuObject::new(h.pos, h.start_time, true, clock_rate))
OsuObject::from(h, clock_rate)
}
HitObjectKind::Hold { .. } => None,
});
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();
@@ -100,11 +110,16 @@ 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;
}
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));
@@ -115,39 +130,82 @@ 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 {
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);
for skill in skills.iter_mut() {
skill.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 aim_strain = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_strain = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let stars = aim_strain + speed_strain + (aim_strain - speed_strain).abs() / 2.0;
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
};
StarResult::Osu(DifficultyAttributes {
stars,
stars: star_rating,
ar: attributes.ar,
hp: attributes.hp,
od,
speed_strain,
aim_strain,
speed_strain: speed_rating,
aim_strain: aim_rating,
flashlight_rating,
max_combo,
n_circles: map.n_circles as usize,
n_spinners: map.n_spinners as usize,
n_sliders: map.n_sliders as usize,
})
}
@@ -157,6 +215,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
/// 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);
let hit_window = super::difficulty_range_od(attributes.od).floor() / attributes.clock_rate;
if map.hit_objects.len() < 2 {
return Strains::default();
@@ -172,18 +231,20 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
let clock_rate = attributes.clock_rate;
let mut hit_objects = map.hit_objects.iter().filter_map(|h| match &h.kind {
HitObjectKind::Circle | HitObjectKind::Slider { .. } => {
Some(OsuObject::new(h.pos, h.start_time, false, clock_rate))
}
HitObjectKind::Spinner { .. } => {
Some(OsuObject::new(h.pos, h.start_time, true, clock_rate))
}
HitObjectKind::Hold { .. } => None,
});
let mut hit_objects = map
.hit_objects
.iter()
.filter_map(|h| OsuObject::from(h, clock_rate));
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();
@@ -197,11 +258,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));
@@ -212,31 +278,47 @@ 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);
for skill in skills.iter_mut() {
skill.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,18 +1,35 @@
use crate::parse::Pos2;
use crate::parse::{HitObject, HitObjectKind, Pos2};
pub(crate) struct OsuObject {
pub(crate) pos: Pos2,
pub(crate) time: f32,
pub(crate) is_spinner: bool,
pub(crate) is_slider: bool,
}
impl OsuObject {
#[inline]
pub(crate) fn new(pos: Pos2, time: f32, is_spinner: bool, clock_rate: f32) -> Self {
Self {
pos,
time: time / clock_rate,
is_spinner,
pub(crate) fn from(h: &HitObject, clock_rate: f32) -> Option<Self> {
match &h.kind {
HitObjectKind::Circle => Some(Self {
pos: h.pos,
time: h.start_time / clock_rate,
is_spinner: false,
is_slider: false,
}),
HitObjectKind::Slider { .. } => Some(Self {
pos: h.pos,
time: h.start_time / clock_rate,
is_spinner: false,
is_slider: true,
}),
HitObjectKind::Spinner { .. } => Some(Self {
pos: h.pos,
time: h.start_time / clock_rate,
is_spinner: true,
is_slider: false,
}),
HitObjectKind::Hold { .. } => None,
}
}
}
@@ -1,21 +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 AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const DECAY_WEIGHT: f32 = 0.9;
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>,
@@ -27,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),
@@ -37,27 +30,29 @@ impl Skill {
}
}
#[inline]
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.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.time);
// 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;
@@ -65,12 +60,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);
@@ -79,37 +71,39 @@ 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;
weight *= decay_weight;
}
difficulty * difficulty_multiplier
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_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::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 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,96 +1,237 @@
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 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 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 AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
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.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,
}
}
}
#[derive(Copy, Clone)]
pub(crate) enum SkillKind {
Aim,
Speed,
Flashlight {
history: VecDeque<FlashlightHistoryEntry>,
scaling_factor: f32,
},
Speed {
curr_rhythm: f32,
history: VecDeque<SpeedHistoryEntry>,
hit_window: f32,
},
}
impl SkillKind {
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
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 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 jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
(result + jump_dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(jump_dist_exp / current.strain_time)
(aim_strain + jump_dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
.max(jump_dist_exp / curr.strain_time)
}
Self::Speed => {
if current.base.is_spinner {
Self::Flashlight {
history,
scaling_factor,
} => {
if curr.base.is_spinner {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut small_dist_nerf = 1.0;
let mut result = 0.0;
let mut cumulative_strain_time = 0.0;
let mut history = history.iter();
let mut speed_bonus = 1.0;
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;
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;
}
// 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);
let mut angle_bonus = 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);
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;
result += stack_nerf * scaling_factor * jump_dist / cumulative_strain_time;
}
if angle < PI_OVER_2 {
angle_bonus = 1.28;
let factors = iter::successors(Some(0.8), |s| Some(s * 0.8));
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();
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;
}
}
}
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.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.jump_dist);
(speed_bonus + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ strain_time
}
}
}
@@ -99,9 +240,151 @@ impl SkillKind {
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
match self {
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
Self::Speed => (SPEED_REDUCED_SECTION_COUNT, SPEED_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]