f32 -> f64 & osu_precise fixes + update
This commit is contained in:
+2
-1
@@ -7,8 +7,9 @@
|
|||||||
Additionally, instead of importing through `rosu_pp::osu::{version}`, you now have to import through `rosu_pp::osu`
|
Additionally, instead of importing through `rosu_pp::osu::{version}`, you now have to import through `rosu_pp::osu`
|
||||||
- [BREAKING] Instead of returning `PpResult`, performance calculations now return `PerformanceAttributes` depending on the mode.
|
- [BREAKING] Instead of returning `PpResult`, performance calculations now return `PerformanceAttributes` depending on the mode.
|
||||||
- [BREAKING] Instead of returning `StarResult`, difficulty calculations now return `DifficultyAttributes` depending on the mode.
|
- [BREAKING] Instead of returning `StarResult`, difficulty calculations now return `DifficultyAttributes` depending on the mode.
|
||||||
|
- [BREAKING] Various fields and methods now include `f64` instead of `f32` to stay true to osu!'s original code
|
||||||
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
|
- added internal binary crate `pp-gen` to calculate difficulty & pp values via `PerformanceCalculator.dll`
|
||||||
- osu: Updated up to commit [6944151486e677bfd11f2390163aca9161defbbf](https://github.com/ppy/osu/commit/6944151486e677bfd11f2390163aca9161defbbf) (2021-10-27)
|
- osu: Updated up to commit [baa5285b59911efa1433a298f365133254a96874](https://github.com/ppy/osu/commit/baa5285b59911efa1433a298f365133254a96874) (2021-11-09)
|
||||||
|
|
||||||
# v0.2.3
|
# v0.2.3
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -229,22 +229,22 @@ impl Data {
|
|||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct GenericData {
|
struct GenericData {
|
||||||
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "Aim", skip_serializing_if = "Option::is_none")]
|
||||||
aim: Option<f32>,
|
aim: Option<f64>,
|
||||||
#[serde(default, alias = "Speed", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "Speed", skip_serializing_if = "Option::is_none")]
|
||||||
speed: Option<f32>,
|
speed: Option<f64>,
|
||||||
#[serde(default, alias = "Accuracy", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "Accuracy", skip_serializing_if = "Option::is_none")]
|
||||||
accuracy: Option<f32>,
|
accuracy: Option<f64>,
|
||||||
#[serde(default, alias = "Flashlight", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "Flashlight", skip_serializing_if = "Option::is_none")]
|
||||||
flashlight: Option<f32>,
|
flashlight: Option<f64>,
|
||||||
#[serde(default, alias = "Strain", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "Strain", skip_serializing_if = "Option::is_none")]
|
||||||
strain: Option<f32>,
|
strain: Option<f64>,
|
||||||
#[serde(default, alias = "OD", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "OD", skip_serializing_if = "Option::is_none")]
|
||||||
od: Option<f32>,
|
od: Option<f64>,
|
||||||
#[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")]
|
#[serde(default, alias = "AR", skip_serializing_if = "Option::is_none")]
|
||||||
ar: Option<f32>,
|
ar: Option<f64>,
|
||||||
#[serde(alias = "Mods")]
|
#[serde(alias = "Mods")]
|
||||||
mods: String,
|
mods: String,
|
||||||
#[serde(alias = "Stars")]
|
#[serde(alias = "Stars")]
|
||||||
stars: f32,
|
stars: f64,
|
||||||
pp: f32,
|
pp: f64,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ pub(crate) struct ControlPointIter<'p> {
|
|||||||
timing_points: Iter<'p, TimingPoint>,
|
timing_points: Iter<'p, TimingPoint>,
|
||||||
difficulty_points: Iter<'p, DifficultyPoint>,
|
difficulty_points: Iter<'p, DifficultyPoint>,
|
||||||
|
|
||||||
next_timing: Option<(f32, f32)>,
|
next_timing: Option<(f64, f64)>,
|
||||||
next_difficulty: Option<(f32, f32)>,
|
next_difficulty: Option<(f64, f64)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> ControlPointIter<'p> {
|
impl<'p> ControlPointIter<'p> {
|
||||||
@@ -39,20 +39,20 @@ impl<'p> ControlPointIter<'p> {
|
|||||||
|
|
||||||
pub(crate) enum ControlPoint {
|
pub(crate) enum ControlPoint {
|
||||||
Timing {
|
Timing {
|
||||||
time: f32,
|
time: f64,
|
||||||
#[allow(dead_code)] // not used in `osu_fast` feature
|
#[allow(dead_code)] // not used in `osu_fast` feature
|
||||||
beat_len: f32,
|
beat_len: f64,
|
||||||
},
|
},
|
||||||
Difficulty {
|
Difficulty {
|
||||||
time: f32,
|
time: f64,
|
||||||
slider_velocity: f32,
|
slider_velocity: f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "osu_precise", feature = "fruits"))]
|
#[cfg(any(feature = "osu_precise", feature = "fruits"))]
|
||||||
impl ControlPoint {
|
impl ControlPoint {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn time(&self) -> f32 {
|
pub(crate) fn time(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Timing { time, .. } => *time,
|
Self::Timing { time, .. } => *time,
|
||||||
Self::Difficulty { time, .. } => *time,
|
Self::Difficulty { time, .. } => *time,
|
||||||
@@ -70,13 +70,13 @@ impl<'p> Iterator for ControlPointIter<'p> {
|
|||||||
|
|
||||||
Some(ControlPoint::Timing { time, beat_len })
|
Some(ControlPoint::Timing { time, beat_len })
|
||||||
}
|
}
|
||||||
(_, Some((time, speed_mult))) => {
|
(_, Some((time, slider_velocity))) => {
|
||||||
self.next_difficulty =
|
self.next_difficulty =
|
||||||
next_tuple!(self.difficulty_points, (time, speed_multiplier));
|
next_tuple!(self.difficulty_points, (time, speed_multiplier));
|
||||||
|
|
||||||
Some(ControlPoint::Difficulty {
|
Some(ControlPoint::Difficulty {
|
||||||
time,
|
time,
|
||||||
slider_velocity: speed_mult,
|
slider_velocity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
(Some((time, beat_len)), None) => {
|
(Some((time, beat_len)), None) => {
|
||||||
|
|||||||
+41
-41
@@ -19,6 +19,7 @@ struct BezierBuffers {
|
|||||||
buf1: Vec<Pos2>,
|
buf1: Vec<Pos2>,
|
||||||
buf2: Vec<Pos2>,
|
buf2: Vec<Pos2>,
|
||||||
buf3: Vec<Pos2>,
|
buf3: Vec<Pos2>,
|
||||||
|
buf4: Vec<Pos2>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BezierBuffers {
|
impl BezierBuffers {
|
||||||
@@ -38,26 +39,28 @@ impl BezierBuffers {
|
|||||||
.extend(iter::repeat(Pos2::zero()).take(additional));
|
.extend(iter::repeat(Pos2::zero()).take(additional));
|
||||||
self.buf3
|
self.buf3
|
||||||
.extend(iter::repeat(Pos2::zero()).take(additional));
|
.extend(iter::repeat(Pos2::zero()).take(additional));
|
||||||
|
self.buf4
|
||||||
|
.extend(iter::repeat(Pos2::zero()).take(additional));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CircularArcProperties {
|
struct CircularArcProperties {
|
||||||
theta_start: f32,
|
theta_start: f64,
|
||||||
theta_range: f32,
|
theta_range: f64,
|
||||||
direction: f32,
|
direction: f64,
|
||||||
radius: f32,
|
radius: f32,
|
||||||
centre: Pos2,
|
centre: Pos2,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Curve {
|
pub(crate) struct Curve {
|
||||||
path: Vec<Pos2>,
|
path: Vec<Pos2>,
|
||||||
lengths: Vec<f32>,
|
lengths: Vec<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Curve {
|
impl Curve {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
points: &[PathControlPoint],
|
points: &[PathControlPoint],
|
||||||
expected_len: f32,
|
expected_len: f64,
|
||||||
bufs: &mut CurveBuffers,
|
bufs: &mut CurveBuffers,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut path = Self::calculate_path(points, bufs);
|
let mut path = Self::calculate_path(points, bufs);
|
||||||
@@ -66,28 +69,28 @@ impl Curve {
|
|||||||
Self { path, lengths }
|
Self { path, lengths }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn position_at(&self, progress: f32) -> Pos2 {
|
pub(crate) fn position_at(&self, progress: f64) -> Pos2 {
|
||||||
let d = self.progress_to_dist(progress);
|
let d = self.progress_to_dist(progress);
|
||||||
let i = self.idx_of_dist(d);
|
let i = self.idx_of_dist(d);
|
||||||
|
|
||||||
self.interpolate_vertices(i, d)
|
self.interpolate_vertices(i, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn progress_to_dist(&self, progress: f32) -> f32 {
|
fn progress_to_dist(&self, progress: f64) -> f64 {
|
||||||
progress.clamp(0.0, 1.0) * self.dist()
|
progress.clamp(0.0, 1.0) * self.dist()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn dist(&self) -> f32 {
|
pub(crate) fn dist(&self) -> f64 {
|
||||||
self.lengths.last().copied().unwrap_or(0.0)
|
self.lengths.last().copied().unwrap_or(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn idx_of_dist(&self, d: f32) -> usize {
|
fn idx_of_dist(&self, d: f64) -> usize {
|
||||||
self.lengths
|
self.lengths
|
||||||
.binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal))
|
.binary_search_by(|len| len.partial_cmp(&d).unwrap_or(Ordering::Equal))
|
||||||
.map_or_else(identity, identity)
|
.map_or_else(identity, identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interpolate_vertices(&self, i: usize, d: f32) -> Pos2 {
|
fn interpolate_vertices(&self, i: usize, d: f64) -> Pos2 {
|
||||||
if self.path.is_empty() {
|
if self.path.is_empty() {
|
||||||
return Pos2::zero();
|
return Pos2::zero();
|
||||||
}
|
}
|
||||||
@@ -107,13 +110,13 @@ impl Curve {
|
|||||||
|
|
||||||
// * Avoid division by an almost-zero number in case
|
// * Avoid division by an almost-zero number in case
|
||||||
// * two points are extremely close to each other
|
// * two points are extremely close to each other
|
||||||
if (d0 - d1).abs() <= f32::EPSILON {
|
if (d0 - d1).abs() <= f64::EPSILON {
|
||||||
return p0;
|
return p0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let w = (d - d0) / (d1 - d0);
|
let w = (d - d0) / (d1 - d0);
|
||||||
|
|
||||||
p0 + (p1 - p0) * w
|
p0 + (p1 - p0) * w as f32
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_path(points: &[PathControlPoint], bufs: &mut CurveBuffers) -> Vec<Pos2> {
|
fn calculate_path(points: &[PathControlPoint], bufs: &mut CurveBuffers) -> Vec<Pos2> {
|
||||||
@@ -152,18 +155,18 @@ impl Curve {
|
|||||||
fn calculate_length(
|
fn calculate_length(
|
||||||
points: &[PathControlPoint],
|
points: &[PathControlPoint],
|
||||||
path: &mut Vec<Pos2>,
|
path: &mut Vec<Pos2>,
|
||||||
expected_len: f32,
|
expected_len: f64,
|
||||||
) -> Vec<f32> {
|
) -> Vec<f64> {
|
||||||
let mut calculated_len = 0.0;
|
let mut calculated_len = 0.0;
|
||||||
let mut cumulative_len = vec![0.0];
|
let mut cumulative_len = vec![0.0];
|
||||||
|
|
||||||
for (&curr, &next) in path.iter().zip(path.iter().skip(1)) {
|
for (&curr, &next) in path.iter().zip(path.iter().skip(1)) {
|
||||||
let diff = next - curr;
|
let diff = next - curr;
|
||||||
calculated_len += diff.length();
|
calculated_len += diff.length() as f64;
|
||||||
cumulative_len.push(calculated_len);
|
cumulative_len.push(calculated_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expected_len - calculated_len).abs() > f32::EPSILON {
|
if (expected_len - calculated_len).abs() > f64::EPSILON {
|
||||||
// * In osu-stable, if the last two control points of a slider are equal, extension is not performed
|
// * In osu-stable, if the last two control points of a slider are equal, extension is not performed
|
||||||
let condition_opt = points
|
let condition_opt = points
|
||||||
.len()
|
.len()
|
||||||
@@ -207,7 +210,7 @@ impl Curve {
|
|||||||
// * The direction of the segment to shorten or lengthen
|
// * The direction of the segment to shorten or lengthen
|
||||||
let dir = (path[end_idx] - path[prev_idx]).normalize();
|
let dir = (path[end_idx] - path[prev_idx]).normalize();
|
||||||
|
|
||||||
path[end_idx] = path[prev_idx] + dir * (expected_len - cumulative_len[prev_idx]);
|
path[end_idx] = path[prev_idx] + dir * (expected_len - cumulative_len[prev_idx]) as f32;
|
||||||
cumulative_len.push(expected_len);
|
cumulative_len.push(expected_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,18 +289,22 @@ impl Curve {
|
|||||||
} else {
|
} else {
|
||||||
let divisor = 2.0 * (1.0 - CIRCULAR_ARC_TOLERANCE / pr.radius).acos();
|
let divisor = 2.0 * (1.0 - CIRCULAR_ARC_TOLERANCE / pr.radius).acos();
|
||||||
|
|
||||||
((pr.theta_range / divisor).ceil() as usize).max(2)
|
((pr.theta_range / divisor as f64).ceil() as usize).max(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
path.reserve_exact(amount_points);
|
path.reserve_exact(amount_points);
|
||||||
let divisor = (amount_points - 1) as f32;
|
let divisor = (amount_points - 1) as f64;
|
||||||
let directed_range = pr.direction * pr.theta_range;
|
let directed_range = pr.direction * pr.theta_range;
|
||||||
|
|
||||||
let subpath = (0..amount_points).map(|i| {
|
let subpath = (0..amount_points).map(|i| {
|
||||||
let fract = i as f32 / divisor;
|
let fract = i as f64 / divisor;
|
||||||
let theta = pr.theta_start + fract * directed_range;
|
let theta = pr.theta_start + fract * directed_range;
|
||||||
let (sin, cos) = theta.sin_cos();
|
let (sin, cos) = theta.sin_cos();
|
||||||
let origin = Pos2 { x: cos, y: sin };
|
|
||||||
|
let origin = Pos2 {
|
||||||
|
x: cos as f32,
|
||||||
|
y: sin as f32,
|
||||||
|
};
|
||||||
|
|
||||||
pr.centre + origin * pr.radius
|
pr.centre + origin * pr.radius
|
||||||
});
|
});
|
||||||
@@ -323,7 +330,7 @@ impl Curve {
|
|||||||
// * <a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a>
|
// * <a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a>
|
||||||
// * over the tree resulting from the subdivisions we make.)
|
// * over the tree resulting from the subdivisions we make.)
|
||||||
|
|
||||||
let mut left_child = bufs.buf2.to_owned();
|
// bufs.buf4 will serve as left_child
|
||||||
|
|
||||||
while let Some(mut parent) = to_flatten.pop() {
|
while let Some(mut parent) = to_flatten.pop() {
|
||||||
if Self::bezier_is_flat_enough(&parent) {
|
if Self::bezier_is_flat_enough(&parent) {
|
||||||
@@ -345,13 +352,13 @@ impl Curve {
|
|||||||
|
|
||||||
Self::bezier_subdivide(
|
Self::bezier_subdivide(
|
||||||
&parent,
|
&parent,
|
||||||
&mut left_child,
|
&mut bufs.buf4,
|
||||||
right_child.to_mut(),
|
right_child.to_mut(),
|
||||||
&mut bufs.buf1,
|
&mut bufs.buf1,
|
||||||
);
|
);
|
||||||
|
|
||||||
// * We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
|
// * We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
|
||||||
parent.to_mut().copy_from_slice(&left_child[..p]);
|
parent.to_mut().copy_from_slice(&bufs.buf4[..p]);
|
||||||
|
|
||||||
to_flatten.push(right_child);
|
to_flatten.push(right_child);
|
||||||
to_flatten.push(parent);
|
to_flatten.push(parent);
|
||||||
@@ -395,6 +402,7 @@ impl Curve {
|
|||||||
buf1: l,
|
buf1: l,
|
||||||
buf2: r,
|
buf2: r,
|
||||||
buf3: midpoints,
|
buf3: midpoints,
|
||||||
|
..
|
||||||
} = bufs;
|
} = bufs;
|
||||||
|
|
||||||
Self::bezier_subdivide(points, l, r, midpoints);
|
Self::bezier_subdivide(points, l, r, midpoints);
|
||||||
@@ -403,22 +411,13 @@ impl Curve {
|
|||||||
let l = &l[..count];
|
let l = &l[..count];
|
||||||
let r = &r[1..count];
|
let r = &r[1..count];
|
||||||
|
|
||||||
let left_iter = l
|
let subpath = l
|
||||||
.iter()
|
.iter()
|
||||||
|
.chain(r)
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.zip(l.iter().skip(2))
|
.zip(l.iter().chain(r).skip(2))
|
||||||
.zip(l.iter().skip(3))
|
.zip(l.iter().chain(r).skip(3))
|
||||||
.step_by(2);
|
.step_by(2)
|
||||||
|
|
||||||
let right_iter = r
|
|
||||||
.iter()
|
|
||||||
.skip(1)
|
|
||||||
.zip(r.iter().skip(2))
|
|
||||||
.zip(r.iter().skip(3))
|
|
||||||
.step_by(2);
|
|
||||||
|
|
||||||
let subpath = left_iter
|
|
||||||
.chain(right_iter)
|
|
||||||
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
|
.map(|((&prev, &curr), &next)| (prev + curr * 2.0 + next) * 0.25);
|
||||||
|
|
||||||
path.extend(subpath);
|
path.extend(subpath);
|
||||||
@@ -472,6 +471,7 @@ impl Curve {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2
|
||||||
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
|
let d = 2.0 * (a.x * (b - c).y + b.x * (c - a).y + c.x * (a - b).y);
|
||||||
let a_sq = a.length_squared();
|
let a_sq = a.length_squared();
|
||||||
let b_sq = b.length_squared();
|
let b_sq = b.length_squared();
|
||||||
@@ -479,7 +479,7 @@ impl Curve {
|
|||||||
|
|
||||||
let centre = Pos2 {
|
let centre = Pos2 {
|
||||||
x: (a_sq * (b - c).y + b_sq * (c - a).y + c_sq * (a - b).y) / d,
|
x: (a_sq * (b - c).y + b_sq * (c - a).y + c_sq * (a - b).y) / d,
|
||||||
y: ((c - b).x + b_sq * (a - c).x + c_sq * (b - a).x) / d,
|
y: (a_sq * (c - b).x + b_sq * (a - c).x + c_sq * (b - a).x) / d,
|
||||||
};
|
};
|
||||||
|
|
||||||
let d_a = a - centre;
|
let d_a = a - centre;
|
||||||
@@ -512,8 +512,8 @@ impl Curve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Some(CircularArcProperties {
|
Some(CircularArcProperties {
|
||||||
theta_start,
|
theta_start: theta_start as f64,
|
||||||
theta_range,
|
theta_range: theta_range as f64,
|
||||||
direction,
|
direction,
|
||||||
radius,
|
radius,
|
||||||
centre,
|
centre,
|
||||||
|
|||||||
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
use crate::parse::Pos2;
|
use crate::parse::Pos2;
|
||||||
|
|
||||||
const PLAYFIELD_WIDTH: f32 = 512.0;
|
const PLAYFIELD_WIDTH: f32 = 512.0;
|
||||||
const BASE_SPEED: f32 = 1.0;
|
const BASE_SPEED: f64 = 1.0;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CatchObject {
|
pub struct CatchObject {
|
||||||
pub(crate) pos: f32,
|
pub(crate) pos: f32,
|
||||||
pub(crate) time: f32,
|
pub(crate) time: f64,
|
||||||
|
|
||||||
pub(crate) hyper_dash: bool,
|
pub(crate) hyper_dash: bool,
|
||||||
pub(crate) hyper_dist: f32,
|
pub(crate) hyper_dist: f32,
|
||||||
@@ -14,7 +14,7 @@ pub struct CatchObject {
|
|||||||
|
|
||||||
impl CatchObject {
|
impl CatchObject {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new((pos, time): (Pos2, f32)) -> Self {
|
pub(crate) fn new((pos, time): (Pos2, f64)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pos: pos.x,
|
pos: pos.x,
|
||||||
time,
|
time,
|
||||||
@@ -23,7 +23,7 @@ impl CatchObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f32) -> Self {
|
pub(crate) fn with_hr(mut self, last_pos: &mut Option<f32>, last_time: &mut f64) -> Self {
|
||||||
let mut offset_pos = self.pos;
|
let mut offset_pos = self.pos;
|
||||||
let time_diff = self.time - *last_time;
|
let time_diff = self.time - *last_time;
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ impl CatchObject {
|
|||||||
let pos_diff = offset_pos - last_pos_ref;
|
let pos_diff = offset_pos - last_pos_ref;
|
||||||
|
|
||||||
if pos_diff.abs() > f32::EPSILON {
|
if pos_diff.abs() > f32::EPSILON {
|
||||||
if pos_diff.abs() < (time_diff / 3.0).floor() {
|
if pos_diff.abs() < (time_diff as f32 / 3.0).floor() {
|
||||||
if pos_diff > 0.0 {
|
if pos_diff > 0.0 {
|
||||||
if offset_pos + pos_diff < PLAYFIELD_WIDTH {
|
if offset_pos + pos_diff < PLAYFIELD_WIDTH {
|
||||||
offset_pos += pos_diff;
|
offset_pos += pos_diff;
|
||||||
@@ -56,10 +56,10 @@ impl CatchObject {
|
|||||||
|
|
||||||
pub(crate) fn init_hyper_dash(
|
pub(crate) fn init_hyper_dash(
|
||||||
&mut self,
|
&mut self,
|
||||||
half_catcher_width: f32,
|
half_catcher_width: f64,
|
||||||
next: &CatchObject,
|
next: &CatchObject,
|
||||||
last_direction: &mut i8,
|
last_direction: &mut i8,
|
||||||
last_excess: &mut f32,
|
last_excess: &mut f64,
|
||||||
) {
|
) {
|
||||||
let next_x = next.pos;
|
let next_x = next.pos;
|
||||||
let curr_x = self.pos;
|
let curr_x = self.pos;
|
||||||
@@ -73,15 +73,15 @@ impl CatchObject {
|
|||||||
half_catcher_width
|
half_catcher_width
|
||||||
};
|
};
|
||||||
|
|
||||||
let dist_to_next = (next_x - curr_x).abs() - sub;
|
let dist_to_next = (next_x - curr_x).abs() as f64 - sub;
|
||||||
let hyper_dist = time_to_next * BASE_SPEED - dist_to_next;
|
let hyper_dist = (time_to_next * BASE_SPEED - dist_to_next) as f32;
|
||||||
|
|
||||||
if hyper_dist < 0.0 {
|
if hyper_dist < 0.0 {
|
||||||
self.hyper_dash = true;
|
self.hyper_dash = true;
|
||||||
*last_excess = half_catcher_width;
|
*last_excess = half_catcher_width;
|
||||||
} else {
|
} else {
|
||||||
self.hyper_dist = hyper_dist;
|
self.hyper_dist = hyper_dist;
|
||||||
*last_excess = hyper_dist.max(0.0).min(half_catcher_width);
|
*last_excess = (hyper_dist as f64).max(0.0).min(half_catcher_width);
|
||||||
}
|
}
|
||||||
|
|
||||||
*last_direction = this_direction;
|
*last_direction = this_direction;
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ pub(crate) struct DifficultyObject<'o> {
|
|||||||
pub(crate) base: &'o CatchObject,
|
pub(crate) base: &'o CatchObject,
|
||||||
pub(crate) last: &'o CatchObject,
|
pub(crate) last: &'o CatchObject,
|
||||||
|
|
||||||
pub(crate) delta: f32,
|
pub(crate) delta: f64,
|
||||||
pub(crate) start_time: f32,
|
pub(crate) start_time: f64,
|
||||||
|
|
||||||
pub(crate) normalized_pos: f32,
|
pub(crate) normalized_pos: f32,
|
||||||
pub(crate) last_normalized_pos: f32,
|
pub(crate) last_normalized_pos: f32,
|
||||||
|
|
||||||
pub(crate) strain_time: f32,
|
pub(crate) strain_time: f64,
|
||||||
pub(crate) clock_rate: f32,
|
pub(crate) clock_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'o> DifficultyObject<'o> {
|
impl<'o> DifficultyObject<'o> {
|
||||||
@@ -22,7 +22,7 @@ impl<'o> DifficultyObject<'o> {
|
|||||||
base: &'o CatchObject,
|
base: &'o CatchObject,
|
||||||
last: &'o CatchObject,
|
last: &'o CatchObject,
|
||||||
half_catcher_width: f32,
|
half_catcher_width: f32,
|
||||||
clock_rate: f32,
|
clock_rate: f64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let delta = (base.time - last.time) / clock_rate;
|
let delta = (base.time - last.time) / clock_rate;
|
||||||
let start_time = base.time / clock_rate;
|
let start_time = base.time / clock_rate;
|
||||||
|
|||||||
+33
-31
@@ -18,14 +18,14 @@ use crate::{
|
|||||||
Beatmap, Mods, Strains,
|
Beatmap, Mods, Strains,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SECTION_LENGTH: f32 = 750.0;
|
const SECTION_LENGTH: f64 = 750.0;
|
||||||
const STAR_SCALING_FACTOR: f32 = 0.153;
|
const STAR_SCALING_FACTOR: f64 = 0.153;
|
||||||
|
|
||||||
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
||||||
const CATCHER_SIZE: f32 = 106.75;
|
const CATCHER_SIZE: f32 = 106.75;
|
||||||
|
|
||||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
||||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||||
|
|
||||||
/// Star calculation for osu!ctb maps
|
/// Star calculation for osu!ctb maps
|
||||||
///
|
///
|
||||||
@@ -79,7 +79,7 @@ pub fn stars(
|
|||||||
// Responsible for timing point values
|
// Responsible for timing point values
|
||||||
slider_state.update(h.start_time);
|
slider_state.update(h.start_time);
|
||||||
|
|
||||||
let span_count = (*repeats + 1) as f32;
|
let span_count = (*repeats + 1) as f64;
|
||||||
|
|
||||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||||
|
|
||||||
@@ -143,15 +143,15 @@ pub fn stars(
|
|||||||
slider_objects.extend(&ticks);
|
slider_objects.extend(&ticks);
|
||||||
|
|
||||||
for span_idx in 1..=*repeats {
|
for span_idx in 1..=*repeats {
|
||||||
let progress = (span_idx % 2 == 1) as u8 as f32;
|
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||||
let pos = h.pos + curve.position_at(progress);
|
let pos = h.pos + curve.position_at(progress);
|
||||||
let time_offset = span_duration * span_idx as f32;
|
let time_offset = span_duration * span_idx as f64;
|
||||||
|
|
||||||
// Reverse tick
|
// Reverse tick
|
||||||
slider_objects.push((pos, h.start_time + time_offset));
|
slider_objects.push((pos, h.start_time + time_offset));
|
||||||
|
|
||||||
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
||||||
(*pos, *time + time_offset + time_add * i as f32)
|
(*pos, *time + time_offset + time_add * i as f64)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actual ticks
|
// Actual ticks
|
||||||
@@ -166,7 +166,7 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Slider tail
|
// Slider tail
|
||||||
let progress = (*repeats % 2 == 0) as u8 as f32;
|
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||||
let pos = h.pos + curve.position_at(progress);
|
let pos = h.pos + curve.position_at(progress);
|
||||||
slider_objects.push((pos, h.start_time + duration));
|
slider_objects.push((pos, h.start_time + duration));
|
||||||
|
|
||||||
@@ -185,12 +185,13 @@ pub fn stars(
|
|||||||
.take(take);
|
.take(take);
|
||||||
|
|
||||||
// Hyper dash business
|
// Hyper dash business
|
||||||
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
|
let half_catcher_width =
|
||||||
|
(calculate_catch_width(attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||||
let mut last_direction = 0;
|
let mut last_direction = 0;
|
||||||
let mut last_excess = half_catcher_width;
|
let mut last_excess = half_catcher_width;
|
||||||
|
|
||||||
// Strain business
|
// Strain business
|
||||||
let mut movement = Movement::new(attributes.cs);
|
let mut movement = Movement::new(attributes.cs as f32);
|
||||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||||
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;
|
||||||
@@ -329,7 +330,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
// Responsible for timing point values
|
// Responsible for timing point values
|
||||||
slider_state.update(h.start_time);
|
slider_state.update(h.start_time);
|
||||||
|
|
||||||
let span_count = (*repeats + 1) as f32;
|
let span_count = (*repeats + 1) as f64;
|
||||||
|
|
||||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||||
|
|
||||||
@@ -385,15 +386,15 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
slider_objects.extend(&ticks);
|
slider_objects.extend(&ticks);
|
||||||
|
|
||||||
for span_idx in 1..=*repeats {
|
for span_idx in 1..=*repeats {
|
||||||
let progress = (span_idx % 2 == 1) as u8 as f32;
|
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||||
let pos = h.pos + curve.position_at(progress);
|
let pos = h.pos + curve.position_at(progress);
|
||||||
let time_offset = span_duration * span_idx as f32;
|
let time_offset = span_duration * span_idx as f64;
|
||||||
|
|
||||||
// Reverse tick
|
// Reverse tick
|
||||||
slider_objects.push((pos, h.start_time + time_offset));
|
slider_objects.push((pos, h.start_time + time_offset));
|
||||||
|
|
||||||
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
let new_ticks = ticks.iter().enumerate().map(|(i, (pos, time))| {
|
||||||
(*pos, *time + time_offset + time_add * i as f32)
|
(*pos, *time + time_offset + time_add * i as f64)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actual ticks
|
// Actual ticks
|
||||||
@@ -408,7 +409,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Slider tail
|
// Slider tail
|
||||||
let progress = (*repeats % 2 == 0) as u8 as f32;
|
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||||
let pos = h.pos + curve.position_at(progress);
|
let pos = h.pos + curve.position_at(progress);
|
||||||
slider_objects.push((pos, h.start_time + duration));
|
slider_objects.push((pos, h.start_time + duration));
|
||||||
|
|
||||||
@@ -422,12 +423,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
// Hyper dash business
|
// Hyper dash business
|
||||||
let half_catcher_width = calculate_catch_width(attributes.cs) / 2.0 / ALLOWED_CATCH_RANGE;
|
let half_catcher_width =
|
||||||
|
(calculate_catch_width(attributes.cs as f32) / 2.0 / ALLOWED_CATCH_RANGE) as f64;
|
||||||
let mut last_direction = 0;
|
let mut last_direction = 0;
|
||||||
let mut last_excess = half_catcher_width;
|
let mut last_excess = half_catcher_width;
|
||||||
|
|
||||||
// Strain business
|
// Strain business
|
||||||
let mut movement = Movement::new(attributes.cs);
|
let mut movement = Movement::new(attributes.cs as f32);
|
||||||
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
let section_len = SECTION_LENGTH * attributes.clock_rate;
|
||||||
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;
|
||||||
@@ -522,11 +524,11 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
// BUG: Sometimes there are off-by-one errors,
|
// BUG: Sometimes there are off-by-one errors,
|
||||||
// presumably caused by floating point inaccuracies
|
// presumably caused by floating point inaccuracies
|
||||||
fn tiny_droplet_count(
|
fn tiny_droplet_count(
|
||||||
start_time: f32,
|
start_time: f64,
|
||||||
time_between_ticks: f32,
|
time_between_ticks: f64,
|
||||||
duration: f32,
|
duration: f64,
|
||||||
span_count: usize,
|
span_count: usize,
|
||||||
ticks: &[(Pos2, f32)],
|
ticks: &[(Pos2, f64)],
|
||||||
) -> usize {
|
) -> usize {
|
||||||
// tiny droplets preceeding a _tick_
|
// tiny droplets preceeding a _tick_
|
||||||
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
|
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
|
||||||
@@ -542,7 +544,7 @@ fn tiny_droplet_count(
|
|||||||
|
|
||||||
// tiny droplets preceeding a _reverse_
|
// tiny droplets preceeding a _reverse_
|
||||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||||
let repeat_time = start_time + duration / span_count as f32;
|
let repeat_time = start_time + duration / span_count as f64;
|
||||||
let since_last_tick = repeat_time - last;
|
let since_last_tick = repeat_time - last;
|
||||||
|
|
||||||
let span_last_section = if since_last_tick > 80.0 {
|
let span_last_section = if since_last_tick > 80.0 {
|
||||||
@@ -556,7 +558,7 @@ fn tiny_droplet_count(
|
|||||||
// tiny droplets preceeding the slider tail
|
// tiny droplets preceeding the slider tail
|
||||||
// necessary to handle distinctly because of the legacy last tick
|
// necessary to handle distinctly because of the legacy last tick
|
||||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||||
let end_time = start_time + duration / span_count as f32 - LEGACY_LAST_TICK_OFFSET;
|
let end_time = start_time + duration / span_count as f64 - LEGACY_LAST_TICK_OFFSET;
|
||||||
let since_last_tick = end_time - last;
|
let since_last_tick = end_time - last;
|
||||||
|
|
||||||
let last_section = if since_last_tick > 80.0 {
|
let last_section = if since_last_tick > 80.0 {
|
||||||
@@ -574,7 +576,7 @@ fn tiny_droplet_count(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn shrink_down(mut val: f32) -> f32 {
|
fn shrink_down(mut val: f64) -> f64 {
|
||||||
while val > 100.0 {
|
while val > 100.0 {
|
||||||
val /= 2.0;
|
val /= 2.0;
|
||||||
}
|
}
|
||||||
@@ -583,7 +585,7 @@ fn shrink_down(mut val: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn count_iterations(mut start: f32, step: f32, end: f32) -> usize {
|
fn count_iterations(mut start: f64, step: f64, end: f64) -> usize {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
while start < end {
|
while start < end {
|
||||||
@@ -631,9 +633,9 @@ impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
|
|||||||
/// This data is necessary to calculate PP.
|
/// This data is necessary to calculate PP.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct DifficultyAttributes {
|
pub struct DifficultyAttributes {
|
||||||
pub stars: f32,
|
pub stars: f64,
|
||||||
pub max_combo: usize,
|
pub max_combo: usize,
|
||||||
pub ar: f32,
|
pub ar: f64,
|
||||||
pub n_fruits: usize,
|
pub n_fruits: usize,
|
||||||
pub n_droplets: usize,
|
pub n_droplets: usize,
|
||||||
pub n_tiny_droplets: usize,
|
pub n_tiny_droplets: usize,
|
||||||
@@ -643,19 +645,19 @@ pub struct DifficultyAttributes {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct PerformanceAttributes {
|
pub struct PerformanceAttributes {
|
||||||
pub attributes: DifficultyAttributes,
|
pub attributes: DifficultyAttributes,
|
||||||
pub pp: f32,
|
pub pp: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerformanceAttributes {
|
impl PerformanceAttributes {
|
||||||
/// Return the star value.
|
/// Return the star value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f64 {
|
||||||
self.attributes.stars
|
self.attributes.stars
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the performance point value.
|
/// Return the performance point value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pp(&self) -> f32 {
|
pub fn pp(&self) -> f64 {
|
||||||
self.pp
|
self.pp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-20
@@ -5,23 +5,23 @@ use std::cmp::Ordering;
|
|||||||
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
|
const ABSOLUTE_PLAYER_POSITIONING_ERROR: f32 = 16.0;
|
||||||
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
|
const NORMALIZED_HITOBJECT_RADIUS: f32 = 41.0;
|
||||||
const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR;
|
const POSITION_EPSILON: f32 = NORMALIZED_HITOBJECT_RADIUS - ABSOLUTE_PLAYER_POSITIONING_ERROR;
|
||||||
const DIRECTION_CHANGE_BONUS: f32 = 21.0;
|
const DIRECTION_CHANGE_BONUS: f64 = 21.0;
|
||||||
const SKILL_MULTIPLIER: f32 = 900.0;
|
const SKILL_MULTIPLIER: f64 = 900.0;
|
||||||
const STRAIN_DECAY_BASE: f32 = 0.2;
|
const STRAIN_DECAY_BASE: f64 = 0.2;
|
||||||
const DECAY_WEIGHT: f32 = 0.94;
|
const DECAY_WEIGHT: f64 = 0.94;
|
||||||
|
|
||||||
pub(crate) struct Movement {
|
pub(crate) struct Movement {
|
||||||
pub(crate) half_catcher_width: f32,
|
pub(crate) half_catcher_width: f32,
|
||||||
|
|
||||||
last_player_position: Option<f32>,
|
last_player_position: Option<f32>,
|
||||||
last_distance_moved: f32,
|
last_distance_moved: f32,
|
||||||
last_strain_time: f32,
|
last_strain_time: f64,
|
||||||
|
|
||||||
current_strain: f32,
|
current_strain: f64,
|
||||||
current_section_peak: f32,
|
current_section_peak: f64,
|
||||||
|
|
||||||
pub(crate) strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Movement {
|
impl Movement {
|
||||||
@@ -51,7 +51,7 @@ impl Movement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ impl Movement {
|
|||||||
self.prev_time.replace(current.start_time);
|
self.prev_time.replace(current.start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ impl Movement {
|
|||||||
difficulty
|
difficulty
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strain_value_of(&mut self, current: &DifficultyObject<'_>) -> f32 {
|
fn strain_value_of(&mut self, current: &DifficultyObject<'_>) -> f64 {
|
||||||
let last_player_pos = self
|
let last_player_pos = self
|
||||||
.last_player_position
|
.last_player_position
|
||||||
.unwrap_or(current.last_normalized_pos);
|
.unwrap_or(current.last_normalized_pos);
|
||||||
@@ -89,14 +89,15 @@ impl Movement {
|
|||||||
let dist_moved = pos - last_player_pos;
|
let dist_moved = pos - last_player_pos;
|
||||||
let weighted_strain_time = current.strain_time + 13.0 + (3.0 / current.clock_rate);
|
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;
|
let mut dist_addition = (dist_moved.abs().powf(1.3) / 510.0) as f64;
|
||||||
|
|
||||||
if dist_moved.abs() > 0.1 {
|
if dist_moved.abs() > 0.1 {
|
||||||
if self.last_distance_moved.abs() > 0.1
|
if self.last_distance_moved.abs() > 0.1
|
||||||
&& dist_moved.signum() != self.last_distance_moved.signum()
|
&& dist_moved.signum() != self.last_distance_moved.signum()
|
||||||
{
|
{
|
||||||
let bonus_factor = dist_moved.abs().min(50.0) / 50.0;
|
let bonus_factor = (dist_moved.abs().min(50.0) / 50.0) as f64;
|
||||||
let anti_flow_factor = (self.last_distance_moved.abs().min(70.0) / 70.0).max(0.38);
|
let anti_flow_factor =
|
||||||
|
(self.last_distance_moved.abs().min(70.0) / 70.0).max(0.38) as f64;
|
||||||
|
|
||||||
dist_addition += DIRECTION_CHANGE_BONUS / (self.last_strain_time + 16.0).sqrt()
|
dist_addition += DIRECTION_CHANGE_BONUS / (self.last_strain_time + 16.0).sqrt()
|
||||||
* bonus_factor
|
* bonus_factor
|
||||||
@@ -104,8 +105,8 @@ impl Movement {
|
|||||||
* (1.0 - (weighted_strain_time / 1000.0).powi(3)).max(0.0);
|
* (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)
|
dist_addition += (12.5 * dist_moved.abs().min(NORMALIZED_HITOBJECT_RADIUS * 2.0)
|
||||||
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0)
|
/ (NORMALIZED_HITOBJECT_RADIUS * 6.0)) as f64
|
||||||
/ weighted_strain_time.sqrt();
|
/ weighted_strain_time.sqrt();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ impl Movement {
|
|||||||
|
|
||||||
dist_addition *= 1.0
|
dist_addition *= 1.0
|
||||||
+ edge_dash_bonus
|
+ edge_dash_bonus
|
||||||
* ((20.0 - current.last.hyper_dist) / 20.0)
|
* ((20.0 - current.last.hyper_dist) / 20.0) as f64
|
||||||
* ((current.strain_time * current.clock_rate).min(265.0) / 265.0).powf(1.5);
|
* ((current.strain_time * current.clock_rate).min(265.0) / 265.0).powf(1.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,12 +133,12 @@ impl Movement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
fn peak_strain(&self, delta_time: f64) -> f64 {
|
||||||
self.current_strain * strain_decay(delta_time)
|
self.current_strain * strain_decay(delta_time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strain_decay(ms: f32) -> f32 {
|
fn strain_decay(ms: f64) -> f64 {
|
||||||
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -145,7 +145,7 @@ impl<'m> FruitsPP<'m> {
|
|||||||
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
/// Generate the hit results with respect to the given accuracy between `0` and `100`.
|
||||||
///
|
///
|
||||||
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
/// Be sure to set `misses` beforehand! Also, if available, set `attributes` beforehand.
|
||||||
pub fn accuracy(mut self, mut acc: f32) -> Self {
|
pub fn accuracy(mut self, mut acc: f64) -> Self {
|
||||||
if self.attributes.is_none() {
|
if self.attributes.is_none() {
|
||||||
self.attributes = Some(stars(self.map, self.mods, self.passed_objects));
|
self.attributes = Some(stars(self.map, self.mods, self.passed_objects));
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ impl<'m> FruitsPP<'m> {
|
|||||||
acc /= 100.0;
|
acc /= 100.0;
|
||||||
|
|
||||||
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
||||||
((acc * (attributes.max_combo + max_tiny_droplets) as f32).round() as usize)
|
((acc * (attributes.max_combo + max_tiny_droplets) as f64).round() as usize)
|
||||||
.saturating_sub(n_fruits)
|
.saturating_sub(n_fruits)
|
||||||
.saturating_sub(n_droplets)
|
.saturating_sub(n_droplets)
|
||||||
});
|
});
|
||||||
@@ -290,17 +290,17 @@ impl FruitsPPInner {
|
|||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
let len_bonus = 0.95
|
let len_bonus = 0.95
|
||||||
+ 0.3 * (combo_hits as f32 / 2500.0).min(1.0)
|
+ 0.3 * (combo_hits as f64 / 2500.0).min(1.0)
|
||||||
+ (combo_hits > 2500) as u8 as f32 * (combo_hits as f32 / 2500.0).log10() * 0.475;
|
+ (combo_hits > 2500) as u8 as f64 * (combo_hits as f64 / 2500.0).log10() * 0.475;
|
||||||
|
|
||||||
pp *= len_bonus;
|
pp *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses exponentially
|
// Penalize misses exponentially
|
||||||
pp *= 0.97_f32.powi(self.n_misses as i32);
|
pp *= 0.97_f64.powi(self.n_misses as i32);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||||
pp *= (combo as f32 / attributes.max_combo as f32)
|
pp *= (combo as f64 / attributes.max_combo as f64)
|
||||||
.powf(0.8)
|
.powf(0.8)
|
||||||
.min(1.0);
|
.min(1.0);
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ impl FruitsPPInner {
|
|||||||
let ar = attributes.ar;
|
let ar = attributes.ar;
|
||||||
let mut ar_factor = 1.0;
|
let mut ar_factor = 1.0;
|
||||||
if ar > 9.0 {
|
if ar > 9.0 {
|
||||||
ar_factor += 0.1 * (ar - 9.0) + (ar > 10.0) as u8 as f32 * 0.1 * (ar - 10.0);
|
ar_factor += 0.1 * (ar - 9.0) + (ar > 10.0) as u8 as f64 * 0.1 * (ar - 10.0);
|
||||||
} else if ar < 8.0 {
|
} else if ar < 8.0 {
|
||||||
ar_factor += 0.025 * (8.0 - ar);
|
ar_factor += 0.025 * (8.0 - ar);
|
||||||
}
|
}
|
||||||
@@ -359,13 +359,13 @@ impl FruitsPPInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn acc(&self) -> f32 {
|
fn acc(&self) -> f64 {
|
||||||
let total_hits = self.total_hits();
|
let total_hits = self.total_hits();
|
||||||
|
|
||||||
if total_hits == 0 {
|
if total_hits == 0 {
|
||||||
1.0
|
1.0
|
||||||
} else {
|
} else {
|
||||||
(self.successful_hits() as f32 / total_hits as f32)
|
(self.successful_hits() as f64 / total_hits as f64)
|
||||||
.max(0.0)
|
.max(0.0)
|
||||||
.min(1.0)
|
.min(1.0)
|
||||||
}
|
}
|
||||||
@@ -447,7 +447,7 @@ mod test {
|
|||||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||||
let denominator =
|
let denominator =
|
||||||
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
||||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(target_acc - acc).abs() < 1.0,
|
(target_acc - acc).abs() < 1.0,
|
||||||
@@ -487,7 +487,7 @@ mod test {
|
|||||||
+ calculator.n_tiny_droplets.unwrap_or(0);
|
+ calculator.n_tiny_droplets.unwrap_or(0);
|
||||||
let denominator =
|
let denominator =
|
||||||
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
numerator + calculator.n_tiny_droplet_misses.unwrap_or(0) + calculator.n_misses;
|
||||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(target_acc - acc).abs() < 1.0,
|
(target_acc - acc).abs() < 1.0,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use crate::{Beatmap, ControlPoint, ControlPointIter};
|
|||||||
pub(crate) struct SliderState<'p> {
|
pub(crate) struct SliderState<'p> {
|
||||||
control_points: ControlPointIter<'p>,
|
control_points: ControlPointIter<'p>,
|
||||||
next: Option<ControlPoint>,
|
next: Option<ControlPoint>,
|
||||||
pub(crate) beat_len: f32,
|
pub(crate) beat_len: f64,
|
||||||
pub(crate) slider_velocity: f32,
|
pub(crate) slider_velocity: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> SliderState<'p> {
|
impl<'p> SliderState<'p> {
|
||||||
@@ -29,7 +29,7 @@ impl<'p> SliderState<'p> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn update(&mut self, time: f32) {
|
pub(crate) fn update(&mut self, time: f64) {
|
||||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||||
match next {
|
match next {
|
||||||
ControlPoint::Timing { beat_len, .. } => {
|
ControlPoint::Timing { beat_len, .. } => {
|
||||||
@@ -88,14 +88,14 @@ mod test {
|
|||||||
let mut state = SliderState::new(&map);
|
let mut state = SliderState::new(&map);
|
||||||
|
|
||||||
state.update(2.0);
|
state.update(2.0);
|
||||||
assert!((state.beat_len - 10.0).abs() <= f32::EPSILON);
|
assert!((state.beat_len - 10.0).abs() <= f64::EPSILON);
|
||||||
|
|
||||||
state.update(3.0);
|
state.update(3.0);
|
||||||
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON);
|
assert!((state.beat_len - 20.0).abs() <= f64::EPSILON);
|
||||||
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON);
|
assert!((state.slider_velocity - 1.0).abs() <= f64::EPSILON);
|
||||||
|
|
||||||
state.update(5.0);
|
state.update(5.0);
|
||||||
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON);
|
assert!((state.beat_len - 30.0).abs() <= f64::EPSILON);
|
||||||
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON);
|
assert!((state.slider_velocity - 45.0).abs() <= f64::EPSILON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -298,8 +298,8 @@ impl BeatmapExt for Beatmap {
|
|||||||
/// `section_length` is the time in ms inbetween two strains.
|
/// `section_length` is the time in ms inbetween two strains.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Strains {
|
pub struct Strains {
|
||||||
pub section_length: f32,
|
pub section_length: f64,
|
||||||
pub strains: Vec<f32>,
|
pub strains: Vec<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic enum containing the result of a star calculation based on the mode.
|
/// Basic enum containing the result of a star calculation based on the mode.
|
||||||
@@ -318,7 +318,7 @@ pub enum StarResult {
|
|||||||
impl StarResult {
|
impl StarResult {
|
||||||
/// The final star value.
|
/// The final star value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(attributes) => attributes.stars,
|
Self::Fruits(attributes) => attributes.stars,
|
||||||
@@ -348,7 +348,7 @@ pub enum PpResult {
|
|||||||
impl PpResult {
|
impl PpResult {
|
||||||
/// The final pp value.
|
/// The final pp value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pp(&self) -> f32 {
|
pub fn pp(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(attributes) => attributes.pp,
|
Self::Fruits(attributes) => attributes.pp,
|
||||||
@@ -363,7 +363,7 @@ impl PpResult {
|
|||||||
|
|
||||||
/// The final star value.
|
/// The final star value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(attributes) => attributes.stars(),
|
Self::Fruits(attributes) => attributes.stars(),
|
||||||
@@ -379,7 +379,7 @@ impl PpResult {
|
|||||||
|
|
||||||
#[cfg(any(feature = "osu", feature = "taiko"))]
|
#[cfg(any(feature = "osu", feature = "taiko"))]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn difficulty_range(val: f32, max: f32, avg: f32, min: f32) -> f32 {
|
fn difficulty_range(val: f64, max: f64, avg: f64, min: f64) -> f64 {
|
||||||
if val > 5.0 {
|
if val > 5.0 {
|
||||||
avg + (max - avg) * (val - 5.0) / 5.0
|
avg + (max - avg) * (val - 5.0) / 5.0
|
||||||
} else if val < 5.0 {
|
} else if val < 5.0 {
|
||||||
|
|||||||
+11
-11
@@ -8,8 +8,8 @@ use strain::Strain;
|
|||||||
|
|
||||||
use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
|
use crate::{parse::HitObject, Beatmap, GameMode, Mods, Strains};
|
||||||
|
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f64 = 400.0;
|
||||||
const STAR_SCALING_FACTOR: f32 = 0.018;
|
const STAR_SCALING_FACTOR: f64 = 0.018;
|
||||||
|
|
||||||
/// Star calculation for osu!mania maps
|
/// Star calculation for osu!mania maps
|
||||||
///
|
///
|
||||||
@@ -150,13 +150,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
pub(crate) struct DifficultyHitObject<'o> {
|
pub(crate) struct DifficultyHitObject<'o> {
|
||||||
base: &'o HitObject,
|
base: &'o HitObject,
|
||||||
column: usize,
|
column: usize,
|
||||||
delta: f32,
|
delta: f64,
|
||||||
start_time: f32,
|
start_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'o> DifficultyHitObject<'o> {
|
impl<'o> DifficultyHitObject<'o> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f32) -> Self {
|
fn new(base: &'o HitObject, prev: &'o HitObject, columns: f32, clock_rate: f64) -> Self {
|
||||||
let x_divisor = 512.0 / columns;
|
let x_divisor = 512.0 / columns;
|
||||||
let column = (base.pos.x / x_divisor).floor().min(columns - 1.0) as usize;
|
let column = (base.pos.x / x_divisor).floor().min(columns - 1.0) as usize;
|
||||||
|
|
||||||
@@ -173,28 +173,28 @@ impl<'o> DifficultyHitObject<'o> {
|
|||||||
/// This data is necessary to calculate PP.
|
/// This data is necessary to calculate PP.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
pub struct DifficultyAttributes {
|
pub struct DifficultyAttributes {
|
||||||
pub stars: f32,
|
pub stars: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Various data created through the pp calculation.
|
/// Various data created through the pp calculation.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
pub struct PerformanceAttributes {
|
pub struct PerformanceAttributes {
|
||||||
pub attributes: DifficultyAttributes,
|
pub attributes: DifficultyAttributes,
|
||||||
pub pp_acc: f32,
|
pub pp_acc: f64,
|
||||||
pub pp_strain: f32,
|
pub pp_strain: f64,
|
||||||
pub pp: f32,
|
pub pp: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerformanceAttributes {
|
impl PerformanceAttributes {
|
||||||
/// Return the star value.
|
/// Return the star value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f64 {
|
||||||
self.attributes.stars
|
self.attributes.stars
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the performance point value.
|
/// Return the performance point value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pp(&self) -> f32 {
|
pub fn pp(&self) -> f64 {
|
||||||
self.pp
|
self.pp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -30,9 +30,9 @@ use crate::{Beatmap, Mods, PpResult, StarResult};
|
|||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct ManiaPP<'m> {
|
pub struct ManiaPP<'m> {
|
||||||
map: &'m Beatmap,
|
map: &'m Beatmap,
|
||||||
stars: Option<f32>,
|
stars: Option<f64>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
score: Option<f32>,
|
score: Option<f64>,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ impl<'m> ManiaPP<'m> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn attributes(mut self, attributes: impl ManiaAttributeProvider) -> Self {
|
pub fn attributes(mut self, attributes: impl ManiaAttributeProvider) -> Self {
|
||||||
if let Some(stars) = attributes.attributes() {
|
if let Some(stars) = attributes.attributes() {
|
||||||
self.stars.replace(stars);
|
self.stars = Some(stars);
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -76,7 +76,7 @@ impl<'m> ManiaPP<'m> {
|
|||||||
/// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
|
/// On `NoMod` its between 0 and 1,000,000, on `Easy` between 0 and 500,000, etc.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn score(mut self, score: u32) -> Self {
|
pub fn score(mut self, score: u32) -> Self {
|
||||||
self.score.replace(score as f32);
|
self.score = Some(score as f64);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -100,17 +100,17 @@ impl<'m> ManiaPP<'m> {
|
|||||||
let ht = self.mods.ht();
|
let ht = self.mods.ht();
|
||||||
|
|
||||||
let mut scaled_score = self.score.map_or(1_000_000.0, |score| {
|
let mut scaled_score = self.score.map_or(1_000_000.0, |score| {
|
||||||
score / 0.5_f32.powi(ez as i32 + nf as i32 + ht as i32)
|
score / 0.5_f64.powi(ez as i32 + nf as i32 + ht as i32)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(passed_objects) = self.passed_objects {
|
if let Some(passed_objects) = self.passed_objects {
|
||||||
let percent_passed =
|
let percent_passed =
|
||||||
passed_objects as f32 / (self.map.n_circles + self.map.n_sliders) as f32;
|
passed_objects as f64 / (self.map.n_circles + self.map.n_sliders) as f64;
|
||||||
|
|
||||||
scaled_score /= percent_passed;
|
scaled_score /= percent_passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut od = 34.0 + 3.0 * (10.0 - self.map.od).max(0.0).min(10.0);
|
let mut od = 34.0 + 3.0 * (10.0 - self.map.od as f64).max(0.0).min(10.0);
|
||||||
let clock_rate = self.mods.speed();
|
let clock_rate = self.mods.speed();
|
||||||
|
|
||||||
let mut multiplier = 0.8;
|
let mut multiplier = 0.8;
|
||||||
@@ -139,10 +139,10 @@ impl<'m> ManiaPP<'m> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_strain(&self, score: f32, stars: f32) -> f32 {
|
fn compute_strain(&self, score: f64, stars: f64) -> f64 {
|
||||||
let mut strain_value = (5.0 * (stars / 0.2).max(1.0) - 4.0).powf(2.2) / 135.0;
|
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);
|
strain_value *= 1.0 + 0.1 * (self.map.hit_objects.len() as f64 / 1500.0).min(1.0);
|
||||||
|
|
||||||
if score <= 500_000.0 {
|
if score <= 500_000.0 {
|
||||||
strain_value = 0.0;
|
strain_value = 0.0;
|
||||||
@@ -162,7 +162,7 @@ impl<'m> ManiaPP<'m> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn compute_accuracy_value(&self, score: f32, strain: f32, hit_window: f32) -> f32 {
|
fn compute_accuracy_value(&self, score: f64, strain: f64, hit_window: f64) -> f64 {
|
||||||
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
|
(0.2 - (hit_window - 34.0) * 0.006667).max(0.0)
|
||||||
* strain
|
* strain
|
||||||
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
|
* ((score - 960_000.0).max(0.0) / 40_000.0).powf(1.1)
|
||||||
@@ -170,33 +170,33 @@ impl<'m> ManiaPP<'m> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait ManiaAttributeProvider {
|
pub trait ManiaAttributeProvider {
|
||||||
fn attributes(self) -> Option<f32>;
|
fn attributes(self) -> Option<f64>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManiaAttributeProvider for f32 {
|
impl ManiaAttributeProvider for f64 {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManiaAttributeProvider for DifficultyAttributes {
|
impl ManiaAttributeProvider for DifficultyAttributes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
Some(self.stars)
|
Some(self.stars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManiaAttributeProvider for PerformanceAttributes {
|
impl ManiaAttributeProvider for PerformanceAttributes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
Some(self.attributes.stars)
|
Some(self.attributes.stars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManiaAttributeProvider for StarResult {
|
impl ManiaAttributeProvider for StarResult {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
#[allow(irrefutable_let_patterns)]
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Mania(attributes) = self {
|
if let Self::Mania(attributes) = self {
|
||||||
Some(attributes.stars)
|
Some(attributes.stars)
|
||||||
@@ -208,7 +208,7 @@ impl ManiaAttributeProvider for StarResult {
|
|||||||
|
|
||||||
impl ManiaAttributeProvider for PpResult {
|
impl ManiaAttributeProvider for PpResult {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
#[allow(irrefutable_let_patterns)]
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Mania(attributes) = self {
|
if let Self::Mania(attributes) = self {
|
||||||
Some(attributes.attributes.stars)
|
Some(attributes.attributes.stars)
|
||||||
|
|||||||
+19
-19
@@ -3,25 +3,25 @@ use super::DifficultyHitObject;
|
|||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
pub(crate) struct Strain {
|
pub(crate) struct Strain {
|
||||||
current_strain: f32,
|
current_strain: f64,
|
||||||
current_section_peak: f32,
|
current_section_peak: f64,
|
||||||
|
|
||||||
individual_strain: f32,
|
individual_strain: f64,
|
||||||
overall_strain: f32,
|
overall_strain: f64,
|
||||||
|
|
||||||
hold_end_times: Vec<f32>,
|
hold_end_times: Vec<f64>,
|
||||||
individual_strains: Vec<f32>,
|
individual_strains: Vec<f64>,
|
||||||
pub(crate) strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const INDIVISUAL_DECAY_BASE: f32 = 0.125;
|
const INDIVISUAL_DECAY_BASE: f64 = 0.125;
|
||||||
const OVERALL_DECAY_BASE: f32 = 0.3;
|
const OVERALL_DECAY_BASE: f64 = 0.3;
|
||||||
const STRAIN_DECAY_BASE: f32 = 1.0;
|
const STRAIN_DECAY_BASE: f64 = 1.0;
|
||||||
|
|
||||||
const SKILL_MULTIPLIER: f32 = 1.0;
|
const SKILL_MULTIPLIER: f64 = 1.0;
|
||||||
const DECAY_WEIGHT: f32 = 0.9;
|
const DECAY_WEIGHT: f64 = 0.9;
|
||||||
|
|
||||||
impl Strain {
|
impl Strain {
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -47,18 +47,18 @@ impl Strain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
fn peak_strain(&self, delta_time: f64) -> f64 {
|
||||||
apply_decay(self.individual_strain, delta_time, INDIVISUAL_DECAY_BASE)
|
apply_decay(self.individual_strain, delta_time, INDIVISUAL_DECAY_BASE)
|
||||||
+ apply_decay(self.overall_strain, delta_time, OVERALL_DECAY_BASE)
|
+ apply_decay(self.overall_strain, delta_time, OVERALL_DECAY_BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strain_decay(&self, ms: f32) -> f32 {
|
fn strain_decay(&self, ms: f64) -> f64 {
|
||||||
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
STRAIN_DECAY_BASE.powf(ms / 1000.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ impl Strain {
|
|||||||
self.prev_time.replace(current.start_time);
|
self.prev_time.replace(current.start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strain_value_of(&mut self, current: &DifficultyHitObject<'_>) -> f32 {
|
fn strain_value_of(&mut self, current: &DifficultyHitObject<'_>) -> f64 {
|
||||||
let end_time = current.base.end_time();
|
let end_time = current.base.end_time();
|
||||||
|
|
||||||
let mut hold_factor = 1.0;
|
let mut hold_factor = 1.0;
|
||||||
@@ -107,7 +107,7 @@ impl Strain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
@@ -124,6 +124,6 @@ impl Strain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_decay(value: f32, delta_time: f32, decay_base: f32) -> f32 {
|
fn apply_decay(value: f64, delta_time: f64, decay_base: f64) -> f64 {
|
||||||
value * decay_base.powf(delta_time / 1000.0)
|
value * decay_base.powf(delta_time / 1000.0)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,6 +10,6 @@ pub(crate) fn is_linear(
|
|||||||
|
|
||||||
#[cfg(feature = "osu")]
|
#[cfg(feature = "osu")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn lerp(start: f32, end: f32, percent: f32) -> f32 {
|
pub(crate) fn lerp(start: f64, end: f64, percent: f64) -> f64 {
|
||||||
start + (end - start) * percent
|
start + (end - start) * percent
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -21,8 +21,8 @@ pub trait Mods: Copy {
|
|||||||
|
|
||||||
fn change_speed(self) -> bool;
|
fn change_speed(self) -> bool;
|
||||||
fn change_map(self) -> bool;
|
fn change_map(self) -> bool;
|
||||||
fn speed(self) -> f32;
|
fn speed(self) -> f64;
|
||||||
fn od_ar_hp_multiplier(self) -> f32;
|
fn od_ar_hp_multiplier(self) -> f64;
|
||||||
fn nf(self) -> bool;
|
fn nf(self) -> bool;
|
||||||
fn ez(self) -> bool;
|
fn ez(self) -> bool;
|
||||||
fn td(self) -> bool;
|
fn td(self) -> bool;
|
||||||
@@ -47,7 +47,7 @@ impl Mods for u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn speed(self) -> f32 {
|
fn speed(self) -> f64 {
|
||||||
if self & Self::DT > 0 {
|
if self & Self::DT > 0 {
|
||||||
1.5
|
1.5
|
||||||
} else if self & Self::HT > 0 {
|
} else if self & Self::HT > 0 {
|
||||||
@@ -58,7 +58,7 @@ impl Mods for u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn od_ar_hp_multiplier(self) -> f32 {
|
fn od_ar_hp_multiplier(self) -> f64 {
|
||||||
if self & Self::HR > 0 {
|
if self & Self::HR > 0 {
|
||||||
1.4
|
1.4
|
||||||
} else if self & Self::EZ > 0 {
|
} else if self & Self::EZ > 0 {
|
||||||
|
|||||||
+17
-17
@@ -22,16 +22,16 @@ pub use fast::*;
|
|||||||
/// This data is necessary to calculate PP.
|
/// This data is necessary to calculate PP.
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct DifficultyAttributes {
|
pub struct DifficultyAttributes {
|
||||||
pub aim_strain: f32,
|
pub aim_strain: f64,
|
||||||
pub speed_strain: f32,
|
pub speed_strain: f64,
|
||||||
pub flashlight_rating: f32,
|
pub flashlight_rating: f64,
|
||||||
pub ar: f32,
|
pub ar: f64,
|
||||||
pub od: f32,
|
pub od: f64,
|
||||||
pub hp: f32,
|
pub hp: f64,
|
||||||
pub n_circles: usize,
|
pub n_circles: usize,
|
||||||
pub n_sliders: usize,
|
pub n_sliders: usize,
|
||||||
pub n_spinners: usize,
|
pub n_spinners: usize,
|
||||||
pub stars: f32,
|
pub stars: f64,
|
||||||
pub max_combo: usize,
|
pub max_combo: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,29 +39,29 @@ pub struct DifficultyAttributes {
|
|||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct PerformanceAttributes {
|
pub struct PerformanceAttributes {
|
||||||
pub attributes: DifficultyAttributes,
|
pub attributes: DifficultyAttributes,
|
||||||
pub pp_acc: f32,
|
pub pp_acc: f64,
|
||||||
pub pp_aim: f32,
|
pub pp_aim: f64,
|
||||||
pub pp_flashlight: f32,
|
pub pp_flashlight: f64,
|
||||||
pub pp_speed: f32,
|
pub pp_speed: f64,
|
||||||
pub pp: f32,
|
pub pp: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerformanceAttributes {
|
impl PerformanceAttributes {
|
||||||
/// Return the star value.
|
/// Return the star value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f64 {
|
||||||
self.attributes.stars
|
self.attributes.stars
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the performance point value.
|
/// Return the performance point value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pp(&self) -> f32 {
|
pub fn pp(&self) -> f64 {
|
||||||
self.pp
|
self.pp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn difficulty_range_od(od: f32) -> f32 {
|
fn difficulty_range_od(od: f64) -> f64 {
|
||||||
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
super::difficulty_range(od, 20.0, 50.0, 80.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ fn custom_osu() {
|
|||||||
|
|
||||||
use crate::{Beatmap, OsuPP};
|
use crate::{Beatmap, OsuPP};
|
||||||
|
|
||||||
let path = "E:Games/osu!/beatmaps/1402167_.osu";
|
let path = "E:Games/osu!/beatmaps/70090_.osu";
|
||||||
let file = File::open(path).unwrap();
|
let file = File::open(path).unwrap();
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -94,7 +94,7 @@ fn custom_osu() {
|
|||||||
println!("Parsing average: {:?}", accum / iters);
|
println!("Parsing average: {:?}", accum / iters);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let result = OsuPP::new(&map).mods(0).calculate();
|
let result = OsuPP::new(&map).mods(16 + 64).calculate();
|
||||||
|
|
||||||
let iters = 100;
|
let iters = 100;
|
||||||
let accum = start.elapsed();
|
let accum = start.elapsed();
|
||||||
|
|||||||
+58
-59
@@ -35,7 +35,7 @@ pub struct OsuPP<'m> {
|
|||||||
attributes: Option<DifficultyAttributes>,
|
attributes: Option<DifficultyAttributes>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
acc: Option<f32>,
|
acc: Option<f64>,
|
||||||
|
|
||||||
n300: Option<usize>,
|
n300: Option<usize>,
|
||||||
n100: Option<usize>,
|
n100: Option<usize>,
|
||||||
@@ -138,7 +138,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
///
|
///
|
||||||
/// Be sure to set `misses` beforehand!
|
/// Be sure to set `misses` beforehand!
|
||||||
/// In case of a partial play, be also sure to set `passed_objects` beforehand!
|
/// In case of a partial play, be also sure to set `passed_objects` beforehand!
|
||||||
pub fn accuracy(mut self, acc: f32) -> Self {
|
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||||
let n_objects = self
|
let n_objects = self
|
||||||
.passed_objects
|
.passed_objects
|
||||||
.unwrap_or_else(|| self.map.hit_objects.len());
|
.unwrap_or_else(|| self.map.hit_objects.len());
|
||||||
@@ -152,7 +152,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
let placed_points = 2 * n100 + n50 + self.n_misses;
|
let placed_points = 2 * n100 + n50 + self.n_misses;
|
||||||
let missing_objects = n_objects - n100 - n50 - self.n_misses;
|
let missing_objects = n_objects - n100 - n50 - self.n_misses;
|
||||||
let missing_points =
|
let missing_points =
|
||||||
((6.0 * acc * n_objects as f32).round() as usize).saturating_sub(placed_points);
|
((6.0 * acc * n_objects as f64).round() as usize).saturating_sub(placed_points);
|
||||||
|
|
||||||
let mut n300 = missing_objects.min(missing_points / 6);
|
let mut n300 = missing_objects.min(missing_points / 6);
|
||||||
n50 += missing_objects - n300;
|
n50 += missing_objects - n300;
|
||||||
@@ -171,10 +171,10 @@ impl<'m> OsuPP<'m> {
|
|||||||
self.n100 = Some(n100);
|
self.n100 = Some(n100);
|
||||||
self.n50 = Some(n50);
|
self.n50 = Some(n50);
|
||||||
|
|
||||||
acc = (6 * n300 + 2 * n100 + n50) as f32 / (6 * n_objects) as f32;
|
acc = (6 * n300 + 2 * n100 + n50) as f64 / (6 * n_objects) as f64;
|
||||||
} else {
|
} else {
|
||||||
let misses = self.n_misses.min(n_objects);
|
let misses = self.n_misses.min(n_objects);
|
||||||
let target_total = (acc * n_objects as f32 * 6.0).round() as usize;
|
let target_total = (acc * n_objects as f64 * 6.0).round() as usize;
|
||||||
let delta = target_total - (n_objects - misses);
|
let delta = target_total - (n_objects - misses);
|
||||||
|
|
||||||
let mut n300 = delta / 5;
|
let mut n300 = delta / 5;
|
||||||
@@ -191,7 +191,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
self.n100 = Some(n100);
|
self.n100 = Some(n100);
|
||||||
self.n50 = Some(n50);
|
self.n50 = Some(n50);
|
||||||
|
|
||||||
acc = (6 * n300 + 2 * n100 + n50) as f32 / (6 * n_objects) as f32;
|
acc = (6 * n300 + 2 * n100 + n50) as f64 / (6 * n_objects) as f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.acc = Some(acc);
|
self.acc = Some(acc);
|
||||||
@@ -213,7 +213,7 @@ impl<'m> OsuPP<'m> {
|
|||||||
let n100 = n100.unwrap_or(0);
|
let n100 = n100.unwrap_or(0);
|
||||||
let n50 = n50.unwrap_or(0);
|
let n50 = n50.unwrap_or(0);
|
||||||
|
|
||||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f32;
|
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64;
|
||||||
|
|
||||||
let effective_misses =
|
let effective_misses =
|
||||||
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
||||||
@@ -259,9 +259,9 @@ impl<'m> OsuPP<'m> {
|
|||||||
let n50 = n50.unwrap_or(0);
|
let n50 = n50.unwrap_or(0);
|
||||||
|
|
||||||
let numerator = n300 * 6 + n100 * 2 + n50;
|
let numerator = n300 * 6 + n100 * 2 + n50;
|
||||||
let acc = numerator as f32 / n_objects as f32 / 6.0;
|
let acc = numerator as f64 / n_objects as f64 / 6.0;
|
||||||
|
|
||||||
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f32;
|
let total_hits = (n300 + n100 + n50 + self.n_misses).min(n_objects) as f64;
|
||||||
|
|
||||||
let effective_misses =
|
let effective_misses =
|
||||||
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
calculate_effective_misses(&attributes, self.combo, self.n_misses, total_hits);
|
||||||
@@ -295,13 +295,13 @@ struct OsuPPInner {
|
|||||||
attributes: DifficultyAttributes,
|
attributes: DifficultyAttributes,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
acc: f32,
|
acc: f64,
|
||||||
|
|
||||||
n300: usize,
|
n300: usize,
|
||||||
n100: usize,
|
n100: usize,
|
||||||
n50: usize,
|
n50: usize,
|
||||||
|
|
||||||
total_hits: f32,
|
total_hits: f64,
|
||||||
effective_misses: usize,
|
effective_misses: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,18 +311,23 @@ impl OsuPPInner {
|
|||||||
|
|
||||||
// NF penalty
|
// NF penalty
|
||||||
if self.mods.nf() {
|
if self.mods.nf() {
|
||||||
multiplier *= (1.0 - 0.02 * (self.effective_misses as f32)).max(0.9);
|
multiplier *= (1.0 - 0.02 * (self.effective_misses as f64)).max(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SO penalty
|
// SO penalty
|
||||||
if self.mods.so() {
|
if self.mods.so() {
|
||||||
let n_spinners = self.attributes.n_spinners;
|
let n_spinners = self.attributes.n_spinners;
|
||||||
multiplier *= 1.0 - (n_spinners as f32 / self.total_hits).powf(0.85);
|
multiplier *= 1.0 - (n_spinners as f64 / self.total_hits).powf(0.85);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relax penalty
|
// Relax penalty
|
||||||
if self.mods.rx() {
|
if self.mods.rx() {
|
||||||
self.effective_misses += self.n100 + self.n50;
|
// * As we're adding 100s and 50s to an approximated number of combo breaks\
|
||||||
|
// * the result can be higher than total hits in specific scenarios
|
||||||
|
// * (which breaks some calculations) so we need to clamp it.
|
||||||
|
self.effective_misses =
|
||||||
|
(self.effective_misses + self.n100 + self.n50).min(self.total_hits as usize);
|
||||||
|
|
||||||
multiplier *= 0.6;
|
multiplier *= 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +353,7 @@ impl OsuPPInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_aim_value(&self) -> f32 {
|
fn compute_aim_value(&self) -> f64 {
|
||||||
let attributes = &self.attributes;
|
let attributes = &self.attributes;
|
||||||
let total_hits = self.total_hits;
|
let total_hits = self.total_hits;
|
||||||
|
|
||||||
@@ -364,48 +369,44 @@ impl OsuPPInner {
|
|||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
let len_bonus = 0.95
|
let len_bonus = 0.95
|
||||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||||
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
+ (total_hits > 2000.0) as u8 as f64 * 0.5 * (total_hits / 2000.0).log10();
|
||||||
aim_value *= len_bonus;
|
aim_value *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses
|
// Penalize misses
|
||||||
let effective_misses = self.effective_misses as i32;
|
let effective_misses = self.effective_misses as i32;
|
||||||
if effective_misses > 0 {
|
if effective_misses > 0 {
|
||||||
aim_value *= 0.97
|
aim_value *= 0.97
|
||||||
* (1.0 - (effective_misses as f32 / total_hits).powf(0.775)).powi(effective_misses);
|
* (1.0 - (effective_misses as f64 / total_hits).powf(0.775)).powi(effective_misses);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
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);
|
aim_value *= ((combo as f64 / attributes.max_combo as f64).powf(0.8)).min(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AR bonus
|
// AR bonus
|
||||||
let ar_factor = if attributes.ar > 10.33 {
|
let ar_factor = if attributes.ar > 10.33 {
|
||||||
attributes.ar - 10.33
|
0.3 * (attributes.ar - 10.33)
|
||||||
} else if attributes.ar < 8.0 {
|
} else if attributes.ar < 8.0 {
|
||||||
0.025 * (8.0 - attributes.ar)
|
0.1 * (8.0 - attributes.ar)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip();
|
aim_value *= 1.0 + ar_factor * len_bonus; // * Buff for longer maps with high AR.
|
||||||
let ar_bonus = 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
|
|
||||||
|
|
||||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
aim_value *= 1.0 + 0.04 * (12.0 - attributes.ar);
|
||||||
}
|
}
|
||||||
|
|
||||||
aim_value *= ar_bonus;
|
aim_value *= self.acc;
|
||||||
|
|
||||||
// Scale with accuracy
|
|
||||||
aim_value *= 0.5 + self.acc / 2.0;
|
|
||||||
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
aim_value *= 0.98 + attributes.od * attributes.od / 2500.0;
|
||||||
|
|
||||||
aim_value
|
aim_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_speed_value(&self) -> f32 {
|
fn compute_speed_value(&self) -> f64 {
|
||||||
let attributes = &self.attributes;
|
let attributes = &self.attributes;
|
||||||
let total_hits = self.total_hits;
|
let total_hits = self.total_hits;
|
||||||
|
|
||||||
@@ -415,11 +416,11 @@ impl OsuPPInner {
|
|||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
let len_bonus = 0.95
|
let len_bonus = 0.95
|
||||||
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
+ 0.4 * (total_hits / 2000.0).min(1.0)
|
||||||
+ (total_hits > 2000.0) as u8 as f32 * 0.5 * (total_hits / 2000.0).log10();
|
+ (total_hits > 2000.0) as u8 as f64 * 0.5 * (total_hits / 2000.0).log10();
|
||||||
speed_value *= len_bonus;
|
speed_value *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses
|
// Penalize misses
|
||||||
let effective_misses = self.effective_misses as f32;
|
let effective_misses = self.effective_misses as f64;
|
||||||
if effective_misses > 0.0 {
|
if effective_misses > 0.0 {
|
||||||
speed_value *= 0.97
|
speed_value *= 0.97
|
||||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||||
@@ -428,19 +429,17 @@ impl OsuPPInner {
|
|||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
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);
|
speed_value *= ((combo as f64 / attributes.max_combo as f64).powf(0.8)).min(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// AR bonus
|
// AR bonus
|
||||||
let ar_factor = if attributes.ar > 10.33 {
|
let ar_factor = if attributes.ar > 10.33 {
|
||||||
attributes.ar - 10.33
|
0.3 * (attributes.ar - 10.33)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let ar_total_hits_factor = (1.0 + (-(0.007 * (total_hits - 400.0))).exp()).recip();
|
speed_value *= 1.0 + ar_factor * len_bonus; // * Buff for longer maps with high AR.
|
||||||
|
|
||||||
speed_value *= 1.0 + (0.03 + 0.37 * ar_total_hits_factor) * ar_factor;
|
|
||||||
|
|
||||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
@@ -453,34 +452,34 @@ impl OsuPPInner {
|
|||||||
speed_value *= od_factor * acc_factor;
|
speed_value *= od_factor * acc_factor;
|
||||||
|
|
||||||
// Penalize n50s
|
// Penalize n50s
|
||||||
speed_value *= 0.98_f32.powf(
|
speed_value *= 0.98_f64.powf(
|
||||||
(self.n50 as f32 >= total_hits / 500.0) as u8 as f32
|
(self.n50 as f64 >= total_hits / 500.0) as u8 as f64
|
||||||
* (self.n50 as f32 - total_hits / 500.0),
|
* (self.n50 as f64 - total_hits / 500.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
speed_value
|
speed_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_accuracy_value(&self) -> f32 {
|
fn compute_accuracy_value(&self) -> f64 {
|
||||||
if self.mods.rx() {
|
if self.mods.rx() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes = &self.attributes;
|
let attributes = &self.attributes;
|
||||||
let total_hits = self.total_hits;
|
let total_hits = self.total_hits;
|
||||||
let n_circles = attributes.n_circles as f32;
|
let n_circles = attributes.n_circles as f64;
|
||||||
let n300 = self.n300 as f32;
|
let n300 = self.n300 as f64;
|
||||||
let n100 = self.n100 as f32;
|
let n100 = self.n100 as f64;
|
||||||
let n50 = self.n50 as f32;
|
let n50 = self.n50 as f64;
|
||||||
|
|
||||||
let better_acc_percentage = (n_circles > 0.0) as u8 as f32
|
let better_acc_percentage = (n_circles > 0.0) as u8 as f64
|
||||||
* (((n300 - (total_hits - n_circles)) * 6.0 + n100 * 2.0 + n50) / (n_circles * 6.0))
|
* (((n300 - (total_hits - n_circles)) * 6.0 + n100 * 2.0 + n50) / (n_circles * 6.0))
|
||||||
.max(0.0);
|
.max(0.0);
|
||||||
|
|
||||||
let mut acc_value = 1.52163_f32.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
let mut acc_value = 1.52163_f64.powf(attributes.od) * better_acc_percentage.powi(24) * 2.83;
|
||||||
|
|
||||||
// Bonus for many hitcircles
|
// Bonus for many hitcircles
|
||||||
acc_value *= ((n_circles as f32 / 1000.0).powf(0.3)).min(1.15);
|
acc_value *= ((n_circles as f64 / 1000.0).powf(0.3)).min(1.15);
|
||||||
|
|
||||||
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
// HD bonus (this would include the Blinds mod but it's currently not representable)
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
@@ -495,7 +494,7 @@ impl OsuPPInner {
|
|||||||
acc_value
|
acc_value
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_flashlight_value(&self) -> f32 {
|
fn compute_flashlight_value(&self) -> f64 {
|
||||||
if !self.mods.fl() {
|
if !self.mods.fl() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
@@ -519,7 +518,7 @@ impl OsuPPInner {
|
|||||||
|
|
||||||
// Penalize misses by assessing # of misses relative to the total # of objects.
|
// Penalize misses by assessing # of misses relative to the total # of objects.
|
||||||
// Default a 3% reduction for any # of misses
|
// Default a 3% reduction for any # of misses
|
||||||
let effective_misses = self.effective_misses as f32;
|
let effective_misses = self.effective_misses as f64;
|
||||||
if effective_misses > 0.0 {
|
if effective_misses > 0.0 {
|
||||||
flashlight_value *= 0.97
|
flashlight_value *= 0.97
|
||||||
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
* (1.0 - (effective_misses / total_hits).powf(0.775))
|
||||||
@@ -528,13 +527,13 @@ impl OsuPPInner {
|
|||||||
|
|
||||||
// Combo scaling
|
// Combo scaling
|
||||||
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
if let Some(combo) = self.combo.filter(|_| attributes.max_combo > 0) {
|
||||||
flashlight_value *= ((combo as f32 / attributes.max_combo as f32).powf(0.8)).min(1.0);
|
flashlight_value *= ((combo as f64 / attributes.max_combo as f64).powf(0.8)).min(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius
|
||||||
flashlight_value *= 0.7
|
flashlight_value *= 0.7
|
||||||
+ 0.1 * (total_hits / 200.0).min(1.0)
|
+ 0.1 * (total_hits / 200.0).min(1.0)
|
||||||
+ (total_hits > 200.0) as u8 as f32 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
+ (total_hits > 200.0) as u8 as f64 * (0.2 * ((total_hits - 200.0) / 200.0).min(1.0));
|
||||||
|
|
||||||
// Scale the aim value with accuracy _slightly_
|
// Scale the aim value with accuracy _slightly_
|
||||||
flashlight_value *= 0.5 + self.acc / 2.0;
|
flashlight_value *= 0.5 + self.acc / 2.0;
|
||||||
@@ -550,23 +549,23 @@ fn calculate_effective_misses(
|
|||||||
attributes: &DifficultyAttributes,
|
attributes: &DifficultyAttributes,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
n_misses: usize,
|
n_misses: usize,
|
||||||
total_hits: f32,
|
total_hits: f64,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
// Guess the number of misses + slider breaks from combo
|
// * Guess the number of misses + slider breaks from combo
|
||||||
let mut combo_based_misses: f32 = 0.0;
|
let mut combo_based_misses: f64 = 0.0;
|
||||||
|
|
||||||
if attributes.n_sliders > 0 {
|
if attributes.n_sliders > 0 {
|
||||||
let full_combo_threshold = attributes.max_combo as f32 - 0.1 * attributes.n_sliders as f32;
|
let full_combo_threshold = attributes.max_combo as f64 - 0.1 * attributes.n_sliders as f64;
|
||||||
|
|
||||||
let f32_combo = combo.map(|c| c as f32);
|
let f64_combo = combo.map(|c| c as f64);
|
||||||
|
|
||||||
if let Some(combo) = f32_combo.filter(|&c| c < full_combo_threshold) {
|
if let Some(combo) = f64_combo.filter(|&c| c < full_combo_threshold) {
|
||||||
combo_based_misses = full_combo_threshold / combo.max(1.0);
|
combo_based_misses = full_combo_threshold / combo.max(1.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're clamping misses because since it's derived from combo it
|
// * Clamp misscount since it's derived from combo and can be
|
||||||
// can be higher than total hits and that breaks some calculations
|
// * higher than total hits and that breaks some calculations
|
||||||
combo_based_misses = combo_based_misses.min(total_hits);
|
combo_based_misses = combo_based_misses.min(total_hits);
|
||||||
|
|
||||||
n_misses.max(combo_based_misses.floor() as usize)
|
n_misses.max(combo_based_misses.floor() as usize)
|
||||||
@@ -634,7 +633,7 @@ mod test {
|
|||||||
+ 2 * calculator.n100.unwrap_or(0)
|
+ 2 * calculator.n100.unwrap_or(0)
|
||||||
+ calculator.n50.unwrap_or(0);
|
+ calculator.n50.unwrap_or(0);
|
||||||
let denominator = 6 * total_objects;
|
let denominator = 6 * total_objects;
|
||||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(target_acc - acc).abs() < 1.0,
|
(target_acc - acc).abs() < 1.0,
|
||||||
@@ -668,7 +667,7 @@ mod test {
|
|||||||
+ 2 * calculator.n100.unwrap_or(0)
|
+ 2 * calculator.n100.unwrap_or(0)
|
||||||
+ calculator.n50.unwrap_or(0);
|
+ calculator.n50.unwrap_or(0);
|
||||||
let denominator = 6 * total_objects;
|
let denominator = 6 * total_objects;
|
||||||
let acc = 100.0 * numerator as f32 / denominator as f32;
|
let acc = 100.0 * numerator as f64 / denominator as f64;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(target_acc - acc).abs() < 1.0,
|
(target_acc - acc).abs() < 1.0,
|
||||||
|
|||||||
@@ -1,67 +1,238 @@
|
|||||||
use super::OsuObject;
|
use crate::{
|
||||||
|
osu::precise::osu_object::{NestedObjectKind, OsuObjectKind},
|
||||||
|
parse::Pos2,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{OsuObject, ScalingFactor, NORMALIZED_RADIUS};
|
||||||
|
|
||||||
|
const MIN_DELTA_TIME: f64 = 25.0;
|
||||||
|
const MAXIMUM_SLIDER_RADIUS: f32 = NORMALIZED_RADIUS * 2.4;
|
||||||
|
const ASSUMED_SLIDER_RADIUS: f32 = NORMALIZED_RADIUS * 1.65;
|
||||||
|
|
||||||
pub(crate) struct DifficultyObject<'h> {
|
pub(crate) struct DifficultyObject<'h> {
|
||||||
pub(crate) base: &'h OsuObject,
|
pub(crate) base: &'h OsuObject,
|
||||||
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
|
|
||||||
|
|
||||||
pub(crate) jump_dist: f32,
|
pub(crate) delta: f64,
|
||||||
pub(crate) travel_dist: f32,
|
pub(crate) strain_time: f64,
|
||||||
pub(crate) angle: Option<f32>,
|
|
||||||
|
|
||||||
pub(crate) delta: f32,
|
pub(crate) angle: Option<f64>,
|
||||||
pub(crate) strain_time: f32,
|
pub(crate) jump_dist: f64,
|
||||||
|
|
||||||
|
pub(crate) movement_dist: f64,
|
||||||
|
pub(crate) movement_time: f64,
|
||||||
|
|
||||||
|
pub(crate) travel_dist: f64,
|
||||||
|
pub(crate) travel_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'h> DifficultyObject<'h> {
|
impl<'h> DifficultyObject<'h> {
|
||||||
pub(crate) fn new(
|
pub(super) fn new(
|
||||||
base: &'h OsuObject,
|
base: &'h OsuObject,
|
||||||
prev: &OsuObject,
|
prev: &mut OsuObject,
|
||||||
prev_vals: Option<(f32, f32)>, // (jump_dist, strain_time)
|
prev_prev: Option<&OsuObject>,
|
||||||
prev_prev: Option<OsuObject>,
|
scaling_factor: &ScalingFactor,
|
||||||
scale_factor: f32,
|
clock_rate: f64,
|
||||||
scaling_factor: f32,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let delta = base.time - prev.time;
|
let delta = base.time - prev.time;
|
||||||
|
|
||||||
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
|
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects
|
||||||
let strain_time = delta.max(25.0);
|
let strain_time = delta.max(MIN_DELTA_TIME);
|
||||||
|
|
||||||
let pos = base.pos; // stacked position
|
// * We don't need to calculate either angle or distances
|
||||||
let travel_dist = prev.travel_dist();
|
// * when one of the last->curr objects is a spinner
|
||||||
let prev_cursor_pos = prev.lazy_end_pos(scale_factor);
|
let (travel_dist, travel_time, movement_dist, movement_time, jump_dist, angle) =
|
||||||
|
if base.is_spinner() || prev.is_spinner() {
|
||||||
|
(0.0, 0.0, 0.0, 0.0, 0.0, None)
|
||||||
|
} else {
|
||||||
|
let prev_stack_offset = scaling_factor.stack_offset(prev.stack_height);
|
||||||
|
|
||||||
// We don't need to calculate either angle or distance
|
// Important to call `Self::compute_slider_cursor_pos` before using `prev.lazy_end_pos`
|
||||||
// when one of the last->curr objects is a spinner
|
// because the lazy end position is being calculated in that function
|
||||||
let (jump_dist, angle) = if base.is_spinner() || prev.is_spinner() {
|
let (travel_dist, travel_time) = Self::compute_slider_cursor_pos(
|
||||||
(0.0, None)
|
prev,
|
||||||
} else {
|
prev_stack_offset,
|
||||||
let jump_dist = ((pos - prev_cursor_pos) * scaling_factor).length();
|
scaling_factor.raw(),
|
||||||
|
clock_rate,
|
||||||
|
);
|
||||||
|
|
||||||
let angle = prev_prev.map(|prev_prev| {
|
let prev_cursor_pos = prev.lazy_end_pos(prev_stack_offset);
|
||||||
let prev_prev_cursor_pos = prev_prev.lazy_end_pos(scale_factor);
|
|
||||||
|
|
||||||
let v1 = prev_prev_cursor_pos - prev.pos;
|
let jump_dist =
|
||||||
let v2 = pos - prev_cursor_pos;
|
((base.pos - prev_cursor_pos) * scaling_factor.adjusted()).length() as f64;
|
||||||
|
|
||||||
let dot = v1.dot(v2);
|
let angle =
|
||||||
let det = v1.x * v2.y - v1.y * v2.x;
|
prev_prev
|
||||||
|
.filter(|prev_prev| !prev_prev.is_spinner())
|
||||||
|
.map(|prev_prev| {
|
||||||
|
let prev_prev_cursor_pos = prev_prev
|
||||||
|
.lazy_end_pos(scaling_factor.stack_offset(prev_prev.stack_height));
|
||||||
|
|
||||||
det.atan2(dot).abs()
|
let v1 = prev_prev_cursor_pos - prev.pos;
|
||||||
});
|
let v2 = base.pos - prev_cursor_pos;
|
||||||
|
|
||||||
(jump_dist, angle)
|
let dot = (v1.dot(v2)) as f64;
|
||||||
};
|
let det = (v1.x * v2.y - v1.y * v2.x) as f64;
|
||||||
|
|
||||||
|
det.atan2(dot).abs()
|
||||||
|
});
|
||||||
|
|
||||||
|
let (movement_dist, movement_time) = Self::compute_movement_values(
|
||||||
|
prev,
|
||||||
|
base.pos,
|
||||||
|
jump_dist,
|
||||||
|
strain_time,
|
||||||
|
travel_time,
|
||||||
|
scaling_factor.adjusted(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(
|
||||||
|
travel_dist,
|
||||||
|
travel_time,
|
||||||
|
movement_dist,
|
||||||
|
movement_time,
|
||||||
|
jump_dist,
|
||||||
|
angle,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
base,
|
base,
|
||||||
prev: prev_vals,
|
|
||||||
|
|
||||||
jump_dist,
|
|
||||||
travel_dist,
|
|
||||||
angle,
|
|
||||||
|
|
||||||
delta,
|
delta,
|
||||||
strain_time,
|
strain_time,
|
||||||
|
jump_dist,
|
||||||
|
angle,
|
||||||
|
movement_dist,
|
||||||
|
movement_time,
|
||||||
|
travel_dist,
|
||||||
|
travel_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_slider_cursor_pos(
|
||||||
|
prev: &mut OsuObject,
|
||||||
|
stack_offset: Pos2,
|
||||||
|
scaling_factor: f64,
|
||||||
|
clock_rate: f64,
|
||||||
|
) -> (f64, f64) {
|
||||||
|
match &mut prev.kind {
|
||||||
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => (0.0, 0.0),
|
||||||
|
OsuObjectKind::Slider {
|
||||||
|
lazy_end_pos,
|
||||||
|
nested_objects,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let mut travel_dist = 0.0;
|
||||||
|
let pos = prev.pos - stack_offset; // stack offset is ignored everywhere
|
||||||
|
let mut curr_cursor_pos = pos;
|
||||||
|
|
||||||
|
let last_idx = nested_objects.len() - 1;
|
||||||
|
|
||||||
|
for (i, nested) in nested_objects.iter_mut().enumerate() {
|
||||||
|
let mut curr_movement = nested.pos - curr_cursor_pos;
|
||||||
|
let mut curr_movement_len = scaling_factor * curr_movement.length() as f64;
|
||||||
|
|
||||||
|
// * Amount of movement required so that the cursor position needs to be updated.
|
||||||
|
let mut required_movement = ASSUMED_SLIDER_RADIUS as f64;
|
||||||
|
|
||||||
|
if i == last_idx {
|
||||||
|
// * The end of a slider has special aim rules due
|
||||||
|
// * to the relaxed time constraint on position.
|
||||||
|
// * There is both a lazy end position as well as the actual end slider position.
|
||||||
|
// * We assume the player takes the simpler movement.
|
||||||
|
// * For sliders that are circular, the lazy end position
|
||||||
|
// * may actually be farther away than the sliders true end.
|
||||||
|
// * This code is designed to prevent buffing situations
|
||||||
|
// * where lazy end is actually a less efficient movement.
|
||||||
|
let lazy_movement = *lazy_end_pos - curr_cursor_pos;
|
||||||
|
|
||||||
|
if lazy_movement.length() < curr_movement.length() {
|
||||||
|
curr_movement = lazy_movement;
|
||||||
|
}
|
||||||
|
|
||||||
|
curr_movement_len = scaling_factor * curr_movement.length() as f64;
|
||||||
|
} else if let NestedObjectKind::Repeat = nested.kind {
|
||||||
|
// * For a slider repeat, assume a tighter movement
|
||||||
|
// * threshold to better assess repeat sliders.
|
||||||
|
required_movement = NORMALIZED_RADIUS as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
if curr_movement_len > required_movement {
|
||||||
|
// * this finds the positional delta from the required
|
||||||
|
// * radius and the current position, and updates the
|
||||||
|
// * currCursorPosition accordingly, as well as rewarding distance.
|
||||||
|
curr_cursor_pos = curr_cursor_pos
|
||||||
|
+ curr_movement
|
||||||
|
* ((curr_movement_len - required_movement) / curr_movement_len)
|
||||||
|
as f32;
|
||||||
|
|
||||||
|
curr_movement_len *=
|
||||||
|
(curr_movement_len - required_movement) / curr_movement_len;
|
||||||
|
|
||||||
|
travel_dist += curr_movement_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == last_idx {
|
||||||
|
*lazy_end_pos = curr_cursor_pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let repeats = nested_objects
|
||||||
|
.iter()
|
||||||
|
.filter(|nested| matches!(nested.kind, NestedObjectKind::Repeat))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// * Bonus for repeat sliders until a better per
|
||||||
|
// * nested object strain system can be achieved.
|
||||||
|
travel_dist *= (1.0 + repeats as f64 / 2.5).powf(1.0 / 2.5);
|
||||||
|
let prev_time = prev.time;
|
||||||
|
|
||||||
|
let lazy_travel_time = nested_objects
|
||||||
|
.last()
|
||||||
|
.map_or(0.0, |nested| nested.time / clock_rate - prev_time);
|
||||||
|
|
||||||
|
let travel_time = MIN_DELTA_TIME.max(lazy_travel_time);
|
||||||
|
|
||||||
|
(travel_dist, travel_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_movement_values(
|
||||||
|
prev: &OsuObject,
|
||||||
|
base_pos: Pos2,
|
||||||
|
jump_dist: f64,
|
||||||
|
strain_time: f64,
|
||||||
|
travel_time: f64,
|
||||||
|
scaling_factor: f32,
|
||||||
|
) -> (f64, f64) {
|
||||||
|
match &prev.kind {
|
||||||
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => (jump_dist, strain_time),
|
||||||
|
OsuObjectKind::Slider { end_pos, .. } => {
|
||||||
|
let movement_time = MIN_DELTA_TIME.max(strain_time - travel_time);
|
||||||
|
|
||||||
|
// * Jump distance from the slider tail to the next object,
|
||||||
|
// * as opposed to the lazy position of JumpDistance.
|
||||||
|
let tail_jump_dist = (*end_pos - base_pos).length() * scaling_factor;
|
||||||
|
|
||||||
|
// * For hitobjects which continue in the direction of the slider,
|
||||||
|
// * the player will normally follow through the slider,
|
||||||
|
// * such that they're not jumping from the lazy position but
|
||||||
|
// * rather from very close to (or the end of) the slider.
|
||||||
|
// * In such cases, a leniency is applied by also considering the
|
||||||
|
// * jump distance from the tail of the slider,
|
||||||
|
// * and taking the minimum jump distance.
|
||||||
|
// * Additional distance is removed based on position of jump
|
||||||
|
// * relative to slider follow circle radius.
|
||||||
|
// * JumpDistance is the leniency distance beyond the assumed_slider_radius.
|
||||||
|
// * tailJumpDistance is maximum_slider_radius since
|
||||||
|
// * the full distance of radial leniency is still possible.
|
||||||
|
let movement_dist = (jump_dist
|
||||||
|
- (MAXIMUM_SLIDER_RADIUS - ASSUMED_SLIDER_RADIUS) as f64)
|
||||||
|
.min((tail_jump_dist - MAXIMUM_SLIDER_RADIUS) as f64)
|
||||||
|
.max(0.0);
|
||||||
|
|
||||||
|
(movement_dist, movement_time)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-79
@@ -8,24 +8,25 @@ use std::mem;
|
|||||||
|
|
||||||
mod difficulty_object;
|
mod difficulty_object;
|
||||||
mod osu_object;
|
mod osu_object;
|
||||||
|
mod scaling_factor;
|
||||||
mod skill;
|
mod skill;
|
||||||
mod skill_kind;
|
mod skill_kind;
|
||||||
mod slider_state;
|
mod slider_state;
|
||||||
|
|
||||||
use difficulty_object::DifficultyObject;
|
use difficulty_object::DifficultyObject;
|
||||||
use osu_object::{ObjectParameters, OsuObject};
|
use osu_object::{ObjectParameters, OsuObject};
|
||||||
|
use scaling_factor::ScalingFactor;
|
||||||
use skill::Skill;
|
use skill::Skill;
|
||||||
use skill_kind::SkillKind;
|
use skill_kind::SkillKind;
|
||||||
use slider_state::SliderState;
|
use slider_state::SliderState;
|
||||||
|
|
||||||
use crate::{curve::CurveBuffers, parse::Pos2, Beatmap, Mods, Strains};
|
use crate::{curve::CurveBuffers, Beatmap, Mods, Strains};
|
||||||
|
|
||||||
use super::DifficultyAttributes;
|
use super::DifficultyAttributes;
|
||||||
|
|
||||||
const OBJECT_RADIUS: f32 = 64.0;
|
const SECTION_LEN: f64 = 400.0;
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const DIFFICULTY_MULTIPLIER: f64 = 0.0675;
|
||||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
const NORMALIZED_RADIUS: f32 = 50.0; // * diameter of 100; easier mental maths.
|
||||||
const NORMALIZED_RADIUS: f32 = 52.0;
|
|
||||||
const STACK_DISTANCE: f32 = 3.0;
|
const STACK_DISTANCE: f32 = 3.0;
|
||||||
|
|
||||||
/// Star calculation for osu!standard maps.
|
/// Star calculation for osu!standard maps.
|
||||||
@@ -54,7 +55,7 @@ pub fn stars(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut raw_ar = map.ar;
|
let mut raw_ar = map.ar as f64;
|
||||||
let hr = mods.hr();
|
let hr = mods.hr();
|
||||||
|
|
||||||
if hr {
|
if hr {
|
||||||
@@ -64,19 +65,10 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let time_preempt = difficulty_range_ar(raw_ar);
|
let time_preempt = difficulty_range_ar(raw_ar);
|
||||||
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||||
let radius = OBJECT_RADIUS * scale;
|
|
||||||
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 mut params = ObjectParameters {
|
let mut params = ObjectParameters {
|
||||||
map,
|
map,
|
||||||
radius,
|
|
||||||
scaling_factor,
|
|
||||||
max_combo: 0,
|
max_combo: 0,
|
||||||
slider_state: SliderState::new(map),
|
slider_state: SliderState::new(map),
|
||||||
ticks: Vec::new(),
|
ticks: Vec::new(),
|
||||||
@@ -92,7 +84,7 @@ pub fn stars(
|
|||||||
let mut hit_objects = Vec::with_capacity(take);
|
let mut hit_objects = Vec::with_capacity(take);
|
||||||
hit_objects.extend(hit_objects_iter);
|
hit_objects.extend(hit_objects_iter);
|
||||||
|
|
||||||
let stack_threshold = time_preempt * map.stack_leniency;
|
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||||
|
|
||||||
if map.version >= 6 {
|
if map.version >= 6 {
|
||||||
stacking(&mut hit_objects, stack_threshold);
|
stacking(&mut hit_objects, stack_threshold);
|
||||||
@@ -100,13 +92,13 @@ pub fn stars(
|
|||||||
old_stacking(&mut hit_objects, stack_threshold);
|
old_stacking(&mut hit_objects, stack_threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scale_factor = scale * -6.4;
|
// let scale_factor = (scaling_factor.scale * -6.4) as f32;
|
||||||
|
|
||||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||||
let stack_offset = h.stack_height * scale_factor;
|
// let stack_offset = Pos2::new(h.stack_height * scale_factor);
|
||||||
|
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
||||||
|
h.pos += stack_offset;
|
||||||
h.time /= map_attributes.clock_rate;
|
h.time /= map_attributes.clock_rate;
|
||||||
h.pos += Pos2::new(stack_offset);
|
|
||||||
|
|
||||||
h
|
h
|
||||||
});
|
});
|
||||||
@@ -114,16 +106,16 @@ pub fn stars(
|
|||||||
let fl = mods.fl();
|
let fl = mods.fl();
|
||||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||||
|
|
||||||
skills.push(Skill::new(SkillKind::Aim));
|
skills.push(Skill::aim());
|
||||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
skills.push(Skill::speed(hit_window));
|
||||||
|
|
||||||
if fl {
|
if fl {
|
||||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
||||||
|
skills.push(Skill::flashlight(52.0 / scaling_factor.radius() as f64));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut prev_prev = None;
|
let mut prev_prev = None;
|
||||||
let mut prev = hit_objects.next().unwrap();
|
let mut prev = hit_objects.next().unwrap();
|
||||||
let mut prev_vals = None;
|
|
||||||
|
|
||||||
// First object has no predecessor and thus no strain, handle distinctly
|
// First object has no predecessor and thus no strain, handle distinctly
|
||||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
@@ -132,11 +124,10 @@ pub fn stars(
|
|||||||
let curr = hit_objects.next().unwrap();
|
let curr = hit_objects.next().unwrap();
|
||||||
let h = DifficultyObject::new(
|
let h = DifficultyObject::new(
|
||||||
&curr,
|
&curr,
|
||||||
&prev,
|
&mut prev,
|
||||||
prev_vals,
|
prev_prev.as_ref(),
|
||||||
prev_prev,
|
&scaling_factor,
|
||||||
scale_factor,
|
map_attributes.clock_rate,
|
||||||
scaling_factor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > current_section_end {
|
||||||
@@ -152,18 +143,16 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
|
||||||
prev = curr;
|
prev = curr;
|
||||||
|
|
||||||
// Handle all other objects
|
// Handle all other objects
|
||||||
for curr in hit_objects {
|
for curr in hit_objects {
|
||||||
let h = DifficultyObject::new(
|
let h = DifficultyObject::new(
|
||||||
&curr,
|
&curr,
|
||||||
&prev,
|
&mut prev,
|
||||||
prev_vals,
|
prev_prev.as_ref(),
|
||||||
prev_prev,
|
&scaling_factor,
|
||||||
scale_factor,
|
map_attributes.clock_rate,
|
||||||
scaling_factor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > current_section_end {
|
||||||
@@ -180,7 +169,6 @@ pub fn stars(
|
|||||||
}
|
}
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
|
||||||
prev = curr;
|
prev = curr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,9 +212,9 @@ pub fn stars(
|
|||||||
.powf(1.0 / 1.1);
|
.powf(1.0 / 1.1);
|
||||||
|
|
||||||
let star_rating = if base_performance > 0.00001 {
|
let star_rating = if base_performance > 0.00001 {
|
||||||
1.12_f32.cbrt()
|
1.12_f64.cbrt()
|
||||||
* 0.027
|
* 0.027
|
||||||
* ((100_000.0 / (1.0_f32 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
* ((100_000.0 / (1.0_f64 / 1.1).exp2() * base_performance).cbrt() + 4.0)
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
@@ -258,7 +246,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
return Strains::default();
|
return Strains::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut raw_ar = map.ar;
|
let mut raw_ar = map.ar as f64;
|
||||||
let hr = mods.hr();
|
let hr = mods.hr();
|
||||||
|
|
||||||
if hr {
|
if hr {
|
||||||
@@ -268,19 +256,10 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let time_preempt = difficulty_range_ar(raw_ar);
|
let time_preempt = difficulty_range_ar(raw_ar);
|
||||||
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||||
let radius = OBJECT_RADIUS * scale;
|
|
||||||
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 mut params = ObjectParameters {
|
let mut params = ObjectParameters {
|
||||||
map,
|
map,
|
||||||
radius,
|
|
||||||
scaling_factor,
|
|
||||||
max_combo: 0,
|
max_combo: 0,
|
||||||
slider_state: SliderState::new(map),
|
slider_state: SliderState::new(map),
|
||||||
ticks: Vec::new(),
|
ticks: Vec::new(),
|
||||||
@@ -295,7 +274,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||||
hit_objects.extend(hit_objects_iter);
|
hit_objects.extend(hit_objects_iter);
|
||||||
|
|
||||||
let stack_threshold = time_preempt * map.stack_leniency;
|
let stack_threshold = time_preempt * map.stack_leniency as f64;
|
||||||
|
|
||||||
if map.version >= 6 {
|
if map.version >= 6 {
|
||||||
stacking(&mut hit_objects, stack_threshold);
|
stacking(&mut hit_objects, stack_threshold);
|
||||||
@@ -303,13 +282,13 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
old_stacking(&mut hit_objects, stack_threshold);
|
old_stacking(&mut hit_objects, stack_threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scale_factor = scale * -6.4;
|
// let scale_factor = (scaling_factor.scale * -6.4) as f32;
|
||||||
|
|
||||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||||
let stack_offset = h.stack_height * scale_factor;
|
// let stack_offset = Pos2::new(h.stack_height * scale_factor);
|
||||||
|
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
||||||
|
h.pos += stack_offset;
|
||||||
h.time /= map_attributes.clock_rate;
|
h.time /= map_attributes.clock_rate;
|
||||||
h.pos += Pos2::new(stack_offset);
|
|
||||||
|
|
||||||
h
|
h
|
||||||
});
|
});
|
||||||
@@ -317,16 +296,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
let fl = mods.fl();
|
let fl = mods.fl();
|
||||||
let mut skills = Vec::with_capacity(2 + fl as usize);
|
let mut skills = Vec::with_capacity(2 + fl as usize);
|
||||||
|
|
||||||
skills.push(Skill::new(SkillKind::Aim));
|
skills.push(Skill::aim());
|
||||||
skills.push(Skill::new(SkillKind::speed(hit_window)));
|
skills.push(Skill::speed(hit_window));
|
||||||
|
|
||||||
if fl {
|
if fl {
|
||||||
skills.push(Skill::new(SkillKind::flashlight(scaling_factor)));
|
// NOTE: Instead of having `NORMALIZED_RADIUS` as dividend, it still uses 52.0.
|
||||||
|
skills.push(Skill::flashlight(52.0 / scaling_factor.radius() as f64));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut prev_prev = None;
|
let mut prev_prev = None;
|
||||||
let mut prev = hit_objects.next().unwrap();
|
let mut prev = hit_objects.next().unwrap();
|
||||||
let mut prev_vals = None;
|
|
||||||
|
|
||||||
// First object has no predecessor and thus no strain, handle distinctly
|
// First object has no predecessor and thus no strain, handle distinctly
|
||||||
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
let mut current_section_end = (prev.time / SECTION_LEN).ceil() * SECTION_LEN;
|
||||||
@@ -335,11 +314,10 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
let curr = hit_objects.next().unwrap();
|
let curr = hit_objects.next().unwrap();
|
||||||
let h = DifficultyObject::new(
|
let h = DifficultyObject::new(
|
||||||
&curr,
|
&curr,
|
||||||
&prev,
|
&mut prev,
|
||||||
prev_vals,
|
prev_prev.as_ref(),
|
||||||
prev_prev,
|
&scaling_factor,
|
||||||
scale_factor,
|
map_attributes.clock_rate,
|
||||||
scaling_factor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > current_section_end {
|
||||||
@@ -355,18 +333,16 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
|
||||||
prev = curr;
|
prev = curr;
|
||||||
|
|
||||||
// Handle all other objects
|
// Handle all other objects
|
||||||
for curr in hit_objects {
|
for curr in hit_objects {
|
||||||
let h = DifficultyObject::new(
|
let h = DifficultyObject::new(
|
||||||
&curr,
|
&curr,
|
||||||
&prev,
|
&mut prev,
|
||||||
prev_vals,
|
prev_prev.as_ref(),
|
||||||
prev_prev,
|
&scaling_factor,
|
||||||
scale_factor,
|
map_attributes.clock_rate,
|
||||||
scaling_factor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
while h.base.time > current_section_end {
|
while h.base.time > current_section_end {
|
||||||
@@ -383,7 +359,6 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prev_prev = Some(prev);
|
prev_prev = Some(prev);
|
||||||
prev_vals = Some((h.jump_dist, h.strain_time));
|
|
||||||
prev = curr;
|
prev = curr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +393,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||||
let mut extended_start_idx = 0;
|
let mut extended_start_idx = 0;
|
||||||
let extended_end_idx = hit_objects.len() - 1;
|
let extended_end_idx = hit_objects.len() - 1;
|
||||||
|
|
||||||
@@ -529,7 +504,7 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||||
for i in 0..hit_objects.len() {
|
for i in 0..hit_objects.len() {
|
||||||
if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() {
|
if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() {
|
||||||
continue;
|
continue;
|
||||||
@@ -557,11 +532,7 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OSU_AR_MAX: f32 = 450.0;
|
|
||||||
const OSU_AR_AVG: f32 = 1200.0;
|
|
||||||
const OSU_AR_MIN: f32 = 1800.0;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn difficulty_range_ar(ar: f32) -> f32 {
|
fn difficulty_range_ar(ar: f64) -> f64 {
|
||||||
crate::difficulty_range(ar, OSU_AR_MAX, OSU_AR_AVG, OSU_AR_MIN)
|
crate::difficulty_range(ar, 450.0, 1200.0, 1800.0)
|
||||||
}
|
}
|
||||||
|
|||||||
+130
-86
@@ -6,35 +6,47 @@ use crate::{
|
|||||||
Beatmap,
|
Beatmap,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
const LEGACY_LAST_TICK_OFFSET: f64 = 36.0;
|
||||||
const BASE_SCORING_DISTANCE: f32 = 100.0;
|
const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||||
|
|
||||||
pub(crate) struct OsuObject {
|
pub(crate) struct OsuObject {
|
||||||
pub(crate) time: f32,
|
pub(crate) time: f64,
|
||||||
pub(crate) pos: Pos2,
|
pub(crate) pos: Pos2,
|
||||||
pub(crate) stack_height: f32,
|
pub(crate) stack_height: f32,
|
||||||
kind: OsuObjectKind,
|
pub(crate) kind: OsuObjectKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OsuObjectKind {
|
pub(crate) enum OsuObjectKind {
|
||||||
Circle,
|
Circle,
|
||||||
Slider {
|
Slider {
|
||||||
end_time: f32,
|
end_time: f64,
|
||||||
end_pos: Pos2,
|
end_pos: Pos2,
|
||||||
lazy_end_pos: Pos2,
|
lazy_end_pos: Pos2,
|
||||||
travel_dist: f32,
|
nested_objects: Vec<NestedObject>,
|
||||||
},
|
},
|
||||||
Spinner {
|
Spinner {
|
||||||
end_time: f32,
|
end_time: f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct NestedObject {
|
||||||
|
pub(crate) pos: Pos2,
|
||||||
|
pub(crate) time: f64,
|
||||||
|
pub(crate) kind: NestedObjectKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub(crate) enum NestedObjectKind {
|
||||||
|
Repeat,
|
||||||
|
Tail,
|
||||||
|
Tick,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct ObjectParameters<'a> {
|
pub(crate) struct ObjectParameters<'a> {
|
||||||
pub(crate) map: &'a Beatmap,
|
pub(crate) map: &'a Beatmap,
|
||||||
pub(crate) radius: f32,
|
|
||||||
pub(crate) scaling_factor: f32,
|
|
||||||
pub(crate) max_combo: usize,
|
pub(crate) max_combo: usize,
|
||||||
pub(crate) ticks: Vec<f32>,
|
pub(crate) ticks: Vec<(Pos2, f64)>,
|
||||||
pub(crate) slider_state: SliderState<'a>,
|
pub(crate) slider_state: SliderState<'a>,
|
||||||
pub(crate) curve_bufs: CurveBuffers,
|
pub(crate) curve_bufs: CurveBuffers,
|
||||||
}
|
}
|
||||||
@@ -44,8 +56,6 @@ impl OsuObject {
|
|||||||
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||||
let ObjectParameters {
|
let ObjectParameters {
|
||||||
map,
|
map,
|
||||||
radius,
|
|
||||||
scaling_factor,
|
|
||||||
max_combo,
|
max_combo,
|
||||||
ticks,
|
ticks,
|
||||||
slider_state,
|
slider_state,
|
||||||
@@ -71,18 +81,15 @@ impl OsuObject {
|
|||||||
repeats,
|
repeats,
|
||||||
control_points,
|
control_points,
|
||||||
} => {
|
} => {
|
||||||
// Key values which are computed here
|
|
||||||
let mut lazy_end_pos = pos;
|
|
||||||
let mut travel_dist = 0.0;
|
|
||||||
|
|
||||||
// Responsible for timing point values
|
// Responsible for timing point values
|
||||||
slider_state.update(h.start_time);
|
slider_state.update(h.start_time);
|
||||||
|
|
||||||
let span_count = (*repeats + 1) as f32;
|
let span_count = (*repeats + 1) as f64;
|
||||||
|
|
||||||
let approx_follow_circle_radius = *radius * 3.0;
|
|
||||||
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
let mut tick_dist = 100.0 * map.slider_mult / map.tick_rate;
|
||||||
|
|
||||||
|
// * prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||||
|
// * this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||||
if map.version >= 8 {
|
if map.version >= 8 {
|
||||||
tick_dist /=
|
tick_dist /=
|
||||||
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
(100.0 / slider_state.slider_velocity).max(10.0).min(1000.0) / 100.0;
|
||||||
@@ -99,35 +106,6 @@ impl OsuObject {
|
|||||||
let duration = end_time - h.start_time;
|
let duration = end_time - h.start_time;
|
||||||
let span_duration = duration / span_count;
|
let span_duration = duration / span_count;
|
||||||
|
|
||||||
// Called on each slider object except for the head.
|
|
||||||
// Increases combo and adjusts `lazy_end_pos` and `travel_dist`
|
|
||||||
// w.r.t. the object position at the given time on the slider curve.
|
|
||||||
let mut compute_vertex = |mut progress: f32| {
|
|
||||||
*max_combo += 1;
|
|
||||||
|
|
||||||
if progress % 2.0 >= 1.0 {
|
|
||||||
progress = 1.0 - progress % 1.0;
|
|
||||||
} else {
|
|
||||||
progress %= 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
|
||||||
|
|
||||||
if hr {
|
|
||||||
curr_pos.y = 384.0 - curr_pos.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
let diff = curr_pos - lazy_end_pos;
|
|
||||||
let mut dist = diff.length();
|
|
||||||
|
|
||||||
if dist > approx_follow_circle_radius {
|
|
||||||
// * The cursor would be outside the follow circle, we need to move it
|
|
||||||
dist -= approx_follow_circle_radius;
|
|
||||||
lazy_end_pos += diff.normalize() * dist;
|
|
||||||
travel_dist += dist;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// * A very lenient maximum length of a slider for ticks to be generated.
|
// * A very lenient maximum length of a slider for ticks to be generated.
|
||||||
// * This exists for edge cases such as /b/1573664 where the beatmap has
|
// * This exists for edge cases such as /b/1573664 where the beatmap has
|
||||||
// * been edited by the user, and should never be reached in normal usage.
|
// * been edited by the user, and should never be reached in normal usage.
|
||||||
@@ -141,54 +119,132 @@ impl OsuObject {
|
|||||||
|
|
||||||
ticks.clear();
|
ticks.clear();
|
||||||
ticks.reserve((len / tick_dist) as usize);
|
ticks.reserve((len / tick_dist) as usize);
|
||||||
|
let mut nested_objects =
|
||||||
|
Vec::with_capacity((len * span_count / tick_dist) as usize);
|
||||||
|
|
||||||
// Tick of the first span
|
// Ticks of the first span
|
||||||
while curr_dist < len - min_dist_from_end {
|
while curr_dist < len - min_dist_from_end {
|
||||||
let progress = curr_dist / len;
|
let progress = curr_dist / len;
|
||||||
|
|
||||||
compute_vertex(progress);
|
let curr_time = h.start_time + progress * span_duration;
|
||||||
ticks.push(progress);
|
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||||
|
|
||||||
|
if hr {
|
||||||
|
curr_pos.y = 384.0 - curr_pos.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tick = NestedObject {
|
||||||
|
pos: curr_pos,
|
||||||
|
time: curr_time,
|
||||||
|
kind: NestedObjectKind::Tick,
|
||||||
|
};
|
||||||
|
|
||||||
|
nested_objects.push(tick);
|
||||||
|
ticks.push((curr_pos, curr_time));
|
||||||
|
|
||||||
curr_dist += tick_dist;
|
curr_dist += tick_dist;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other spans
|
// Other spans
|
||||||
for span_idx in 1..=*repeats {
|
for span_idx in 1..=*repeats {
|
||||||
let progress = (span_idx % 2 == 1) as u8 as f32;
|
let progress = (span_idx % 2 == 1) as u8 as f64;
|
||||||
|
let span_idx_f64 = span_idx as f64;
|
||||||
|
|
||||||
// Reverse tick
|
// Repeat point
|
||||||
compute_vertex(progress);
|
let curr_time = h.start_time + span_duration * span_idx_f64;
|
||||||
|
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||||
|
|
||||||
// Actual ticks
|
if hr {
|
||||||
|
curr_pos.y = 384.0 - curr_pos.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let repeat = NestedObject {
|
||||||
|
pos: curr_pos,
|
||||||
|
time: curr_time,
|
||||||
|
kind: NestedObjectKind::Repeat,
|
||||||
|
};
|
||||||
|
|
||||||
|
nested_objects.push(repeat);
|
||||||
|
|
||||||
|
// Ticks
|
||||||
if span_idx & 1 == 1 {
|
if span_idx & 1 == 1 {
|
||||||
ticks
|
// S-------->R | Span 0
|
||||||
.iter()
|
// 2 4 6 8 | => span_duration = 8
|
||||||
.rev()
|
// R<--------- | Span 1
|
||||||
.for_each(|&tick_progress| compute_vertex(tick_progress + progress));
|
// 16 14 12 10 | => offset = 1 * span_duration
|
||||||
|
// --------->R | Span 2
|
||||||
|
// 18 20 22 24 | => not reverse; simple case
|
||||||
|
// T<--------- | Span 3
|
||||||
|
// 32 30 28 26 | => offset = 3 * span_duration
|
||||||
|
//
|
||||||
|
// n = offset + tick
|
||||||
|
// 26 = 24 + 2
|
||||||
|
// 28 = 24 + 4
|
||||||
|
// 30 = 24 + 6
|
||||||
|
// 32 = 24 + 8
|
||||||
|
|
||||||
|
let offset = span_idx_f64 * span_duration;
|
||||||
|
|
||||||
|
let tick_iter = ticks.iter().rev().zip(ticks.iter()).map(
|
||||||
|
|((rev_pos, _), (_, time))| NestedObject {
|
||||||
|
pos: *rev_pos,
|
||||||
|
time: offset + time,
|
||||||
|
kind: NestedObjectKind::Tick,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
nested_objects.extend(tick_iter);
|
||||||
} else {
|
} else {
|
||||||
ticks
|
let tick_iter = ticks.iter().map(|(pos, time)| NestedObject {
|
||||||
.iter()
|
pos: *pos,
|
||||||
.for_each(|&tick_progress| compute_vertex(tick_progress + progress));
|
time: time + span_duration * span_idx_f64,
|
||||||
|
kind: NestedObjectKind::Tick,
|
||||||
|
});
|
||||||
|
|
||||||
|
nested_objects.extend(tick_iter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slider tail
|
// Slider tail
|
||||||
let final_span_start_time = h.start_time + *repeats as f32 * span_duration;
|
let final_span_start_time = h.start_time + *repeats as f64 * span_duration;
|
||||||
let final_span_end_time = (h.start_time + duration / 2.0)
|
let final_span_end_time = (h.start_time + duration / 2.0)
|
||||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||||
let progress = (*repeats % 2 == 1) as u8 as f32;
|
|
||||||
let final_progress =
|
|
||||||
(final_span_end_time - final_span_start_time) / span_duration + progress;
|
|
||||||
|
|
||||||
compute_vertex(final_progress);
|
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||||
|
let mut end_pos = h.pos + curve.position_at(progress);
|
||||||
let mut end_pos = h.pos + curve.position_at(1.0 - progress);
|
|
||||||
travel_dist *= *scaling_factor;
|
|
||||||
|
|
||||||
if hr {
|
if hr {
|
||||||
end_pos.y = 384.0 - end_pos.y;
|
end_pos.y = 384.0 - end_pos.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * we need to use the LegacyLastTick here for compatibility reasons (difficulty).
|
||||||
|
// * it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
|
||||||
|
// * if this is to change, we should revisit this.
|
||||||
|
let legacy_last_tick = NestedObject {
|
||||||
|
pos: end_pos,
|
||||||
|
time: final_span_end_time,
|
||||||
|
kind: NestedObjectKind::Tail,
|
||||||
|
};
|
||||||
|
|
||||||
|
nested_objects.push(legacy_last_tick);
|
||||||
|
*max_combo += nested_objects.len();
|
||||||
|
|
||||||
|
let lazy_travel_time = final_span_end_time - h.start_time;
|
||||||
|
let mut end_time_min = lazy_travel_time / span_duration;
|
||||||
|
|
||||||
|
if end_time_min % 2.0 >= 1.0 {
|
||||||
|
end_time_min = 1.0 - end_time_min % 1.0;
|
||||||
|
} else {
|
||||||
|
end_time_min %= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * temporary lazy end position until a real result can be derived.
|
||||||
|
let mut lazy_end_pos = h.pos + curve.position_at(end_time_min);
|
||||||
|
|
||||||
|
if hr {
|
||||||
|
lazy_end_pos.y = 384.0 - lazy_end_pos.y;
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
time: h.start_time,
|
time: h.start_time,
|
||||||
pos,
|
pos,
|
||||||
@@ -197,7 +253,7 @@ impl OsuObject {
|
|||||||
end_time: final_span_end_time,
|
end_time: final_span_end_time,
|
||||||
end_pos,
|
end_pos,
|
||||||
lazy_end_pos,
|
lazy_end_pos,
|
||||||
travel_dist,
|
nested_objects,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,15 +272,7 @@ impl OsuObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn travel_dist(&self) -> f32 {
|
pub(crate) fn end_time(&self) -> f64 {
|
||||||
match &self.kind {
|
|
||||||
OsuObjectKind::Slider { travel_dist, .. } => *travel_dist,
|
|
||||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn end_time(&self) -> f32 {
|
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
OsuObjectKind::Circle => self.time,
|
OsuObjectKind::Circle => self.time,
|
||||||
OsuObjectKind::Slider { end_time, .. } => *end_time,
|
OsuObjectKind::Slider { end_time, .. } => *end_time,
|
||||||
@@ -241,14 +289,10 @@ impl OsuObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn lazy_end_pos(&self, scale_factor: f32) -> Pos2 {
|
pub(crate) fn lazy_end_pos(&self, stack_offset: Pos2) -> Pos2 {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||||
OsuObjectKind::Slider { lazy_end_pos, .. } => {
|
OsuObjectKind::Slider { lazy_end_pos, .. } => *lazy_end_pos + stack_offset,
|
||||||
let stack_offset = scale_factor * self.stack_height;
|
|
||||||
|
|
||||||
*lazy_end_pos + Pos2::new(stack_offset)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use crate::parse::Pos2;
|
||||||
|
|
||||||
|
use super::NORMALIZED_RADIUS;
|
||||||
|
|
||||||
|
const OBJECT_RADIUS: f32 = 64.0;
|
||||||
|
|
||||||
|
pub(crate) struct ScalingFactor {
|
||||||
|
adjusted_factor: f32,
|
||||||
|
factor: f32,
|
||||||
|
radius: f32,
|
||||||
|
scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScalingFactor {
|
||||||
|
pub(crate) fn new(cs: f64) -> Self {
|
||||||
|
let scale = (1.0 - 0.7 * (cs as f32 - 5.0) / 5.0) / 2.0;
|
||||||
|
|
||||||
|
let radius = OBJECT_RADIUS * scale;
|
||||||
|
let factor = NORMALIZED_RADIUS / radius;
|
||||||
|
|
||||||
|
let adjusted_factor = if radius < 30.0 {
|
||||||
|
factor * (1.0 + (30.0 - radius).min(5.0) / 50.0)
|
||||||
|
} else {
|
||||||
|
factor
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
adjusted_factor,
|
||||||
|
factor,
|
||||||
|
radius,
|
||||||
|
scale: scale * -6.4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn raw(&self) -> f64 {
|
||||||
|
self.factor as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn adjusted(&self) -> f32 {
|
||||||
|
self.adjusted_factor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn radius(&self) -> f32 {
|
||||||
|
self.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stack_offset(&self, stack_height: f32) -> Pos2 {
|
||||||
|
Pos2::new(stack_height * self.scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-15
@@ -4,23 +4,38 @@ use super::{skill_kind::calculate_speed_rhythm_bonus, DifficultyObject, SkillKin
|
|||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
const REDUCED_STRAIN_BASELINE: f32 = 0.75;
|
const REDUCED_STRAIN_BASELINE: f64 = 0.75;
|
||||||
|
|
||||||
pub(crate) struct Skill {
|
pub(crate) struct Skill {
|
||||||
curr_strain: f32,
|
curr_strain: f64,
|
||||||
curr_section_peak: f32,
|
curr_section_peak: f64,
|
||||||
|
|
||||||
kind: SkillKind,
|
kind: SkillKind,
|
||||||
pub(crate) strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Skill {
|
impl Skill {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new(kind: SkillKind) -> Self {
|
pub(crate) fn aim() -> Self {
|
||||||
|
Self::new(SkillKind::aim())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn flashlight(scaling_factor: f64) -> Self {
|
||||||
|
Self::new(SkillKind::flashlight(scaling_factor))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn speed(hit_window: f64) -> Self {
|
||||||
|
Self::new(SkillKind::speed(hit_window))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn new(kind: SkillKind) -> Self {
|
||||||
Self {
|
Self {
|
||||||
curr_strain: 1.0,
|
curr_strain: 0.0,
|
||||||
curr_section_peak: 0.0,
|
curr_section_peak: 0.0,
|
||||||
|
|
||||||
kind,
|
kind,
|
||||||
@@ -44,18 +59,18 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
// The maximum strain of the new section is not zero by default
|
// The maximum strain of the new section is not zero by default
|
||||||
self.curr_section_peak = self.calculate_initial_strain(time);
|
self.curr_section_peak = self.calculate_initial_strain(time);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn difficulty_value(&mut self) -> f32 {
|
pub(crate) fn difficulty_value(&mut self) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
let decay_weight = self.kind.decay_weight();
|
let decay_weight = self.kind.decay_weight();
|
||||||
|
|
||||||
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
let (reduced_section_count, difficulty_multiplier) = self.kind.difficulty_values();
|
||||||
let reduced_section_count_f32 = reduced_section_count as f32;
|
let reduced_section_count_f64 = reduced_section_count as f64;
|
||||||
|
|
||||||
self.strain_peaks
|
self.strain_peaks
|
||||||
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
|
||||||
@@ -63,7 +78,7 @@ impl Skill {
|
|||||||
let peaks = self.strain_peaks.iter_mut();
|
let peaks = self.strain_peaks.iter_mut();
|
||||||
|
|
||||||
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
for (i, strain) in peaks.take(reduced_section_count).enumerate() {
|
||||||
let clamped = (i as f32 / reduced_section_count_f32).clamp(0.0, 1.0);
|
let clamped = (i as f64 / reduced_section_count_f64).clamp(0.0, 1.0);
|
||||||
let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
|
let scale = (math_util::lerp(1.0, 10.0, clamped)).log10();
|
||||||
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
*strain *= math_util::lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);
|
||||||
}
|
}
|
||||||
@@ -79,22 +94,22 @@ impl Skill {
|
|||||||
difficulty * difficulty_multiplier
|
difficulty * difficulty_multiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn calculate_initial_strain(&self, time: f32) -> f32 {
|
pub(crate) fn calculate_initial_strain(&self, time: f64) -> f64 {
|
||||||
let prev_time = self.prev_time.unwrap_or(0.0);
|
let prev_time = self.prev_time.unwrap_or(0.0);
|
||||||
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
|
let decayed_strain = self.curr_strain * self.kind.strain_decay(time - prev_time);
|
||||||
|
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
SkillKind::Aim | SkillKind::Flashlight { .. } => decayed_strain,
|
SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => decayed_strain,
|
||||||
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
|
SkillKind::Speed { curr_rhythm, .. } => curr_rhythm * decayed_strain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f32 {
|
pub(crate) fn strain_value_at(&mut self, curr: &DifficultyObject<'_>) -> f64 {
|
||||||
self.curr_strain *= self.kind.strain_decay(curr.delta);
|
self.curr_strain *= self.kind.strain_decay(curr.delta);
|
||||||
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
|
self.curr_strain += self.kind.strain_value_of(curr) * self.kind.skill_multiplier();
|
||||||
|
|
||||||
match &mut self.kind {
|
match &mut self.kind {
|
||||||
SkillKind::Aim | SkillKind::Flashlight { .. } => self.curr_strain,
|
SkillKind::Aim { .. } | SkillKind::Flashlight { .. } => self.curr_strain,
|
||||||
SkillKind::Speed {
|
SkillKind::Speed {
|
||||||
curr_rhythm,
|
curr_rhythm,
|
||||||
history,
|
history,
|
||||||
|
|||||||
+238
-75
@@ -1,47 +1,81 @@
|
|||||||
use std::{collections::VecDeque, f32::consts::PI, iter};
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
f64::consts::{FRAC_PI_2, PI},
|
||||||
|
iter,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{math_util, parse::Pos2};
|
use crate::{math_util, parse::Pos2};
|
||||||
|
|
||||||
use super::DifficultyObject;
|
use super::DifficultyObject;
|
||||||
|
|
||||||
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
|
const SINGLE_SPACING_TRESHOLD: f64 = 125.0;
|
||||||
|
|
||||||
const MIN_SPEED_BONUS: f32 = 75.0;
|
const SPEED_BALANCING_FACTOR: f64 = 40.0;
|
||||||
const SPEED_BALANCING_FACTOR: f32 = 40.0;
|
|
||||||
|
|
||||||
const TIMING_THRESHOLD: f32 = 107.0;
|
const AIM_SKILL_MULTIPLIER: f64 = 23.25;
|
||||||
|
const AIM_STRAIN_DECAY_BASE: f64 = 0.15;
|
||||||
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
|
const AIM_DECAY_WEIGHT: f64 = 0.9;
|
||||||
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
|
const AIM_DIFFICULTY_MULTIPLIER: f64 = 1.06;
|
||||||
const AIM_DECAY_WEIGHT: f32 = 0.9;
|
|
||||||
const AIM_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
|
||||||
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
const AIM_REDUCED_SECTION_COUNT: usize = 10;
|
||||||
|
|
||||||
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
|
const AIM_HISTORY_LENGTH: usize = 2;
|
||||||
|
const AIM_WIDE_ANGLE_MULTIPLIER: f64 = 1.5;
|
||||||
|
const AIM_ACUTE_ANGLE_MULTIPLIER: f64 = 2.0;
|
||||||
|
const AIM_SLIDER_MULTIPLIER: f64 = 1.5;
|
||||||
|
const AIM_VELOCITY_CHANGE_MULTIPLIER: f64 = 0.75;
|
||||||
|
|
||||||
const SPEED_SKILL_MULTIPLIER: f32 = 1375.0;
|
const SPEED_SKILL_MULTIPLIER: f64 = 1375.0;
|
||||||
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
|
const SPEED_STRAIN_DECAY_BASE: f64 = 0.3;
|
||||||
const SPEED_DECAY_WEIGHT: f32 = 0.9;
|
const SPEED_DECAY_WEIGHT: f64 = 0.9;
|
||||||
const SPEED_DIFFICULTY_MULTIPLIER: f32 = 1.04;
|
const SPEED_DIFFICULTY_MULTIPLIER: f64 = 1.04;
|
||||||
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
const SPEED_REDUCED_SECTION_COUNT: usize = 5;
|
||||||
|
|
||||||
const SPEED_HISTORY_LENGTH: usize = 32;
|
const SPEED_HISTORY_LENGTH: usize = 32;
|
||||||
const SPEED_HISTORY_TIME_MAX: f32 = 5000.0;
|
const SPEED_RHYTHM_MULTIPLIER: f64 = 0.75;
|
||||||
const SPEED_RHYTHM_MULTIPLIER: f32 = 0.75;
|
const SPEED_HISTORY_TIME_MAX: f64 = 5000.0; // * 5 seconds of calculate_speed_rhythm_bonus max
|
||||||
|
const MIN_SPEED_BONUS: f64 = 75.0; // * ~200BPM
|
||||||
|
|
||||||
const FLASHLIGHT_SKILL_MULTIPLIER: f32 = 0.15;
|
const FLASHLIGHT_SKILL_MULTIPLIER: f64 = 0.15;
|
||||||
const FLASHLIGHT_STRAIN_DECAY_BASE: f32 = 0.15;
|
const FLASHLIGHT_STRAIN_DECAY_BASE: f64 = 0.15;
|
||||||
const FLASHLIGHT_DECAY_WEIGHT: f32 = 1.0;
|
const FLASHLIGHT_DECAY_WEIGHT: f64 = 1.0;
|
||||||
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f32 = 1.06;
|
const FLASHLIGHT_DIFFICULTY_MULTIPLIER: f64 = 1.06;
|
||||||
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
const FLASHLIGHT_REDUCED_SECTION_COUNT: usize = 10;
|
||||||
|
|
||||||
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
const FLASHLIGHT_HISTORY_LENGTH: usize = 10;
|
||||||
|
|
||||||
|
pub(crate) struct AimHistoryEntry {
|
||||||
|
angle: Option<f64>,
|
||||||
|
is_slider: bool,
|
||||||
|
is_spinner: bool,
|
||||||
|
strain_time: f64,
|
||||||
|
jump_dist: f64,
|
||||||
|
movement_dist: f64,
|
||||||
|
movement_time: f64,
|
||||||
|
travel_dist: f64,
|
||||||
|
travel_time: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&DifficultyObject<'_>> for AimHistoryEntry {
|
||||||
|
fn from(h: &DifficultyObject<'_>) -> Self {
|
||||||
|
Self {
|
||||||
|
angle: h.angle,
|
||||||
|
is_slider: h.base.is_slider(),
|
||||||
|
is_spinner: h.base.is_spinner(),
|
||||||
|
strain_time: h.strain_time,
|
||||||
|
jump_dist: h.jump_dist,
|
||||||
|
movement_dist: h.movement_dist,
|
||||||
|
movement_time: h.movement_time,
|
||||||
|
travel_dist: h.travel_dist,
|
||||||
|
travel_time: h.travel_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct FlashlightHistoryEntry {
|
pub(crate) struct FlashlightHistoryEntry {
|
||||||
end_pos: Pos2,
|
end_pos: Pos2,
|
||||||
is_spinner: bool,
|
is_spinner: bool,
|
||||||
jump_dist: f32,
|
jump_dist: f64,
|
||||||
strain_time: f32,
|
strain_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
||||||
@@ -57,8 +91,8 @@ impl From<&DifficultyObject<'_>> for FlashlightHistoryEntry {
|
|||||||
|
|
||||||
pub(crate) struct SpeedHistoryEntry {
|
pub(crate) struct SpeedHistoryEntry {
|
||||||
is_slider: bool,
|
is_slider: bool,
|
||||||
start_time: f32,
|
start_time: f64,
|
||||||
strain_time: f32,
|
strain_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
||||||
@@ -72,37 +106,45 @@ impl From<&DifficultyObject<'_>> for SpeedHistoryEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum SkillKind {
|
pub(crate) enum SkillKind {
|
||||||
Aim,
|
Aim {
|
||||||
|
history: VecDeque<AimHistoryEntry>,
|
||||||
|
},
|
||||||
Flashlight {
|
Flashlight {
|
||||||
history: VecDeque<FlashlightHistoryEntry>,
|
history: VecDeque<FlashlightHistoryEntry>,
|
||||||
scaling_factor: f32,
|
scaling_factor: f64,
|
||||||
},
|
},
|
||||||
Speed {
|
Speed {
|
||||||
curr_rhythm: f32,
|
curr_rhythm: f64,
|
||||||
history: VecDeque<SpeedHistoryEntry>,
|
history: VecDeque<SpeedHistoryEntry>,
|
||||||
hit_window: f32,
|
hit_window: f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SkillKind {
|
impl SkillKind {
|
||||||
pub(crate) fn flashlight(scaling_factor: f32) -> Self {
|
pub(crate) fn aim() -> Self {
|
||||||
|
Self::Aim {
|
||||||
|
history: VecDeque::with_capacity(AIM_HISTORY_LENGTH + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn flashlight(scaling_factor: f64) -> Self {
|
||||||
Self::Flashlight {
|
Self::Flashlight {
|
||||||
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH),
|
history: VecDeque::with_capacity(FLASHLIGHT_HISTORY_LENGTH + 1),
|
||||||
scaling_factor,
|
scaling_factor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn speed(hit_window: f32) -> Self {
|
pub(crate) fn speed(hit_window: f64) -> Self {
|
||||||
Self::Speed {
|
Self::Speed {
|
||||||
curr_rhythm: 1.0,
|
curr_rhythm: 1.0,
|
||||||
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH),
|
history: VecDeque::with_capacity(SPEED_HISTORY_LENGTH + 1),
|
||||||
hit_window,
|
hit_window,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pre_process(&mut self) {
|
pub(crate) fn pre_process(&mut self) {
|
||||||
match self {
|
match self {
|
||||||
Self::Aim => {}
|
Self::Aim { history } => history.truncate(AIM_HISTORY_LENGTH),
|
||||||
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
|
Self::Flashlight { history, .. } => history.truncate(FLASHLIGHT_HISTORY_LENGTH),
|
||||||
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
|
Self::Speed { history, .. } => history.truncate(SPEED_HISTORY_LENGTH),
|
||||||
}
|
}
|
||||||
@@ -110,43 +152,159 @@ impl SkillKind {
|
|||||||
|
|
||||||
pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
|
pub(crate) fn post_process(&mut self, current: &DifficultyObject<'_>) {
|
||||||
match self {
|
match self {
|
||||||
Self::Aim => {}
|
Self::Aim { history } => history.push_front(current.into()),
|
||||||
Self::Flashlight { history, .. } => history.push_front(current.into()),
|
Self::Flashlight { history, .. } => history.push_front(current.into()),
|
||||||
Self::Speed { history, .. } => history.push_front(current.into()),
|
Self::Speed { history, .. } => history.push_front(current.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f32 {
|
pub(crate) fn strain_value_of(&self, curr: &DifficultyObject<'_>) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Aim => {
|
Self::Aim { history } => {
|
||||||
if curr.base.is_spinner() {
|
if curr.base.is_spinner() || history.len() < 2 || history[0].is_spinner {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut aim_strain = 0.0;
|
let prev = &history[0];
|
||||||
|
let prev_prev = &history[1];
|
||||||
|
|
||||||
if let Some((prev_jump_dist, prev_strain_time)) = curr.prev {
|
// * Calculate the velocity to the current hitobject,
|
||||||
if let Some(angle) = curr.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
|
// * which starts with a base distance / time assuming the last object is a hitcircle.
|
||||||
let scale = 90.0;
|
let mut curr_velocity = curr.jump_dist / curr.strain_time;
|
||||||
|
|
||||||
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
|
// * But if the last object is a slider, then we extend the
|
||||||
* (prev_jump_dist - scale).max(0.0)
|
// * travel velocity through the slider into the current object.
|
||||||
* (curr.jump_dist - scale).max(0.0))
|
if prev.is_slider {
|
||||||
.sqrt();
|
// * calculate the movement velocity from slider end to current object
|
||||||
|
let movement_velocity = curr.movement_dist / curr.movement_time;
|
||||||
|
|
||||||
aim_strain = 1.4 * apply_diminishing_exp(angle_bonus.max(0.0))
|
// * calculate the slider velocity from slider head to slider end.
|
||||||
/ (TIMING_THRESHOLD).max(prev_strain_time)
|
let travel_velocity = curr.travel_dist / curr.travel_time;
|
||||||
|
|
||||||
|
// * take the larger total combined velocity.
|
||||||
|
curr_velocity = curr_velocity.max(movement_velocity + travel_velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// * As above, do the same for the previous hitobject.
|
||||||
|
let mut prev_velocity = prev.jump_dist / prev.strain_time;
|
||||||
|
|
||||||
|
if prev_prev.is_slider {
|
||||||
|
let movement_velocity = prev.movement_dist / prev.movement_time;
|
||||||
|
let travel_velocity = prev.travel_dist / prev.travel_time;
|
||||||
|
prev_velocity = prev_velocity.max(movement_velocity + travel_velocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut wide_angle_bonus = 0.0;
|
||||||
|
let mut acute_angle_bonus = 0.0;
|
||||||
|
let mut slider_bonus = 0.0;
|
||||||
|
let mut velocity_change_bonus = 0.0;
|
||||||
|
|
||||||
|
// * Start strain with regular velocity
|
||||||
|
let mut aim_strain = curr_velocity;
|
||||||
|
|
||||||
|
// * If rhythms are the same.
|
||||||
|
if curr.strain_time.max(prev.strain_time)
|
||||||
|
< 1.25 * curr.strain_time.min(prev.strain_time)
|
||||||
|
{
|
||||||
|
if let (Some(curr_angle), Some(prev_angle), Some(prev_prev_angle)) =
|
||||||
|
(curr.angle, prev.angle, prev_prev.angle)
|
||||||
|
{
|
||||||
|
// * Rewarding angles, take the smaller velocity as base.
|
||||||
|
let angle_bonus = curr_velocity.min(prev_velocity);
|
||||||
|
|
||||||
|
wide_angle_bonus = calculate_wide_angle_bonus(curr_angle);
|
||||||
|
|
||||||
|
// * Only bufff delta_time exceeding 300 bpm 1/2.
|
||||||
|
if curr.strain_time <= 100.0 {
|
||||||
|
let curr_bonus = calculate_acute_angle_bonus(curr_angle);
|
||||||
|
|
||||||
|
// * Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||||
|
let prev_bonus = calculate_acute_angle_bonus(prev_angle);
|
||||||
|
|
||||||
|
// * The maximum velocity we buff is equal to 125 / strainTime
|
||||||
|
let angle_bonus = angle_bonus.min(125.0 / curr.strain_time);
|
||||||
|
|
||||||
|
// * scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||||
|
let base1 =
|
||||||
|
(FRAC_PI_2 * ((100.0 - curr.strain_time) / 25.0).min(1.0)).sin();
|
||||||
|
|
||||||
|
// * Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||||
|
let base2 = (FRAC_PI_2 * (curr.jump_dist.clamp(50.0, 100.0) - 50.0)
|
||||||
|
/ 50.0)
|
||||||
|
.sin();
|
||||||
|
|
||||||
|
acute_angle_bonus = curr_bonus
|
||||||
|
* prev_bonus
|
||||||
|
* angle_bonus
|
||||||
|
* base1
|
||||||
|
* base1
|
||||||
|
* base2
|
||||||
|
* base2
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Penalize wide angles if they're repeated,
|
||||||
|
// * reducing the penalty as the lastAngle gets more acute.
|
||||||
|
let base = calculate_wide_angle_bonus(prev_angle);
|
||||||
|
wide_angle_bonus *=
|
||||||
|
angle_bonus * (1.0 - wide_angle_bonus.min(base * base * base));
|
||||||
|
|
||||||
|
// * Penalize acute angles if they're repeated,
|
||||||
|
// * reducing the penalty as the lastLastAngle gets more obtuse.
|
||||||
|
let base = calculate_acute_angle_bonus(prev_prev_angle);
|
||||||
|
acute_angle_bonus *=
|
||||||
|
0.5 + 0.5 * (1.0 - acute_angle_bonus.min(base * base * base));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let jump_dist_exp = apply_diminishing_exp(curr.jump_dist);
|
if prev_velocity.max(curr_velocity).abs() > f64::EPSILON {
|
||||||
let travel_dist_exp = apply_diminishing_exp(curr.travel_dist);
|
// * We want to use the average velocity over the whole object when
|
||||||
|
// * awarding differences, not the individual jump and slider path velocities.
|
||||||
|
prev_velocity = (prev.jump_dist + prev.travel_dist) / prev.strain_time;
|
||||||
|
curr_velocity = (curr.jump_dist + curr.travel_dist) / curr.strain_time;
|
||||||
|
|
||||||
let dist_exp =
|
let velocity_diff = (prev_velocity - curr_velocity).abs();
|
||||||
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
|
|
||||||
|
|
||||||
(aim_strain + dist_exp / (curr.strain_time).max(TIMING_THRESHOLD))
|
// * Scale with ratio of difference compared to 0.5 * max dist.
|
||||||
.max(dist_exp / curr.strain_time)
|
let base = (FRAC_PI_2 * velocity_diff / prev_velocity.max(curr_velocity)).sin();
|
||||||
|
let dist_ratio = base * base;
|
||||||
|
|
||||||
|
// * Reward for % distance up to 125 / strainTime
|
||||||
|
// * for overlaps where velocity is still changing.
|
||||||
|
let overlap_velocity_buff =
|
||||||
|
velocity_diff.min(125.0 / curr.strain_time.min(prev.strain_time));
|
||||||
|
|
||||||
|
// * Reward for % distance slowed down compared to previous,
|
||||||
|
// * paying attention to not award overlap
|
||||||
|
let base =
|
||||||
|
(FRAC_PI_2 * (curr.jump_dist.min(prev.jump_dist) / 100.0).min(1.0)).sin();
|
||||||
|
let non_overlap_velocity_buff = velocity_diff * base * base;
|
||||||
|
|
||||||
|
// * Choose the largest bonus, multiplied by ratio.
|
||||||
|
velocity_change_bonus =
|
||||||
|
overlap_velocity_buff.max(non_overlap_velocity_buff) * dist_ratio;
|
||||||
|
|
||||||
|
// * Penalize for rhythm changes.
|
||||||
|
let base = curr.strain_time.min(prev.strain_time)
|
||||||
|
/ curr.strain_time.max(prev.strain_time);
|
||||||
|
velocity_change_bonus *= base * base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if curr.travel_time.abs() > f64::EPSILON {
|
||||||
|
// * Reward sliders based on velocity
|
||||||
|
slider_bonus = curr.travel_dist / curr.travel_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Add in acute angle bonus or wide angle bonus + velocity change bonus,
|
||||||
|
// * whichever is larger
|
||||||
|
aim_strain += (acute_angle_bonus * AIM_ACUTE_ANGLE_MULTIPLIER).max(
|
||||||
|
wide_angle_bonus * AIM_WIDE_ANGLE_MULTIPLIER
|
||||||
|
+ velocity_change_bonus * AIM_VELOCITY_CHANGE_MULTIPLIER,
|
||||||
|
);
|
||||||
|
|
||||||
|
// * Add in additional slider velocity bonus.
|
||||||
|
aim_strain += slider_bonus * AIM_SLIDER_MULTIPLIER;
|
||||||
|
|
||||||
|
aim_strain
|
||||||
}
|
}
|
||||||
Self::Flashlight {
|
Self::Flashlight {
|
||||||
history,
|
history,
|
||||||
@@ -164,7 +322,7 @@ impl SkillKind {
|
|||||||
if let Some(prev) = history.next() {
|
if let Some(prev) = history.next() {
|
||||||
// Handle first entry distinctly for slight optimization
|
// Handle first entry distinctly for slight optimization
|
||||||
if !prev.is_spinner {
|
if !prev.is_spinner {
|
||||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
let jump_dist = (curr.base.pos - prev.end_pos).length() as f64;
|
||||||
cumulative_strain_time += prev.strain_time;
|
cumulative_strain_time += prev.strain_time;
|
||||||
|
|
||||||
// * We want to nerf objects that can be easily seen within the Flashlight circle radius
|
// * We want to nerf objects that can be easily seen within the Flashlight circle radius
|
||||||
@@ -180,7 +338,7 @@ impl SkillKind {
|
|||||||
|
|
||||||
for (factor, prev) in factors.zip(history) {
|
for (factor, prev) in factors.zip(history) {
|
||||||
if !prev.is_spinner {
|
if !prev.is_spinner {
|
||||||
let jump_dist = (curr.base.pos - prev.end_pos).length();
|
let jump_dist = (curr.base.pos - prev.end_pos).length() as f64;
|
||||||
cumulative_strain_time += prev.strain_time;
|
cumulative_strain_time += prev.strain_time;
|
||||||
|
|
||||||
// * We also want to nerf stacks so that only the first object of the stack is accounted for
|
// * We also want to nerf stacks so that only the first object of the stack is accounted for
|
||||||
@@ -240,9 +398,9 @@ impl SkillKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn difficulty_values(&self) -> (usize, f32) {
|
pub(crate) fn difficulty_values(&self) -> (usize, f64) {
|
||||||
match self {
|
match self {
|
||||||
Self::Aim => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
Self::Aim { .. } => (AIM_REDUCED_SECTION_COUNT, AIM_DIFFICULTY_MULTIPLIER),
|
||||||
Self::Flashlight { .. } => (
|
Self::Flashlight { .. } => (
|
||||||
FLASHLIGHT_REDUCED_SECTION_COUNT,
|
FLASHLIGHT_REDUCED_SECTION_COUNT,
|
||||||
FLASHLIGHT_DIFFICULTY_MULTIPLIER,
|
FLASHLIGHT_DIFFICULTY_MULTIPLIER,
|
||||||
@@ -252,34 +410,34 @@ impl SkillKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn skill_multiplier(&self) -> f32 {
|
pub(crate) fn skill_multiplier(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
|
SkillKind::Aim { .. } => AIM_SKILL_MULTIPLIER,
|
||||||
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
SkillKind::Flashlight { .. } => FLASHLIGHT_SKILL_MULTIPLIER,
|
||||||
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
|
SkillKind::Speed { .. } => SPEED_SKILL_MULTIPLIER,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn strain_decay_base(&self) -> f32 {
|
pub(crate) fn strain_decay_base(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
|
SkillKind::Aim { .. } => AIM_STRAIN_DECAY_BASE,
|
||||||
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
SkillKind::Flashlight { .. } => FLASHLIGHT_STRAIN_DECAY_BASE,
|
||||||
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
|
SkillKind::Speed { .. } => SPEED_STRAIN_DECAY_BASE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn decay_weight(&self) -> f32 {
|
pub(crate) fn decay_weight(&self) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
SkillKind::Aim => AIM_DECAY_WEIGHT,
|
SkillKind::Aim { .. } => AIM_DECAY_WEIGHT,
|
||||||
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
SkillKind::Flashlight { .. } => FLASHLIGHT_DECAY_WEIGHT,
|
||||||
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
|
SkillKind::Speed { .. } => SPEED_DECAY_WEIGHT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn strain_decay(&self, ms: f32) -> f32 {
|
pub(crate) fn strain_decay(&self, ms: f64) -> f64 {
|
||||||
self.strain_decay_base().powf(ms / 1000.0)
|
self.strain_decay_base().powf(ms / 1000.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,8 +445,8 @@ impl SkillKind {
|
|||||||
pub(crate) fn calculate_speed_rhythm_bonus(
|
pub(crate) fn calculate_speed_rhythm_bonus(
|
||||||
current: &DifficultyObject<'_>,
|
current: &DifficultyObject<'_>,
|
||||||
history: &VecDeque<SpeedHistoryEntry>,
|
history: &VecDeque<SpeedHistoryEntry>,
|
||||||
hit_window: f32,
|
hit_window: f64,
|
||||||
) -> f32 {
|
) -> f64 {
|
||||||
if current.base.is_spinner() {
|
if current.base.is_spinner() {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
@@ -298,7 +456,7 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
|||||||
let mut island_size = 1;
|
let mut island_size = 1;
|
||||||
let mut first_delta_switch = false;
|
let mut first_delta_switch = false;
|
||||||
let adjusted_hit_window = hit_window * 0.6;
|
let adjusted_hit_window = hit_window * 0.6;
|
||||||
let history_len = history.len() as f32;
|
let history_len = history.len() as f64;
|
||||||
|
|
||||||
// * Store the ratio of the current start of an island to buff for tighter rhythms
|
// * Store the ratio of the current start of an island to buff for tighter rhythms
|
||||||
let mut start_ratio = 0.0;
|
let mut start_ratio = 0.0;
|
||||||
@@ -312,9 +470,9 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
|||||||
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
|
(SPEED_HISTORY_TIME_MAX - (current.base.time - curr.start_time)).max(0.0)
|
||||||
/ SPEED_HISTORY_TIME_MAX;
|
/ SPEED_HISTORY_TIME_MAX;
|
||||||
|
|
||||||
if curr_historical_decay.abs() > f32::EPSILON {
|
if curr_historical_decay.abs() > f64::EPSILON {
|
||||||
// * Either we're limited by time or limited by object count
|
// * Either we're limited by time or limited by object count
|
||||||
curr_historical_decay = curr_historical_decay.min(i as f32 / history_len);
|
curr_historical_decay = curr_historical_decay.min(i as f64 / history_len);
|
||||||
|
|
||||||
let curr_delta = curr.strain_time;
|
let curr_delta = curr.strain_time;
|
||||||
let prev_delta = prev.strain_time;
|
let prev_delta = prev.strain_time;
|
||||||
@@ -362,8 +520,8 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
|||||||
|
|
||||||
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
rhythm_complexity_sum += (effective_ratio * start_ratio).sqrt()
|
||||||
* curr_historical_decay
|
* curr_historical_decay
|
||||||
* ((4 + island_size) as f32).sqrt()
|
* ((4 + island_size) as f64).sqrt()
|
||||||
* ((4 + prev_island_size) as f32).sqrt()
|
* ((4 + prev_island_size) as f64).sqrt()
|
||||||
/ 4.0;
|
/ 4.0;
|
||||||
|
|
||||||
start_ratio = effective_ratio;
|
start_ratio = effective_ratio;
|
||||||
@@ -390,7 +548,12 @@ pub(crate) fn calculate_speed_rhythm_bonus(
|
|||||||
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
|
(4.0 + rhythm_complexity_sum * SPEED_RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
fn calculate_wide_angle_bonus(angle: f64) -> f64 {
|
||||||
fn apply_diminishing_exp(val: f32) -> f32 {
|
let base = (3.0 / 4.0 * ((PI / 6.0).max(angle).min(5.0 / 6.0 * PI) - PI / 6.0)).sin();
|
||||||
val.powf(0.99)
|
|
||||||
|
base * base
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate_acute_angle_bonus(angle: f64) -> f64 {
|
||||||
|
1.0 - calculate_wide_angle_bonus(angle)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use crate::{Beatmap, ControlPoint, ControlPointIter};
|
|||||||
pub(crate) struct SliderState<'p> {
|
pub(crate) struct SliderState<'p> {
|
||||||
control_points: ControlPointIter<'p>,
|
control_points: ControlPointIter<'p>,
|
||||||
next: Option<ControlPoint>,
|
next: Option<ControlPoint>,
|
||||||
pub(crate) beat_len: f32,
|
pub(crate) beat_len: f64,
|
||||||
pub(crate) slider_velocity: f32,
|
pub(crate) slider_velocity: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'p> SliderState<'p> {
|
impl<'p> SliderState<'p> {
|
||||||
@@ -29,7 +29,7 @@ impl<'p> SliderState<'p> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn update(&mut self, time: f32) {
|
pub(crate) fn update(&mut self, time: f64) {
|
||||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||||
match next {
|
match next {
|
||||||
ControlPoint::Timing { beat_len, .. } => {
|
ControlPoint::Timing { beat_len, .. } => {
|
||||||
@@ -88,14 +88,14 @@ mod test {
|
|||||||
let mut state = SliderState::new(&map);
|
let mut state = SliderState::new(&map);
|
||||||
|
|
||||||
state.update(2.0);
|
state.update(2.0);
|
||||||
assert!((state.beat_len - 10.0).abs() <= f32::EPSILON);
|
assert!((state.beat_len - 10.0).abs() <= f64::EPSILON);
|
||||||
|
|
||||||
state.update(3.0);
|
state.update(3.0);
|
||||||
assert!((state.beat_len - 20.0).abs() <= f32::EPSILON);
|
assert!((state.beat_len - 20.0).abs() <= f64::EPSILON);
|
||||||
assert!((state.slider_velocity - 1.0).abs() <= f32::EPSILON);
|
assert!((state.slider_velocity - 1.0).abs() <= f64::EPSILON);
|
||||||
|
|
||||||
state.update(5.0);
|
state.update(5.0);
|
||||||
assert!((state.beat_len - 30.0).abs() <= f32::EPSILON);
|
assert!((state.beat_len - 30.0).abs() <= f64::EPSILON);
|
||||||
assert!((state.slider_velocity - 45.0).abs() <= f32::EPSILON);
|
assert!((state.slider_velocity - 45.0).abs() <= f64::EPSILON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-15
@@ -3,27 +3,27 @@ use crate::Mods;
|
|||||||
/// Summary struct for a [`Beatmap`](crate::Beatmap)'s attributes.
|
/// Summary struct for a [`Beatmap`](crate::Beatmap)'s attributes.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct BeatmapAttributes {
|
pub struct BeatmapAttributes {
|
||||||
pub ar: f32,
|
pub ar: f64,
|
||||||
pub od: f32,
|
pub od: f64,
|
||||||
pub cs: f32,
|
pub cs: f64,
|
||||||
pub hp: f32,
|
pub hp: f64,
|
||||||
pub clock_rate: f32,
|
pub clock_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BeatmapAttributes {
|
impl BeatmapAttributes {
|
||||||
const AR0_MS: f32 = 1800.0;
|
const AR0_MS: f64 = 1800.0;
|
||||||
const AR5_MS: f32 = 1200.0;
|
const AR5_MS: f64 = 1200.0;
|
||||||
const AR10_MS: f32 = 450.0;
|
const AR10_MS: f64 = 450.0;
|
||||||
const AR_MS_STEP_1: f32 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
const AR_MS_STEP_1: f64 = (Self::AR0_MS - Self::AR5_MS) / 5.0;
|
||||||
const AR_MS_STEP_2: f32 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
const AR_MS_STEP_2: f64 = (Self::AR5_MS - Self::AR10_MS) / 5.0;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
|
pub(crate) fn new(ar: f32, od: f32, cs: f32, hp: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ar,
|
ar: ar as f64,
|
||||||
od,
|
od: od as f64,
|
||||||
cs,
|
cs: cs as f64,
|
||||||
hp,
|
hp: hp as f64,
|
||||||
clock_rate: 1.0,
|
clock_rate: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ impl BeatmapAttributes {
|
|||||||
let multiplier = mods.od_ar_hp_multiplier();
|
let multiplier = mods.od_ar_hp_multiplier();
|
||||||
|
|
||||||
// AR
|
// AR
|
||||||
let mut ar = self.ar * multiplier;
|
let mut ar = (self.ar * multiplier) as f64;
|
||||||
let mut ar_ms = if ar <= 5.0 {
|
let mut ar_ms = if ar <= 5.0 {
|
||||||
Self::AR0_MS - Self::AR_MS_STEP_1 * ar
|
Self::AR0_MS - Self::AR_MS_STEP_1 * ar
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use std::cmp::Ordering;
|
|||||||
/// New rhythm speed change.
|
/// New rhythm speed change.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub struct TimingPoint {
|
pub struct TimingPoint {
|
||||||
pub beat_len: f32,
|
pub beat_len: f64,
|
||||||
pub time: f32,
|
pub time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for TimingPoint {
|
impl PartialOrd for TimingPoint {
|
||||||
@@ -16,8 +16,8 @@ impl PartialOrd for TimingPoint {
|
|||||||
/// [`TimingPoint`](crate::parse::TimingPoint) that depends on a previous one.
|
/// [`TimingPoint`](crate::parse::TimingPoint) that depends on a previous one.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub struct DifficultyPoint {
|
pub struct DifficultyPoint {
|
||||||
pub time: f32,
|
pub time: f64,
|
||||||
pub speed_multiplier: f32,
|
pub speed_multiplier: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialOrd for DifficultyPoint {
|
impl PartialOrd for DifficultyPoint {
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ use std::cmp::Ordering;
|
|||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct HitObject {
|
pub struct HitObject {
|
||||||
pub pos: Pos2,
|
pub pos: Pos2,
|
||||||
pub start_time: f32,
|
pub start_time: f64,
|
||||||
pub kind: HitObjectKind,
|
pub kind: HitObjectKind,
|
||||||
pub sound: u8,
|
pub sound: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HitObject {
|
impl HitObject {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn end_time(&self) -> f32 {
|
pub fn end_time(&self) -> f64 {
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
HitObjectKind::Circle { .. } => self.start_time,
|
HitObjectKind::Circle { .. } => self.start_time,
|
||||||
// incorrect, only called in mania which has no sliders though
|
// incorrect, only called in mania which has no sliders though
|
||||||
@@ -52,20 +52,20 @@ pub enum HitObjectKind {
|
|||||||
Circle,
|
Circle,
|
||||||
#[cfg(feature = "sliders")]
|
#[cfg(feature = "sliders")]
|
||||||
Slider {
|
Slider {
|
||||||
pixel_len: f32,
|
pixel_len: f64,
|
||||||
repeats: usize,
|
repeats: usize,
|
||||||
control_points: Vec<super::PathControlPoint>,
|
control_points: Vec<super::PathControlPoint>,
|
||||||
},
|
},
|
||||||
#[cfg(not(feature = "sliders"))]
|
#[cfg(not(feature = "sliders"))]
|
||||||
Slider {
|
Slider {
|
||||||
pixel_len: f32,
|
pixel_len: f64,
|
||||||
span_count: usize,
|
span_count: usize,
|
||||||
last_control_point: Pos2,
|
last_control_point: Pos2,
|
||||||
},
|
},
|
||||||
Spinner {
|
Spinner {
|
||||||
end_time: f32,
|
end_time: f64,
|
||||||
},
|
},
|
||||||
Hold {
|
Hold {
|
||||||
end_time: f32,
|
end_time: f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -42,11 +42,11 @@ impl<T> OptionExt<T> for Option<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait F32Ext: Sized {
|
trait FloatExt: Sized {
|
||||||
fn validate(self) -> Result<Self, ParseError>;
|
fn validate(self) -> Result<Self, ParseError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl F32Ext for f32 {
|
impl FloatExt for f64 {
|
||||||
fn validate(self) -> Result<Self, ParseError> {
|
fn validate(self) -> Result<Self, ParseError> {
|
||||||
self.is_finite()
|
self.is_finite()
|
||||||
.then(|| self)
|
.then(|| self)
|
||||||
@@ -312,10 +312,10 @@ macro_rules! parse_timingpoints_body {
|
|||||||
.next()
|
.next()
|
||||||
.next_field("timing point time")?
|
.next_field("timing point time")?
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<f32>()?
|
.parse::<f64>()?
|
||||||
.validate()?;
|
.validate()?;
|
||||||
|
|
||||||
let beat_len: f32 = split.next().next_field("beat len")?.trim().parse()?;
|
let beat_len: f64 = split.next().next_field("beat len")?.trim().parse()?;
|
||||||
|
|
||||||
if beat_len < 0.0 {
|
if beat_len < 0.0 {
|
||||||
let point = DifficultyPoint {
|
let point = DifficultyPoint {
|
||||||
@@ -429,7 +429,7 @@ macro_rules! parse_hitobjects_body {
|
|||||||
.next()
|
.next()
|
||||||
.next_field("hitobject time")?
|
.next_field("hitobject time")?
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<f32>()?
|
.parse::<f64>()?
|
||||||
.validate()?;
|
.validate()?;
|
||||||
|
|
||||||
if !$self.hit_objects.is_empty() && time < prev_time {
|
if !$self.hit_objects.is_empty() && time < prev_time {
|
||||||
@@ -519,7 +519,7 @@ macro_rules! parse_hitobjects_body {
|
|||||||
let pixel_len = split
|
let pixel_len = split
|
||||||
.next()
|
.next()
|
||||||
.next_field("pixel len")?
|
.next_field("pixel len")?
|
||||||
.parse::<f32>()?
|
.parse::<f64>()?
|
||||||
.max(0.0)
|
.max(0.0)
|
||||||
.min(MAX_COORDINATE_VALUE);
|
.min(MAX_COORDINATE_VALUE);
|
||||||
|
|
||||||
@@ -733,8 +733,8 @@ pub struct Beatmap {
|
|||||||
pub od: f32,
|
pub od: f32,
|
||||||
pub cs: f32,
|
pub cs: f32,
|
||||||
pub hp: f32,
|
pub hp: f32,
|
||||||
pub slider_mult: f32,
|
pub slider_mult: f64,
|
||||||
pub tick_rate: f32,
|
pub tick_rate: f64,
|
||||||
pub hit_objects: Vec<HitObject>,
|
pub hit_objects: Vec<HitObject>,
|
||||||
|
|
||||||
#[cfg(any(feature = "osu", feature = "fruits"))]
|
#[cfg(any(feature = "osu", feature = "fruits"))]
|
||||||
@@ -769,7 +769,7 @@ mod osu_fruits {
|
|||||||
|
|
||||||
use super::Pos2;
|
use super::Pos2;
|
||||||
|
|
||||||
pub(super) const MAX_COORDINATE_VALUE: f32 = 131_072.0;
|
pub(super) const MAX_COORDINATE_VALUE: f64 = 131_072.0;
|
||||||
|
|
||||||
pub(super) fn convert_points(
|
pub(super) fn convert_points(
|
||||||
points: &[&str],
|
points: &[&str],
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ impl<'m> AnyPP<'m> {
|
|||||||
/// Irrelevant for osu!mania.
|
/// Irrelevant for osu!mania.
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn accuracy(self, acc: f32) -> Self {
|
pub fn accuracy(self, acc: f64) -> Self {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(feature = "fruits")]
|
#[cfg(feature = "fruits")]
|
||||||
Self::Fruits(f) => Self::Fruits(f.accuracy(acc)),
|
Self::Fruits(f) => Self::Fruits(f.accuracy(acc)),
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ pub(crate) struct DifficultyObject<'o> {
|
|||||||
pub(crate) idx: usize,
|
pub(crate) idx: usize,
|
||||||
pub(crate) base: &'o HitObject,
|
pub(crate) base: &'o HitObject,
|
||||||
pub(crate) prev: &'o HitObject,
|
pub(crate) prev: &'o HitObject,
|
||||||
pub(crate) delta: f32,
|
pub(crate) delta: f64,
|
||||||
pub(crate) rhythm: &'static HitObjectRhythm,
|
pub(crate) rhythm: &'static HitObjectRhythm,
|
||||||
pub(crate) start_time: f32,
|
pub(crate) start_time: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'o> DifficultyObject<'o> {
|
impl<'o> DifficultyObject<'o> {
|
||||||
@@ -18,7 +18,7 @@ impl<'o> DifficultyObject<'o> {
|
|||||||
base: &'o HitObject,
|
base: &'o HitObject,
|
||||||
prev: &'o HitObject,
|
prev: &'o HitObject,
|
||||||
prev_prev: &HitObject,
|
prev_prev: &HitObject,
|
||||||
clock_rate: f32,
|
clock_rate: f64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let delta = (base.start_time - prev.start_time) / clock_rate;
|
let delta = (base.start_time - prev.start_time) / clock_rate;
|
||||||
let rhythm = closest_rhythm(delta, prev, prev_prev, clock_rate);
|
let rhythm = closest_rhythm(delta, prev, prev_prev, clock_rate);
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ static COMMON_RHYTHMS: [HitObjectRhythm; 9] = [
|
|||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub(crate) struct HitObjectRhythm {
|
pub(crate) struct HitObjectRhythm {
|
||||||
id: u8,
|
id: u8,
|
||||||
ratio: f32,
|
ratio: f64,
|
||||||
pub(crate) difficulty: f32,
|
pub(crate) difficulty: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for HitObjectRhythm {
|
impl PartialEq for HitObjectRhythm {
|
||||||
@@ -68,10 +68,10 @@ impl Eq for HitObjectRhythm {}
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn closest_rhythm(
|
pub(crate) fn closest_rhythm(
|
||||||
delta_time: f32,
|
delta_time: f64,
|
||||||
last: &HitObject,
|
last: &HitObject,
|
||||||
last_last: &HitObject,
|
last_last: &HitObject,
|
||||||
clock_rate: f32,
|
clock_rate: f64,
|
||||||
) -> &'static HitObjectRhythm {
|
) -> &'static HitObjectRhythm {
|
||||||
let prev_len = (last.start_time - last_last.start_time) / clock_rate;
|
let prev_len = (last.start_time - last_last.start_time) / clock_rate;
|
||||||
let ratio = delta_time / prev_len;
|
let ratio = delta_time / prev_len;
|
||||||
|
|||||||
+15
-15
@@ -21,13 +21,13 @@ use stamina_cheese::StaminaCheeseDetector;
|
|||||||
use crate::{Beatmap, Mods, Strains};
|
use crate::{Beatmap, Mods, Strains};
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::f32::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
const SECTION_LEN: f32 = 400.0;
|
const SECTION_LEN: f64 = 400.0;
|
||||||
|
|
||||||
const COLOR_SKILL_MULTIPLIER: f32 = 0.01;
|
const COLOR_SKILL_MULTIPLIER: f64 = 0.01;
|
||||||
const RHYTHM_SKILL_MULTIPLIER: f32 = 0.014;
|
const RHYTHM_SKILL_MULTIPLIER: f64 = 0.014;
|
||||||
const STAMINA_SKILL_MULTIPLIER: f32 = 0.02;
|
const STAMINA_SKILL_MULTIPLIER: f64 = 0.02;
|
||||||
|
|
||||||
/// Star calculation for osu!taiko maps.
|
/// Star calculation for osu!taiko maps.
|
||||||
///
|
///
|
||||||
@@ -209,7 +209,7 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn rescale(stars: f32) -> f32 {
|
fn rescale(stars: f64) -> f64 {
|
||||||
if stars < 0.0 {
|
if stars < 0.0 {
|
||||||
stars
|
stars
|
||||||
} else {
|
} else {
|
||||||
@@ -218,7 +218,7 @@ fn rescale(stars: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn simple_color_penalty(stamina: f32, color: f32) -> f32 {
|
fn simple_color_penalty(stamina: f64, color: f64) -> f64 {
|
||||||
if color <= 0.0 {
|
if color <= 0.0 {
|
||||||
0.79 - 0.25
|
0.79 - 0.25
|
||||||
} else {
|
} else {
|
||||||
@@ -226,7 +226,7 @@ fn simple_color_penalty(stamina: f32, color: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f32) -> f32 {
|
fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f64) -> f64 {
|
||||||
let mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
|
let mut peaks = Vec::with_capacity(skills[0].strain_peaks.len());
|
||||||
|
|
||||||
let iter = skills[0]
|
let iter = skills[0]
|
||||||
@@ -259,7 +259,7 @@ fn locally_combined_difficulty(skills: &[Skill], stamina_penalty: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
|
fn norm(p: f64, a: f64, b: f64, c: f64) -> f64 {
|
||||||
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
|
(a.powf(p) + b.powf(p) + c.powf(p)).powf(p.recip())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,28 +267,28 @@ fn norm(p: f32, a: f32, b: f32, c: f32) -> f32 {
|
|||||||
/// This data is necessary to calculate PP.
|
/// This data is necessary to calculate PP.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
pub struct DifficultyAttributes {
|
pub struct DifficultyAttributes {
|
||||||
pub stars: f32,
|
pub stars: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Various data created through the pp calculation.
|
/// Various data created through the pp calculation.
|
||||||
#[derive(Copy, Clone, Debug, Default)]
|
#[derive(Copy, Clone, Debug, Default)]
|
||||||
pub struct PerformanceAttributes {
|
pub struct PerformanceAttributes {
|
||||||
pub attributes: DifficultyAttributes,
|
pub attributes: DifficultyAttributes,
|
||||||
pub pp: f32,
|
pub pp: f64,
|
||||||
pub pp_acc: f32,
|
pub pp_acc: f64,
|
||||||
pub pp_strain: f32,
|
pub pp_strain: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PerformanceAttributes {
|
impl PerformanceAttributes {
|
||||||
/// Return the star value.
|
/// Return the star value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn stars(&self) -> f32 {
|
pub fn stars(&self) -> f64 {
|
||||||
self.attributes.stars
|
self.attributes.stars
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the performance point value.
|
/// Return the performance point value.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pp(&self) -> f32 {
|
pub fn pp(&self) -> f64 {
|
||||||
self.pp
|
self.pp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-26
@@ -32,11 +32,11 @@ use crate::{Beatmap, Mods, PpResult, StarResult};
|
|||||||
#[allow(clippy::upper_case_acronyms)]
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
pub struct TaikoPP<'m> {
|
pub struct TaikoPP<'m> {
|
||||||
map: &'m Beatmap,
|
map: &'m Beatmap,
|
||||||
stars: Option<f32>,
|
stars: Option<f64>,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
max_combo: usize,
|
max_combo: usize,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
acc: f32,
|
acc: f64,
|
||||||
n_misses: usize,
|
n_misses: usize,
|
||||||
passed_objects: Option<usize>,
|
passed_objects: Option<usize>,
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ impl<'m> TaikoPP<'m> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self {
|
pub fn attributes(mut self, attributes: impl TaikoAttributeProvider) -> Self {
|
||||||
if let Some(stars) = attributes.attributes() {
|
if let Some(stars) = attributes.attributes() {
|
||||||
self.stars.replace(stars);
|
self.stars = Some(stars);
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
@@ -119,7 +119,7 @@ impl<'m> TaikoPP<'m> {
|
|||||||
|
|
||||||
/// Set the accuracy between 0.0 and 100.0.
|
/// Set the accuracy between 0.0 and 100.0.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn accuracy(mut self, acc: f32) -> Self {
|
pub fn accuracy(mut self, acc: f64) -> Self {
|
||||||
self.acc = acc / 100.0;
|
self.acc = acc / 100.0;
|
||||||
self.n300.take();
|
self.n300.take();
|
||||||
self.n100.take();
|
self.n100.take();
|
||||||
@@ -158,7 +158,7 @@ impl<'m> TaikoPP<'m> {
|
|||||||
(None, None) => unreachable!(),
|
(None, None) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.acc = (2 * n300 + n100) as f32 / (2 * (n300 + n100 + misses)) as f32;
|
self.acc = (2 * n300 + n100) as f64 / (2 * (n300 + n100 + misses)) as f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
let inner = TaikoPPInner {
|
let inner = TaikoPPInner {
|
||||||
@@ -176,10 +176,10 @@ impl<'m> TaikoPP<'m> {
|
|||||||
|
|
||||||
struct TaikoPPInner<'m> {
|
struct TaikoPPInner<'m> {
|
||||||
map: &'m Beatmap,
|
map: &'m Beatmap,
|
||||||
stars: f32,
|
stars: f64,
|
||||||
mods: u32,
|
mods: u32,
|
||||||
max_combo: usize,
|
max_combo: usize,
|
||||||
acc: f32,
|
acc: f64,
|
||||||
n_misses: usize,
|
n_misses: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,16 +208,16 @@ impl<'m> TaikoPPInner<'m> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_strain_value(&self, stars: f32) -> f32 {
|
fn compute_strain_value(&self, stars: f64) -> f64 {
|
||||||
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
|
let exp_base = 5.0 * (stars / 0.0075).max(1.0) - 4.0;
|
||||||
let mut strain = exp_base * exp_base / 100_000.0;
|
let mut strain = exp_base * exp_base / 100_000.0;
|
||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more
|
||||||
let len_bonus = 1.0 + 0.1 * (self.max_combo as f32 / 1500.0).min(1.0);
|
let len_bonus = 1.0 + 0.1 * (self.max_combo as f64 / 1500.0).min(1.0);
|
||||||
strain *= len_bonus;
|
strain *= len_bonus;
|
||||||
|
|
||||||
// Penalize misses exponentially
|
// Penalize misses exponentially
|
||||||
strain *= 0.985_f32.powi(self.n_misses as i32);
|
strain *= 0.985_f64.powi(self.n_misses as i32);
|
||||||
|
|
||||||
// HD bonus
|
// HD bonus
|
||||||
if self.mods.hd() {
|
if self.mods.hd() {
|
||||||
@@ -234,8 +234,8 @@ impl<'m> TaikoPPInner<'m> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn compute_accuracy_value(&self) -> f32 {
|
fn compute_accuracy_value(&self) -> f64 {
|
||||||
let mut od = self.map.od;
|
let mut od = self.map.od as f64;
|
||||||
|
|
||||||
if self.mods.hr() {
|
if self.mods.hr() {
|
||||||
od *= 1.4;
|
od *= 1.4;
|
||||||
@@ -248,47 +248,43 @@ impl<'m> TaikoPPInner<'m> {
|
|||||||
(150.0 / hit_window).powf(1.1)
|
(150.0 / hit_window).powf(1.1)
|
||||||
* self.acc.powi(15)
|
* self.acc.powi(15)
|
||||||
* 22.0
|
* 22.0
|
||||||
* (self.max_combo as f32 / 1500.0).powf(0.3).min(1.15)
|
* (self.max_combo as f64 / 1500.0).powf(0.3).min(1.15)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const HITWINDOW_MIN: f32 = 50.0;
|
|
||||||
const HITWINDOW_AVG: f32 = 35.0;
|
|
||||||
const HITWINDOW_MAX: f32 = 20.0;
|
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn difficulty_range_od(od: f32) -> f32 {
|
fn difficulty_range_od(od: f64) -> f64 {
|
||||||
crate::difficulty_range(od, HITWINDOW_MAX, HITWINDOW_AVG, HITWINDOW_MIN)
|
crate::difficulty_range(od, 20.0, 35.0, 50.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait TaikoAttributeProvider {
|
pub trait TaikoAttributeProvider {
|
||||||
fn attributes(self) -> Option<f32>;
|
fn attributes(self) -> Option<f64>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaikoAttributeProvider for f32 {
|
impl TaikoAttributeProvider for f64 {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaikoAttributeProvider for DifficultyAttributes {
|
impl TaikoAttributeProvider for DifficultyAttributes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
Some(self.stars)
|
Some(self.stars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaikoAttributeProvider for PerformanceAttributes {
|
impl TaikoAttributeProvider for PerformanceAttributes {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
Some(self.attributes.stars)
|
Some(self.attributes.stars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaikoAttributeProvider for StarResult {
|
impl TaikoAttributeProvider for StarResult {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
#[allow(irrefutable_let_patterns)]
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Taiko(attributes) = self {
|
if let Self::Taiko(attributes) = self {
|
||||||
Some(attributes.stars)
|
Some(attributes.stars)
|
||||||
@@ -300,7 +296,7 @@ impl TaikoAttributeProvider for StarResult {
|
|||||||
|
|
||||||
impl TaikoAttributeProvider for PpResult {
|
impl TaikoAttributeProvider for PpResult {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn attributes(self) -> Option<f32> {
|
fn attributes(self) -> Option<f64> {
|
||||||
#[allow(irrefutable_let_patterns)]
|
#[allow(irrefutable_let_patterns)]
|
||||||
if let Self::Taiko(attributes) = self {
|
if let Self::Taiko(attributes) = self {
|
||||||
Some(attributes.attributes.stars)
|
Some(attributes.attributes.stars)
|
||||||
|
|||||||
+17
-17
@@ -2,25 +2,25 @@ use super::{DifficultyObject, SkillKind};
|
|||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
const DECAY_WEIGHT: f32 = 0.9;
|
const DECAY_WEIGHT: f64 = 0.9;
|
||||||
|
|
||||||
const COLOR_SKILL_MULTIPLIER: f32 = 1.0;
|
const COLOR_SKILL_MULTIPLIER: f64 = 1.0;
|
||||||
const COLOR_STRAIN_DECAY_BASE: f32 = 0.4;
|
const COLOR_STRAIN_DECAY_BASE: f64 = 0.4;
|
||||||
|
|
||||||
const RHYTHM_SKILL_MULTIPLIER: f32 = 10.0;
|
const RHYTHM_SKILL_MULTIPLIER: f64 = 10.0;
|
||||||
const RHYTHM_STRAIN_DECAY_BASE: f32 = 0.0;
|
const RHYTHM_STRAIN_DECAY_BASE: f64 = 0.0;
|
||||||
|
|
||||||
const STAMINA_SKILL_MULTIPLIER: f32 = 1.0;
|
const STAMINA_SKILL_MULTIPLIER: f64 = 1.0;
|
||||||
const STAMINA_STRAIN_DECAY_BASE: f32 = 0.4;
|
const STAMINA_STRAIN_DECAY_BASE: f64 = 0.4;
|
||||||
|
|
||||||
pub(crate) struct Skill {
|
pub(crate) struct Skill {
|
||||||
pub current_strain: f32,
|
pub current_strain: f64,
|
||||||
current_section_peak: f32,
|
current_section_peak: f64,
|
||||||
|
|
||||||
kind: SkillKind,
|
kind: SkillKind,
|
||||||
pub(crate) strain_peaks: Vec<f32>,
|
pub(crate) strain_peaks: Vec<f64>,
|
||||||
|
|
||||||
prev_time: Option<f32>,
|
prev_time: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Skill {
|
impl Skill {
|
||||||
@@ -43,7 +43,7 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn start_new_section_from(&mut self, time: f32) {
|
pub(crate) fn start_new_section_from(&mut self, time: f64) {
|
||||||
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
self.current_section_peak = self.peak_strain(time - self.prev_time.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn difficulty_value(&self, buf: &mut [f32]) -> f32 {
|
pub(crate) fn difficulty_value(&self, buf: &mut [f64]) -> f64 {
|
||||||
let mut difficulty = 0.0;
|
let mut difficulty = 0.0;
|
||||||
let mut weight = 1.0;
|
let mut weight = 1.0;
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn skill_multiplier(&self) -> f32 {
|
fn skill_multiplier(&self) -> f64 {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
|
SkillKind::Color { .. } => COLOR_SKILL_MULTIPLIER,
|
||||||
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
|
SkillKind::Rhythm { .. } => RHYTHM_SKILL_MULTIPLIER,
|
||||||
@@ -81,7 +81,7 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strain_decay_base(&self) -> f32 {
|
fn strain_decay_base(&self) -> f64 {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
|
SkillKind::Color { .. } => COLOR_STRAIN_DECAY_BASE,
|
||||||
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
|
SkillKind::Rhythm { .. } => RHYTHM_STRAIN_DECAY_BASE,
|
||||||
@@ -90,12 +90,12 @@ impl Skill {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn peak_strain(&self, delta_time: f32) -> f32 {
|
fn peak_strain(&self, delta_time: f64) -> f64 {
|
||||||
self.current_strain * self.strain_decay(delta_time)
|
self.current_strain * self.strain_decay(delta_time)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn strain_decay(&self, ms: f32) -> f32 {
|
fn strain_decay(&self, ms: f64) -> f64 {
|
||||||
self.strain_decay_base().powf(ms / 1000.0)
|
self.strain_decay_base().powf(ms / 1000.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -2,7 +2,7 @@ use super::{DifficultyObject, HitObjectRhythm, LimitedQueue, Rim};
|
|||||||
|
|
||||||
use std::ops::Index;
|
use std::ops::Index;
|
||||||
|
|
||||||
const RHYTHM_STRAIN_DECAY: f32 = 0.96;
|
const RHYTHM_STRAIN_DECAY: f64 = 0.96;
|
||||||
const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
|
const MOST_RECENT_PATTERNS_TO_COMPARE: usize = 2;
|
||||||
|
|
||||||
const MONO_HISTORY_MAX_LEN: usize = 5;
|
const MONO_HISTORY_MAX_LEN: usize = 5;
|
||||||
@@ -18,12 +18,12 @@ pub(crate) enum SkillKind {
|
|||||||
Rhythm {
|
Rhythm {
|
||||||
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
|
rhythm_history: LimitedQueue<(usize, HitObjectRhythm)>, // (idx, rhythm)
|
||||||
notes_since_rhythm_change: usize,
|
notes_since_rhythm_change: usize,
|
||||||
current_strain: f32,
|
current_strain: f64,
|
||||||
},
|
},
|
||||||
Stamina {
|
Stamina {
|
||||||
note_pair_duration_history: LimitedQueue<f32>,
|
note_pair_duration_history: LimitedQueue<f64>,
|
||||||
hand: u8,
|
hand: u8,
|
||||||
off_hand_object_duration: f32,
|
off_hand_object_duration: f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ impl SkillKind {
|
|||||||
Self::Stamina {
|
Self::Stamina {
|
||||||
note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN),
|
note_pair_duration_history: LimitedQueue::new(STAMINA_HISTORY_MAX_LEN),
|
||||||
hand: right_hand as u8,
|
hand: right_hand as u8,
|
||||||
off_hand_object_duration: f32::MAX,
|
off_hand_object_duration: f64::MAX,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ impl SkillKind {
|
|||||||
&mut self,
|
&mut self,
|
||||||
current: &DifficultyObject<'_>,
|
current: &DifficultyObject<'_>,
|
||||||
cheese: &[bool],
|
cheese: &[bool],
|
||||||
) -> f32 {
|
) -> f64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Color {
|
Self::Color {
|
||||||
mono_history,
|
mono_history,
|
||||||
@@ -157,7 +157,7 @@ impl SkillKind {
|
|||||||
*current_strain *= RHYTHM_STRAIN_DECAY;
|
*current_strain *= RHYTHM_STRAIN_DECAY;
|
||||||
*notes_since_rhythm_change += 1;
|
*notes_since_rhythm_change += 1;
|
||||||
|
|
||||||
if current.rhythm.difficulty.abs() <= f32::EPSILON {
|
if current.rhythm.difficulty.abs() <= f64::EPSILON {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,15 +250,16 @@ impl SkillKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn pattern_len_penalty(pattern_len: usize) -> f32 {
|
fn pattern_len_penalty(pattern_len: usize) -> f64 {
|
||||||
let short_pattern_penalty = (0.15 * pattern_len as f32).min(1.0);
|
let pattern_len = pattern_len as f64;
|
||||||
let long_pattern_penalty = (2.5 - 0.15 * pattern_len as f32).max(0.0).min(1.0);
|
let short_pattern_penalty = (0.15 * pattern_len).min(1.0);
|
||||||
|
let long_pattern_penalty = (2.5 - 0.15 * pattern_len).max(0.0).min(1.0);
|
||||||
|
|
||||||
short_pattern_penalty.min(long_pattern_penalty)
|
short_pattern_penalty.min(long_pattern_penalty)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cheese_penalty(note_pair_duration: f32) -> f32 {
|
fn cheese_penalty(note_pair_duration: f64) -> f64 {
|
||||||
if note_pair_duration > 125.0 {
|
if note_pair_duration > 125.0 {
|
||||||
1.0
|
1.0
|
||||||
} else if note_pair_duration < 100.0 {
|
} else if note_pair_duration < 100.0 {
|
||||||
@@ -269,7 +270,7 @@ fn cheese_penalty(note_pair_duration: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn speed_bonus(note_pair_duration: f32) -> f32 {
|
fn speed_bonus(note_pair_duration: f64) -> f64 {
|
||||||
if note_pair_duration > 200.0 {
|
if note_pair_duration > 200.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
@@ -281,6 +282,6 @@ fn speed_bonus(note_pair_duration: f32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn repetition_penalty(notes_since: usize) -> f32 {
|
fn repetition_penalty(notes_since: usize) -> f64 {
|
||||||
(0.032 * notes_since as f32).min(1.0)
|
(0.032 * notes_since as f64).min(1.0)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user