osu: added pp computation
This commit is contained in:
@@ -9,7 +9,7 @@ A standalone crate to calculate star ratings and performance points for all [osu
|
|||||||
- [x] taiko sr
|
- [x] taiko sr
|
||||||
- [x] ctb sr
|
- [x] ctb sr
|
||||||
- [x] mania sr
|
- [x] mania sr
|
||||||
- [ ] osu pp
|
- [x] osu pp (need sr & testing!)
|
||||||
- [ ] taiko pp
|
- [x] taiko pp (need testing!)
|
||||||
- [x] ctb pp (need testing!)
|
- [x] ctb pp (need testing!)
|
||||||
- [x] mania pp (need testing!)
|
- [x] mania pp (need testing!)
|
||||||
|
|||||||
+9
-5
@@ -2,11 +2,13 @@ mod curve;
|
|||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
mod math_util;
|
mod math_util;
|
||||||
mod osu_object;
|
mod osu_object;
|
||||||
|
mod pp;
|
||||||
mod skill;
|
mod skill;
|
||||||
mod skill_kind;
|
mod skill_kind;
|
||||||
|
|
||||||
use difficulty_object::DifficultyObject;
|
use difficulty_object::DifficultyObject;
|
||||||
use osu_object::OsuObject;
|
use osu_object::OsuObject;
|
||||||
|
pub use pp::*;
|
||||||
use skill::Skill;
|
use skill::Skill;
|
||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
|
|
||||||
@@ -16,9 +18,9 @@ const SECTION_LEN: f32 = 400.0;
|
|||||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||||
|
|
||||||
/// Star calculation for osu!standard maps
|
/// Star calculation for osu!standard maps
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||||
if map.hit_objects.len() < 2 {
|
if map.hit_objects.len() < 2 {
|
||||||
return 0.0;
|
return todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes = map.attributes().mods(mods);
|
let attributes = map.attributes().mods(mods);
|
||||||
@@ -77,7 +79,9 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
|||||||
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
|
||||||
|
|
||||||
aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0
|
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
|
||||||
|
|
||||||
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -101,7 +105,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map: {}", why),
|
Err(why) => panic!("Error while parsing map: {}", why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stars = stars(&map, 0);
|
let stars = stars(&map, 0).stars;
|
||||||
|
|
||||||
println!("Stars: {}", stars);
|
println!("Stars: {}", stars);
|
||||||
}
|
}
|
||||||
@@ -151,7 +155,7 @@ mod tests {
|
|||||||
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
|
||||||
};
|
};
|
||||||
|
|
||||||
let stars = stars(&map, mods);
|
let stars = stars(&map, mods).stars;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(stars - expected_stars).abs() < margin,
|
(stars - expected_stars).abs() < margin,
|
||||||
|
|||||||
+354
@@ -0,0 +1,354 @@
|
|||||||
|
use super::stars;
|
||||||
|
|
||||||
|
use parse::{Beatmap, Mods};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DifficultyAttributes {
|
||||||
|
pub stars: f32,
|
||||||
|
pub ar: f32,
|
||||||
|
pub od: f32,
|
||||||
|
pub speed_strain: f32,
|
||||||
|
pub aim_strain: f32,
|
||||||
|
pub max_combo: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PpResult {
|
||||||
|
pub pp: f32,
|
||||||
|
pub attributes: DifficultyAttributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PpProvider {
|
||||||
|
fn pp(&self) -> PpCalculator;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PpProvider for Beatmap {
|
||||||
|
#[inline]
|
||||||
|
fn pp(&self) -> PpCalculator {
|
||||||
|
PpCalculator::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Allow partial plays
|
||||||
|
pub struct PpCalculator<'m> {
|
||||||
|
map: &'m Beatmap,
|
||||||
|
attributes: Option<DifficultyAttributes>,
|
||||||
|
mods: u32,
|
||||||
|
combo: Option<usize>,
|
||||||
|
acc: Option<f32>,
|
||||||
|
|
||||||
|
n300: Option<usize>,
|
||||||
|
n100: Option<usize>,
|
||||||
|
n50: Option<usize>,
|
||||||
|
n_misses: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'m> PpCalculator<'m> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(map: &'m Beatmap) -> Self {
|
||||||
|
Self {
|
||||||
|
map,
|
||||||
|
attributes: None,
|
||||||
|
mods: 0,
|
||||||
|
combo: None,
|
||||||
|
acc: None,
|
||||||
|
|
||||||
|
n300: None,
|
||||||
|
n100: None,
|
||||||
|
n50: None,
|
||||||
|
n_misses: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn attributes(mut self, attributes: DifficultyAttributes) -> Self {
|
||||||
|
self.attributes.replace(attributes);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mods(mut self, mods: u32) -> Self {
|
||||||
|
self.mods = mods;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn combo(mut self, combo: usize) -> Self {
|
||||||
|
self.combo.replace(combo);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn n300(mut self, n300: usize) -> Self {
|
||||||
|
self.n300.replace(n300);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn n100(mut self, n100: usize) -> Self {
|
||||||
|
self.n100.replace(n100);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn n50(mut self, n50: usize) -> Self {
|
||||||
|
self.n50.replace(n50);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn misses(mut self, n_misses: usize) -> Self {
|
||||||
|
self.n_misses = n_misses;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
||||||
|
///
|
||||||
|
/// Be sure to set `misses` beforehand!
|
||||||
|
pub fn accuracy(mut self, acc: f32) -> Self {
|
||||||
|
let n_objects = self.map.hit_objects.len();
|
||||||
|
|
||||||
|
if self.n100.or(self.n50).is_none() {
|
||||||
|
self.n300.replace(
|
||||||
|
n_objects - self.n100.unwrap_or(0) - self.n50.unwrap_or(0) - self.n_misses,
|
||||||
|
);
|
||||||
|
self.n100.get_or_insert(0);
|
||||||
|
self.n50.get_or_insert(0);
|
||||||
|
} else {
|
||||||
|
let target_total = (acc * n_objects as f32 * 6.0).round() as usize;
|
||||||
|
let delta = target_total - (n_objects - self.n_misses);
|
||||||
|
|
||||||
|
self.n300.replace(delta / 5);
|
||||||
|
self.n100.replace(delta % 5);
|
||||||
|
self.n50
|
||||||
|
.replace(n_objects - self.n300.unwrap() - self.n100.unwrap() - self.n_misses);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.acc.replace(acc / 100.0);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(mut self) -> PpResult {
|
||||||
|
if self.attributes.is_none() {
|
||||||
|
let attribtes = stars(self.map, self.mods);
|
||||||
|
self.attributes.replace(attribtes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.acc.is_none() {
|
||||||
|
let n_objects = self.map.hit_objects.len();
|
||||||
|
|
||||||
|
let remaining = n_objects
|
||||||
|
.saturating_sub(self.n300.unwrap_or(0))
|
||||||
|
.saturating_sub(self.n100.unwrap_or(0))
|
||||||
|
.saturating_sub(self.n50.unwrap_or(0))
|
||||||
|
.saturating_sub(self.n_misses);
|
||||||
|
|
||||||
|
if remaining > 0 {
|
||||||
|
if self.n300.is_none() {
|
||||||
|
self.n300.replace(remaining);
|
||||||
|
self.n100.get_or_insert(0);
|
||||||
|
self.n50.get_or_insert(0);
|
||||||
|
} else if self.n100.is_none() {
|
||||||
|
self.n100.replace(remaining);
|
||||||
|
self.n50.get_or_insert(0);
|
||||||
|
} else if self.n50.is_none() {
|
||||||
|
self.n50.replace(remaining);
|
||||||
|
} else {
|
||||||
|
*self.n300.as_mut().unwrap() += remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let numerator =
|
||||||
|
self.n50.unwrap() * 50 + self.n100.unwrap() * 100 + self.n300.unwrap() * 300;
|
||||||
|
self.acc.replace(numerator as f32 / n_objects as f32);
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_hits = self.total_hits();
|
||||||
|
let mut multiplier = 1.12;
|
||||||
|
|
||||||
|
if self.mods.nf() {
|
||||||
|
multiplier *= (1.0 - 0.02 * self.n_misses as f32).max(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.mods.so() {
|
||||||
|
let spinner_count = self
|
||||||
|
.map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.filter(|h| h.is_spinner())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
multiplier *= 1.0 - (spinner_count as f32 / total_hits as f32).powf(0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
let aim_value = self.compute_aim_value(total_hits as f32);
|
||||||
|
let speed_value = self.compute_speed_value(total_hits as f32);
|
||||||
|
let acc_value = self.compute_accuracy_value(total_hits);
|
||||||
|
|
||||||
|
let pp = (aim_value.powf(1.1) + speed_value.powf(1.1) + acc_value.powf(1.1))
|
||||||
|
.powf(1.0 / 1.1)
|
||||||
|
* multiplier;
|
||||||
|
|
||||||
|
PpResult {
|
||||||
|
pp,
|
||||||
|
attributes: self.attributes.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_aim_value(&self, total_hits: f32) -> f32 {
|
||||||
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
|
|
||||||
|
// TD penalty
|
||||||
|
let raw_aim = if self.mods.td() {
|
||||||
|
attributes.aim_strain.powf(0.8)
|
||||||
|
} else {
|
||||||
|
attributes.aim_strain
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut aim_value = (5.0 * (raw_aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||||
|
|
||||||
|
// Longer maps are worth more
|
||||||
|
let len_bonus = 0.95
|
||||||
|
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||||
|
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
||||||
|
aim_value *= len_bonus;
|
||||||
|
|
||||||
|
// Penalize misses
|
||||||
|
if self.n_misses > 0 {
|
||||||
|
aim_value *= 0.97
|
||||||
|
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
|
||||||
|
.powi(self.n_misses as i32);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo scaling
|
||||||
|
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||||
|
aim_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AR bonus
|
||||||
|
let mut ar_factor = 0.0;
|
||||||
|
if attributes.ar > 10.33 {
|
||||||
|
ar_factor += 0.4 * (attributes.ar - 10.33);
|
||||||
|
} else if attributes.ar < 8.0 {
|
||||||
|
ar_factor += 0.1 * (8.0 - attributes.ar);
|
||||||
|
}
|
||||||
|
aim_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
|
||||||
|
|
||||||
|
// HD bonus
|
||||||
|
if self.mods.hd() {
|
||||||
|
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FL bonus
|
||||||
|
if self.mods.fl() {
|
||||||
|
aim_value *= 1.0
|
||||||
|
+ 0.35 * (total_hits / 200.0).min(1.0)
|
||||||
|
+ (total_hits > 200.0) as u8 as f32 * 0.3 * ((total_hits - 200.0) / 300.0).min(1.0)
|
||||||
|
+ (total_hits > 500.0) as u8 as f32 * (total_hits - 500.0) / 1200.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale with accuracy
|
||||||
|
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
|
||||||
|
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||||
|
|
||||||
|
aim_value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_speed_value(&self, total_hits: f32) -> f32 {
|
||||||
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
|
|
||||||
|
let mut speed_value =
|
||||||
|
(5.0 * (attributes.speed_strain / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
|
||||||
|
|
||||||
|
// Longer maps are worth more
|
||||||
|
let len_bonus = 0.95
|
||||||
|
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||||
|
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
||||||
|
speed_value *= len_bonus;
|
||||||
|
|
||||||
|
// Penalize misses
|
||||||
|
if self.n_misses > 0 {
|
||||||
|
speed_value *= 0.97
|
||||||
|
* (1.0 - (self.n_misses as f32 / total_hits).powf(0.775))
|
||||||
|
.powf((self.n_misses as f32).powf(0.875));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combo scaling
|
||||||
|
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||||
|
speed_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// AR bonus
|
||||||
|
if attributes.ar > 10.33 {
|
||||||
|
let ar_factor = 0.4 * (attributes.ar - 10.33);
|
||||||
|
speed_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HD bonus
|
||||||
|
if self.mods.hd() {
|
||||||
|
speed_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scaling the speed value with accuracy and OD
|
||||||
|
speed_value *= (0.95 + attributes.od * attributes.od / 750.0)
|
||||||
|
* self
|
||||||
|
.acc
|
||||||
|
.unwrap()
|
||||||
|
.powf((14.5 - attributes.od.max(8.0)) / 8.0);
|
||||||
|
|
||||||
|
// Penalize n50s
|
||||||
|
speed_value *= 0.98_f32.powf(
|
||||||
|
(self.n50.unwrap_or(0) as f32 >= total_hits / 500.0) as u8 as f32
|
||||||
|
* (self.n50.unwrap_or(0) as f32 - total_hits / 500.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
speed_value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_accuracy_value(&self, total_hits: usize) -> f32 {
|
||||||
|
let n_circles = self
|
||||||
|
.map
|
||||||
|
.hit_objects
|
||||||
|
.iter()
|
||||||
|
.filter(|h| h.is_circle())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let better_acc_percentage = (n_circles > 0) as u8 as f32
|
||||||
|
* (((self.n300.unwrap() - (total_hits - n_circles)) * 6
|
||||||
|
+ self.n100.unwrap_or(0) * 2
|
||||||
|
+ self.n50.unwrap_or(0)) as f32
|
||||||
|
/ (n_circles * 6) as f32)
|
||||||
|
.max(0.0);
|
||||||
|
|
||||||
|
let attributes = self.attributes.as_ref().unwrap();
|
||||||
|
|
||||||
|
let mut acc_value = 1.52163_f32.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
||||||
|
|
||||||
|
// Bonus for many hitcircles
|
||||||
|
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
|
||||||
|
|
||||||
|
// HD bonus
|
||||||
|
if self.mods.hd() {
|
||||||
|
acc_value *= 1.08;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FL bonus
|
||||||
|
if self.mods.fl() {
|
||||||
|
acc_value *= 1.02;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc_value
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn total_hits(&self) -> usize {
|
||||||
|
self.n300.unwrap_or(0) + self.n100.unwrap_or(0) + self.n50.unwrap_or(0) + self.n_misses
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,6 +151,158 @@ fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
|
|||||||
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
|
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PpResult {
|
||||||
|
pub pp: f32,
|
||||||
|
pub stars: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PpProvider {
|
||||||
|
fn pp(&self) -> PpCalculator;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PpProvider for Beatmap {
|
||||||
|
#[inline]
|
||||||
|
fn pp(&self) -> PpCalculator {
|
||||||
|
PpCalculator::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Allow partial plays
|
||||||
|
pub struct PpCalculator<'m> {
|
||||||
|
map: &'m Beatmap,
|
||||||
|
stars: Option<f32>,
|
||||||
|
mods: u32,
|
||||||
|
max_combo: usize,
|
||||||
|
combo: Option<usize>,
|
||||||
|
acc: f32,
|
||||||
|
n_misses: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'m> PpCalculator<'m> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(map: &'m Beatmap) -> Self {
|
||||||
|
let max_combo = map.hit_objects.iter().filter(|h| h.is_circle()).count();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
map,
|
||||||
|
stars: None,
|
||||||
|
mods: 0,
|
||||||
|
max_combo,
|
||||||
|
combo: None,
|
||||||
|
acc: 100.0,
|
||||||
|
n_misses: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn stars(mut self, stars: f32) -> Self {
|
||||||
|
self.stars.replace(stars);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn mods(mut self, mods: u32) -> Self {
|
||||||
|
self.mods = mods;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn combo(mut self, combo: usize) -> Self {
|
||||||
|
self.combo.replace(combo);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn misses(mut self, n_misses: usize) -> Self {
|
||||||
|
self.n_misses = n_misses;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the accuracy between 0.0 and 100.0;
|
||||||
|
#[inline]
|
||||||
|
pub fn accuracy(mut self, acc: f32) -> Self {
|
||||||
|
self.acc = acc / 100.0;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(self) -> PpResult {
|
||||||
|
let stars = self.stars.unwrap_or_else(|| stars(self.map, self.mods));
|
||||||
|
|
||||||
|
let mut multiplier = 1.1;
|
||||||
|
|
||||||
|
if self.mods.nf() {
|
||||||
|
multiplier *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.mods.hd() {
|
||||||
|
multiplier *= 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Consider HR & co?
|
||||||
|
let hit_window = difficulty_range(self.map.od) as i32 as f32 / self.mods.speed();
|
||||||
|
|
||||||
|
let strain_value = self.compute_strain_value(stars);
|
||||||
|
let acc_value = self.compute_accuracy_value(hit_window);
|
||||||
|
|
||||||
|
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
||||||
|
|
||||||
|
PpResult { stars, pp }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_strain_value(&self, stars: f32) -> f32 {
|
||||||
|
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
|
||||||
|
let mut strain = exp_base * exp_base / 100_000.0;
|
||||||
|
|
||||||
|
// Longer maps are worth more
|
||||||
|
let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
|
||||||
|
strain *= len_bonus;
|
||||||
|
|
||||||
|
// Penalize misses exponentially
|
||||||
|
strain *= 0.985_f32.powi(self.n_misses as i32);
|
||||||
|
|
||||||
|
// HD bonus
|
||||||
|
if self.mods.hd() {
|
||||||
|
strain *= 1.025;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FL bonus
|
||||||
|
if self.mods.fl() {
|
||||||
|
strain *= 1.05 * len_bonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale with accuracy
|
||||||
|
strain * self.acc
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn compute_accuracy_value(&self, hit_window: f32) -> f32 {
|
||||||
|
(150.0 / hit_window).powf(1.1)
|
||||||
|
* self.acc.powi(15)
|
||||||
|
* 22.0
|
||||||
|
* (self.max_combo as f32 / 1500.0).powf(0.3).min(1.15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HITWINDOW_MIN: f32 = 50.0;
|
||||||
|
const HITWINDOW_AVG: f32 = 35.0;
|
||||||
|
const HITWINDOW_MAX: f32 = 20.0;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn difficulty_range(od: f32) -> f32 {
|
||||||
|
if od > 5.0 {
|
||||||
|
HITWINDOW_AVG + (HITWINDOW_MAX - HITWINDOW_AVG) * (od - 5.0) / 5.0
|
||||||
|
} else if od < 5.0 {
|
||||||
|
HITWINDOW_AVG - (HITWINDOW_AVG - HITWINDOW_MIN) * (5.0 - od) / 5.0
|
||||||
|
} else {
|
||||||
|
HITWINDOW_AVG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user