use std::{cmp, iter::Peekable, vec};
use rosu_map::section::events::BreakPeriod;
use crate::{
Beatmap,
any::hit_result::HitResult,
model::beatmap::BeatmapAttributes,
osu::{
legacy_score_simulator::{
AddScoreComboMultiplier, IncreaseCombo, IsBonus, LegacyScoreAttributes,
LegacyScoreSimulatorInner,
},
object::{NestedSliderObjectKind, OsuObject, OsuObjectKind},
},
util::ruleset_ext::calculate_difficulty_peppy_stars,
};
pub struct GradualLegacyScoreSimulator {
map_attrs: BeatmapAttributes,
map_attrs_nomod: BeatmapAttributes,
attrs: LegacyScoreAttributes,
inner: super::LegacyScoreSimulatorInner,
combo_score_factors: Vec<f64>,
breaks: Peekable<vec::IntoIter<BreakPeriod>>,
elapsed_curr_break: Option<i32>,
break_len_prelim: i32,
object_count: i32,
start_time: Option<i32>,
}
impl GradualLegacyScoreSimulator {
pub fn new(map: &Beatmap, map_attrs: BeatmapAttributes) -> Self {
Self {
map_attrs,
map_attrs_nomod: map.attributes().build(),
attrs: LegacyScoreAttributes::default(),
inner: LegacyScoreSimulatorInner::default(),
combo_score_factors: Vec::new(),
breaks: map.breaks.clone().into_iter().peekable(),
elapsed_curr_break: None,
break_len_prelim: 0,
object_count: 0,
start_time: None,
}
}
const fn break_len(&self) -> i32 {
self.break_len_prelim
+ if let Some(elapsed) = self.elapsed_curr_break {
elapsed
} else {
0
}
}
pub fn score_multiplier(&self, obj: &OsuObject, nomod: bool) -> f64 {
let start_time = self.start_time.unwrap_or(round_time(obj.start_time));
let end_time = round_time(obj.start_time);
let drain_len = (end_time - start_time - self.break_len()) / 1000;
let map_attrs = if nomod {
&self.map_attrs_nomod
} else {
&self.map_attrs
};
f64::from(calculate_difficulty_peppy_stars(
map_attrs,
self.object_count,
drain_len,
))
}
fn prepare_score_multiplier(&mut self, obj: &OsuObject) {
while let Some(b) = self.breaks.peek() {
if b.start_time >= obj.start_time {
break;
}
if b.end_time < obj.start_time {
self.break_len_prelim += round_time(b.end_time) - round_time(b.start_time);
self.elapsed_curr_break.take();
self.breaks.next();
} else {
let period_end = cmp::min(round_time(b.end_time), round_time(obj.start_time));
self.elapsed_curr_break = Some(period_end - round_time(b.start_time));
}
}
self.object_count += 1;
self.start_time.get_or_insert(round_time(obj.start_time));
}
pub fn simulate_next(&mut self, obj: &OsuObject) -> LegacyScoreAttributes {
self.prepare_score_multiplier(obj);
let score_multiplier = self.score_multiplier(obj, true);
self.simulate_hit(obj);
let combo_score = self
.combo_score_factors
.iter()
.fold(0.0, |combo_score, &factor| {
combo_score + factor * score_multiplier
});
self.attrs.combo_score = i64::from(combo_score as i32);
self.inner.finalize(&mut self.attrs);
self.attrs.clone()
}
fn simulate_hit(&mut self, hit_object: &OsuObject) {
const DEFAULT_BONUS_RESULT: HitResult = HitResult::None;
match hit_object.kind {
OsuObjectKind::Circle => {
self.unrolled_recursion(
AddScoreComboMultiplier::Yes,
IsBonus::default(),
IncreaseCombo::default(),
300,
DEFAULT_BONUS_RESULT,
);
}
OsuObjectKind::Slider(ref slider) => {
self.unrolled_recursion(
AddScoreComboMultiplier::default(),
IsBonus::default(),
IncreaseCombo::default(),
30,
DEFAULT_BONUS_RESULT,
);
for nested in slider.nested_objects.iter() {
match nested.kind {
NestedSliderObjectKind::Repeat | NestedSliderObjectKind::Tail => {
self.unrolled_recursion(
AddScoreComboMultiplier::default(),
IsBonus::default(),
IncreaseCombo::default(),
30,
DEFAULT_BONUS_RESULT,
);
}
NestedSliderObjectKind::Tick => {
self.unrolled_recursion(
AddScoreComboMultiplier::default(),
IsBonus::default(),
IncreaseCombo::default(),
10,
DEFAULT_BONUS_RESULT,
);
}
}
}
self.unrolled_recursion(
AddScoreComboMultiplier::Yes,
IsBonus::default(),
IncreaseCombo::No,
300,
DEFAULT_BONUS_RESULT,
);
}
OsuObjectKind::Spinner(ref spinner) => {
const MAXIMUM_ROTATIONS_PER_SECOND: f64 = 477.0 / 60.0;
const MINIMUM_ROTATIONS_PER_SECOND: f64 = 3.0;
let seconds_duration = spinner.duration / 1000.0;
let total_half_spins_possible =
(seconds_duration * MAXIMUM_ROTATIONS_PER_SECOND * 2.0) as i32;
let half_spins_required_for_completion =
(seconds_duration * MINIMUM_ROTATIONS_PER_SECOND) as i32;
let half_spins_required_before_bonus = half_spins_required_for_completion + 3;
for i in 0..=total_half_spins_possible {
if i > half_spins_required_before_bonus
&& (i - half_spins_required_before_bonus) % 2 == 0
{
self.unrolled_recursion(
AddScoreComboMultiplier::default(),
IsBonus::Yes,
IncreaseCombo::No,
1100,
HitResult::LargeBonus,
);
} else if i > 1 && i % 2 == 0 {
self.unrolled_recursion(
AddScoreComboMultiplier::default(),
IsBonus::Yes,
IncreaseCombo::No,
100,
HitResult::SmallBonus,
);
}
}
self.unrolled_recursion(
AddScoreComboMultiplier::Yes,
IsBonus::default(),
IncreaseCombo::default(),
300,
DEFAULT_BONUS_RESULT,
);
}
}
}
fn unrolled_recursion(
&mut self,
add_score_combo_multiplier: AddScoreComboMultiplier,
is_bonus: IsBonus,
increase_combo: IncreaseCombo,
score_increase: i32,
bonus_result: HitResult,
) {
let factor = self.inner.unrolled_recursion(
&mut self.attrs,
add_score_combo_multiplier,
is_bonus,
increase_combo,
score_increase,
bonus_result,
);
if let Some(factor) = factor {
self.combo_score_factors.push(factor);
}
}
}
const fn round_time(time: f64) -> i32 {
time.round_ties_even() as i32
}