peace_performance/taiko/
pp.rs1use super::{stars, DifficultyAttributes};
2use crate::{Beatmap, Mods, PpRaw, PpResult, StarResult};
3
4#[derive(Clone, Debug)]
32#[allow(clippy::upper_case_acronyms)]
33pub struct TaikoPP<'m> {
34 map: &'m Beatmap,
35 stars: Option<f32>,
36 mods: u32,
37 max_combo: usize,
38 combo: Option<usize>,
39 acc: f32,
40 n_misses: usize,
41 passed_objects: Option<usize>,
42
43 n300: Option<usize>,
44 n100: Option<usize>,
45}
46
47impl<'m> TaikoPP<'m> {
48 #[inline]
49 pub fn new(map: &'m Beatmap) -> Self {
50 Self {
51 map,
52 stars: None,
53 mods: 0,
54 max_combo: map.n_circles as usize,
55 combo: None,
56 acc: 1.0,
57 n_misses: 0,
58 passed_objects: None,
59 n300: None,
60 n100: None,
61 }
62 }
63
64 #[inline]
70 pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self {
71 if let Some(stars) = attributes.attributes() {
72 self.stars.replace(stars);
73 }
74
75 self
76 }
77
78 #[inline]
82 pub fn mods(mut self, mods: u32) -> Self {
83 self.mods = mods;
84
85 self
86 }
87
88 #[inline]
90 pub fn combo(mut self, combo: usize) -> Self {
91 self.combo.replace(combo);
92
93 self
94 }
95
96 #[inline]
98 pub fn n300(mut self, n300: usize) -> Self {
99 self.n300.replace(n300);
100
101 self
102 }
103
104 #[inline]
106 pub fn n100(mut self, n100: usize) -> Self {
107 self.n100.replace(n100);
108
109 self
110 }
111
112 #[inline]
114 pub fn misses(mut self, n_misses: usize) -> Self {
115 self.n_misses = n_misses.min(self.map.n_circles as usize);
116
117 self
118 }
119
120 #[inline]
122 pub fn accuracy(mut self, acc: f32) -> Self {
123 self.set_accuracy(acc);
124 self
125 }
126
127 #[inline(always)]
128 pub fn set_accuracy(&mut self, acc: f32) {
173 self.acc = acc / 100.0;
174 self.n300.take();
175 self.n100.take();
176 }
177
178 #[inline]
180 pub fn passed_objects(mut self, passed_objects: usize) -> Self {
181 self.passed_objects.replace(passed_objects);
182
183 self
184 }
185
186 pub fn calculate(&mut self) -> PpResult {
188 let stars = self
189 .stars
190 .unwrap_or_else(|| stars(self.map, self.mods, self.passed_objects).stars());
191
192 if self.n300.or(self.n100).is_some() {
193 let total = self.map.n_circles as usize;
194 let misses = self.n_misses;
195
196 let mut n300 = self.n300.unwrap_or(0).min(total - misses);
197 let mut n100 = self.n100.unwrap_or(0).min(total - n300 - misses);
198
199 let given = n300 + n100 + misses;
200 let missing = total - given;
201
202 match (self.n300, self.n100) {
203 (Some(_), Some(_)) => n300 += missing,
204 (Some(_), None) => n100 += missing,
205 (None, Some(_)) => n300 += missing,
206 (None, None) => unreachable!(),
207 };
208
209 self.acc = (2 * n300 + n100) as f32 / (2 * (n300 + n100 + misses)) as f32;
210 }
211
212 let mut multiplier = 1.1;
213
214 if self.mods.nf() {
215 multiplier *= 0.9;
216 }
217
218 if self.mods.hd() {
219 multiplier *= 1.1;
220 }
221
222 let strain_value = self.compute_strain_value(stars);
223 let acc_value = self.compute_accuracy_value();
224
225 let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
226
227 PpResult {
228 mode: 1,
229 mods: self.mods,
230 pp,
231 raw: PpRaw::new(None, None, Some(strain_value), Some(acc_value), pp),
232 attributes: StarResult::Taiko(DifficultyAttributes { stars }),
233 }
234 }
235
236 #[inline]
237 pub async fn calculate_async(&mut self) -> PpResult {
238 self.calculate()
239 }
240
241 fn compute_strain_value(&self, stars: f32) -> f32 {
242 let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
243 let mut strain = exp_base * exp_base / 100_000.0;
244
245 let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
247 strain *= len_bonus;
248
249 strain *= 0.985_f32.powi(self.n_misses as i32);
251
252 if self.mods.hd() {
254 strain *= 1.025;
255 }
256
257 if self.mods.fl() {
259 strain *= 1.05 * len_bonus;
260 }
261
262 strain * self.acc
264 }
265
266 #[inline]
267 fn compute_accuracy_value(&self) -> f32 {
268 let mut od = self.map.od;
269
270 if self.mods.hr() {
271 od *= 1.4;
272 } else if self.mods.ez() {
273 od *= 0.5;
274 }
275
276 let hit_window = difficulty_range_od(od).floor() / self.mods.speed();
277
278 (150.0 / hit_window).powf(1.1)
279 * self.acc.powi(15)
280 * 22.0
281 * (self.max_combo as f32 / 1500.0).powf(0.3).min(1.15)
282 }
283}
284
285const HITWINDOW_MIN: f32 = 50.0;
286const HITWINDOW_AVG: f32 = 35.0;
287const HITWINDOW_MAX: f32 = 20.0;
288
289#[inline]
290fn difficulty_range_od(od: f32) -> f32 {
291 crate::difficulty_range(od, HITWINDOW_MAX, HITWINDOW_AVG, HITWINDOW_MIN)
292}
293
294pub trait TaikoAttributeProvider {
295 fn attributes(self) -> Option<f32>;
296}
297
298impl TaikoAttributeProvider for f32 {
299 #[inline]
300 fn attributes(self) -> Option<f32> {
301 Some(self)
302 }
303}
304
305impl TaikoAttributeProvider for DifficultyAttributes {
306 #[inline]
307 fn attributes(self) -> Option<f32> {
308 Some(self.stars)
309 }
310}
311
312impl TaikoAttributeProvider for StarResult {
313 #[inline]
314 fn attributes(self) -> Option<f32> {
315 #[allow(irrefutable_let_patterns)]
316 if let StarResult::Taiko(attributes) = self {
317 Some(attributes.stars)
318 } else {
319 None
320 }
321 }
322}
323
324impl TaikoAttributeProvider for PpResult {
325 #[inline]
326 fn attributes(self) -> Option<f32> {
327 self.attributes.attributes()
328 }
329}