feat: Added BeatMap and input scanning

This commit is contained in:
Alexey 2026-02-01 22:40:20 +03:00
commit 6e1ab7d860
3 changed files with 150 additions and 3 deletions

View file

@ -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

BIN
assets/sfx/hit.wav Normal file

Binary file not shown.

View file

@ -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<Beat>);
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<String, BeatMap>,
pub input: BeatMap,
}
#[derive(Component)]
struct Metronome;
@ -13,6 +74,13 @@ struct Metronome;
struct MetronomeData {
pub sound: Handle<AudioSource>,
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<AssetServer>) {
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<Entity, With<Metronome>>,
) {
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<AssetServer>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut metronome: ResMut<MetronomeData>,
mut bm: ResMut<BeatMapManager>,
) {
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)