osu: split into versions, finished oppai version

This commit is contained in:
MaxOhn
2021-01-15 01:12:10 +01:00
parent 2d975ff21c
commit 43cf973197
20 changed files with 2075 additions and 220 deletions
+10
View File
@@ -0,0 +1,10 @@
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 n_circles: usize,
pub n_spinners: usize,
}
+26 -158
View File
@@ -1,171 +1,39 @@
mod versions;
pub use versions::*;
mod curve;
mod difficulty_object;
mod difficulty_attributes;
mod math_util;
mod osu_object;
mod pp;
mod skill;
mod skill_kind;
use difficulty_object::DifficultyObject;
use osu_object::OsuObject;
pub use difficulty_attributes::DifficultyAttributes;
pub use pp::*;
use skill::Skill;
use skill_kind::SkillKind;
use parse::{Beatmap, Mods};
const HITWINDOW_OD_MIN: f32 = 80.0;
const HITWINDOW_OD_AVG: f32 = 50.0;
const HITWINDOW_OD_MAX: f32 = 20.0;
const SECTION_LEN: f32 = 400.0;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
const HITWINDOW_AR_MIN: f32 = 1800.0;
const HITWINDOW_AR_AVG: f32 = 1200.0;
const HITWINDOW_AR_MAX: f32 = 450.0;
/// Star calculation for osu!standard maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
if map.hit_objects.len() < 2 {
return todo!();
}
let attributes = map.attributes().mods(mods);
let section_len = SECTION_LEN * attributes.clock_rate;
let mut hit_objects = map
.hit_objects
.iter()
.map(|h| OsuObject::new(h, map, &attributes));
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_diff = None;
for curr in hit_objects {
let h = DifficultyObject::new(
curr.clone(),
prev.clone(),
prev_diff,
prev_prev,
attributes.clock_rate,
);
// println!(
// "strain_time={} | travel_dist={} | jump_dist={} | angle={:?}",
// h.strain_time, h.travel_dist, h.jump_dist, h.angle
// );
while h.base.time() > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
prev_diff = Some(h);
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
todo!()
#[inline]
pub(crate) fn difficulty_range_od(od: f32) -> f32 {
difficulty_range(od, HITWINDOW_OD_MAX, HITWINDOW_OD_AVG, HITWINDOW_OD_MIN)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[inline]
pub(crate) fn difficulty_range_ar(ar: f32) -> f32 {
difficulty_range(ar, HITWINDOW_AR_MAX, HITWINDOW_AR_AVG, HITWINDOW_AR_MIN)
}
#[test]
fn test_single() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/1851299.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let stars = stars(&map, 0).stars;
println!("Stars: {}", stars);
}
#[test]
#[ignore]
fn test_osu() {
let margin = 0.005;
#[rustfmt::skip]
// TODO: More mods
let data = vec![
(1851299, 1 << 8, 4.23514130038547), // HT
(1851299, 0, 5.356786475158158), // NM
(1851299, 1 << 6, 7.450616908751305), // DT
(1851299, 1 << 4, 5.6834681957637665),// HR
(1851299, 1 << 1, 4.937817303399699), // EZ
(70090, 1 << 8, 2.2929922580201803), // HT
(70090, 0, 2.8322940761833983), // NM
(70090, 1 << 6, 3.8338563325375485), // DT
(70090, 1 << 4, 3.0617492228478174), // HR
(70090, 1 << 1, 2.698823231324141), // EZ
(1241370, 1 << 8, 5.662809600985943), // HT
(1241370, 0, 7.0367002127481975), // NM
(1241370, 1 << 6, 11.144720506574934),// DT
(1241370, 1 << 4, 7.641688110458715), // HR
(1241370, 1 << 1, 6.316288616688052), // EZ
// Slider fiesta
// (1657535, 1 << 8, 4.1727975286379895),// HT
// (1657535, 0, 5.16048239944917), // NM
// (1657535, 1 << 6, 7.125936779100417), // DT
// (1657535, 1 << 4, 5.545877027713307), // HR
// (1657535, 1 << 1, 4.66015083361088), // EZ
];
for (map_id, mods, expected_stars) in data {
let file = match File::open(format!("./test/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let stars = stars(&map, mods).stars;
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
#[inline]
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
if val > 5.0 {
avg + (max - avg) * (val - 5.0) / 5.0
} else if val < 5.0 {
avg - (avg - min) * (5.0 - val) / 5.0
} else {
avg
}
}
+128 -39
View File
@@ -1,20 +1,10 @@
use super::stars;
use super::DifficultyAttributes as Attributes;
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 attributes: Attributes,
}
pub trait PpProvider {
@@ -31,7 +21,7 @@ impl PpProvider for Beatmap {
// TODO: Allow partial plays
pub struct PpCalculator<'m> {
map: &'m Beatmap,
attributes: Option<DifficultyAttributes>,
attributes: Option<Attributes>,
mods: u32,
combo: Option<usize>,
acc: Option<f32>,
@@ -40,6 +30,8 @@ pub struct PpCalculator<'m> {
n100: Option<usize>,
n50: Option<usize>,
n_misses: usize,
stars_func: Option<Box<dyn Fn(&Beatmap, u32) -> Attributes>>,
}
impl<'m> PpCalculator<'m> {
@@ -56,11 +48,13 @@ impl<'m> PpCalculator<'m> {
n100: None,
n50: None,
n_misses: 0,
stars_func: None,
}
}
#[inline]
pub fn attributes(mut self, attributes: DifficultyAttributes) -> Self {
pub fn attributes(mut self, attributes: Attributes) -> Self {
self.attributes.replace(attributes);
self
@@ -108,13 +102,21 @@ impl<'m> PpCalculator<'m> {
self
}
#[inline]
pub fn stars_function(mut self, func: impl Fn(&Beatmap, u32) -> Attributes + 'static) -> Self {
self.stars_func.replace(Box::new(func));
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();
let acc = acc / 100.0;
if self.n100.or(self.n50).is_none() {
if self.n100.or(self.n50).is_some() {
self.n300.replace(
n_objects - self.n100.unwrap_or(0) - self.n50.unwrap_or(0) - self.n_misses,
);
@@ -126,18 +128,43 @@ impl<'m> PpCalculator<'m> {
self.n300.replace(delta / 5);
self.n100.replace(delta % 5);
// println!(
// "{} - {} - {} - {}",
// n_objects,
// self.n300.unwrap(),
// self.n100.unwrap(),
// self.n_misses
// );
self.n50
.replace(n_objects - self.n300.unwrap() - self.n100.unwrap() - self.n_misses);
}
self.acc.replace(acc / 100.0);
let acc = (6 * self.n300.unwrap() + 2 * self.n100.unwrap() + self.n50.unwrap()) as f32
/ (6 * n_objects) as f32;
self.acc.replace(acc);
// println!(
// "n300: {:?} | n100: {:?} | n50: {:?} | nMiss: {:?} => {}",
// self.n300, self.n100, self.n50, self.n_misses, acc
// );
self
}
pub fn calculate(mut self) -> PpResult {
if self.attributes.is_none() {
let attribtes = stars(self.map, self.mods);
let stars_func = self
.stars_func
.take()
.unwrap_or_else(|| Box::new(super::no_sliders_no_leniency::stars));
let attribtes = stars_func(self.map, self.mods);
// println!("> stars={}", attribtes.stars);
self.attributes.replace(attribtes);
}
@@ -165,9 +192,13 @@ impl<'m> PpCalculator<'m> {
}
}
let numerator =
self.n50.unwrap() * 50 + self.n100.unwrap() * 100 + self.n300.unwrap() * 300;
self.acc.replace(numerator as f32 / n_objects as f32);
// println!(
// "n300: {:?} | n100: {:?} | n50: {:?} | nMiss: {:?}",
// self.n300, self.n100, self.n50, self.n_misses
// );
let numerator = self.n50.unwrap() + self.n100.unwrap() * 2 + self.n300.unwrap() * 6;
self.acc.replace(numerator as f32 / n_objects as f32 / 6.0);
}
let total_hits = self.total_hits();
@@ -178,20 +209,19 @@ impl<'m> PpCalculator<'m> {
}
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 n_spinners = self.attributes.as_ref().unwrap().n_spinners;
multiplier *= 1.0 - (n_spinners 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);
// println!(
// "aim={} | speed={} | acc={}",
// aim_value, speed_value, acc_value
// );
let pp = (aim_value.powf(1.1) + speed_value.powf(1.1) + acc_value.powf(1.1))
.powf(1.0 / 1.1)
* multiplier;
@@ -205,6 +235,8 @@ impl<'m> PpCalculator<'m> {
fn compute_aim_value(&self, total_hits: f32) -> f32 {
let attributes = self.attributes.as_ref().unwrap();
// println!("aim_strain={}", attributes.aim_strain);
// TD penalty
let raw_aim = if self.mods.td() {
attributes.aim_strain.powf(0.8)
@@ -212,14 +244,20 @@ impl<'m> PpCalculator<'m> {
attributes.aim_strain
};
// println!("raw={}", raw_aim);
let mut aim_value = (5.0 * (raw_aim / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
// println!("init: {}", aim_value);
// 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;
// println!("len bonus: {} => {}", len_bonus, aim_value);
// Penalize misses
if self.n_misses > 0 {
aim_value *= 0.97
@@ -227,11 +265,20 @@ impl<'m> PpCalculator<'m> {
.powi(self.n_misses as i32);
}
// println!("miss penalty: {}", aim_value);
// println!(
// "combo={:?} | max_combo={}",
// self.combo, attributes.max_combo
// );
// 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);
}
// println!("combo scaling: {}", aim_value);
// AR bonus
let mut ar_factor = 0.0;
if attributes.ar > 10.33 {
@@ -241,11 +288,15 @@ impl<'m> PpCalculator<'m> {
}
aim_value *= 1.0 + ar_factor.min(ar_factor * total_hits / 1000.0);
// println!("ar bonus: {} => {}", ar_factor, aim_value);
// HD bonus
if self.mods.hd() {
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
}
// println!("hd bonus: {}", aim_value);
// FL bonus
if self.mods.fl() {
aim_value *= 1.0
@@ -254,25 +305,43 @@ impl<'m> PpCalculator<'m> {
+ (total_hits > 500.0) as u8 as f32 * (total_hits - 500.0) / 1200.0;
}
// println!("fl bonus: {}", aim_value);
// Scale with accuracy
aim_value *= 0.5 + self.acc.unwrap() / 2.0;
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
// println!("> acc: {:?}", self.acc);
// println!("final: {}", aim_value);
aim_value
}
fn compute_speed_value(&self, total_hits: f32) -> f32 {
let attributes = self.attributes.as_ref().unwrap();
// println!("speed_strain={}", attributes.speed_strain);
let mut speed_value =
(5.0 * (attributes.speed_strain / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0;
// println!(
// "curr={} | modified={}",
// speed_value,
// (5.0 * (2.0994549379474163_f32 / 0.0675).max(1.0) - 4.0).powi(3) / 100_000.0
// );
// println!("init: {}", speed_value);
// 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;
// println!("len bonus: {} => {}", len_bonus, speed_value);
// Penalize misses
if self.n_misses > 0 {
speed_value *= 0.97
@@ -280,28 +349,39 @@ impl<'m> PpCalculator<'m> {
.powf((self.n_misses as f32).powf(0.875));
}
// println!("miss penalty: {}", speed_value);
// 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);
}
// println!("combo scaling: {}", speed_value);
// 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);
}
// println!("ar bonus: {}", speed_value);
// HD bonus
if self.mods.hd() {
speed_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
}
// println!("hidden bonus: {}", speed_value);
// 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);
let od_factor = 0.95 + attributes.od * attributes.od / 750.0;
let acc_factor = self
.acc
.unwrap()
.powf((14.5 - attributes.od.max(8.0)) / 2.0);
speed_value *= od_factor * acc_factor;
// println!("acc & od scaling: {}", speed_value);
// Penalize n50s
speed_value *= 0.98_f32.powf(
@@ -309,16 +389,16 @@ impl<'m> PpCalculator<'m> {
* (self.n50.unwrap_or(0) as f32 - total_hits / 500.0),
);
// println!("final: {}", speed_value);
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 attributes = self.attributes.as_ref().unwrap();
let n_circles = attributes.n_circles;
// println!("n_circles={}", n_circles);
let better_acc_percentage = (n_circles > 0) as u8 as f32
* (((self.n300.unwrap() - (total_hits - n_circles)) * 6
@@ -327,10 +407,19 @@ impl<'m> PpCalculator<'m> {
/ (n_circles * 6) as f32)
.max(0.0);
// println!("better_acc_percentage={}", better_acc_percentage);
let attributes = self.attributes.as_ref().unwrap();
let mut acc_value = 1.52163_f32.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
// println!(
// "1.52163^{} * {}^24 * 2.83 = {}",
// attributes.od, better_acc_percentage, acc_value
// );
// println!("init: {}", acc_value);
// Bonus for many hitcircles
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
@@ -33,9 +33,11 @@ impl DifficultyObject {
scaling_factor *= 1.0 + small_circle_bonus;
}
let travel_dist = base.travel_dist();
let travel_dist = prev.travel_dist();
let prev_cursor_pos = prev.cursor_end_position();
// println!("travel_dist={} | prev_cursor_pos={:?}", travel_dist, prev_cursor_pos);
let jump_dist = match base {
OsuObject::Spinner { .. } => 0.0,
_ => (base.stacked_pos() * scaling_factor - prev_cursor_pos * scaling_factor).length(),
+234
View File
@@ -0,0 +1,234 @@
mod difficulty_object;
mod osu_object;
mod skill;
mod skill_kind;
use difficulty_object::DifficultyObject;
use osu_object::OsuObject;
use skill::Skill;
use skill_kind::SkillKind;
use crate::DifficultyAttributes;
use parse::{Beatmap, Mods};
const SECTION_LEN: f32 = 400.0;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
/// Star calculation for osu!standard maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
let attributes = map.attributes().mods(mods);
if map.hit_objects.len() < 2 {
return DifficultyAttributes {
stars: 0.0,
ar: attributes.ar,
od: attributes.od,
speed_strain: 0.0,
aim_strain: 0.0,
max_combo: 0,
n_circles: 0,
n_spinners: 0,
};
}
let section_len = SECTION_LEN * attributes.clock_rate;
let mut hit_objects = map
.hit_objects
.iter()
.map(|h| OsuObject::new(h, map, &attributes));
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_diff = None;
let mut _i = 0;
for curr in hit_objects {
let h = DifficultyObject::new(
curr.clone(),
prev.clone(),
prev_diff,
prev_prev,
attributes.clock_rate,
);
// println!(
// "strain_time={} | travel_dist={} | jump_dist={} | angle={:?}",
// h.strain_time, h.travel_dist, h.jump_dist, h.angle
// );
// println!("[{}] time={}", _i, curr.time());
while h.base.time() > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
_i += 1;
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
prev_diff = Some(h);
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
// println!("Aim:");
// for (i, strain) in skills[0].strain_peaks.iter().enumerate() {
// println!("{}: {}", i, strain);
// }
// println!("Speed:");
// for (i, strain) in skills[1].strain_peaks.iter().enumerate() {
// println!("{}: {}", i, strain);
// }
// println!("Aim: {:?}", skills[0].strain_peaks);
// println!("Speed: {:?}", skills[1].strain_peaks);
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
// println!("After:\n{:?}", skills[0].strain_peaks);
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
// println!("After:\n{:?}", skills[1].strain_peaks);
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
DifficultyAttributes {
stars,
ar: attributes.ar,
od: attributes.od,
speed_strain: speed_rating,
aim_strain: aim_rating,
max_combo: 0, // TODO
n_circles: 0, // TODO
n_spinners: 0, // TODO
}
}
#[cfg(test)]
mod tests {
use super::stars;
use crate::PpCalculator;
use parse::Beatmap;
use std::fs::File;
#[test]
fn all_included_single_stars() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let stars = stars(&map, 0).stars;
println!("Stars: {}", stars);
}
#[test]
#[ignore]
fn all_included_stars() {
let margin = 0.005;
#[rustfmt::skip]
// TODO: More mods
let data = vec![
(1851299, 1 << 8, 4.23514130038547), // HT
(1851299, 0, 5.356786475158158), // NM
(1851299, 1 << 6, 7.450616908751305), // DT
(1851299, 1 << 4, 5.6834681957637665),// HR
(1851299, 1 << 1, 4.937817303399699), // EZ
(70090, 1 << 8, 2.2929922580201803), // HT
(70090, 0, 2.8322940761833983), // NM
(70090, 1 << 6, 3.8338563325375485), // DT
(70090, 1 << 4, 3.0617492228478174), // HR
(70090, 1 << 1, 2.698823231324141), // EZ
(1241370, 1 << 8, 5.662809600985943), // HT
(1241370, 0, 7.0367002127481975), // NM
(1241370, 1 << 6, 11.144720506574934),// DT
(1241370, 1 << 4, 7.641688110458715), // HR
(1241370, 1 << 1, 6.316288616688052), // EZ
// Slider fiesta
// (1657535, 1 << 8, 4.1727975286379895),// HT
// (1657535, 0, 5.16048239944917), // NM
// (1657535, 1 << 6, 7.125936779100417), // DT
// (1657535, 1 << 4, 5.545877027713307), // HR
// (1657535, 1 << 1, 4.66015083361088), // EZ
];
for (map_id, mods, expected_stars) in data {
let file = match File::open(format!("./test/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let stars = stars(&map, mods).stars;
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
}
#[test]
fn all_included_single_pp() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let calculator = PpCalculator::new(&map).mods(0).stars_function(stars);
let result = calculator.calculate();
println!("PP: {}", result.pp);
}
}
@@ -1,6 +1,6 @@
#![allow(unused)]
use super::curve::Curve;
use crate::curve::Curve;
use parse::{Beatmap, BeatmapAttributes, HitObject, HitObjectKind, PathType, Pos2};
use std::cmp::Ordering;
@@ -170,12 +170,45 @@ impl OsuObject {
}
// Slider tail
let dist_end = (repeats % 2) as f32 * pixel_len;
// let dist_end = velocity * (duration - LEGACY_LAST_TICK_OFFSET);
let pos = curve.point_at_distance(dist_end);
slider_objects.push(SliderTick::new(pos, h.start_time + duration));
let span_duration = duration / *repeats as f32;
let final_span_idx = repeats.saturating_sub(1);
let final_span_start_time = h.start_time + final_span_idx as f32 * span_duration;
let final_span_end_time = (h.start_time + duration / 2.0)
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
let mut final_progress =
(final_span_end_time - final_span_start_time) / span_duration;
println!("> Slider: {:?}", slider_objects);
if *repeats & 1 == 0 {
final_progress = 1.0 - final_progress;
}
// println!(
// "final_span_index={} | final_span_start_time={} | \
// final_span_end_time={} | final_progress={}",
// final_span_idx, final_span_start_time, final_span_end_time, final_progress
// );
// println!("len={}", final_progress * *pixel_len as f32);
let dist_end = (repeats % 2) as f32 * pixel_len;
let pos = curve.point_at_distance(dist_end);
slider_objects.push(SliderTick::new(pos, final_span_end_time));
// println!(
// "start_time={} | span_duration={} | vel={} | \
// tick_dist={} | dist={} | span_count={} | \
// legacy_last_tick_offset={}",
// h.start_time,
// duration / *repeats as f32,
// *pixel_len as f32 / duration,
// tick_distance,
// *pixel_len,
// *repeats,
// 36
// );
// println!("> Slider: {:?}", slider_objects);
let radius = OBJECT_RADIUS * scale;
@@ -187,26 +220,74 @@ impl OsuObject {
// println!("radius={} | stack_offset={:?}", radius, stack_offset);
let stacked_pos = pos + stack_offset;
let pos = h.pos;
let stacked_pos = pos + stack_offset; // TODO: Simplify for below
// println!(
// "stacked_pos = {:?} + {:?} = {:?}",
// pos, stack_offset, stacked_pos
// );
let mut cursor_end_pos = stacked_pos;
let mut cursor_travel_dist = 0.0;
let approx_follow_circle_radius = radius * 3.0;
// println!(
// "stacked_pos={:?} | approx_follow_circle_radius={}",
// stacked_pos, approx_follow_circle_radius
// );
// let mut curr_offset = tick_distance;
for (i, tick) in slider_objects.iter().skip(1).enumerate() {
let diff = stacked_pos + tick.pos - cursor_end_pos;
let mut progress = (tick.time - h.start_time) / span_duration;
if progress % 2.0 >= 1.0 {
progress = 1.0 - progress % 1.0;
} else {
progress %= 1.0;
}
let curr_dist = pixel_len * progress;
let curr_pos = curve.point_at_distance(curr_dist);
let diff = stacked_pos + curr_pos - pos - cursor_end_pos;
let mut dist = diff.length();
println!("[{}] diff={:?} | dist={}", i, diff, dist);
// println!(
// "position at: progress=? | d={} => {:?}",
// curr_offset, tick.pos
// );
// curr_offset += tick_distance;
println!(
"[{}] diff = {:?} + {:?} - {:?} = {:?} | dist={}",
i,
stacked_pos,
tick.pos - pos,
cursor_end_pos,
diff,
dist
);
// println!("{} > {}", dist, approx_follow_circle_radius);
if dist > approx_follow_circle_radius {
let normalized = diff.normalize();
// println!("diff before: {:?}", diff);
// println!("diff after: {:?}", normalized);
dist -= approx_follow_circle_radius;
cursor_end_pos += normalized * dist;
// println!("+= {} * {} => {:?}", normalized, dist, cursor_end_pos);
cursor_travel_dist += dist;
// println!("+= {} => {}", dist, cursor_travel_dist);
}
}
println!("cursor_travel_dist={}", cursor_travel_dist);
println!("---");
Self::Slider {
@@ -288,8 +369,9 @@ impl OsuObject {
}
}
// TODO: Remove pub
#[inline]
fn pos(&self) -> Pos2 {
pub fn pos(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { objects, .. } => objects[0].pos,
@@ -15,7 +15,7 @@ pub(crate) struct Skill {
current_section_peak: f32,
kind: SkillKind,
strain_peaks: Vec<f32>,
pub strain_peaks: Vec<f32>, // TODO: Remove pub
prev_time: Option<f32>,
}
+119
View File
@@ -0,0 +1,119 @@
use super::DifficultyObject;
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
const SPEED_ANGLE_BONUS_BEGIN: f32 = 5.0 * std::f32::consts::FRAC_PI_6;
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
const MIN_SPEED_BONUS: f32 = 75.0;
const MAX_SPEED_BONUS: f32 = 45.0;
const SPEED_BALANCING_FACTOR: f32 = 40.0;
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
const TIMING_THRESHOLD: f32 = 107.0;
#[derive(Copy, Clone)]
pub(crate) enum SkillKind {
Aim,
Speed,
}
impl SkillKind {
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
match self {
Self::Aim => {
if current.base.is_spinner() {
return 0.0;
}
// println!("pos={:?}", current.base.pos());
let mut result = 0.0;
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
if let Some(angle) = current.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
let scale = 90.0;
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
* (prev_jump_dist - scale).max(0.0)
* (current.jump_dist - scale).max(0.0))
.sqrt();
// println!("angle_bonus={}", angle_bonus);
result = 1.5 * apply_diminishing_exp(angle_bonus.max(0.0))
/ (TIMING_THRESHOLD).max(prev_strain_time)
} else {
// println!("nop");
}
} else {
// println!("no prev");
}
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
// println!("jump_dist={} => {}", current.jump_dist, jump_dist_exp);
// println!("travel_dist={} => {}", current.travel_dist, travel_dist_exp);
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / current.strain_time)
}
Self::Speed => {
if current.base.is_spinner() {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut speed_bonus = 1.0;
if delta_time < MIN_SPEED_BONUS {
let exp_base = (MIN_SPEED_BONUS - delta_time) / SPEED_BALANCING_FACTOR;
speed_bonus = 1.0 + exp_base * exp_base;
}
let mut angle_bonus = 1.0;
// println!("angle: {:?}", current.angle);
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
if angle < PI_OVER_2 {
angle_bonus = 1.28;
// TODO: Improve ifs
if dist < 90.0 && angle < PI_OVER_4 {
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
} else if dist < 90.0 {
angle_bonus += (1.0 - angle_bonus)
* ((90.0 - dist) / 10.0).min(1.0)
* ((PI_OVER_2 - angle) / PI_OVER_4).sin();
}
}
}
// println!(
// "dist={} | speed_bonus={} | angle_bonus={}",
// dist, speed_bonus, angle_bonus
// );
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.strain_time
}
}
}
}
#[inline]
fn apply_diminishing_exp(val: f32) -> f32 {
val.powf(0.99)
}
+3
View File
@@ -0,0 +1,3 @@
pub mod all_included;
pub mod no_leniency;
pub mod no_sliders_no_leniency;
@@ -0,0 +1,72 @@
use super::OsuObject;
const NORMALIZED_RADIUS: f32 = 52.0;
pub(crate) struct DifficultyObject {
pub(crate) base: OsuObject,
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
pub(crate) jump_dist: f32,
pub(crate) travel_dist: f32,
pub(crate) angle: Option<f32>,
pub(crate) delta: f32,
pub(crate) strain_time: f32,
}
impl DifficultyObject {
pub(crate) fn new(
base: OsuObject,
prev: OsuObject,
prev_diff: Option<DifficultyObject>,
prev_prev: Option<OsuObject>,
clock_rate: f32,
radius: f32,
) -> Self {
let delta = (base.time() - prev.time()) / clock_rate;
let strain_time = delta.max(50.0);
let mut scaling_factor = NORMALIZED_RADIUS / radius;
if radius < 30.0 {
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
scaling_factor *= 1.0 + small_circle_bonus;
}
let pos = base.pos();
let travel_dist = prev.travel_dist();
let prev_cursor_pos = prev.pos();
let jump_dist = if base.is_spinner() {
0.0
} else {
(pos * scaling_factor - prev_cursor_pos * scaling_factor).length()
};
let angle = prev_prev.map(|prev_prev| {
let prev_prev_cursor_pos = prev_prev.pos();
let v1 = prev_prev_cursor_pos - prev_cursor_pos;
let v2 = pos - prev_cursor_pos;
let dot = v1.dot(v2);
let det = v1.x * v2.y - v1.y * v2.x;
det.atan2(dot).abs()
});
let prev = prev_diff.map(|o| (o.jump_dist, o.strain_time));
Self {
base,
prev,
jump_dist,
travel_dist,
angle,
delta,
strain_time,
}
}
}
+237
View File
@@ -0,0 +1,237 @@
use crate::DifficultyAttributes;
mod difficulty_object;
mod osu_object;
mod skill;
mod skill_kind;
use difficulty_object::DifficultyObject;
use osu_object::OsuObject;
use skill::Skill;
use skill_kind::SkillKind;
use parse::{Beatmap, Mods};
const OBJECT_RADIUS: f32 = 64.0;
const SECTION_LEN: f32 = 400.0;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
/// Star calculation for osu!standard maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
let attributes = map.attributes().mods(mods);
if map.hit_objects.len() < 2 {
return DifficultyAttributes {
stars: 0.0,
ar: attributes.ar,
od: attributes.od,
speed_strain: 0.0,
aim_strain: 0.0,
max_combo: 0,
n_circles: 0,
n_spinners: 0,
};
}
let section_len = SECTION_LEN * attributes.clock_rate;
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
let mut hit_objects = map
.hit_objects
.iter()
.map(|h| OsuObject::new(h, map, &attributes));
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_diff = None;
let mut _i = 0;
for curr in hit_objects {
let h = DifficultyObject::new(
curr.clone(),
prev.clone(),
prev_diff,
prev_prev,
attributes.clock_rate,
radius,
);
// println!(
// "strain_time={} | travel_dist={} | jump_dist={} | angle={:?}",
// h.strain_time, h.travel_dist, h.jump_dist, h.angle
// );
// println!("[{}] time={}", _i, curr.time());
while h.base.time() > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
_i += 1;
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
prev_diff = Some(h);
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
// println!("Aim:");
// for (i, strain) in skills[0].strain_peaks.iter().enumerate() {
// println!("{}: {}", i, strain);
// }
// println!("Speed:");
// for (i, strain) in skills[1].strain_peaks.iter().enumerate() {
// println!("{}: {}", i, strain);
// }
// println!("Aim: {:?}", skills[0].strain_peaks);
// println!("Speed: {:?}", skills[1].strain_peaks);
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
// println!("After:\n{:?}", skills[0].strain_peaks);
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
// println!("After:\n{:?}", skills[1].strain_peaks);
let stars = aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0;
DifficultyAttributes {
stars,
ar: attributes.ar,
od: attributes.od,
speed_strain: speed_rating,
aim_strain: aim_rating,
max_combo: 0, // TODO
n_circles: 0, // TODO
n_spinners: 0, // TODO
}
}
#[cfg(test)]
mod tests {
use super::stars;
use crate::PpCalculator;
use parse::Beatmap;
use std::fs::File;
#[test]
fn no_leniency_single_stars() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let stars = stars(&map, 0).stars;
println!("Stars: {}", stars);
}
#[test]
#[ignore]
fn no_leniency_stars() {
let margin = 0.005;
#[rustfmt::skip]
// TODO: More mods
let data = vec![
(1851299, 1 << 8, 4.23514130038547), // HT
(1851299, 0, 5.356786475158158), // NM
(1851299, 1 << 6, 7.450616908751305), // DT
(1851299, 1 << 4, 5.6834681957637665),// HR
(1851299, 1 << 1, 4.937817303399699), // EZ
(70090, 1 << 8, 2.2929922580201803), // HT
(70090, 0, 2.8322940761833983), // NM
(70090, 1 << 6, 3.8338563325375485), // DT
(70090, 1 << 4, 3.0617492228478174), // HR
(70090, 1 << 1, 2.698823231324141), // EZ
(1241370, 1 << 8, 5.662809600985943), // HT
(1241370, 0, 7.0367002127481975), // NM
(1241370, 1 << 6, 11.144720506574934),// DT
(1241370, 1 << 4, 7.641688110458715), // HR
(1241370, 1 << 1, 6.316288616688052), // EZ
// Slider fiesta
// (1657535, 1 << 8, 4.1727975286379895),// HT
// (1657535, 0, 5.16048239944917), // NM
// (1657535, 1 << 6, 7.125936779100417), // DT
// (1657535, 1 << 4, 5.545877027713307), // HR
// (1657535, 1 << 1, 4.66015083361088), // EZ
];
for (map_id, mods, expected_stars) in data {
let file = match File::open(format!("./test/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let stars = stars(&map, mods).stars;
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
}
#[test]
fn no_leniency_single_pp() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/2578801.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let calculator = PpCalculator::new(&map).mods(0).stars_function(stars);
let result = calculator.calculate();
println!("PP: {}", result.pp);
}
}
+352
View File
@@ -0,0 +1,352 @@
#![allow(unused)]
use crate::curve::Curve;
use parse::{Beatmap, BeatmapAttributes, HitObject, HitObjectKind, PathType, Pos2};
use std::cmp::Ordering;
macro_rules! binary_search {
($slice:expr, $target:expr) => {
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
};
}
const OBJECT_RADIUS: f32 = 64.0;
const STACK_DIST: f32 = 3.0;
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
#[derive(Clone)] // TODO: Remove clone
pub(crate) enum OsuObject {
Circle {
pos: Pos2,
time: f32,
},
Slider {
objects: Vec<SliderTick>,
cursor_end_pos: Pos2,
cursor_travel_dist: f32,
},
Spinner {
pos: Pos2,
time: f32,
},
}
impl OsuObject {
pub(crate) fn new(h: &HitObject, map: &Beatmap, attributes: &BeatmapAttributes) -> Self {
let pos = h.pos;
let time = h.start_time;
let scale = (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
let mut stack_height = 0.0;
match &h.kind {
HitObjectKind::Circle => Self::Circle { pos, time },
HitObjectKind::Slider {
pixel_len,
repeats,
curve_points,
path_type,
} => {
let (beat_len, timing_time) = {
match binary_search!(map.timing_points, time) {
Ok(idx) => {
let point = &map.timing_points[idx];
(point.beat_len, point.time)
}
Err(0) => (1000.0, 0.0),
Err(idx) => {
let point = &map.timing_points[idx - 1];
(point.beat_len, point.time)
}
}
};
let (speed_multiplier, diff_time) = {
match binary_search!(map.difficulty_points, time) {
Ok(idx) => {
let point = &map.difficulty_points[idx];
(point.speed_multiplier, point.time)
}
Err(0) => (1.0, 0.0),
Err(idx) => {
let point = &map.difficulty_points[idx - 1];
(point.speed_multiplier, point.time)
}
}
};
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
if map.version >= 8 {
tick_distance /= (100.0 / speed_multiplier).max(10.0).min(1000.0) / 100.0;
}
let spm = if timing_time > diff_time {
1.0
} else {
speed_multiplier
};
let duration = *repeats as f32 * beat_len * pixel_len / (map.sv * spm) / 100.0;
// let velocity = *pixel_len as f32 / duration;
// println!("duration={}", duration);
// println!("velocity={}", velocity);
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
PathType::Bezier
} else if curve_points.len() == 2 {
PathType::Linear
} else {
*path_type
};
let curve = match path_type {
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
PathType::Bezier => Curve::bezier(&curve_points),
PathType::Catmull => Curve::catmull(&curve_points),
PathType::PerfectCurve => Curve::perfect(&curve_points),
};
let mut current_distance = tick_distance;
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
let target = pixel_len - tick_distance / 8.0;
let mut ticks = Vec::with_capacity((target / tick_distance) as usize);
while current_distance < target {
let pos = curve.point_at_distance(current_distance);
let time = h.start_time + time_add * (ticks.len() + 1) as f32;
ticks.push(SliderTick::new(pos, time));
current_distance += tick_distance;
}
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
slider_objects.push(SliderTick::new(h.pos, h.start_time));
if *repeats <= 1 {
slider_objects.append(&mut ticks);
} else {
slider_objects.append(&mut ticks.clone());
for repeat_id in 1..repeats - 1 {
let dist = (repeat_id % 2) as f32 * pixel_len;
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
let pos = curve.point_at_distance(dist);
// Reverse tick / last legacy tick
slider_objects.push(SliderTick::new(pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.extend_from_slice(&ticks); // tick time doesn't need to be adjusted for some reason
}
// Handling last span separatly so that `ticks` vector isn't cloned again
let dist = ((repeats - 1) % 2) as f32 * pixel_len;
let time_offset = (duration / *repeats as f32) * (repeats - 1) as f32;
let pos = curve.point_at_distance(dist);
slider_objects.push(SliderTick::new(pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.append(&mut ticks);
}
// Slider tail
let span_duration = duration / *repeats as f32;
let final_span_idx = repeats.saturating_sub(1);
let final_span_start_time = h.start_time + final_span_idx as f32 * span_duration;
let final_span_end_time = (h.start_time + duration / 2.0)
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
let mut final_progress =
(final_span_end_time - final_span_start_time) / span_duration;
if *repeats & 1 == 0 {
final_progress = 1.0 - final_progress;
}
// println!(
// "final_span_index={} | final_span_start_time={} | \
// final_span_end_time={} | final_progress={}",
// final_span_idx, final_span_start_time, final_span_end_time, final_progress
// );
// println!("len={}", final_progress * *pixel_len as f32);
let dist_end = (repeats % 2) as f32 * pixel_len;
let pos = curve.point_at_distance(dist_end);
slider_objects.push(SliderTick::new(pos, final_span_end_time));
// println!(
// "start_time={} | span_duration={} | vel={} | \
// tick_dist={} | dist={} | span_count={} | \
// legacy_last_tick_offset={}",
// h.start_time,
// duration / *repeats as f32,
// *pixel_len as f32 / duration,
// tick_distance,
// *pixel_len,
// *repeats,
// 36
// );
// println!("> Slider: {:?}", slider_objects);
let radius = OBJECT_RADIUS * scale;
let stack_offset = {
let c = stack_height * scale * -6.4;
Pos2 { x: c, y: c }
};
// println!("radius={} | stack_offset={:?}", radius, stack_offset);
let pos = h.pos;
let stacked_pos = pos + stack_offset; // TODO: Simplify for below
// println!(
// "stacked_pos = {:?} + {:?} = {:?}",
// pos, stack_offset, stacked_pos
// );
let mut cursor_end_pos = stacked_pos;
let mut cursor_travel_dist = 0.0;
let approx_follow_circle_radius = radius * 3.0;
// println!(
// "stacked_pos={:?} | approx_follow_circle_radius={}",
// stacked_pos, approx_follow_circle_radius
// );
// let mut curr_offset = tick_distance;
for (i, tick) in slider_objects.iter().skip(1).enumerate() {
let mut progress = (tick.time - h.start_time) / span_duration;
if progress % 2.0 >= 1.0 {
progress = 1.0 - progress % 1.0;
} else {
progress %= 1.0;
}
let curr_dist = pixel_len * progress;
let curr_pos = curve.point_at_distance(curr_dist);
let diff = stacked_pos + curr_pos - pos - cursor_end_pos;
let mut dist = diff.length();
// println!(
// "position at: progress=? | d={} => {:?}",
// curr_offset, tick.pos
// );
// curr_offset += tick_distance;
println!(
"[{}] diff = {:?} + {:?} - {:?} = {:?} | dist={}",
i,
stacked_pos,
tick.pos - pos,
cursor_end_pos,
diff,
dist
);
// println!("{} > {}", dist, approx_follow_circle_radius);
if dist > approx_follow_circle_radius {
let normalized = diff.normalize();
// println!("diff before: {:?}", diff);
// println!("diff after: {:?}", normalized);
dist -= approx_follow_circle_radius;
cursor_end_pos += normalized * dist;
// println!("+= {} * {} => {:?}", normalized, dist, cursor_end_pos);
cursor_travel_dist += dist;
// println!("+= {} => {}", dist, cursor_travel_dist);
}
}
println!("cursor_travel_dist={}", cursor_travel_dist);
println!("---");
Self::Slider {
objects: slider_objects,
cursor_end_pos,
cursor_travel_dist,
}
}
HitObjectKind::Spinner { .. } => Self::Spinner { pos, time },
HitObjectKind::Hold { .. } => panic!("found Hold object in osu!standard file"),
}
}
#[inline]
pub(crate) fn time(&self) -> f32 {
match self {
Self::Circle { time, .. } => *time,
Self::Slider { objects, .. } => objects[0].time,
Self::Spinner { time, .. } => *time,
}
}
#[inline]
pub(crate) fn travel_dist(&self) -> f32 {
match self {
Self::Slider {
cursor_travel_dist, ..
} => *cursor_travel_dist,
_ => 0.0,
}
}
#[inline]
pub(crate) fn cursor_end_position(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { cursor_end_pos, .. } => *cursor_end_pos,
Self::Spinner { pos, .. } => *pos,
}
}
#[inline]
pub(crate) fn is_spinner(&self) -> bool {
matches!(self, Self::Spinner { .. })
}
// TODO: Remove pub
#[inline]
pub fn pos(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { objects, .. } => objects[0].pos,
Self::Spinner { .. } => Pos2::default(),
}
}
}
#[derive(Copy, Clone)]
pub(crate) struct SliderTick {
pos: Pos2,
time: f32,
}
impl SliderTick {
fn new(pos: Pos2, time: f32) -> Self {
Self { pos, time }
}
}
// TODO: Remove
impl std::fmt::Debug for SliderTick {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{{pos={:?} | time={}}}", self.pos, self.time)
}
}
+99
View File
@@ -0,0 +1,99 @@
use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering;
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const DECAY_WEIGHT: f32 = 0.9;
pub(crate) struct Skill {
pub current_strain: f32,
current_section_peak: f32,
kind: SkillKind,
pub strain_peaks: Vec<f32>, // TODO: Remove pub
prev_time: Option<f32>,
}
impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
if self.prev_time.is_some() {
self.strain_peaks.push(self.current_section_peak);
}
}
#[inline]
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 - prev);
}
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyObject) {
self.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.time());
}
pub(crate) fn difficulty_value(&mut self) -> f32 {
let mut difficulty = 0.0;
let mut weight = 1.0;
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in self.strain_peaks.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
}
difficulty
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
}
}
#[inline]
fn strain_decay_base(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
SkillKind::Speed => SPEED_STRAIN_DECAY_BASE,
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
+119
View File
@@ -0,0 +1,119 @@
use super::DifficultyObject;
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
const SPEED_ANGLE_BONUS_BEGIN: f32 = 5.0 * std::f32::consts::FRAC_PI_6;
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
const MIN_SPEED_BONUS: f32 = 75.0;
const MAX_SPEED_BONUS: f32 = 45.0;
const SPEED_BALANCING_FACTOR: f32 = 40.0;
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
const TIMING_THRESHOLD: f32 = 107.0;
#[derive(Copy, Clone)]
pub(crate) enum SkillKind {
Aim,
Speed,
}
impl SkillKind {
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
match self {
Self::Aim => {
if current.base.is_spinner() {
return 0.0;
}
// println!("pos={:?}", current.base.pos());
let mut result = 0.0;
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
if let Some(angle) = current.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
let scale = 90.0;
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
* (prev_jump_dist - scale).max(0.0)
* (current.jump_dist - scale).max(0.0))
.sqrt();
// println!("angle_bonus={}", angle_bonus);
result = 1.5 * apply_diminishing_exp(angle_bonus.max(0.0))
/ (TIMING_THRESHOLD).max(prev_strain_time)
} else {
// println!("nop");
}
} else {
// println!("no prev");
}
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
// println!("jump_dist={} => {}", current.jump_dist, jump_dist_exp);
// println!("travel_dist={} => {}", current.travel_dist, travel_dist_exp);
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / current.strain_time)
}
Self::Speed => {
if current.base.is_spinner() {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut speed_bonus = 1.0;
if delta_time < MIN_SPEED_BONUS {
let exp_base = (MIN_SPEED_BONUS - delta_time) / SPEED_BALANCING_FACTOR;
speed_bonus = 1.0 + exp_base * exp_base;
}
let mut angle_bonus = 1.0;
// println!("angle: {:?}", current.angle);
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
if angle < PI_OVER_2 {
angle_bonus = 1.28;
// TODO: Improve ifs
if dist < 90.0 && angle < PI_OVER_4 {
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
} else if dist < 90.0 {
angle_bonus += (1.0 - angle_bonus)
* ((90.0 - dist) / 10.0).min(1.0)
* ((PI_OVER_2 - angle) / PI_OVER_4).sin();
}
}
}
// println!(
// "dist={} | speed_bonus={} | angle_bonus={}",
// dist, speed_bonus, angle_bonus
// );
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.strain_time
}
}
}
}
#[inline]
fn apply_diminishing_exp(val: f32) -> f32 {
val.powf(0.99)
}
@@ -0,0 +1,75 @@
use parse::{Beatmap, DifficultyPoint, TimingPoint};
use std::slice::Iter;
pub(crate) struct ControlPointIter<'p> {
timing_points: Iter<'p, TimingPoint>,
difficulty_points: Iter<'p, DifficultyPoint>,
next_timing: Option<f32>,
next_difficulty: Option<(f32, f32)>,
}
impl<'p> ControlPointIter<'p> {
pub(crate) fn new(map: &'p Beatmap) -> Self {
let mut timing_points = map.timing_points.iter();
let mut difficulty_points = map.difficulty_points.iter();
Self {
next_timing: timing_points.next().map(|t| t.time),
next_difficulty: difficulty_points
.next()
.map(|d| (d.time, d.speed_multiplier)),
timing_points,
difficulty_points,
}
}
}
#[derive(Debug)]
pub(crate) enum ControlPoint {
Timing { time: f32 },
Difficulty { time: f32, speed_multiplier: f32 },
}
impl<'p> Iterator for ControlPointIter<'p> {
type Item = ControlPoint;
fn next(&mut self) -> Option<Self::Item> {
match (self.next_timing, self.next_difficulty) {
(Some(time), Some((d, _))) if time < d => {
self.next_timing = self.timing_points.next().map(|t| t.time);
Some(ControlPoint::Timing { time })
}
(Some(_), Some((time, speed_multiplier))) => {
self.next_difficulty = self
.difficulty_points
.next()
.map(|d| (d.time, d.speed_multiplier));
Some(ControlPoint::Difficulty {
time,
speed_multiplier,
})
}
(Some(time), _) => {
self.next_timing = self.timing_points.next().map(|t| t.time);
Some(ControlPoint::Timing { time })
}
(_, Some((time, speed_multiplier))) => {
self.next_difficulty = self
.difficulty_points
.next()
.map(|d| (d.time, d.speed_multiplier));
Some(ControlPoint::Difficulty {
time,
speed_multiplier,
})
}
(None, None) => None,
}
}
}
@@ -0,0 +1,68 @@
use parse::HitObject;
use std::borrow::Cow;
const NORMALIZED_RADIUS: f32 = 52.0;
pub(crate) struct DifficultyObject<'h> {
pub(crate) base: &'h HitObject,
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
pub(crate) jump_dist: f32,
pub(crate) angle: Option<f32>,
pub(crate) delta: f32,
pub(crate) strain_time: f32,
}
impl<'h> DifficultyObject<'h> {
pub(crate) fn new(
base: &'h HitObject,
prev: &HitObject,
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time)
prev_prev: Option<Cow<HitObject>>,
clock_rate: f32,
radius: f32,
) -> Self {
let delta = (base.start_time - prev.start_time) / clock_rate;
let strain_time = delta.max(50.0);
let mut scaling_factor = NORMALIZED_RADIUS / radius;
let prev_cursor_pos = prev.pos;
if radius < 30.0 {
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
scaling_factor *= 1.0 + small_circle_bonus;
}
let jump_dist = if base.is_spinner() {
0.0
} else {
(base.pos * scaling_factor - prev_cursor_pos * scaling_factor).length()
};
let angle = prev_prev.map(|prev_prev| {
let prev_prev_cursor_pos = prev_prev.pos;
let v1 = prev_prev_cursor_pos - prev.pos;
let v2 = base.pos - prev_cursor_pos;
let dot = v1.dot(v2);
let det = v1.x * v2.y - v1.y * v2.x;
det.atan2(dot).abs()
});
// let prev = prev_diff.map(|o| (o.jump_dist, o.strain_time));
Self {
base,
prev: prev_vals,
jump_dist,
angle,
delta,
strain_time,
}
}
}
@@ -0,0 +1,330 @@
use crate::{difficulty_range_ar, difficulty_range_od, DifficultyAttributes};
mod control_point_iter;
mod difficulty_object;
mod skill;
mod skill_kind;
use control_point_iter::{ControlPoint, ControlPointIter};
use difficulty_object::DifficultyObject;
use skill::Skill;
use skill_kind::SkillKind;
use parse::{Beatmap, HitObject, HitObjectKind, Mods};
use std::borrow::Cow;
const OBJECT_RADIUS: f32 = 64.0;
const SECTION_LEN: f32 = 400.0;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
/// Star calculation for osu!standard maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
let attributes = map.attributes().mods(mods);
if map.hit_objects.len() < 2 {
return DifficultyAttributes {
stars: 0.0,
ar: attributes.ar,
od: attributes.od,
speed_strain: 0.0,
aim_strain: 0.0,
max_combo: 0,
n_circles: 0,
n_spinners: 0,
};
}
let section_len = SECTION_LEN * attributes.clock_rate;
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
let mut max_combo = 0;
let mut n_circles = 0;
let mut n_spinners = 0;
let mut state = SliderState::new(&map);
let mut hit_objects = map.hit_objects.iter().map(|h| match &h.kind {
HitObjectKind::Circle => {
max_combo += 1;
n_circles += 1;
Cow::Borrowed(h)
}
HitObjectKind::Slider {
pixel_len, repeats, ..
} => {
max_combo += count_ticks(h.start_time, *pixel_len, *repeats, &map, &mut state);
let h = HitObject {
pos: h.pos,
start_time: h.start_time,
kind: HitObjectKind::Circle,
sound: h.sound,
};
Cow::Owned(h)
}
HitObjectKind::Spinner { .. } => {
max_combo += 1;
n_spinners += 1;
Cow::Borrowed(h)
}
HitObjectKind::Hold { .. } => panic!("found Hold object in osu!standard map"),
});
let mut aim = Skill::new(SkillKind::Aim);
let mut speed = Skill::new(SkillKind::Speed);
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_vals = None;
// Handle first object separately to remove if-branching
let curr = hit_objects.next().unwrap();
let h = DifficultyObject::new(
curr.as_ref(),
prev.as_ref(),
prev_vals,
prev_prev,
attributes.clock_rate,
radius,
);
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
// Handle all other objects
for curr in hit_objects {
let h = DifficultyObject::new(
curr.as_ref(),
prev.as_ref(),
prev_vals,
prev_prev,
attributes.clock_rate,
radius,
);
while h.base.start_time > current_section_end {
aim.save_current_peak();
aim.start_new_section_from(current_section_end);
speed.save_current_peak();
speed.start_new_section_from(current_section_end);
current_section_end += section_len;
}
aim.process(&h);
speed.process(&h);
prev_prev = Some(prev);
prev_vals = Some((h.jump_dist, h.strain_time));
prev = curr;
}
aim.save_current_peak();
speed.save_current_peak();
let aim_strain = aim.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_strain = speed.difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let stars = aim_strain + speed_strain + (aim_strain - speed_strain).abs() / 2.0;
let hit_window_od = difficulty_range_od(attributes.od) as i32 as f32 / attributes.clock_rate;
let hit_window_ar = difficulty_range_ar(attributes.ar) as i32 as f32 / attributes.clock_rate;
let od = (80.0 - hit_window_od) / 6.0;
let ar = if hit_window_ar > 1200.0 {
(1800.0 - hit_window_ar) / 120.0
} else {
(1200.0 - hit_window_ar) / 150.0 + 5.0
};
DifficultyAttributes {
stars,
ar,
od,
speed_strain,
aim_strain,
max_combo,
n_circles,
n_spinners,
}
}
struct SliderState<'p> {
control_points: ControlPointIter<'p>,
next_time: f32,
px_per_beat: f32,
prev_sv: f32,
}
impl<'p> SliderState<'p> {
#[inline]
fn new(map: &'p Beatmap) -> Self {
Self {
control_points: ControlPointIter::new(map),
next_time: std::f32::NEG_INFINITY,
px_per_beat: 1.0,
prev_sv: 1.0,
}
}
}
fn count_ticks(
time: f32,
pixel_len: f32,
repeats: usize,
map: &Beatmap,
state: &mut SliderState,
) -> usize {
while time >= state.next_time {
state.px_per_beat = map.sv * 100.0 * state.prev_sv;
match state.control_points.next() {
Some(ControlPoint::Timing { time }) => {
state.next_time = time;
state.prev_sv = 1.0;
}
Some(ControlPoint::Difficulty {
time,
speed_multiplier,
}) => {
state.next_time = time;
state.prev_sv = speed_multiplier;
}
None => break,
}
}
let spans = repeats as f32;
let beats = pixel_len * spans / state.px_per_beat;
let ticks = ((beats - 0.1) / spans * map.tick_rate).ceil() as usize;
ticks
.checked_sub(1)
.map_or(0, |ticks| ticks * repeats + repeats + 1)
}
#[cfg(test)]
mod tests {
use super::stars;
use crate::PpCalculator;
use parse::Beatmap;
use std::fs::File;
#[test]
fn no_sliders_no_leniency_single_stars() {
let file = match File::open("E:/Games/osu!/beatmaps/1241370.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
// let file = match File::open("C:/Users/Max/Desktop/1241370.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let stars = stars(&map, 0).stars;
println!("Stars: {}", stars);
}
#[test]
#[ignore]
fn no_sliders_no_leniency_stars() {
let margin = 0.5;
#[rustfmt::skip]
// TODO: More mods
let data = vec![
(1851299, 1 << 8, 4.23514130038547), // HT
(1851299, 0, 5.356786475158158), // NM
(1851299, 1 << 6, 7.450616908751305), // DT
(1851299, 1 << 4, 5.6834681957637665),// HR
(1851299, 1 << 1, 4.937817303399699), // EZ
(70090, 1 << 8, 2.2929922580201803), // HT
(70090, 0, 2.8322940761833983), // NM
(70090, 1 << 6, 3.8338563325375485), // DT
(70090, 1 << 4, 3.0617492228478174), // HR
(70090, 1 << 1, 2.698823231324141), // EZ
(1241370, 1 << 8, 5.662809600985943), // HT
(1241370, 0, 7.0367002127481975), // NM
(1241370, 1 << 6, 11.144720506574934),// DT
(1241370, 1 << 4, 7.641688110458715), // HR
(1241370, 1 << 1, 6.316288616688052), // EZ
// Slider fiesta
// (1657535, 1 << 8, 4.1727975286379895),// HT
// (1657535, 0, 5.16048239944917), // NM
// (1657535, 1 << 6, 7.125936779100417), // DT
// (1657535, 1 << 4, 5.545877027713307), // HR
// (1657535, 1 << 1, 4.66015083361088), // EZ
];
for (map_id, mods, expected_stars) in data {
let file = match File::open(format!("./test/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let stars = stars(&map, mods).stars;
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
}
#[test]
fn no_sliders_no_leniency_single_pp() {
let file = match File::open("E:/Games/osu!/beatmaps/1241370.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
// let file = match File::open("C:/Users/Max/Desktop/1851299_base.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let calculator = PpCalculator::new(&map)
.stars_function(stars)
// .misses(2)
// .accuracy(96.78)
// .combo(100)
// .n100(0)
.mods(0);
let result = calculator.calculate();
println!("Stars: {}", result.attributes.stars);
println!("PP: {}", result.pp);
}
}
@@ -0,0 +1,95 @@
use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering;
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const DECAY_WEIGHT: f32 = 0.9;
pub(crate) struct Skill {
current_strain: f32,
current_section_peak: f32,
kind: SkillKind,
strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
self.strain_peaks.push(self.current_section_peak);
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f32) {
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyObject) {
self.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.start_time);
}
pub(crate) fn difficulty_value(&mut self) -> f32 {
let mut difficulty = 0.0;
let mut weight = 1.0;
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in self.strain_peaks.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
}
difficulty
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
}
}
#[inline]
fn strain_decay_base(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
SkillKind::Speed => SPEED_STRAIN_DECAY_BASE,
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
@@ -1,7 +1,7 @@
use super::DifficultyObject;
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
const SPEED_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_6;
const SPEED_ANGLE_BONUS_BEGIN: f32 = 5.0 * std::f32::consts::FRAC_PI_6;
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
@@ -43,20 +43,16 @@ impl SkillKind {
}
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / current.strain_time)
(result + jump_dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(jump_dist_exp / current.strain_time)
}
Self::Speed => {
if current.base.is_spinner() {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
let dist = SINGLE_SPACING_TRESHOLD.min(current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut speed_bonus = 1.0;
@@ -75,7 +71,6 @@ impl SkillKind {
if angle < PI_OVER_2 {
angle_bonus = 1.28;
// TODO: Improve ifs
if dist < 90.0 && angle < PI_OVER_4 {
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
} else if dist < 90.0 {
+8 -2
View File
@@ -96,8 +96,14 @@ impl ops::AddAssign for Pos2 {
}
}
impl fmt::Debug for Pos2 {
impl fmt::Display for Pos2 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({},{})", self.x, self.y)
write!(f, "{:?}", self)
}
}
impl fmt::Debug for Pos2 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}