big first commit

This commit is contained in:
MaxOhn
2021-01-08 09:25:46 +01:00
commit 496216843f
61 changed files with 32981 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
use super::math_util;
use parse::Pos2;
const SLIDER_QUALITY: f32 = 50.0;
pub(crate) enum Points {
Single(Pos2),
Multi(Vec<Pos2>),
}
pub(crate) enum Curve {
Linear {
a: Pos2,
b: Pos2,
},
Bezier(Points),
Catmull(Points),
Perfect {
origin: Pos2,
cx: f32,
cy: f32,
radius: f32,
},
}
impl Curve {
pub(crate) fn linear(a: Pos2, b: Pos2) -> Self {
Self::Linear { a, b }
}
pub(crate) fn bezier(points: &[Pos2]) -> Self {
if points.len() == 1 {
return Self::Bezier(Points::Single(points[0]));
}
let mut start = 0;
let mut end = 0;
let mut result = Vec::with_capacity(4);
for i in 0..points.len() - 1 {
if end - start > 1 && points[i] == points[end - 1] {
Self::_bezier(&mut result, &points[start..end]);
start = end;
}
end += 1;
}
Self::_bezier(&mut result, &points[start..end + 1]);
Self::Bezier(Points::Multi(result))
}
fn _bezier(result: &mut Vec<Pos2>, points: &[Pos2]) {
let step = 0.25 / SLIDER_QUALITY / points.len() as f32;
let mut i = 0.0;
let n = points.len() as i32 - 1;
while i < 1.0 + step {
let point = (0..=n).fold(Pos2 { x: 0.0, y: 0.0 }, |point, p| {
let factor = math_util::cpn(p, n) * (1.0 - i).powi(n - p) * i.powi(p);
point + points[p as usize] * factor
});
result.push(point);
i += step;
}
}
pub(crate) fn catmull(points: &[Pos2]) -> Self {
if points.len() == 1 {
return Self::Catmull(Points::Single(points[0]));
}
let order = points.len();
let step = 2.5 / SLIDER_QUALITY;
let target = step + 1.0;
let mut resulting_points = Vec::with_capacity(4);
for x in 0..order - 1 {
let mut t = 0.0;
while t < target {
let v1 = if x >= 1 { points[x - 1] } else { points[x] };
let v2 = points[x];
let v3 = if x + 1 < order {
points[x + 1]
} else {
v2.add_scaled(v2.add_scaled(v1, -1.0), 1.0)
};
let v4 = if x + 2 < order {
points[x + 2]
} else {
v3.add_scaled(v3.add_scaled(v2, -1.0), 1.0)
};
let point = Self::catmull_point(v1, v2, v3, v4, t);
resulting_points.push(point);
t += step;
}
}
Self::Catmull(Points::Multi(resulting_points))
}
#[inline]
fn catmull_point(p0: Pos2, p1: Pos2, p2: Pos2, p3: Pos2, len: f32) -> Pos2 {
Pos2 {
x: math_util::catmull(p0.x, p1.x, p2.x, p3.x, len),
y: math_util::catmull(p0.y, p1.y, p2.y, p3.y, len),
}
}
pub(crate) fn perfect(points: &[Pos2]) -> Self {
let (cx, cy, mut radius) = math_util::get_circum_circle(&points);
radius *= ((!math_util::is_left(&points)) as i8 * 2 - 1) as f32;
Self::Perfect {
origin: points[0],
cx,
cy,
radius,
}
}
pub(crate) fn point_at_distance(&self, len: f32) -> Pos2 {
let points = match self {
Self::Bezier(points) => points,
Self::Catmull(points) => points,
Self::Linear { a, b } => return math_util::point_on_line(*a, *b, len),
Self::Perfect {
origin,
cx,
cy,
radius,
} => return math_util::rotate(*cx, *cy, *origin, len / *radius),
};
match points {
Points::Single(point) => *point,
Points::Multi(points) => math_util::point_at_distance(points, len),
}
}
}
+70
View File
@@ -0,0 +1,70 @@
use super::OsuObject;
const NORMALIZED_RADIUS: f32 = 52.0;
pub(crate) struct DifficultyObject {
pub(crate) base: OsuObject,
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
pub(crate) jump_dist: f32,
pub(crate) travel_dist: f32,
pub(crate) angle: Option<f32>,
pub(crate) delta: f32,
pub(crate) strain_time: f32,
}
impl DifficultyObject {
pub(crate) fn new(
base: OsuObject,
prev: OsuObject,
prev_diff: Option<DifficultyObject>,
prev_prev: Option<OsuObject>,
clock_rate: f32,
) -> Self {
let delta = (base.time() - prev.time()) / clock_rate;
let strain_time = delta.max(50.0);
let radius = base.radius();
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 travel_dist = base.travel_dist();
let prev_cursor_pos = prev.cursor_end_position();
let jump_dist = match base {
OsuObject::Spinner { .. } => 0.0,
_ => (base.stacked_pos() * scaling_factor - prev_cursor_pos * scaling_factor).length(),
};
let angle = prev_prev.map(|prev_prev| {
let prev_prev_cursor_pos = prev_prev.cursor_end_position();
let v1 = prev_prev_cursor_pos - prev.stacked_pos();
let v2 = base.stacked_pos() - prev_cursor_pos;
let dot = v1.dot(v2);
let det = v1.x * v2.y - v1.y * v2.x;
det.atan2(dot).abs()
});
let prev = prev_diff.map(|o| (o.jump_dist, o.strain_time));
Self {
base,
prev,
jump_dist,
travel_dist,
angle,
delta,
strain_time,
}
}
}
+166
View File
@@ -0,0 +1,166 @@
mod curve;
mod difficulty_object;
mod math_util;
mod osu_object;
mod skill;
mod skill_kind;
use difficulty_object::DifficultyObject;
use osu_object::OsuObject;
use skill::Skill;
use skill_kind::SkillKind;
use parse::{Beatmap, Mods};
const SECTION_LEN: f32 = 400.0;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
/// Star calculation for osu!standard maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
if map.hit_objects.len() < 2 {
return 0.0;
}
let attributes = map.attributes().mods(mods);
let section_len = SECTION_LEN * attributes.clock_rate;
let mut hit_objects = map
.hit_objects
.iter()
.map(|h| OsuObject::new(h, map, &attributes));
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_diff = None;
for curr in hit_objects {
let h = DifficultyObject::new(
curr.clone(),
prev.clone(),
prev_diff,
prev_prev,
attributes.clock_rate,
);
// println!(
// "strain_time={} | travel_dist={} | jump_dist={} | angle={:?}",
// h.strain_time, h.travel_dist, h.jump_dist, h.angle
// );
while h.base.time() > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
prev_diff = Some(h);
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn test_single() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/1851299.osu") {
Ok(file) => file,
Err(why) => panic!("Could not open file: {}", why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map: {}", why),
};
let stars = stars(&map, 0);
println!("Stars: {}", stars);
}
#[test]
#[ignore]
fn test_osu() {
let margin = 0.005;
#[rustfmt::skip]
let data = vec![
(1851299, 1 << 8, 4.23514130038547), // HT
(1851299, 0, 5.356786475158158), // NM
(1851299, 1 << 6, 7.450616908751305), // DT
(1851299, 1 << 4, 5.6834681957637665),// HR
(1851299, 1 << 1, 4.937817303399699), // EZ
(70090, 1 << 8, 2.2929922580201803), // HT
(70090, 0, 2.8322940761833983), // NM
(70090, 1 << 6, 3.8338563325375485), // DT
(70090, 1 << 4, 3.0617492228478174), // HR
(70090, 1 << 1, 2.698823231324141), // EZ
(1241370, 1 << 8, 5.662809600985943), // HT
(1241370, 0, 7.0367002127481975), // NM
(1241370, 1 << 6, 11.144720506574934),// DT
(1241370, 1 << 4, 7.641688110458715), // HR
(1241370, 1 << 1, 6.316288616688052), // EZ
// Slider fiesta
// (1657535, 1 << 8, 4.1727975286379895),// HT
// (1657535, 0, 5.16048239944917), // NM
// (1657535, 1 << 6, 7.125936779100417), // DT
// (1657535, 1 << 4, 5.545877027713307), // HR
// (1657535, 1 << 1, 4.66015083361088), // EZ
];
for (map_id, mods, expected_stars) in data {
let file = match File::open(format!("./test/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let stars = stars(&map, mods);
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
}
}
+133
View File
@@ -0,0 +1,133 @@
use parse::Pos2;
#[inline]
pub(crate) fn cpn(mut p: i32, n: i32) -> f32 {
if p < 0 || p > n {
return 0.0;
}
p = p.min(n - p);
let mut out = 1.0;
for i in 1..=p {
out *= (n - p + i) as f32 / i as f32;
}
out
}
#[inline]
pub(crate) fn catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
0.5 * ((2.0 * p1)
+ (-p0 + p2) * t
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t.powi(2)
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3 * t.powi(3)))
}
#[inline]
pub(crate) fn point_on_line(p1: Pos2, p2: Pos2, len: f32) -> Pos2 {
let mut full_len = ((p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sqrt();
let n = full_len - len;
if full_len.abs() < f32::EPSILON {
full_len = 1.0;
}
(p1 * n + p2 * len) / full_len
}
#[inline]
pub(crate) fn angle_from_points(p0: Pos2, p1: Pos2) -> f32 {
(p1.y - p0.y).atan2(p1.x - p0.x)
}
#[inline]
pub(crate) fn distance_from_points(arr: &[Pos2]) -> f32 {
arr.iter()
.skip(1)
.zip(arr.iter())
.map(|(curr, prev)| curr.distance(prev))
.sum()
}
#[inline]
pub(crate) fn cart_from_pol(r: f32, t: f32) -> Pos2 {
Pos2 {
x: r * t.cos(),
y: r * t.sin(),
}
}
pub(crate) fn point_at_distance(array: &[Pos2], distance: f32) -> Pos2 {
if array.len() < 2 {
return Pos2 { x: 0.0, y: 0.0 };
} else if distance.abs() < f32::EPSILON {
return array[0];
} else if distance_from_points(array) <= distance {
return array[array.len() - 1];
}
let mut i = 0;
let mut current_distance = 0.0;
let mut new_distance = 0.0;
while i < array.len() - 2 {
new_distance = (array[i] - array[i + 1]).length();
current_distance += new_distance;
if distance <= current_distance {
break;
}
i += 1;
}
current_distance -= new_distance;
if (distance - current_distance).abs() <= f32::EPSILON {
array[i]
} else {
let angle = angle_from_points(array[i], array[i + 1]);
let cart = cart_from_pol(distance - current_distance, angle);
if array[i].x > array[i + 1].x {
array[i] - cart
} else {
array[i] + cart
}
}
}
pub(crate) fn get_circum_circle(p: &[Pos2]) -> (f32, f32, f32) {
let d = 2.0
* (p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y) + p[2].x * (p[0].y - p[1].y));
let p0 = p[0].x * p[0].x + p[0].y * p[0].y;
let p1 = p[1].x * p[1].x + p[1].y * p[1].y;
let p2 = p[2].x * p[2].x + p[2].y * p[2].y;
let ux = (p0 * (p[1].y - p[2].y) + p1 * (p[2].y - p[0].y) + p2 * (p[0].y - p[1].y)) / d;
let uy = (p0 * (p[2].x - p[1].x) + p1 * (p[0].x - p[2].x) + p2 * (p[1].x - p[0].x)) / d;
let px = ux - p[0].x;
let py = uy - p[0].y;
let r = (px * px + py * py).sqrt();
(ux, uy, r)
}
#[inline]
pub(crate) fn is_left(p: &[Pos2]) -> bool {
((p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[1].y - p[0].y) * (p[2].x - p[0].x)) < 0.0
}
#[inline]
pub(crate) fn rotate(cx: f32, cy: f32, p: Pos2, radians: f32) -> Pos2 {
let cos = radians.cos();
let sin = radians.sin();
Pos2 {
x: (cos * (p.x - cx)) - (sin * (p.y - cy)) + cx,
y: (sin * (p.x - cx)) + (cos * (p.y - cy)) + cy,
}
}
+324
View File
@@ -0,0 +1,324 @@
use super::curve::Curve;
use parse::{Beatmap, BeatmapAttributes, HitObject, HitObjectKind, PathType, Pos2};
use std::cmp::Ordering;
macro_rules! binary_search {
($slice:expr, $target:expr) => {
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
};
}
const OBJECT_RADIUS: f32 = 64.0;
const STACK_DIST: f32 = 3.0;
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
#[derive(Clone)] // TODO: Remove clone
pub(crate) enum OsuObject {
Circle {
pos: Pos2,
time: f32,
scale: f32,
stack_height: f32,
},
Slider {
pixel_len: f32, // TODO: Redundant?
repeats: usize, // TODO: Redundant?
objects: Vec<SliderTick>,
scale: f32,
stack_height: f32,
cursor_end_pos: Pos2,
cursor_travel_dist: f32,
},
Spinner {
pos: Pos2,
time: f32,
},
}
impl OsuObject {
pub(crate) fn new(h: &HitObject, map: &Beatmap, attributes: &BeatmapAttributes) -> Self {
let pos = h.pos;
let time = h.start_time;
let scale = (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
let mut stack_height = 0.0;
match &h.kind {
HitObjectKind::Circle => Self::Circle {
pos,
time,
scale,
stack_height,
},
HitObjectKind::Slider {
pixel_len,
repeats,
curve_points,
path_type,
} => {
let (beat_len, timing_time) = {
match binary_search!(map.timing_points, time) {
Ok(idx) => {
let point = &map.timing_points[idx];
(point.beat_len, point.time)
}
Err(0) => (1000.0, 0.0),
Err(idx) => {
let point = &map.timing_points[idx - 1];
(point.beat_len, point.time)
}
}
};
let (speed_multiplier, diff_time) = {
match binary_search!(map.difficulty_points, time) {
Ok(idx) => {
let point = &map.difficulty_points[idx];
(point.speed_multiplier, point.time)
}
Err(0) => (1.0, 0.0),
Err(idx) => {
let point = &map.difficulty_points[idx - 1];
(point.speed_multiplier, point.time)
}
}
};
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
if map.version >= 8 {
tick_distance /= (100.0 / speed_multiplier).max(10.0).min(1000.0) / 100.0;
}
let spm = if timing_time > diff_time {
1.0
} else {
speed_multiplier
};
let duration = *repeats as f32 * beat_len * pixel_len / (map.sv * spm) / 100.0;
// let velocity = *pixel_len as f32 / duration;
// println!("duration={}", duration);
// println!("velocity={}", velocity);
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
PathType::Bezier
} else if curve_points.len() == 2 {
PathType::Linear
} else {
*path_type
};
let curve = match path_type {
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
PathType::Bezier => Curve::bezier(&curve_points),
PathType::Catmull => Curve::catmull(&curve_points),
PathType::PerfectCurve => Curve::perfect(&curve_points),
};
let mut current_distance = tick_distance;
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
let target = pixel_len - tick_distance / 8.0;
let mut ticks = Vec::with_capacity((target / tick_distance) as usize);
while current_distance < target {
let pos = curve.point_at_distance(current_distance);
let time = h.start_time + time_add * (ticks.len() + 1) as f32;
ticks.push(SliderTick::new(pos, time));
current_distance += tick_distance;
}
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
slider_objects.push(SliderTick::new(h.pos, h.start_time));
if *repeats <= 1 {
slider_objects.append(&mut ticks);
} else {
slider_objects.append(&mut ticks.clone());
for repeat_id in 1..repeats - 1 {
let dist = (repeat_id % 2) as f32 * pixel_len;
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
let pos = curve.point_at_distance(dist);
// Reverse tick / last legacy tick
slider_objects.push(SliderTick::new(pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.extend_from_slice(&ticks); // tick time doesn't need to be adjusted for some reason
}
// Handling last span separatly so that `ticks` vector isn't cloned again
let dist = ((repeats - 1) % 2) as f32 * pixel_len;
let time_offset = (duration / *repeats as f32) * (repeats - 1) as f32;
let pos = curve.point_at_distance(dist);
slider_objects.push(SliderTick::new(pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.append(&mut ticks);
}
// Slider tail
let dist_end = (repeats % 2) as f32 * pixel_len;
// let dist_end = velocity * (duration - LEGACY_LAST_TICK_OFFSET);
let pos = curve.point_at_distance(dist_end);
slider_objects.push(SliderTick::new(pos, h.start_time + duration));
println!("> Slider: {:?}", slider_objects);
let radius = OBJECT_RADIUS * scale;
let stack_offset = {
let c = stack_height * scale * -6.4;
Pos2 { x: c, y: c }
};
// println!("radius={} | stack_offset={:?}", radius, stack_offset);
let stacked_pos = pos + stack_offset;
let mut cursor_end_pos = stacked_pos;
let mut cursor_travel_dist = 0.0;
let approx_follow_circle_radius = radius * 3.0;
for (i, tick) in slider_objects.iter().skip(1).enumerate() {
let diff = stacked_pos + tick.pos - cursor_end_pos;
let mut dist = diff.length();
println!("[{}] diff={:?} | dist={}", i, diff, dist);
if dist > approx_follow_circle_radius {
let normalized = diff.normalize();
dist -= approx_follow_circle_radius;
cursor_end_pos += normalized * dist;
cursor_travel_dist += dist;
}
}
println!("---");
Self::Slider {
pixel_len: *pixel_len,
repeats: *repeats,
objects: slider_objects,
scale,
stack_height,
cursor_end_pos,
cursor_travel_dist,
}
}
HitObjectKind::Spinner { .. } => Self::Spinner { pos, time },
HitObjectKind::Hold { .. } => panic!("found Hold object in osu!standard file"),
}
}
#[inline]
pub(crate) fn time(&self) -> f32 {
match self {
Self::Circle { time, .. } => *time,
Self::Slider { objects, .. } => objects[0].time,
Self::Spinner { time, .. } => *time,
}
}
#[inline]
pub(crate) fn radius(&self) -> f32 {
OBJECT_RADIUS * self.scale()
}
#[inline]
pub(crate) fn travel_dist(&self) -> f32 {
match self {
Self::Slider {
cursor_travel_dist, ..
} => *cursor_travel_dist,
_ => 0.0,
}
}
#[inline]
pub(crate) fn stacked_pos(&self) -> Pos2 {
self.pos() + self.stack_offset()
}
#[inline]
pub(crate) fn cursor_end_position(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { cursor_end_pos, .. } => *cursor_end_pos,
Self::Spinner { pos, .. } => *pos,
}
}
#[inline]
pub(crate) fn is_spinner(&self) -> bool {
matches!(self, Self::Spinner { .. })
}
#[inline]
fn scale(&self) -> f32 {
match self {
Self::Circle { scale, .. } => *scale,
Self::Slider { scale, .. } => *scale,
Self::Spinner { .. } => 1.0,
}
}
#[inline]
fn stack_height(&self) -> f32 {
match self {
Self::Circle { stack_height, .. } => *stack_height,
Self::Slider { stack_height, .. } => *stack_height,
Self::Spinner { .. } => 0.0,
}
}
#[inline]
fn pos(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { objects, .. } => objects[0].pos,
Self::Spinner { .. } => Pos2::default(),
}
}
#[inline]
fn stack_offset(&self) -> Pos2 {
let c = self.stack_height() * self.scale() * -6.4;
Pos2 { x: c, y: c }
}
}
#[derive(Copy, Clone)]
pub(crate) struct SliderTick {
pos: Pos2,
time: f32,
}
impl SliderTick {
fn new(pos: Pos2, time: f32) -> Self {
Self { pos, time }
}
}
use std::fmt;
impl fmt::Debug for SliderTick {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{{pos={:?} | time={}}}", self.pos, self.time)
}
}
+99
View File
@@ -0,0 +1,99 @@
use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering;
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const DECAY_WEIGHT: f32 = 0.9;
pub(crate) struct Skill {
pub current_strain: f32,
current_section_peak: f32,
kind: SkillKind,
strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
if self.prev_time.is_some() {
self.strain_peaks.push(self.current_section_peak);
}
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f32) {
if let Some(prev) = self.prev_time {
self.current_section_peak = self.peak_strain(time - prev);
}
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyObject) {
self.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.time());
}
pub(crate) fn difficulty_value(&mut self) -> f32 {
let mut difficulty = 0.0;
let mut weight = 1.0;
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in self.strain_peaks.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
}
difficulty
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
}
}
#[inline]
fn strain_decay_base(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
SkillKind::Speed => SPEED_STRAIN_DECAY_BASE,
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
+101
View File
@@ -0,0 +1,101 @@
use super::DifficultyObject;
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
const SPEED_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_6;
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
const MIN_SPEED_BONUS: f32 = 75.0;
const MAX_SPEED_BONUS: f32 = 45.0;
const SPEED_BALANCING_FACTOR: f32 = 40.0;
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
const TIMING_THRESHOLD: f32 = 107.0;
#[derive(Copy, Clone)]
pub(crate) enum SkillKind {
Aim,
Speed,
}
impl SkillKind {
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
match self {
Self::Aim => {
if current.base.is_spinner() {
return 0.0;
}
let mut result = 0.0;
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
if let Some(angle) = current.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
let scale = 90.0;
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
* (prev_jump_dist - scale).max(0.0)
* (current.jump_dist - scale).max(0.0))
.sqrt();
result = 1.5 * apply_diminishing_exp(angle_bonus.max(0.0))
/ (TIMING_THRESHOLD).max(prev_strain_time)
}
}
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / current.strain_time)
}
Self::Speed => {
if current.base.is_spinner() {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut speed_bonus = 1.0;
if delta_time < MIN_SPEED_BONUS {
let exp_base = (MIN_SPEED_BONUS - delta_time) / SPEED_BALANCING_FACTOR;
speed_bonus = 1.0 + exp_base * exp_base;
}
let mut angle_bonus = 1.0;
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
if angle < PI_OVER_2 {
angle_bonus = 1.28;
// TODO: Improve ifs
if dist < 90.0 && angle < PI_OVER_4 {
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
} else if dist < 90.0 {
angle_bonus += (1.0 - angle_bonus)
* ((90.0 - dist) / 10.0).min(1.0)
* ((PI_OVER_2 - angle) / PI_OVER_4).sin();
}
}
}
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.strain_time
}
}
}
}
#[inline]
fn apply_diminishing_exp(val: f32) -> f32 {
val.powf(0.99)
}