rgc_chart/models/osu/
difficulty.rs1use 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}