use std::{
fmt::{Debug, Formatter, Result as FmtResult},
num::NonZeroU64,
};
use rosu_map::section::general::GameMode;
use crate::{
GradualDifficulty, GradualPerformance,
any::CalculateError,
catch::Catch,
mania::Mania,
model::{
beatmap::{Beatmap, BeatmapAttribute, TooSuspicious, attributes::BeatmapDifficulty},
mode::ConvertError,
mods::GameMods,
},
osu::Osu,
taiko::Taiko,
};
use super::{InspectDifficulty, Strains, attributes::DifficultyAttributes};
pub mod gradual;
pub mod inspect;
pub mod object;
pub mod skills;
use crate::model::mode::IGameMode;
#[derive(Clone, PartialEq)]
#[must_use]
pub struct Difficulty {
mods: GameMods,
passed_objects: Option<u32>,
clock_rate: Option<NonZeroU64>,
#[expect(
clippy::struct_field_names,
reason = "it's a different kind of difficulty"
)]
map_difficulty: BeatmapDifficulty,
hardrock_offsets: Option<bool>,
lazer: Option<bool>,
}
impl Difficulty {
pub const fn new() -> Self {
Self {
mods: GameMods::DEFAULT,
passed_objects: None,
clock_rate: None,
map_difficulty: BeatmapDifficulty::DEFAULT,
hardrock_offsets: None,
lazer: None,
}
}
pub fn inspect(self) -> InspectDifficulty {
let Self {
mods,
passed_objects,
clock_rate,
map_difficulty,
hardrock_offsets,
lazer,
} = self;
InspectDifficulty {
mods,
passed_objects,
clock_rate: clock_rate.map(non_zero_u64_to_f64),
ar: map_difficulty.ar,
cs: map_difficulty.cs,
hp: map_difficulty.hp,
od: map_difficulty.od,
hardrock_offsets,
lazer,
}
}
pub fn mods(self, mods: impl Into<GameMods>) -> Self {
Self {
mods: mods.into(),
..self
}
}
pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
self.passed_objects = Some(passed_objects);
self
}
pub fn clock_rate(self, clock_rate: f64) -> Self {
let clock_rate = clock_rate.clamp(0.01, 100.0).to_bits();
let non_zero = unsafe { NonZeroU64::new_unchecked(clock_rate) };
Self {
clock_rate: Some(non_zero),
..self
}
}
pub const fn ar(mut self, ar: f32, fixed: bool) -> Self {
let ar = f32::clamp(ar, -20.0, 20.0);
self.map_difficulty.ar = if fixed {
BeatmapAttribute::Fixed(ar)
} else {
BeatmapAttribute::Given(ar)
};
self
}
pub const fn cs(mut self, cs: f32, fixed: bool) -> Self {
let cs = f32::clamp(cs, -20.0, 20.0);
self.map_difficulty.cs = if fixed {
BeatmapAttribute::Fixed(cs)
} else {
BeatmapAttribute::Given(cs)
};
self
}
pub const fn hp(mut self, hp: f32, fixed: bool) -> Self {
let hp = f32::clamp(hp, -20.0, 20.0);
self.map_difficulty.hp = if fixed {
BeatmapAttribute::Fixed(hp)
} else {
BeatmapAttribute::Given(hp)
};
self
}
pub const fn od(mut self, od: f32, fixed: bool) -> Self {
let od = f32::clamp(od, -20.0, 20.0);
self.map_difficulty.od = if fixed {
BeatmapAttribute::Fixed(od)
} else {
BeatmapAttribute::Given(od)
};
self
}
pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
self.hardrock_offsets = Some(hardrock_offsets);
self
}
pub const fn lazer(mut self, lazer: bool) -> Self {
self.lazer = Some(lazer);
self
}
#[expect(clippy::missing_panics_doc, reason = "unreachable")]
pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
match map.mode {
GameMode::Osu => DifficultyAttributes::Osu(
Osu::difficulty(self, map).expect("no conversion required"),
),
GameMode::Taiko => DifficultyAttributes::Taiko(
Taiko::difficulty(self, map).expect("no conversion required"),
),
GameMode::Catch => DifficultyAttributes::Catch(
Catch::difficulty(self, map).expect("no conversion required"),
),
GameMode::Mania => DifficultyAttributes::Mania(
Mania::difficulty(self, map).expect("no conversion required"),
),
}
}
pub fn checked_calculate(&self, map: &Beatmap) -> Result<DifficultyAttributes, TooSuspicious> {
map.check_suspicion()?;
Ok(self.calculate(map))
}
pub fn calculate_for_mode<M: IGameMode>(
&self,
map: &Beatmap,
) -> Result<M::DifficultyAttributes, ConvertError> {
M::difficulty(self, map)
}
pub fn checked_calculate_for_mode<M: IGameMode>(
&self,
map: &Beatmap,
) -> Result<M::DifficultyAttributes, CalculateError> {
M::checked_difficulty(self, map)
}
#[expect(clippy::missing_panics_doc, reason = "unreachable")]
pub fn strains(&self, map: &Beatmap) -> Strains {
match map.mode {
GameMode::Osu => Strains::Osu(Osu::strains(self, map).expect("no conversion required")),
GameMode::Taiko => {
Strains::Taiko(Taiko::strains(self, map).expect("no conversion required"))
}
GameMode::Catch => {
Strains::Catch(Catch::strains(self, map).expect("no conversion required"))
}
GameMode::Mania => {
Strains::Mania(Mania::strains(self, map).expect("no conversion required"))
}
}
}
pub fn checked_strains(&self, map: &Beatmap) -> Result<Strains, TooSuspicious> {
map.check_suspicion()?;
Ok(self.strains(map))
}
pub fn strains_for_mode<M: IGameMode>(
&self,
map: &Beatmap,
) -> Result<M::Strains, ConvertError> {
M::strains(self, map)
}
pub fn gradual_difficulty(self, map: &Beatmap) -> GradualDifficulty {
GradualDifficulty::new(self, map)
}
pub fn checked_gradual_difficulty(
self,
map: &Beatmap,
) -> Result<GradualDifficulty, TooSuspicious> {
GradualDifficulty::checked_new(self, map)
}
pub fn gradual_difficulty_for_mode<M: IGameMode>(
self,
map: &Beatmap,
) -> Result<M::GradualDifficulty, ConvertError> {
M::gradual_difficulty(self, map)
}
pub fn gradual_performance(self, map: &Beatmap) -> GradualPerformance {
GradualPerformance::new(self, map)
}
pub fn checked_gradual_performance(
self,
map: &Beatmap,
) -> Result<GradualPerformance, TooSuspicious> {
GradualPerformance::checked_new(self, map)
}
pub fn gradual_performance_for_mode<M: IGameMode>(
self,
map: &Beatmap,
) -> Result<M::GradualPerformance, ConvertError> {
M::gradual_performance(self, map)
}
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_u64_to_f64)
}
pub(crate) fn get_passed_objects(&self) -> usize {
self.passed_objects.map_or(usize::MAX, |n| n as usize)
}
pub(crate) const fn get_map_difficulty(&self) -> &BeatmapDifficulty {
&self.map_difficulty
}
pub(crate) fn get_hardrock_offsets(&self) -> bool {
self.hardrock_offsets
.unwrap_or_else(|| self.mods.hardrock_offsets())
}
pub(crate) fn get_lazer(&self) -> bool {
self.lazer.unwrap_or(true)
}
}
const fn non_zero_u64_to_f64(n: NonZeroU64) -> f64 {
f64::from_bits(n.get())
}
impl Debug for Difficulty {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let Self {
mods,
passed_objects,
clock_rate,
map_difficulty,
hardrock_offsets,
lazer,
} = self;
f.debug_struct("Difficulty")
.field("mods", mods)
.field("passed_objects", passed_objects)
.field("clock_rate", &clock_rate.map(non_zero_u64_to_f64))
.field("ar", &map_difficulty.ar)
.field("cs", &map_difficulty.cs)
.field("hp", &map_difficulty.hp)
.field("od", &map_difficulty.od)
.field("hardrock_offsets", hardrock_offsets)
.field("lazer", lazer)
.finish()
}
}
impl Default for Difficulty {
fn default() -> Self {
Self::new()
}
}