finished osu std + bunch of bug fixes
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::{
|
||||
parse::Pos2,
|
||||
};
|
||||
|
||||
use super::{osu_object::NestedObject, OsuObject, ScalingFactor};
|
||||
use super::{osu_object::OsuSlider, OsuObject, ScalingFactor};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OsuDifficultyObject<'h> {
|
||||
@@ -31,21 +31,6 @@ impl<'h> OsuDifficultyObject<'h> {
|
||||
// * Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
|
||||
let strain_time = delta_time.max(Self::MIN_DELTA_TIME as f64);
|
||||
|
||||
// TODO: remove
|
||||
// println!(
|
||||
// "[{}] lazy_jump_dist={} | lazy_travel_dist={} | \
|
||||
// min_jump_dist={} | min_jump_time={} \
|
||||
// | travel_dist={} | travel_time={} | angle={:?}",
|
||||
// base.start_time,
|
||||
// dists.lazy_jump_dist,
|
||||
// dists.lazy_travel_dist,
|
||||
// dists.min_jump_dist,
|
||||
// dists.min_jump_time,
|
||||
// dists.travel_dist,
|
||||
// dists.travel_time,
|
||||
// dists.angle,
|
||||
// );
|
||||
|
||||
Self {
|
||||
start_time,
|
||||
delta_time,
|
||||
@@ -56,7 +41,13 @@ impl<'h> OsuDifficultyObject<'h> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn opacity_at(&self, time: f64, hidden: bool) -> f64 {
|
||||
pub(crate) fn opacity_at(
|
||||
&self,
|
||||
time: f64,
|
||||
hidden: bool,
|
||||
time_preempt: f64,
|
||||
time_fade_in: f64,
|
||||
) -> f64 {
|
||||
if time > self.base.start_time {
|
||||
// * Consider a hitobject as being invisible when its start time is passed.
|
||||
// * In reality the hitobject will be visible beyond its start time up until its hittable window has passed,
|
||||
@@ -64,15 +55,14 @@ impl<'h> OsuDifficultyObject<'h> {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let fade_in_start_time = self.base.start_time - self.base.time_preempt;
|
||||
let fade_in_duration = self.base.time_fade_in;
|
||||
let fade_in_start_time = self.base.start_time - time_preempt;
|
||||
let fade_in_duration = time_fade_in;
|
||||
|
||||
if hidden {
|
||||
// * Taken from OsuModHidden.
|
||||
let fade_out_start_time =
|
||||
self.base.start_time - self.base.time_preempt + self.base.time_fade_in;
|
||||
let fade_out_start_time = self.base.start_time - time_preempt + time_fade_in;
|
||||
const FADE_OUT_DURATION_MULTIPLIER: f64 = 0.3;
|
||||
let fade_out_duration = self.base.time_preempt * FADE_OUT_DURATION_MULTIPLIER;
|
||||
let fade_out_duration = time_preempt * FADE_OUT_DURATION_MULTIPLIER;
|
||||
|
||||
(((time - fade_in_start_time) / fade_in_duration).clamp(0.0, 1.0))
|
||||
.min(1.0 - ((time - fade_out_start_time) / fade_out_duration).clamp(0.0, 1.0))
|
||||
@@ -107,38 +97,28 @@ impl Distances {
|
||||
strain_time: f64,
|
||||
scaling_factor_: &ScalingFactor,
|
||||
) -> Self {
|
||||
let mut this = if let OsuObjectKind::Slider {
|
||||
lazy_end_pos,
|
||||
lazy_travel_time,
|
||||
nested_objects,
|
||||
..
|
||||
} = &mut base.kind
|
||||
{
|
||||
let lazy_travel_dist = Self::compute_slider_cursor_pos(
|
||||
base.pos,
|
||||
base.start_time,
|
||||
lazy_end_pos,
|
||||
lazy_travel_time,
|
||||
nested_objects,
|
||||
scaling_factor_,
|
||||
);
|
||||
let mut this =
|
||||
if let Some(slider_values) = Self::compute_slider_cursor_pos(base, scaling_factor_) {
|
||||
let SliderValues {
|
||||
lazy_travel_dist,
|
||||
slider,
|
||||
} = slider_values;
|
||||
|
||||
let repeat_count = nested_objects.iter().fold(0, |repeats, nested| {
|
||||
repeats + matches!(nested.kind, NestedObjectKind::Repeat) as usize
|
||||
});
|
||||
let repeat_count = slider.repeat_count();
|
||||
|
||||
Self {
|
||||
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||
travel_dist: (lazy_travel_dist
|
||||
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
|
||||
as f64,
|
||||
travel_time: lazy_travel_time.max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
|
||||
lazy_travel_dist,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
Self::default()
|
||||
};
|
||||
Self {
|
||||
// * Bonus for repeat sliders until a better per nested object strain system can be achieved.
|
||||
travel_dist: (lazy_travel_dist
|
||||
* (1.0 + repeat_count as f64 / 2.5).powf(1.0 / 2.5) as f32)
|
||||
as f64,
|
||||
travel_time: (base.lazy_travel_time() / clock_rate)
|
||||
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64),
|
||||
lazy_travel_dist,
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
Self::default()
|
||||
};
|
||||
|
||||
// * We don't need to calculate either angle or distance when
|
||||
// * one of the last->curr objects is a spinner
|
||||
@@ -149,23 +129,19 @@ impl Distances {
|
||||
// * We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
let scaling_factor = scaling_factor_.factor;
|
||||
|
||||
let last_cursor_pos = Self::get_end_cursor_pos(last, scaling_factor_);
|
||||
let last_cursor_pos = Self::get_end_cursor_pos(last);
|
||||
|
||||
this.lazy_jump_dist =
|
||||
(base.pos * scaling_factor - last_cursor_pos * scaling_factor).length() as f64;
|
||||
this.lazy_jump_dist = (base.stacked_pos() * scaling_factor
|
||||
- last_cursor_pos * scaling_factor)
|
||||
.length() as f64;
|
||||
this.min_jump_time = strain_time;
|
||||
this.min_jump_dist = this.lazy_jump_dist;
|
||||
|
||||
if let OsuObjectKind::Slider {
|
||||
end_pos,
|
||||
lazy_travel_time,
|
||||
..
|
||||
} = &last.kind
|
||||
{
|
||||
let last_travel_dist =
|
||||
(lazy_travel_time / clock_rate).max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
||||
if let OsuObjectKind::Slider(slider) = &last.kind {
|
||||
let last_travel_time = (last.lazy_travel_time() / clock_rate)
|
||||
.max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
||||
this.min_jump_time =
|
||||
(strain_time - last_travel_dist).max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
||||
(strain_time - last_travel_time).max(OsuDifficultyObject::MIN_DELTA_TIME as f64);
|
||||
|
||||
// * There are two types of slider-to-object patterns to consider in order
|
||||
// * to better approximate the real movement a player will take to jump between the hitobjects.
|
||||
@@ -190,19 +166,22 @@ impl Distances {
|
||||
// *
|
||||
// * Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
||||
|
||||
let tail_jump_dist = (*end_pos - base.pos).length() * scaling_factor;
|
||||
let stacked_tail_pos =
|
||||
slider.tail().map_or_else(|| last.pos(), |tail| tail.pos) + last.stack_offset;
|
||||
|
||||
this.min_jump_dist = ((this.lazy_jump_dist
|
||||
let tail_jump_dist = (stacked_tail_pos - base.stacked_pos()).length() * scaling_factor;
|
||||
|
||||
this.min_jump_dist = (this.lazy_jump_dist
|
||||
- (Self::MAXIMUM_SLIDER_RADIUS - Self::ASSUMED_SLIDER_RADIUS) as f64)
|
||||
.min((tail_jump_dist - Self::MAXIMUM_SLIDER_RADIUS) as f64))
|
||||
.max(0.0);
|
||||
.min((tail_jump_dist - Self::MAXIMUM_SLIDER_RADIUS) as f64)
|
||||
.max(0.0);
|
||||
}
|
||||
|
||||
if let Some(last_last) = last_last.filter(|obj| !obj.is_spinner()) {
|
||||
let last_last_cursor_pos = Self::get_end_cursor_pos(last_last, scaling_factor_);
|
||||
let last_last_cursor_pos = Self::get_end_cursor_pos(last_last);
|
||||
|
||||
let v1 = last_last_cursor_pos - last.pos;
|
||||
let v2 = base.pos - last_cursor_pos;
|
||||
let v1 = last_last_cursor_pos - last.stacked_pos();
|
||||
let v2 = base.stacked_pos() - last_cursor_pos;
|
||||
|
||||
let dot = v1.dot(v2) as f64;
|
||||
let det = (v1.x * v2.y - v1.y * v2.x) as f64;
|
||||
@@ -213,27 +192,32 @@ impl Distances {
|
||||
this
|
||||
}
|
||||
|
||||
pub(crate) fn compute_slider_cursor_pos(
|
||||
stacked_pos: Pos2,
|
||||
start_time: f64,
|
||||
lazy_end_pos: &mut Pos2,
|
||||
lazy_travel_time: &mut f64,
|
||||
nested_objects: &[NestedObject],
|
||||
pub(crate) fn compute_slider_cursor_pos<'h>(
|
||||
hit_object: &'h mut OsuObject,
|
||||
scaling_factor_: &ScalingFactor,
|
||||
) -> f32 {
|
||||
let mut curr_cursor_pos = stacked_pos;
|
||||
) -> Option<SliderValues<'h>> {
|
||||
let pos = hit_object.pos();
|
||||
|
||||
let slider = if let OsuObjectKind::Slider(slider) = &mut hit_object.kind {
|
||||
slider
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut curr_cursor_pos = pos + hit_object.stack_offset;
|
||||
let scaling_factor = Self::NORMALISED_RADIUS as f64 / scaling_factor_.radius as f64;
|
||||
|
||||
let mut lazy_travel_dist: f32 = 0.0;
|
||||
|
||||
for (curr_movement_obj, i) in nested_objects.iter().zip(1..) {
|
||||
let mut curr_movement = curr_movement_obj.pos - curr_cursor_pos;
|
||||
for (curr_movement_obj, i) in slider.nested_iter().zip(1..) {
|
||||
let mut curr_movement =
|
||||
(curr_movement_obj.pos + hit_object.stack_offset) - 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 = Self::ASSUMED_SLIDER_RADIUS as f64;
|
||||
|
||||
if i == nested_objects.len() {
|
||||
if i == slider.nested_len() {
|
||||
// * 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.
|
||||
@@ -242,7 +226,7 @@ impl Distances {
|
||||
// * 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;
|
||||
let lazy_movement = slider.lazy_end_pos - curr_cursor_pos;
|
||||
|
||||
if lazy_movement.length() < curr_movement.length() {
|
||||
curr_movement = lazy_movement;
|
||||
@@ -261,26 +245,22 @@ impl Distances {
|
||||
curr_movement_len *= (curr_movement_len - required_movement) / curr_movement_len;
|
||||
lazy_travel_dist += curr_movement_len as f32;
|
||||
}
|
||||
|
||||
if i == nested_objects.len() {
|
||||
*lazy_end_pos = curr_cursor_pos;
|
||||
}
|
||||
}
|
||||
|
||||
*lazy_travel_time = nested_objects
|
||||
.last()
|
||||
.map_or(0.0, |nested| nested.start_time - start_time);
|
||||
slider.lazy_end_pos = curr_cursor_pos;
|
||||
|
||||
lazy_travel_dist
|
||||
Some(SliderValues {
|
||||
lazy_travel_dist,
|
||||
slider,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_end_cursor_pos(hit_object: &OsuObject, scaling_factor: &ScalingFactor) -> Pos2 {
|
||||
if hit_object.is_slider() {
|
||||
let stack_offset = scaling_factor.stack_offset(hit_object.stack_height);
|
||||
|
||||
hit_object.lazy_end_pos(stack_offset)
|
||||
} else {
|
||||
hit_object.pos
|
||||
}
|
||||
fn get_end_cursor_pos(hit_object: &OsuObject) -> Pos2 {
|
||||
hit_object.lazy_end_pos()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SliderValues<'s> {
|
||||
lazy_travel_dist: f32,
|
||||
slider: &'s OsuSlider,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
fmt::{Debug, Formatter, Result as FmtResult},
|
||||
mem,
|
||||
vec::IntoIter,
|
||||
};
|
||||
|
||||
use crate::{curve::CurveBuffers, Beatmap, Mods};
|
||||
@@ -13,7 +12,8 @@ use super::{
|
||||
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
||||
scaling_factor::ScalingFactor,
|
||||
skills::{Aim, Flashlight, Skill, Speed},
|
||||
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, PERFORMANCE_BASE_MULTIPLIER,
|
||||
stacking, OsuDifficultyAttributes, DIFFICULTY_MULTIPLIER, FADE_IN_DURATION_MULTIPLIER,
|
||||
PERFORMANCE_BASE_MULTIPLIER, PREEMPT_MIN,
|
||||
};
|
||||
|
||||
/// Gradually calculate the difficulty attributes of an osu!standard map.
|
||||
@@ -74,8 +74,22 @@ impl OsuGradualDifficultyAttributes {
|
||||
let map_attrs = map.attributes().mods(mods).build();
|
||||
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||
let hr = mods.hr();
|
||||
let time_preempt = map_attrs.hit_windows.ar;
|
||||
let hit_window = 2.0 * map_attrs.hit_windows.od;
|
||||
let time_preempt = map_attrs.hit_windows.ar;
|
||||
|
||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||
// * This uniform speedup is hard to match 1:1, however we can at least make
|
||||
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
||||
// * classically known, but it feels good.
|
||||
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
||||
// * become smaller leading to hitcircles not fully fading in.
|
||||
let time_fade_in = if mods.hd() {
|
||||
time_preempt * FADE_IN_DURATION_MULTIPLIER
|
||||
} else {
|
||||
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
|
||||
};
|
||||
|
||||
let mut attrs = OsuDifficultyAttributes {
|
||||
ar: map_attrs.ar,
|
||||
@@ -94,7 +108,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
let hit_objects_iter = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
||||
.filter_map(|h| OsuObject::new(h, &mut params));
|
||||
|
||||
let mut hit_objects = Vec::with_capacity(map.hit_objects.len());
|
||||
hit_objects.extend(hit_objects_iter);
|
||||
@@ -113,13 +127,12 @@ impl OsuGradualDifficultyAttributes {
|
||||
}
|
||||
|
||||
let mut hit_objects_iter = hit_objects.iter_mut().map(|h| {
|
||||
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
||||
h.pos += stack_offset;
|
||||
h.post_process(hr, &scaling_factor);
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let skills = create_skills(mods, scaling_factor.radius);
|
||||
let skills = create_skills(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||
|
||||
let last = match hit_objects_iter.next() {
|
||||
Some(prev) => prev,
|
||||
@@ -139,22 +152,7 @@ impl OsuGradualDifficultyAttributes {
|
||||
let mut last_last = None;
|
||||
|
||||
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
||||
if let OsuObjectKind::Slider {
|
||||
lazy_travel_time,
|
||||
lazy_end_pos,
|
||||
nested_objects,
|
||||
..
|
||||
} = &mut last.kind
|
||||
{
|
||||
Distances::compute_slider_cursor_pos(
|
||||
last.pos,
|
||||
last.start_time,
|
||||
lazy_end_pos,
|
||||
lazy_travel_time,
|
||||
nested_objects,
|
||||
&scaling_factor,
|
||||
);
|
||||
}
|
||||
Distances::compute_slider_cursor_pos(last, &scaling_factor);
|
||||
|
||||
let mut last = &*last;
|
||||
let mut diff_objects = Vec::with_capacity(map.hit_objects.len().saturating_sub(2));
|
||||
@@ -218,9 +216,9 @@ impl Iterator for OsuGradualDifficultyAttributes {
|
||||
|
||||
match &curr.base.kind {
|
||||
OsuObjectKind::Circle => attrs.n_circles += 1,
|
||||
OsuObjectKind::Slider { nested_objects, .. } => {
|
||||
OsuObjectKind::Slider(slider) => {
|
||||
attrs.n_sliders += 1;
|
||||
attrs.max_combo += nested_objects.len();
|
||||
attrs.max_combo += slider.nested_len();
|
||||
}
|
||||
OsuObjectKind::Spinner { .. } => attrs.n_spinners += 1,
|
||||
}
|
||||
@@ -316,37 +314,6 @@ impl ExactSizeIterator for OsuGradualDifficultyAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct OsuObjectIter {
|
||||
hit_objects: IntoIter<OsuObject>,
|
||||
scaling_factor: ScalingFactor,
|
||||
}
|
||||
|
||||
impl Iterator for OsuObjectIter {
|
||||
type Item = OsuObject;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut h = self.hit_objects.next()?;
|
||||
let stack_offset = self.scaling_factor.stack_offset(h.stack_height);
|
||||
h.pos += stack_offset;
|
||||
|
||||
Some(h)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.hit_objects.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for OsuObjectIter {
|
||||
#[inline]
|
||||
fn len(&self) -> usize {
|
||||
self.hit_objects.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -213,7 +213,7 @@ mod tests {
|
||||
n300: 88,
|
||||
n100: 8,
|
||||
n50: 2,
|
||||
misses: 2,
|
||||
n_misses: 2,
|
||||
};
|
||||
|
||||
let next = gradual1.process_next_object(state.clone());
|
||||
@@ -236,7 +236,7 @@ mod tests {
|
||||
n300: 601,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
misses: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let gradual_end = gradual.process_next_n_objects(state, usize::MAX).unwrap();
|
||||
@@ -259,7 +259,7 @@ mod tests {
|
||||
n300: 100,
|
||||
n100: 0,
|
||||
n50: 0,
|
||||
misses: 0,
|
||||
n_misses: 0,
|
||||
};
|
||||
|
||||
let gradual = gradual.process_next_n_objects(state, n).unwrap();
|
||||
|
||||
+54
-44
@@ -6,11 +6,11 @@ mod pp;
|
||||
mod scaling_factor;
|
||||
mod skills;
|
||||
|
||||
use crate::{curve::CurveBuffers, AnyStars, Beatmap, GameMode, Mods};
|
||||
use crate::{curve::CurveBuffers, parse::Pos2, AnyStars, Beatmap, GameMode, Mods};
|
||||
|
||||
use self::{
|
||||
difficulty_object::{Distances, OsuDifficultyObject},
|
||||
osu_object::{ObjectParameters, OsuObject, OsuObjectKind},
|
||||
osu_object::{ObjectParameters, OsuObject},
|
||||
scaling_factor::ScalingFactor,
|
||||
skills::{Aim, Flashlight, Skill, Speed},
|
||||
};
|
||||
@@ -24,6 +24,9 @@ const NORMALIZED_RADIUS: f32 = 50.0;
|
||||
const STACK_DISTANCE: f32 = 3.0;
|
||||
// * This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
const PERFORMANCE_BASE_MULTIPLIER: f64 = 1.14;
|
||||
const PREEMPT_MIN: f64 = 450.0;
|
||||
const FADE_IN_DURATION_MULTIPLIER: f64 = 0.4;
|
||||
const PLAYFIELD_BASE_SIZE: Pos2 = Pos2 { x: 512.0, y: 384.0 };
|
||||
|
||||
/// Difficulty calculator on osu!standard maps.
|
||||
///
|
||||
@@ -237,16 +240,30 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
let take = passed_objects.unwrap_or(map.hit_objects.len());
|
||||
let clock_rate = clock_rate.unwrap_or_else(|| mods.clock_rate());
|
||||
|
||||
let map_attributes = map.attributes().mods(mods).clock_rate(clock_rate).build();
|
||||
let scaling_factor = ScalingFactor::new(map_attributes.cs);
|
||||
let map_attrs = map.attributes().mods(mods).clock_rate(clock_rate).build();
|
||||
let scaling_factor = ScalingFactor::new(map_attrs.cs);
|
||||
let hr = mods.hr();
|
||||
let time_preempt = map_attributes.hit_windows.ar;
|
||||
let hit_window = 2.0 * map_attributes.hit_windows.od;
|
||||
let hit_window = 2.0 * map_attrs.hit_windows.od;
|
||||
let time_preempt = (map_attrs.hit_windows.ar * clock_rate) as f32 as f64;
|
||||
|
||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||
// * This uniform speedup is hard to match 1:1, however we can at least make
|
||||
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
||||
// * classically known, but it feels good.
|
||||
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
||||
// * become smaller leading to hitcircles not fully fading in.
|
||||
let time_fade_in = if mods.hd() {
|
||||
time_preempt * FADE_IN_DURATION_MULTIPLIER
|
||||
} else {
|
||||
400.0 * (time_preempt / PREEMPT_MIN).min(1.0)
|
||||
};
|
||||
|
||||
let mut attributes = OsuDifficultyAttributes {
|
||||
ar: map_attributes.ar,
|
||||
hp: map_attributes.hp,
|
||||
od: map_attributes.od,
|
||||
ar: map_attrs.ar,
|
||||
hp: map_attrs.hp,
|
||||
od: map_attrs.od,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -261,7 +278,7 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
.hit_objects
|
||||
.iter()
|
||||
.take(take)
|
||||
.filter_map(|h| OsuObject::new(h, hr, &mut params));
|
||||
.filter_map(|h| OsuObject::new(h, &mut params));
|
||||
|
||||
let mut hit_objects = Vec::with_capacity(take.min(map.hit_objects.len()));
|
||||
hit_objects.extend(hit_objects_iter);
|
||||
@@ -275,13 +292,12 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
}
|
||||
|
||||
let mut hit_objects = hit_objects.iter_mut().map(|h| {
|
||||
let stack_offset = scaling_factor.stack_offset(h.stack_height);
|
||||
h.pos += stack_offset;
|
||||
h.post_process(hr, &scaling_factor);
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let mut skills = create_skills(mods, scaling_factor.radius);
|
||||
let mut skills = create_skills(mods, scaling_factor.radius, time_preempt, time_fade_in);
|
||||
|
||||
let last = match hit_objects.next() {
|
||||
Some(prev) => prev,
|
||||
@@ -291,25 +307,10 @@ fn calculate_skills(params: OsuStars<'_>) -> ([Box<dyn Skill>; 4], OsuDifficulty
|
||||
let mut last_last = None;
|
||||
|
||||
// Prepare `lazy_travel_dist` and `lazy_end_pos` for `last` manually
|
||||
if let OsuObjectKind::Slider {
|
||||
lazy_travel_time,
|
||||
lazy_end_pos,
|
||||
nested_objects,
|
||||
..
|
||||
} = &mut last.kind
|
||||
{
|
||||
Distances::compute_slider_cursor_pos(
|
||||
last.pos,
|
||||
last.start_time,
|
||||
lazy_end_pos,
|
||||
lazy_travel_time,
|
||||
nested_objects,
|
||||
&scaling_factor,
|
||||
);
|
||||
}
|
||||
Distances::compute_slider_cursor_pos(last, &scaling_factor);
|
||||
|
||||
let mut last = &*last;
|
||||
let mut diff_objects = Vec::with_capacity(hit_objects.len().saturating_sub(2));
|
||||
let mut diff_objects = Vec::with_capacity(hit_objects.len());
|
||||
|
||||
for (i, curr) in hit_objects.enumerate() {
|
||||
let delta_time = (curr.start_time - last.start_time) / clock_rate;
|
||||
@@ -399,8 +400,8 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
// * o <- hitCircle has stack of -2
|
||||
if hit_objects[n].is_slider()
|
||||
&& hit_objects[n]
|
||||
.end_pos()
|
||||
.distance(hit_objects[obj_i_idx].pos)
|
||||
.pre_stacked_end_pos()
|
||||
.distance(hit_objects[obj_i_idx].pos())
|
||||
< STACK_DISTANCE
|
||||
{
|
||||
let offset =
|
||||
@@ -409,7 +410,11 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
for j in n + 1..=i {
|
||||
// * For each object which was declared under this slider, we will offset
|
||||
// * it to appear *below* the slider end (rather than above).
|
||||
if hit_objects[n].end_pos().distance(hit_objects[j].pos) < STACK_DISTANCE {
|
||||
if hit_objects[n]
|
||||
.pre_stacked_end_pos()
|
||||
.distance(hit_objects[j].pos())
|
||||
< STACK_DISTANCE
|
||||
{
|
||||
hit_objects[j].stack_height -= offset;
|
||||
}
|
||||
}
|
||||
@@ -420,7 +425,7 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
break;
|
||||
}
|
||||
|
||||
if hit_objects[n].pos.distance(hit_objects[obj_i_idx].pos) < STACK_DISTANCE {
|
||||
if hit_objects[n].pos().distance(hit_objects[obj_i_idx].pos()) < STACK_DISTANCE {
|
||||
// * Keep processing as if there are no sliders.
|
||||
// * If we come across a slider, this gets cancelled out.
|
||||
// * NOTE: Sliders with start positions stacking
|
||||
@@ -441,15 +446,15 @@ fn stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
|
||||
if hit_objects[n].is_spinner() {
|
||||
continue;
|
||||
} else if hit_objects[obj_i_idx].start_time - hit_objects[n].start_time
|
||||
> stack_threshold
|
||||
{
|
||||
}
|
||||
|
||||
if hit_objects[obj_i_idx].start_time - hit_objects[n].start_time > stack_threshold {
|
||||
break; // * We are no longer within stacking range of the previous object.
|
||||
}
|
||||
|
||||
if hit_objects[n]
|
||||
.end_pos()
|
||||
.distance(hit_objects[obj_i_idx].pos)
|
||||
.pre_stacked_end_pos()
|
||||
.distance(hit_objects[obj_i_idx].pos())
|
||||
< STACK_DISTANCE
|
||||
{
|
||||
hit_objects[n].stack_height = hit_objects[obj_i_idx].stack_height + 1.0;
|
||||
@@ -467,7 +472,7 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
}
|
||||
|
||||
let mut start_time = hit_objects[i].end_time();
|
||||
let end_pos = hit_objects[i].end_pos();
|
||||
let pos2 = hit_objects[i].old_stacking_pos2();
|
||||
|
||||
let mut slider_stack = 0.0;
|
||||
|
||||
@@ -476,10 +481,10 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
break;
|
||||
}
|
||||
|
||||
if hit_objects[j].pos.distance(hit_objects[i].pos) < STACK_DISTANCE {
|
||||
if hit_objects[j].pos().distance(hit_objects[i].pos()) < STACK_DISTANCE {
|
||||
hit_objects[i].stack_height += 1.0;
|
||||
start_time = hit_objects[j].end_time();
|
||||
} else if hit_objects[j].pos.distance(end_pos) < STACK_DISTANCE {
|
||||
} else if hit_objects[j].pos().distance(pos2) < STACK_DISTANCE {
|
||||
slider_stack += 1.0;
|
||||
hit_objects[j].stack_height -= slider_stack;
|
||||
start_time = hit_objects[j].end_time();
|
||||
@@ -488,12 +493,17 @@ fn old_stacking(hit_objects: &mut [OsuObject], stack_threshold: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_skills(mods: u32, radius: f32) -> [Box<dyn Skill>; 4] {
|
||||
fn create_skills(
|
||||
mods: u32,
|
||||
radius: f32,
|
||||
time_preempt: f64,
|
||||
time_fade_in: f64,
|
||||
) -> [Box<dyn Skill>; 4] {
|
||||
[
|
||||
Box::new(Aim::new(true)) as Box<dyn Skill>,
|
||||
Box::new(Aim::new(false)) as Box<dyn Skill>,
|
||||
Box::new(Speed::new()) as Box<dyn Skill>,
|
||||
Box::new(Flashlight::new(mods, radius)) as Box<dyn Skill>,
|
||||
Box::new(Flashlight::new(mods, radius, time_preempt, time_fade_in)) as Box<dyn Skill>,
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+198
-139
@@ -1,9 +1,8 @@
|
||||
use std::{cmp::Ordering, convert::identity};
|
||||
use std::slice::Iter;
|
||||
|
||||
use super::OsuDifficultyAttributes;
|
||||
use super::{scaling_factor::ScalingFactor, OsuDifficultyAttributes, PLAYFIELD_BASE_SIZE};
|
||||
|
||||
use crate::{
|
||||
beatmap::DifficultyPoint,
|
||||
curve::{Curve, CurveBuffers},
|
||||
parse::{HitObject, HitObjectKind, Pos2},
|
||||
Beatmap,
|
||||
@@ -14,31 +13,65 @@ const BASE_SCORING_DISTANCE: f64 = 100.0;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OsuObject {
|
||||
pos: Pos2,
|
||||
pub(crate) start_time: f64,
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) stack_offset: Pos2,
|
||||
pub(crate) stack_height: f32,
|
||||
pub(crate) time_preempt: f64,
|
||||
pub(crate) time_fade_in: f64,
|
||||
pub(crate) kind: OsuObjectKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum OsuObjectKind {
|
||||
Circle,
|
||||
Slider {
|
||||
end_time: f64,
|
||||
end_pos: Pos2,
|
||||
lazy_travel_time: f64,
|
||||
lazy_end_pos: Pos2,
|
||||
nested_objects: Vec<NestedObject>,
|
||||
},
|
||||
Spinner {
|
||||
end_time: f64,
|
||||
},
|
||||
Slider(OsuSlider),
|
||||
Spinner { end_time: f64 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OsuSlider {
|
||||
pub(crate) end_time: f64,
|
||||
pub(crate) lazy_end_pos: Pos2,
|
||||
nested_objects: Vec<NestedObject>,
|
||||
}
|
||||
|
||||
impl OsuSlider {
|
||||
pub(crate) fn nested_len(&self) -> usize {
|
||||
self.nested_objects.len()
|
||||
}
|
||||
|
||||
pub(crate) fn nested_iter(&self) -> Iter<'_, NestedObject> {
|
||||
self.nested_objects.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn repeat_count(&self) -> usize {
|
||||
self.nested_objects.iter().fold(0, |count, nested| {
|
||||
count + matches!(nested.kind, NestedObjectKind::Repeat) as usize
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn end_pos(&self) -> Option<Pos2> {
|
||||
self.tail().map(|tail| tail.pos)
|
||||
}
|
||||
|
||||
pub(crate) fn tail(&self) -> Option<&NestedObject> {
|
||||
self.nested_objects
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|nested| matches!(nested.kind, NestedObjectKind::Tail))
|
||||
}
|
||||
|
||||
pub(crate) fn tail_mut(&mut self) -> Option<(usize, &mut NestedObject)> {
|
||||
self.nested_objects
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, nested)| matches!(nested.kind, NestedObjectKind::Tail))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct NestedObject {
|
||||
/// Note: `pos` does not include stacking!
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) start_time: f64,
|
||||
pub(crate) kind: NestedObjectKind,
|
||||
@@ -59,11 +92,7 @@ pub(crate) struct ObjectParameters<'a> {
|
||||
}
|
||||
|
||||
impl OsuObject {
|
||||
const PREEMPT_MIN: f64 = 450.0;
|
||||
const TIME_PREEMPT: f64 = 600.0;
|
||||
const TIME_FADE_IN: f64 = 400.0;
|
||||
|
||||
pub(crate) fn new(h: &HitObject, hr: bool, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
pub(crate) fn new(h: &HitObject, params: &mut ObjectParameters<'_>) -> Option<Self> {
|
||||
let ObjectParameters {
|
||||
map,
|
||||
attributes,
|
||||
@@ -72,37 +101,17 @@ impl OsuObject {
|
||||
} = params;
|
||||
|
||||
attributes.max_combo += 1; // hitcircle, slider head, or spinner
|
||||
let mut pos = h.pos;
|
||||
|
||||
if hr {
|
||||
pos.y = 384.0 - pos.y;
|
||||
}
|
||||
let pos = h.pos;
|
||||
|
||||
let obj = match &h.kind {
|
||||
HitObjectKind::Circle => {
|
||||
attributes.n_circles += 1;
|
||||
|
||||
// TODO: check if ar needs to be adjusted
|
||||
let tmp_preempt =
|
||||
difficulty_range(map.ar as f64, 1800.0, 1200.0, Self::PREEMPT_MIN) as f32;
|
||||
let time_preempt = tmp_preempt as f64;
|
||||
|
||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||
// * This uniform speedup is hard to match 1:1, however we can at least make
|
||||
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
||||
// * classically known, but it feels good.
|
||||
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
||||
// * become smaller leading to hitcircles not fully fading in.
|
||||
let time_fade_in = 400.0 * (time_preempt / Self::PREEMPT_MIN).min(1.0);
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
pos,
|
||||
stack_offset: Pos2::default(),
|
||||
stack_height: 0.0,
|
||||
time_preempt,
|
||||
time_fade_in,
|
||||
kind: OsuObjectKind::Circle,
|
||||
}
|
||||
}
|
||||
@@ -125,14 +134,7 @@ impl OsuObject {
|
||||
// * 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.
|
||||
let tick_dist_mult = if map.version < 8 {
|
||||
let first_slider_vel = map
|
||||
.difficulty_points
|
||||
.first()
|
||||
.map_or(DifficultyPoint::DEFAULT_SLIDER_VEL, |point| {
|
||||
point.slider_vel
|
||||
});
|
||||
|
||||
first_slider_vel.recip()
|
||||
difficulty_point.slider_vel.recip()
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
@@ -163,21 +165,6 @@ impl OsuObject {
|
||||
|
||||
let mut curr_dist = tick_dist;
|
||||
|
||||
// TODO: check if ar needs to be adjusted
|
||||
let tmp_preempt =
|
||||
difficulty_range(map.ar as f64, 1800.0, 1200.0, Self::PREEMPT_MIN) as f32;
|
||||
let head_time_preempt = tmp_preempt as f64;
|
||||
|
||||
// * Preempt time can go below 450ms. Normally, this is achieved via the DT mod
|
||||
// * which uniformly speeds up all animations game wide regardless of AR.
|
||||
// * This uniform speedup is hard to match 1:1, however we can at least make
|
||||
// * AR>10 (via mods) feel good by extending the upper linear function above.
|
||||
// * Note that this doesn't exactly match the AR>10 visuals as they're
|
||||
// * classically known, but it feels good.
|
||||
// * This adjustment is necessary for AR>10, otherwise TimePreempt can
|
||||
// * become smaller leading to hitcircles not fully fading in.
|
||||
let head_time_fade_in = 400.0 * (head_time_preempt / Self::PREEMPT_MIN).min(1.0);
|
||||
|
||||
ticks.clear();
|
||||
|
||||
let mut nested_objects = if tick_dist != 0.0 {
|
||||
@@ -190,11 +177,7 @@ impl OsuObject {
|
||||
let progress = curr_dist / len;
|
||||
|
||||
let curr_time = h.start_time + progress * span_duration;
|
||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
if hr {
|
||||
curr_pos.y = 384.0 - curr_pos.y;
|
||||
}
|
||||
let curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
let tick = NestedObject {
|
||||
pos: curr_pos,
|
||||
@@ -215,11 +198,7 @@ impl OsuObject {
|
||||
|
||||
// Repeat point
|
||||
let curr_time = h.start_time + span_duration * span_idx_f64;
|
||||
let mut curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
if hr {
|
||||
curr_pos.y = 384.0 - curr_pos.y;
|
||||
}
|
||||
let curr_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
let repeat = NestedObject {
|
||||
pos: curr_pos,
|
||||
@@ -228,6 +207,7 @@ impl OsuObject {
|
||||
};
|
||||
|
||||
nested_objects.push(repeat);
|
||||
let span_offset = span_idx_f64 * span_duration;
|
||||
|
||||
// Ticks
|
||||
if span_idx & 1 == 1 {
|
||||
@@ -246,30 +226,20 @@ impl OsuObject {
|
||||
// 30 = 24 + 6
|
||||
// 32 = 24 + 8
|
||||
|
||||
let offset = span_idx_f64 * span_duration;
|
||||
let base = h.start_time + h.start_time + span_duration;
|
||||
|
||||
let tick_iter = ticks.iter().rev().zip(ticks.iter()).map(
|
||||
|((rev_pos, _), (_, time))| {
|
||||
let start_time = offset + time;
|
||||
|
||||
NestedObject {
|
||||
pos: *rev_pos,
|
||||
start_time,
|
||||
kind: NestedObjectKind::Tick,
|
||||
}
|
||||
},
|
||||
);
|
||||
let tick_iter = ticks.iter().rev().map(|(pos, time)| NestedObject {
|
||||
pos: *pos,
|
||||
start_time: span_offset + base - time,
|
||||
kind: NestedObjectKind::Tick,
|
||||
});
|
||||
|
||||
nested_objects.extend(tick_iter);
|
||||
} else {
|
||||
let tick_iter = ticks.iter().map(|(pos, time)| {
|
||||
let start_time = time + span_duration * span_idx_f64;
|
||||
|
||||
NestedObject {
|
||||
pos: *pos,
|
||||
start_time,
|
||||
kind: NestedObjectKind::Tick,
|
||||
}
|
||||
let tick_iter = ticks.iter().map(|(pos, time)| NestedObject {
|
||||
pos: *pos,
|
||||
start_time: time + span_offset,
|
||||
kind: NestedObjectKind::Tick,
|
||||
});
|
||||
|
||||
nested_objects.extend(tick_iter);
|
||||
@@ -287,11 +257,7 @@ impl OsuObject {
|
||||
.max(final_span_start_time + span_duration - LEGACY_LAST_TICK_OFFSET);
|
||||
|
||||
let progress = (*repeats % 2 == 0) as u8 as f64;
|
||||
let mut end_pos = h.pos + curve.position_at(progress);
|
||||
|
||||
if hr {
|
||||
end_pos.y = 384.0 - end_pos.y;
|
||||
}
|
||||
let end_pos = curve.position_at(progress);
|
||||
|
||||
// * 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.
|
||||
@@ -307,13 +273,10 @@ impl OsuObject {
|
||||
match nested_objects.last() {
|
||||
Some(last) if last.start_time > final_span_end_time => {
|
||||
let idx = nested_objects
|
||||
.binary_search_by(|nested| {
|
||||
nested
|
||||
.start_time
|
||||
.partial_cmp(&final_span_end_time)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
})
|
||||
.map_or_else(identity, identity);
|
||||
.iter()
|
||||
.rev()
|
||||
.position(|nested| nested.start_time <= final_span_end_time)
|
||||
.map_or(0, |i| nested_objects.len() - i);
|
||||
|
||||
nested_objects.insert(idx, legacy_last_tick);
|
||||
}
|
||||
@@ -336,25 +299,22 @@ impl OsuObject {
|
||||
}
|
||||
|
||||
// * temporary lazy end position until a real result can be derived.
|
||||
let mut lazy_end_pos = h.pos + curve.position_at(end_time_min);
|
||||
// The position is added after the stacking for the correct order of
|
||||
// floating point operations.
|
||||
let lazy_end_pos = curve.position_at(end_time_min);
|
||||
|
||||
if hr {
|
||||
lazy_end_pos.y = 384.0 - lazy_end_pos.y;
|
||||
}
|
||||
let slider = OsuSlider {
|
||||
end_time,
|
||||
lazy_end_pos,
|
||||
nested_objects,
|
||||
};
|
||||
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
pos,
|
||||
stack_offset: Pos2::default(),
|
||||
stack_height: 0.0,
|
||||
time_preempt: head_time_preempt,
|
||||
time_fade_in: head_time_fade_in,
|
||||
kind: OsuObjectKind::Slider {
|
||||
end_time,
|
||||
end_pos,
|
||||
lazy_end_pos,
|
||||
lazy_travel_time,
|
||||
nested_objects,
|
||||
},
|
||||
kind: OsuObjectKind::Slider(slider),
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { end_time } => {
|
||||
@@ -363,9 +323,8 @@ impl OsuObject {
|
||||
Self {
|
||||
start_time: h.start_time,
|
||||
pos,
|
||||
stack_offset: Pos2::default(),
|
||||
stack_height: 0.0,
|
||||
time_preempt: Self::TIME_PREEMPT,
|
||||
time_fade_in: Self::TIME_FADE_IN,
|
||||
kind: OsuObjectKind::Spinner {
|
||||
end_time: *end_time,
|
||||
},
|
||||
@@ -377,28 +336,78 @@ impl OsuObject {
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end_time(&self) -> f64 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle => self.start_time,
|
||||
OsuObjectKind::Slider { end_time, .. } => *end_time,
|
||||
OsuObjectKind::Slider(slider) => slider.end_time,
|
||||
OsuObjectKind::Spinner { end_time } => *end_time,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end_pos(&self) -> Pos2 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||
OsuObjectKind::Slider { end_pos, .. } => *end_pos,
|
||||
OsuObjectKind::Slider(slider) => slider.end_pos().unwrap_or(self.pos),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn lazy_end_pos(&self, stack_offset: Pos2) -> Pos2 {
|
||||
pub(crate) fn pre_stacked_end_pos(&self) -> Pos2 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||
OsuObjectKind::Slider { lazy_end_pos, .. } => *lazy_end_pos + stack_offset,
|
||||
OsuObjectKind::Slider(slider) => slider
|
||||
.end_pos()
|
||||
.map_or(self.pos, |end_pos| self.pos + end_pos),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn old_stacking_pos2(&self) -> Pos2 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.pos,
|
||||
OsuObjectKind::Slider(slider) => {
|
||||
// Old stacking requires the path end position
|
||||
// instead of slider end position
|
||||
let repeat_count = slider.repeat_count();
|
||||
|
||||
if repeat_count % 2 == 0 {
|
||||
slider
|
||||
.end_pos()
|
||||
.map_or(self.pos, |end_pos| self.pos + end_pos)
|
||||
} else {
|
||||
slider
|
||||
.nested_iter()
|
||||
.find(|nested| matches!(nested.kind, NestedObjectKind::Repeat))
|
||||
.map_or(self.pos, |repeat| repeat.pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn pos(&self) -> Pos2 {
|
||||
self.pos
|
||||
}
|
||||
|
||||
pub(crate) fn stacked_pos(&self) -> Pos2 {
|
||||
self.pos + self.stack_offset
|
||||
}
|
||||
|
||||
pub(crate) fn stacked_end_pos(&self) -> Pos2 {
|
||||
self.end_pos() + self.stack_offset
|
||||
}
|
||||
|
||||
pub(crate) fn lazy_end_pos(&self) -> Pos2 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => self.stacked_pos(),
|
||||
OsuObjectKind::Slider(slider) => slider.lazy_end_pos,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn lazy_travel_time(&self) -> f64 {
|
||||
match &self.kind {
|
||||
OsuObjectKind::Circle | OsuObjectKind::Spinner { .. } => 0.0,
|
||||
OsuObjectKind::Slider(slider) => slider
|
||||
.nested_objects
|
||||
.last()
|
||||
.map_or(0.0, |nested| nested.start_time - self.start_time),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,15 +425,65 @@ impl OsuObject {
|
||||
pub(crate) fn is_spinner(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Spinner { .. })
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: cleanup
|
||||
fn difficulty_range(difficulty: f64, min: f64, mid: f64, max: f64) -> f64 {
|
||||
if difficulty > 5.0 {
|
||||
mid + (max - mid) * (difficulty - 5.0) / 5.0
|
||||
} else if difficulty < 5.0 {
|
||||
mid - (mid - min) * (5.0 - difficulty) / 5.0
|
||||
} else {
|
||||
mid
|
||||
/// Applies stack offset, flips playfield for HR,
|
||||
/// and adjusts slider tails and lazy_end_positions.
|
||||
pub(crate) fn post_process(&mut self, hr: bool, scaling_factor: &ScalingFactor) {
|
||||
self.stack_offset = scaling_factor.stack_offset(self.stack_height);
|
||||
let pos = self.pos();
|
||||
|
||||
if let OsuObjectKind::Slider(slider) = &mut self.kind {
|
||||
if hr {
|
||||
let mut lazy_end_pos = pos;
|
||||
lazy_end_pos.y = PLAYFIELD_BASE_SIZE.y - lazy_end_pos.y;
|
||||
lazy_end_pos += self.stack_offset;
|
||||
|
||||
lazy_end_pos += Pos2 {
|
||||
x: slider.lazy_end_pos.x,
|
||||
y: -slider.lazy_end_pos.y,
|
||||
};
|
||||
|
||||
slider.lazy_end_pos = lazy_end_pos;
|
||||
|
||||
let tail_idx = slider.tail_mut().map(|(tail_idx, tail)| {
|
||||
let mut tail_pos = pos;
|
||||
tail_pos.y = PLAYFIELD_BASE_SIZE.y - tail_pos.y;
|
||||
|
||||
tail_pos += Pos2 {
|
||||
x: tail.pos.x,
|
||||
y: -tail.pos.y,
|
||||
};
|
||||
|
||||
tail.pos = tail_pos;
|
||||
|
||||
tail_idx
|
||||
});
|
||||
|
||||
if let Some(tail_idx) = tail_idx {
|
||||
for nested in slider.nested_objects[..tail_idx].iter_mut() {
|
||||
nested.pos.y = PLAYFIELD_BASE_SIZE.y - nested.pos.y;
|
||||
}
|
||||
|
||||
for nested in slider.nested_objects[tail_idx + 1..].iter_mut() {
|
||||
nested.pos.y = PLAYFIELD_BASE_SIZE.y - nested.pos.y;
|
||||
}
|
||||
} else {
|
||||
// Should never happen since sliders are bound to have a tail
|
||||
for nested in slider.nested_objects.iter_mut() {
|
||||
nested.pos.y = PLAYFIELD_BASE_SIZE.y - nested.pos.y;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
slider.lazy_end_pos += pos + self.stack_offset;
|
||||
|
||||
if let Some((_, tail)) = slider.tail_mut() {
|
||||
tail.pos += pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hr {
|
||||
self.pos.y = PLAYFIELD_BASE_SIZE.y - pos.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ impl ScalingFactor {
|
||||
Self {
|
||||
factor,
|
||||
radius,
|
||||
scale: scale * -6.4,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stack_offset(&self, stack_height: f32) -> Pos2 {
|
||||
Pos2::new(stack_height * self.scale)
|
||||
Pos2::new(stack_height * self.scale * -6.4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
f64::consts::{FRAC_PI_2, FRAC_PI_6, PI},
|
||||
f64::consts::{FRAC_PI_2, PI},
|
||||
mem,
|
||||
};
|
||||
|
||||
@@ -262,7 +262,7 @@ impl AimEvaluator {
|
||||
}
|
||||
|
||||
fn calc_wide_angle_bonus(angle: f64) -> f64 {
|
||||
let base = (3.0 / 4.0 * ((5.0 / 6.0 * PI).min(angle.max(FRAC_PI_6)) - FRAC_PI_6)).sin();
|
||||
let base = (3.0 / 4.0 * ((5.0 / 6.0 * PI).min(angle.max(PI / 6.0)) - PI / 6.0)).sin();
|
||||
|
||||
base * base
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::{any::Any, mem};
|
||||
|
||||
use crate::{
|
||||
osu::{
|
||||
difficulty_object::OsuDifficultyObject,
|
||||
osu_object::{NestedObjectKind, OsuObjectKind},
|
||||
},
|
||||
osu::{difficulty_object::OsuDifficultyObject, osu_object::OsuObjectKind},
|
||||
Mods,
|
||||
};
|
||||
|
||||
@@ -18,13 +15,15 @@ pub(crate) struct Flashlight {
|
||||
strain_peaks: Vec<f64>,
|
||||
has_hidden_mod: bool,
|
||||
scaling_factor: f64,
|
||||
time_preempt: f64,
|
||||
time_fade_in: f64,
|
||||
}
|
||||
|
||||
impl Flashlight {
|
||||
const SKILL_MULTIPLIER: f64 = 0.052;
|
||||
const STRAIN_DECAY_BASE: f64 = 0.15;
|
||||
|
||||
pub(crate) fn new(mods: u32, radius: f32) -> Self {
|
||||
pub(crate) fn new(mods: u32, radius: f32, time_preempt: f64, time_fade_in: f64) -> Self {
|
||||
Self {
|
||||
curr_strain: 0.0,
|
||||
curr_section_peak: 0.0,
|
||||
@@ -32,6 +31,8 @@ impl Flashlight {
|
||||
strain_peaks: Vec::new(),
|
||||
has_hidden_mod: mods.hd(),
|
||||
scaling_factor: 52.0 / radius as f64,
|
||||
time_preempt,
|
||||
time_fade_in,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +95,8 @@ impl StrainSkill for Flashlight {
|
||||
diff_objects,
|
||||
self.has_hidden_mod,
|
||||
self.scaling_factor,
|
||||
self.time_preempt,
|
||||
self.time_fade_in,
|
||||
) * Self::SKILL_MULTIPLIER;
|
||||
|
||||
self.curr_strain
|
||||
@@ -131,6 +134,8 @@ impl FlashlightEvaluator {
|
||||
diff_objects: &[OsuDifficultyObject<'_>],
|
||||
hidden: bool,
|
||||
scaling_factor: f64,
|
||||
time_preempt: f64,
|
||||
time_fade_in: f64,
|
||||
) -> f64 {
|
||||
if curr.base.is_spinner() {
|
||||
return 0.0;
|
||||
@@ -159,7 +164,8 @@ impl FlashlightEvaluator {
|
||||
let curr_hit_obj = curr_obj.base;
|
||||
|
||||
if !curr_obj.base.is_spinner() {
|
||||
let jump_dist = (osu_hit_obj.pos - curr_hit_obj.end_pos()).length() as f64;
|
||||
let jump_dist =
|
||||
(osu_hit_obj.stacked_pos() - curr_hit_obj.stacked_end_pos()).length() as f64;
|
||||
cumulative_strain_time += last_obj.strain_time;
|
||||
|
||||
// * We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||
@@ -173,7 +179,13 @@ impl FlashlightEvaluator {
|
||||
// * Bonus based on how visible the object is.
|
||||
let opacity_bonus = 1.0
|
||||
+ Self::MAX_OPACITY_BONUS
|
||||
* (1.0 - osu_curr.opacity_at(curr_hit_obj.start_time, hidden));
|
||||
* (1.0
|
||||
- osu_curr.opacity_at(
|
||||
curr_hit_obj.start_time,
|
||||
hidden,
|
||||
time_preempt,
|
||||
time_fade_in,
|
||||
));
|
||||
|
||||
result += stack_nerf * opacity_bonus * scaling_factor * jump_dist
|
||||
/ cumulative_strain_time;
|
||||
@@ -205,7 +217,7 @@ impl FlashlightEvaluator {
|
||||
|
||||
let mut slider_bonus = 0.0;
|
||||
|
||||
if let OsuObjectKind::Slider { nested_objects, .. } = &osu_curr.base.kind {
|
||||
if let OsuObjectKind::Slider(slider) = &osu_curr.base.kind {
|
||||
// * Invert the scaling factor to determine the true travel distance independent of circle size.
|
||||
let pixel_travel_dist = osu_curr.dists.lazy_travel_dist as f64 / scaling_factor;
|
||||
|
||||
@@ -219,9 +231,7 @@ impl FlashlightEvaluator {
|
||||
slider_bonus *= pixel_travel_dist;
|
||||
|
||||
// * Nerf sliders with repeats, as less memorisation is required.
|
||||
let repeat_count = nested_objects.iter().fold(0, |count, nested| {
|
||||
count + matches!(nested.kind, NestedObjectKind::Repeat) as usize
|
||||
});
|
||||
let repeat_count = slider.repeat_count();
|
||||
|
||||
if repeat_count > 0 {
|
||||
slider_bonus /= (repeat_count + 1) as f64;
|
||||
|
||||
@@ -249,13 +249,13 @@ impl RhythmEvaluator {
|
||||
let base = (PI / (prev_delta.min(curr_delta) / prev_delta.max(curr_delta))).sin();
|
||||
let curr_ratio = 1.0 + 6.0 * (base * base).min(0.5);
|
||||
|
||||
let hit_window = !curr_obj.base.is_spinner() as u64 as f64 * hit_window;
|
||||
|
||||
let mut window_penalty = ((((prev_delta - curr_delta).abs() - hit_window * 0.3)
|
||||
.max(0.0))
|
||||
/ (hit_window * 0.3))
|
||||
.min(1.0);
|
||||
|
||||
println!("window_penalty: prev={prev_delta} | curr={curr_delta} | window={hit_window} => {window_penalty}");
|
||||
|
||||
window_penalty = window_penalty.min(1.0);
|
||||
|
||||
let mut effective_ratio = window_penalty * curr_ratio;
|
||||
@@ -322,10 +322,6 @@ impl RhythmEvaluator {
|
||||
}
|
||||
|
||||
// * produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
let res = (4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0;
|
||||
|
||||
println!("res={res}");
|
||||
|
||||
res
|
||||
(4.0 + rhythm_complexity_sum * Self::RHYTHM_MULTIPLIER).sqrt() / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user