use crate::{
any::difficulty::object::IDifficultyObject,
osu::difficulty::object::OsuDifficultyObject,
util::{
difficulty::{milliseconds_to_bpm, reverse_lerp, smootherstep, smoothstep},
float_ext::FloatExt,
},
};
pub struct AimEvaluator;
impl AimEvaluator {
const WIDE_ANGLE_MULTIPLIER: f64 = 1.5;
const ACUTE_ANGLE_MULTIPLIER: f64 = 2.55;
const SLIDER_MULTIPLIER: f64 = 1.35;
const VELOCITY_CHANGE_MULTIPLIER: f64 = 0.75;
const WIGGLE_MULTIPLIER: f64 = 1.02;
#[expect(clippy::too_many_lines, reason = "staying in-sync with lazer")]
pub fn evaluate_diff_of<'a>(
curr: &'a OsuDifficultyObject<'a>,
diff_objects: &'a [OsuDifficultyObject<'a>],
with_slider_travel_dist: bool,
) -> f64 {
let osu_curr_obj = curr;
let Some((osu_last_last_obj, osu_last_obj)) = curr
.previous(1, diff_objects)
.zip(curr.previous(0, diff_objects))
.filter(|(_, last)| !(curr.base.is_spinner() || last.base.is_spinner()))
else {
return 0.0;
};
#[expect(clippy::items_after_statements, reason = "staying in-sync with lazer")]
const RADIUS: i32 = OsuDifficultyObject::NORMALIZED_RADIUS;
#[expect(clippy::items_after_statements, reason = "staying in-sync with lazer")]
const DIAMETER: i32 = OsuDifficultyObject::NORMALIZED_DIAMETER;
let mut curr_vel = osu_curr_obj.lazy_jump_dist / osu_curr_obj.adjusted_delta_time;
if osu_last_obj.base.is_slider() && with_slider_travel_dist {
let travel_vel = osu_last_obj.travel_dist / osu_last_obj.travel_time;
let movement_vel = osu_curr_obj.min_jump_dist / osu_curr_obj.min_jump_time;
curr_vel = curr_vel.max(movement_vel + travel_vel);
}
let mut prev_vel = osu_last_obj.lazy_jump_dist / osu_last_obj.adjusted_delta_time;
if osu_last_last_obj.base.is_slider() && with_slider_travel_dist {
let travel_vel = osu_last_last_obj.travel_dist / osu_last_last_obj.travel_time;
let movement_vel = osu_last_obj.min_jump_dist / osu_last_obj.min_jump_time;
prev_vel = prev_vel.max(movement_vel + travel_vel);
}
let mut wide_angle_bonus = 0.0;
let mut acute_angle_bonus = 0.0;
let mut slider_bonus = 0.0;
let mut vel_change_bonus = 0.0;
let mut wiggle_bonus = 0.0;
let mut aim_strain = curr_vel;
if let Some((curr_angle, last_angle)) = osu_curr_obj.angle.zip(osu_last_obj.angle) {
let angle_bonus = curr_vel.min(prev_vel);
if osu_curr_obj
.adjusted_delta_time
.max(osu_last_obj.adjusted_delta_time)
< 1.25
* osu_curr_obj
.adjusted_delta_time
.min(osu_last_obj.adjusted_delta_time)
{
acute_angle_bonus = Self::calc_acute_angle_bonus(curr_angle);
acute_angle_bonus *= 0.08
+ 0.92
* (1.0
- f64::min(
acute_angle_bonus,
f64::powf(Self::calc_acute_angle_bonus(last_angle), 3.0),
));
acute_angle_bonus *= angle_bonus
* smootherstep(
milliseconds_to_bpm(osu_curr_obj.adjusted_delta_time, Some(2)),
300.0,
400.0,
)
* smootherstep(
osu_curr_obj.lazy_jump_dist,
f64::from(DIAMETER),
f64::from(DIAMETER * 2),
);
}
wide_angle_bonus = Self::calc_wide_angle_bonus(curr_angle);
wide_angle_bonus *= 1.0
- f64::min(
wide_angle_bonus,
f64::powf(Self::calc_wide_angle_bonus(last_angle), 3.0),
);
wide_angle_bonus *=
angle_bonus * smootherstep(osu_curr_obj.lazy_jump_dist, 0.0, f64::from(DIAMETER));
wiggle_bonus = angle_bonus
* smootherstep(
osu_curr_obj.lazy_jump_dist,
f64::from(RADIUS),
f64::from(DIAMETER),
)
* f64::powf(
reverse_lerp(
osu_curr_obj.lazy_jump_dist,
f64::from(DIAMETER * 3),
f64::from(DIAMETER),
),
1.8,
)
* smootherstep(curr_angle, f64::to_radians(110.0), f64::to_radians(60.0))
* smootherstep(
osu_last_obj.lazy_jump_dist,
f64::from(RADIUS),
f64::from(DIAMETER),
)
* f64::powf(
reverse_lerp(
osu_last_obj.lazy_jump_dist,
f64::from(DIAMETER * 3),
f64::from(DIAMETER),
),
1.8,
)
* smootherstep(last_angle, f64::to_radians(110.0), f64::to_radians(60.0));
if let Some(osu_last_2_obj) = curr.previous(2, diff_objects) {
let distance =
(osu_last_2_obj.base.stacked_pos() - osu_last_obj.base.stacked_pos()).length();
if distance < 1.0 {
wide_angle_bonus *= 1.0 - 0.35 * f64::from(1.0 - distance);
}
}
}
if prev_vel.max(curr_vel).not_eq(0.0) {
prev_vel = (osu_last_obj.lazy_jump_dist + osu_last_last_obj.travel_dist)
/ osu_last_obj.adjusted_delta_time;
curr_vel = (osu_curr_obj.lazy_jump_dist + osu_last_obj.travel_dist)
/ osu_curr_obj.adjusted_delta_time;
let dist_ratio = smoothstep(
(prev_vel - curr_vel).abs() / prev_vel.max(curr_vel),
0.0,
1.0,
);
let overlap_vel_buff = (f64::from(DIAMETER) * 1.25
/ osu_curr_obj
.adjusted_delta_time
.min(osu_last_obj.adjusted_delta_time))
.min((prev_vel - curr_vel).abs());
vel_change_bonus = overlap_vel_buff * dist_ratio;
let bonus_base = (osu_curr_obj.adjusted_delta_time)
.min(osu_last_obj.adjusted_delta_time)
/ (osu_curr_obj.adjusted_delta_time).max(osu_last_obj.adjusted_delta_time);
vel_change_bonus *= bonus_base.powf(2.0);
}
if osu_last_obj.base.is_slider() {
slider_bonus = osu_last_obj.travel_dist / osu_last_obj.travel_time;
}
aim_strain += wiggle_bonus * Self::WIGGLE_MULTIPLIER;
aim_strain += vel_change_bonus * Self::VELOCITY_CHANGE_MULTIPLIER;
aim_strain += (acute_angle_bonus * Self::ACUTE_ANGLE_MULTIPLIER)
.max(wide_angle_bonus * Self::WIDE_ANGLE_MULTIPLIER);
aim_strain *= osu_curr_obj.small_circle_bonus;
if with_slider_travel_dist {
aim_strain += slider_bonus * Self::SLIDER_MULTIPLIER;
}
aim_strain
}
const fn calc_wide_angle_bonus(angle: f64) -> f64 {
smoothstep(angle, f64::to_radians(40.0), f64::to_radians(140.0))
}
const fn calc_acute_angle_bonus(angle: f64) -> f64 {
smoothstep(angle, f64::to_radians(140.0), f64::to_radians(40.0))
}
}