#![cfg(feature = "mania")]
mod pp;
mod strain;
pub use pp::*;
use strain::Strain;
use crate::{parse::HitObject, Beatmap, GameMode, Mods, StarResult, Strains};
const SECTION_LEN: f32 = 400.0;
const STAR_SCALING_FACTOR: f32 = 0.018;
pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> StarResult {
let take = passed_objects.unwrap_or_else(|| map.hit_objects.len());
if take < 2 {
return StarResult::Mania(DifficultyAttributes::default());
}
let rounded_cs = map.cs.round();
let columns = match map.mode {
GameMode::MNA => rounded_cs.max(1.0) as u8,
GameMode::STD => {
let rounded_od = map.od.round();
let n_objects = map.n_circles + map.n_sliders + map.n_spinners;
let slider_or_spinner_ratio = (n_objects - map.n_circles) as f32 / n_objects as f32;
if slider_or_spinner_ratio < 0.2 {
7
} else if slider_or_spinner_ratio < 0.3 || rounded_cs >= 5.0 {
6 + (rounded_od > 5.0) as u8
} else if slider_or_spinner_ratio > 0.6 {
4 + (rounded_od > 4.0) as u8
} else {
(rounded_od as u8 + 1).max(4).min(7)
}
}
other => panic!("can not calculate mania difficulty on a {:?} map", other),
};
let clock_rate = mods.speed();
let section_len = SECTION_LEN * clock_rate;
let mut strain = Strain::new(columns);
let columns = columns as f32;
let mut hit_objects = map
.hit_objects
.iter()
.take(take)
.skip(1)
.zip(map.hit_objects.iter())
.map(|(base, prev)| DifficultyHitObject::new(base, prev, columns, clock_rate));
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let h = hit_objects.next().unwrap();
while h.base.start_time > current_section_end {
current_section_end += section_len;
}
strain.process(&h);
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();
let stars = strain.difficulty_value() * STAR_SCALING_FACTOR;
StarResult::Mania(DifficultyAttributes { stars })
}
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));
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let h = hit_objects.next().unwrap();
while h.base.start_time > current_section_end {
current_section_end += section_len;
}
strain.process(&h);
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,
}
}
#[derive(Debug)]
pub(crate) struct DifficultyHitObject<'o> {
base: &'o HitObject,
column: usize,
delta: f32,
start_time: f32,
}
impl<'o> DifficultyHitObject<'o> {
#[inline]
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f32) -> Self {
let x_divisor = 512.0 / columns;
let column = (base.pos.x / x_divisor).floor().min(columns - 1.0) as usize;
Self {
base,
column,
delta: (base.start_time - prev.start_time) / clock_rate,
start_time: base.start_time / clock_rate,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct DifficultyAttributes {
pub stars: f32,
}