osu: added rough stack calculation
This commit is contained in:
@@ -25,8 +25,8 @@ impl<'h> DifficultyObject<'h> {
|
||||
let strain_time = delta.max(50.0);
|
||||
|
||||
let pos = base.pos;
|
||||
let travel_dist = prev.travel_dist.unwrap_or(0.0);
|
||||
let prev_cursor_pos = prev.end_pos;
|
||||
let travel_dist = prev.travel_dist();
|
||||
let prev_cursor_pos = prev.end_pos();
|
||||
|
||||
let jump_dist = if base.is_spinner() {
|
||||
0.0
|
||||
@@ -35,7 +35,7 @@ impl<'h> DifficultyObject<'h> {
|
||||
};
|
||||
|
||||
let angle = prev_prev.map(|prev_prev| {
|
||||
let prev_prev_cursor_pos = prev_prev.end_pos;
|
||||
let prev_prev_cursor_pos = prev_prev.end_pos();
|
||||
|
||||
let v1 = prev_prev_cursor_pos - prev_cursor_pos;
|
||||
let v2 = pos - prev_cursor_pos;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#![cfg(feature = "all_included")]
|
||||
|
||||
use super::super::DifficultyAttributes;
|
||||
use crate::Pos2;
|
||||
|
||||
mod difficulty_object;
|
||||
mod osu_object;
|
||||
@@ -24,6 +25,8 @@ const OBJECT_RADIUS: f32 = 64.0;
|
||||
const SECTION_LEN: f32 = 400.0;
|
||||
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
|
||||
const NORMALIZED_RADIUS: f32 = 52.0;
|
||||
const TIME_PREEMPT: f32 = 600.0; // TODO: Check mod influences
|
||||
const STACK_DISTANCE: f32 = 3.0;
|
||||
|
||||
/// Star calculation for osu!standard maps.
|
||||
///
|
||||
@@ -51,7 +54,8 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
}
|
||||
|
||||
let section_len = SECTION_LEN * map_attributes.clock_rate;
|
||||
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let radius = OBJECT_RADIUS * scale;
|
||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
if radius < 30.0 {
|
||||
@@ -78,7 +82,22 @@ pub fn stars(map: &Beatmap, mods: impl Mods, passed_objects: Option<usize>) -> S
|
||||
})
|
||||
.collect();
|
||||
|
||||
// TODO: Calculate stack offsets here
|
||||
if map.version >= 6 {
|
||||
stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||
} else {
|
||||
old_stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||
}
|
||||
|
||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||
let stack_offset = h.stack_height * scale * -6.4;
|
||||
|
||||
h.pos += Pos2 {
|
||||
x: stack_offset,
|
||||
y: stack_offset,
|
||||
};
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let mut aim = Skill::new(SkillKind::Aim);
|
||||
let mut speed = Skill::new(SkillKind::Speed);
|
||||
@@ -176,7 +195,8 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
|
||||
let section_len = SECTION_LEN * map_attributes.clock_rate;
|
||||
let radius = OBJECT_RADIUS * (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let scale = (1.0 - 0.7 * (map_attributes.cs - 5.0) / 5.0) / 2.0;
|
||||
let radius = OBJECT_RADIUS * scale;
|
||||
let mut scaling_factor = NORMALIZED_RADIUS / radius;
|
||||
|
||||
if radius < 30.0 {
|
||||
@@ -187,15 +207,36 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
let mut slider_state = SliderState::new(map);
|
||||
let mut ticks_buf = Vec::new();
|
||||
|
||||
let mut hit_objects = map.hit_objects.iter().filter_map(|h| {
|
||||
OsuObject::new(
|
||||
h,
|
||||
map,
|
||||
radius,
|
||||
&mut ticks_buf,
|
||||
&mut diff_attributes,
|
||||
&mut slider_state,
|
||||
)
|
||||
let mut hit_objects: Vec<_> = map
|
||||
.hit_objects
|
||||
.iter()
|
||||
.filter_map(|h| {
|
||||
OsuObject::new(
|
||||
h,
|
||||
map,
|
||||
radius,
|
||||
&mut ticks_buf,
|
||||
&mut diff_attributes,
|
||||
&mut slider_state,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if map.version >= 6 {
|
||||
stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||
} else {
|
||||
old_stacking(&mut hit_objects, map.stack_leniency, TIME_PREEMPT);
|
||||
}
|
||||
|
||||
let mut hit_objects = hit_objects.into_iter().map(|mut h| {
|
||||
let stack_offset = h.stack_height * scale * -6.4;
|
||||
|
||||
h.pos += Pos2 {
|
||||
x: stack_offset,
|
||||
y: stack_offset,
|
||||
};
|
||||
|
||||
h
|
||||
});
|
||||
|
||||
let mut aim = Skill::new(SkillKind::Aim);
|
||||
@@ -275,6 +316,99 @@ pub fn strains(map: &Beatmap, mods: impl Mods) -> Strains {
|
||||
}
|
||||
}
|
||||
|
||||
fn stacking(hit_objects: &mut [OsuObject], stack_leniency: f32, time_preempt: f32) {
|
||||
let mut extended_start_idx = 0;
|
||||
let extended_end_idx = hit_objects.len() - 1;
|
||||
|
||||
let stack_threshold = time_preempt * stack_leniency; // TODO: Calculate outside?
|
||||
|
||||
for mut i in (1..=extended_end_idx).rev() {
|
||||
let mut n = i;
|
||||
|
||||
if hit_objects[i].stack_height != 0.0 || !hit_objects[i].is_slider() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if hit_objects[i].is_circle() {
|
||||
loop {
|
||||
n = match n.checked_sub(1) {
|
||||
Some(n) => n,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if hit_objects[n].is_spinner() {
|
||||
break;
|
||||
} else if n < extended_start_idx {
|
||||
hit_objects[n].stack_height = 0.0;
|
||||
extended_start_idx = n;
|
||||
}
|
||||
|
||||
if hit_objects[n].is_slider()
|
||||
&& hit_objects[n].end_pos().distance(&hit_objects[i].pos) < STACK_DISTANCE
|
||||
{
|
||||
let offset = hit_objects[i].stack_height - hit_objects[n].stack_height + 1.0;
|
||||
|
||||
for j in n + 1..=i {
|
||||
if hit_objects[n].pos.distance(&hit_objects[j].pos) < STACK_DISTANCE {
|
||||
hit_objects[j].stack_height -= offset;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
} else if hit_objects[n].pos.distance(&hit_objects[i].pos) < STACK_DISTANCE {
|
||||
hit_objects[n].stack_height = hit_objects[i].stack_height + 1.0;
|
||||
i = n;
|
||||
}
|
||||
}
|
||||
} else if hit_objects[i].is_slider() {
|
||||
loop {
|
||||
n = match n.checked_sub(1) {
|
||||
Some(n) => n,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if hit_objects[n].is_spinner() {
|
||||
continue;
|
||||
} else if hit_objects[i].time - hit_objects[n].time > stack_threshold {
|
||||
break;
|
||||
} else if hit_objects[n].end_pos().distance(&hit_objects[i].pos) < STACK_DISTANCE {
|
||||
hit_objects[n].stack_height = hit_objects[i].stack_height + 1.0;
|
||||
i = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn old_stacking(hit_objects: &mut [OsuObject], stack_leniency: f32, time_preempt: f32) {
|
||||
let stack_threshold = time_preempt * stack_leniency; // TODO: Calculate outside?
|
||||
|
||||
for i in 0..hit_objects.len() {
|
||||
if hit_objects[i].stack_height != 0.0 && !hit_objects[i].is_slider() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut start_time = hit_objects[i].end_time();
|
||||
let end_pos = hit_objects[i].end_pos(); // FIXME: Wrong for sliders? Position at 100% progress.
|
||||
let mut slider_stack = 0.0;
|
||||
|
||||
for j in i + 1..hit_objects.len() {
|
||||
if hit_objects[j].time - stack_threshold > start_time {
|
||||
break;
|
||||
}
|
||||
|
||||
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 {
|
||||
slider_stack += 1.0;
|
||||
hit_objects[j].stack_height -= slider_stack;
|
||||
start_time = hit_objects[j].end_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::super::OsuPP;
|
||||
|
||||
@@ -8,10 +8,20 @@ const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
pub(crate) struct OsuObject {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) pos: Pos2,
|
||||
pub(crate) end_pos: Pos2,
|
||||
// circle: Some(0.0) | slider: Some(_) | spinner: None
|
||||
pub(crate) travel_dist: Option<f32>,
|
||||
pub(crate) stack_height: f32,
|
||||
kind: OsuObjectKind,
|
||||
}
|
||||
|
||||
enum OsuObjectKind {
|
||||
Circle,
|
||||
Slider {
|
||||
end_time: f32,
|
||||
end_pos: Pos2,
|
||||
travel_dist: f32,
|
||||
},
|
||||
Spinner {
|
||||
end_time: f32,
|
||||
},
|
||||
}
|
||||
|
||||
impl OsuObject {
|
||||
@@ -33,9 +43,8 @@ impl OsuObject {
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
end_pos: h.pos,
|
||||
travel_dist: Some(0.0),
|
||||
stack_height,
|
||||
kind: OsuObjectKind::Circle,
|
||||
}
|
||||
}
|
||||
HitObjectKind::Slider {
|
||||
@@ -157,20 +166,24 @@ impl OsuObject {
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
end_pos,
|
||||
travel_dist: Some(travel_dist),
|
||||
stack_height,
|
||||
kind: OsuObjectKind::Slider {
|
||||
end_time: final_span_end_time,
|
||||
end_pos,
|
||||
travel_dist,
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Spinner { .. } => {
|
||||
HitObjectKind::Spinner { end_time } => {
|
||||
attributes.n_spinners += 1;
|
||||
|
||||
Self {
|
||||
time: h.start_time,
|
||||
pos: h.pos,
|
||||
end_pos: h.pos,
|
||||
travel_dist: None,
|
||||
stack_height,
|
||||
kind: OsuObjectKind::Spinner {
|
||||
end_time: *end_time,
|
||||
},
|
||||
}
|
||||
}
|
||||
HitObjectKind::Hold { .. } => return None,
|
||||
@@ -179,8 +192,43 @@ impl OsuObject {
|
||||
Some(obj)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn travel_dist(&self) -> f32 {
|
||||
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 {
|
||||
OsuObjectKind::Circle => self.time,
|
||||
OsuObjectKind::Slider { end_time, .. } => *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,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_circle(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Circle)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_slider(&self) -> bool {
|
||||
matches!(self.kind, OsuObjectKind::Slider { .. })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_spinner(&self) -> bool {
|
||||
self.travel_dist.is_none()
|
||||
matches!(self.kind, OsuObjectKind::Spinner { .. })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user