feat: use rosu-mods (#36)
* use rosu-mods * apply values from DifficultyAdjust mod * minor doc addition * consider Blinds mod in perf calc
This commit is contained in:
@@ -18,6 +18,7 @@ tracing = ["rosu-map/tracing"]
|
||||
|
||||
[dependencies]
|
||||
rosu-map = { version = "0.1.1" }
|
||||
rosu-mods = { version = "0.1.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.4.0"
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use crate::Difficulty;
|
||||
use crate::{model::mods::GameMods, Difficulty};
|
||||
|
||||
use super::ModsDependent;
|
||||
|
||||
/// [`Difficulty`] but all fields are public for inspection.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct InspectDifficulty {
|
||||
/// Specify mods through their bit values.
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub mods: u32,
|
||||
/// Specify mods.
|
||||
pub mods: GameMods,
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
pub passed_objects: Option<u32>,
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
|
||||
+41
-26
@@ -9,7 +9,10 @@ use rosu_map::section::general::GameMode;
|
||||
use crate::{
|
||||
catch::Catch,
|
||||
mania::Mania,
|
||||
model::beatmap::{Beatmap, Converted},
|
||||
model::{
|
||||
beatmap::{Beatmap, Converted},
|
||||
mods::GameMods,
|
||||
},
|
||||
osu::Osu,
|
||||
taiko::Taiko,
|
||||
GradualDifficulty, GradualPerformance,
|
||||
@@ -25,7 +28,7 @@ pub mod inspect;
|
||||
pub mod object;
|
||||
pub mod skills;
|
||||
|
||||
use crate::{model::mode::IGameMode, util::mods::Mods};
|
||||
use crate::model::mode::IGameMode;
|
||||
|
||||
/// Difficulty calculator on maps of any mode.
|
||||
///
|
||||
@@ -43,7 +46,7 @@ use crate::{model::mode::IGameMode, util::mods::Mods};
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[must_use]
|
||||
pub struct Difficulty {
|
||||
mods: u32,
|
||||
mods: GameMods,
|
||||
passed_objects: Option<u32>,
|
||||
/// Clock rate will be clamped internally between 0.01 and 100.0.
|
||||
///
|
||||
@@ -86,7 +89,7 @@ impl Difficulty {
|
||||
/// Create a new difficulty calculator.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
mods: 0,
|
||||
mods: GameMods::DEFAULT,
|
||||
passed_objects: None,
|
||||
clock_rate: None,
|
||||
ar: None,
|
||||
@@ -122,7 +125,7 @@ impl Difficulty {
|
||||
InspectDifficulty {
|
||||
mods,
|
||||
passed_objects,
|
||||
clock_rate: clock_rate.map(non_zero_u32_to_f64),
|
||||
clock_rate: clock_rate.map(non_zero_u32_to_f32).map(f64::from),
|
||||
ar,
|
||||
cs,
|
||||
hp,
|
||||
@@ -131,19 +134,28 @@ impl Difficulty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub const fn mods(self, mods: u32) -> Self {
|
||||
Self { mods, ..self }
|
||||
pub fn mods(self, mods: impl Into<GameMods>) -> Self {
|
||||
Self {
|
||||
mods: mods.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Amount of passed objects for partial plays, e.g. a fail.
|
||||
pub const fn passed_objects(self, passed_objects: u32) -> Self {
|
||||
Self {
|
||||
passed_objects: Some(passed_objects),
|
||||
..self
|
||||
}
|
||||
pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
self.passed_objects = Some(passed_objects);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the clock rate used in the calculation.
|
||||
@@ -250,11 +262,10 @@ impl Difficulty {
|
||||
/// Adjust patterns as if the HR mod is enabled.
|
||||
///
|
||||
/// Only relevant for osu!catch.
|
||||
pub const fn hardrock_offsets(self, hardrock_offsets: bool) -> Self {
|
||||
Self {
|
||||
hardrock_offsets: Some(hardrock_offsets),
|
||||
..self
|
||||
}
|
||||
pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
|
||||
self.hardrock_offsets = Some(hardrock_offsets);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the difficulty calculation.
|
||||
@@ -300,13 +311,16 @@ impl Difficulty {
|
||||
GradualPerformance::new(self, map)
|
||||
}
|
||||
|
||||
pub(crate) const fn get_mods(&self) -> u32 {
|
||||
self.mods
|
||||
pub(crate) const fn get_mods(&self) -> &GameMods {
|
||||
&self.mods
|
||||
}
|
||||
|
||||
pub(crate) fn get_clock_rate(&self) -> f64 {
|
||||
self.clock_rate
|
||||
.map_or(self.mods.clock_rate(), non_zero_u32_to_f64)
|
||||
let clock_rate = self
|
||||
.clock_rate
|
||||
.map_or(self.mods.clock_rate(), non_zero_u32_to_f32);
|
||||
|
||||
f64::from(clock_rate)
|
||||
}
|
||||
|
||||
pub(crate) fn get_passed_objects(&self) -> usize {
|
||||
@@ -330,12 +344,13 @@ impl Difficulty {
|
||||
}
|
||||
|
||||
pub(crate) fn get_hardrock_offsets(&self) -> bool {
|
||||
self.hardrock_offsets.unwrap_or(self.mods.hr())
|
||||
self.hardrock_offsets
|
||||
.unwrap_or_else(|| self.mods.hardrock_offsets())
|
||||
}
|
||||
}
|
||||
|
||||
fn non_zero_u32_to_f64(n: NonZeroU32) -> f64 {
|
||||
f64::from(f32::from_bits(n.get()))
|
||||
fn non_zero_u32_to_f32(n: NonZeroU32) -> f32 {
|
||||
f32::from_bits(n.get())
|
||||
}
|
||||
|
||||
impl Debug for Difficulty {
|
||||
@@ -354,7 +369,7 @@ impl Debug for Difficulty {
|
||||
f.debug_struct("Difficulty")
|
||||
.field("mods", mods)
|
||||
.field("passed_objects", passed_objects)
|
||||
.field("clock_rate", &clock_rate.map(non_zero_u32_to_f64))
|
||||
.field("clock_rate", &clock_rate.map(non_zero_u32_to_f32))
|
||||
.field("ar", ar)
|
||||
.field("cs", cs)
|
||||
.field("hp", hp)
|
||||
|
||||
@@ -98,7 +98,14 @@ impl<'map> Performance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub fn mods(self, mods: u32) -> Self {
|
||||
|
||||
@@ -2,8 +2,9 @@ use std::cmp::{self, Ordering};
|
||||
|
||||
use crate::{
|
||||
any::{Difficulty, IntoModePerformance, IntoPerformance},
|
||||
model::mods::GameMods,
|
||||
osu::OsuPerformance,
|
||||
util::{map_or_attrs::MapOrAttrs, mods::Mods},
|
||||
util::map_or_attrs::MapOrAttrs,
|
||||
Performance,
|
||||
};
|
||||
|
||||
@@ -70,10 +71,17 @@ impl<'map> CatchPerformance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub const fn mods(mut self, mods: u32) -> Self {
|
||||
pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
|
||||
self.difficulty = self.difficulty.mods(mods);
|
||||
|
||||
self
|
||||
@@ -122,7 +130,7 @@ impl<'map> CatchPerformance<'map> {
|
||||
}
|
||||
|
||||
/// Use the specified settings of the given [`Difficulty`].
|
||||
pub const fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
self.difficulty = difficulty;
|
||||
|
||||
self
|
||||
@@ -135,7 +143,7 @@ impl<'map> CatchPerformance<'map> {
|
||||
/// `passed_objects`, you should use [`CatchGradualPerformance`].
|
||||
///
|
||||
/// [`CatchGradualPerformance`]: crate::catch::CatchGradualPerformance
|
||||
pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
pub fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
self.difficulty = self.difficulty.passed_objects(passed_objects);
|
||||
|
||||
self
|
||||
@@ -216,7 +224,7 @@ impl<'map> CatchPerformance<'map> {
|
||||
}
|
||||
|
||||
/// Adjust patterns as if the HR mod is enabled.
|
||||
pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
|
||||
pub fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
|
||||
self.difficulty = self.difficulty.hardrock_offsets(hardrock_offsets);
|
||||
|
||||
self
|
||||
@@ -486,13 +494,13 @@ impl<'map, T: IntoModePerformance<'map, Catch>> From<T> for CatchPerformance<'ma
|
||||
}
|
||||
}
|
||||
|
||||
struct CatchPerformanceInner {
|
||||
struct CatchPerformanceInner<'mods> {
|
||||
attrs: CatchDifficultyAttributes,
|
||||
mods: u32,
|
||||
mods: &'mods GameMods,
|
||||
state: CatchScoreState,
|
||||
}
|
||||
|
||||
impl CatchPerformanceInner {
|
||||
impl CatchPerformanceInner<'_> {
|
||||
fn calculate(self) -> CatchPerformanceAttributes {
|
||||
let attributes = &self.attrs;
|
||||
let stars = attributes.stars;
|
||||
|
||||
+4
-1
@@ -163,7 +163,10 @@
|
||||
#[doc(inline)]
|
||||
pub use self::{
|
||||
any::{Difficulty, GradualDifficulty, GradualPerformance, Performance},
|
||||
model::beatmap::{Beatmap, Converted},
|
||||
model::{
|
||||
beatmap::{Beatmap, Converted},
|
||||
mods::GameMods,
|
||||
},
|
||||
};
|
||||
|
||||
/// Types for calculations of any mode.
|
||||
|
||||
@@ -2,8 +2,9 @@ use std::cmp;
|
||||
|
||||
use crate::{
|
||||
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
|
||||
model::mods::GameMods,
|
||||
osu::OsuPerformance,
|
||||
util::{map_or_attrs::MapOrAttrs, mods::Mods},
|
||||
util::map_or_attrs::MapOrAttrs,
|
||||
Performance,
|
||||
};
|
||||
|
||||
@@ -71,17 +72,24 @@ impl<'map> ManiaPerformance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub const fn mods(mut self, mods: u32) -> Self {
|
||||
pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
|
||||
self.difficulty = self.difficulty.mods(mods);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the specified settings of the given [`Difficulty`].
|
||||
pub const fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
self.difficulty = difficulty;
|
||||
|
||||
self
|
||||
@@ -94,7 +102,7 @@ impl<'map> ManiaPerformance<'map> {
|
||||
/// `passed_objects`, you should use [`ManiaGradualPerformance`].
|
||||
///
|
||||
/// [`ManiaGradualPerformance`]: crate::mania::ManiaGradualPerformance
|
||||
pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
pub fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
self.difficulty = self.difficulty.passed_objects(passed_objects);
|
||||
|
||||
self
|
||||
@@ -840,13 +848,13 @@ impl<'map, T: IntoModePerformance<'map, Mania>> From<T> for ManiaPerformance<'ma
|
||||
}
|
||||
}
|
||||
|
||||
struct ManiaPerformanceInner {
|
||||
struct ManiaPerformanceInner<'mods> {
|
||||
attrs: ManiaDifficultyAttributes,
|
||||
mods: u32,
|
||||
mods: &'mods GameMods,
|
||||
state: ManiaScoreState,
|
||||
}
|
||||
|
||||
impl ManiaPerformanceInner {
|
||||
impl ManiaPerformanceInner<'_> {
|
||||
fn calculate(self) -> ManiaPerformanceAttributes {
|
||||
// * Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
|
||||
// * The specific number has no intrinsic meaning and can be adjusted as needed.
|
||||
|
||||
+199
-105
@@ -1,6 +1,6 @@
|
||||
use rosu_map::section::general::GameMode;
|
||||
|
||||
use crate::{any::difficulty::ModsDependent, util::mods::Mods, Difficulty};
|
||||
use crate::{any::difficulty::ModsDependent, model::mods::GameMods, Difficulty};
|
||||
|
||||
use super::{converted::Converted, Beatmap};
|
||||
|
||||
@@ -36,11 +36,11 @@ pub struct HitWindows {
|
||||
pub struct BeatmapAttributesBuilder {
|
||||
mode: GameMode,
|
||||
is_convert: bool,
|
||||
ar: ModsDependent,
|
||||
od: ModsDependent,
|
||||
cs: ModsDependent,
|
||||
hp: ModsDependent,
|
||||
mods: u32,
|
||||
ar: ModsDependentKind,
|
||||
od: ModsDependentKind,
|
||||
cs: ModsDependentKind,
|
||||
hp: ModsDependentKind,
|
||||
mods: GameMods,
|
||||
clock_rate: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -60,24 +60,24 @@ impl BeatmapAttributesBuilder {
|
||||
Self {
|
||||
mode: GameMode::Osu,
|
||||
is_convert: false,
|
||||
ar: ModsDependent::new(5.0),
|
||||
od: ModsDependent::new(5.0),
|
||||
cs: ModsDependent::new(5.0),
|
||||
hp: ModsDependent::new(5.0),
|
||||
mods: 0,
|
||||
ar: ModsDependentKind::DEFAULT,
|
||||
od: ModsDependentKind::DEFAULT,
|
||||
cs: ModsDependentKind::DEFAULT,
|
||||
hp: ModsDependentKind::DEFAULT,
|
||||
mods: GameMods::DEFAULT,
|
||||
clock_rate: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the given [`Beatmap`]'s attributes, mode, and convert status.
|
||||
pub const fn map(self, map: &Beatmap) -> Self {
|
||||
pub fn map(self, map: &Beatmap) -> Self {
|
||||
Self {
|
||||
mode: map.mode,
|
||||
ar: ModsDependent::new(map.ar),
|
||||
od: ModsDependent::new(map.od),
|
||||
cs: ModsDependent::new(map.cs),
|
||||
hp: ModsDependent::new(map.hp),
|
||||
mods: 0,
|
||||
ar: ModsDependentKind::Default(ModsDependent::new(map.ar)),
|
||||
od: ModsDependentKind::Default(ModsDependent::new(map.od)),
|
||||
cs: ModsDependentKind::Default(ModsDependent::new(map.cs)),
|
||||
hp: ModsDependentKind::Default(ModsDependent::new(map.hp)),
|
||||
mods: GameMods::DEFAULT,
|
||||
clock_rate: None,
|
||||
is_convert: map.is_convert,
|
||||
}
|
||||
@@ -88,14 +88,13 @@ impl BeatmapAttributesBuilder {
|
||||
/// `with_mods` determines if the given value should be used before
|
||||
/// or after accounting for mods, e.g. on `true` the value will be
|
||||
/// used as is and on `false` it will be modified based on the mods.
|
||||
pub const fn ar(self, ar: f32, with_mods: bool) -> Self {
|
||||
Self {
|
||||
ar: ModsDependent {
|
||||
value: ar,
|
||||
with_mods,
|
||||
},
|
||||
..self
|
||||
}
|
||||
pub const fn ar(mut self, ar: f32, with_mods: bool) -> Self {
|
||||
self.ar = ModsDependentKind::Custom(ModsDependent {
|
||||
value: ar,
|
||||
with_mods,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the overall difficulty.
|
||||
@@ -103,14 +102,13 @@ impl BeatmapAttributesBuilder {
|
||||
/// `with_mods` determines if the given value should be used before
|
||||
/// or after accounting for mods, e.g. on `true` the value will be
|
||||
/// used as is and on `false` it will be modified based on the mods.
|
||||
pub const fn od(self, od: f32, with_mods: bool) -> Self {
|
||||
Self {
|
||||
od: ModsDependent {
|
||||
value: od,
|
||||
with_mods,
|
||||
},
|
||||
..self
|
||||
}
|
||||
pub const fn od(mut self, od: f32, with_mods: bool) -> Self {
|
||||
self.od = ModsDependentKind::Custom(ModsDependent {
|
||||
value: od,
|
||||
with_mods,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the circle size.
|
||||
@@ -118,14 +116,13 @@ impl BeatmapAttributesBuilder {
|
||||
/// `with_mods` determines if the given value should be used before
|
||||
/// or after accounting for mods, e.g. on `true` the value will be
|
||||
/// used as is and on `false` it will be modified based on the mods.
|
||||
pub const fn cs(self, cs: f32, with_mods: bool) -> Self {
|
||||
Self {
|
||||
cs: ModsDependent {
|
||||
value: cs,
|
||||
with_mods,
|
||||
},
|
||||
..self
|
||||
}
|
||||
pub const fn cs(mut self, cs: f32, with_mods: bool) -> Self {
|
||||
self.cs = ModsDependentKind::Custom(ModsDependent {
|
||||
value: cs,
|
||||
with_mods,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the drain rate.
|
||||
@@ -133,36 +130,44 @@ impl BeatmapAttributesBuilder {
|
||||
/// `with_mods` determines if the given value should be used before
|
||||
/// or after accounting for mods, e.g. on `true` the value will be
|
||||
/// used as is and on `false` it will be modified based on the mods.
|
||||
pub const fn hp(self, hp: f32, with_mods: bool) -> Self {
|
||||
Self {
|
||||
hp: ModsDependent {
|
||||
value: hp,
|
||||
with_mods,
|
||||
},
|
||||
..self
|
||||
}
|
||||
pub const fn hp(mut self, hp: f32, with_mods: bool) -> Self {
|
||||
self.hp = ModsDependentKind::Custom(ModsDependent {
|
||||
value: hp,
|
||||
with_mods,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the mods.
|
||||
pub const fn mods(self, mods: u32) -> Self {
|
||||
Self { mods, ..self }
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
|
||||
self.mods = mods.into();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify a custom clock rate.
|
||||
pub const fn clock_rate(self, clock_rate: f64) -> Self {
|
||||
Self {
|
||||
clock_rate: Some(clock_rate),
|
||||
..self
|
||||
}
|
||||
pub const fn clock_rate(mut self, clock_rate: f64) -> Self {
|
||||
self.clock_rate = Some(clock_rate);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify a [`GameMode`] and whether it's a converted map.
|
||||
pub const fn mode(self, mode: GameMode, is_convert: bool) -> Self {
|
||||
Self {
|
||||
mode,
|
||||
is_convert,
|
||||
..self
|
||||
}
|
||||
pub const fn mode(mut self, mode: GameMode, is_convert: bool) -> Self {
|
||||
self.mode = mode;
|
||||
self.is_convert = is_convert;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify all settings through [`Difficulty`].
|
||||
@@ -170,22 +175,33 @@ impl BeatmapAttributesBuilder {
|
||||
Self {
|
||||
mode: self.mode,
|
||||
is_convert: self.is_convert,
|
||||
ar: difficulty.get_ar().unwrap_or(self.ar),
|
||||
od: difficulty.get_od().unwrap_or(self.od),
|
||||
cs: difficulty.get_cs().unwrap_or(self.cs),
|
||||
hp: difficulty.get_hp().unwrap_or(self.hp),
|
||||
mods: difficulty.get_mods(),
|
||||
ar: difficulty
|
||||
.get_ar()
|
||||
.map_or(self.ar, ModsDependentKind::Custom),
|
||||
od: difficulty
|
||||
.get_od()
|
||||
.map_or(self.od, ModsDependentKind::Custom),
|
||||
cs: difficulty
|
||||
.get_cs()
|
||||
.map_or(self.cs, ModsDependentKind::Custom),
|
||||
hp: difficulty
|
||||
.get_hp()
|
||||
.map_or(self.hp, ModsDependentKind::Custom),
|
||||
mods: difficulty.get_mods().clone(),
|
||||
clock_rate: Some(difficulty.get_clock_rate()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the AR and OD hit windows.
|
||||
pub fn hit_windows(&self) -> HitWindows {
|
||||
let mods = self.mods;
|
||||
let mods = &self.mods;
|
||||
|
||||
let clock_rate = self.clock_rate.unwrap_or(mods.clock_rate());
|
||||
let ar_clock_rate = if self.ar.with_mods { 1.0 } else { clock_rate };
|
||||
let od_clock_rate = if self.od.with_mods { 1.0 } else { clock_rate };
|
||||
let clock_rate = self
|
||||
.clock_rate
|
||||
.unwrap_or_else(|| f64::from(mods.clock_rate()));
|
||||
|
||||
let ar_clock_rate = if self.ar.with_mods() { 1.0 } else { clock_rate };
|
||||
let od_clock_rate = if self.od.with_mods() { 1.0 } else { clock_rate };
|
||||
|
||||
let mod_mult = |val: f32| {
|
||||
if mods.hr() {
|
||||
@@ -197,10 +213,10 @@ impl BeatmapAttributesBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
let raw_ar = if self.ar.with_mods {
|
||||
self.ar.value
|
||||
let raw_ar = if self.ar.with_mods() {
|
||||
self.ar.value(mods, GameMods::ar)
|
||||
} else {
|
||||
mod_mult(self.ar.value)
|
||||
mod_mult(self.ar.value(mods, GameMods::ar))
|
||||
};
|
||||
|
||||
let preempt = difficulty_range(f64::from(raw_ar), 1800.0, 1200.0, 450.0) / ar_clock_rate;
|
||||
@@ -208,10 +224,10 @@ impl BeatmapAttributesBuilder {
|
||||
// OD
|
||||
let hit_window = match self.mode {
|
||||
GameMode::Osu | GameMode::Catch => {
|
||||
let raw_od = if self.od.with_mods {
|
||||
self.od.value
|
||||
let raw_od = if self.od.with_mods() {
|
||||
self.od.value(mods, GameMods::od)
|
||||
} else {
|
||||
mod_mult(self.od.value)
|
||||
mod_mult(self.od.value(mods, GameMods::od))
|
||||
};
|
||||
|
||||
difficulty_range(
|
||||
@@ -222,10 +238,10 @@ impl BeatmapAttributesBuilder {
|
||||
) / od_clock_rate
|
||||
}
|
||||
GameMode::Taiko => {
|
||||
let raw_od = if self.od.with_mods {
|
||||
self.od.value
|
||||
let raw_od = if self.od.with_mods() {
|
||||
self.od.value(mods, GameMods::od)
|
||||
} else {
|
||||
mod_mult(self.od.value)
|
||||
mod_mult(self.od.value(mods, GameMods::od))
|
||||
};
|
||||
|
||||
let diff_range = difficulty_range(
|
||||
@@ -239,14 +255,14 @@ impl BeatmapAttributesBuilder {
|
||||
}
|
||||
GameMode::Mania => {
|
||||
let mut value = if !self.is_convert {
|
||||
34.0 + 3.0 * (10.0 - self.od.value).clamp(0.0, 10.0)
|
||||
} else if self.od.value.round_ties_even() > 4.0 {
|
||||
34.0 + 3.0 * (10.0 - self.od.value(mods, GameMods::od)).clamp(0.0, 10.0)
|
||||
} else if self.od.value(mods, GameMods::od).round_ties_even() > 4.0 {
|
||||
34.0
|
||||
} else {
|
||||
47.0
|
||||
};
|
||||
|
||||
if !self.od.with_mods {
|
||||
if !self.od.with_mods() {
|
||||
if mods.hr() {
|
||||
value /= 1.4;
|
||||
} else if mods.ez() {
|
||||
@@ -266,22 +282,24 @@ impl BeatmapAttributesBuilder {
|
||||
|
||||
/// Calculate the [`BeatmapAttributes`].
|
||||
pub fn build(&self) -> BeatmapAttributes {
|
||||
let mods = self.mods;
|
||||
let clock_rate = self.clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||
let mods = &self.mods;
|
||||
let clock_rate = self
|
||||
.clock_rate
|
||||
.unwrap_or_else(|| f64::from(mods.clock_rate()));
|
||||
|
||||
// HP
|
||||
let mut hp = self.hp.value;
|
||||
let mut hp = self.hp.value(mods, GameMods::hp);
|
||||
|
||||
if !self.hp.with_mods {
|
||||
if !self.hp.with_mods() {
|
||||
hp *= mods.od_ar_hp_multiplier() as f32;
|
||||
}
|
||||
|
||||
hp = hp.min(10.0);
|
||||
|
||||
// CS
|
||||
let mut cs = self.cs.value;
|
||||
let mut cs = self.cs.value(mods, GameMods::cs);
|
||||
|
||||
if !self.cs.with_mods {
|
||||
if !self.cs.with_mods() {
|
||||
if mods.hr() {
|
||||
cs = (cs * 1.3).min(10.0);
|
||||
} else if mods.ez() {
|
||||
@@ -303,7 +321,7 @@ impl BeatmapAttributesBuilder {
|
||||
let od = match self.mode {
|
||||
GameMode::Osu => (Self::OSU_MIN - od) / 6.0,
|
||||
GameMode::Taiko => (Self::TAIKO_MIN - od) / (Self::TAIKO_MIN - Self::TAIKO_AVG) * 5.0,
|
||||
GameMode::Catch | GameMode::Mania => f64::from(self.od.value),
|
||||
GameMode::Catch | GameMode::Mania => f64::from(self.od.value(mods, GameMods::od)),
|
||||
};
|
||||
|
||||
BeatmapAttributes {
|
||||
@@ -345,33 +363,109 @@ impl Default for BeatmapAttributesBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum ModsDependentKind {
|
||||
Default(ModsDependent),
|
||||
Custom(ModsDependent),
|
||||
}
|
||||
|
||||
impl ModsDependentKind {
|
||||
const DEFAULT: Self = Self::Default(ModsDependent::new(5.0));
|
||||
|
||||
const fn with_mods(&self) -> bool {
|
||||
match self {
|
||||
ModsDependentKind::Default(inner) | ModsDependentKind::Custom(inner) => inner.with_mods,
|
||||
}
|
||||
}
|
||||
|
||||
fn value(&self, mods: &GameMods, mods_fn: impl Fn(&GameMods) -> Option<f32>) -> f32 {
|
||||
match self {
|
||||
ModsDependentKind::Default(inner) => mods_fn(mods).unwrap_or(inner.value),
|
||||
ModsDependentKind::Custom(inner) => inner.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::util::float_ext::FloatExt;
|
||||
use rosu_mods::{generated_mods::DifficultyAdjustOsu, GameMod, GameMods};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn consider_mods() {
|
||||
let attrs = BeatmapAttributesBuilder::new()
|
||||
.ar(8.5, false)
|
||||
.mods(64)
|
||||
.build();
|
||||
fn default_ar() {
|
||||
let gamemod = GameMod::HiddenOsu(Default::default());
|
||||
let diff = Difficulty::new().mods(GameMods::from(gamemod));
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
let expected = 10.0;
|
||||
|
||||
assert!(attrs.ar.eq(expected), "{} != {expected}", attrs.ar);
|
||||
assert_eq!(attrs.ar, 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_mods() {
|
||||
fn custom_ar_without_mods() {
|
||||
let gamemod = GameMod::DoubleTimeOsu(Default::default());
|
||||
let diff = Difficulty::new().mods(GameMods::from(gamemod));
|
||||
let attrs = BeatmapAttributesBuilder::new()
|
||||
.ar(8.5, true)
|
||||
.mods(64)
|
||||
.ar(8.5, false)
|
||||
.difficulty(&diff)
|
||||
.build();
|
||||
|
||||
let expected = 8.5;
|
||||
assert_eq!(attrs.ar, 10.0);
|
||||
}
|
||||
|
||||
assert!(attrs.ar.eq(expected), "{} != {expected}", attrs.ar);
|
||||
#[test]
|
||||
fn custom_ar_with_mods() {
|
||||
let gamemod = GameMod::DoubleTimeOsu(Default::default());
|
||||
let diff = Difficulty::new().mods(GameMods::from(gamemod));
|
||||
let attrs = BeatmapAttributesBuilder::new()
|
||||
.ar(8.5, true)
|
||||
.difficulty(&diff)
|
||||
.build();
|
||||
|
||||
assert_eq!(attrs.ar, 8.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_mods_ar() {
|
||||
let mut mods = GameMods::new();
|
||||
mods.insert(GameMod::DoubleTimeCatch(Default::default()));
|
||||
mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu {
|
||||
approach_rate: Some(7.0),
|
||||
..Default::default()
|
||||
}));
|
||||
let diff = Difficulty::new().mods(mods);
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 9.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ar_custom_mods_ar_without_mods() {
|
||||
let mut mods = GameMods::new();
|
||||
mods.insert(GameMod::DoubleTimeCatch(Default::default()));
|
||||
mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu {
|
||||
approach_rate: Some(9.0),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let diff = Difficulty::new().mods(mods).ar(8.5, false);
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_ar_custom_mods_ar_with_mods() {
|
||||
let mut mods = GameMods::new();
|
||||
mods.insert(GameMod::DoubleTimeCatch(Default::default()));
|
||||
mods.insert(GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu {
|
||||
approach_rate: Some(9.0),
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
let diff = Difficulty::new().mods(mods).ar(8.5, true);
|
||||
let attrs = BeatmapAttributesBuilder::new().difficulty(&diff).build();
|
||||
|
||||
assert_eq!(attrs.ar, 8.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ impl Beatmap {
|
||||
|
||||
/// Returns a [`BeatmapAttributesBuilder`] to calculate modified beatmap
|
||||
/// attributes.
|
||||
pub const fn attributes(&self) -> BeatmapAttributesBuilder {
|
||||
pub fn attributes(&self) -> BeatmapAttributesBuilder {
|
||||
BeatmapAttributesBuilder::new().map(self)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,3 +9,5 @@ pub mod hit_object;
|
||||
|
||||
/// Gamemode related types.
|
||||
pub mod mode;
|
||||
|
||||
pub(crate) mod mods;
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
use std::fmt::{Debug, Formatter, Result as FmtResult};
|
||||
|
||||
use rosu_mods::{
|
||||
generated_mods::{
|
||||
DifficultyAdjustCatch, DifficultyAdjustMania, DifficultyAdjustOsu, DifficultyAdjustTaiko,
|
||||
},
|
||||
GameMod, GameModIntermode, GameMods as GameModsLazer, GameModsIntermode, GameModsLegacy,
|
||||
};
|
||||
|
||||
/// Collection of game mods.
|
||||
///
|
||||
/// This type can be created through its `From<T>` implementations where `T`
|
||||
/// can be
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rosu_pp::GameMods;
|
||||
/// use rosu_mods::{GameModsIntermode, GameModsLegacy, GameMods as GameModsLazer};
|
||||
///
|
||||
/// let int = GameMods::from(64 + 8);
|
||||
/// let legacy = GameMods::from(GameModsLegacy::Hidden | GameModsLegacy::Easy);
|
||||
/// let lazer = GameMods::from(GameModsLazer::new());
|
||||
/// let intermode = GameMods::from(GameModsIntermode::new());
|
||||
/// ```
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct GameMods {
|
||||
inner: GameModsInner,
|
||||
}
|
||||
|
||||
impl Debug for GameMods {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self.inner {
|
||||
GameModsInner::Lazer(ref mods) => Debug::fmt(mods, f),
|
||||
GameModsInner::Intermode(ref mods) => Debug::fmt(mods, f),
|
||||
GameModsInner::Legacy(ref mods) => Debug::fmt(mods, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner type of [`GameMods`] so that remote types contained in variants don't
|
||||
/// need to be re-exported.
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum GameModsInner {
|
||||
Lazer(GameModsLazer),
|
||||
Intermode(GameModsIntermode),
|
||||
Legacy(GameModsLegacy),
|
||||
}
|
||||
|
||||
impl GameMods {
|
||||
pub(crate) const DEFAULT: Self = Self {
|
||||
inner: GameModsInner::Legacy(GameModsLegacy::NoMod),
|
||||
};
|
||||
|
||||
/// Returns the mods' clock rate.
|
||||
///
|
||||
/// In case of variable clock rates like for `WindUp`, this will return
|
||||
/// `1.0`.
|
||||
pub(crate) fn clock_rate(&self) -> f32 {
|
||||
match self.inner {
|
||||
GameModsInner::Lazer(ref mods) => mods.clock_rate().unwrap_or(1.0),
|
||||
GameModsInner::Intermode(ref mods) => mods.legacy_clock_rate(),
|
||||
GameModsInner::Legacy(mods) => mods.clock_rate(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn od_ar_hp_multiplier(&self) -> f64 {
|
||||
if self.hr() {
|
||||
1.4
|
||||
} else if self.ez() {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the mods enable `hardrock_offsets`.
|
||||
pub(crate) fn hardrock_offsets(&self) -> bool {
|
||||
fn custom_hardrock_offsets(mods: &GameMods) -> Option<bool> {
|
||||
match mods.inner {
|
||||
GameModsInner::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod {
|
||||
GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch {
|
||||
hard_rock_offsets,
|
||||
..
|
||||
}) => *hard_rock_offsets,
|
||||
_ => None,
|
||||
}),
|
||||
GameModsInner::Intermode(_) | GameModsInner::Legacy(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
custom_hardrock_offsets(self).unwrap_or_else(|| self.hr())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_map_attr {
|
||||
( $( $fn:ident: $field:ident [ $( $mode:ident ),* ] [$s:literal] ;)* ) => {
|
||||
impl GameMods {
|
||||
$(
|
||||
#[doc = "Check whether the mods specify a custom "]
|
||||
#[doc = $s]
|
||||
#[doc = "value."]
|
||||
pub(crate) fn $fn(&self) -> Option<f32> {
|
||||
match self.inner {
|
||||
GameModsInner::Lazer(ref mods) => mods.iter().find_map(|gamemod| match gamemod {
|
||||
$( impl_map_attr!( @ $mode $field) => *$field, )*
|
||||
_ => None,
|
||||
}),
|
||||
GameModsInner::Intermode(_) | GameModsInner::Legacy(_) => None,
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
|
||||
( @ Osu $field:ident) => { GameMod::DifficultyAdjustOsu(DifficultyAdjustOsu { $field, .. }) };
|
||||
( @ Taiko $field:ident) => { GameMod::DifficultyAdjustTaiko(DifficultyAdjustTaiko { $field, .. }) };
|
||||
( @ Catch $field:ident) => { GameMod::DifficultyAdjustCatch(DifficultyAdjustCatch { $field, .. }) };
|
||||
( @ Mania $field:ident) => { GameMod::DifficultyAdjustMania(DifficultyAdjustMania { $field, .. }) };
|
||||
}
|
||||
|
||||
impl_map_attr! {
|
||||
ar: approach_rate [Osu, Catch] ["ar"];
|
||||
cs: circle_size [Osu, Catch] ["cs"];
|
||||
hp: drain_rate [Osu, Taiko, Catch, Mania] ["hp"];
|
||||
od: overall_difficulty [Osu, Taiko, Catch, Mania] ["od"];
|
||||
}
|
||||
|
||||
macro_rules! impl_has_mod {
|
||||
( $( $fn:ident: $sign:tt $name:ident [ $s:literal ], )* ) => {
|
||||
impl GameMods {
|
||||
$(
|
||||
// workaround for <https://github.com/rust-lang/rust-analyzer/issues/8092>
|
||||
#[doc = "Check whether [`GameMods`] contain `"]
|
||||
#[doc = $s]
|
||||
#[doc = "`."]
|
||||
pub(crate) fn $fn(&self) -> bool {
|
||||
match self.inner {
|
||||
GameModsInner::Lazer(ref mods) => {
|
||||
mods.contains_intermode(GameModIntermode::$name)
|
||||
},
|
||||
GameModsInner::Intermode(ref mods) => {
|
||||
mods.contains(GameModIntermode::$name)
|
||||
},
|
||||
GameModsInner::Legacy(_mods) => {
|
||||
impl_has_mod!(LEGACY $sign $name _mods)
|
||||
},
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
};
|
||||
|
||||
( LEGACY + $name:ident $mods:ident ) => {
|
||||
$mods.contains(GameModsLegacy::$name)
|
||||
};
|
||||
|
||||
( LEGACY - $name:ident $mods:ident ) => {
|
||||
false
|
||||
};
|
||||
}
|
||||
|
||||
impl_has_mod! {
|
||||
nf: + NoFail ["NoFail"],
|
||||
ez: + Easy ["Easy"],
|
||||
td: + TouchDevice ["TouchDevice"],
|
||||
hd: + Hidden ["Hidden"],
|
||||
hr: + HardRock ["HardRock"],
|
||||
rx: + Relax ["Relax"],
|
||||
fl: + Flashlight ["Flashlight"],
|
||||
so: + SpunOut ["SpunOut"],
|
||||
bl: - Blinds ["Blinds"],
|
||||
}
|
||||
|
||||
impl Default for GameMods {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GameModsLazer> for GameMods {
|
||||
fn from(mods: GameModsLazer) -> Self {
|
||||
Self {
|
||||
inner: GameModsInner::Lazer(mods),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GameModsIntermode> for GameMods {
|
||||
fn from(mods: GameModsIntermode) -> Self {
|
||||
Self {
|
||||
inner: GameModsInner::Intermode(mods),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&GameModsIntermode> for GameMods {
|
||||
fn from(mods: &GameModsIntermode) -> Self {
|
||||
// If only legacy mods are set, use `GameModsLegacy` and thus avoid
|
||||
// allocating an owned `GameModsIntermode` instance.
|
||||
match mods.checked_bits() {
|
||||
Some(bits) => bits.into(),
|
||||
None => mods.to_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GameModsLegacy> for GameMods {
|
||||
fn from(mods: GameModsLegacy) -> Self {
|
||||
Self {
|
||||
inner: GameModsInner::Legacy(mods),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for GameMods {
|
||||
fn from(bits: u32) -> Self {
|
||||
GameModsLegacy::from_bits(bits).into()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ use crate::{
|
||||
object::{OsuObject, OsuObjectKind},
|
||||
OsuBeatmap,
|
||||
},
|
||||
util::mods::Mods,
|
||||
Difficulty,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,14 +2,13 @@ use std::{cmp, pin::Pin};
|
||||
|
||||
use crate::{
|
||||
any::difficulty::{skills::Skill, Difficulty},
|
||||
model::beatmap::BeatmapAttributes,
|
||||
model::{beatmap::BeatmapAttributes, mods::GameMods},
|
||||
osu::{
|
||||
convert::convert_objects,
|
||||
difficulty::{object::OsuDifficultyObject, scaling_factor::ScalingFactor},
|
||||
object::OsuObject,
|
||||
performance::PERFORMANCE_BASE_MULTIPLIER,
|
||||
},
|
||||
util::mods::Mods,
|
||||
};
|
||||
|
||||
use self::skills::OsuSkills;
|
||||
@@ -146,7 +145,7 @@ impl DifficultyValues {
|
||||
/// Process the difficulty values and store the results in `attrs`.
|
||||
pub fn eval(
|
||||
attrs: &mut OsuDifficultyAttributes,
|
||||
mods: u32,
|
||||
mods: &GameMods,
|
||||
aim_difficulty_value: f64,
|
||||
aim_no_sliders_difficulty_value: f64,
|
||||
speed_difficulty_value: f64,
|
||||
|
||||
@@ -5,8 +5,9 @@ use crate::{
|
||||
object::IDifficultyObject,
|
||||
skills::{strain_decay, ISkill, Skill, StrainSkill},
|
||||
},
|
||||
model::mods::GameMods,
|
||||
osu::{difficulty::object::OsuDifficultyObject, object::OsuObjectKind},
|
||||
util::{mods::Mods, strains_vec::StrainsVec},
|
||||
util::strains_vec::StrainsVec,
|
||||
};
|
||||
|
||||
use super::strain::OsuStrainSkill;
|
||||
@@ -22,7 +23,7 @@ pub struct Flashlight {
|
||||
}
|
||||
|
||||
impl Flashlight {
|
||||
pub fn new(mods: u32, radius: f64, time_preempt: f64, time_fade_in: f64) -> Self {
|
||||
pub fn new(mods: &GameMods, radius: f64, time_preempt: f64, time_fade_in: f64) -> Self {
|
||||
let scaling_factor = 52.0 / radius;
|
||||
|
||||
Self {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::{model::beatmap::BeatmapAttributes, osu::object::OsuObject, util::mods::Mods};
|
||||
use crate::{
|
||||
model::{beatmap::BeatmapAttributes, mods::GameMods},
|
||||
osu::object::OsuObject,
|
||||
};
|
||||
|
||||
use self::{aim::Aim, flashlight::Flashlight, speed::Speed};
|
||||
|
||||
@@ -18,7 +21,7 @@ pub struct OsuSkills {
|
||||
|
||||
impl OsuSkills {
|
||||
pub fn new(
|
||||
mods: u32,
|
||||
mods: &GameMods,
|
||||
scaling_factor: &ScalingFactor,
|
||||
map_attrs: &BeatmapAttributes,
|
||||
time_preempt: f64,
|
||||
|
||||
+33
-12
@@ -6,8 +6,9 @@ use crate::{
|
||||
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance, Performance},
|
||||
catch::CatchPerformance,
|
||||
mania::ManiaPerformance,
|
||||
model::mods::GameMods,
|
||||
taiko::TaikoPerformance,
|
||||
util::{float_ext::FloatExt, map_or_attrs::MapOrAttrs, mods::Mods},
|
||||
util::{float_ext::FloatExt, map_or_attrs::MapOrAttrs},
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -118,10 +119,17 @@ impl<'map> OsuPerformance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub const fn mods(mut self, mods: u32) -> Self {
|
||||
pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
|
||||
self.difficulty = self.difficulty.mods(mods);
|
||||
|
||||
self
|
||||
@@ -172,7 +180,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
}
|
||||
|
||||
/// Use the specified settings of the given [`Difficulty`].
|
||||
pub const fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
self.difficulty = difficulty;
|
||||
|
||||
self
|
||||
@@ -185,7 +193,7 @@ impl<'map> OsuPerformance<'map> {
|
||||
/// `passed_objects`, you should use [`OsuGradualPerformance`].
|
||||
///
|
||||
/// [`OsuGradualPerformance`]: crate::osu::OsuGradualPerformance
|
||||
pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
pub fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
self.difficulty = self.difficulty.passed_objects(passed_objects);
|
||||
|
||||
self
|
||||
@@ -522,15 +530,15 @@ impl<'map, T: IntoModePerformance<'map, Osu>> From<T> for OsuPerformance<'map> {
|
||||
|
||||
pub const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
|
||||
|
||||
struct OsuPerformanceInner {
|
||||
struct OsuPerformanceInner<'mods> {
|
||||
attrs: OsuDifficultyAttributes,
|
||||
mods: u32,
|
||||
mods: &'mods GameMods,
|
||||
acc: f64,
|
||||
state: OsuScoreState,
|
||||
effective_miss_count: f64,
|
||||
}
|
||||
|
||||
impl OsuPerformanceInner {
|
||||
impl OsuPerformanceInner<'_> {
|
||||
fn calculate(mut self) -> OsuPerformanceAttributes {
|
||||
let total_hits = self.state.total_hits();
|
||||
|
||||
@@ -632,7 +640,13 @@ impl OsuPerformanceInner {
|
||||
// * Buff for longer maps with high AR.
|
||||
aim_value *= 1.0 + ar_factor * len_bonus;
|
||||
|
||||
if self.mods.hd() {
|
||||
if self.mods.bl() {
|
||||
aim_value *= 1.3
|
||||
+ (total_hits
|
||||
* (0.0016 / (1.0 + 2.0 * self.effective_miss_count))
|
||||
* self.acc.powf(16.0))
|
||||
* (1.0 - 0.003 * self.attrs.hp * self.attrs.hp);
|
||||
} else if self.mods.hd() {
|
||||
// * We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aim_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
|
||||
}
|
||||
@@ -695,7 +709,11 @@ impl OsuPerformanceInner {
|
||||
// * Buff for longer maps with high AR.
|
||||
speed_value *= 1.0 + ar_factor * len_bonus;
|
||||
|
||||
if self.mods.hd() {
|
||||
if self.mods.bl() {
|
||||
// * Increasing the speed value by object count for Blinds isn't
|
||||
// * ideal, so the minimum buff is given.
|
||||
speed_value *= 1.12;
|
||||
} else if self.mods.hd() {
|
||||
// * We want to give more reward for lower AR when it comes to aim and HD.
|
||||
// * This nerfs high AR and buffs lower AR.
|
||||
speed_value *= 1.0 + 0.04 * (12.0 - self.attrs.ar);
|
||||
@@ -764,8 +782,11 @@ impl OsuPerformanceInner {
|
||||
.powf(0.3)
|
||||
.min(1.15);
|
||||
|
||||
// * Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
|
||||
if self.mods.hd() {
|
||||
// * Increasing the accuracy value by object count for Blinds isn't
|
||||
// * ideal, so the minimum buff is given.
|
||||
if self.mods.bl() {
|
||||
acc_value *= 1.14;
|
||||
} else if self.mods.hd() {
|
||||
acc_value *= 1.08;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ use std::cmp;
|
||||
|
||||
use crate::{
|
||||
any::{Difficulty, HitResultPriority, IntoModePerformance, IntoPerformance},
|
||||
model::mods::GameMods,
|
||||
osu::OsuPerformance,
|
||||
util::{map_or_attrs::MapOrAttrs, mods::Mods},
|
||||
util::map_or_attrs::MapOrAttrs,
|
||||
Performance,
|
||||
};
|
||||
|
||||
@@ -69,10 +70,17 @@ impl<'map> TaikoPerformance<'map> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Specify mods through their bit values.
|
||||
/// Specify mods.
|
||||
///
|
||||
/// Accepted types are
|
||||
/// - `u32`
|
||||
/// - [`rosu_mods::GameModsLegacy`]
|
||||
/// - [`rosu_mods::GameMods`]
|
||||
/// - [`rosu_mods::GameModsIntermode`]
|
||||
/// - [`&rosu_mods::GameModsIntermode`](rosu_mods::GameModsIntermode)
|
||||
///
|
||||
/// See <https://github.com/ppy/osu-api/wiki#mods>
|
||||
pub const fn mods(mut self, mods: u32) -> Self {
|
||||
pub fn mods(mut self, mods: impl Into<GameMods>) -> Self {
|
||||
self.difficulty = self.difficulty.mods(mods);
|
||||
|
||||
self
|
||||
@@ -124,7 +132,7 @@ impl<'map> TaikoPerformance<'map> {
|
||||
}
|
||||
|
||||
/// Use the specified settings of the given [`Difficulty`].
|
||||
pub const fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
pub fn difficulty(mut self, difficulty: Difficulty) -> Self {
|
||||
self.difficulty = difficulty;
|
||||
|
||||
self
|
||||
@@ -137,7 +145,7 @@ impl<'map> TaikoPerformance<'map> {
|
||||
/// `passed_objects`, you should use [`TaikoGradualPerformance`].
|
||||
///
|
||||
/// [`TaikoGradualPerformance`]: crate::taiko::TaikoGradualPerformance
|
||||
pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
pub fn passed_objects(mut self, passed_objects: u32) -> Self {
|
||||
self.difficulty = self.difficulty.passed_objects(passed_objects);
|
||||
|
||||
self
|
||||
@@ -377,13 +385,13 @@ impl<'map, T: IntoModePerformance<'map, Taiko>> From<T> for TaikoPerformance<'ma
|
||||
}
|
||||
}
|
||||
|
||||
struct TaikoPerformanceInner {
|
||||
struct TaikoPerformanceInner<'mods> {
|
||||
attrs: TaikoDifficultyAttributes,
|
||||
mods: u32,
|
||||
mods: &'mods GameMods,
|
||||
state: TaikoScoreState,
|
||||
}
|
||||
|
||||
impl TaikoPerformanceInner {
|
||||
impl TaikoPerformanceInner<'_> {
|
||||
fn calculate(self) -> TaikoPerformanceAttributes {
|
||||
// * The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits
|
||||
// * and increasing the miss penalty for shorter object counts lower than 1000.
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod float_ext;
|
||||
pub mod generic_fmt;
|
||||
pub mod limited_queue;
|
||||
pub mod map_or_attrs;
|
||||
pub mod mods;
|
||||
pub mod random;
|
||||
pub mod sort;
|
||||
pub mod strains_vec;
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
pub trait Mods: Copy {
|
||||
fn nf(self) -> bool;
|
||||
fn ez(self) -> bool;
|
||||
fn td(self) -> bool;
|
||||
fn hd(self) -> bool;
|
||||
fn hr(self) -> bool;
|
||||
fn dt(self) -> bool;
|
||||
fn rx(self) -> bool;
|
||||
fn ht(self) -> bool;
|
||||
fn fl(self) -> bool;
|
||||
fn so(self) -> bool;
|
||||
|
||||
fn clock_rate(self) -> f64 {
|
||||
if self.dt() {
|
||||
1.5
|
||||
} else if self.ht() {
|
||||
0.75
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
fn od_ar_hp_multiplier(self) -> f64 {
|
||||
if self.hr() {
|
||||
1.4
|
||||
} else if self.ez() {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_mods_fn {
|
||||
( $fn_name:ident, $bits:expr ) => {
|
||||
fn $fn_name(self) -> bool {
|
||||
self & ($bits) != 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Mods for u32 {
|
||||
impl_mods_fn!(nf, 1 << 0);
|
||||
impl_mods_fn!(ez, 1 << 1);
|
||||
impl_mods_fn!(td, 1 << 2);
|
||||
impl_mods_fn!(hd, 1 << 3);
|
||||
impl_mods_fn!(hr, 1 << 4);
|
||||
impl_mods_fn!(dt, 1 << 6);
|
||||
impl_mods_fn!(rx, 1 << 7);
|
||||
impl_mods_fn!(ht, 1 << 8);
|
||||
impl_mods_fn!(fl, 1 << 10);
|
||||
impl_mods_fn!(so, 1 << 12);
|
||||
}
|
||||
Reference in New Issue
Block a user