From 6e1ab7d860e8fab3e4c38e0c116342ff11603117 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Sun, 1 Feb 2026 22:40:20 +0300 Subject: [PATCH] feat: Added BeatMap and input scanning --- Cargo.toml | 2 +- assets/sfx/hit.wav | Bin 0 -> 3524 bytes src/main.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 assets/sfx/hit.wav 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 0000000000000000000000000000000000000000..f5e27d044dac31ba48a3281b995b9c3d5afe46fe GIT binary patch literal 3524 zcmZ9Od013cAIE3k=gu%23^R;MiijcN3x@la_-46p82aW?D&Uq0h31AUYG{&$3*>^P zX-3&1F1+rT8ELPX8YwWtFf$By_L+0HbKYbBz3=@z_qq4n=iGCi-}!CdsjubcURLvX zvqsGvyJ*?!?phv?$H%;SmdD$X$L9%nTHf39SI@7)b9lURVY!f&($YbyL8^3Zy0%f@ zsP~vWrV4X~`LyM1FH7)`^J|6Ql9b_zYu)QL2;bDu@cA zUQw@Tk~B%~2<`|PAck(Bo1#t8r@^Pe;lAO%_RjXsW>>Q-!QZ%=wo4mth&Q-RZqu9QH_Zzz3oXzJt-Wo%ZOs^KH)}VS&1D;d>7Ud; z={w7JmOspYm}}x|;tfWFag=V9E>n}KA(f=EMph%UiEZM2{C#{ml*66$PC6l$5W5z- z7TV+ASGAGc=P zv+d1}W=Es5(Rtr>-_@_PUuQ#CLszN4)Sntk4cVf$XeyOT4P}S2W@JXa1ib`R;wmvE zqh!NW!&Gl--qeiLjnw%JKEs^&Iq_@FYt2(FQ!S6JkF64$#MXvc(rrn%{g~yLpe<-i z#i){0NoOo)EYJ+ingmUP-{?2)(eKe~gD`4LB4#1RfvIh+ZT-0IYuL5ybJzlh^M~<$LQ;5Bazj!s-ywfRm8H5v+$OT(GUH10 z@9B3Lw;SJ!UlHHa+}&K0crdZW(roFnx~!Lyen^^Rd(C#rcHGuzyKXy&-|}rUZU0I7 zCCQYeO-ix$u*xh#%VqPA=HUs06ONjyO)Z9I1JW@%ejL<#2{&tMGw?DP-vhTGovKQIkwinp9+jraV+JCiY zIIw@*4S8 z zal2u&VVZuD-k{U!UhFkbGglK;htwaaidD1%C`#n31$chb&HAkLB_J%9NQ$o{2-Ga$MpFimT)A!u>m~1B7 zyWCx(E?HNWH^=+p%@D z(dle8n`twA@r+Dl)xenrF&4%`i-ojWi85Wt;k&(oDTfdJ|!~ zYrJJFHm=0gQe*tWaLmxp(8q9A|AoH0-m2fF+otozx#Du;M#WWX_h|ppJkfO5ST%Eq z*+iv!ullCynks;6Yoan@~$@kqN?~(_}&&ic!F*${tNoJ9Q$R1=0sU?l1oFqsosrvh= zBjd^LWG`|6`3m_uIhiaVmy^55J><9K59D8D8>#li`9}G2eQWUA-}^4&xS)MG{$c)2 z{%!uB{rCOmKoYL5%L11JR|C4BDOea>9=sL&DVQ8e32h2(4cSA^P;U6O@R{(rur6YZ ztck3P*dxx!#ORdh#pso2?^vJMC$Z0BLLdgKz-sUmG=uTf8`MSWGS!z(rw`$tDP+XV zYNnWRFzw8Ab_RQiz09U@eYq;`0Qcf5goW@IOb*IH$1wt|8H#v?ygQhF{B-^fjF;cZ ze--CtBk55)nZk>>U)(?4OQdRW7ON# zW$Nqd1{~30bzdTd$Rnl^n}`pIlf)6?7IBqmC7KC9gb1ZZtTAXb8o5TKK?FsF2_LT7 z4&oW{gm_3a5kC<(i3XycI8U4-P7}w88sY#^iM7l7#Jj{)Vgm7EZ9c)Oe^=jEpHv@J zuU4;6k5ms)E7W531Jym%r>cFb*{bO(i^`<>Q~5|)rTj!WLpfQgRjQPC6}J_e6zdc( zY$D~I^5gO&@;v!;xl%5bUzS~vEteI_y2-4vU#0h?8>Q={8Pas=b4jzLLb6*jOfp2` z6??>o#0SLV#s3tuA{uA+X;HptHqHaN=#sErxKvmu>?X7c9|-OXHVeuG0|nWF7k)T` z$>Y!B6MQxQ7Vjo+1LhS>7=_U}%u=)r^}%?c2cE_h!KE+*_J=_(#9iR(xsBW=E|(j} zX}LJ=Dcj7RWKXdx*;VXtb|g!%8ul5}!qhR}GG)vbW+s!z^kvc+J|kit(NE}a=VH`FM|aKmk~WcYPnM1)IP&unX)3pMrzn815@yfp0)PXaKjsUGN)t1fBy2=mG(3 z0RnPLO~q3dY*iYSMGd0HP~)g+)GTTNwTN0pl~85WHfkUBDRqoGNu8&Dq;61msE5>J zsvT>ZC`D6JT1h9+RyvLDM-QP#&~MO_>3`7+=#}()^d@>MT}gjN*V3oxALxtp9r_+V z^}py&+DCIVk0BTx)1B$b3}CXEG0Zq-2KLV)rjU7`S<93&yP1PbHFJhJ$6RKvGQThn zm}g8I<70vhj}@|7*1+~)d$I%AY<4s|mYs@g!2jxfbYXntX|6CPPiLZ!u_xs*1!|+3-~pxgFnK0coklQci>%k zAN~fPz^7O-wm}z;eLoDq7z7YP9+Ds_QX>KxkP%ss74<;LC>7^OKa_#8&_FZ<4Mn5S zXq1b_p*PS3G!;!lvrrzIgXW_7XaQP`mY^b>S?{7%s2G)?HJJ5i1KNzXpzUY}+J$zb zJ!mhgM4zC~P!&3ab@b<$W4NB4#GFQ7qO<5LREPN%ok!oJAJ7FSir literal 0 HcmV?d00001 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)