use bevy::prelude::*;
pub const BGM_BPM: f32 = 198.0;
pub const BGM_OFFSET: f32 = 1.325;
pub const BEAT_DURATION: f32 = 60.0 / BGM_BPM;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BeatEvent {
WholeNote,
HalfNote,
QuarterNote,
EighthNote,
SixteenthNote,
ThirtySecondNote,
}
impl bevy::ecs::message::Message for BeatEvent {}
impl BeatEvent {
pub fn subdivision(&self) -> u32 {
match self {
BeatEvent::WholeNote => 1,
BeatEvent::HalfNote => 2,
BeatEvent::QuarterNote => 4,
BeatEvent::EighthNote => 8,
BeatEvent::SixteenthNote => 16,
BeatEvent::ThirtySecondNote => 32,
}
}
pub fn duration(&self) -> f32 {
(BEAT_DURATION * 4.0) / self.subdivision() as f32
}
}
#[derive(Resource)]
pub struct BeatTracker {
pub elapsed: f32,
pub active: bool,
pub counts: BeatCounts,
}
#[derive(Default, Clone)]
pub struct BeatCounts {
pub whole: u32,
pub half: u32,
pub quarter: u32,
pub eighth: u32,
pub sixteenth: u32,
pub thirty_second: u32,
}
impl Default for BeatTracker {
fn default() -> Self {
Self {
elapsed: -BGM_OFFSET, active: false,
counts: BeatCounts::default(),
}
}
}
impl BeatTracker {
pub fn current_beat(&self, subdivision: u32) -> u32 {
if self.elapsed < 0.0 {
return 0;
}
let note_duration = (BEAT_DURATION * 4.0) / subdivision as f32;
(self.elapsed / note_duration).floor() as u32
}
fn should_trigger(&self, subdivision: u32, count: u32) -> bool {
if self.elapsed < 0.0 {
return false;
}
let current = self.current_beat(subdivision);
current > count
}
}
pub struct BeatPlugin;
impl Plugin for BeatPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<BeatTracker>()
.add_message::<BeatEvent>()
.add_systems(
Update,
(
activate_beat_tracker_system,
update_beat_tracker_system,
)
.chain()
.in_set(super::super::OverworldUpdate),
);
}
}
fn activate_beat_tracker_system(
mut beat_tracker: ResMut<BeatTracker>,
current_bgm: Res<super::CurrentMapBgm>,
) {
if current_bgm.0.is_some() && !beat_tracker.active {
beat_tracker.active = true;
beat_tracker.elapsed = -BGM_OFFSET;
beat_tracker.counts = BeatCounts::default();
info!(
"Beat tracker activated: BPM={}, offset={}s, beat_duration={}s",
BGM_BPM, BGM_OFFSET, BEAT_DURATION
);
}
}
fn update_beat_tracker_system(
time: Res<Time>,
mut beat_tracker: ResMut<BeatTracker>,
mut beat_events: MessageWriter<BeatEvent>,
) {
if !beat_tracker.active {
return;
}
beat_tracker.elapsed += time.delta_secs();
if beat_tracker.should_trigger(1, beat_tracker.counts.whole) {
beat_tracker.counts.whole = beat_tracker.current_beat(1);
beat_events.write(BeatEvent::WholeNote);
}
if beat_tracker.should_trigger(2, beat_tracker.counts.half) {
beat_tracker.counts.half = beat_tracker.current_beat(2);
beat_events.write(BeatEvent::HalfNote);
}
if beat_tracker.should_trigger(4, beat_tracker.counts.quarter) {
beat_tracker.counts.quarter = beat_tracker.current_beat(4);
beat_events.write(BeatEvent::QuarterNote);
}
if beat_tracker.should_trigger(8, beat_tracker.counts.eighth) {
beat_tracker.counts.eighth = beat_tracker.current_beat(8);
beat_events.write(BeatEvent::EighthNote);
}
if beat_tracker.should_trigger(16, beat_tracker.counts.sixteenth) {
beat_tracker.counts.sixteenth = beat_tracker.current_beat(16);
beat_events.write(BeatEvent::SixteenthNote);
}
if beat_tracker.should_trigger(32, beat_tracker.counts.thirty_second) {
beat_tracker.counts.thirty_second = beat_tracker.current_beat(32);
beat_events.write(BeatEvent::ThirtySecondNote);
}
}
#[cfg(all(feature = "bevy_kira_audio", not(feature = "firewheel")))]
fn test_metronome_system(
mut beat_events: MessageReader<BeatEvent>,
beat_tracker: Res<BeatTracker>,
audio: Res<bevy_kira_audio::Audio>,
asset_server: Res<AssetServer>,
) {
for event in beat_events.read() {
if matches!(event, BeatEvent::QuarterNote) {
let beat_in_bar = (beat_tracker.counts.quarter - 1) % 4;
if beat_in_bar == 0 {
crate::core::audio::play_sound(&audio, &asset_server, "selected.wav");
} else {
crate::core::audio::play_sound(&audio, &asset_server, "choice.wav");
}
}
}
}
#[cfg(feature = "firewheel")]
fn test_metronome_system(
mut commands: Commands,
mut beat_events: MessageReader<BeatEvent>,
beat_tracker: Res<BeatTracker>,
asset_server: Res<AssetServer>,
) {
for event in beat_events.read() {
if matches!(event, BeatEvent::QuarterNote) {
let beat_in_bar = (beat_tracker.counts.quarter - 1) % 4;
if beat_in_bar == 0 {
crate::core::audio::play_sound(&mut commands, &asset_server, "selected.wav");
} else {
crate::core::audio::play_sound(&mut commands, &asset_server, "choice.wav");
}
}
}
}