1use std::{
2 fmt::{Debug, Formatter, Result as FmtResult},
3 num::NonZeroU64,
4};
5
6use rosu_map::section::general::GameMode;
7
8use crate::{
9 GradualDifficulty, GradualPerformance,
10 any::CalculateError,
11 catch::Catch,
12 mania::Mania,
13 model::{
14 beatmap::{Beatmap, BeatmapAttribute, TooSuspicious, attributes::BeatmapDifficulty},
15 mode::ConvertError,
16 mods::GameMods,
17 },
18 osu::Osu,
19 taiko::Taiko,
20};
21
22use super::{InspectDifficulty, Strains, attributes::DifficultyAttributes};
23
24pub mod gradual;
25pub mod inspect;
26pub mod object;
27pub mod skills;
28
29use crate::model::mode::IGameMode;
30
31#[derive(Clone, PartialEq)]
45#[must_use]
46pub struct Difficulty {
47 mods: GameMods,
48 passed_objects: Option<u32>,
49 clock_rate: Option<NonZeroU64>,
56 #[expect(
57 clippy::struct_field_names,
58 reason = "it's a different kind of difficulty"
59 )]
60 map_difficulty: BeatmapDifficulty,
61 hardrock_offsets: Option<bool>,
62 lazer: Option<bool>,
63}
64
65impl Difficulty {
66 pub const fn new() -> Self {
68 Self {
69 mods: GameMods::DEFAULT,
70 passed_objects: None,
71 clock_rate: None,
72 map_difficulty: BeatmapDifficulty::DEFAULT,
73 hardrock_offsets: None,
74 lazer: None,
75 }
76 }
77
78 pub fn inspect(self) -> InspectDifficulty {
81 let Self {
82 mods,
83 passed_objects,
84 clock_rate,
85 map_difficulty,
86 hardrock_offsets,
87 lazer,
88 } = self;
89
90 InspectDifficulty {
91 mods,
92 passed_objects,
93 clock_rate: clock_rate.map(non_zero_u64_to_f64),
94 ar: map_difficulty.ar,
95 cs: map_difficulty.cs,
96 hp: map_difficulty.hp,
97 od: map_difficulty.od,
98 hardrock_offsets,
99 lazer,
100 }
101 }
102
103 pub fn mods(self, mods: impl Into<GameMods>) -> Self {
114 Self {
115 mods: mods.into(),
116 ..self
117 }
118 }
119
120 pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
122 self.passed_objects = Some(passed_objects);
123
124 self
125 }
126
127 pub fn clock_rate(self, clock_rate: f64) -> Self {
136 let clock_rate = clock_rate.clamp(0.01, 100.0).to_bits();
137
138 let non_zero = unsafe { NonZeroU64::new_unchecked(clock_rate) };
141
142 Self {
143 clock_rate: Some(non_zero),
144 ..self
145 }
146 }
147
148 pub const fn ar(mut self, ar: f32, fixed: bool) -> Self {
160 let ar = f32::clamp(ar, -20.0, 20.0);
161
162 self.map_difficulty.ar = if fixed {
163 BeatmapAttribute::Fixed(ar)
164 } else {
165 BeatmapAttribute::Given(ar)
166 };
167
168 self
169 }
170
171 pub const fn cs(mut self, cs: f32, fixed: bool) -> Self {
183 let cs = f32::clamp(cs, -20.0, 20.0);
184
185 self.map_difficulty.cs = if fixed {
186 BeatmapAttribute::Fixed(cs)
187 } else {
188 BeatmapAttribute::Given(cs)
189 };
190
191 self
192 }
193
194 pub const fn hp(mut self, hp: f32, fixed: bool) -> Self {
204 let hp = f32::clamp(hp, -20.0, 20.0);
205
206 self.map_difficulty.hp = if fixed {
207 BeatmapAttribute::Fixed(hp)
208 } else {
209 BeatmapAttribute::Given(hp)
210 };
211
212 self
213 }
214
215 pub const fn od(mut self, od: f32, fixed: bool) -> Self {
225 let od = f32::clamp(od, -20.0, 20.0);
226
227 self.map_difficulty.od = if fixed {
228 BeatmapAttribute::Fixed(od)
229 } else {
230 BeatmapAttribute::Given(od)
231 };
232
233 self
234 }
235
236 pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
240 self.hardrock_offsets = Some(hardrock_offsets);
241
242 self
243 }
244
245 pub const fn lazer(mut self, lazer: bool) -> Self {
250 self.lazer = Some(lazer);
251
252 self
253 }
254
255 #[expect(clippy::missing_panics_doc, reason = "unreachable")]
257 pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
258 match map.mode {
259 GameMode::Osu => DifficultyAttributes::Osu(
260 Osu::difficulty(self, map).expect("no conversion required"),
261 ),
262 GameMode::Taiko => DifficultyAttributes::Taiko(
263 Taiko::difficulty(self, map).expect("no conversion required"),
264 ),
265 GameMode::Catch => DifficultyAttributes::Catch(
266 Catch::difficulty(self, map).expect("no conversion required"),
267 ),
268 GameMode::Mania => DifficultyAttributes::Mania(
269 Mania::difficulty(self, map).expect("no conversion required"),
270 ),
271 }
272 }
273
274 pub fn checked_calculate(&self, map: &Beatmap) -> Result<DifficultyAttributes, TooSuspicious> {
277 map.check_suspicion()?;
278
279 Ok(self.calculate(map))
280 }
281
282 pub fn calculate_for_mode<M: IGameMode>(
284 &self,
285 map: &Beatmap,
286 ) -> Result<M::DifficultyAttributes, ConvertError> {
287 M::difficulty(self, map)
288 }
289
290 pub fn checked_calculate_for_mode<M: IGameMode>(
293 &self,
294 map: &Beatmap,
295 ) -> Result<M::DifficultyAttributes, CalculateError> {
296 M::checked_difficulty(self, map)
297 }
298
299 #[expect(clippy::missing_panics_doc, reason = "unreachable")]
304 pub fn strains(&self, map: &Beatmap) -> Strains {
305 match map.mode {
306 GameMode::Osu => Strains::Osu(Osu::strains(self, map).expect("no conversion required")),
307 GameMode::Taiko => {
308 Strains::Taiko(Taiko::strains(self, map).expect("no conversion required"))
309 }
310 GameMode::Catch => {
311 Strains::Catch(Catch::strains(self, map).expect("no conversion required"))
312 }
313 GameMode::Mania => {
314 Strains::Mania(Mania::strains(self, map).expect("no conversion required"))
315 }
316 }
317 }
318
319 pub fn checked_strains(&self, map: &Beatmap) -> Result<Strains, TooSuspicious> {
324 map.check_suspicion()?;
325
326 Ok(self.strains(map))
327 }
328
329 pub fn strains_for_mode<M: IGameMode>(
331 &self,
332 map: &Beatmap,
333 ) -> Result<M::Strains, ConvertError> {
334 M::strains(self, map)
335 }
336
337 pub fn gradual_difficulty(self, map: &Beatmap) -> GradualDifficulty {
339 GradualDifficulty::new(self, map)
340 }
341
342 pub fn checked_gradual_difficulty(
345 self,
346 map: &Beatmap,
347 ) -> Result<GradualDifficulty, TooSuspicious> {
348 GradualDifficulty::checked_new(self, map)
349 }
350
351 pub fn gradual_difficulty_for_mode<M: IGameMode>(
353 self,
354 map: &Beatmap,
355 ) -> Result<M::GradualDifficulty, ConvertError> {
356 M::gradual_difficulty(self, map)
357 }
358
359 pub fn gradual_performance(self, map: &Beatmap) -> GradualPerformance {
361 GradualPerformance::new(self, map)
362 }
363
364 pub fn checked_gradual_performance(
367 self,
368 map: &Beatmap,
369 ) -> Result<GradualPerformance, TooSuspicious> {
370 GradualPerformance::checked_new(self, map)
371 }
372
373 pub fn gradual_performance_for_mode<M: IGameMode>(
375 self,
376 map: &Beatmap,
377 ) -> Result<M::GradualPerformance, ConvertError> {
378 M::gradual_performance(self, map)
379 }
380
381 pub(crate) const fn get_mods(&self) -> &GameMods {
382 &self.mods
383 }
384
385 pub(crate) fn get_clock_rate(&self) -> f64 {
386 self.clock_rate
387 .map_or(self.mods.clock_rate(), non_zero_u64_to_f64)
388 }
389
390 pub(crate) fn get_passed_objects(&self) -> usize {
391 self.passed_objects.map_or(usize::MAX, |n| n as usize)
392 }
393
394 pub(crate) const fn get_map_difficulty(&self) -> &BeatmapDifficulty {
395 &self.map_difficulty
396 }
397
398 pub(crate) fn get_hardrock_offsets(&self) -> bool {
399 self.hardrock_offsets
400 .unwrap_or_else(|| self.mods.hardrock_offsets())
401 }
402
403 pub(crate) fn get_lazer(&self) -> bool {
404 self.lazer.unwrap_or(true)
405 }
406}
407
408const fn non_zero_u64_to_f64(n: NonZeroU64) -> f64 {
409 f64::from_bits(n.get())
410}
411
412impl Debug for Difficulty {
413 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
414 let Self {
415 mods,
416 passed_objects,
417 clock_rate,
418 map_difficulty,
419 hardrock_offsets,
420 lazer,
421 } = self;
422
423 f.debug_struct("Difficulty")
424 .field("mods", mods)
425 .field("passed_objects", passed_objects)
426 .field("clock_rate", &clock_rate.map(non_zero_u64_to_f64))
427 .field("ar", &map_difficulty.ar)
428 .field("cs", &map_difficulty.cs)
429 .field("hp", &map_difficulty.hp)
430 .field("od", &map_difficulty.od)
431 .field("hardrock_offsets", hardrock_offsets)
432 .field("lazer", lazer)
433 .finish()
434 }
435}
436
437impl Default for Difficulty {
438 fn default() -> Self {
439 Self::new()
440 }
441}