Port osu!std updates since f08134f
This commit is contained in:
@@ -299,6 +299,23 @@ impl<'map> Performance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the calculated attributes belong to an osu!lazer or osu!stable
|
||||
/// score.
|
||||
///
|
||||
/// Defaults to lazer.
|
||||
///
|
||||
/// This affects internal accuracy calculation because lazer considers
|
||||
/// slider heads for accuracy whereas stable does not.
|
||||
///
|
||||
/// Only relevant for osu!standard.
|
||||
pub fn lazer(self, lazer: bool) -> Self {
|
||||
if let Self::Osu(osu) = self {
|
||||
Self::Osu(osu.lazer(lazer))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify the amount of 300s of a play.
|
||||
pub fn n300(self, n300: u32) -> Self {
|
||||
match self {
|
||||
|
||||
@@ -479,6 +479,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for CatchPerformance<'map> {
|
||||
n50,
|
||||
misses,
|
||||
hitresult_priority: _,
|
||||
lazer: _,
|
||||
} = osu;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -832,6 +832,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for ManiaPerformance<'map> {
|
||||
n50,
|
||||
misses,
|
||||
hitresult_priority,
|
||||
lazer: _,
|
||||
} = osu;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
@@ -182,6 +182,26 @@ impl_has_mod! {
|
||||
tc: - Traceable ["Traceable"],
|
||||
}
|
||||
|
||||
impl GameMods {
|
||||
pub fn no_slider_head_acc(&self, lazer: bool) -> bool {
|
||||
match self.inner {
|
||||
GameModsInner::Lazer(ref mods) => mods
|
||||
.iter()
|
||||
.find_map(|m| match m {
|
||||
GameMod::ClassicOsu(classic) => Some(classic),
|
||||
_ => None,
|
||||
})
|
||||
.map_or(!lazer, |classic| {
|
||||
classic.no_slider_head_accuracy.unwrap_or(true)
|
||||
}),
|
||||
GameModsInner::Intermode(ref mods) => {
|
||||
mods.contains(GameModIntermode::Classic) || !lazer
|
||||
}
|
||||
GameModsInner::Legacy(_) => !lazer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GameMods {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
|
||||
@@ -13,6 +13,10 @@ pub struct OsuDifficultyAttributes {
|
||||
pub slider_factor: f64,
|
||||
/// The number of clickable objects weighted by difficulty.
|
||||
pub speed_note_count: f64,
|
||||
/// Weighted sum of aim strains.
|
||||
pub aim_difficult_strain_count: f64,
|
||||
/// Weighted sum of speed strains.
|
||||
pub speed_difficult_strain_count: f64,
|
||||
/// The approach rate.
|
||||
pub ar: f64,
|
||||
/// The overall difficulty
|
||||
|
||||
+10
-3
@@ -67,7 +67,7 @@ pub fn convert_objects(
|
||||
.iter_mut()
|
||||
.for_each(OsuObject::reflect_vertically);
|
||||
} else {
|
||||
osu_objects.iter_mut().for_each(OsuObject::finalize_tail);
|
||||
osu_objects.iter_mut().for_each(OsuObject::finalize_nested);
|
||||
}
|
||||
|
||||
let stack_threshold = time_preempt * f64::from(converted.stack_leniency);
|
||||
@@ -246,13 +246,20 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
break;
|
||||
}
|
||||
|
||||
// * Note the use of `StartTime` in the code below doesn't match stable's use of `EndTime`.
|
||||
// * This is because in the stable implementation, `UpdateCalculations` is not called on the inner-loop hitobject (j)
|
||||
// * and therefore it does not have a correct `EndTime`, but instead the default of `EndTime = StartTime`.
|
||||
// *
|
||||
// * Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||
// * if we use `EndTime` here it would result in unexpected stacking.
|
||||
|
||||
if hit_objects[j].pos.distance(hit_objects[i].pos) < STACK_DISTANCE {
|
||||
hit_objects[i].stack_height += 1;
|
||||
start_time = hit_objects[j].end_time();
|
||||
start_time = hit_objects[j].start_time;
|
||||
} else if hit_objects[j].pos.distance(pos2) < STACK_DISTANCE {
|
||||
slider_stack += 1;
|
||||
hit_objects[j].stack_height -= slider_stack;
|
||||
start_time = hit_objects[j].end_time();
|
||||
start_time = hit_objects[j].start_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::{cmp, pin::Pin};
|
||||
|
||||
use skills::{flashlight::Flashlight, strain::OsuStrainSkill};
|
||||
use skills::{
|
||||
flashlight::Flashlight,
|
||||
strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
any::difficulty::{skills::Skill, Difficulty},
|
||||
@@ -148,15 +151,16 @@ impl DifficultyValues {
|
||||
pub fn eval(
|
||||
attrs: &mut OsuDifficultyAttributes,
|
||||
mods: &GameMods,
|
||||
aim_difficulty_value: f64,
|
||||
aim_no_sliders_difficulty_value: f64,
|
||||
speed_difficulty_value: f64,
|
||||
aim: UsedOsuStrainSkills<DifficultyValue>,
|
||||
aim_no_sliders: UsedOsuStrainSkills<DifficultyValue>,
|
||||
speed: UsedOsuStrainSkills<DifficultyValue>,
|
||||
speed_relevant_note_count: f64,
|
||||
flashlight_difficulty_value: f64,
|
||||
) {
|
||||
let mut aim_rating = aim_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let aim_rating_no_sliders = aim_no_sliders_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let mut speed_rating = speed_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let mut aim_rating = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let aim_rating_no_sliders =
|
||||
aim_no_sliders.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let mut speed_rating = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
let mut flashlight_rating = flashlight_difficulty_value.sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let slider_factor = if aim_rating > 0.0 {
|
||||
@@ -165,6 +169,9 @@ impl DifficultyValues {
|
||||
1.0
|
||||
};
|
||||
|
||||
let aim_difficult_strain_count = aim.count_difficult_strains();
|
||||
let speed_difficult_strain_count = speed.count_difficult_strains();
|
||||
|
||||
if mods.td() {
|
||||
aim_rating = aim_rating.powf(0.8);
|
||||
flashlight_rating = flashlight_rating.powf(0.8);
|
||||
@@ -207,6 +214,8 @@ impl DifficultyValues {
|
||||
attrs.speed = speed_rating;
|
||||
attrs.flashlight = flashlight_rating;
|
||||
attrs.slider_factor = slider_factor;
|
||||
attrs.aim_difficult_strain_count = aim_difficult_strain_count;
|
||||
attrs.speed_difficult_strain_count = speed_difficult_strain_count;
|
||||
attrs.stars = star_rating;
|
||||
attrs.speed_note_count = speed_relevant_note_count;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub struct OsuDifficultyObject<'a> {
|
||||
impl<'a> OsuDifficultyObject<'a> {
|
||||
pub const NORMALIZED_RADIUS: f32 = 50.0;
|
||||
|
||||
const MIN_DELTA_TIME: f64 = 25.0;
|
||||
pub const MIN_DELTA_TIME: f64 = 25.0;
|
||||
const MAX_SLIDER_RADIUS: f32 = Self::NORMALIZED_RADIUS * 2.4;
|
||||
const ASSUMED_SLIDER_RADIUS: f32 = Self::NORMALIZED_RADIUS * 1.8;
|
||||
|
||||
@@ -86,6 +86,24 @@ impl<'a> OsuDifficultyObject<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_doubletapness(&self, next: Option<&Self>, hit_window: f64) -> f64 {
|
||||
let Some(next) = next else { return 0.0 };
|
||||
|
||||
let hit_window = if self.base.is_spinner() {
|
||||
0.0
|
||||
} else {
|
||||
hit_window
|
||||
};
|
||||
|
||||
let curr_delta_time = self.delta_time.max(1.0);
|
||||
let next_delta_time = next.delta_time.max(1.0);
|
||||
let delta_diff = (next_delta_time - curr_delta_time).abs();
|
||||
let speed_ratio = curr_delta_time / curr_delta_time.max(delta_diff);
|
||||
let window_ratio = (curr_delta_time / hit_window).min(1.0).powf(2.0);
|
||||
|
||||
1.0 - (speed_ratio).powf(1.0 - window_ratio)
|
||||
}
|
||||
|
||||
fn set_distances(
|
||||
&mut self,
|
||||
last_object: &OsuObject,
|
||||
|
||||
@@ -19,8 +19,8 @@ pub struct ScalingFactor {
|
||||
|
||||
impl ScalingFactor {
|
||||
pub fn new(cs: f64) -> Self {
|
||||
let scale =
|
||||
(1.0 - 0.7 * ((cs - 5.0) / 5.0)) as f32 / 2.0 * BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE;
|
||||
let scale = (f64::from(1.0_f32) - f64::from(0.7_f32) * ((cs - 5.0) / 5.0)) as f32 / 2.0
|
||||
* BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE;
|
||||
|
||||
let radius = f64::from(OsuObject::OBJECT_RADIUS * scale);
|
||||
let factor = OsuDifficultyObject::NORMALIZED_RADIUS / radius as f32;
|
||||
|
||||
@@ -9,9 +9,9 @@ use crate::{
|
||||
util::{float_ext::FloatExt, strains_vec::StrainsVec},
|
||||
};
|
||||
|
||||
use super::strain::OsuStrainSkill;
|
||||
use super::strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills};
|
||||
|
||||
const SKILL_MULTIPLIER: f64 = 24.963;
|
||||
const SKILL_MULTIPLIER: f64 = 25.18;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.15;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -31,20 +31,20 @@ impl Aim {
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||
self.inner.get_curr_strain_peaks()
|
||||
self.inner.get_curr_strain_peaks().strains()
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self) -> f64 {
|
||||
pub fn difficulty_value(self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
Self::static_difficulty_value(self.inner)
|
||||
}
|
||||
|
||||
/// Use [`difficulty_value`] instead whenever possible because
|
||||
/// [`as_difficulty_value`] clones internally.
|
||||
pub fn as_difficulty_value(&self) -> f64 {
|
||||
pub fn as_difficulty_value(&self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
Self::static_difficulty_value(self.inner.clone())
|
||||
}
|
||||
|
||||
fn static_difficulty_value(skill: OsuStrainSkill) -> f64 {
|
||||
fn static_difficulty_value(skill: OsuStrainSkill) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
skill.difficulty_value(
|
||||
OsuStrainSkill::REDUCED_SECTION_COUNT,
|
||||
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
||||
@@ -104,6 +104,7 @@ impl<'a> Skill<'a, Aim> {
|
||||
self.inner.curr_strain +=
|
||||
AimEvaluator::evaluate_diff_of(curr, self.diff_objects, self.inner.with_sliders)
|
||||
* SKILL_MULTIPLIER;
|
||||
self.inner.inner.object_strains.push(self.inner.curr_strain);
|
||||
|
||||
self.inner.curr_strain
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{cmp, f64::consts::PI};
|
||||
use std::{
|
||||
cmp,
|
||||
f64::consts::{E, PI},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
any::difficulty::{
|
||||
@@ -10,7 +13,7 @@ use crate::{
|
||||
GameMods,
|
||||
};
|
||||
|
||||
use super::strain::OsuStrainSkill;
|
||||
use super::strain::{DifficultyValue, OsuStrainSkill, UsedOsuStrainSkills};
|
||||
|
||||
const SKILL_MULTIPLIER: f64 = 1.430;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.3;
|
||||
@@ -21,7 +24,6 @@ const REDUCED_SECTION_COUNT: usize = 5;
|
||||
pub struct Speed {
|
||||
curr_strain: f64,
|
||||
curr_rhythm: f64,
|
||||
object_strains: Vec<f64>,
|
||||
hit_window: f64,
|
||||
has_autopilot_mod: bool,
|
||||
inner: OsuStrainSkill,
|
||||
@@ -32,8 +34,6 @@ impl Speed {
|
||||
Self {
|
||||
curr_strain: 0.0,
|
||||
curr_rhythm: 0.0,
|
||||
// mean=406.72 | median=307
|
||||
object_strains: Vec::with_capacity(256),
|
||||
hit_window,
|
||||
has_autopilot_mod: mods.ap(),
|
||||
inner: OsuStrainSkill::default(),
|
||||
@@ -41,20 +41,20 @@ impl Speed {
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||
self.inner.get_curr_strain_peaks()
|
||||
self.inner.get_curr_strain_peaks().strains()
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self) -> f64 {
|
||||
pub fn difficulty_value(self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
Self::static_difficulty_value(self.inner)
|
||||
}
|
||||
|
||||
/// Use [`difficulty_value`] instead whenever possible because
|
||||
/// [`as_difficulty_value`] clones internally.
|
||||
pub fn as_difficulty_value(&self) -> f64 {
|
||||
pub fn as_difficulty_value(&self) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
Self::static_difficulty_value(self.inner.clone())
|
||||
}
|
||||
|
||||
fn static_difficulty_value(skill: OsuStrainSkill) -> f64 {
|
||||
fn static_difficulty_value(skill: OsuStrainSkill) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
skill.difficulty_value(
|
||||
REDUCED_SECTION_COUNT,
|
||||
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
||||
@@ -63,13 +63,14 @@ impl Speed {
|
||||
}
|
||||
|
||||
pub fn relevant_note_count(&self) -> f64 {
|
||||
self.object_strains
|
||||
self.inner
|
||||
.object_strains
|
||||
.iter()
|
||||
.copied()
|
||||
.max_by(f64::total_cmp)
|
||||
.filter(|&n| n > 0.0)
|
||||
.map_or(0.0, |max_strain| {
|
||||
self.object_strains.iter().fold(0.0, |sum, strain| {
|
||||
self.inner.object_strains.iter().fold(0.0, |sum, strain| {
|
||||
sum + (1.0 + (-(strain / max_strain * 12.0 - 6.0)).exp()).recip()
|
||||
})
|
||||
})
|
||||
@@ -135,7 +136,7 @@ impl<'a> Skill<'a, Speed> {
|
||||
RhythmEvaluator::evaluate_diff_of(curr, self.diff_objects, self.inner.hit_window);
|
||||
|
||||
let total_strain = self.inner.curr_strain * self.inner.curr_rhythm;
|
||||
self.inner.object_strains.push(total_strain);
|
||||
self.inner.inner.object_strains.push(total_strain);
|
||||
|
||||
total_strain
|
||||
}
|
||||
@@ -146,7 +147,8 @@ struct SpeedEvaluator;
|
||||
impl SpeedEvaluator {
|
||||
const SINGLE_SPACING_THRESHOLD: f64 = 125.0; // 1.25 circlers distance between centers
|
||||
const MIN_SPEED_BONUS: f64 = 75.0; // ~200BPM
|
||||
const SPEED_BALANCING_FACTOR: f64 = 40.;
|
||||
const SPEED_BALANCING_FACTOR: f64 = 40.0;
|
||||
const DIST_MULTIPLIER: f64 = 0.94;
|
||||
|
||||
fn evaluate_diff_of<'a>(
|
||||
curr: &'a OsuDifficultyObject<'a>,
|
||||
@@ -164,17 +166,10 @@ impl SpeedEvaluator {
|
||||
let osu_next_obj = curr.next(0, diff_objects);
|
||||
|
||||
let mut strain_time = curr.strain_time;
|
||||
let mut doubletapness = 1.0;
|
||||
|
||||
// * Nerf doubletappable doubles.
|
||||
if let Some(osu_next_obj) = osu_next_obj {
|
||||
let curr_delta_time = osu_curr_obj.delta_time.max(1.0);
|
||||
let next_delta_time = osu_next_obj.delta_time.max(1.0);
|
||||
let delta_diff = (next_delta_time - curr_delta_time).abs();
|
||||
let speed_ratio = curr_delta_time / curr_delta_time.max(delta_diff);
|
||||
let window_ratio = (curr_delta_time / hit_window).min(1.0).powf(2.0);
|
||||
doubletapness = speed_ratio.powf(1.0 - window_ratio);
|
||||
}
|
||||
// Note: Technically `osu_next_obj` is never `None` but instead the
|
||||
// default value. This could maybe invalidate the `get_doubletapness`
|
||||
// result.
|
||||
let doubletapness = 1.0 - osu_curr_obj.get_doubletapness(osu_next_obj, hit_window);
|
||||
|
||||
// * Cap deltatime to the OD 300 hitwindow.
|
||||
// * 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||
@@ -184,10 +179,10 @@ impl SpeedEvaluator {
|
||||
// * Add additional scaling bonus for streams/bursts higher than 200bpm
|
||||
let base = (Self::MIN_SPEED_BONUS - strain_time) / Self::SPEED_BALANCING_FACTOR;
|
||||
|
||||
1.0 + 0.75 * base.powf(2.0)
|
||||
0.75 * base.powf(2.0)
|
||||
} else {
|
||||
// * speedBonus will be 1.0 for BPM < 200
|
||||
1.0
|
||||
// * speedBonus will be 0.0 for BPM < 200
|
||||
0.0
|
||||
};
|
||||
|
||||
let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.travel_dist);
|
||||
@@ -196,15 +191,15 @@ impl SpeedEvaluator {
|
||||
// * Cap distance at single_spacing_threshold
|
||||
dist = Self::SINGLE_SPACING_THRESHOLD.min(dist);
|
||||
|
||||
// * Max distance bonus is 2 at single_spacing_threshold
|
||||
// * Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
let dist_bonus = if has_autopilot_mod {
|
||||
1.0
|
||||
0.0
|
||||
} else {
|
||||
1.0 + (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5)
|
||||
(dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.95) * Self::DIST_MULTIPLIER
|
||||
};
|
||||
|
||||
// * Base difficulty with all bonuses
|
||||
let difficulty = speed_bonus * dist_bonus * 1000.0 / strain_time;
|
||||
let difficulty = (1.0 + speed_bonus + dist_bonus) * 1000.0 / strain_time;
|
||||
|
||||
// * Apply penalty if there's doubletappable doubles
|
||||
difficulty * doubletapness
|
||||
@@ -214,9 +209,10 @@ impl SpeedEvaluator {
|
||||
struct RhythmEvaluator;
|
||||
|
||||
impl RhythmEvaluator {
|
||||
// * 5 seconds of calculatingRhythmBonus max.
|
||||
const HISTORY_TIME_MAX: u32 = 5000;
|
||||
const RHYTHM_MULTIPLIER: f64 = 0.75;
|
||||
const HISTORY_TIME_MAX: u32 = 5 * 1000; // 5 seconds
|
||||
const HISTORY_OBJECTS_MAX: usize = 32;
|
||||
const RHYTHM_OVERALL_MULTIPLIER: f64 = 0.95;
|
||||
const RHYTHM_RATIO_MULTIPLIER: f64 = 12.0;
|
||||
|
||||
fn evaluate_diff_of<'a>(
|
||||
curr: &'a OsuDifficultyObject<'a>,
|
||||
@@ -227,16 +223,23 @@ impl RhythmEvaluator {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut prev_island_size = 0;
|
||||
|
||||
let mut rhythm_complexity_sum = 0.0;
|
||||
let mut island_size = 1;
|
||||
|
||||
let delta_difference_eps = hit_window * 0.3;
|
||||
|
||||
let mut island = RhythmIsland::new(delta_difference_eps);
|
||||
let mut prev_island = RhythmIsland::new(delta_difference_eps);
|
||||
|
||||
// * we can't use dictionary here because we need to compare island with a tolerance
|
||||
// * which is impossible to pass into the hash comparer
|
||||
let mut island_counts = Vec::<IslandCount>::new();
|
||||
|
||||
// * store the ratio of the current start of an island to buff for tighter rhythms
|
||||
let mut start_ratio = 0.0;
|
||||
|
||||
let mut first_delta_switch = false;
|
||||
|
||||
let historical_note_count = cmp::min(curr.idx, 32);
|
||||
let historical_note_count = cmp::min(curr.idx, Self::HISTORY_OBJECTS_MAX);
|
||||
|
||||
let mut rhythm_start = 0;
|
||||
|
||||
@@ -255,47 +258,50 @@ impl RhythmEvaluator {
|
||||
.previous(rhythm_start, diff_objects)
|
||||
.zip(curr.previous(rhythm_start + 1, diff_objects))
|
||||
{
|
||||
// * we go from the furthest object back to the current one
|
||||
for i in (1..=rhythm_start).rev() {
|
||||
let Some(curr_obj) = curr.previous(i - 1, diff_objects) else {
|
||||
break;
|
||||
};
|
||||
|
||||
// * scales note 0 to 1 from history to now
|
||||
let mut curr_historical_decay = (f64::from(Self::HISTORY_TIME_MAX)
|
||||
let time_decay = (f64::from(Self::HISTORY_TIME_MAX)
|
||||
- (curr.start_time - curr_obj.start_time))
|
||||
/ f64::from(Self::HISTORY_TIME_MAX);
|
||||
let note_decay = (historical_note_count - i) as f64 / historical_note_count as f64;
|
||||
|
||||
// * either we're limited by time or limited by object count.
|
||||
curr_historical_decay = curr_historical_decay
|
||||
.min((historical_note_count - i) as f64 / historical_note_count as f64);
|
||||
let curr_historical_decay = note_decay.min(time_decay);
|
||||
|
||||
let curr_delta = curr_obj.strain_time;
|
||||
let prev_delta = prev_obj.strain_time;
|
||||
let last_delta = last_obj.strain_time;
|
||||
|
||||
// * fancy function to calculate rhythmbonuses.
|
||||
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
||||
let curr_ratio = 1.0 + 6.0 * base.powf(2.0).min(0.5);
|
||||
// * calculate how much current delta difference deserves a rhythm bonus
|
||||
// * this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
|
||||
let delta_difference_ratio =
|
||||
prev_delta.min(curr_delta) / prev_delta.max(curr_delta);
|
||||
let curr_ratio = 1.0
|
||||
+ Self::RHYTHM_RATIO_MULTIPLIER
|
||||
* (PI / delta_difference_ratio).sin().powf(2.0).min(0.5);
|
||||
|
||||
let hit_window = u64::from(!curr_obj.base.is_spinner()) as f64 * hit_window;
|
||||
// reduce ratio bonus if delta difference is too big
|
||||
let fraction = (prev_delta / curr_delta).max(curr_delta / prev_delta);
|
||||
let fraction_multiplier = (2.0 - fraction / 8.0).clamp(0.0, 1.0);
|
||||
|
||||
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
||||
.max(0.0))
|
||||
/ (hit_window * 0.3))
|
||||
let window_penalty = (((prev_delta - curr_delta).abs() - delta_difference_eps)
|
||||
.max(0.0)
|
||||
/ delta_difference_eps)
|
||||
.min(1.0);
|
||||
|
||||
window_penalty = window_penalty.min(1.0);
|
||||
|
||||
let mut effective_ratio = window_penalty * curr_ratio;
|
||||
let mut effective_ratio = window_penalty * curr_ratio * fraction_multiplier;
|
||||
|
||||
if first_delta_switch {
|
||||
// Keep in-sync with lazer
|
||||
#[allow(clippy::if_not_else)]
|
||||
if !(prev_delta > 1.25 * curr_delta || prev_delta * 1.25 < curr_delta) {
|
||||
if island_size < 7 {
|
||||
// * island is still progressing, count size.
|
||||
island_size += 1;
|
||||
}
|
||||
if (prev_delta - curr_delta).abs() < delta_difference_eps {
|
||||
// * island is still progressing
|
||||
island.add_delta(curr_delta as i32);
|
||||
} else {
|
||||
// * bpm change is into slider, this is easy acc window
|
||||
if curr_obj.base.is_slider() {
|
||||
@@ -303,51 +309,86 @@ impl RhythmEvaluator {
|
||||
}
|
||||
|
||||
// * bpm change was from a slider, this is easier typically than circle -> circle
|
||||
// * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||
if prev_obj.base.is_slider() {
|
||||
effective_ratio *= 0.25;
|
||||
effective_ratio *= 0.3;
|
||||
}
|
||||
|
||||
// * repeated island size (ex: triplet -> triplet)
|
||||
if prev_island_size == island_size {
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
// * repeated island polartiy (2 -> 4, 3 -> 5)
|
||||
if prev_island_size % 2 == island_size % 2 {
|
||||
// * repeated island polarity (2 -> 4, 3 -> 5)
|
||||
if island.is_similar_polarity(&prev_island) {
|
||||
effective_ratio *= 0.5;
|
||||
}
|
||||
|
||||
// * previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||
if last_delta > prev_delta + 10.0 && prev_delta > curr_delta + 10.0 {
|
||||
if last_delta > prev_delta + delta_difference_eps
|
||||
&& prev_delta > curr_delta + delta_difference_eps
|
||||
{
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
||||
* curr_historical_decay
|
||||
* f64::from(4 + island_size).sqrt()
|
||||
/ 2.0
|
||||
* f64::from(4 + prev_island_size).sqrt()
|
||||
/ 2.0;
|
||||
// * repeated island size (ex: triplet -> triplet)
|
||||
// * TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
|
||||
if prev_island.delta_count == island.delta_count {
|
||||
effective_ratio *= 0.5;
|
||||
}
|
||||
|
||||
if let Some(island_count) = island_counts
|
||||
.iter_mut()
|
||||
.find(|entry| entry.island == island)
|
||||
.filter(|entry| !entry.island.is_default())
|
||||
{
|
||||
// * only add island to island counts if they're going one after another
|
||||
if prev_island == island {
|
||||
island_count.count += 1;
|
||||
}
|
||||
|
||||
// * repeated island (ex: triplet -> triplet)
|
||||
let power = logistic(f64::from(island.delta), 2.75, 0.24, 14.0);
|
||||
effective_ratio *= (3.0 / island_count.count as f64)
|
||||
.min((island_count.count as f64).recip().powf(power));
|
||||
} else {
|
||||
island_counts.push(IslandCount { island, count: 1 });
|
||||
}
|
||||
|
||||
// * scale down the difficulty if the object is doubletappable
|
||||
let doubletapness = prev_obj.get_doubletapness(Some(curr_obj), hit_window);
|
||||
effective_ratio *= 1.0 - doubletapness * 0.75;
|
||||
|
||||
rhythm_complexity_sum +=
|
||||
(effective_ratio * start_ratio).sqrt() * curr_historical_decay;
|
||||
|
||||
start_ratio = effective_ratio;
|
||||
|
||||
// * log the last island size.
|
||||
prev_island_size = island_size;
|
||||
prev_island = island;
|
||||
|
||||
// * 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.
|
||||
if prev_delta + delta_difference_eps < curr_delta {
|
||||
// * if we're speeding up, this stays true and we keep counting island size.
|
||||
first_delta_switch = false;
|
||||
}
|
||||
|
||||
island_size = 1;
|
||||
island =
|
||||
RhythmIsland::new_with_delta(curr_delta as i32, delta_difference_eps);
|
||||
}
|
||||
} else if prev_delta > 1.25 * curr_delta {
|
||||
// * we want to be speeding up.
|
||||
} else if prev_delta > curr_delta + delta_difference_eps {
|
||||
// * we're speeding up.
|
||||
// * Begin counting island until we change speed again.
|
||||
first_delta_switch = true;
|
||||
|
||||
// * bpm change is into slider, this is easy acc window
|
||||
if curr_obj.base.is_slider() {
|
||||
effective_ratio *= 0.6;
|
||||
}
|
||||
|
||||
// * bpm change was from a slider, this is easier typically than circle -> circle
|
||||
// * unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
|
||||
if prev_obj.base.is_slider() {
|
||||
effective_ratio *= 0.6;
|
||||
}
|
||||
|
||||
start_ratio = effective_ratio;
|
||||
island_size = 1;
|
||||
|
||||
island = RhythmIsland::new_with_delta(curr_delta as i32, delta_difference_eps);
|
||||
}
|
||||
|
||||
last_obj = prev_obj;
|
||||
@@ -356,6 +397,74 @@ impl RhythmEvaluator {
|
||||
}
|
||||
|
||||
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
(4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||
(4.0 + rhythm_complexity_sum * Self::RHYTHM_OVERALL_MULTIPLIER).sqrt() / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
fn logistic(x: f64, max_value: f64, multiplier: f64, offset: f64) -> f64 {
|
||||
max_value / (1.0 + E.powf(offset - (multiplier * x)))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct RhythmIsland {
|
||||
delta_difference_eps: f64,
|
||||
delta: i32,
|
||||
delta_count: i32,
|
||||
}
|
||||
|
||||
const MIN_DELTA_TIME: i32 = 25;
|
||||
|
||||
// Compile-time check in case `OsuDifficultyObject::MIN_DELTA_TIME` changes
|
||||
// but we forget to update this value.
|
||||
const _: [(); 0 - !{ MIN_DELTA_TIME - OsuDifficultyObject::MIN_DELTA_TIME as i32 == 0 } as usize] =
|
||||
[];
|
||||
|
||||
impl RhythmIsland {
|
||||
fn new(delta_difference_eps: f64) -> Self {
|
||||
Self {
|
||||
delta_difference_eps,
|
||||
delta: 0,
|
||||
delta_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_with_delta(delta: i32, delta_difference_eps: f64) -> Self {
|
||||
Self {
|
||||
delta_difference_eps,
|
||||
delta: delta.max(MIN_DELTA_TIME),
|
||||
delta_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_delta(&mut self, delta: i32) {
|
||||
if self.delta == i32::MAX {
|
||||
self.delta = delta.max(MIN_DELTA_TIME);
|
||||
}
|
||||
|
||||
self.delta_count += 1;
|
||||
}
|
||||
|
||||
fn is_similar_polarity(&self, other: &Self) -> bool {
|
||||
// * TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
|
||||
// * naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
|
||||
self.delta_count % 2 == other.delta_count % 2
|
||||
}
|
||||
|
||||
fn is_default(&self) -> bool {
|
||||
self.delta_difference_eps.abs() < f64::EPSILON
|
||||
&& self.delta == i32::MAX
|
||||
&& self.delta_count == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for RhythmIsland {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
f64::from((self.delta - other.delta).abs()) < self.delta_difference_eps
|
||||
&& self.delta_count == other.delta_count
|
||||
}
|
||||
}
|
||||
|
||||
struct IslandCount {
|
||||
island: RhythmIsland,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
use crate::{any::difficulty::skills::StrainSkill, util::strains_vec::StrainsVec};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone)]
|
||||
pub struct OsuStrainSkill {
|
||||
pub object_strains: Vec<f64>,
|
||||
pub inner: StrainSkill,
|
||||
}
|
||||
|
||||
impl Default for OsuStrainSkill {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// mean=406.72 | median=307
|
||||
object_strains: Vec::with_capacity(256),
|
||||
inner: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OsuStrainSkill {
|
||||
pub const REDUCED_SECTION_COUNT: usize = 10;
|
||||
pub const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||
@@ -20,8 +31,11 @@ impl OsuStrainSkill {
|
||||
self.inner.start_new_section_from(initial_strain);
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> StrainsVec {
|
||||
self.inner.get_curr_strain_peaks()
|
||||
pub fn get_curr_strain_peaks(self) -> UsedOsuStrainSkills<StrainsVec> {
|
||||
UsedOsuStrainSkills {
|
||||
value: self.inner.get_curr_strain_peaks(),
|
||||
object_strains: self.object_strains,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn difficulty_value(
|
||||
@@ -29,11 +43,14 @@ impl OsuStrainSkill {
|
||||
reduced_section_count: usize,
|
||||
reduced_strain_baseline: f64,
|
||||
decay_weight: f64,
|
||||
) -> f64 {
|
||||
) -> UsedOsuStrainSkills<DifficultyValue> {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
let mut peaks = self.get_curr_strain_peaks();
|
||||
let UsedOsuStrainSkills {
|
||||
value: mut peaks,
|
||||
object_strains,
|
||||
} = self.get_curr_strain_peaks();
|
||||
|
||||
let peaks_iter = peaks.sorted_non_zero_iter_mut().take(reduced_section_count);
|
||||
|
||||
@@ -50,7 +67,10 @@ impl OsuStrainSkill {
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty
|
||||
UsedOsuStrainSkills {
|
||||
value: DifficultyValue(difficulty),
|
||||
object_strains,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
|
||||
@@ -61,3 +81,39 @@ impl OsuStrainSkill {
|
||||
fn lerp(start: f64, end: f64, amount: f64) -> f64 {
|
||||
start + (end - start) * amount
|
||||
}
|
||||
|
||||
pub struct DifficultyValue(f64);
|
||||
|
||||
pub struct UsedOsuStrainSkills<T> {
|
||||
value: T,
|
||||
object_strains: Vec<f64>,
|
||||
}
|
||||
|
||||
impl UsedOsuStrainSkills<DifficultyValue> {
|
||||
pub fn difficulty_value(&self) -> f64 {
|
||||
self.value.0
|
||||
}
|
||||
|
||||
pub fn count_difficult_strains(&self) -> f64 {
|
||||
let DifficultyValue(diff) = self.value;
|
||||
|
||||
if diff.abs() < f64::EPSILON {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// * What would the top strain be if all strain values were identical
|
||||
let consistent_top_strain = diff / 10.0;
|
||||
|
||||
// * Use a weighted sum of all strains. Constants are arbitrary and give nice values
|
||||
self.object_strains
|
||||
.iter()
|
||||
.map(|s| 1.1 / (1.0 + (-10.0 * (s / consistent_top_strain - 0.88)).exp()))
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl UsedOsuStrainSkills<StrainsVec> {
|
||||
pub fn strains(self) -> StrainsVec {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
+9
-54
@@ -66,53 +66,21 @@ impl OsuObject {
|
||||
reflect_y(&mut self.pos.y);
|
||||
|
||||
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
|
||||
let repeat_count = slider.repeat_count();
|
||||
|
||||
// Requires `stack_offset` so we can't add `h.pos` just yet
|
||||
slider.lazy_end_pos.y = -slider.lazy_end_pos.y;
|
||||
|
||||
let mut nested_iter = slider.nested_objects.iter_mut();
|
||||
|
||||
// Since the tail is handled differently but it's not necessarily
|
||||
// the last object, we first search for it, and then handle the
|
||||
// other nested objects
|
||||
for nested in nested_iter.by_ref().rev() {
|
||||
if let NestedSliderObjectKind::Tail = nested.kind {
|
||||
let mut tail_pos = self.pos; // already reflected at this point
|
||||
tail_pos += Pos::new(nested.pos.x, -nested.pos.y);
|
||||
nested.pos = tail_pos;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
reflect_y(&mut nested.pos.y);
|
||||
}
|
||||
|
||||
// Same for the last repeat point
|
||||
for nested in nested_iter.by_ref().rev() {
|
||||
if let NestedSliderObjectKind::Repeat = nested.kind {
|
||||
nested.pos = if repeat_count % 2 == 0 {
|
||||
self.pos
|
||||
} else {
|
||||
self.pos + Pos::new(slider.path_end_pos.x, -slider.path_end_pos.y)
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
reflect_y(&mut nested.pos.y);
|
||||
}
|
||||
|
||||
for nested in nested_iter {
|
||||
reflect_y(&mut nested.pos.y);
|
||||
for nested in slider.nested_objects.iter_mut() {
|
||||
let mut nested_pos = self.pos; // already reflected at this point
|
||||
nested_pos += Pos::new(nested.pos.x, -nested.pos.y);
|
||||
nested.pos = nested_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize_tail(&mut self) {
|
||||
pub fn finalize_nested(&mut self) {
|
||||
if let OsuObjectKind::Slider(ref mut slider) = self.kind {
|
||||
if let Some(tail) = slider.tail_mut() {
|
||||
tail.pos += self.pos;
|
||||
for nested in slider.nested_objects.iter_mut() {
|
||||
nested.pos += self.pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,9 +141,6 @@ pub struct OsuSlider {
|
||||
pub lazy_end_pos: Pos,
|
||||
pub lazy_travel_dist: f32,
|
||||
pub lazy_travel_time: f64,
|
||||
// Very annoyingly, this position might be needed solely to update the last
|
||||
// repeat point's position on HR.
|
||||
pub path_end_pos: Pos,
|
||||
pub nested_objects: Vec<NestedSliderObject>,
|
||||
}
|
||||
|
||||
@@ -256,12 +221,12 @@ impl OsuSlider {
|
||||
.filter_map(|e| {
|
||||
let obj = match e.kind {
|
||||
SliderEventType::Tick => NestedSliderObject {
|
||||
pos: h.pos + path.position_at(e.path_progress),
|
||||
pos: path.position_at(e.path_progress),
|
||||
start_time: e.time,
|
||||
kind: NestedSliderObjectKind::Tick,
|
||||
},
|
||||
SliderEventType::Repeat => NestedSliderObject {
|
||||
pos: h.pos + path.position_at(e.path_progress),
|
||||
pos: path.position_at(e.path_progress),
|
||||
start_time: start_time + f64::from(e.span_idx + 1) * span_duration,
|
||||
kind: NestedSliderObjectKind::Repeat,
|
||||
},
|
||||
@@ -293,14 +258,12 @@ impl OsuSlider {
|
||||
}
|
||||
|
||||
let lazy_end_pos = path.position_at(end_time_min);
|
||||
let path_end_pos = path.position_at(1.0);
|
||||
|
||||
Self {
|
||||
end_time,
|
||||
lazy_end_pos,
|
||||
lazy_travel_dist: 0.0,
|
||||
lazy_travel_time,
|
||||
path_end_pos,
|
||||
nested_objects,
|
||||
}
|
||||
}
|
||||
@@ -352,14 +315,6 @@ impl OsuSlider {
|
||||
// short and fast buzz sliders (/b/1001757)
|
||||
.rfind(|nested| matches!(nested.kind, NestedSliderObjectKind::Tail))
|
||||
}
|
||||
|
||||
fn tail_mut(&mut self) -> Option<&mut NestedSliderObject> {
|
||||
self.nested_objects
|
||||
.iter_mut()
|
||||
// The tail is not necessarily the last nested object, e.g. on very
|
||||
// short and fast buzz sliders (/b/1001757)
|
||||
.rfind(|nested| matches!(nested.kind, NestedSliderObjectKind::Tail))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
+44
-19
@@ -33,6 +33,7 @@ pub struct OsuPerformance<'map> {
|
||||
pub(crate) n50: Option<u32>,
|
||||
pub(crate) misses: Option<u32>,
|
||||
pub(crate) hitresult_priority: HitResultPriority,
|
||||
pub(crate) lazer: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'map> OsuPerformance<'map> {
|
||||
@@ -152,6 +153,19 @@ impl<'map> OsuPerformance<'map> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether the calculated attributes belong to an osu!lazer or osu!stable
|
||||
/// score.
|
||||
///
|
||||
/// Defaults to lazer.
|
||||
///
|
||||
/// This affects internal accuracy calculation because lazer considers
|
||||
/// slider heads for accuracy whereas stable does not.
|
||||
pub const fn lazer(mut self, lazer: bool) -> Self {
|
||||
self.lazer = Some(lazer);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the amount of 300s of a play.
|
||||
pub const fn n300(mut self, n300: u32) -> Self {
|
||||
self.n300 = Some(n300);
|
||||
@@ -509,6 +523,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
acc: state.accuracy(),
|
||||
state,
|
||||
effective_miss_count,
|
||||
lazer: self.lazer.unwrap_or(true),
|
||||
};
|
||||
|
||||
inner.calculate()
|
||||
@@ -525,6 +540,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
n50: None,
|
||||
misses: None,
|
||||
hitresult_priority: HitResultPriority::DEFAULT,
|
||||
lazer: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,7 +551,8 @@ impl<'map, T: IntoModePerformance<'map, Osu>> From<T> for OsuPerformance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
|
||||
// * This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.15;
|
||||
|
||||
struct OsuPerformanceInner<'mods> {
|
||||
attrs: OsuDifficultyAttributes,
|
||||
@@ -543,10 +560,13 @@ struct OsuPerformanceInner<'mods> {
|
||||
acc: f64,
|
||||
state: OsuScoreState,
|
||||
effective_miss_count: f64,
|
||||
lazer: bool,
|
||||
}
|
||||
|
||||
impl OsuPerformanceInner<'_> {
|
||||
fn calculate(mut self) -> OsuPerformanceAttributes {
|
||||
let using_classic_slider_acc = self.mods.no_slider_head_acc(self.lazer);
|
||||
|
||||
let total_hits = self.state.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
@@ -592,7 +612,7 @@ impl OsuPerformanceInner<'_> {
|
||||
|
||||
let aim_value = self.compute_aim_value();
|
||||
let speed_value = self.compute_speed_value();
|
||||
let acc_value = self.compute_accuracy_value();
|
||||
let acc_value = self.compute_accuracy_value(using_classic_slider_acc);
|
||||
let flashlight_value = self.compute_flashlight_value();
|
||||
|
||||
let pp = (aim_value.powf(1.1)
|
||||
@@ -628,16 +648,13 @@ impl OsuPerformanceInner<'_> {
|
||||
|
||||
aim_value *= len_bonus;
|
||||
|
||||
// * Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// * Default a 3% reduction for any # of misses.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
aim_value *= 0.97
|
||||
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
|
||||
.powf(self.effective_miss_count);
|
||||
aim_value *= self.calculate_miss_penalty(
|
||||
self.effective_miss_count,
|
||||
self.attrs.aim_difficult_strain_count,
|
||||
);
|
||||
}
|
||||
|
||||
aim_value *= self.get_combo_scaling_factor();
|
||||
|
||||
let ar_factor = if self.mods.rx() {
|
||||
0.0
|
||||
} else if self.attrs.ar > 10.33 {
|
||||
@@ -700,16 +717,13 @@ impl OsuPerformanceInner<'_> {
|
||||
|
||||
speed_value *= len_bonus;
|
||||
|
||||
// * Penalize misses by assessing # of misses relative to the total # of objects.
|
||||
// * Default a 3% reduction for any # of misses.
|
||||
if self.effective_miss_count > 0.0 {
|
||||
speed_value *= 0.97
|
||||
* (1.0 - (self.effective_miss_count / total_hits).powf(0.775))
|
||||
.powf(self.effective_miss_count.powf(0.875));
|
||||
speed_value *= self.calculate_miss_penalty(
|
||||
self.effective_miss_count,
|
||||
self.attrs.speed_difficult_strain_count,
|
||||
);
|
||||
}
|
||||
|
||||
speed_value *= self.get_combo_scaling_factor();
|
||||
|
||||
let ar_factor = if self.mods.ap() {
|
||||
0.0
|
||||
} else if self.attrs.ar > 10.33 {
|
||||
@@ -750,7 +764,7 @@ impl OsuPerformanceInner<'_> {
|
||||
|
||||
// * Scale the speed value with accuracy and OD.
|
||||
speed_value *= (0.95 + self.attrs.od * self.attrs.od / 750.0)
|
||||
* ((self.acc + relevant_acc) / 2.0).powf((14.5 - (self.attrs.od).max(8.0)) / 2.0);
|
||||
* ((self.acc + relevant_acc) / 2.0).powf((14.5 - self.attrs.od) / 2.0);
|
||||
|
||||
// * Scale the speed value with # of 50s to punish doubletapping.
|
||||
speed_value *= 0.99_f64.powf(
|
||||
@@ -761,14 +775,18 @@ impl OsuPerformanceInner<'_> {
|
||||
speed_value
|
||||
}
|
||||
|
||||
fn compute_accuracy_value(&self) -> f64 {
|
||||
fn compute_accuracy_value(&self, using_classic_slider_acc: bool) -> f64 {
|
||||
if self.mods.rx() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// * This percentage only considers HitCircles of any value - in this part
|
||||
// * of the calculation we focus on hitting the timing hit window.
|
||||
let amount_hit_objects_with_acc = self.attrs.n_circles;
|
||||
let mut amount_hit_objects_with_acc = self.attrs.n_circles;
|
||||
|
||||
if using_classic_slider_acc {
|
||||
amount_hit_objects_with_acc += self.attrs.n_sliders;
|
||||
}
|
||||
|
||||
let better_acc_percentage = if amount_hit_objects_with_acc > 0 {
|
||||
let sub = self.state.total_hits() - amount_hit_objects_with_acc;
|
||||
@@ -842,6 +860,13 @@ impl OsuPerformanceInner<'_> {
|
||||
flashlight_value
|
||||
}
|
||||
|
||||
// * Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||
// * so we use the amount of relatively difficult sections to adjust miss penalty
|
||||
// * to make it more punishing on maps with lower amount of hard sections.
|
||||
fn calculate_miss_penalty(&self, miss_count: f64, diff_strain_count: f64) -> f64 {
|
||||
0.96 / ((miss_count / (4.0 * diff_strain_count.ln().powf(0.94))) + 1.0)
|
||||
}
|
||||
|
||||
fn get_combo_scaling_factor(&self) -> f64 {
|
||||
if self.attrs.max_combo == 0 {
|
||||
1.0
|
||||
|
||||
@@ -369,6 +369,7 @@ impl<'map> TryFrom<OsuPerformance<'map>> for TaikoPerformance<'map> {
|
||||
n50: _,
|
||||
misses,
|
||||
hitresult_priority,
|
||||
lazer: _,
|
||||
} = osu;
|
||||
|
||||
Ok(Self {
|
||||
|
||||
Reference in New Issue
Block a user