1use std::{
2 fmt::{Debug, Formatter, Result as FmtResult},
3 num::NonZeroU64,
4};
5
6use rosu_map::section::general::GameMode;
7
8use crate::{
9 catch::Catch,
10 mania::Mania,
11 model::{beatmap::Beatmap, mode::ConvertError, mods::GameMods},
12 osu::Osu,
13 taiko::Taiko,
14 GradualDifficulty, GradualPerformance,
15};
16
17use super::{attributes::DifficultyAttributes, InspectDifficulty, Strains};
18
19pub mod gradual;
20pub mod inspect;
21pub mod object;
22pub mod skills;
23
24use crate::model::mode::IGameMode;
25
26#[derive(Clone, PartialEq)]
40#[must_use]
41pub struct Difficulty {
42 mods: GameMods,
43 passed_objects: Option<u32>,
44 clock_rate: Option<NonZeroU64>,
51 ar: Option<ModsDependent>,
52 cs: Option<ModsDependent>,
53 hp: Option<ModsDependent>,
54 od: Option<ModsDependent>,
55 hardrock_offsets: Option<bool>,
56 lazer: Option<bool>,
57}
58
59#[derive(Copy, Clone, Debug, Default, PartialEq)]
61pub struct ModsDependent {
62 pub value: f32,
64 pub with_mods: bool,
69}
70
71impl ModsDependent {
72 pub const fn new(value: f32) -> Self {
73 Self {
74 value,
75 with_mods: false,
76 }
77 }
78}
79
80impl Difficulty {
81 pub const fn new() -> Self {
83 Self {
84 mods: GameMods::DEFAULT,
85 passed_objects: None,
86 clock_rate: None,
87 ar: None,
88 cs: None,
89 hp: None,
90 od: None,
91 hardrock_offsets: None,
92 lazer: None,
93 }
94 }
95
96 pub fn inspect(self) -> InspectDifficulty {
99 let Self {
100 mods,
101 passed_objects,
102 clock_rate,
103 ar,
104 cs,
105 hp,
106 od,
107 hardrock_offsets,
108 lazer,
109 } = self;
110
111 InspectDifficulty {
112 mods,
113 passed_objects,
114 clock_rate: clock_rate.map(non_zero_u64_to_f64),
115 ar,
116 cs,
117 hp,
118 od,
119 hardrock_offsets,
120 lazer,
121 }
122 }
123
124 pub fn mods(self, mods: impl Into<GameMods>) -> Self {
135 Self {
136 mods: mods.into(),
137 ..self
138 }
139 }
140
141 pub const fn passed_objects(mut self, passed_objects: u32) -> Self {
143 self.passed_objects = Some(passed_objects);
144
145 self
146 }
147
148 pub fn clock_rate(self, clock_rate: f64) -> Self {
157 let clock_rate = clock_rate.clamp(0.01, 100.0).to_bits();
158
159 let non_zero = unsafe { NonZeroU64::new_unchecked(clock_rate) };
162
163 Self {
164 clock_rate: Some(non_zero),
165 ..self
166 }
167 }
168
169 pub fn ar(self, ar: f32, with_mods: bool) -> Self {
181 Self {
182 ar: Some(ModsDependent {
183 value: ar.clamp(-20.0, 20.0),
184 with_mods,
185 }),
186 ..self
187 }
188 }
189
190 pub fn cs(self, cs: f32, with_mods: bool) -> Self {
202 Self {
203 cs: Some(ModsDependent {
204 value: cs.clamp(-20.0, 20.0),
205 with_mods,
206 }),
207 ..self
208 }
209 }
210
211 pub fn hp(self, hp: f32, with_mods: bool) -> Self {
221 Self {
222 hp: Some(ModsDependent {
223 value: hp.clamp(-20.0, 20.0),
224 with_mods,
225 }),
226 ..self
227 }
228 }
229
230 pub fn od(self, od: f32, with_mods: bool) -> Self {
240 Self {
241 od: Some(ModsDependent {
242 value: od.clamp(-20.0, 20.0),
243 with_mods,
244 }),
245 ..self
246 }
247 }
248
249 pub const fn hardrock_offsets(mut self, hardrock_offsets: bool) -> Self {
253 self.hardrock_offsets = Some(hardrock_offsets);
254
255 self
256 }
257
258 pub const fn lazer(mut self, lazer: bool) -> Self {
263 self.lazer = Some(lazer);
264
265 self
266 }
267
268 #[allow(clippy::missing_panics_doc)]
270 pub fn calculate(&self, map: &Beatmap) -> DifficultyAttributes {
271 match map.mode {
272 GameMode::Osu => DifficultyAttributes::Osu(
273 Osu::difficulty(self, map).expect("no conversion required"),
274 ),
275 GameMode::Taiko => DifficultyAttributes::Taiko(
276 Taiko::difficulty(self, map).expect("no conversion required"),
277 ),
278 GameMode::Catch => DifficultyAttributes::Catch(
279 Catch::difficulty(self, map).expect("no conversion required"),
280 ),
281 GameMode::Mania => DifficultyAttributes::Mania(
282 Mania::difficulty(self, map).expect("no conversion required"),
283 ),
284 }
285 }
286
287 pub fn calculate_for_mode<M: IGameMode>(
289 &self,
290 map: &Beatmap,
291 ) -> Result<M::DifficultyAttributes, ConvertError> {
292 M::difficulty(self, map)
293 }
294
295 #[allow(clippy::missing_panics_doc)]
300 pub fn strains(&self, map: &Beatmap) -> Strains {
301 match map.mode {
302 GameMode::Osu => Strains::Osu(Osu::strains(self, map).expect("no conversion required")),
303 GameMode::Taiko => {
304 Strains::Taiko(Taiko::strains(self, map).expect("no conversion required"))
305 }
306 GameMode::Catch => {
307 Strains::Catch(Catch::strains(self, map).expect("no conversion required"))
308 }
309 GameMode::Mania => {
310 Strains::Mania(Mania::strains(self, map).expect("no conversion required"))
311 }
312 }
313 }
314
315 pub fn strains_for_mode<M: IGameMode>(
317 &self,
318 map: &Beatmap,
319 ) -> Result<M::Strains, ConvertError> {
320 M::strains(self, map)
321 }
322
323 pub fn gradual_difficulty(self, map: &Beatmap) -> GradualDifficulty {
325 GradualDifficulty::new(self, map)
326 }
327
328 pub fn gradual_difficulty_for_mode<M: IGameMode>(
330 self,
331 map: &Beatmap,
332 ) -> Result<M::GradualDifficulty, ConvertError> {
333 M::gradual_difficulty(self, map)
334 }
335
336 pub fn gradual_performance(self, map: &Beatmap) -> GradualPerformance {
338 GradualPerformance::new(self, map)
339 }
340
341 pub fn gradual_performance_for_mode<M: IGameMode>(
343 self,
344 map: &Beatmap,
345 ) -> Result<M::GradualPerformance, ConvertError> {
346 M::gradual_performance(self, map)
347 }
348
349 pub(crate) const fn get_mods(&self) -> &GameMods {
350 &self.mods
351 }
352
353 pub(crate) fn get_clock_rate(&self) -> f64 {
354 self.clock_rate
355 .map_or(self.mods.clock_rate(), non_zero_u64_to_f64)
356 }
357
358 pub(crate) fn get_passed_objects(&self) -> usize {
359 self.passed_objects.map_or(usize::MAX, |n| n as usize)
360 }
361
362 pub(crate) const fn get_ar(&self) -> Option<ModsDependent> {
363 self.ar
364 }
365
366 pub(crate) const fn get_cs(&self) -> Option<ModsDependent> {
367 self.cs
368 }
369
370 pub(crate) const fn get_hp(&self) -> Option<ModsDependent> {
371 self.hp
372 }
373
374 pub(crate) const fn get_od(&self) -> Option<ModsDependent> {
375 self.od
376 }
377
378 pub(crate) fn get_hardrock_offsets(&self) -> bool {
379 self.hardrock_offsets
380 .unwrap_or_else(|| self.mods.hardrock_offsets())
381 }
382
383 pub(crate) fn get_lazer(&self) -> bool {
384 self.lazer.unwrap_or(true)
385 }
386}
387
388const fn non_zero_u64_to_f64(n: NonZeroU64) -> f64 {
389 f64::from_bits(n.get())
390}
391
392impl Debug for Difficulty {
393 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
394 let Self {
395 mods,
396 passed_objects,
397 clock_rate,
398 ar,
399 cs,
400 hp,
401 od,
402 hardrock_offsets,
403 lazer,
404 } = self;
405
406 f.debug_struct("Difficulty")
407 .field("mods", mods)
408 .field("passed_objects", passed_objects)
409 .field("clock_rate", &clock_rate.map(non_zero_u64_to_f64))
410 .field("ar", ar)
411 .field("cs", cs)
412 .field("hp", hp)
413 .field("od", od)
414 .field("hardrock_offsets", hardrock_offsets)
415 .field("lazer", lazer)
416 .finish()
417 }
418}
419
420impl Default for Difficulty {
421 fn default() -> Self {
422 Self::new()
423 }
424}