rosu_pp/catch/difficulty/
gradual.rs

1use std::cmp;
2
3use rosu_map::section::general::GameMode;
4
5use crate::{
6    any::difficulty::skills::StrainSkill,
7    catch::{
8        attributes::{GradualObjectCount, ObjectCountBuilder},
9        catcher::Catcher,
10        convert::convert_objects,
11        CatchDifficultyAttributes,
12    },
13    model::mode::ConvertError,
14    Beatmap, Difficulty,
15};
16
17use super::{
18    object::CatchDifficultyObject, skills::movement::Movement, CatchDifficultySetup,
19    DifficultyValues,
20};
21
22/// Gradually calculate the difficulty attributes of an osu!catch map.
23///
24/// Note that this struct implements [`Iterator`].
25/// On every call of [`Iterator::next`], the map's next fruit or droplet
26/// will be processed and the [`CatchDifficultyAttributes`] will be updated and
27/// returned.
28///
29/// Note that it does not return attributes after a tiny droplet. Only for
30/// fruits and droplets.
31///
32/// If you want to calculate performance attributes, use
33/// [`CatchGradualPerformance`] instead.
34///
35/// # Example
36///
37/// ```
38/// use rosu_pp::{Beatmap, Difficulty};
39/// use rosu_pp::catch::{Catch, CatchGradualDifficulty};
40///
41/// let map = Beatmap::from_path("./resources/2118524.osu").unwrap();
42///
43/// let difficulty = Difficulty::new().mods(64); // DT
44/// let mut iter = CatchGradualDifficulty::new(difficulty, &map).unwrap();
45///
46/// // the difficulty of the map after the first hit object
47/// let attrs1 = iter.next();
48/// // ... after the second hit object
49/// let attrs2 = iter.next();
50///
51/// // Remaining hit objects
52/// for difficulty in iter {
53///     // ...
54/// }
55/// ```
56///
57/// [`CatchGradualPerformance`]: crate::catch::CatchGradualPerformance
58pub struct CatchGradualDifficulty {
59    pub(crate) idx: usize,
60    pub(crate) difficulty: Difficulty,
61    attrs: CatchDifficultyAttributes,
62    /// The delta of object counts after each palpable object
63    count: Vec<GradualObjectCount>,
64    diff_objects: Box<[CatchDifficultyObject]>,
65    movement: Movement,
66}
67
68impl CatchGradualDifficulty {
69    /// Create a new difficulty attributes iterator for osu!catch maps.
70    pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
71        let map = map.convert_ref(GameMode::Catch, difficulty.get_mods())?;
72
73        let clock_rate = difficulty.get_clock_rate();
74
75        let CatchDifficultySetup { map_attrs, attrs } =
76            CatchDifficultySetup::new(&difficulty, &map);
77
78        let hr_offsets = difficulty.get_hardrock_offsets();
79        let reflection = difficulty.get_mods().reflection();
80        let mut count = ObjectCountBuilder::new_gradual();
81
82        let palpable_objects = convert_objects(
83            &map,
84            &mut count,
85            reflection,
86            hr_offsets,
87            map_attrs.cs as f32,
88        );
89
90        let mut half_catcher_width = Catcher::calculate_catch_width(map_attrs.cs as f32) * 0.5;
91        half_catcher_width *= 1.0 - ((map_attrs.cs as f32 - 5.5).max(0.0) * 0.0625);
92
93        let diff_objects = DifficultyValues::create_difficulty_objects(
94            clock_rate,
95            half_catcher_width,
96            palpable_objects.iter(),
97        );
98
99        let count = count.into_gradual();
100        let movement = Movement::new(half_catcher_width, clock_rate);
101
102        Ok(Self {
103            idx: 0,
104            difficulty,
105            attrs,
106            count,
107            diff_objects,
108            movement,
109        })
110    }
111}
112
113impl Iterator for CatchGradualDifficulty {
114    type Item = CatchDifficultyAttributes;
115
116    fn next(&mut self) -> Option<Self::Item> {
117        // The first difficulty object belongs to the second palpable object
118        // since each difficulty object requires the current and the last note.
119        // Hence, if we're still on the first object, we don't have a difficulty
120        // object yet and just skip processing.
121        if self.idx > 0 {
122            let curr = self.diff_objects.get(self.idx - 1)?;
123            self.movement.process(curr, &self.diff_objects);
124        } else if self.count.is_empty() {
125            return None;
126        }
127
128        self.attrs.add_object_count(self.count[self.idx]);
129        self.idx += 1;
130
131        let mut attrs = self.attrs.clone();
132
133        let movement = self.movement.cloned_difficulty_value();
134        DifficultyValues::eval(&mut attrs, movement);
135
136        Some(attrs)
137    }
138
139    fn size_hint(&self) -> (usize, Option<usize>) {
140        let len = self.len();
141
142        (len, Some(len))
143    }
144
145    fn nth(&mut self, n: usize) -> Option<Self::Item> {
146        let skip_iter = self.diff_objects.iter().skip(self.idx.saturating_sub(1));
147
148        let mut take = cmp::min(n, self.len().saturating_sub(1));
149
150        // The first palpable object has no difficulty object
151        if self.idx == 0 && take > 0 {
152            take -= 1;
153            self.attrs.add_object_count(self.count[self.idx]);
154            self.idx += 1;
155        }
156
157        for curr in skip_iter.take(take) {
158            self.movement.process(curr, &self.diff_objects);
159
160            self.attrs.add_object_count(self.count[self.idx]);
161            self.idx += 1;
162        }
163
164        self.next()
165    }
166}
167
168impl ExactSizeIterator for CatchGradualDifficulty {
169    fn len(&self) -> usize {
170        self.diff_objects.len() + 1 - self.idx
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use crate::{catch::Catch, Beatmap};
177
178    use super::*;
179
180    #[test]
181    fn empty() {
182        let map = Beatmap::from_bytes(&[]).unwrap();
183        let mut gradual = CatchGradualDifficulty::new(Difficulty::new(), &map).unwrap();
184        assert!(gradual.next().is_none());
185    }
186
187    #[test]
188    fn next_and_nth() {
189        let map = Beatmap::from_path("./resources/2118524.osu").unwrap();
190
191        let difficulty = Difficulty::new();
192
193        let mut gradual = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
194        let mut gradual_2nd = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
195        let mut gradual_3rd = CatchGradualDifficulty::new(difficulty.clone(), &map).unwrap();
196
197        for i in 1.. {
198            let Some(next_gradual) = gradual.next() else {
199                assert_eq!(i, 731);
200                assert!(gradual_2nd.last().is_none()); // 730 % 2 == 0
201                assert!(gradual_3rd.last().is_some()); // 730 % 3 == 1
202                break;
203            };
204
205            if i % 2 == 0 {
206                let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
207                assert_eq!(next_gradual, next_gradual_2nd);
208            }
209
210            if i % 3 == 0 {
211                let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
212                assert_eq!(next_gradual, next_gradual_3rd);
213            }
214
215            let expected = difficulty
216                .clone()
217                .passed_objects(i as u32)
218                .calculate_for_mode::<Catch>(&map)
219                .unwrap();
220
221            assert_eq!(next_gradual, expected);
222        }
223    }
224}