fruits, mania & taiko: handle 2nd object separately
This commit is contained in:
@@ -205,6 +205,32 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
|||||||
&mut last_excess,
|
&mut last_excess,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let next = hit_objects.next().unwrap();
|
||||||
|
curr.init_hyper_dash(
|
||||||
|
half_catcher_width,
|
||||||
|
&next,
|
||||||
|
&mut last_direction,
|
||||||
|
&mut last_excess,
|
||||||
|
);
|
||||||
|
|
||||||
|
let h = DifficultyObject::new(
|
||||||
|
&curr,
|
||||||
|
&prev,
|
||||||
|
movement.half_catcher_width,
|
||||||
|
attributes.clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
movement.process(&h);
|
||||||
|
|
||||||
|
prev = curr;
|
||||||
|
curr = next;
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
for next in hit_objects {
|
for next in hit_objects {
|
||||||
curr.init_hyper_dash(
|
curr.init_hyper_dash(
|
||||||
half_catcher_width,
|
half_catcher_width,
|
||||||
|
|||||||
@@ -47,17 +47,12 @@ impl Movement {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
// TODO: Remove branching
|
self.strain_peaks.push(self.current_section_peak);
|
||||||
if self.prev_time.is_some() {
|
|
||||||
self.strain_peaks.push(self.current_section_peak);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||||
if let Some(prev_time) = self.prev_time {
|
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
self.current_section_peak = self.peak_strain(time - prev_time);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
pub(crate) fn process(&mut self, current: &DifficultyObject) {
|
||||||
|
|||||||
+14
-129
@@ -1,5 +1,7 @@
|
|||||||
|
mod pp;
|
||||||
mod strain;
|
mod strain;
|
||||||
|
|
||||||
|
pub use pp::*;
|
||||||
use strain::Strain;
|
use strain::Strain;
|
||||||
|
|
||||||
use crate::{Beatmap, HitObject, Mods};
|
use crate::{Beatmap, HitObject, Mods};
|
||||||
@@ -9,7 +11,7 @@ const STAR_SCALING_FACTOR: f32 = 0.018;
|
|||||||
|
|
||||||
/// Star calculation for osu!mania maps
|
/// Star calculation for osu!mania maps
|
||||||
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
||||||
if map.hit_objects.is_empty() {
|
if map.hit_objects.len() < 2 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
|||||||
let section_len = SECTION_LEN * clock_rate;
|
let section_len = SECTION_LEN * clock_rate;
|
||||||
let mut strain = Strain::new(map.cs as u8);
|
let mut strain = Strain::new(map.cs as u8);
|
||||||
|
|
||||||
let hit_objects = map
|
let mut hit_objects = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.skip(1)
|
.skip(1)
|
||||||
@@ -28,6 +30,16 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
|||||||
let mut current_section_end =
|
let mut current_section_end =
|
||||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let h = hit_objects.next().unwrap();
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
strain.process(&h);
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
for h in hit_objects {
|
for h in hit_objects {
|
||||||
while h.base.start_time > current_section_end {
|
while h.base.start_time > current_section_end {
|
||||||
strain.save_current_peak();
|
strain.save_current_peak();
|
||||||
@@ -64,133 +76,6 @@ impl<'o> DifficultyHitObject<'o> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
score: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'m> PpCalculator<'m> {
|
|
||||||
#[inline]
|
|
||||||
pub fn new(map: &'m Beatmap) -> Self {
|
|
||||||
Self {
|
|
||||||
map,
|
|
||||||
stars: None,
|
|
||||||
mods: 0,
|
|
||||||
score: 1_000_000.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 score(mut self, score: u32) -> Self {
|
|
||||||
self.score = score as f32;
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn calculate(self) -> PpResult {
|
|
||||||
let stars = self.stars.unwrap_or_else(|| stars(self.map, self.mods));
|
|
||||||
|
|
||||||
let ez = self.mods.ez();
|
|
||||||
let nf = self.mods.nf();
|
|
||||||
let ht = self.mods.ht();
|
|
||||||
|
|
||||||
let score_multiplier = 0.5_f32.powi(ez as i32 + nf as i32 + ht as i32);
|
|
||||||
let scaled_score = self.score / score_multiplier;
|
|
||||||
|
|
||||||
let mut multiplier = 0.8;
|
|
||||||
|
|
||||||
if nf {
|
|
||||||
multiplier *= 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ez {
|
|
||||||
multiplier *= 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hit_window = {
|
|
||||||
let od = (10.0 - self.map.od).max(0.0).min(10.0);
|
|
||||||
let mut val = 34.0 + 3.0 * od;
|
|
||||||
|
|
||||||
if ez {
|
|
||||||
val *= 1.4;
|
|
||||||
} else if self.mods.hr() {
|
|
||||||
val /= 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
val * self.mods.speed()
|
|
||||||
};
|
|
||||||
|
|
||||||
let strain_value = self.compute_strain(scaled_score, stars);
|
|
||||||
let acc_value = self.compute_accuracy_value(scaled_score, strain_value, hit_window);
|
|
||||||
|
|
||||||
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
|
||||||
|
|
||||||
PpResult { pp, stars }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_strain(&self, score: f32, stars: f32) -> f32 {
|
|
||||||
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
|
|
||||||
|
|
||||||
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f32 / 1500.0).min(1.0);
|
|
||||||
|
|
||||||
if score <= 500_000.0 {
|
|
||||||
strain_value = 0.0;
|
|
||||||
} else if score <= 600_000.0 {
|
|
||||||
strain_value *= (score - 500_000.0) / 100_000.0 * 0.3;
|
|
||||||
} else if score <= 700_000.0 {
|
|
||||||
strain_value *= 0.3 + (score - 600_000.0) / 100_000.0 * 0.25;
|
|
||||||
} else if score <= 800_000.0 {
|
|
||||||
strain_value *= 0.55 + (score - 700_000.0) / 100_000.0 * 0.2;
|
|
||||||
} else if score <= 900_000.0 {
|
|
||||||
strain_value *= 0.75 + (score - 800_000.0) / 100_000.0 * 0.15;
|
|
||||||
} else {
|
|
||||||
strain_value *= 0.9 + (score - 900_000.0) / 100_000.0 * 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
strain_value
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn compute_accuracy_value(&self, score: f32, strain: f32, hit_window: f32) -> f32 {
|
|
||||||
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
|
|
||||||
* strain
|
|
||||||
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
use super::stars;
|
||||||
|
use crate::{Beatmap, Mods};
|
||||||
|
|
||||||
|
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,
|
||||||
|
score: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'m> PpCalculator<'m> {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(map: &'m Beatmap) -> Self {
|
||||||
|
Self {
|
||||||
|
map,
|
||||||
|
stars: None,
|
||||||
|
mods: 0,
|
||||||
|
score: 1_000_000.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 score(mut self, score: u32) -> Self {
|
||||||
|
self.score = score as f32;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate(self) -> PpResult {
|
||||||
|
let stars = self.stars.unwrap_or_else(|| stars(self.map, self.mods));
|
||||||
|
|
||||||
|
let ez = self.mods.ez();
|
||||||
|
let nf = self.mods.nf();
|
||||||
|
let ht = self.mods.ht();
|
||||||
|
|
||||||
|
let score_multiplier = 0.5_f32.powi(ez as i32 + nf as i32 + ht as i32);
|
||||||
|
let scaled_score = self.score / score_multiplier;
|
||||||
|
|
||||||
|
let mut multiplier = 0.8;
|
||||||
|
|
||||||
|
if nf {
|
||||||
|
multiplier *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ez {
|
||||||
|
multiplier *= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hit_window = {
|
||||||
|
let od = (10.0 - self.map.od).max(0.0).min(10.0);
|
||||||
|
let mut val = 34.0 + 3.0 * od;
|
||||||
|
|
||||||
|
if ez {
|
||||||
|
val *= 1.4;
|
||||||
|
} else if self.mods.hr() {
|
||||||
|
val /= 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
val * self.mods.speed()
|
||||||
|
};
|
||||||
|
|
||||||
|
let strain_value = self.compute_strain(scaled_score, stars);
|
||||||
|
let acc_value = self.compute_accuracy_value(scaled_score, strain_value, hit_window);
|
||||||
|
|
||||||
|
let pp = (strain_value.powf(1.1) + acc_value.powf(1.1)).powf(1.0 / 1.1) * multiplier;
|
||||||
|
|
||||||
|
PpResult { pp, stars }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_strain(&self, score: f32, stars: f32) -> f32 {
|
||||||
|
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
|
||||||
|
|
||||||
|
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f32 / 1500.0).min(1.0);
|
||||||
|
|
||||||
|
if score <= 500_000.0 {
|
||||||
|
strain_value = 0.0;
|
||||||
|
} else if score <= 600_000.0 {
|
||||||
|
strain_value *= (score - 500_000.0) / 100_000.0 * 0.3;
|
||||||
|
} else if score <= 700_000.0 {
|
||||||
|
strain_value *= 0.3 + (score - 600_000.0) / 100_000.0 * 0.25;
|
||||||
|
} else if score <= 800_000.0 {
|
||||||
|
strain_value *= 0.55 + (score - 700_000.0) / 100_000.0 * 0.2;
|
||||||
|
} else if score <= 900_000.0 {
|
||||||
|
strain_value *= 0.75 + (score - 800_000.0) / 100_000.0 * 0.15;
|
||||||
|
} else {
|
||||||
|
strain_value *= 0.9 + (score - 900_000.0) / 100_000.0 * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
strain_value
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn compute_accuracy_value(&self, score: f32, strain: f32, hit_window: f32) -> f32 {
|
||||||
|
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
|
||||||
|
* strain
|
||||||
|
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,10 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
|||||||
scaling_factor,
|
scaling_factor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
while h.base.time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
aim.process(&h);
|
aim.process(&h);
|
||||||
speed.process(&h);
|
speed.process(&h);
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
|||||||
scaling_factor,
|
scaling_factor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
aim.process(&h);
|
aim.process(&h);
|
||||||
speed.process(&h);
|
speed.process(&h);
|
||||||
|
|
||||||
|
|||||||
+13
-1
@@ -50,7 +50,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
|||||||
let mut current_section_end =
|
let mut current_section_end =
|
||||||
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
|
||||||
|
|
||||||
let hit_objects = map
|
let mut hit_objects = map
|
||||||
.hit_objects
|
.hit_objects
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -61,6 +61,18 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
|
|||||||
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
DifficultyObject::new(idx, base, prev, prev_prev, clock_rate)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle second object separately to remove later if-branching
|
||||||
|
let h = hit_objects.next().unwrap();
|
||||||
|
|
||||||
|
while h.base.start_time > current_section_end {
|
||||||
|
current_section_end += section_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
for skill in skills.iter_mut() {
|
||||||
|
skill.process(&h, &cheese);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all other objects
|
||||||
for h in hit_objects {
|
for h in hit_objects {
|
||||||
while h.base.start_time > current_section_end {
|
while h.base.start_time > current_section_end {
|
||||||
for skill in skills.iter_mut() {
|
for skill in skills.iter_mut() {
|
||||||
|
|||||||
+2
-6
@@ -30,16 +30,12 @@ impl Skill {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn save_current_peak(&mut self) {
|
pub(crate) fn save_current_peak(&mut self) {
|
||||||
if self.prev_time.is_some() {
|
self.strain_peaks.push(self.current_section_peak);
|
||||||
self.strain_peaks.push(self.current_section_peak);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
||||||
if let Some(prev) = self.prev_time {
|
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
self.current_section_peak = self.peak_strain(time - prev);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
Reference in New Issue
Block a user