initial rewrite
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{
|
||||
catch::{Catch, CatchBeatmap},
|
||||
mania::{Mania, ManiaBeatmap},
|
||||
model::beatmap::{Beatmap, Converted},
|
||||
osu::{Osu, OsuBeatmap},
|
||||
taiko::{Taiko, TaikoBeatmap},
|
||||
};
|
||||
|
||||
pub use self::mode::ModeDifficulty;
|
||||
|
||||
use super::attributes::DifficultyAttributes;
|
||||
|
||||
mod mode;
|
||||
pub mod object;
|
||||
pub mod skills;
|
||||
|
||||
/// Difficulty calculator on maps of any mode.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Difficulty<'map> {
|
||||
map: Cow<'map, Beatmap>,
|
||||
is_convert: bool,
|
||||
inner: ModeDifficulty,
|
||||
}
|
||||
|
||||
impl<'map> Difficulty<'map> {
|
||||
/// Create a new difficulty calculator for the given beatmap.
|
||||
pub fn new(map: &'map Beatmap) -> Self {
|
||||
Self::new_with_is_convert(Cow::Borrowed(map), false)
|
||||
}
|
||||
|
||||
fn new_with_is_convert(map: Cow<'map, Beatmap>, is_convert: bool) -> Self {
|
||||
Self {
|
||||
map,
|
||||
is_convert,
|
||||
inner: ModeDifficulty::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_mode {
|
||||
( $mode:ident ) => {
|
||||
impl<'a> From<Converted<'a, $mode>> for Difficulty<'a> {
|
||||
fn from(converted: Converted<'a, $mode>) -> Self {
|
||||
Self::new_with_is_convert(converted.map, converted.is_convert)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b: 'a> From<&'b Converted<'a, $mode>> for Difficulty<'a> {
|
||||
fn from(converted: &'b Converted<'a, $mode>) -> Self {
|
||||
Self::new_with_is_convert(
|
||||
Cow::Borrowed(converted.map.as_ref()),
|
||||
converted.is_convert,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_mode!(Osu);
|
||||
impl_from_mode!(Taiko);
|
||||
impl_from_mode!(Catch);
|
||||
impl_from_mode!(Mania);
|
||||
|
||||
impl Difficulty<'_> {
|
||||
/// Attempt to convert the map to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, `None` is returned.
|
||||
///
|
||||
/// If the given mode should be ignored in case it is incompatible, use
|
||||
/// [`mode_or_ignore`] instead.
|
||||
///
|
||||
/// [`mode_or_ignore`]: Self::mode_or_ignore
|
||||
pub fn try_mode(&mut self, mode: GameMode) -> Option<&mut Self> {
|
||||
let (map, is_convert) = match mode {
|
||||
GameMode::Osu => {
|
||||
let converted = OsuBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Taiko => {
|
||||
let converted = TaikoBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Catch => {
|
||||
let converted = CatchBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Mania => {
|
||||
let converted = ManiaBeatmap::try_from_ref(self.map.as_ref())?;
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(map, Cow::Owned(_)) {
|
||||
self.is_convert |= is_convert;
|
||||
let map = map.into_owned();
|
||||
self.map = Cow::Owned(map);
|
||||
}
|
||||
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Attempt to convert the map to the specified mode.
|
||||
///
|
||||
/// If the conversion is incompatible, the map won't be modified.
|
||||
///
|
||||
/// To see whether the given mode is incompatible, use [`try_mode`]
|
||||
/// instead.
|
||||
///
|
||||
/// [`try_mode`]: Self::try_mode
|
||||
pub fn mode_or_ignore(&mut self, mode: GameMode) -> &mut Self {
|
||||
let (map, is_convert) = match mode {
|
||||
GameMode::Osu => {
|
||||
let Some(converted) = OsuBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Taiko => {
|
||||
let Some(converted) = TaikoBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Catch => {
|
||||
let Some(converted) = CatchBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
GameMode::Mania => {
|
||||
let Some(converted) = ManiaBeatmap::try_from_ref(self.map.as_ref()) else {
|
||||
return self;
|
||||
};
|
||||
|
||||
(converted.map, converted.is_convert)
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(map, Cow::Owned(_)) {
|
||||
self.is_convert |= is_convert;
|
||||
let map = map.into_owned();
|
||||
self.map = Cow::Owned(map);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
pub fn mods(&mut self, mods: u32) -> &mut Self {
|
||||
self.inner.mods(mods);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
pub fn passed_objects(&mut self, passed_objects: u32) -> &mut Self {
|
||||
self.inner.passed_objects(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
///
|
||||
/// If none is specified, it will take the clock rate based on the mods
|
||||
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
|
||||
pub fn clock_rate(&mut self, clock_rate: f64) -> &mut Self {
|
||||
self.inner.clock_rate(clock_rate);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the difficulty calculation.
|
||||
///
|
||||
/// The returned attributes depend on the map's mode.
|
||||
pub fn calculate(&self) -> DifficultyAttributes {
|
||||
let map = Cow::Borrowed(self.map.as_ref());
|
||||
|
||||
match self.map.mode {
|
||||
GameMode::Osu => DifficultyAttributes::Osu(
|
||||
self.inner
|
||||
.calculate(&Converted::<Osu>::new(map, self.is_convert)),
|
||||
),
|
||||
GameMode::Taiko => DifficultyAttributes::Taiko(
|
||||
self.inner
|
||||
.calculate(&Converted::<Taiko>::new(map, self.is_convert)),
|
||||
),
|
||||
GameMode::Catch => DifficultyAttributes::Catch(
|
||||
self.inner
|
||||
.calculate(&Converted::<Catch>::new(map, self.is_convert)),
|
||||
),
|
||||
GameMode::Mania => DifficultyAttributes::Mania(
|
||||
self.inner
|
||||
.calculate(&Converted::<Mania>::new(map, self.is_convert)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use crate::{
|
||||
model::{beatmap::Converted, mode::IGameMode},
|
||||
util::mods::Mods,
|
||||
};
|
||||
|
||||
/// Difficulty calculator on maps of a given mode.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ModeDifficulty {
|
||||
mods: u32,
|
||||
passed_objects: Option<u32>,
|
||||
clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl ModeDifficulty {
|
||||
/// Create a new difficulty calculator.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
|
||||
pub fn mods(&mut self, mods: u32) -> &mut Self {
|
||||
self.mods = mods;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
pub fn passed_objects(&mut self, passed_objects: u32) -> &mut Self {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
///
|
||||
/// If none is specified, it will take the clock rate based on the mods
|
||||
/// i.e. 1.5 for DT, 0.75 for HT and 1.0 otherwise.
|
||||
pub fn clock_rate(&mut self, clock_rate: f64) -> &mut Self {
|
||||
self.clock_rate = Some(clock_rate);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the difficulty calculation for a [`Converted`] beatmap and
|
||||
/// process the final skill values.
|
||||
pub fn calculate<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::DifficultyAttributes {
|
||||
M::difficulty(self, map)
|
||||
}
|
||||
|
||||
/// Perform a difficulty calculation for a [`Converted`] beatmap without
|
||||
/// processing the final skill values.
|
||||
pub fn strains<M: IGameMode>(&self, map: &Converted<'_, M>) -> M::Strains {
|
||||
M::strains(self, map)
|
||||
}
|
||||
|
||||
pub(crate) const fn get_mods(&self) -> u32 {
|
||||
self.mods
|
||||
}
|
||||
|
||||
pub(crate) fn get_clock_rate(&self) -> f64 {
|
||||
self.clock_rate.unwrap_or_else(|| self.mods.clock_rate())
|
||||
}
|
||||
|
||||
pub(crate) fn get_passed_objects(&self) -> usize {
|
||||
self.passed_objects.map_or(usize::MAX, |n| n as usize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub trait IDifficultyObject: Sized {
|
||||
fn idx(&self) -> usize;
|
||||
|
||||
fn previous<'a, D>(&self, backwards_idx: usize, diff_objects: &'a [D]) -> Option<&'a D> {
|
||||
self.idx()
|
||||
.checked_sub(backwards_idx + 1)
|
||||
.and_then(|idx| diff_objects.get(idx))
|
||||
}
|
||||
|
||||
fn next<'a, D>(&self, forwards_idx: usize, diff_objects: &'a [D]) -> Option<&'a D> {
|
||||
diff_objects.get(self.idx() + (forwards_idx + 1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
pub fn strain_decay(ms: f64, strain_decay_base: f64) -> f64 {
|
||||
strain_decay_base.powf(ms / 1000.0)
|
||||
}
|
||||
|
||||
/// Wrapper around a difficulty skill that carries a list of all difficulty
|
||||
/// objects.
|
||||
pub struct Skill<'a, S: ISkill> {
|
||||
pub inner: S,
|
||||
pub diff_objects: &'a S::DifficultyObjects<'a>,
|
||||
}
|
||||
|
||||
impl<'a, S: ISkill> Skill<'a, S> {
|
||||
pub const fn new(skill: S, diff_objects: &'a S::DifficultyObjects<'a>) -> Self {
|
||||
Self {
|
||||
inner: skill,
|
||||
diff_objects,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait required for [`Skill`].
|
||||
pub trait ISkill {
|
||||
type DifficultyObjects<'a>: ?Sized;
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StrainSkill {
|
||||
pub curr_section_peak: f64,
|
||||
pub curr_section_end: f64,
|
||||
pub strain_peaks: Vec<f64>, // TODO: default capacity
|
||||
}
|
||||
|
||||
impl StrainSkill {
|
||||
pub const DECAY_WEIGHT: f64 = 0.9;
|
||||
pub const SECTION_LEN: f64 = 400.0;
|
||||
|
||||
pub fn save_curr_peak(&mut self) {
|
||||
self.strain_peaks.push(self.curr_section_peak);
|
||||
}
|
||||
|
||||
pub fn start_new_section_from(&mut self, initial_strain: f64) {
|
||||
self.curr_section_peak = initial_strain;
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
||||
let mut strain_peaks = self.strain_peaks;
|
||||
strain_peaks.push(self.curr_section_peak);
|
||||
|
||||
strain_peaks
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self, decay_weight: f64) -> f64 {
|
||||
let mut difficulty = 0.0;
|
||||
let mut weight = 1.0;
|
||||
|
||||
let mut peaks = self.get_curr_strain_peaks();
|
||||
peaks.retain(|&strain| strain > 0.0);
|
||||
peaks.sort_by(|a, b| b.total_cmp(a));
|
||||
|
||||
for strain in peaks {
|
||||
difficulty += strain * weight;
|
||||
weight *= decay_weight;
|
||||
}
|
||||
|
||||
difficulty
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StrainDecaySkill {
|
||||
pub inner: StrainSkill,
|
||||
pub curr_strain: f64,
|
||||
}
|
||||
|
||||
impl StrainDecaySkill {
|
||||
pub const DECAY_WEIGHT: f64 = StrainSkill::DECAY_WEIGHT;
|
||||
pub const SECTION_LEN: f64 = StrainSkill::SECTION_LEN;
|
||||
|
||||
pub fn save_curr_peak(&mut self) {
|
||||
self.inner.save_curr_peak();
|
||||
}
|
||||
|
||||
pub fn start_new_section_from(&mut self, initial_strain: f64) {
|
||||
self.inner.start_new_section_from(initial_strain);
|
||||
}
|
||||
|
||||
pub fn get_curr_strain_peaks(self) -> Vec<f64> {
|
||||
self.inner.get_curr_strain_peaks()
|
||||
}
|
||||
|
||||
pub fn difficulty_value(self, decay_weight: f64) -> f64 {
|
||||
self.inner.difficulty_value(decay_weight)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user