added OsuDifficultyAttributesIter struct

adjusted changelog
This commit is contained in:
MaxOhn
2021-11-22 00:40:06 +01:00
parent 86bae8c190
commit 331115758c
9 changed files with 518 additions and 170 deletions
+1
View File
@@ -9,6 +9,7 @@
- Added method `Beatmap::bpm`
- Added method `max_combo` for `DifficultyAttributes`, `PerformanceAttributes`, and all `{Mode}PerformanceAttributes`
- [BREAKING] Renamed the `attributes` field to `difficulty` for all `{Mode}PerformanceAttributes` structs
- Added `OsuDifficultyAttributesIter`. Suitable to calculate a map's difficulty after every or every few objects instead of calling the `stars` function over and over.
# v0.3.0
+1
View File
@@ -37,6 +37,7 @@ impl<'p> ControlPointIter<'p> {
}
}
#[derive(Debug)]
pub(crate) enum ControlPoint {
Timing { time: f64, beat_len: f64 },
Difficulty { time: f64, slider_velocity: f64 },
-30
View File
@@ -470,33 +470,3 @@ impl From<FruitsPerformanceAttributes> for FruitsDifficultyAttributes {
attributes.difficulty
}
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
// #[ignore]
fn custom_fruits() {
use std::time::Instant;
use crate::{Beatmap, FruitsPP};
let path = "E:Games/osu!/beatmaps/2919116_.osu";
let map = Beatmap::from_path(path).unwrap();
let start = Instant::now();
let result = FruitsPP::new(&map).mods(256).calculate();
let iters = 100;
let accum = start.elapsed();
// * Tiny benchmark for pp calculation
// let mut accum = accum;
// for _ in 0..iters {
// let start = Instant::now();
// let _result = OsuPP::new(&map).mods(0).calculate();
// accum += start.elapsed();
// }
println!("{:#?}", result);
println!("Calculation average: {:?}", accum / iters);
}
+317
View File
@@ -0,0 +1,317 @@
use std::{iter, mem, vec::IntoIter};
use crate::{
curve::CurveBuffers, osu::difficulty_object::DifficultyObject, parse::Pos2, Beatmap, Mods,
};
use super::{
calculate_star_rating, difficulty_range_ar, difficulty_range_od, old_stacking,
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
scaling_factor::ScalingFactor,
skill::{Skill, Skills},
slider_state::SliderState,
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, SECTION_LEN,
};
/// Iterate over a map's hit objects and update the difficulty attributes each time.
///
/// Note that this struct implements [`Iterator`](std::iter::Iterator).
/// On every call of [`Iterator::next`](std::iter::Iterator::next), the map's next hit object will
/// be processed and the [`OsuDifficultyAttributes`](`crate::osu::OsuDifficultyAttributes`)
/// will be updated and returned.
///
/// TODO: Mention struct that does the same for performance
///
/// # Example
///
/// ```
/// use rosu_pp::{Beatmap, osu::OsuDifficultyAttributesIter};
///
/// # /*
/// let map: Beatmap = ...
/// # */
/// # let map = Beatmap::default();
///
/// let mods = 64; // DT
/// let mut iter = OsuDifficultyAttributesIter::new(&map, mods);
///
/// let attrs1 = iter.next(); // the difficulty of the map after the first hit object
/// let attrs2 = iter.next(); // after the second hit object
///
/// // Remaining hit objects
/// for difficulty in iter {
/// // ...
/// }
/// ```
#[derive(Debug)]
pub struct OsuDifficultyAttributesIter {
idx: usize,
attributes: OsuDifficultyAttributes,
clock_rate: f64,
hit_objects: OsuObjectIter,
skills: Skills,
prev_prev: Option<OsuObject>,
prev: OsuObject,
curr_section_end: f64,
strain_peak_buf: Vec<f64>,
}
impl OsuDifficultyAttributesIter {
/// Create a new difficulty attributes iterator for osu!standard maps.
pub fn new(map: &Beatmap, mods: impl Mods) -> Self {
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;
let mut raw_ar = map.ar as f64;
let hr = mods.hr();
if hr {
raw_ar = (raw_ar * 1.4).min(10.0);
} 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(),
};
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 attributes = OsuDifficultyAttributes {
ar: map_attributes.ar,
hp: map_attributes.hp,
od,
..Default::default()
};
let skills = Skills::new(hit_window, mods.rx(), scaling_factor.radius(), mods.fl());
let hit_objects = OsuObjectIter {
hit_objects: hit_objects.into_iter(),
scaling_factor,
};
let prev_prev = None;
let prev = OsuObject {
time: 0.0,
pos: Pos2::zero(),
stack_height: 0.0,
kind: OsuObjectKind::Circle,
};
let curr_section_end =
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
Self {
idx: 0,
attributes,
clock_rate: map_attributes.clock_rate,
hit_objects,
skills,
curr_section_end,
prev_prev,
prev,
strain_peak_buf: Vec::new(),
}
}
}
impl Iterator for OsuDifficultyAttributesIter {
type Item = OsuDifficultyAttributes;
fn next(&mut self) -> Option<Self::Item> {
let curr = self.hit_objects.next()?;
self.attributes.max_combo += 1;
match &curr.kind {
OsuObjectKind::Circle => self.attributes.n_circles += 1,
OsuObjectKind::Slider { nested_objects, .. } => {
self.attributes.max_combo += nested_objects.len();
self.attributes.n_sliders += 1
}
OsuObjectKind::Spinner { .. } => self.attributes.n_spinners += 1,
};
self.idx += 1;
if self.idx == 1 {
self.prev = curr;
return Some(self.attributes.clone());
}
let h = DifficultyObject::new(
&curr,
&mut self.prev,
self.prev_prev.as_ref(),
&self.hit_objects.scaling_factor,
self.clock_rate,
);
let base_time = h.base.time / self.clock_rate;
if self.idx == 2 {
while base_time > self.curr_section_end {
self.skills.start_new_section_from(self.curr_section_end);
self.curr_section_end += SECTION_LEN;
}
} else {
while base_time > self.curr_section_end {
self.skills
.save_peak_and_start_new_section(self.curr_section_end);
self.curr_section_end += SECTION_LEN;
}
}
self.skills.process(&h);
self.prev_prev = Some(mem::replace(&mut self.prev, curr));
if self.hit_objects.len() == 0 {
self.skills.save_current_peak();
}
let missing = self.skills.aim().strain_peaks.len() - self.strain_peak_buf.len();
self.strain_peak_buf.extend(iter::repeat(0.0).take(missing));
let aim_rating = {
let aim = self.skills.aim();
self.strain_peak_buf.copy_from_slice(&aim.strain_peaks);
Skill::difficulty_value(&mut self.strain_peak_buf, aim).sqrt() * DIFFICULTY_MULTIPLIER
};
let slider_factor = if aim_rating > 0.0 {
let aim_no_sliders = self.skills.aim_no_sliders();
self.strain_peak_buf
.copy_from_slice(&aim_no_sliders.strain_peaks);
let aim_rating_no_sliders =
Skill::difficulty_value(&mut self.strain_peak_buf, aim_no_sliders).sqrt()
* DIFFICULTY_MULTIPLIER;
aim_rating_no_sliders / aim_rating
} else {
1.0
};
let (speed, flashlight) = self.skills.speed_flashlight();
let speed_rating = if let Some(speed) = speed {
self.strain_peak_buf.copy_from_slice(&speed.strain_peaks);
Skill::difficulty_value(&mut self.strain_peak_buf, speed).sqrt() * DIFFICULTY_MULTIPLIER
} else {
0.0
};
let flashlight_rating = if let Some(flashlight) = flashlight {
self.strain_peak_buf
.copy_from_slice(&flashlight.strain_peaks);
Skill::difficulty_value(&mut self.strain_peak_buf, flashlight).sqrt()
* DIFFICULTY_MULTIPLIER
} else {
0.0
};
let star_rating = calculate_star_rating(aim_rating, speed_rating, flashlight_rating);
self.attributes.aim_strain = aim_rating;
self.attributes.speed_strain = speed_rating;
self.attributes.flashlight_rating = flashlight_rating;
self.attributes.slider_factor = slider_factor;
self.attributes.stars = star_rating;
Some(self.attributes.clone())
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.hit_objects.size_hint()
}
}
impl ExactSizeIterator for OsuDifficultyAttributesIter {
#[inline]
fn len(&self) -> usize {
self.hit_objects.len()
}
}
#[derive(Debug)]
struct OsuObjectIter {
hit_objects: IntoIter<OsuObject>,
scaling_factor: ScalingFactor,
}
impl Iterator for OsuObjectIter {
type Item = OsuObject;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let mut h = self.hit_objects.next()?;
let stack_offset = self.scaling_factor.stack_offset(h.stack_height);
h.pos += stack_offset;
Some(h)
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.hit_objects.size_hint()
}
}
impl ExactSizeIterator for OsuObjectIter {
#[inline]
fn len(&self) -> usize {
self.hit_objects.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_map() {
let map = Beatmap::default();
assert!(OsuDifficultyAttributesIter::new(&map, 0).next().is_none());
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
fn iter_end_eq_regular() {
let map = Beatmap::from_path("./maps/2785319.osu").expect("failed to parse map");
let mods = 64;
let regular = crate::osu::stars(&map, mods, None);
let iter_end = OsuDifficultyAttributesIter::new(&map, mods)
.reduce(|_, next| next)
.expect("empty iter");
assert_eq!(regular, iter_end);
}
}
+83 -129
View File
@@ -1,5 +1,6 @@
#![cfg(feature = "osu")]
mod difficulty_iter;
mod difficulty_object;
mod osu_object;
mod pp;
@@ -8,6 +9,9 @@ mod skill;
mod skill_kind;
mod slider_state;
use std::mem;
pub use difficulty_iter::OsuDifficultyAttributesIter;
use difficulty_object::DifficultyObject;
use osu_object::{ObjectParameters, OsuObject};
pub use pp::*;
@@ -18,6 +22,8 @@ use slider_state::SliderState;
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
use self::skill::Skills;
const SECTION_LEN: f64 = 400.0;
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
@@ -47,26 +53,56 @@ pub fn stars(
}
};
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let aim_rating = {
let aim = skills.aim();
let mut aim_strains = mem::take(&mut aim.strain_peaks);
Skill::difficulty_value(&mut aim_strains, aim).sqrt() * DIFFICULTY_MULTIPLIER
};
let slider_factor = if aim_rating > 0.0 {
let aim_rating_no_sliders = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let aim_no_sliders = skills.aim_no_sliders();
let mut aim_strains_no_sliders = mem::take(&mut aim_no_sliders.strain_peaks);
let aim_rating_no_sliders =
Skill::difficulty_value(&mut aim_strains_no_sliders, aim_no_sliders).sqrt()
* DIFFICULTY_MULTIPLIER;
aim_rating_no_sliders / aim_rating
} else {
1.0
};
let speed_rating = if mods.rx() {
0.0
let (speed, flashlight) = skills.speed_flashlight();
let speed_rating = if let Some(speed) = speed {
let mut speed_strains = mem::take(&mut speed.strain_peaks);
Skill::difficulty_value(&mut speed_strains, speed).sqrt() * DIFFICULTY_MULTIPLIER
} else {
skills[2].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
0.0
};
let flashlight_rating = skills.get_mut(3).map_or(0.0, |skill| {
skill.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER
});
let flashlight_rating = if let Some(flashlight) = flashlight {
let mut flashlight_strains = mem::take(&mut flashlight.strain_peaks);
Skill::difficulty_value(&mut flashlight_strains, flashlight).sqrt() * DIFFICULTY_MULTIPLIER
} else {
0.0
};
let star_rating = calculate_star_rating(aim_rating, speed_rating, flashlight_rating);
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
}
fn calculate_star_rating(aim_rating: f64, speed_rating: f64, flashlight_rating: f64) -> f64 {
let base_aim_performance = {
let base = 5.0 * (aim_rating / 0.0675).max(1.0) - 4.0;
@@ -79,32 +115,20 @@ pub fn stars(
base * base * base / 100_000.0
};
let base_flashlight_performance = if mods.fl() {
flashlight_rating * flashlight_rating * 25.0
} else {
0.0
};
let base_flashlight_performance = flashlight_rating * flashlight_rating * 25.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 {
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
@@ -117,25 +141,29 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
None => return Strains::default(),
};
skills.reverse();
let mut aim = mem::take(&mut skills.aim().strain_peaks);
let tuple = skills.speed_flashlight();
let _ = skills.pop();
let aim_strains = skills.pop().unwrap().strain_peaks; // no sliders
let speed_strains = skills.pop().unwrap().strain_peaks;
let strains = match tuple {
(Some(speed), Some(flashlight)) => {
for ((aim, speed), flashlight) in aim
.iter_mut()
.zip(&speed.strain_peaks)
.zip(&flashlight.strain_peaks)
{
*aim += speed + flashlight;
}
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()
aim
}
(Some(strains), None) | (None, Some(strains)) => {
for (aim, strain) in aim.iter_mut().zip(&strains.strain_peaks) {
*aim += strain;
}
aim
}
(None, None) => aim,
};
Strains {
@@ -148,7 +176,7 @@ fn calculate_skills(
map: &Beatmap,
mods: impl Mods,
passed_objects: Option<usize>,
) -> Option<(Vec<Skill>, OsuDifficultyAttributes)> {
) -> Option<(Skills, OsuDifficultyAttributes)> {
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
let map_attributes = map.attributes().mods(mods);
@@ -203,23 +231,13 @@ fn calculate_skills(
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 skills = Skills::new(hit_window, mods.rx(), scaling_factor.radius(), mods.fl());
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 =
let mut curr_section_end =
(prev.time / map_attributes.clock_rate / SECTION_LEN).ceil() * SECTION_LEN;
// Handle second object separately to remove later if-branching
@@ -234,20 +252,13 @@ fn calculate_skills(
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;
while base_time > curr_section_end {
skills.start_new_section_from(curr_section_end);
curr_section_end += SECTION_LEN;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
skills.process(&h);
prev_prev = Some(mem::replace(&mut prev, curr));
// Handle all other objects
for curr in hit_objects {
@@ -261,26 +272,16 @@ fn calculate_skills(
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;
while base_time > curr_section_end {
skills.save_peak_and_start_new_section(curr_section_end);
curr_section_end += SECTION_LEN;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
skills.process(&h);
prev_prev = Some(mem::replace(&mut prev, curr));
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
skills.save_current_peak();
let attributes = OsuDifficultyAttributes {
ar: map_attributes.ar,
@@ -445,7 +446,7 @@ fn lerp(start: f64, end: f64, percent: f64) -> f64 {
}
/// The result of a difficulty calculation on an osu!standard map.
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct OsuDifficultyAttributes {
/// The aim portion of the total strain.
pub aim_strain: f64,
@@ -520,50 +521,3 @@ impl From<OsuPerformanceAttributes> for OsuDifficultyAttributes {
fn difficulty_range_od(od: f64) -> f64 {
super::difficulty_range(od, 20.0, 50.0, 80.0)
}
#[cfg(not(any(feature = "async_tokio", feature = "async_std")))]
#[test]
// #[ignore]
fn custom_osu() {
use std::time::Instant;
use crate::{Beatmap, OsuPP};
let path = "E:Games/osu!/beatmaps/116169_.osu";
let start = Instant::now();
let map = Beatmap::from_path(path).unwrap();
let iters = 100;
let accum = start.elapsed();
// * Tiny benchmark for map parsing
// let mut accum = accum;
// for _ in 0..iters {
// let file = File::open(path).unwrap();
// let start = Instant::now();
// let _map = Beatmap::parse(file).unwrap();
// accum += start.elapsed();
// }
println!("Parsing average: {:?}", accum / iters);
let start = Instant::now();
let result = OsuPP::new(&map).mods(2 + 64).calculate();
let iters = 100;
let accum = start.elapsed();
// * Tiny benchmark for pp calculation
// let mut accum = accum;
// for _ in 0..iters {
// let start = Instant::now();
// let _result = OsuPP::new(&map).mods(0).calculate();
// accum += start.elapsed();
// }
println!("{:#?}", result);
println!("Calculation average: {:?}", accum / iters);
}
+1
View File
@@ -11,6 +11,7 @@ use crate::{
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
const BASE_SCORING_DISTANCE: f64 = 100.0;
#[derive(Debug)]
pub(crate) struct OsuObject {
pub(crate) time: f64,
pub(crate) pos: Pos2,
+1
View File
@@ -4,6 +4,7 @@ use super::NORMALIZED_RADIUS;
const OBJECT_RADIUS: f32 = 64.0;
#[derive(Debug)]
pub(crate) struct ScalingFactor {
adjusted_factor: f32,
factor: f32,
+103 -10
View File
@@ -1,9 +1,92 @@
use super::{lerp, skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKind};
use std::cmp::Ordering;
use std::{cmp::Ordering, fmt};
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
#[derive(Debug)]
pub(crate) struct Skills {
skills: Box<[Skill]>,
mask: u8,
}
impl Skills {
const RX: u8 = 1 << 0;
const FL: u8 = 1 << 1;
pub(crate) fn new(hit_window: f64, rx: bool, radius: f32, fl: bool) -> Self {
let mut skills = Vec::with_capacity(2 + !rx as usize + fl as usize);
skills.push(Skill::aim(true));
skills.push(Skill::aim(false));
if !rx {
skills.push(Skill::speed(hit_window));
}
if fl {
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
let scaling_factor = 52.0 / radius as f64;
skills.push(Skill::flashlight(scaling_factor));
}
let mask = rx as u8 * Self::RX + fl as u8 * Self::FL;
let skills = skills.into_boxed_slice();
Self { skills, mask }
}
pub(crate) fn start_new_section_from(&mut self, curr_section_end: f64) {
for skill in self.skills.iter_mut() {
skill.start_new_section_from(curr_section_end);
}
}
pub(crate) fn save_peak_and_start_new_section(&mut self, curr_section_end: f64) {
for skill in self.skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(curr_section_end);
}
}
pub(crate) fn save_current_peak(&mut self) {
for skill in self.skills.iter_mut() {
skill.save_current_peak();
}
}
pub(crate) fn process(&mut self, h: &DifficultyObject<'_>) {
for skill in self.skills.iter_mut() {
skill.process(h);
}
}
pub(crate) fn aim(&mut self) -> &mut Skill {
&mut self.skills[0]
}
pub(crate) fn aim_no_sliders(&mut self) -> &mut Skill {
&mut self.skills[1]
}
pub(crate) fn speed_flashlight(&mut self) -> (Option<&mut Skill>, Option<&mut Skill>) {
match (self.mask & Self::RX, self.mask & Self::FL) {
// only speed
(0, 0) => (Some(&mut self.skills[2]), None),
// both speed and flashlight
(0, _) => {
let (left, right) = self.skills.split_at_mut(3);
(Some(&mut left[2]), Some(&mut right[0]))
}
// neither
(_, 0) => (None, None),
// only flashlight
(_, _) => (None, Some(&mut self.skills[2])),
}
}
}
pub(crate) struct Skill {
curr_strain: f64,
curr_section_peak: f64,
@@ -62,7 +145,7 @@ impl Skill {
self.curr_section_peak = self.calculate_initial_strain(time);
}
pub(crate) fn difficulty_value(&mut self) -> f64 {
pub(crate) fn difficulty_value(strain_peaks: &mut [f64], this: &Self) -> f64 {
// ? Common values to debug
// println!("---");
@@ -72,15 +155,14 @@ impl Skill {
let mut difficulty = 0.0;
let mut weight = 1.0;
let decay_weight = self.kind.decay_weight();
let decay_weight = this.kind.decay_weight();
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
let (reduced_section_count, difficulty_multiplier) = this.kind.difficulty_values();
let reduced_section_count_f64 = reduced_section_count as f64;
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
let peaks = self.strain_peaks.iter_mut();
let peaks = strain_peaks.iter_mut();
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
let clamped = (i as f64 / reduced_section_count_f64).clamp(0.0, 1.0);
@@ -88,10 +170,9 @@ impl Skill {
*strain *= lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
}
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
strain_peaks.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in self.strain_peaks.iter() {
for &strain in strain_peaks.iter() {
difficulty += strain * weight;
weight *= decay_weight;
}
@@ -127,3 +208,15 @@ impl Skill {
}
}
}
impl fmt::Debug for Skill {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Skill")
.field("curr_strain", &self.curr_strain)
.field("curr_section_peak", &self.curr_section_peak)
.field("kind", &self.kind)
.field("strain_peaks_len", &self.strain_peaks.len())
.field("prev_time", &self.prev_time)
.finish()
}
}
+11 -1
View File
@@ -1,7 +1,7 @@
use std::{
collections::VecDeque,
f64::consts::{FRAC_PI_2, PI},
iter,
fmt, iter,
};
use crate::parse::Pos2;
@@ -565,3 +565,13 @@ fn calculate_wide_angle_bonus(angle: f64) -> f64 {
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
1.0 - calculate_wide_angle_bonus(angle)
}
impl fmt::Debug for SkillKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Aim { .. } => f.debug_struct("Aim").finish(),
Self::Flashlight { .. } => f.debug_struct("Flashlight").finish(),
Self::Speed { .. } => f.debug_struct("Speed").finish(),
}
}
}