diff --git a/Cargo.toml b/Cargo.toml index beba998..671c253 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ version = "0.1.0" edition = "2024" [dependencies] -bevy = { version = "0.18.0", features = ["dynamic_linking", "wav"] } +bevy = { version = "0.18.0", features = ["debug", "dynamic_linking", "wav"] } [profile.dev] opt-level = 1 diff --git a/assets/sfx/hit.wav b/assets/sfx/hit.wav new file mode 100644 index 0000000..f5e27d0 Binary files /dev/null and b/assets/sfx/hit.wav differ diff --git a/src/main.rs b/src/main.rs index 0a83a5e..5ff398f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{cmp::Ordering, collections::BTreeMap, time::Duration}; use bevy::{audio::PlaybackMode, prelude::*}; @@ -6,6 +6,67 @@ fn duration_from_bpm(bpm: f32) -> Duration { Duration::from_millis((60_000f32 / bpm) as u64) } +#[derive(Debug, PartialEq, PartialOrd)] +enum BeatDirection { + Up, + Down, + Left, + Right, +} + +#[derive(Debug, PartialEq, PartialOrd)] +struct Beat { + pub position: f32, + pub direction: BeatDirection, +} + +impl Beat { + pub fn new(position: f32, direction: BeatDirection) -> Self { + Self { position, direction } + } + + pub fn accuracy(&self, compared: &Beat) -> f32 { + if self.direction != compared.direction || + (self.position - compared.position).abs() >= 1.0 { + return 0f32; + } + + let Some(comparison) = self.position.partial_cmp(&compared.position) else { + return 0f32; + }; + + let x = match comparison { + Ordering::Equal => return 1f32, + Ordering::Less => { + 1f32 + compared.position - self.position + }, + Ordering::Greater => { + 1f32 - (self.position - compared.position) + }, + }; + (1f32 - (x - 1f32).powi(2)).sqrt() + } +} + +#[derive(PartialEq, PartialOrd, Debug)] +struct BeatMap(Vec); + +impl BeatMap { + fn beat_length(&self) -> f32 { + let Some(last_beat) = self.0.last() else { + return 0f32; + }; + + last_beat.position + } +} + +#[derive(Resource, Debug)] +struct BeatMapManager { + pub beatmaps: BTreeMap, + pub input: BeatMap, +} + #[derive(Component)] struct Metronome; @@ -13,6 +74,13 @@ struct Metronome; struct MetronomeData { pub sound: Handle, pub timer: Timer, + pub elapsed_beats: i32, +} + +impl MetronomeData { + pub fn current_beat(&self) -> f32 { + self.elapsed_beats as f32 + self.timer.fraction() + } } pub struct RhythmPlugin; @@ -20,7 +88,7 @@ pub struct RhythmPlugin; impl Plugin for RhythmPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_metronome) - .add_systems(Update, tick_metronome); + .add_systems(Update, (tick_metronome, handle_input).chain()); } } @@ -28,8 +96,31 @@ fn setup_metronome(mut commands: Commands, asset_server: Res) { commands.insert_resource( MetronomeData { sound: asset_server.load("sfx/metronome.wav"), timer: Timer::new(duration_from_bpm(120f32), TimerMode::Repeating), + elapsed_beats: 0, }); commands.spawn(Metronome); + + let beatmap = vec![ + Beat::new(0.0, BeatDirection::Down), + Beat::new(1.0, BeatDirection::Down), + Beat::new(2.0, BeatDirection::Left), + Beat::new(2.5, BeatDirection::Up), + Beat::new(3.0, BeatDirection::Right), + Beat::new(4.0, BeatDirection::Up), + Beat::new(4.5, BeatDirection::Right), + Beat::new(5.0, BeatDirection::Down), + Beat::new(5.5, BeatDirection::Left), + Beat::new(6.5, BeatDirection::Up), + Beat::new(7.0, BeatDirection::Up), + ]; + + let mut beatmaps = BTreeMap::new(); + beatmaps.insert("test".into(), BeatMap(beatmap)); + + commands.insert_resource(BeatMapManager { + beatmaps, + input: BeatMap(vec![]), + }); } fn tick_metronome( @@ -39,6 +130,7 @@ fn tick_metronome( metronome: Single>, ) { if metronome_data.timer.tick(time.delta()).just_finished() { + metronome_data.elapsed_beats += 1; commands.entity(*metronome).insert(( AudioPlayer::new(metronome_data.sound.clone()), PlaybackSettings { @@ -49,6 +141,61 @@ fn tick_metronome( } } +fn handle_input( + mut commands: Commands, + asset_server: Res, + keyboard_input: Res>, + mut metronome: ResMut, + mut bm: ResMut, +) { + if metronome.current_beat() > bm.beatmaps["test"].beat_length() + 1f32 + && !bm.input.0.is_empty() { + bm.input.0.clear(); + println!("track ended, cleared input"); + } + + let input_directions = vec![ + (KeyCode::ArrowUp, BeatDirection::Up), + (KeyCode::ArrowDown, BeatDirection::Down), + (KeyCode::ArrowLeft, BeatDirection::Left), + (KeyCode::ArrowRight, BeatDirection::Right), + ]; + + for (key, direction) in input_directions { + if keyboard_input.just_pressed(key) { + if bm.input.0.is_empty() { + metronome.elapsed_beats = 0; + if metronome.timer.fraction() > 0.5 { + metronome.elapsed_beats = -1; + } + println!("starting new input"); + } + + commands.spawn(( + AudioPlayer::new(asset_server.load("sfx/hit.wav")), + PlaybackSettings { + mode: PlaybackMode::Despawn, + ..default() + } + )); + + bm.input.0.push(Beat::new(metronome.current_beat(), direction)); + println!("pushed {:?}", bm.input.0.last().unwrap()); + + let last_index = bm.input.0.len() - 1; + + if bm.beatmaps["test"].0.len() > last_index { + let accuracy = bm.beatmaps["test"].0[last_index].accuracy(bm.input.0.last().unwrap()); + println!("accuracy: {}%", accuracy * 100f32); + } else { + println!("accuracy: 0%; too much beats!"); + } + + break; + } + } +} + fn main() { App::new() .add_plugins(DefaultPlugins)