refactored strain functions

This commit is contained in:
MaxOhn
2021-11-20 20:38:31 +01:00
parent 7883bf85cc
commit 8aa1631af2
4 changed files with 259 additions and 643 deletions
+49 -259
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]