big first commit

This commit is contained in:
MaxOhn
2021-01-08 09:25:46 +01:00
commit 496216843f
61 changed files with 32981 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
use parse::Pos2;
const PLAYFIELD_WIDHT: f32 = 512.0;
const BASE_SPEED: f32 = 1.0;
#[derive(Clone)]
pub struct CatchObject {
pub(crate) pos: f32,
pub(crate) time: f32,
pub(crate) hyper_dash: bool,
pub(crate) hyper_dist: f32,
}
impl CatchObject {
#[inline]
pub(crate) fn new((pos, time): (Pos2, f32)) -> Self {
Self {
pos: pos.x,
time,
hyper_dash: false,
hyper_dist: 0.0,
}
}
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f32) -> Self {
let mut offset_pos = self.pos;
let time_diff = self.time - *last_time;
if let Some(last_pos_ref) = last_pos.filter(|_| time_diff <= 1000.0) {
let pos_diff = offset_pos - last_pos_ref;
if pos_diff.abs() > f32::EPSILON {
if pos_diff.abs() < (time_diff / 3.0).floor() {
if pos_diff > 0.0 {
if offset_pos + pos_diff < PLAYFIELD_WIDHT {
offset_pos += pos_diff;
}
} else if offset_pos + pos_diff > 0.0 {
offset_pos += pos_diff;
}
}
last_pos.replace(offset_pos);
*last_time = self.time;
}
self.pos = offset_pos;
} else {
last_pos.replace(offset_pos);
*last_time = self.time;
}
self
}
pub(crate) fn init_hyper_dash(
&mut self,
half_catcher_width: f32,
next: &CatchObject,
last_direction: &mut i8,
last_excess: &mut f32,
) {
let next_x = next.pos;
let curr_x = self.pos;
let this_direction = (next_x > curr_x) as i8 * 2 - 1;
let time_to_next = next.time - self.time - 1000.0 / 60.0 / 4.0;
let sub = if *last_direction == this_direction {
*last_excess
} else {
half_catcher_width
};
let dist_to_next = (next_x - curr_x).abs() - sub;
let hyper_dist = time_to_next * BASE_SPEED - dist_to_next;
if hyper_dist < 0.0 {
self.hyper_dash = true;
*last_excess = half_catcher_width;
} else {
self.hyper_dist = hyper_dist;
*last_excess = hyper_dist.max(0.0).min(half_catcher_width);
}
*last_direction = this_direction;
}
}
+149
View File
@@ -0,0 +1,149 @@
use super::math_util;
use parse::Pos2;
const SLIDER_QUALITY: f32 = 50.0;
pub(crate) enum Points {
Single(Pos2),
Multi(Vec<Pos2>),
}
pub(crate) enum Curve {
Linear {
a: Pos2,
b: Pos2,
},
Bezier(Points),
Catmull(Points),
Perfect {
origin: Pos2,
cx: f32,
cy: f32,
radius: f32,
},
}
impl Curve {
pub(crate) fn linear(a: Pos2, b: Pos2) -> Self {
Self::Linear { a, b }
}
pub(crate) fn bezier(points: &[Pos2]) -> Self {
if points.len() == 1 {
return Self::Bezier(Points::Single(points[0]));
}
let mut start = 0;
let mut end = 0;
let mut result = Vec::with_capacity(4);
for i in 0..points.len() - 1 {
if end - start > 1 && points[i] == points[end - 1] {
Self::_bezier(&mut result, &points[start..end]);
start = end;
}
end += 1;
}
Self::_bezier(&mut result, &points[start..end + 1]);
Self::Bezier(Points::Multi(result))
}
fn _bezier(result: &mut Vec<Pos2>, points: &[Pos2]) {
let step = 0.25 / SLIDER_QUALITY / points.len() as f32;
let mut i = 0.0;
let n = points.len() as i32 - 1;
while i < 1.0 + step {
let point = (0..=n).fold(Pos2 { x: 0.0, y: 0.0 }, |point, p| {
let factor = math_util::cpn(p, n) * (1.0 - i).powi(n - p) * i.powi(p);
point + points[p as usize] * factor
});
result.push(point);
i += step;
}
}
pub(crate) fn catmull(points: &[Pos2]) -> Self {
if points.len() == 1 {
return Self::Catmull(Points::Single(points[0]));
}
let order = points.len();
let step = 2.5 / SLIDER_QUALITY;
let target = step + 1.0;
let mut resulting_points = Vec::with_capacity(4);
for x in 0..order - 1 {
let mut t = 0.0;
while t < target {
let v1 = if x >= 1 { points[x - 1] } else { points[x] };
let v2 = points[x];
let v3 = if x + 1 < order {
points[x + 1]
} else {
v2.add_scaled(v2.add_scaled(v1, -1.0), 1.0)
};
let v4 = if x + 2 < order {
points[x + 2]
} else {
v3.add_scaled(v3.add_scaled(v2, -1.0), 1.0)
};
let point = Self::catmull_point(v1, v2, v3, v4, t);
resulting_points.push(point);
t += step;
}
}
Self::Catmull(Points::Multi(resulting_points))
}
#[inline]
fn catmull_point(p0: Pos2, p1: Pos2, p2: Pos2, p3: Pos2, len: f32) -> Pos2 {
Pos2 {
x: math_util::catmull(p0.x, p1.x, p2.x, p3.x, len),
y: math_util::catmull(p0.y, p1.y, p2.y, p3.y, len),
}
}
pub(crate) fn perfect(points: &[Pos2]) -> Self {
let (cx, cy, mut radius) = math_util::get_circum_circle(&points);
radius *= ((!math_util::is_left(&points)) as i8 * 2 - 1) as f32;
Self::Perfect {
origin: points[0],
cx,
cy,
radius,
}
}
pub(crate) fn point_at_distance(&self, len: f32) -> Pos2 {
let points = match self {
Self::Bezier(points) => points,
Self::Catmull(points) => points,
Self::Linear { a, b } => return math_util::point_on_line(*a, *b, len),
Self::Perfect {
origin,
cx,
cy,
radius,
} => return math_util::rotate(*cx, *cy, *origin, len / *radius),
};
match points {
Points::Single(point) => *point,
Points::Multi(points) => math_util::point_at_distance(points, len),
}
}
}
+43
View File
@@ -0,0 +1,43 @@
use super::CatchObject;
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
pub(crate) struct DifficultyObject<'o> {
pub(crate) base: &'o CatchObject,
pub(crate) last: &'o CatchObject,
pub(crate) delta: f32,
pub(crate) normalized_pos: f32,
pub(crate) last_normalized_pos: f32,
pub(crate) strain_time: f32,
pub(crate) clock_rate: f32,
}
impl<'o> DifficultyObject<'o> {
#[inline]
pub(crate) fn new(
base: &'o CatchObject,
last: &'o CatchObject,
half_catcher_width: f32,
clock_rate: f32,
) -> Self {
let delta = (base.time - last.time) / clock_rate;
let strain_time = delta.max(40.0);
let scaling_factor = NORMALIZED_HITOBJECT_RADIUS / half_catcher_width;
let normalized_pos = base.pos * scaling_factor;
let last_normalized_pos = last.pos * scaling_factor;
Self {
base,
last,
delta,
normalized_pos,
last_normalized_pos,
strain_time,
clock_rate,
}
}
}
+378
View File
@@ -0,0 +1,378 @@
mod catch_object;
mod curve;
mod difficulty_object;
mod math_util;
mod movement;
use catch_object::CatchObject;
use curve::Curve;
use difficulty_object::DifficultyObject;
use movement::Movement;
use parse::{Beatmap, HitObjectKind, Mods, PathType};
use std::cmp::Ordering;
use std::convert::identity;
const SECTION_LENGTH: f32 = 750.0;
const STAR_SCALING_FACTOR: f32 = 0.153;
const ALLOWED_CATCH_RANGE: f32 = 0.8;
const CATCHER_SIZE: f32 = 106.75;
macro_rules! binary_search {
($slice:expr, $target:expr) => {
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
};
}
/// Star calculation for osu!ctb maps
// Slider parsing based on https://github.com/osufx/catch-the-pp
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
if map.hit_objects.len() < 2 {
return 0.0;
}
let attributes = map.attributes().mods(mods);
let with_hr = mods.hr();
let mut ticks = Vec::new(); // using the same buffer for all sliders
// BUG: Incorrect object order on 2B maps that have fruits within sliders
let mut hit_objects = map
.hit_objects
.iter()
.scan((None, 0.0), |(last_pos, last_time), h| match &h.kind {
HitObjectKind::Circle => {
let mut h = CatchObject::new((h.pos, h.start_time));
if with_hr {
h = h.with_hr(last_pos, last_time);
}
Some(Some(FruitOrJuice::Fruit(Some(h))))
}
HitObjectKind::Slider {
pixel_len,
repeats,
curve_points,
path_type,
} => {
// HR business
last_pos
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
*last_time = h.start_time;
let (beat_len, timing_time) = {
match binary_search!(map.timing_points, h.start_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, h.start_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 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;
ticks.reserve((target / tick_distance) as usize);
while current_distance < target {
let pos = curve.point_at_distance(current_distance);
ticks.push((pos, h.start_time + time_add * (ticks.len() + 1) as f32));
current_distance += tick_distance;
}
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
slider_objects.push((h.pos, h.start_time));
if *repeats <= 1 {
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
} 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((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((pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
}
// Slider tail
let dist_end = (*repeats % 2) as f32 * *pixel_len;
let pos = curve.point_at_distance(dist_end);
slider_objects.push((pos, h.start_time + duration));
let iter = slider_objects.into_iter().map(CatchObject::new);
Some(Some(FruitOrJuice::Juice(iter)))
}
HitObjectKind::Spinner { .. } | HitObjectKind::Hold { .. } => Some(None),
})
.filter_map(identity)
.flatten();
// Hyper dash business
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
let mut last_direction = 0;
let mut last_excess = half_catcher_width;
// Strain business
let mut movement = Movement::new(attributes.cs);
let section_len = SECTION_LENGTH * attributes.clock_rate;
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev = hit_objects.next().unwrap();
let mut curr = hit_objects.next().unwrap();
prev.init_hyper_dash(
half_catcher_width,
&curr,
&mut last_direction,
&mut last_excess,
);
for next in hit_objects {
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 {
movement.save_current_peak();
movement.start_new_section_from(current_section_end);
current_section_end += section_len;
}
movement.process(&h);
prev = curr;
curr = next;
}
// Same as in loop but without init_hyper_dash because `curr` is the last element
let h = DifficultyObject::new(
&curr,
&prev,
movement.half_catcher_width,
attributes.clock_rate,
);
while h.base.time > current_section_end {
movement.save_current_peak();
movement.start_new_section_from(current_section_end);
current_section_end += section_len;
}
movement.process(&h);
movement.save_current_peak();
movement.difficulty_value().sqrt() * STAR_SCALING_FACTOR
}
#[inline]
pub(crate) fn calculate_catch_width(cs: f32) -> f32 {
let scale = 1.0 - 0.7 * (cs - 5.0) / 5.0;
CATCHER_SIZE * scale.abs() * ALLOWED_CATCH_RANGE
}
enum FruitOrJuice<I> {
Fruit(Option<CatchObject>),
Juice(I),
}
impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
type Item = CatchObject;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Fruit(fruit) => fruit.take(),
Self::Juice(slider) => slider.next(),
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
Self::Fruit(Some(_)) => (1, Some(1)),
Self::Fruit(None) => (0, Some(0)),
Self::Juice(slider) => slider.size_hint(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn test_single() {
let map_id = 1972149;
let file = match File::open(format!("E:/Games/osu!/beatmaps/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
// let file = match File::open(format!("E:/Games/osu!/beatmaps/{}.osu", map_id)) {
// 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 mods = 0;
let stars = stars(&map, mods);
println!("Stars: {} [map={} | mods={}]", stars, map_id, mods);
}
#[test]
fn test_fruits() {
let margin = 0.005;
#[rustfmt::skip]
let data = vec![
(1977380, 1 << 8, 2.0564713386286573),// HT
(1977380, 0, 2.5695489769068742), // NM
(1977380, 1 << 6, 3.589887228221038), // DT
(1977380, 1 << 4, 3.1515873669521928),// HR
(1977380, 1 << 1, 3.0035260129778396),// EZ
(1974968, 1 << 8, 1.9544305373156605),// HT
(1974968, 0, 2.521701539665241), // NM
(1974968, 1 << 6, 3.650649037957456), // DT
(1974968, 1 << 4, 3.566302788963401), // HR
(1974968, 1 << 1, 2.2029392066882654),// EZ
(2420076, 1 << 8, 4.791039358886245), // HT
(2420076, 0, 6.223136555625056), // NM
(2420076, 1 << 6, 8.908315960310958), // DT
(2420076, 1 << 4, 6.54788067620051), // HR
(2420076, 1 << 1, 6.067971540209479), // EZ
(2206596, 1 << 8, 4.767182611189798), // HT
(2206596, 0, 6.157660207091584), // NM
(2206596, 1 << 6, 8.93391286552717), // DT
(2206596, 1 << 4, 6.8639096665110735),// HR
(2206596, 1 << 1, 5.60279198088948), // EZ
// Super long juice stream towards end
// (1972149, 1 << 8, 4.671425766413811), // HT
// (1972149, 0, 6.043742871084152), // NM
// (1972149, 1 << 6, 8.469259368304225), // DT
// (1972149, 1 << 4, 6.81222485322862), // HR
// (1972149, 1 << 1, 5.289343020686747), // EZ
// Convert slider fiesta
// (1657535, 1 << 8, 3.862453635711741), // HT
// (1657535, 0, 4.792543335869686), // NM
// (1657535, 1 << 6, 6.655478646330863), // DT
// (1657535, 1 << 4, 5.259728567781568), // HR
// (1657535, 1 << 1, 4.127535166776765), // 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);
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
}
}
+133
View File
@@ -0,0 +1,133 @@
use parse::Pos2;
#[inline]
pub(crate) fn cpn(mut p: i32, n: i32) -> f32 {
if p < 0 || p > n {
return 0.0;
}
p = p.min(n - p);
let mut out = 1.0;
for i in 1..=p {
out *= (n - p + i) as f32 / i as f32;
}
out
}
#[inline]
pub(crate) fn catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
0.5 * ((2.0 * p1)
+ (-p0 + p2) * t
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t.powi(2)
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3 * t.powi(3)))
}
#[inline]
pub(crate) fn point_on_line(p1: Pos2, p2: Pos2, len: f32) -> Pos2 {
let mut full_len = ((p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sqrt();
let n = full_len - len;
if full_len.abs() < f32::EPSILON {
full_len = 1.0;
}
(p1 * n + p2 * len) / full_len
}
#[inline]
pub(crate) fn angle_from_points(p0: Pos2, p1: Pos2) -> f32 {
(p1.y - p0.y).atan2(p1.x - p0.x)
}
#[inline]
pub(crate) fn distance_from_points(arr: &[Pos2]) -> f32 {
arr.iter()
.skip(1)
.zip(arr.iter())
.map(|(curr, prev)| curr.distance(prev))
.sum()
}
#[inline]
pub(crate) fn cart_from_pol(r: f32, t: f32) -> Pos2 {
Pos2 {
x: r * t.cos(),
y: r * t.sin(),
}
}
pub(crate) fn point_at_distance(array: &[Pos2], distance: f32) -> Pos2 {
if array.len() < 2 {
return Pos2 { x: 0.0, y: 0.0 };
} else if distance.abs() < f32::EPSILON {
return array[0];
} else if distance_from_points(array) <= distance {
return array[array.len() - 1];
}
let mut i = 0;
let mut current_distance = 0.0;
let mut new_distance = 0.0;
while i < array.len() - 2 {
new_distance = (array[i] - array[i + 1]).length();
current_distance += new_distance;
if distance <= current_distance {
break;
}
i += 1;
}
current_distance -= new_distance;
if (distance - current_distance).abs() <= f32::EPSILON {
array[i]
} else {
let angle = angle_from_points(array[i], array[i + 1]);
let cart = cart_from_pol(distance - current_distance, angle);
if array[i].x > array[i + 1].x {
array[i] - cart
} else {
array[i] + cart
}
}
}
pub(crate) fn get_circum_circle(p: &[Pos2]) -> (f32, f32, f32) {
let d = 2.0
* (p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y) + p[2].x * (p[0].y - p[1].y));
let p0 = p[0].x * p[0].x + p[0].y * p[0].y;
let p1 = p[1].x * p[1].x + p[1].y * p[1].y;
let p2 = p[2].x * p[2].x + p[2].y * p[2].y;
let ux = (p0 * (p[1].y - p[2].y) + p1 * (p[2].y - p[0].y) + p2 * (p[0].y - p[1].y)) / d;
let uy = (p0 * (p[2].x - p[1].x) + p1 * (p[0].x - p[2].x) + p2 * (p[1].x - p[0].x)) / d;
let px = ux - p[0].x;
let py = uy - p[0].y;
let r = (px * px + py * py).sqrt();
(ux, uy, r)
}
#[inline]
pub(crate) fn is_left(p: &[Pos2]) -> bool {
((p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[1].y - p[0].y) * (p[2].x - p[0].x)) < 0.0
}
#[inline]
pub(crate) fn rotate(cx: f32, cy: f32, p: Pos2, radians: f32) -> Pos2 {
let cos = radians.cos();
let sin = radians.sin();
Pos2 {
x: (cos * (p.x - cx)) - (sin * (p.y - cy)) + cx,
y: (sin * (p.x - cx)) + (cos * (p.y - cy)) + cy,
}
}
+148
View File
@@ -0,0 +1,148 @@
use super::DifficultyObject;
use std::cmp::Ordering;
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR;
const DIRECTION_CHANGE_BONUS: f32 = 21.0;
const SKILL_MULTIPLIER: f32 = 900.0;
const STRAIN_DECAY_BASE: f32 = 0.2;
const DECAY_WEIGHT: f32 = 0.94;
pub(crate) struct Movement {
pub(crate) half_catcher_width: f32,
last_player_position: Option<f32>,
last_distance_moved: f32,
last_strain_time: f32,
current_strain: f32,
current_section_peak: f32,
strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
impl Movement {
#[inline]
pub(crate) fn new(cs: f32) -> Self {
let mut half_catcher_width = super::calculate_catch_width(cs) * 0.5;
half_catcher_width *= 1.0 - ((cs - 5.5).max(0.0) * 0.0625);
Self {
half_catcher_width,
last_player_position: None,
last_distance_moved: 0.0,
last_strain_time: 0.0,
current_strain: 1.0,
current_section_peak: 1.0,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
// TODO: Remove branching
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_time) = self.prev_time {
self.current_section_peak = self.peak_strain(time - prev_time);
}
}
pub(crate) fn process(&mut self, current: &DifficultyObject) {
self.current_strain *= strain_decay(current.delta);
self.current_strain += self.strain_value_of(&current) * SKILL_MULTIPLIER;
self.current_section_peak = self.current_strain.max(self.current_section_peak);
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
}
fn strain_value_of(&mut self, current: &DifficultyObject) -> f32 {
let last_player_pos = self
.last_player_position
.unwrap_or(current.last_normalized_pos);
let mut pos = last_player_pos
.max(current.normalized_pos - POSITION_EPSILON)
.min(current.normalized_pos + POSITION_EPSILON);
let dist_moved = pos - last_player_pos;
let weighted_strain_time = current.strain_time + 13.0 + (3.0 / current.clock_rate);
let mut dist_addition = dist_moved.abs().powf(1.3) / 510.0;
if dist_moved.abs() > 0.1 {
if self.last_distance_moved.abs() > 0.1
&& dist_moved.signum() != self.last_distance_moved.signum()
{
let bonus_factor = dist_moved.abs().min(50.0) / 50.0;
let anti_flow_factor = (self.last_distance_moved.abs().min(70.0) / 70.0).max(0.38);
dist_addition += DIRECTION_CHANGE_BONUS / (self.last_strain_time + 16.0).sqrt()
* bonus_factor
* anti_flow_factor
* (1.0 - (weighted_strain_time / 1000.0).powi(3)).max(0.0);
}
dist_addition += 12.5 * dist_moved.abs().min(NORMALIZED_HITOBJECT_RADIUS * 2.0)
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0)
/ weighted_strain_time.sqrt();
}
let mut edge_dash_bonus = 0.0;
if current.last.hyper_dist <= 20.0 {
if !current.last.hyper_dash {
edge_dash_bonus += 5.7;
} else {
pos = current.normalized_pos;
}
dist_addition *= 1.0
+ edge_dash_bonus
* ((20.0 - current.last.hyper_dist) / 20.0)
* ((current.strain_time * current.clock_rate).min(265.0) / 265.0).powf(1.5);
}
self.last_player_position.replace(pos);
self.last_distance_moved = dist_moved;
self.last_strain_time = current.strain_time;
dist_addition / weighted_strain_time
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * strain_decay(delta_time)
}
}
#[inline]
fn strain_decay(ms: f32) -> f32 {
STRAIN_DECAY_BASE.powf(ms / 1000.0)
}