osu_map_analyzer/analyze/
jump.rs1use crate::utils::{bpm, calculate_distance};
2use rosu_map::{section::hit_objects::HitObject, Beatmap};
3use std::collections::VecDeque;
4
5pub struct Jump {
6 map: Beatmap,
7}
8
9#[derive(Debug, Clone, Copy, serde::Deserialize)]
10#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
11pub struct JumpAnalysis {
12 pub overall_confidence: f64,
13 pub total_jump_count: usize,
14
15 pub max_jump_length: usize,
16 pub long_jumps: usize,
17 pub medium_jumps: usize,
18 pub short_jumps: usize,
19
20 pub jump_density: f64,
21 pub bpm_consistency: f64,
22}
23
24impl Jump {
25 pub fn new(map: Beatmap) -> Self {
31 Self { map }
32 }
33
34 pub fn analyze(&mut self) -> JumpAnalysis {
47 let bpm = bpm(
48 self.map.hit_objects.last_mut(),
49 &self.map.control_points.timing_points,
50 );
51 let beat_length = 60.0 / bpm * 1000.0;
52 let expected_jump_interval = beat_length / 2.0; let hit_objects = &self.map.hit_objects;
54
55 let (consecutive_notes, bpm_variations) =
56 self.calculate_consecutive_notes(hit_objects, expected_jump_interval);
57
58 let short_jumps_amount = consecutive_notes
60 .iter()
61 .filter(|&&len| len >= 4 && len < 7)
62 .count();
63 let medium_jumps_amount = consecutive_notes
64 .iter()
65 .filter(|&&len| len >= 7 && len < 12)
66 .count();
67 let long_jumps_amount = consecutive_notes.iter().filter(|&&len| len >= 12).count();
68
69 let jumps_lengths: Vec<usize> = consecutive_notes
73 .iter()
74 .filter(|&&len| len >= 4)
75 .map(|&len| len)
76 .collect();
77
78 let total_jump_notes: usize = jumps_lengths.iter().sum();
79 let jump_density = total_jump_notes as f64 / hit_objects.len() as f64;
80
81 let max_jump_length = *consecutive_notes.iter().max().unwrap_or(&0);
82 let total_jumps_amount = short_jumps_amount + medium_jumps_amount + long_jumps_amount;
83
84 let bpm_consistency = if !bpm_variations.is_empty() {
85 1.0 - (bpm_variations.iter().sum::<f64>() / bpm_variations.len() as f64)
86 / expected_jump_interval
87 } else {
88 0.0
89 };
90
91 let average_jump_length = if total_jumps_amount > 0 {
92 total_jump_notes as f64 / total_jumps_amount as f64
93 } else {
94 0.0
95 };
96
97 let jump_variety = (medium_jumps_amount * 2 + long_jumps_amount * 3) as f64
98 / (short_jumps_amount + medium_jumps_amount + long_jumps_amount).max(1) as f64;
99
100 let long_jump_ratio = long_jumps_amount as f64 / total_jumps_amount as f64;
101
102 let overall_confidence = (jump_density * 0.4
103 + bpm_consistency * 0.2
104 + jump_variety * 0.35
105 + long_jump_ratio * 0.45
106 + (average_jump_length / 3.0).min(1.0) * 0.3)
107 .min(1.0);
108
109 JumpAnalysis {
110 long_jumps: long_jumps_amount,
111 medium_jumps: medium_jumps_amount,
112 short_jumps: short_jumps_amount,
113 max_jump_length,
114 total_jump_count: total_jumps_amount,
115 overall_confidence,
116 jump_density,
117 bpm_consistency,
118 }
119 }
120
121 fn calculate_consecutive_notes(
122 &self,
123 hit_objects: &[HitObject],
124 expected_interval: f64,
125 ) -> (Vec<usize>, Vec<f64>) {
126 let mut jumps_lengths = Vec::new();
127 let mut curr_jump = VecDeque::new();
128 let mut bpm_variations = Vec::new();
129 let tolerance = 0.10; let distance_threshold = 120.0_f32;
131
132 for pair in hit_objects.windows(2) {
133 let obj1 = &pair[0];
134 let obj2 = &pair[1];
135
136 let time_diff = obj2.start_time - obj1.start_time;
137 let distance = calculate_distance(obj1, obj2);
138
139 if (time_diff - expected_interval).abs() / expected_interval <= tolerance
141 && distance >= distance_threshold
142 {
143 curr_jump.push_back(time_diff);
144 if curr_jump.len() > 1 {
145 let prev_diff = curr_jump[curr_jump.len() - 2];
146 bpm_variations.push((time_diff - prev_diff).abs());
147 }
148 } else if !curr_jump.is_empty() {
149 jumps_lengths.push(curr_jump.len());
150 curr_jump.clear();
151 }
152 }
153
154 if !curr_jump.is_empty() {
155 jumps_lengths.push(curr_jump.len());
156 }
157
158 (jumps_lengths, bpm_variations)
159 }
160}