Skip to main content

rgc_chart/models/osu/
difficulty.rs

1use std::str::FromStr;
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, PartialEq)]
5pub struct Difficulty {
6    pub hp_drain_rate: f32,
7    
8    pub circle_size: f32,
9    
10    pub overall_difficulty: f32,
11    
12    pub approach_rate: f32,
13    
14    pub slider_multiplier: f32,
15    
16    pub slider_tick_rate: f32,
17}
18
19impl Default for Difficulty {
20    fn default() -> Self {
21        Self {
22            hp_drain_rate: 5.0,
23            circle_size: 5.0,
24            overall_difficulty: 5.0,
25            approach_rate: 5.0,
26            slider_multiplier: 1.4,
27            slider_tick_rate: 1.0,
28        }
29    }
30}
31
32impl FromStr for Difficulty {
33    type Err = String;
34    
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        let mut difficulty = Difficulty::default();
37        
38        let mut key_values = HashMap::new();
39        for line in s.lines() {
40            let line = line.trim();
41            if line.is_empty() || line.starts_with("//") {
42                continue;
43            }
44            
45            if let Some((key, value)) = line.split_once(':') {
46                let key = key.trim();
47                let value = value.trim();
48                key_values.insert(key.to_string(), value.to_string());
49            }
50        }
51        
52        if let Some(value) = key_values.get("HPDrainRate") {
53            difficulty.hp_drain_rate = value.parse::<f32>()
54                .map_err(|_| format!("Invalid HPDrainRate value: {}", value))?;
55        }
56        
57        if let Some(value) = key_values.get("CircleSize") {
58            difficulty.circle_size = value.parse::<f32>()
59                .map_err(|_| format!("Invalid CircleSize value: {}", value))?;
60        }
61        
62        if let Some(value) = key_values.get("OverallDifficulty") {
63            difficulty.overall_difficulty = value.parse::<f32>()
64                .map_err(|_| format!("Invalid OverallDifficulty value: {}", value))?;
65        }
66        
67        if let Some(value) = key_values.get("ApproachRate") {
68            difficulty.approach_rate = value.parse::<f32>()
69                .map_err(|_| format!("Invalid ApproachRate value: {}", value))?;
70        }
71        
72        if let Some(value) = key_values.get("SliderMultiplier") {
73            difficulty.slider_multiplier = value.parse::<f32>()
74                .map_err(|_| format!("Invalid SliderMultiplier value: {}", value))?;
75        }
76        
77        if let Some(value) = key_values.get("SliderTickRate") {
78            difficulty.slider_tick_rate = value.parse::<f32>()
79                .map_err(|_| format!("Invalid SliderTickRate value: {}", value))?;
80        }
81        
82        Ok(difficulty)
83    }
84}
85
86impl Difficulty {
87    pub fn new(
88        hp_drain_rate: f32,
89        circle_size: f32,
90        overall_difficulty: f32,
91        approach_rate: f32,
92        slider_multiplier: f32,
93        slider_tick_rate: f32,
94    ) -> Self {
95        Self {
96            hp_drain_rate,
97            circle_size,
98            overall_difficulty,
99            approach_rate,
100            slider_multiplier,
101            slider_tick_rate,
102        }
103    }
104    
105    pub fn is_valid(&self) -> bool {
106        let in_range = |val: f32| val >= 0.0 && val <= 10.0;
107        
108        in_range(self.hp_drain_rate)
109            && in_range(self.circle_size)
110            && in_range(self.overall_difficulty)
111            && in_range(self.approach_rate)
112            && self.slider_multiplier > 0.0
113            && self.slider_tick_rate > 0.0
114    }
115    
116    pub fn difficulty_rating(&self) -> &'static str {
117        let avg = (self.hp_drain_rate + self.circle_size + self.overall_difficulty + self.approach_rate) / 4.0;
118        
119        match avg {
120            x if x < 2.0 => "Easy",
121            x if x < 2.7 => "Normal",
122            x if x < 4.0 => "Hard",
123            x if x < 5.3 => "Insane",
124            x if x < 6.5 => "Expert",
125            _ => "Expert+",
126        }
127    }
128    
129    pub fn circle_size_pixels(&self) -> f32 {
130        54.4 - 4.48 * self.circle_size
131    }
132    
133    pub fn approach_rate_ms(&self) -> f32 {
134        if self.approach_rate < 5.0 {
135            1800.0 - 120.0 * self.approach_rate
136        } else {
137            1950.0 - 150.0 * self.approach_rate
138        }
139    }
140    
141    pub fn od_300_window(&self, overall_difficulty: Option<f32>) -> f32 {
142        if overall_difficulty.is_some() {
143            return 80.0 - 6.0 * overall_difficulty.unwrap()
144        }
145
146        80.0 - 6.0 * self.overall_difficulty
147    }
148    
149    pub fn od_100_window(&self, overall_difficulty: Option<f32>) -> f32 {
150        if overall_difficulty.is_some() {
151            return 140.0 - 8.0 * overall_difficulty.unwrap()
152        }
153
154        140.0 - 8.0 * self.overall_difficulty
155    }
156    
157    pub fn od_50_window(&self, overall_difficulty: Option<f32>) -> f32 {
158        if overall_difficulty.is_some() {
159            return 200.0 - 10.0 * overall_difficulty.unwrap()
160        }
161
162        200.0 - 10.0 * self.overall_difficulty
163    }
164    
165    pub fn to_osu_format(&self) -> String {
166        format!(
167            "HPDrainRate: {}\nCircleSize: {}\nOverallDifficulty: {}\nApproachRate: {}\nSliderMultiplier: {}\nSliderTickRate: {}",
168            self.hp_drain_rate,
169            self.circle_size,
170            self.overall_difficulty,
171            self.approach_rate,
172            self.slider_multiplier,
173            self.slider_tick_rate
174        )
175    }
176}