refactored strain functions
This commit is contained in:
+49
-259
@@ -35,21 +35,52 @@ pub fn stars(
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> FruitsDifficultyAttributes {
|
||||
match calculate_movement(map, mods, passed_objects) {
|
||||
Some((mut movement, mut attributes)) => {
|
||||
attributes.stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
||||
attributes.max_combo = attributes.n_fruits + attributes.n_droplets;
|
||||
|
||||
attributes
|
||||
}
|
||||
None => return FruitsDifficultyAttributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
match calculate_movement(map, mods, None) {
|
||||
Some((movement, _)) => Strains {
|
||||
section_length: SECTION_LENGTH * mods.speed(),
|
||||
strains: movement.strain_peaks,
|
||||
},
|
||||
None => Strains::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_movement(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> Option<(Movement, FruitsDifficultyAttributes)> {
|
||||
if map.hit_objects.len() < 2 {
|
||||
return FruitsDifficultyAttributes::default();
|
||||
return None;
|
||||
}
|
||||
|
||||
let take = passed_objects.unwrap_or(usize::MAX);
|
||||
|
||||
let attributes = map.attributes().mods(mods);
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let with_hr = mods.hr();
|
||||
let mut ticks = Vec::new(); // using the same buffer for all sliders
|
||||
let mut slider_state = SliderState::new(map);
|
||||
let mut curve_bufs = CurveBuffers::default();
|
||||
|
||||
let mut fruits = 0;
|
||||
let mut droplets = 0;
|
||||
let mut tiny_droplets = 0;
|
||||
let mut attributes = FruitsDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
||||
let mut hit_objects = map
|
||||
@@ -63,7 +94,7 @@ pub fn stars(
|
||||
h = h.with_hr(last_pos, last_time);
|
||||
}
|
||||
|
||||
fruits += 1;
|
||||
attributes.n_fruits += 1;
|
||||
|
||||
Some(Some(FruitOrJuice::Fruit(Some(h))))
|
||||
}
|
||||
@@ -124,7 +155,7 @@ pub fn stars(
|
||||
curr_dist += tick_dist;
|
||||
}
|
||||
|
||||
tiny_droplets += tiny_droplet_count(
|
||||
attributes.n_tiny_droplets += tiny_droplet_count(
|
||||
h.start_time,
|
||||
time_add,
|
||||
duration,
|
||||
@@ -171,8 +202,8 @@ pub fn stars(
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
let new_fruits = *repeats + 2;
|
||||
fruits += new_fruits;
|
||||
droplets += slider_objects.len() - new_fruits;
|
||||
attributes.n_fruits += new_fruits;
|
||||
attributes.n_droplets += slider_objects.len() - new_fruits;
|
||||
|
||||
let iter = slider_objects.into_iter().map(CatchObject::new);
|
||||
|
||||
@@ -186,13 +217,13 @@ pub fn stars(
|
||||
|
||||
// Hyper dash business
|
||||
let half_catcher_width =
|
||||
(calculate_catch_width(attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||
(calculate_catch_width(map_attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||
let mut last_direction = 0;
|
||||
let mut last_excess = half_catcher_width;
|
||||
|
||||
// Strain business
|
||||
let mut movement = Movement::new(attributes.cs as f32);
|
||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||
let mut movement = Movement::new(map_attributes.cs as f32);
|
||||
let section_len = SECTION_LENGTH * map_attributes.clock_rate;
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
|
||||
@@ -219,7 +250,7 @@ pub fn stars(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
@@ -244,12 +275,12 @@ pub fn stars(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
movement.save_current_peak();
|
||||
movement.start_new_section_from(current_section_end / attributes.clock_rate);
|
||||
movement.start_new_section_from(current_section_end / map_attributes.clock_rate);
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
@@ -264,12 +295,12 @@ pub fn stars(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
movement.save_current_peak();
|
||||
movement.start_new_section_from(current_section_end / attributes.clock_rate);
|
||||
movement.start_new_section_from(current_section_end / map_attributes.clock_rate);
|
||||
|
||||
current_section_end += section_len;
|
||||
}
|
||||
@@ -277,248 +308,7 @@ pub fn stars(
|
||||
movement.process(&h);
|
||||
movement.save_current_peak();
|
||||
|
||||
let stars = movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR;
|
||||
|
||||
FruitsDifficultyAttributes {
|
||||
stars,
|
||||
ar: attributes.ar,
|
||||
n_fruits: fruits,
|
||||
n_droplets: droplets,
|
||||
n_tiny_droplets: tiny_droplets,
|
||||
max_combo: fruits + droplets,
|
||||
}
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
if map.hit_objects.len() < 2 {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
let attributes = map.attributes().mods(mods);
|
||||
let with_hr = mods.hr();
|
||||
let mut ticks = Vec::new(); // using the same buffer for all sliders
|
||||
let mut slider_state = SliderState::new(map);
|
||||
let mut curve_bufs = CurveBuffers::default();
|
||||
|
||||
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
let mut h = CatchObject::new((h.pos, h.start_time));
|
||||
|
||||
if with_hr {
|
||||
h = h.with_hr(last_pos, last_time);
|
||||
}
|
||||
|
||||
Some(Some(FruitOrJuice::Fruit(Some(h))))
|
||||
}
|
||||
HitObjectKind::Slider {
|
||||
pixel_len,
|
||||
repeats,
|
||||
control_points,
|
||||
} => {
|
||||
// HR business
|
||||
*last_pos = Some(h.pos.x + control_points[control_points.len() - 1].pos.x);
|
||||
*last_time = h.start_time;
|
||||
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let span_count = (*repeats + 1) as f64;
|
||||
|
||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||
|
||||
if map.version >= 8 {
|
||||
tick_dist /=
|
||||
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
||||
}
|
||||
|
||||
// Build the curve w.r.t. the control points
|
||||
let curve = Curve::new(control_points, *pixel_len, &mut curve_bufs);
|
||||
|
||||
let velocity =
|
||||
(BASE_SCORING_DISTANCE * map.slider_mult * slider_state.slider_velocity)
|
||||
/ slider_state.beat_len;
|
||||
|
||||
let end_time = h.start_time + span_count * curve.dist() / velocity;
|
||||
let duration = end_time - h.start_time;
|
||||
let span_duration = duration / span_count;
|
||||
|
||||
// * A very lenient maximum length of a slider for ticks to be generated.
|
||||
// * This exists for edge cases such as /b/1573664 where the beatmap has
|
||||
// * been edited by the user, and should never be reached in normal usage.
|
||||
let max_len = 100_000.0;
|
||||
|
||||
let len = curve.dist().min(max_len);
|
||||
tick_dist = tick_dist.clamp(0.0, len);
|
||||
let min_dist_from_end = velocity * 10.0;
|
||||
|
||||
let mut curr_dist = tick_dist;
|
||||
let time_add = duration * tick_dist / (*pixel_len * span_count);
|
||||
|
||||
let target = *pixel_len - tick_dist / 8.0;
|
||||
|
||||
ticks.reserve((target / tick_dist) as usize);
|
||||
|
||||
// Tick of the first span
|
||||
while curr_dist < len - min_dist_from_end {
|
||||
let progress = curr_dist / len;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
let time = h.start_time + progress * span_duration;
|
||||
ticks.push((pos, time));
|
||||
curr_dist += tick_dist;
|
||||
}
|
||||
|
||||
let mut slider_objects =
|
||||
Vec::with_capacity(span_count as usize * (ticks.len() + 1));
|
||||
slider_objects.push((h.pos, h.start_time));
|
||||
|
||||
// Other spans
|
||||
if *repeats == 0 {
|
||||
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
|
||||
} else {
|
||||
slider_objects.extend(&ticks);
|
||||
|
||||
for span_idx in 1..=*repeats {
|
||||
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
let time_offset = span_duration * span_idx as f64;
|
||||
|
||||
// Reverse tick
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
|
||||
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
||||
(*pos, *time + time_offset + time_add * i as f64)
|
||||
});
|
||||
|
||||
// Actual ticks
|
||||
if span_idx & 1 == 1 {
|
||||
slider_objects.extend(new_ticks.rev());
|
||||
} else {
|
||||
slider_objects.extend(new_ticks);
|
||||
}
|
||||
}
|
||||
|
||||
ticks.clear();
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||
let pos = h.pos + curve.position_at(progress);
|
||||
slider_objects.push((pos, h.start_time + duration));
|
||||
|
||||
let iter = slider_objects.into_iter().map(CatchObject::new);
|
||||
|
||||
Some(Some(FruitOrJuice::Juice(iter)))
|
||||
}
|
||||
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => Some(None),
|
||||
})
|
||||
.flatten()
|
||||
.flatten();
|
||||
|
||||
// Hyper dash business
|
||||
let half_catcher_width =
|
||||
(calculate_catch_width(attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||
let mut last_direction = 0;
|
||||
let mut last_excess = half_catcher_width;
|
||||
|
||||
// Strain business
|
||||
let mut movement = Movement::new(attributes.cs as f32);
|
||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
let mut curr = hit_objects.next().unwrap();
|
||||
|
||||
prev.init_hyper_dash(
|
||||
half_catcher_width,
|
||||
&curr,
|
||||
&mut last_direction,
|
||||
&mut last_excess,
|
||||
);
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let next = hit_objects.next().unwrap();
|
||||
curr.init_hyper_dash(
|
||||
half_catcher_width,
|
||||
&next,
|
||||
&mut last_direction,
|
||||
&mut last_excess,
|
||||
);
|
||||
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
movement.process(&h);
|
||||
|
||||
prev = curr;
|
||||
curr = next;
|
||||
|
||||
// Handle all other objects
|
||||
for next in hit_objects {
|
||||
curr.init_hyper_dash(
|
||||
half_catcher_width,
|
||||
&next,
|
||||
&mut last_direction,
|
||||
&mut last_excess,
|
||||
);
|
||||
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
movement.save_current_peak();
|
||||
movement.start_new_section_from(current_section_end / attributes.clock_rate);
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
movement.process(&h);
|
||||
|
||||
prev = curr;
|
||||
curr = next;
|
||||
}
|
||||
|
||||
// Same as in loop but without init_hyper_dash because `curr` is the last element
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&prev,
|
||||
movement.half_catcher_width,
|
||||
attributes.clock_rate,
|
||||
);
|
||||
|
||||
while h.base.time > current_section_end {
|
||||
movement.save_current_peak();
|
||||
movement.start_new_section_from(current_section_end / attributes.clock_rate);
|
||||
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
movement.process(&h);
|
||||
movement.save_current_peak();
|
||||
|
||||
Strains {
|
||||
section_length: section_len,
|
||||
strains: movement.strain_peaks,
|
||||
}
|
||||
Some((movement, attributes))
|
||||
}
|
||||
|
||||
// BUG: Sometimes there are off-by-one errors,
|
||||
|
||||
+29
-57
@@ -19,10 +19,37 @@ pub fn stars(
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> ManiaDifficultyAttributes {
|
||||
match calculate_strain(map, mods, passed_objects) {
|
||||
Some(mut strain) => ManiaDifficultyAttributes {
|
||||
stars: strain.difficulty_value() * STAR_SCALING_FACTOR,
|
||||
},
|
||||
None => ManiaDifficultyAttributes::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
match calculate_strain(map, mods, None) {
|
||||
Some(strain) => Strains {
|
||||
section_length: SECTION_LEN * mods.speed(),
|
||||
strains: strain.strain_peaks,
|
||||
},
|
||||
None => Strains::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_strain(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> Option<Strain> {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
if take < 2 {
|
||||
return ManiaDifficultyAttributes::default();
|
||||
return None;
|
||||
}
|
||||
|
||||
let rounded_cs = map.cs.round();
|
||||
@@ -88,62 +115,7 @@ pub fn stars(
|
||||
|
||||
strain.save_current_peak();
|
||||
|
||||
let stars = strain.difficulty_value() * STAR_SCALING_FACTOR;
|
||||
|
||||
ManiaDifficultyAttributes { stars }
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
if map.hit_objects.len() < 2 {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
let clock_rate = mods.speed();
|
||||
let section_len = SECTION_LEN * clock_rate;
|
||||
let mut strain = Strain::new(map.cs as u8);
|
||||
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.skip(1)
|
||||
.zip(map.hit_objects.iter())
|
||||
.map(|(base, prev)| DifficultyHitObject::new(base, prev, map.cs, clock_rate));
|
||||
|
||||
// No strain for first object
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let h = hit_objects.next().unwrap();
|
||||
|
||||
while h.base.start_time > current_section_end {
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
strain.process(&h);
|
||||
|
||||
// Handle all other objects
|
||||
for h in hit_objects {
|
||||
while h.base.start_time > current_section_end {
|
||||
strain.save_current_peak();
|
||||
strain.start_new_section_from(current_section_end / clock_rate);
|
||||
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
strain.process(&h);
|
||||
}
|
||||
|
||||
strain.save_current_peak();
|
||||
|
||||
Strains {
|
||||
section_length: section_len,
|
||||
strains: strain.strain_peaks,
|
||||
}
|
||||
Some(strain)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
+122
-223
@@ -31,6 +31,124 @@ pub fn stars(
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> OsuDifficultyAttributes {
|
||||
let (mut skills, mut attributes) = match calculate_skills(map, mods, passed_objects) {
|
||||
Some(tuple) => tuple,
|
||||
None => {
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
return OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let slider_factor = if aim_rating > 0.0 {
|
||||
let aim_rating_no_sliders = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
aim_rating_no_sliders / aim_rating
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let speed_rating = if mods.rx() {
|
||||
0.0
|
||||
} else {
|
||||
skills[2].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
};
|
||||
|
||||
let flashlight_rating = skills.get_mut(3).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 mods.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_f64.cbrt()
|
||||
* 0.027
|
||||
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
attributes.aim_strain = aim_rating;
|
||||
attributes.speed_strain = speed_rating;
|
||||
attributes.flashlight_rating = flashlight_rating;
|
||||
attributes.slider_factor = slider_factor;
|
||||
attributes.stars = star_rating;
|
||||
|
||||
attributes
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let mut skills = match calculate_skills(map, mods, None) {
|
||||
Some((skills, _)) => skills,
|
||||
None => return Strains::default(),
|
||||
};
|
||||
|
||||
skills.reverse();
|
||||
|
||||
let _ = skills.pop();
|
||||
let aim_strains = skills.pop().unwrap().strain_peaks; // no sliders
|
||||
let speed_strains = skills.pop().unwrap().strain_peaks;
|
||||
|
||||
let strains = if let Some(flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
|
||||
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 * mods.speed(),
|
||||
strains,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_skills(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> Option<(Vec<Skill>, OsuDifficultyAttributes)> {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
@@ -38,12 +156,7 @@ pub fn stars(
|
||||
let od = (80.0 - hit_window) / 6.0;
|
||||
|
||||
if take < 2 {
|
||||
return OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
..Default::default()
|
||||
};
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut raw_ar = map.ar as f64;
|
||||
@@ -169,232 +282,18 @@ pub fn stars(
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
let slider_factor = if aim_rating > 0.0 {
|
||||
let aim_rating_no_sliders = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||
|
||||
aim_rating_no_sliders / aim_rating
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let speed_rating = if mods.rx() {
|
||||
0.0
|
||||
} else {
|
||||
skills[2].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
|
||||
};
|
||||
|
||||
let flashlight_rating = skills.get_mut(3).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_f64.cbrt()
|
||||
* 0.027
|
||||
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
OsuDifficultyAttributes {
|
||||
let attributes = OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od,
|
||||
aim_strain: aim_rating,
|
||||
speed_strain: speed_rating,
|
||||
flashlight_rating,
|
||||
slider_factor,
|
||||
n_circles: map.n_circles as usize,
|
||||
n_sliders: map.n_sliders as usize,
|
||||
n_spinners: map.n_spinners as usize,
|
||||
stars: star_rating,
|
||||
max_combo: params.max_combo,
|
||||
}
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let map_attributes = map.attributes().mods(mods);
|
||||
let hit_window = difficulty_range_od(map_attributes.od) / map_attributes.clock_rate;
|
||||
|
||||
if map.hit_objects.len() < 2 {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
let mut raw_ar = map.ar as f64;
|
||||
let hr = mods.hr();
|
||||
|
||||
if hr {
|
||||
raw_ar *= 1.4;
|
||||
} else if mods.ez() {
|
||||
raw_ar *= 0.5;
|
||||
}
|
||||
|
||||
let time_preempt = difficulty_range_ar(raw_ar);
|
||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||
|
||||
let mut params = ObjectParameters {
|
||||
map,
|
||||
max_combo: 0,
|
||||
slider_state: SliderState::new(map),
|
||||
ticks: Vec::new(),
|
||||
curve_bufs: CurveBuffers::default(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let hit_objects_iter = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
||||
|
||||
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||
hit_objects.extend(hit_objects_iter);
|
||||
|
||||
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||
|
||||
if map.version >= 6 {
|
||||
stacking(&mut hit_objects, stack_threshold);
|
||||
} else {
|
||||
old_stacking(&mut hit_objects, stack_threshold);
|
||||
}
|
||||
|
||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
||||
h.pos += stack_offset;
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let fl = mods.fl();
|
||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||
|
||||
skills.push(Skill::aim(true));
|
||||
skills.push(Skill::aim(false));
|
||||
skills.push(Skill::speed(hit_window));
|
||||
|
||||
if fl {
|
||||
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
||||
skills.push(Skill::flashlight(52.0 / scaling_factor.radius() as f64));
|
||||
}
|
||||
|
||||
let mut prev_prev = None;
|
||||
let mut prev = hit_objects.next().unwrap();
|
||||
|
||||
// First object has no predecessor and thus no strain, handle distinctly
|
||||
let mut current_section_end =
|
||||
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let curr = hit_objects.next().unwrap();
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&mut prev,
|
||||
prev_prev.as_ref(),
|
||||
&scaling_factor,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
let base_time = h.base.time / map_attributes.clock_rate;
|
||||
|
||||
while base_time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev = curr;
|
||||
|
||||
// Handle all other objects
|
||||
for curr in hit_objects {
|
||||
let h = DifficultyObject::new(
|
||||
&curr,
|
||||
&mut prev,
|
||||
prev_prev.as_ref(),
|
||||
&scaling_factor,
|
||||
map_attributes.clock_rate,
|
||||
);
|
||||
|
||||
let base_time = h.base.time / map_attributes.clock_rate;
|
||||
|
||||
while base_time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.start_new_section_from(current_section_end);
|
||||
}
|
||||
|
||||
current_section_end += SECTION_LEN;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h);
|
||||
}
|
||||
|
||||
prev_prev = Some(prev);
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
skills.reverse();
|
||||
|
||||
let _ = skills.pop();
|
||||
let aim_strains = skills.pop().unwrap().strain_peaks; // no sliders
|
||||
let speed_strains = skills.pop().unwrap().strain_peaks;
|
||||
|
||||
let strains = if let Some(flashlight_strains) = skills.pop().map(|s| s.strain_peaks) {
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.zip(flashlight_strains)
|
||||
.map(|((aim, speed), flashlight)| aim + speed + flashlight)
|
||||
.collect()
|
||||
} else {
|
||||
aim_strains
|
||||
.into_iter()
|
||||
.zip(speed_strains)
|
||||
.map(|(aim, speed)| aim + speed)
|
||||
.collect()
|
||||
};
|
||||
|
||||
Strains {
|
||||
section_length: SECTION_LEN,
|
||||
strains,
|
||||
}
|
||||
Some((skills, attributes))
|
||||
}
|
||||
|
||||
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
|
||||
+59
-104
@@ -37,10 +37,67 @@ pub fn stars(
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> TaikoDifficultyAttributes {
|
||||
let skills = match calculate_skills(map, mods, passed_objects) {
|
||||
Some(skills) => skills,
|
||||
None => return TaikoDifficultyAttributes { stars: 0.0 },
|
||||
};
|
||||
|
||||
let mut buf = vec![0.0; skills[0].strain_peaks.len()];
|
||||
|
||||
let color_rating = skills[0].difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
||||
let rhythm_rating = skills[1].difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
||||
|
||||
let mut stamina_rating = (skills[2].difficulty_value(&mut buf)
|
||||
+ skills[3].difficulty_value(&mut buf))
|
||||
* STAMINA_SKILL_MULTIPLIER;
|
||||
|
||||
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
||||
stamina_rating *= stamina_penalty;
|
||||
|
||||
let combined_rating = locally_combined_difficulty(&skills, stamina_penalty);
|
||||
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
||||
|
||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
||||
|
||||
TaikoDifficultyAttributes { stars }
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let skills = match calculate_skills(map, mods, None) {
|
||||
Some(skills) => skills,
|
||||
None => return Strains::default(),
|
||||
};
|
||||
|
||||
let strains = skills[0]
|
||||
.strain_peaks
|
||||
.iter()
|
||||
.zip(skills[1].strain_peaks.iter())
|
||||
.zip(skills[2].strain_peaks.iter())
|
||||
.zip(skills[3].strain_peaks.iter())
|
||||
.map(|(((color, rhythm), stamina_right), stamina_left)| {
|
||||
color + rhythm + stamina_right + stamina_left
|
||||
})
|
||||
.collect();
|
||||
|
||||
Strains {
|
||||
section_length: SECTION_LEN * mods.speed(),
|
||||
strains,
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_skills(
|
||||
map: &Beatmap,
|
||||
mods: impl Mods,
|
||||
passed_objects: Option<usize>,
|
||||
) -> Option<Vec<Skill>> {
|
||||
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
|
||||
|
||||
if take < 2 {
|
||||
return TaikoDifficultyAttributes { stars: 0.0 };
|
||||
return None;
|
||||
}
|
||||
|
||||
// True if the object at that index is stamina cheese
|
||||
@@ -103,109 +160,7 @@ pub fn stars(
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let mut buf = vec![0.0; skills[0].strain_peaks.len()];
|
||||
|
||||
let color_rating = skills[0].difficulty_value(&mut buf) * COLOR_SKILL_MULTIPLIER;
|
||||
let rhythm_rating = skills[1].difficulty_value(&mut buf) * RHYTHM_SKILL_MULTIPLIER;
|
||||
|
||||
let mut stamina_rating = (skills[2].difficulty_value(&mut buf)
|
||||
+ skills[3].difficulty_value(&mut buf))
|
||||
* STAMINA_SKILL_MULTIPLIER;
|
||||
|
||||
let stamina_penalty = simple_color_penalty(stamina_rating, color_rating);
|
||||
stamina_rating *= stamina_penalty;
|
||||
|
||||
let combined_rating = locally_combined_difficulty(&skills, stamina_penalty);
|
||||
let separate_rating = norm(1.5, color_rating, rhythm_rating, stamina_rating);
|
||||
|
||||
let stars = rescale(1.4 * separate_rating + 0.5 * combined_rating);
|
||||
|
||||
TaikoDifficultyAttributes { stars }
|
||||
}
|
||||
|
||||
/// Essentially the same as the [`stars`] function but instead of
|
||||
/// evaluating the final strains, it just returns them as is.
|
||||
///
|
||||
/// Suitable to plot the difficulty of a map over time.
|
||||
pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
if map.hit_objects.len() < 2 {
|
||||
return Strains::default();
|
||||
}
|
||||
|
||||
// True if the object at that index is stamina cheese
|
||||
let cheese = map.find_cheese();
|
||||
|
||||
let mut skills = vec![
|
||||
Skill::new(SkillKind::color()),
|
||||
Skill::new(SkillKind::rhythm()),
|
||||
Skill::new(SkillKind::stamina(true)),
|
||||
Skill::new(SkillKind::stamina(false)),
|
||||
];
|
||||
|
||||
let clock_rate = mods.speed();
|
||||
let section_len = SECTION_LEN * clock_rate;
|
||||
|
||||
// No strain for first object
|
||||
let mut current_section_end =
|
||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||
|
||||
let mut hit_objects = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(2)
|
||||
.zip(map.hit_objects.iter().skip(1))
|
||||
.zip(map.hit_objects.iter())
|
||||
.map(|(((idx, base), prev), prev_prev)| {
|
||||
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
||||
});
|
||||
|
||||
// Handle second object separately to remove later if-branching
|
||||
let h = hit_objects.next().unwrap();
|
||||
|
||||
while h.base.start_time > current_section_end {
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h, &cheese);
|
||||
}
|
||||
|
||||
// Handle all other objects
|
||||
for h in hit_objects {
|
||||
while h.base.start_time > current_section_end {
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
skill.start_new_section_from(current_section_end / clock_rate);
|
||||
}
|
||||
|
||||
current_section_end += section_len;
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.process(&h, &cheese);
|
||||
}
|
||||
}
|
||||
|
||||
for skill in skills.iter_mut() {
|
||||
skill.save_current_peak();
|
||||
}
|
||||
|
||||
let strains = skills[0]
|
||||
.strain_peaks
|
||||
.iter()
|
||||
.zip(skills[1].strain_peaks.iter())
|
||||
.zip(skills[2].strain_peaks.iter())
|
||||
.zip(skills[3].strain_peaks.iter())
|
||||
.map(|(((color, rhythm), stamina_right), stamina_left)| {
|
||||
color + rhythm + stamina_right + stamina_left
|
||||
})
|
||||
.collect();
|
||||
|
||||
Strains {
|
||||
section_length: section_len,
|
||||
strains,
|
||||
}
|
||||
Some(skills)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
Reference in New Issue
Block a user