rosu_pp/mania/difficulty/
gradual.rs1use std::cmp;
2
3use rosu_map::section::general::GameMode;
4
5use crate::{
6 any::difficulty::skills::StrainSkill,
7 mania::{convert, object::ObjectParams},
8 model::{hit_object::HitObject, mode::ConvertError},
9 Beatmap, Difficulty,
10};
11
12use super::{
13 object::ManiaDifficultyObject, skills::strain::Strain, DifficultyValues,
14 ManiaDifficultyAttributes, ManiaObject, DIFFICULTY_MULTIPLIER,
15};
16
17pub struct ManiaGradualDifficulty {
50 pub(crate) idx: usize,
51 pub(crate) difficulty: Difficulty,
52 objects_is_circle: Box<[bool]>,
53 is_convert: bool,
54 strain: Strain,
55 diff_objects: Box<[ManiaDifficultyObject]>,
56 note_state: NoteState,
57}
58
59#[derive(Default)]
60struct NoteState {
61 curr_combo: u32,
62 n_hold_notes: u32,
63}
64
65impl ManiaGradualDifficulty {
66 pub fn new(difficulty: Difficulty, map: &Beatmap) -> Result<Self, ConvertError> {
68 let mut map = map.convert_ref(GameMode::Mania, difficulty.get_mods())?;
69
70 if difficulty.get_mods().ho() {
71 convert::apply_hold_off_to_beatmap(map.to_mut());
72 }
73
74 if difficulty.get_mods().invert() {
75 convert::apply_invert_to_beatmap(map.to_mut());
76 }
77
78 if let Some(seed) = difficulty.get_mods().random_seed() {
79 convert::apply_random_to_beatmap(map.to_mut(), seed);
80 }
81
82 let take = difficulty.get_passed_objects();
83 let total_columns = map.cs.round_ties_even().max(1.0);
84 let clock_rate = difficulty.get_clock_rate();
85 let mut params = ObjectParams::new(&map);
86
87 let mania_objects = map
88 .hit_objects
89 .iter()
90 .map(|h| ManiaObject::new(h, total_columns, &mut params))
91 .take(take);
92
93 let diff_objects = DifficultyValues::create_difficulty_objects(clock_rate, mania_objects);
94
95 let strain = Strain::new(total_columns as usize);
96
97 let mut note_state = NoteState::default();
98
99 let objects_is_circle: Box<[_]> =
100 map.hit_objects.iter().map(HitObject::is_circle).collect();
101
102 if let Some(h) = map.hit_objects.first() {
103 let hit_object = ManiaObject::new(h, total_columns, &mut params);
104
105 increment_combo_raw(
106 objects_is_circle[0],
107 hit_object.start_time,
108 hit_object.end_time,
109 &mut note_state,
110 );
111 }
112
113 Ok(Self {
114 idx: 0,
115 difficulty,
116 objects_is_circle,
117 is_convert: map.is_convert,
118 strain,
119 diff_objects,
120 note_state,
121 })
122 }
123}
124
125impl Iterator for ManiaGradualDifficulty {
126 type Item = ManiaDifficultyAttributes;
127
128 fn next(&mut self) -> Option<Self::Item> {
129 if self.idx > 0 {
134 let curr = self.diff_objects.get(self.idx - 1)?;
135 self.strain.process(curr, &self.diff_objects);
136
137 let is_circle = self.objects_is_circle[self.idx];
138 increment_combo(
139 is_circle,
140 curr,
141 &mut self.note_state,
142 self.difficulty.get_clock_rate(),
143 );
144 } else if self.objects_is_circle.is_empty() {
145 return None;
146 }
147
148 self.idx += 1;
149
150 Some(ManiaDifficultyAttributes {
151 stars: self.strain.cloned_difficulty_value() * DIFFICULTY_MULTIPLIER,
152 max_combo: self.note_state.curr_combo,
153 n_objects: self.idx as u32,
154 n_hold_notes: self.note_state.n_hold_notes,
155 is_convert: self.is_convert,
156 })
157 }
158
159 fn size_hint(&self) -> (usize, Option<usize>) {
160 let len = self.len();
161
162 (len, Some(len))
163 }
164
165 fn nth(&mut self, n: usize) -> Option<Self::Item> {
166 let skip_iter = self
167 .diff_objects
168 .iter()
169 .zip(self.objects_is_circle.iter().skip(1))
170 .skip(self.idx.saturating_sub(1));
171
172 let mut take = cmp::min(n, self.len().saturating_sub(1));
173
174 if self.idx == 0 && take > 0 {
176 take -= 1;
177 self.idx += 1;
178 }
179
180 let clock_rate = self.difficulty.get_clock_rate();
181
182 for (curr, is_circle) in skip_iter.take(take) {
183 increment_combo(*is_circle, curr, &mut self.note_state, clock_rate);
184 self.strain.process(curr, &self.diff_objects);
185 self.idx += 1;
186 }
187
188 self.next()
189 }
190}
191
192impl ExactSizeIterator for ManiaGradualDifficulty {
193 fn len(&self) -> usize {
194 self.diff_objects.len() + 1 - self.idx
195 }
196}
197
198fn increment_combo(
199 is_circle: bool,
200 diff_obj: &ManiaDifficultyObject,
201 state: &mut NoteState,
202 clock_rate: f64,
203) {
204 increment_combo_raw(
205 is_circle,
206 diff_obj.start_time * clock_rate,
207 diff_obj.end_time * clock_rate,
208 state,
209 );
210}
211
212fn increment_combo_raw(is_circle: bool, start_time: f64, end_time: f64, state: &mut NoteState) {
213 if is_circle {
214 state.curr_combo += 1;
215 } else {
216 state.curr_combo += 1 + ((end_time - start_time) / 100.0) as u32;
217 state.n_hold_notes += 1;
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use crate::{mania::Mania, Beatmap};
224
225 use super::*;
226
227 #[test]
228 fn empty() {
229 let map = Beatmap::from_bytes(&[]).unwrap();
230 let mut gradual = ManiaGradualDifficulty::new(Difficulty::new(), &map).unwrap();
231 assert!(gradual.next().is_none());
232 }
233
234 #[test]
235 fn next_and_nth() {
236 let map = Beatmap::from_path("./resources/1638954.osu").unwrap();
237
238 let difficulty = Difficulty::new();
239
240 let mut gradual = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
241 let mut gradual_2nd = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
242 let mut gradual_3rd = ManiaGradualDifficulty::new(difficulty.clone(), &map).unwrap();
243
244 let hit_objects_len = map.hit_objects.len();
245
246 for i in 1.. {
247 let Some(next_gradual) = gradual.next() else {
248 assert_eq!(i, hit_objects_len + 1);
249 assert!(gradual_2nd.last().is_some() || hit_objects_len % 2 == 0);
250 assert!(gradual_3rd.last().is_some() || hit_objects_len % 3 == 0);
251 break;
252 };
253
254 if i % 2 == 0 {
255 let next_gradual_2nd = gradual_2nd.nth(1).unwrap();
256 assert_eq!(next_gradual, next_gradual_2nd);
257 }
258
259 if i % 3 == 0 {
260 let next_gradual_3rd = gradual_3rd.nth(2).unwrap();
261 assert_eq!(next_gradual, next_gradual_3rd);
262 }
263
264 let expected = difficulty
265 .clone()
266 .passed_objects(i as u32)
267 .calculate_for_mode::<Mania>(&map)
268 .unwrap();
269
270 assert_eq!(next_gradual, expected);
271 }
272 }
273}