use std::{borrow::Cow, cmp};
use rosu_map::{
section::{
general::GameMode, hit_objects::hit_samples::HitSoundType, timing_points::ControlPoint,
},
util::Pos,
};
use crate::{
Difficulty,
model::{
beatmap::Beatmap,
control_point::{DifficultyPoint, EffectPoint, TimingPoint},
hit_object::{HitObject, HitObjectKind, HoldNote, Slider, Spinner},
mode::ConvertError,
},
util::{
float_ext::FloatExt, get_precision_adjusted_beat_len,
random::csharp::Random as CsharpRandom, sort::TandemSorter,
},
};
const VELOCITY_MULTIPLIER: f32 = 1.4;
const OSU_BASE_SCORING_DIST: f32 = 100.0;
pub fn prepare_map<'map>(
difficulty: &Difficulty,
map: &'map Beatmap,
) -> Result<Cow<'map, Beatmap>, ConvertError> {
let mut map = map.convert_ref(GameMode::Taiko, difficulty.get_mods())?;
if let Some(seed) = difficulty.get_mods().random_seed() {
apply_random_to_beatmap(map.to_mut(), seed);
}
Ok(map)
}
pub fn convert(map: &mut Beatmap) {
let mut new_objects = Vec::new();
let mut new_sounds = Vec::new();
let mut idx = 0;
let mut last_scroll_speed = 1.0;
while idx < map.hit_objects.len() {
match map.hit_objects[idx].kind {
HitObjectKind::Circle | HitObjectKind::Spinner(_) => {}
HitObjectKind::Slider(ref slider) => {
let obj = &map.hit_objects[idx];
let slider_velocity = map
.difficulty_point_at(obj.start_time)
.map_or(DifficultyPoint::DEFAULT_SLIDER_VELOCITY, |point| {
point.slider_velocity
});
if !FloatExt::almost_eq(last_scroll_speed, slider_velocity, f64::EPSILON) {
let curr_kiai = map
.effect_point_at(obj.start_time)
.map_or(EffectPoint::DEFAULT_KIAI, |point| point.kiai);
let effect_point = EffectPoint {
scroll_speed: slider_velocity,
..EffectPoint::new(obj.start_time, curr_kiai)
};
last_scroll_speed = slider_velocity;
effect_point.add(&mut map.effect_points);
}
let mut params = SliderParams::new(obj.start_time, slider, slider_velocity);
if should_convert_slider_to_taiko_hits(map, &mut params) {
let mut i = 0;
let mut j = obj.start_time;
let edge_sound_count = cmp::max(slider.node_sounds.len(), 1);
while j
<= obj.start_time + f64::from(params.duration) + params.tick_spacing / 8.0
{
let h = HitObject {
pos: Pos::default(),
start_time: j,
kind: HitObjectKind::Circle,
};
let sound = slider
.node_sounds
.get(i)
.copied()
.unwrap_or(map.hit_sounds[idx]);
new_objects.push(h);
new_sounds.push(sound);
if params.tick_spacing.eq(0.0) {
break;
}
j += params.tick_spacing;
i = (i + 1) % edge_sound_count;
}
if let Some(len) = new_objects.len().checked_sub(1) {
map.hit_objects.splice(idx..=idx, new_objects.drain(..));
map.hit_sounds.splice(idx..=idx, new_sounds.drain(..));
idx += len;
} else {
map.hit_objects.remove(idx);
map.hit_sounds.remove(idx);
idx -= 1;
}
}
}
HitObjectKind::Hold(HoldNote { duration }) => {
map.hit_objects[idx].kind = HitObjectKind::Spinner(Spinner { duration });
}
}
idx += 1;
}
let mut sorter = TandemSorter::new_stable(&map.hit_objects, |a, b| {
a.start_time.total_cmp(&b.start_time)
});
sorter.sort(&mut map.hit_objects);
sorter.sort(&mut map.hit_sounds);
map.mode = GameMode::Taiko;
map.is_convert = true;
}
fn should_convert_slider_to_taiko_hits(map: &Beatmap, params: &mut SliderParams<'_>) -> bool {
let SliderParams {
slider,
duration,
start_time,
tick_spacing,
slider_velocity,
} = params;
let spans = slider.span_count() as f64;
let mut dist = slider.expected_dist.unwrap_or(0.0);
dist *= f64::from(VELOCITY_MULTIPLIER);
dist *= spans;
let timing_beat_len = map
.timing_point_at(*start_time)
.map_or(TimingPoint::DEFAULT_BEAT_LEN, |point| point.beat_len);
let mut beat_len = get_precision_adjusted_beat_len(*slider_velocity, timing_beat_len);
let slider_scoring_point_dist = f64::from(OSU_BASE_SCORING_DIST)
* (map.slider_multiplier * f64::from(VELOCITY_MULTIPLIER))
/ map.slider_tick_rate;
let taiko_vel = slider_scoring_point_dist * map.slider_tick_rate;
*duration = (dist / taiko_vel * beat_len) as u32;
let osu_vel = taiko_vel * (f64::from(1000.0_f32) / beat_len);
if map.version >= 8 {
beat_len = timing_beat_len;
}
*tick_spacing = (beat_len / map.slider_tick_rate).min(f64::from(*duration) / spans);
*tick_spacing > 0.0 && dist / osu_vel * 1000.0 < 2.0 * beat_len
}
struct SliderParams<'c> {
slider: &'c Slider,
duration: u32,
start_time: f64,
tick_spacing: f64,
slider_velocity: f64,
}
impl<'c> SliderParams<'c> {
const fn new(start_time: f64, slider: &'c Slider, slider_velocity: f64) -> Self {
Self {
slider,
start_time,
duration: 0,
tick_spacing: 0.0,
slider_velocity,
}
}
}
fn apply_random_to_beatmap(map: &mut Beatmap, seed: i32) {
let mut rng = CsharpRandom::new(seed);
for (h, s) in map.hit_objects.iter().zip(map.hit_sounds.iter_mut()) {
if !h.is_circle() {
continue;
}
if rng.next_max(2) == 0 {
*s &= !(HitSoundType::CLAP | HitSoundType::WHISTLE);
} else {
*s = HitSoundType::from(u8::from(*s) | HitSoundType::CLAP);
}
}
}