mod colours;
mod difficulty_object;
mod gradual_difficulty;
mod gradual_performance;
mod pp;
mod rim;
mod skills;
mod taiko_object;
use std::{borrow::Cow, cell::RefCell, rc::Rc};
pub use self::{
gradual_difficulty::*, gradual_performance::*, pp::*,
taiko_object::TaikoObjectPub as TaikoObject,
};
pub(crate) use self::taiko_object::IntoTaikoObjectIter;
use crate::{beatmap::BeatmapHitWindows, Beatmap, GameMode, Mods, OsuStars};
use self::{
colours::ColourDifficultyPreprocessor,
difficulty_object::{MonoIndex, ObjectLists, TaikoDifficultyObject},
skills::{Peaks, PeaksDifficultyValues, PeaksRaw, Skill},
};
const SECTION_LEN: usize = 400;
const DIFFICULTY_MULTIPLIER: f64 = 1.35;
#[derive(Clone, Debug)]
pub struct TaikoStars<'map> {
map: Cow<'map, Beatmap>,
mods: u32,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
is_convert: bool,
}
impl<'map> TaikoStars<'map> {
#[inline]
pub fn new(map: &'map Beatmap) -> Self {
let map = map.convert_mode(GameMode::Taiko);
let is_convert = matches!(map, Cow::Owned(_));
Self {
map,
mods: 0,
passed_objects: None,
clock_rate: None,
is_convert,
}
}
#[inline]
pub fn mods(mut self, mods: u32) -> Self {
self.mods = mods;
self
}
#[inline]
pub fn passed_objects(mut self, passed_objects: usize) -> Self {
self.passed_objects = Some(passed_objects);
self
}
#[inline]
pub fn clock_rate(mut self, clock_rate: f64) -> Self {
self.clock_rate = Some(clock_rate);
self
}
#[inline]
pub fn is_convert(mut self, is_convert: bool) -> Self {
self.is_convert = is_convert;
self
}
#[inline]
pub fn calculate(self) -> TaikoDifficultyAttributes {
let clock_rate = self.clock_rate.unwrap_or_else(|| self.mods.clock_rate());
let BeatmapHitWindows { od: hit_window, .. } = self
.map
.attributes()
.mods(self.mods)
.clock_rate(clock_rate)
.hit_windows();
let is_convert = self.is_convert || matches!(self.map, Cow::Owned(_));
let (peaks, max_combo) = calculate_skills(self);
let PeaksDifficultyValues {
mut colour_rating,
mut rhythm_rating,
mut stamina_rating,
mut combined_rating,
} = peaks.difficulty_values();
colour_rating *= DIFFICULTY_MULTIPLIER;
rhythm_rating *= DIFFICULTY_MULTIPLIER;
stamina_rating *= DIFFICULTY_MULTIPLIER;
combined_rating *= DIFFICULTY_MULTIPLIER;
let mut star_rating = rescale(combined_rating * 1.4);
if is_convert {
star_rating *= 0.925;
if colour_rating < 2.0 && stamina_rating > 8.0 {
star_rating *= 0.8;
}
}
TaikoDifficultyAttributes {
stamina: stamina_rating,
rhythm: rhythm_rating,
colour: colour_rating,
peak: combined_rating,
hit_window,
stars: star_rating,
max_combo,
}
}
#[inline]
pub fn strains(self) -> TaikoStrains {
let (peaks, _) = calculate_skills(self);
let PeaksRaw {
colour,
rhythm,
stamina,
} = peaks.into_raw();
TaikoStrains {
section_len: SECTION_LEN as f64,
color: colour,
rhythm,
stamina,
}
}
}
#[derive(Clone, Debug)]
pub struct TaikoStrains {
pub section_len: f64,
pub color: Vec<f64>,
pub rhythm: Vec<f64>,
pub stamina: Vec<f64>,
}
impl TaikoStrains {
#[inline]
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.color.len()
}
}
fn calculate_skills(params: TaikoStars<'_>) -> (Peaks, usize) {
let TaikoStars {
map,
mods,
passed_objects,
clock_rate,
is_convert: _,
} = params;
let mut take = passed_objects.unwrap_or(map.hit_objects.len());
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
let mut peaks = Peaks::new();
let mut max_combo = 0;
let mut diff_objects = map
.taiko_objects()
.take_while(|(h, _)| {
if h.is_hit {
if take == 0 {
return false;
}
max_combo += 1;
take -= 1;
}
true
})
.skip(2)
.zip(map.hit_objects.iter().skip(1))
.zip(map.hit_objects.iter())
.enumerate()
.fold(
ObjectLists::default(),
|mut lists, (idx, (((base, base_start_time), last), last_last))| {
let diff_obj = TaikoDifficultyObject::new(
base,
base_start_time,
last.start_time,
last_last.start_time,
clock_rate,
&lists,
idx,
);
match &diff_obj.mono_idx {
MonoIndex::Centre(_) => lists.centres.push(idx),
MonoIndex::Rim(_) => lists.rims.push(idx),
MonoIndex::None => {}
}
if diff_obj.note_idx.is_some() {
lists.notes.push(idx);
}
lists.all.push(Rc::new(RefCell::new(diff_obj)));
lists
},
);
ColourDifficultyPreprocessor::process_and_assign(&mut diff_objects);
for hit_object in diff_objects.all.iter() {
peaks.process(&hit_object.borrow(), &diff_objects);
}
(peaks, max_combo)
}
#[inline]
fn rescale(stars: f64) -> f64 {
if stars < 0.0 {
stars
} else {
10.43 * (stars / 8.0 + 1.0).ln()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TaikoDifficultyAttributes {
pub stamina: f64,
pub rhythm: f64,
pub colour: f64,
pub peak: f64,
pub hit_window: f64,
pub stars: f64,
pub max_combo: usize,
}
impl TaikoDifficultyAttributes {
#[inline]
pub fn max_combo(&self) -> usize {
self.max_combo
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TaikoPerformanceAttributes {
pub difficulty: TaikoDifficultyAttributes,
pub pp: f64,
pub pp_acc: f64,
pub pp_difficulty: f64,
pub effective_miss_count: f64,
}
impl TaikoPerformanceAttributes {
#[inline]
pub fn stars(&self) -> f64 {
self.difficulty.stars
}
#[inline]
pub fn pp(&self) -> f64 {
self.pp
}
#[inline]
pub fn max_combo(&self) -> usize {
self.difficulty.max_combo
}
}
impl From<TaikoPerformanceAttributes> for TaikoDifficultyAttributes {
#[inline]
fn from(attributes: TaikoPerformanceAttributes) -> Self {
attributes.difficulty
}
}
impl<'map> From<OsuStars<'map>> for TaikoStars<'map> {
#[inline]
fn from(osu: OsuStars<'map>) -> Self {
let OsuStars {
map,
mods,
passed_objects,
clock_rate,
} = osu;
Self {
map: map.convert_mode(GameMode::Taiko),
mods,
passed_objects,
clock_rate,
is_convert: true,
}
}
}