diff --git a/Cargo.lock b/Cargo.lock index 88ba152..2761654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,8 +44,8 @@ dependencies = [ "hashbrown", "paste", "static_assertions", - "windows", - "windows-core", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] @@ -70,6 +70,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -84,7 +106,7 @@ dependencies = [ "jni-sys", "libc", "log", - "ndk", + "ndk 0.9.0", "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", @@ -362,6 +384,23 @@ dependencies = [ "syn", ] +[[package]] +name = "bevy_audio" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b4f6f2a5c6c0e7c6825e791d2a061c76c2d6784f114c8f24382163fabbfaaa" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_math", + "bevy_reflect", + "bevy_transform", + "rodio", + "tracing", +] + [[package]] name = "bevy_color" version = "0.16.2" @@ -624,6 +663,7 @@ dependencies = [ "bevy_a11y", "bevy_app", "bevy_asset", + "bevy_audio", "bevy_color", "bevy_core_pipeline", "bevy_derive", @@ -788,6 +828,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "bevy_pipelines_ready" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f72c9b7b1658b6a1208ee603873981d0f3fc434661c6fda284fc24814307cf6" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_platform" version = "0.16.1" @@ -1221,7 +1270,25 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", + "shlex", + "syn", +] + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", "shlex", "syn", ] @@ -1532,6 +1599,26 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen 0.72.0", +] + [[package]] name = "cosmic-text" version = "0.13.2" @@ -1542,7 +1629,7 @@ dependencies = [ "fontdb", "log", "rangemap", - "rustc-hash", + "rustc-hash 1.1.0", "rustybuzz", "self_cell", "smol_str", @@ -1555,6 +1642,29 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -1628,6 +1738,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.9.0" @@ -2027,7 +2143,7 @@ dependencies = [ "log", "presser", "thiserror 1.0.69", - "windows", + "windows 0.58.0", ] [[package]] @@ -2272,6 +2388,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "lexopt" version = "0.3.1" @@ -2387,6 +2514,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2456,7 +2592,7 @@ dependencies = [ "indexmap", "log", "pp-rs", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "strum", "termcolor", @@ -2478,12 +2614,26 @@ dependencies = [ "once_cell", "regex", "regex-syntax 0.8.5", - "rustc-hash", + "rustc-hash 1.1.0", "thiserror 1.0.69", "tracing", "unicode-ident", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.9.1", + "jni-sys", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2561,6 +2711,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2804,6 +2965,29 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "offset-allocator" version = "0.2.0" @@ -2814,6 +2998,15 @@ dependencies = [ "nonmax", ] +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2941,6 +3134,7 @@ dependencies = [ "bevy_common_assets", "bevy_easings", "bevy_mod_debugdump", + "bevy_pipelines_ready", "bevy_prototype_lyon", "bevy_simple_prefs", "itertools 0.13.0", @@ -3208,6 +3402,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rodio" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +dependencies = [ + "cpal", + "lewton", +] + [[package]] name = "ron" version = "0.8.1" @@ -3243,6 +3447,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -3665,7 +3875,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528bdd1f0e27b5dd9a4ededf154e824b0532731e4af73bb531de46276e0aab1e" dependencies = [ - "bindgen", + "bindgen 0.70.1", "cc", "cfg-if", "once_cell", @@ -3976,7 +4186,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.12", "wgpu-hal", @@ -4019,14 +4229,14 @@ dependencies = [ "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.12", "wasm-bindgen", "web-sys", "wgpu-types", - "windows", - "windows-core", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] @@ -4073,13 +4283,33 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", "windows-targets 0.52.6", ] @@ -4091,7 +4321,7 @@ checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ "windows-implement", "windows-interface", - "windows-result", + "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] @@ -4118,6 +4348,15 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -4133,7 +4372,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] @@ -4426,7 +4665,7 @@ dependencies = [ "dpi", "js-sys", "libc", - "ndk", + "ndk 0.9.0", "objc2", "objc2-app-kit", "objc2-foundation", diff --git a/Cargo.toml b/Cargo.toml index e87301a..84f919b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ debugdump = ["bevy_mod_debugdump"] [dependencies] bevy = { version = "0.16", default-features = false, features = [ "bevy_asset", + "bevy_audio", "bevy_core_pipeline", "bevy_render", "bevy_sprite", @@ -20,7 +21,9 @@ bevy = { version = "0.16", default-features = false, features = [ "bevy_ui", "bevy_winit", "bevy_window", + "default_font", "multi_threaded", + "vorbis", "webgl2", "x11", ] } @@ -42,6 +45,7 @@ log = { version = "0.4", features = [ "max_level_debug", "release_max_level_warn", ] } +bevy_pipelines_ready = "0.6.0" [dev-dependencies] approx = "0.5.1" diff --git a/assets/music/galactic_odyssey_by_alkakrab.ogg b/assets/music/galactic_odyssey_by_alkakrab.ogg new file mode 100644 index 0000000..1b1e824 Binary files /dev/null and b/assets/music/galactic_odyssey_by_alkakrab.ogg differ diff --git a/src/loading.rs b/src/loading.rs index 7a71003..ab710a2 100644 --- a/src/loading.rs +++ b/src/loading.rs @@ -1,16 +1,28 @@ use crate::{save::SaveFile, GameState, Handles, MainCamera}; use bevy::{asset::LoadState, prelude::*}; +use bevy_pipelines_ready::{PipelinesReady, PipelinesReadyPlugin}; +use bevy_prototype_lyon::prelude::*; use bevy_simple_prefs::PrefsStatus; pub struct LoadingPlugin; +#[cfg(not(target_arch = "wasm32"))] +const EXPECTED_PIPELINES: usize = 10; +#[cfg(target_arch = "wasm32")] +const EXPECTED_PIPELINES: usize = 6; + pub const NUM_LEVELS: u32 = 12; impl Plugin for LoadingPlugin { fn build(&self, app: &mut App) { + app.add_plugins(PipelinesReadyPlugin); app.init_resource::(); app.add_systems(OnEnter(GameState::Loading), loading_setup); app.add_systems(Update, loading_update.run_if(in_state(GameState::Loading))); + app.add_systems( + Update, + print_pipelines.run_if(resource_changed::), + ); } } @@ -26,6 +38,13 @@ fn loading_setup( MainCamera, )); + commands.spawn(( + ShapeBuilder::with(&shapes::RegularPolygon::default()) + .fill(Color::BLACK) + .build(), + StateScoped(GameState::Loading), + )); + for i in 1..=NUM_LEVELS { handles .levels @@ -35,6 +54,20 @@ fn loading_setup( handles .fonts .push(asset_server.load("fonts/ChakraPetch-Regular-PixieWrangler.ttf")); + + commands.spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + Children::spawn(Spawn(Text::new("Loading..."))), + StateScoped(GameState::Loading), + )); + + handles.music = asset_server.load("music/galactic_odyssey_by_alkakrab.ogg"); } fn loading_update( @@ -42,7 +75,13 @@ fn loading_update( asset_server: Res, mut next_state: ResMut>, prefs: Res>, + ready: Res, + mut frames_since_pipelines_ready: Local, ) { + if ready.get() >= EXPECTED_PIPELINES { + *frames_since_pipelines_ready += 1; + } + if handles .fonts .iter() @@ -59,9 +98,26 @@ fn loading_update( return; } + if !matches!( + asset_server.get_load_state(&handles.music), + Some(LoadState::Loaded), + ) { + return; + } + + // Firefox's FPS seems to take a few frames to recover after pipelines are + // compiled, resulting in weird audio artifacts. + if *frames_since_pipelines_ready < 10 { + return; + } + if !prefs.loaded { return; } next_state.set(GameState::LevelSelect); } + +fn print_pipelines(ready: Res) { + info!("Pipelines Ready: {}/{}", ready.get(), EXPECTED_PIPELINES); +} diff --git a/src/main.rs b/src/main.rs index f2bb659..0a2b8fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,10 @@ use crate::{ net_ripping::NetRippingPlugin, pixie::{Pixie, PixieEmitter, PixieFlavor, PixiePlugin}, road_drawing::{RoadDrawingPlugin, RoadDrawingState}, - save::{BestScores, SavePlugin, Solution, Solutions}, + save::{BestScores, MusicVolume, SavePlugin, Solution, Solutions}, sim::{SimulationPlugin, SimulationSettings, SimulationState, SimulationSteps}, ui::{ + button, radio_button::{RadioButton, RadioButtonGroup, RadioButtonGroupRelation, RadioButtonSet}, UiPlugin, }, @@ -102,6 +103,7 @@ fn main() { OnEnter(GameState::Playing), (reset_game, spawn_level, spawn_game_ui).chain(), ); + app.add_systems(OnExit(GameState::Loading), spawn_music); app.configure_sets(Update, DrawingInput.run_if(in_state(GameState::Playing))); app.add_systems( @@ -141,7 +143,7 @@ fn main() { ); app.add_systems(Update, draw_cursor_system.in_set(DrawingInteraction)); - // whenever + // whenever, when playing app.add_systems( Update, ( @@ -152,6 +154,13 @@ fn main() { ) .run_if(in_state(GameState::Playing)), ); + // whenever + app.add_systems( + Update, + set_music_volume_system + .run_if(resource_changed::) + .run_if(in_state(GameState::LevelSelect)), + ); app.configure_sets(AfterUpdate, ScoreCalc.run_if(in_state(GameState::Playing))); @@ -235,6 +244,7 @@ enum GameState { struct Handles { levels: Vec>, fonts: Vec>, + music: Handle, } #[derive(Component)] struct MainCamera; @@ -326,6 +336,8 @@ enum Collider { } #[derive(Component)] struct ColliderLayer(u32); +#[derive(Component)] +struct GameMusic; const GRID_SIZE: f32 = 48.0; pub const BOTTOM_BAR_HEIGHT: f32 = 70.0; @@ -1263,6 +1275,18 @@ fn spawn_level( // Build UI } +fn spawn_music(mut commands: Commands, handles: Res, volume: Res) { + if volume.is_muted() { + return; + } + + commands.spawn(( + AudioPlayer::new(handles.music.clone()), + PlaybackSettings::LOOP.with_volume((*volume).into()), + GameMusic, + )); +} + fn spawn_game_ui( mut commands: Commands, simulation_settings: Res, @@ -1315,96 +1339,42 @@ fn spawn_game_ui( }) .with_children(|parent| { // Back button - parent - .spawn(( - Button, - Node { - width: Val::Px(50.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - // extra padding to separate the back button from - // the tools - margin: UiRect { - right: Val::Px(10.0), - ..default() - }, - ..default() - }, - BackgroundColor(theme::UI_NORMAL_BUTTON.into()), + parent.spawn(( + Node { + // extra padding to separate the back button from + // the tools + margin: UiRect::right(Val::Px(10.0)), + ..default() + }, + Children::spawn(Spawn(( + button("←", handles.fonts[0].clone(), 50.0), BackButton, - )) - .with_children(|parent| { - parent.spawn(( - Text::new("←"), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_BUTTON_TEXT.into()), - )); - }); + ))), + )); // Tool Buttons for layer in 1..=level.layers { let id = parent .spawn(( - Button, - Node { - width: Val::Px(50.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(theme::UI_NORMAL_BUTTON.into()), + button(format!("{layer}"), handles.fonts[0].clone(), 50.0), LayerButton(layer), ToolButton, RadioButton { selected: layer == 1, }, )) - .with_children(|parent| { - parent.spawn(( - Text::new(format!("{layer}")), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_BUTTON_TEXT.into()), - )); - }) .id(); - tool_button_ids.push(id); } let net_ripping_id = parent .spawn(( - Button, - Node { - width: Val::Px(50.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(theme::UI_NORMAL_BUTTON.into()), + button("R", handles.fonts[0].clone(), 50.0), NetRippingButton, ToolButton, RadioButton { selected: false }, )) - .with_children(|parent| { - parent.spawn(( - Text::new("R"), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_BUTTON_TEXT.into()), - )); - }) .id(); tool_button_ids.push(net_ripping_id); @@ -1536,52 +1506,20 @@ fn spawn_game_ui( TextColor(theme::UI_BUTTON_TEXT.into()), )); }); - parent - .spawn(( - Button, - Node { - width: Val::Px(50.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(theme::UI_NORMAL_BUTTON.into()), - SpeedButton, - )) - .with_children(|parent| { - parent.spawn(( - Text::new(simulation_settings.speed.label()), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_BUTTON_TEXT.into()), - )); - }); - parent - .spawn(( - Button, - Node { - width: Val::Px(250.), - justify_content: JustifyContent::Center, - align_items: AlignItems::Center, - ..default() - }, - BackgroundColor(theme::UI_NORMAL_BUTTON.into()), - PixieButton, - )) - .with_children(|parent| { - parent.spawn(( - Text::new("RELEASE THE PIXIES"), - TextFont { - font: handles.fonts[0].clone(), - font_size: 25.0, - ..default() - }, - TextColor(theme::UI_BUTTON_TEXT.into()), - )); - }); + + parent.spawn(( + button( + simulation_settings.speed.label(), + handles.fonts[0].clone(), + 50.0, + ), + SpeedButton, + )); + + parent.spawn(( + button("RELEASE THE PIXIES", handles.fonts[0].clone(), 250.0), + PixieButton, + )); }); }); @@ -1611,3 +1549,31 @@ fn spawn_game_ui( .insert(RadioButtonGroupRelation(tool_group_id)); } } + +fn set_music_volume_system( + volume: Res, + sinks: Query<(&mut AudioSink, Entity), With>, + handles: Res, + mut commands: Commands, +) { + match (volume.is_muted(), sinks.is_empty()) { + (false, true) => { + commands.spawn(( + AudioPlayer::new(handles.music.clone()), + PlaybackSettings::LOOP.with_volume((*volume).into()), + GameMusic, + )); + } + (true, false) => { + for (_, entity) in sinks { + commands.entity(entity).despawn(); + } + } + (false, false) => { + for (mut sink, _) in sinks { + sink.set_volume((*volume).into()); + } + } + (true, true) => {} + } +} diff --git a/src/save.rs b/src/save.rs index 555f73a..6007d15 100644 --- a/src/save.rs +++ b/src/save.rs @@ -1,17 +1,42 @@ use crate::RoadSegment; -use bevy::{platform::collections::HashMap, prelude::*}; +use bevy::{audio::Volume, platform::collections::HashMap, prelude::*}; use bevy_simple_prefs::{Prefs, PrefsPlugin}; #[derive(Prefs, Reflect, Default)] pub struct SaveFile { scores: BestScores, solutions: Solutions, + music_volume: MusicVolume, } #[derive(Resource, Clone, Debug, Default, Reflect)] pub struct BestScores(pub HashMap); #[derive(Resource, Clone, Debug, Default, Reflect)] pub struct Solutions(pub HashMap); + +#[derive(Resource, Reflect, Clone, Copy, Eq, PartialEq, Debug)] +pub struct MusicVolume(pub u8); +impl Default for MusicVolume { + fn default() -> Self { + Self(50) + } +} +impl From for Volume { + fn from(val: MusicVolume) -> Self { + if val.0 == 0 { + Volume::Linear(0.0) + } else { + let db = -30.0 * (1.0 - val.0 as f32 / 100.0); + Volume::Decibels(db) + } + } +} + +impl MusicVolume { + pub fn is_muted(&self) -> bool { + self.0 == 0 + } +} #[derive(Clone, Debug, Default, Reflect)] pub struct Solution { pub segments: Vec, diff --git a/src/ui/level_select.rs b/src/ui/level_select.rs index 0300f50..a9458c0 100644 --- a/src/ui/level_select.rs +++ b/src/ui/level_select.rs @@ -1,6 +1,10 @@ use crate::{ - level::Level, loading::NUM_LEVELS, save::BestScores, theme, GameState, Handles, - BOTTOM_BAR_HEIGHT, + level::Level, + loading::NUM_LEVELS, + save::{BestScores, MusicVolume}, + theme, + ui::button, + GameState, Handles, BOTTOM_BAR_HEIGHT, }; use bevy::prelude::*; @@ -14,6 +18,12 @@ pub struct LevelSelectButton(u32); struct SettingsPanelBody; #[derive(Component)] struct LevelsPanelBody; +#[derive(Component)] +struct MusicVolumeDown; +#[derive(Component)] +struct MusicVolumeUp; +#[derive(Component)] +struct MusicVolumeLabel; impl Plugin for LevelSelectPlugin { fn build(&self, app: &mut App) { @@ -21,7 +31,15 @@ impl Plugin for LevelSelectPlugin { app.add_systems( Update, - level_select_button_system.run_if(in_state(GameState::LevelSelect)), + ( + level_select_button_system, + ( + music_volume_button_system, + music_volume_text_system.run_if(resource_changed::), + ) + .chain(), + ) + .run_if(in_state(GameState::LevelSelect)), ); app.add_systems(OnExit(GameState::LevelSelect), level_select_exit); @@ -148,7 +166,11 @@ fn level_select_enter(mut commands: Commands, best_scores: Res, hand .spawn(panel( "\u{01a9} SETTINGS", &handles, - Node::default(), + Node { + row_gap: Val::Px(10.), + flex_direction: FlexDirection::Column, + ..default() + }, SettingsPanelBody, )) .id(); @@ -328,10 +350,45 @@ fn populate_settings_panel_body( trigger: Trigger, mut commands: Commands, handles: Res, + music_volume: Res, ) { commands.entity(trigger.target()).with_child(( - Text::new("There aren't any settings yet! Soon!"), - TextFont::from_font(handles.fonts[0].clone()), + Text::new("Music"), + TextFont { + font: handles.fonts[0].clone(), + font_size: 25.0, + ..default() + }, + )); + + commands.entity(trigger.target()).with_child(( + Node { + flex_direction: FlexDirection::Row, + align_items: AlignItems::Stretch, + height: Val::Px(50.0), + ..default() + }, + Children::spawn(( + Spawn((MusicVolumeDown, button("<", handles.fonts[0].clone(), 50.0))), + Spawn(( + Node { + flex_grow: 1.0, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + Children::spawn(Spawn(( + MusicVolumeLabel, + Text::new(format!("{}%", music_volume.0)), + TextFont { + font: handles.fonts[0].clone(), + font_size: 25.0, + ..default() + }, + ))), + )), + Spawn((MusicVolumeUp, button(">", handles.fonts[0].clone(), 50.0))), + )), )); } @@ -360,3 +417,29 @@ fn populate_levels_panel_body( )); } } + +fn music_volume_button_system( + up_buttons: Query<&Interaction, (Changed, With)>, + down_buttons: Query<&Interaction, (Changed, With)>, + mut volume: ResMut, +) { + let current = volume.bypass_change_detection().0; + + for _ in up_buttons.iter().filter(|i| **i == Interaction::Pressed) { + let new = (current + 10).min(100); + volume.set_if_neq(MusicVolume(new)); + } + for _ in down_buttons.iter().filter(|i| **i == Interaction::Pressed) { + let new = current.saturating_sub(10); + volume.set_if_neq(MusicVolume(new)); + } +} + +fn music_volume_text_system( + volume: Res, + texts: Query<&mut Text, With>, +) { + for mut text in texts { + text.0 = format!("{}%", volume.0); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 355ef11..0b065e9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -31,3 +31,23 @@ fn button_system( } } } + +pub fn button(text_value: impl Into, font_handle: Handle, width: f32) -> impl Bundle { + ( + Button, + Node { + width: Val::Px(width), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + Children::spawn(Spawn(( + Text::new(text_value), + TextFont { + font_size: 25.0, + font: font_handle.clone(), + ..default() + }, + ))), + ) +}