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:
+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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user