peace_performance/taiko/
pp.rs

1use super::{stars, DifficultyAttributes};
2use crate::{Beatmap, Mods, PpRaw, PpResult, StarResult};
3
4/// Calculator for pp on osu!taiko maps.
5///
6/// # Example
7///
8/// ```
9/// # use peace_performance::{TaikoPP, PpResult, Beatmap};
10/// # /*
11/// let map: Beatmap = ...
12/// # */
13/// # let map = Beatmap::default();
14/// let pp_result: PpResult = TaikoPP::new(&map)
15///     .mods(8 + 64) // HDDT
16///     .combo(1234)
17///     .misses(1)
18///     .accuracy(98.5)
19///     .calculate();
20///
21/// println!("PP: {} | Stars: {}", pp_result.pp(), pp_result.stars());
22///
23/// let next_result = TaikoPP::new(&map)
24///     .attributes(pp_result)  // reusing previous results for performance
25///     .mods(8 + 64)           // has to be the same to reuse attributes
26///     .accuracy(99.5)
27///     .calculate();
28///
29/// println!("PP: {} | Stars: {}", next_result.pp(), next_result.stars());
30/// ```
31#[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    /// [`TaikoAttributeProvider`] is implemented by `f32`, [`StarResult`](crate::StarResult),
65    /// and by [`PpResult`](crate::PpResult) meaning you can give the star rating,
66    /// the result of a star calculation, or the result of a pp calculation.
67    /// If you already calculated the stars for the current map-mod combination,
68    /// be sure to put them in here so that they don't have to be recalculated.
69    #[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    /// Specify mods through their bit values.
79    ///
80    /// See [https://github.com/ppy/osu-api/wiki#mods](https://github.com/ppy/osu-api/wiki#mods)
81    #[inline]
82    pub fn mods(mut self, mods: u32) -> Self {
83        self.mods = mods;
84
85        self
86    }
87
88    /// Specify the max combo of the play.
89    #[inline]
90    pub fn combo(mut self, combo: usize) -> Self {
91        self.combo.replace(combo);
92
93        self
94    }
95
96    /// Specify the amount of 300s of a play.
97    #[inline]
98    pub fn n300(mut self, n300: usize) -> Self {
99        self.n300.replace(n300);
100
101        self
102    }
103
104    /// Specify the amount of 100s of a play.
105    #[inline]
106    pub fn n100(mut self, n100: usize) -> Self {
107        self.n100.replace(n100);
108
109        self
110    }
111
112    /// Specify the amount of misses of the play.
113    #[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    /// Set the accuracy between 0.0 and 100.0.
121    #[inline]
122    pub fn accuracy(mut self, acc: f32) -> Self {
123        self.set_accuracy(acc);
124        self
125    }
126
127    #[inline(always)]
128    /// Set acc value
129    /// 
130    /// If it is used to calculate the PP of multiple different ACCs, 
131    /// it should be called from high to low according to the ACC value, otherwise it is invalid.
132    /// 
133    /// Examples:
134    /// ```
135    /// // valid
136    /// let acc_100 = {
137    ///     c.set_accuracy(100.0);
138    ///     c.calculate().await
139    /// };
140    /// let acc_99 = {
141    ///     c.set_accuracy(99.0);
142    ///     c.calculate().await
143    /// };
144    /// let acc_98 = {
145    ///     c.set_accuracy(98.0);
146    ///     c.calculate().await
147    /// };
148    /// let acc_95 = {
149    ///     c.set_accuracy(95.0);
150    ///     c.calculate().await
151    /// };
152    /// 
153    /// // invalid
154    /// let acc_95 = {
155    ///     c.set_accuracy(95.0);
156    ///     c.calculate().await
157    /// };
158    /// let acc_98 = {
159    ///     c.set_accuracy(98.0);
160    ///     c.calculate().await
161    /// };
162    /// let acc_99 = {
163    ///     c.set_accuracy(99.0);
164    ///     c.calculate().await
165    /// };
166    /// let acc_100 = {
167    ///     c.set_accuracy(100.0);
168    ///     c.calculate().await
169    /// };
170    /// ```
171    /// 
172    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    /// Amount of passed objects for partial plays, e.g. a fail.
179    #[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    /// Returns an object which contains the pp and stars.
187    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        // Longer maps are worth more
246        let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
247        strain *= len_bonus;
248
249        // Penalize misses exponentially
250        strain *= 0.985_f32.powi(self.n_misses as i32);
251
252        // HD bonus
253        if self.mods.hd() {
254            strain *= 1.025;
255        }
256
257        // FL bonus
258        if self.mods.fl() {
259            strain *= 1.05 * len_bonus;
260        }
261
262        // Scale with accuracy
263        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}