feat: updated osu!standard difficulate calculation
This commit is contained in:
+2
-1
@@ -16,5 +16,6 @@ sync = []
|
||||
tracing = ["rosu-map/tracing"]
|
||||
|
||||
[dependencies]
|
||||
rosu-map = { version = "0.1.1" }
|
||||
# rosu-map = { version = "0.1.2" }
|
||||
rosu-map = { git = "https://github.com/MaxOhn/rosu-map", branch = "pp-update" }
|
||||
rosu-mods = { version = "0.1.0" }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::vec::Drain;
|
||||
|
||||
use rosu_map::section::hit_objects::{
|
||||
CurveBuffers, PathControlPoint, SliderEvent, SliderEventType, SliderEventsIter,
|
||||
use rosu_map::section::{
|
||||
general::GameMode,
|
||||
hit_objects::{CurveBuffers, PathControlPoint, SliderEvent, SliderEventType, SliderEventsIter},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -41,7 +42,7 @@ impl<'a> JuiceStream<'a> {
|
||||
point.slider_velocity
|
||||
});
|
||||
|
||||
let path = slider.curve(&mut bufs.curve);
|
||||
let path = slider.curve(GameMode::Catch, &mut bufs.curve);
|
||||
|
||||
let velocity_factor = JuiceStream::BASE_SCORING_DIST * slider_multiplier / beat_len;
|
||||
let velocity = velocity_factor * slider_velocity;
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
//! while also providing a significant [boost in performance](#speed).
|
||||
//!
|
||||
//! Last commits of the ported code:
|
||||
//! - [osu!lazer] : `7342fb7f51b34533a42bffda89c3d6c569cc69ce` (2022-10-11)
|
||||
//! - [osu!tools] : `146d5916937161ef65906aa97f85d367035f3712` (2022-10-08)
|
||||
//! - [osu!lazer] : `f08134f443b2cf255fd19c8bc3ef517b6a3bb8e3` (2024-09-23)
|
||||
//! - [osu!tools] : `51965515eb4355fde0591728ef4d38eee119a964` (2024-09-01)
|
||||
//!
|
||||
//! News posts of the latest gamemode updates:
|
||||
//! - osu: <https://osu.ppy.sh/home/news/2022-09-30-changes-to-osu-sr-and-pp>
|
||||
|
||||
@@ -108,7 +108,7 @@ fn convert(map: &mut Beatmap) {
|
||||
last_values.pattern = new_pattern;
|
||||
}
|
||||
HitObjectKind::Slider(ref slider) => {
|
||||
let curve = slider.curve(&mut curve_bufs);
|
||||
let curve = slider.curve(GameMode::Mania, &mut curve_bufs);
|
||||
|
||||
let mut gen = DistanceObjectPatternGenerator::new(
|
||||
&mut random,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use rosu_map::section::hit_objects::CurveBuffers;
|
||||
use rosu_map::section::{general::GameMode, hit_objects::CurveBuffers};
|
||||
|
||||
use crate::model::{
|
||||
beatmap::Beatmap,
|
||||
@@ -26,7 +26,7 @@ impl ManiaObject {
|
||||
HitObjectKind::Slider(ref slider) => {
|
||||
const BASE_SCORING_DIST: f32 = 100.0;
|
||||
|
||||
let dist = slider.curve(&mut params.curve_bufs).dist();
|
||||
let dist = slider.curve(GameMode::Mania, &mut params.curve_bufs).dist();
|
||||
|
||||
let beat_len = params
|
||||
.map
|
||||
|
||||
+10
-3
@@ -1,6 +1,9 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use rosu_map::section::hit_objects::{BorrowedCurve, CurveBuffers};
|
||||
use rosu_map::section::{
|
||||
general::GameMode,
|
||||
hit_objects::{BorrowedCurve, CurveBuffers},
|
||||
};
|
||||
|
||||
pub use rosu_map::{
|
||||
section::hit_objects::{hit_samples::HitSoundType, PathControlPoint, PathType, SplineType},
|
||||
@@ -76,8 +79,12 @@ impl Slider {
|
||||
self.repeats + 1
|
||||
}
|
||||
|
||||
pub(crate) fn curve<'a>(&self, bufs: &'a mut CurveBuffers) -> BorrowedCurve<'a> {
|
||||
BorrowedCurve::new(&self.control_points, self.expected_dist, bufs)
|
||||
pub(crate) fn curve<'a>(
|
||||
&self,
|
||||
mode: GameMode,
|
||||
bufs: &'a mut CurveBuffers,
|
||||
) -> BorrowedCurve<'a> {
|
||||
BorrowedCurve::new(mode, &self.control_points, self.expected_dist, bufs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ impl_has_mod! {
|
||||
ht: + HalfTime ["HalfTime"],
|
||||
ap: + Autopilot ["Autopilot"],
|
||||
bl: - Blinds ["Blinds"],
|
||||
tc: - Traceable ["Traceable"],
|
||||
}
|
||||
|
||||
impl Default for GameMods {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{cmp, pin::Pin};
|
||||
|
||||
use skills::{flashlight::Flashlight, strain::OsuStrainSkill};
|
||||
|
||||
use crate::{
|
||||
any::difficulty::{skills::Skill, Difficulty},
|
||||
model::{beatmap::BeatmapAttributes, mods::GameMods},
|
||||
@@ -179,13 +181,11 @@ impl DifficultyValues {
|
||||
flashlight_rating *= 0.4;
|
||||
}
|
||||
|
||||
let base_aim_performance =
|
||||
(5.0 * (aim_rating / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
|
||||
let base_speed_performance =
|
||||
(5.0 * (speed_rating / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
|
||||
let base_aim_performance = OsuStrainSkill::difficulty_to_performance(aim_rating);
|
||||
let base_speed_performance = OsuStrainSkill::difficulty_to_performance(speed_rating);
|
||||
|
||||
let base_flashlight_performance = if mods.fl() {
|
||||
flashlight_rating.powf(2.0) * 25.0
|
||||
Flashlight::difficulty_to_performance(flashlight_rating)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::pin::Pin;
|
||||
use std::{borrow::Cow, pin::Pin};
|
||||
|
||||
use rosu_map::util::Pos;
|
||||
|
||||
use crate::{
|
||||
any::difficulty::object::IDifficultyObject,
|
||||
osu::object::{OsuObject, OsuObjectKind},
|
||||
osu::object::{OsuObject, OsuObjectKind, OsuSlider},
|
||||
};
|
||||
|
||||
use super::{scaling_factor::ScalingFactor, HD_FADE_OUT_DURATION_MULTIPLIER};
|
||||
@@ -158,20 +158,26 @@ impl<'a> OsuDifficultyObject<'a> {
|
||||
) -> Pin<&mut OsuObject> {
|
||||
let pos = h.pos;
|
||||
let stack_offset = h.stack_offset;
|
||||
let start_time = h.start_time;
|
||||
|
||||
let OsuObjectKind::Slider(ref mut slider) = h.kind else {
|
||||
return h;
|
||||
};
|
||||
|
||||
let mut nested = Cow::Borrowed(slider.nested_objects.as_slice());
|
||||
let duration = slider.end_time - start_time;
|
||||
OsuSlider::lazy_travel_time(start_time, duration, &mut nested);
|
||||
let nested = nested.as_ref();
|
||||
|
||||
let mut curr_cursor_pos = pos + stack_offset;
|
||||
let scaling_factor = f64::from(OsuDifficultyObject::NORMALIZED_RADIUS) / radius;
|
||||
|
||||
for (curr_movement_obj, i) in slider.nested_objects.iter().zip(1..) {
|
||||
for (curr_movement_obj, i) in nested.iter().zip(1..) {
|
||||
let mut curr_movement = curr_movement_obj.pos + stack_offset - curr_cursor_pos;
|
||||
let mut curr_movement_len = scaling_factor * f64::from(curr_movement.length());
|
||||
let mut required_movement = f64::from(OsuDifficultyObject::ASSUMED_SLIDER_RADIUS);
|
||||
|
||||
if i == slider.nested_objects.len() {
|
||||
if i == nested.len() {
|
||||
let lazy_movement = slider.lazy_end_pos - curr_cursor_pos;
|
||||
|
||||
if lazy_movement.length() < curr_movement.length() {
|
||||
@@ -190,7 +196,7 @@ impl<'a> OsuDifficultyObject<'a> {
|
||||
slider.lazy_travel_dist += curr_movement_len as f32;
|
||||
}
|
||||
|
||||
if i == slider.nested_objects.len() {
|
||||
if i == nested.len() {
|
||||
slider.lazy_end_pos = curr_cursor_pos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use crate::osu::object::OsuObject;
|
||||
|
||||
use super::object::OsuDifficultyObject;
|
||||
|
||||
const BROKEN_GAMEFIELD_ROUNDING_ALLOWANCE: f32 = 1.00041;
|
||||
|
||||
/// Fields around the scaling of hit objects.
|
||||
///
|
||||
/// osu!lazer stores these in each hit object but since all objects share the
|
||||
@@ -17,7 +19,8 @@ pub struct ScalingFactor {
|
||||
|
||||
impl ScalingFactor {
|
||||
pub fn new(cs: f64) -> Self {
|
||||
let scale = (1.0 - 0.7 * (cs as f32 - 5.0) / 5.0) / 2.0;
|
||||
let scale =
|
||||
(1.0 - 0.7 * ((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;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
|
||||
use super::strain::OsuStrainSkill;
|
||||
|
||||
const SKILL_MULTIPLIER: f64 = 23.55;
|
||||
const SKILL_MULTIPLIER: f64 = 24.963;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.15;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -49,7 +49,6 @@ impl Aim {
|
||||
OsuStrainSkill::REDUCED_SECTION_COUNT,
|
||||
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
||||
OsuStrainSkill::DECAY_WEIGHT,
|
||||
OsuStrainSkill::DIFFICULTY_MULTIPLER,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -121,7 +120,7 @@ impl AimEvaluator {
|
||||
fn evaluate_diff_of<'a>(
|
||||
curr: &'a OsuDifficultyObject<'a>,
|
||||
diff_objects: &'a [OsuDifficultyObject<'a>],
|
||||
with_sliders: bool,
|
||||
with_slider_travel_dist: bool,
|
||||
) -> f64 {
|
||||
let osu_curr_obj = curr;
|
||||
|
||||
@@ -139,7 +138,7 @@ impl AimEvaluator {
|
||||
|
||||
// * But if the last object is a slider, then we extend the travel
|
||||
// * velocity through the slider into the current object.
|
||||
if osu_last_obj.base.is_slider() && with_sliders {
|
||||
if osu_last_obj.base.is_slider() && with_slider_travel_dist {
|
||||
// * calculate the slider velocity from slider head to slider end.
|
||||
let travel_vel = osu_last_obj.travel_dist / osu_last_obj.travel_time;
|
||||
// * calculate the movement velocity from slider end to current object
|
||||
@@ -152,7 +151,7 @@ impl AimEvaluator {
|
||||
// * As above, do the same for the previous hitobject.
|
||||
let mut prev_vel = osu_last_obj.lazy_jump_dist / osu_last_obj.strain_time;
|
||||
|
||||
if osu_last_last_obj.base.is_slider() && with_sliders {
|
||||
if osu_last_last_obj.base.is_slider() && with_slider_travel_dist {
|
||||
let travel_vel = osu_last_last_obj.travel_dist / osu_last_last_obj.travel_time;
|
||||
let movement_vel = osu_last_obj.min_jump_dist / osu_last_obj.min_jump_time;
|
||||
|
||||
@@ -254,7 +253,7 @@ impl AimEvaluator {
|
||||
);
|
||||
|
||||
// * Add in additional slider velocity bonus.
|
||||
if with_sliders {
|
||||
if with_slider_travel_dist {
|
||||
aim_strain += slider_bonus * Self::SLIDER_MULTIPLIER;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ use crate::{
|
||||
util::strains_vec::StrainsVec,
|
||||
};
|
||||
|
||||
use super::strain::OsuStrainSkill;
|
||||
|
||||
const SKILL_MULTIPLIER: f64 = 0.052;
|
||||
const SKILL_MULTIPLIER: f64 = 0.05512;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.15;
|
||||
|
||||
pub struct Flashlight {
|
||||
@@ -49,7 +47,11 @@ impl Flashlight {
|
||||
}
|
||||
|
||||
fn static_difficulty_value(skill: StrainSkill) -> f64 {
|
||||
skill.get_curr_strain_peaks().sum() * OsuStrainSkill::DIFFICULTY_MULTIPLER
|
||||
skill.get_curr_strain_peaks().sum()
|
||||
}
|
||||
|
||||
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
|
||||
25.0 * (difficulty).powf(2.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,9 @@ use crate::{
|
||||
|
||||
use super::strain::OsuStrainSkill;
|
||||
|
||||
const SKILL_MULTIPLIER: f64 = 1375.0;
|
||||
const SKILL_MULTIPLIER: f64 = 1.430;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.3;
|
||||
|
||||
const DIFFICULTY_MULTIPLER: f64 = 1.04;
|
||||
const REDUCED_SECTION_COUNT: usize = 5;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -60,7 +59,6 @@ impl Speed {
|
||||
REDUCED_SECTION_COUNT,
|
||||
OsuStrainSkill::REDUCED_STRAIN_BASELINE,
|
||||
OsuStrainSkill::DECAY_WEIGHT,
|
||||
DIFFICULTY_MULTIPLER,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -146,7 +144,7 @@ impl<'a> Skill<'a, Speed> {
|
||||
struct SpeedEvaluator;
|
||||
|
||||
impl SpeedEvaluator {
|
||||
const SINGLE_SPACING_THRESHOLD: f64 = 125.0;
|
||||
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.;
|
||||
|
||||
@@ -182,25 +180,34 @@ impl SpeedEvaluator {
|
||||
// * 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) / 0.93).clamp(0.92, 1.0);
|
||||
|
||||
// * derive speedBonus for calculation
|
||||
let speed_bonus = if strain_time < Self::MIN_SPEED_BONUS {
|
||||
// * 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)
|
||||
} else {
|
||||
// * speedBonus will be 1.0 for BPM < 200
|
||||
1.0
|
||||
};
|
||||
|
||||
let travel_dist = osu_prev_obj.map_or(0.0, |obj| obj.travel_dist);
|
||||
let dist = if has_autopilot_mod {
|
||||
0.0
|
||||
let mut dist = travel_dist + osu_curr_obj.min_jump_dist;
|
||||
|
||||
// * Cap distance at single_spacing_threshold
|
||||
dist = Self::SINGLE_SPACING_THRESHOLD.min(dist);
|
||||
|
||||
// * Max distance bonus is 2 at single_spacing_threshold
|
||||
let dist_bonus = if has_autopilot_mod {
|
||||
1.0
|
||||
} else {
|
||||
Self::SINGLE_SPACING_THRESHOLD.min(travel_dist + osu_curr_obj.min_jump_dist)
|
||||
1.0 + (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5)
|
||||
};
|
||||
|
||||
(speed_bonus + speed_bonus * (dist / Self::SINGLE_SPACING_THRESHOLD).powf(3.5))
|
||||
* doubletapness
|
||||
/ strain_time
|
||||
// * Base difficulty with all bonuses
|
||||
let difficulty = speed_bonus * dist_bonus * 1000.0 / strain_time;
|
||||
|
||||
// * Apply penalty if there's doubletappable doubles
|
||||
return difficulty * doubletapness;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,103 +251,107 @@ impl RhythmEvaluator {
|
||||
rhythm_start += 1;
|
||||
}
|
||||
|
||||
for i in (1..=rhythm_start).rev() {
|
||||
let Some(((curr_obj, prev_obj), last_obj)) = curr
|
||||
.previous(i - 1, diff_objects)
|
||||
.zip(curr.previous(i, diff_objects))
|
||||
.zip(curr.previous(i + 1, diff_objects))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
if let Some((mut prev_obj, mut last_obj)) = curr
|
||||
.previous(rhythm_start, diff_objects)
|
||||
.zip(curr.previous(rhythm_start + 1, diff_objects))
|
||||
{
|
||||
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)
|
||||
- (curr.start_time - curr_obj.start_time))
|
||||
/ f64::from(Self::HISTORY_TIME_MAX);
|
||||
// * scales note 0 to 1 from history to now
|
||||
let mut curr_historical_decay = (f64::from(Self::HISTORY_TIME_MAX)
|
||||
- (curr.start_time - curr_obj.start_time))
|
||||
/ f64::from(Self::HISTORY_TIME_MAX);
|
||||
|
||||
// * 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);
|
||||
// * 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_delta = curr_obj.strain_time;
|
||||
let prev_delta = prev_obj.strain_time;
|
||||
let last_delta = last_obj.strain_time;
|
||||
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);
|
||||
// * 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);
|
||||
|
||||
let hit_window = u64::from(!curr_obj.base.is_spinner()) as f64 * hit_window;
|
||||
let hit_window = u64::from(!curr_obj.base.is_spinner()) as f64 * hit_window;
|
||||
|
||||
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
||||
.max(0.0))
|
||||
/ (hit_window * 0.3))
|
||||
.min(1.0);
|
||||
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
||||
.max(0.0))
|
||||
/ (hit_window * 0.3))
|
||||
.min(1.0);
|
||||
|
||||
window_penalty = window_penalty.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;
|
||||
|
||||
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 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;
|
||||
}
|
||||
} else {
|
||||
// * bpm change is into slider, this is easy acc window
|
||||
if curr_obj.base.is_slider() {
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
// * bpm change was from a slider, this is easier typically than circle -> circle
|
||||
if prev_obj.base.is_slider() {
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
// * 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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
start_ratio = effective_ratio;
|
||||
|
||||
// * log the last island size.
|
||||
prev_island_size = island_size;
|
||||
|
||||
// * 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;
|
||||
}
|
||||
|
||||
island_size = 1;
|
||||
}
|
||||
} else {
|
||||
// * bpm change is into slider, this is easy acc window
|
||||
if curr_obj.base.is_slider() {
|
||||
effective_ratio *= 0.125;
|
||||
}
|
||||
|
||||
// * bpm change was from a slider, this is easier typically than circle -> circle
|
||||
if prev_obj.base.is_slider() {
|
||||
effective_ratio *= 0.25;
|
||||
}
|
||||
|
||||
// * 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 {
|
||||
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 {
|
||||
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;
|
||||
|
||||
} 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;
|
||||
|
||||
// * log the last island size.
|
||||
prev_island_size = island_size;
|
||||
|
||||
// * 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;
|
||||
}
|
||||
|
||||
island_size = 1;
|
||||
}
|
||||
} 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;
|
||||
|
||||
last_obj = prev_obj;
|
||||
prev_obj = curr_obj;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ pub struct OsuStrainSkill {
|
||||
impl OsuStrainSkill {
|
||||
pub const REDUCED_SECTION_COUNT: usize = 10;
|
||||
pub const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||
pub const DIFFICULTY_MULTIPLER: f64 = 1.06;
|
||||
|
||||
pub const DECAY_WEIGHT: f64 = 0.9;
|
||||
pub const SECTION_LEN: f64 = 400.0;
|
||||
@@ -30,7 +29,6 @@ impl OsuStrainSkill {
|
||||
reduced_section_count: usize,
|
||||
reduced_strain_baseline: f64,
|
||||
decay_weight: f64,
|
||||
difficulty_multiplier: f64,
|
||||
) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
@@ -52,7 +50,11 @@ impl OsuStrainSkill {
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty * difficulty_multiplier
|
||||
difficulty
|
||||
}
|
||||
|
||||
pub fn difficulty_to_performance(difficulty: f64) -> f64 {
|
||||
(5.0 * (difficulty / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+96
-20
@@ -1,5 +1,10 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rosu_map::{
|
||||
section::hit_objects::{CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter},
|
||||
section::{
|
||||
general::GameMode,
|
||||
hit_objects::{CurveBuffers, SliderEvent, SliderEventType, SliderEventsIter},
|
||||
},
|
||||
util::Pos,
|
||||
};
|
||||
|
||||
@@ -61,6 +66,8 @@ 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;
|
||||
|
||||
@@ -81,6 +88,21 @@ impl OsuObject {
|
||||
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);
|
||||
}
|
||||
@@ -123,12 +145,7 @@ impl OsuObject {
|
||||
pub fn lazy_travel_time(&self) -> f64 {
|
||||
match self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner(_) => 0.0,
|
||||
OsuObjectKind::Slider(ref slider) => slider
|
||||
.nested_objects
|
||||
// Here we really want the last nested object which is not
|
||||
// necessarily the tail
|
||||
.last()
|
||||
.map_or(0.0, |nested| nested.start_time - self.start_time),
|
||||
OsuObjectKind::Slider(ref slider) => slider.lazy_travel_time,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +172,10 @@ pub struct OsuSlider {
|
||||
pub end_time: f64,
|
||||
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>,
|
||||
}
|
||||
|
||||
@@ -182,13 +203,25 @@ impl OsuSlider {
|
||||
|point| (point.slider_velocity, point.generate_ticks),
|
||||
);
|
||||
|
||||
let path = slider.curve(curve_bufs);
|
||||
let path = slider.curve(GameMode::Osu, curve_bufs);
|
||||
|
||||
let span_count = slider.span_count() as f64;
|
||||
|
||||
let scoring_dist =
|
||||
f64::from(OsuObject::BASE_SCORING_DIST) * slider_multiplier * slider_velocity;
|
||||
let velocity = scoring_dist / beat_len;
|
||||
fn get_precision_adjusted_beat_len(slider_velocity_multiplier: f64, beat_len: f64) -> f64 {
|
||||
let slider_velocity_as_beat_len = -100.0 / slider_velocity_multiplier;
|
||||
|
||||
let bpm_multiplier = if slider_velocity_as_beat_len < 0.0 {
|
||||
f64::from(((-slider_velocity_as_beat_len) as f32).clamp(10.0, 10_000.0)) / 100.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
beat_len * bpm_multiplier
|
||||
}
|
||||
|
||||
let velocity = f64::from(OsuObject::BASE_SCORING_DIST) * slider_multiplier
|
||||
/ get_precision_adjusted_beat_len(slider_velocity, beat_len);
|
||||
let scoring_dist = velocity * beat_len;
|
||||
|
||||
let end_time = start_time + span_count * path.dist() / velocity;
|
||||
|
||||
@@ -244,12 +277,14 @@ impl OsuSlider {
|
||||
start_time: start_time + f64::from(e.span_idx + 1) * span_duration,
|
||||
kind: NestedSliderObjectKind::Repeat,
|
||||
},
|
||||
SliderEventType::LastTick => NestedSliderObject {
|
||||
pos: end_path_pos, // no `h.pos` yet to keep order of float operations
|
||||
start_time: e.time,
|
||||
kind: NestedSliderObjectKind::Tail,
|
||||
},
|
||||
SliderEventType::Head | SliderEventType::Tail => return None,
|
||||
SliderEventType::Tail => {
|
||||
NestedSliderObject {
|
||||
pos: end_path_pos, // no `h.pos` yet to keep order of float operations
|
||||
start_time: e.time,
|
||||
kind: NestedSliderObjectKind::Tail,
|
||||
}
|
||||
}
|
||||
SliderEventType::Head | SliderEventType::LastTick => return None,
|
||||
};
|
||||
|
||||
Some(obj)
|
||||
@@ -260,9 +295,8 @@ impl OsuSlider {
|
||||
a.start_time.total_cmp(&b.start_time)
|
||||
});
|
||||
|
||||
let lazy_travel_time = nested_objects
|
||||
.last()
|
||||
.map_or(0.0, |nested| nested.start_time - h.start_time);
|
||||
let mut nested = Cow::Borrowed(nested_objects.as_slice());
|
||||
let lazy_travel_time = OsuSlider::lazy_travel_time(start_time, duration, &mut nested);
|
||||
|
||||
let mut end_time_min = lazy_travel_time / span_duration;
|
||||
|
||||
@@ -273,15 +307,51 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lazy_travel_time(
|
||||
start_time: f64,
|
||||
duration: f64,
|
||||
nested_objects: &mut Cow<'_, [NestedSliderObject]>,
|
||||
) -> f64 {
|
||||
const TAIL_LENIENCY: f64 = -36.0;
|
||||
|
||||
let mut tracking_end_time =
|
||||
(start_time + duration + TAIL_LENIENCY).max(start_time + duration / 2.0);
|
||||
|
||||
let last_real_tick = nested_objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rfind(|(_, nested)| nested.is_tick());
|
||||
|
||||
if let Some((idx, last_real_tick)) =
|
||||
last_real_tick.filter(|(_, tick)| tick.start_time > tracking_end_time)
|
||||
{
|
||||
tracking_end_time = last_real_tick.start_time;
|
||||
|
||||
// * When the last tick falls after the tracking end time, we need to re-sort the nested objects
|
||||
// * based on time. This creates a somewhat weird ordering which is counter to how a user would
|
||||
// * understand the slider, but allows a zero-diff with known diffcalc output.
|
||||
// *
|
||||
// * To reiterate, this is definitely not correct from a difficulty calculation perspective
|
||||
// * and should be revisited at a later date (likely by replacing this whole code with the commented
|
||||
// * version above).
|
||||
nested_objects.to_mut()[idx..].rotate_left(1);
|
||||
}
|
||||
|
||||
tracking_end_time - start_time
|
||||
}
|
||||
|
||||
pub fn repeat_count(&self) -> usize {
|
||||
self.nested_objects
|
||||
.iter()
|
||||
@@ -306,6 +376,7 @@ impl OsuSlider {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NestedSliderObject {
|
||||
pub pos: Pos,
|
||||
pub start_time: f64,
|
||||
@@ -316,8 +387,13 @@ impl NestedSliderObject {
|
||||
pub const fn is_repeat(&self) -> bool {
|
||||
matches!(self.kind, NestedSliderObjectKind::Repeat)
|
||||
}
|
||||
|
||||
pub const fn is_tick(&self) -> bool {
|
||||
matches!(self.kind, NestedSliderObjectKind::Tick)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum NestedSliderObjectKind {
|
||||
Repeat,
|
||||
Tail,
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
attributes::{OsuDifficultyAttributes, OsuPerformanceAttributes},
|
||||
difficulty::skills::{flashlight::Flashlight, strain::OsuStrainSkill},
|
||||
score_state::OsuScoreState,
|
||||
Osu,
|
||||
};
|
||||
@@ -617,7 +618,7 @@ impl OsuPerformanceInner<'_> {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut aim_value = (5.0 * (self.attrs.aim / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
|
||||
let mut aim_value = OsuStrainSkill::difficulty_to_performance(self.attrs.aim);
|
||||
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
@@ -656,7 +657,7 @@ impl OsuPerformanceInner<'_> {
|
||||
* (0.0016 / (1.0 + 2.0 * self.effective_miss_count))
|
||||
* self.acc.powf(16.0))
|
||||
* (1.0 - 0.003 * self.attrs.hp * self.attrs.hp);
|
||||
} else if self.mods.hd() {
|
||||
} else if self.mods.hd() || self.mods.tc() {
|
||||
// * We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aim_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
|
||||
}
|
||||
@@ -689,8 +690,7 @@ impl OsuPerformanceInner<'_> {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut speed_value =
|
||||
(5.0 * (self.attrs.speed / 0.0675).max(1.0) - 4.0).powf(3.0) / 100_000.0;
|
||||
let mut speed_value = OsuStrainSkill::difficulty_to_performance(self.attrs.speed);
|
||||
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
@@ -725,7 +725,7 @@ impl OsuPerformanceInner<'_> {
|
||||
// * Increasing the speed value by object count for Blinds isn't
|
||||
// * ideal, so the minimum buff is given.
|
||||
speed_value *= 1.12;
|
||||
} else if self.mods.hd() {
|
||||
} else if self.mods.hd() || self.mods.tc() {
|
||||
// * We want to give more reward for lower AR when it comes to aim and HD.
|
||||
// * This nerfs high AR and buffs lower AR.
|
||||
speed_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
|
||||
@@ -798,7 +798,7 @@ impl OsuPerformanceInner<'_> {
|
||||
// * ideal, so the minimum buff is given.
|
||||
if self.mods.bl() {
|
||||
acc_value *= 1.14;
|
||||
} else if self.mods.hd() {
|
||||
} else if self.mods.hd() || self.mods.tc() {
|
||||
acc_value *= 1.08;
|
||||
}
|
||||
|
||||
@@ -814,7 +814,7 @@ impl OsuPerformanceInner<'_> {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut flashlight_value = self.attrs.flashlight.powf(2.0) * 25.0;
|
||||
let mut flashlight_value = Flashlight::difficulty_to_performance(self.attrs.flashlight);
|
||||
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ fn should_convert_slider_to_taiko_hits(map: &Beatmap, params: &mut SliderParams<
|
||||
tick_spacing,
|
||||
} = params;
|
||||
|
||||
let curve = slider.curve(bufs);
|
||||
let curve = slider.curve(GameMode::Taiko, bufs);
|
||||
|
||||
// * The true distance, accounting for any repeats. This ends up being the drum roll distance later
|
||||
let spans = slider.span_count() as f64;
|
||||
|
||||
Reference in New Issue
Block a user