use crate::model::Note;
use crate::model::RoxChart;
pub fn nps(chart: &RoxChart) -> f64 {
let duration_s = chart.duration_us() as f64 / 1_000_000.0;
if duration_s <= 0.0 {
return 0.0;
}
chart.note_count() as f64 / duration_s
}
pub fn density(chart: &RoxChart, segments: usize) -> Vec<f64> {
if segments == 0 {
return Vec::new();
}
let duration_us = chart.duration_us();
if duration_us == 0 {
return vec![0.0; segments];
}
let segment_duration_us = duration_us as f64 / segments as f64;
let mut segment_counts = vec![0; segments];
for note in &chart.notes {
let idx = ((note.time_us as f64 / segment_duration_us).floor() as usize).min(segments - 1);
segment_counts[idx] += 1;
}
let segment_duration_s = segment_duration_us / 1_000_000.0;
segment_counts
.into_iter()
.map(|count| {
if segment_duration_s > 0.0 {
count as f64 / segment_duration_s
} else {
0.0
}
})
.collect()
}
pub fn highest_nps(chart: &RoxChart, window_size_s: f64) -> f64 {
let window_us = (window_size_s * 1_000_000.0) as i64;
if window_us <= 0 || chart.notes.is_empty() {
return 0.0;
}
let mut notes: Vec<&Note> = chart.notes.iter().collect();
notes.sort_by_key(|n| n.time_us);
let mut max_notes_in_window = 0;
let mut left = 0;
for right in 0..notes.len() {
let window_end = notes[right].time_us;
let window_start = window_end - window_us;
while left < right && notes[left].time_us <= window_start {
left += 1;
}
let count = right - left + 1;
if count > max_notes_in_window {
max_notes_in_window = count;
}
}
max_notes_in_window as f64 / window_size_s
}
pub fn lowest_nps(chart: &RoxChart, window_size_s: f64) -> f64 {
let window_us = (window_size_s * 1_000_000.0) as i64;
if window_us <= 0 || chart.notes.is_empty() {
return 0.0;
}
let mut notes: Vec<&Note> = chart.notes.iter().collect();
notes.sort_by_key(|n| n.time_us);
let first_note_time = notes[0].time_us;
for window in notes.windows(2) {
let gap = window[1].time_us - window[0].time_us;
if gap > window_us {
return 0.0;
}
}
let mut min_notes_in_window = usize::MAX;
let mut left = 0;
for right in 0..notes.len() {
let window_end = notes[right].time_us;
let window_start = window_end - window_us;
if window_start < first_note_time {
continue;
}
while left < right && notes[left].time_us <= window_start {
left += 1;
}
let count = right - left + 1;
if count < min_notes_in_window {
min_notes_in_window = count;
}
}
if min_notes_in_window == usize::MAX {
return 0.0;
}
min_notes_in_window as f64 / window_size_s
}
pub fn highest_drain_time(chart: &RoxChart) -> f64 {
let peak_nps = highest_nps(chart, 1.0);
if peak_nps <= 0.0 {
return 0.0;
}
let threshold = peak_nps * 0.9;
let duration_us = chart.duration_us();
if duration_us <= 0 {
return 0.0;
}
let scan_step_us = 100_000; let window_size_us = 1_000_000;
let mut max_contiguous_ticks = 0;
let mut current_contiguous_ticks = 0;
let mut notes: Vec<_> = chart.notes.iter().map(|n| n.time_us).collect();
notes.sort_unstable();
if notes.is_empty() {
return 0.0;
}
let mut left = 0;
let mut right = 0;
let mut t = 0;
while t <= duration_us {
let window_end = t;
let window_start = t - window_size_us;
while right < notes.len() && notes[right] <= window_end {
right += 1;
}
while left < right && notes[left] < window_start.max(0) {
left += 1;
}
let count = right - left;
let current_nps = count as f64;
if current_nps >= threshold {
current_contiguous_ticks += 1;
} else {
if current_contiguous_ticks > max_contiguous_ticks {
max_contiguous_ticks = current_contiguous_ticks;
}
current_contiguous_ticks = 0;
}
t += scan_step_us;
}
if current_contiguous_ticks > max_contiguous_ticks {
max_contiguous_ticks = current_contiguous_ticks;
}
(max_contiguous_ticks as f64 * scan_step_us as f64) / 1_000_000.0
}