From eb681999f839f183e83fca221c909b127f431f30 Mon Sep 17 00:00:00 2001 From: hut Date: Mon, 13 May 2024 20:21:56 +0200 Subject: [PATCH] implement game menu --- src/actor.rs | 2 +- src/audio.rs | 14 ++--- src/camera.rs | 4 +- src/common.rs | 4 ++ src/game.rs | 78 +++++++++++++++++++++++- src/hud.rs | 11 ++-- src/main.rs | 18 ++---- src/menu.rs | 163 +++++++++++++++++++++++++++++++++++++++++++++++++- src/var.rs | 17 +++++- 9 files changed, 275 insertions(+), 36 deletions(-) diff --git a/src/actor.rs b/src/actor.rs index 39ff121..df0424c 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -35,7 +35,7 @@ impl Plugin for ActorPlugin { .after(PhysicsSet::Sync) .after(sync::position_to_transform)); app.add_systems(Update, ( - handle_input.run_if(alive), + handle_input.run_if(in_control), handle_collisions, handle_damage, )); diff --git a/src/audio.rs b/src/audio.rs index b48ef2c..2635074 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -19,7 +19,7 @@ pub struct AudioPlugin; impl Plugin for AudioPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup); - app.add_systems(Update, handle_input.run_if(alive)); + app.add_systems(Update, handle_input.run_if(in_control)); app.add_systems(Update, respawn_sinks.run_if(on_event::())); app.add_systems(Update, play_zoom_sfx); app.add_systems(Update, pause_all.run_if(on_event::())); @@ -160,19 +160,19 @@ pub fn respawn_sinks( pub fn handle_input( keyboard_input: Res>, - mut evwriter_toggle: EventWriter, - mut evwriter_sfx: EventWriter, + mut ew_toggle: EventWriter, + mut ew_sfx: EventWriter, mut settings: ResMut, ) { if keyboard_input.just_pressed(settings.key_toggle_music) { settings.mute_music ^= true; - evwriter_sfx.send(PlaySfxEvent(Sfx::Click)); - evwriter_toggle.send(ToggleMusicEvent()); + ew_sfx.send(PlaySfxEvent(Sfx::Click)); + ew_toggle.send(ToggleMusicEvent()); } if keyboard_input.just_pressed(settings.key_toggle_sfx) { settings.mute_sfx ^= true; - evwriter_sfx.send(PlaySfxEvent(Sfx::Click)); - evwriter_toggle.send(ToggleMusicEvent()); + ew_sfx.send(PlaySfxEvent(Sfx::Click)); + ew_toggle.send(ToggleMusicEvent()); } } diff --git a/src/camera.rs b/src/camera.rs index fc527a2..379d2b3 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -31,7 +31,7 @@ pub struct CameraPlugin; impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_camera); - app.add_systems(Update, handle_input.run_if(alive)); + app.add_systems(Update, handle_input.run_if(in_control)); app.add_systems(Update, update_map_only_object_visibility.run_if(alive)); app.add_systems(Update, manage_player_actor.after(handle_input)); app.add_systems(PostUpdate, sync_camera_to_player @@ -391,7 +391,7 @@ pub fn apply_input_to_player( Option<&actor::PlayerDrivesThis>, ), (With, Without)>, ) { - if settings.map_active { + if settings.map_active || !settings.in_control() { return; } let dt = time.delta_seconds(); diff --git a/src/common.rs b/src/common.rs index 8795797..27d0af4 100644 --- a/src/common.rs +++ b/src/common.rs @@ -59,3 +59,7 @@ pub fn style_centered() -> Style { pub fn alive(settings: Res) -> bool { return settings.alive; } + +pub fn in_control(settings: Res) -> bool { + return settings.in_control(); +} diff --git a/src/game.rs b/src/game.rs index 4a7ba8f..e4bf3c9 100644 --- a/src/game.rs +++ b/src/game.rs @@ -14,24 +14,100 @@ use crate::prelude::*; use bevy::prelude::*; use bevy::pbr::ExtendedMaterial; use bevy::scene::SceneInstance; +use bevy::window::{Window, WindowMode, PrimaryWindow}; use bevy_xpbd_3d::prelude::*; use std::collections::HashMap; pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, handle_cheats); + app.add_systems(Update, handle_cheats.run_if(in_control)); app.add_systems(Update, debug); + app.add_systems(Update, handle_game_event); app.add_systems(PreUpdate, handle_player_death); app.add_systems(PostUpdate, update_id2pos); app.insert_resource(Id2Pos(HashMap::new())); app.add_event::(); + app.add_event::(); } } #[derive(Event)] pub struct PlayerDiesEvent(pub actor::DamageType); #[derive(Resource)] pub struct Id2Pos(pub HashMap); +#[derive(Event)] +pub enum GameEvent { + SetAR(Turn), + SetMusic(Turn), + SetSound(Turn), + SetMap(Turn), + SetFullscreen(Turn), +} + +pub enum Turn { + On, + Off, + Toggle, +} + +impl Turn { + pub fn to_bool(&self, current_state: bool) -> bool { + match self { + Turn::On => true, + Turn::Off => false, + Turn::Toggle => !current_state, + } + } +} + +fn handle_game_event( + mut settings: ResMut, + mut er_game: EventReader, + mut ew_sfx: EventWriter, + mut ew_updateoverlays: EventWriter, + mut ew_togglemusic: EventWriter, + mut q_window: Query<&mut Window, With>, + mut mapcam: ResMut, + opt: Res, +) { + for event in er_game.read() { + match event { + GameEvent::SetAR(turn) => { + settings.hud_active = turn.to_bool(settings.hud_active); + ew_togglemusic.send(audio::ToggleMusicEvent()); + ew_updateoverlays.send(hud::UpdateOverlayVisibility); + } + GameEvent::SetMusic(turn) => { + // TODO invert "mute_music" to "music_active" + settings.mute_music = turn.to_bool(settings.mute_music); + ew_togglemusic.send(audio::ToggleMusicEvent()); + } + GameEvent::SetSound(turn) => { + // TODO invert "mute_sfx" to "sfx_active" + settings.mute_sfx = turn.to_bool(settings.mute_sfx); + ew_togglemusic.send(audio::ToggleMusicEvent()); + } + GameEvent::SetMap(turn) => { + settings.map_active = turn.to_bool(settings.map_active); + if settings.map_active { + ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Woosh)); + } + *mapcam = camera::MapCam::default(); + ew_updateoverlays.send(hud::UpdateOverlayVisibility); + } + GameEvent::SetFullscreen(turn) => { + for mut window in &mut q_window { + let current_state = window.mode != WindowMode::Windowed; + window.mode = match turn.to_bool(current_state) { + true => opt.window_mode_fullscreen, + false => WindowMode::Windowed + }; + } + } + } + } +} + fn handle_player_death( mut cmd: Commands, mut er_playerdies: EventReader, diff --git a/src/hud.rs b/src/hud.rs index 341d281..5db8943 100644 --- a/src/hud.rs +++ b/src/hud.rs @@ -41,7 +41,7 @@ impl Plugin for HudPlugin { update_dashboard, update_speedometer, update_gauges, - handle_input.run_if(alive), + handle_input.run_if(in_control), handle_target_event, )); app.add_systems(PostUpdate, ( @@ -938,12 +938,11 @@ fn update_hud( fn handle_input( keyboard_input: Res>, mouse_input: Res>, - mut settings: ResMut, + settings: Res, mut log: ResMut, mut ew_sfx: EventWriter, - mut ew_togglemusic: EventWriter, mut ew_target: EventWriter, - mut ew_updateoverlays: EventWriter, + mut ew_game: EventWriter, q_objects: Query<(Entity, &Transform), (With, Without, Without, Without)>, q_camera: Query<&Transform, With>, ) { @@ -954,10 +953,8 @@ fn handle_input( } } if keyboard_input.just_pressed(settings.key_togglehud) { - settings.hud_active ^= true; + ew_game.send(GameEvent::SetAR(Turn::Toggle)); ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch)); - ew_togglemusic.send(audio::ToggleMusicEvent()); - ew_updateoverlays.send(UpdateOverlayVisibility); } if settings.hud_active && mouse_input.just_pressed(settings.key_selectobject) { if let Ok(camtrans) = q_camera.get_single() { diff --git a/src/main.rs b/src/main.rs index 9814762..30f53ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ pub mod prelude { pub use crate::common::*; pub use crate::var::Settings; pub use crate::load::load_asset; + pub use game::{GameEvent, Turn}; } use bevy::window::{Window, WindowMode, PrimaryWindow, CursorGrabMode}; @@ -54,7 +55,7 @@ Note: borderless fullscreen is the default, but it crashes on some systems."; fn main() { let prefs = var::load_prefs(); - let mut opt = CommandLineOptions { + let mut opt = var::CommandLineOptions { window_mode_fullscreen: prefs.get_fullscreen_mode(), window_mode_initial: prefs.get_window_mode(), use_gl: prefs.render_mode_is_gl(), @@ -146,16 +147,9 @@ impl Plugin for OutFlyPlugin { } } -#[derive(Resource, Default)] -pub struct CommandLineOptions { - window_mode_fullscreen: WindowMode, - window_mode_initial: WindowMode, - use_gl: bool, -} - fn setup( mut windows: Query<&mut Window, With>, - opt: Res, + opt: Res, ) { for mut window in &mut windows { window.cursor.grab_mode = CursorGrabMode::Locked; @@ -168,14 +162,10 @@ fn setup( fn handle_input( keyboard_input: Res>, settings: Res, - opt: Res, - mut app_exit_events: ResMut>, + opt: Res, mut windows: Query<&mut Window, With>, mut ew_sfx: EventWriter, ) { - if keyboard_input.pressed(settings.key_exit) { - app_exit_events.send(bevy::app::AppExit); - } if keyboard_input.just_pressed(settings.key_fullscreen) { ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); for mut window in &mut windows { diff --git a/src/menu.rs b/src/menu.rs index 18dd4ab..da29ec8 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -22,22 +22,44 @@ impl Plugin for MenuPlugin { app.add_systems(Startup, setup.after(hud::setup)); app.add_systems(PreUpdate, show_deathscreen.run_if(on_event::())); app.add_systems(Update, handle_deathscreen_input); + app.add_systems(PostUpdate, update_menu); + app.add_systems(Update, handle_input.run_if(alive)); + app.insert_resource(MenuState::default()); app.add_event::(); } } +#[derive(Component)] pub struct MenuElement; +#[derive(Component)] pub struct MenuTopLevel; #[derive(Component)] pub struct DeathScreenElement; -#[derive(Component)] pub struct BlackScreen; #[derive(Component)] pub struct DeathText; #[derive(Event, PartialEq)] pub enum DeathScreenEvent { Show, Hide } +pub const MENUDEF: &[(&str, MenuAction)] = &[ + ("Open Map", MenuAction::ToggleMap), + ("Toggle Augmented Reality", MenuAction::ToggleAR), + ("Toggle Sound", MenuAction::ToggleSound), + ("Toggle Music", MenuAction::ToggleMusic), + ("Toggle Fullscreen", MenuAction::ToggleFullscreen), + ("Quit", MenuAction::Quit), +]; + +#[derive(Component)] +pub enum MenuAction { + ToggleMap, + ToggleAR, + ToggleSound, + ToggleMusic, + ToggleFullscreen, + Quit, +} + pub fn setup( mut commands: Commands, asset_server: Res, settings: Res, ) { commands.spawn(( - BlackScreen, DeathScreenElement, NodeBundle { style: style_fullscreen(), @@ -98,6 +120,54 @@ pub fn setup( }, )); }); + + let style_menu = TextStyle { + font: font_handle.clone(), + font_size: settings.font_size_hud, + color: settings.hud_color, + ..default() + }; + + let sections: Vec = Vec::from_iter(MENUDEF.iter().map(|(label, _)| + TextSection::new(label.to_string() + "\n", style_menu.clone()) + )); + + commands.spawn(( + MenuElement, + NodeBundle { + style: style_fullscreen(), + background_color: Color::rgba(0.0, 0.0, 0.0, 0.8).into(), + visibility: Visibility::Hidden, + ..default() + }, + )); + + commands.spawn(( + MenuElement, + NodeBundle { + style: Style { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceAround, + ..default() + }, + visibility: Visibility::Hidden, + ..default() + }, + )).with_children(|builder| { + builder.spawn(( + MenuTopLevel, + TextBundle { + text: Text { + sections, + justify: JustifyText::Center, + ..default() + }, + ..default() + }, + )); + }); } pub fn show_deathscreen( @@ -151,3 +221,92 @@ pub fn handle_deathscreen_input( ew_deathscreen.send(DeathScreenEvent::Hide); } } + +#[derive(Resource, Debug, Default)] +pub struct MenuState { + cursor: usize, +} + +pub fn update_menu( + mut q_text: Query<&mut Text, With>, + menustate: Res, + settings: Res, +) { + if !settings.menu_active { + return; + } + if let Ok(mut text) = q_text.get_single_mut() { + for i in 0..text.sections.len() { + if menustate.cursor == i { + text.sections[i].style.color = settings.hud_color_subtitles; + } else { + text.sections[i].style.color = settings.hud_color; + } + } + } +} + +pub fn handle_input( + keyboard_input: Res>, + mut settings: ResMut, + mut q_vis: Query<&mut Visibility, With>, + mut menustate: ResMut, + mut app_exit_events: ResMut>, + mut ew_game: EventWriter, + mut ew_sfx: EventWriter, +) { + if keyboard_input.just_pressed(settings.key_menu) + || keyboard_input.just_pressed(settings.key_vehicle) && settings.menu_active + { + settings.menu_active ^= true; + for mut vis in &mut q_vis { + *vis = bool2vis(settings.menu_active); + } + } + if !settings.menu_active { + return; + } + if keyboard_input.just_pressed(settings.key_forward) + || keyboard_input.just_pressed(settings.key_mouseup) + || keyboard_input.just_pressed(KeyCode::ArrowUp) + { + menustate.cursor = menustate.cursor.saturating_sub(1); + ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); + } + if keyboard_input.just_pressed(settings.key_back) + || keyboard_input.just_pressed(settings.key_mousedown) + || keyboard_input.just_pressed(KeyCode::ArrowDown) + { + menustate.cursor = (menustate.cursor + 1).min(MENUDEF.len() - 1); + ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); + } + if keyboard_input.just_pressed(settings.key_interact) + || keyboard_input.just_pressed(KeyCode::Enter) + { + ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click)); + match MENUDEF[menustate.cursor].1 { + MenuAction::ToggleMap => { + ew_game.send(GameEvent::SetMap(Turn::Toggle)); + settings.menu_active = false; + for mut vis in &mut q_vis { + *vis = Visibility::Hidden; + } + }, + MenuAction::ToggleAR => { + ew_game.send(GameEvent::SetAR(Turn::Toggle)); + }, + MenuAction::ToggleMusic => { + ew_game.send(GameEvent::SetMusic(Turn::Toggle)); + }, + MenuAction::ToggleSound => { + ew_game.send(GameEvent::SetSound(Turn::Toggle)); + }, + MenuAction::ToggleFullscreen => { + ew_game.send(GameEvent::SetFullscreen(Turn::Toggle)); + }, + MenuAction::Quit => { + app_exit_events.send(bevy::app::AppExit); + }, + }; + } +} diff --git a/src/var.rs b/src/var.rs index 13f73f3..2a438c1 100644 --- a/src/var.rs +++ b/src/var.rs @@ -71,6 +71,7 @@ pub struct Settings { pub hud_active: bool, pub map_active: bool, pub deathscreen_active: bool, + pub menu_active: bool, pub death_cause: String, pub is_zooming: bool, pub third_person: bool, @@ -87,7 +88,7 @@ pub struct Settings { //pub key_map_zoom_out_wheel: MouseButton, //pub key_map_zoom_in_wheel: MouseButton, pub key_togglehud: KeyCode, - pub key_exit: KeyCode, + pub key_menu: KeyCode, pub key_restart: KeyCode, pub key_fullscreen: KeyCode, pub key_help: KeyCode, @@ -187,6 +188,7 @@ impl Default for Settings { hud_active: true, map_active: false, deathscreen_active: false, + menu_active: false, death_cause: "Unknown".to_string(), is_zooming: false, third_person: true, @@ -203,7 +205,7 @@ impl Default for Settings { //key_map_zoom_out_wheel: KeyCode::Shift, //key_map_zoom_in_wheel: KeyCode::Shift, key_togglehud: KeyCode::Tab, - key_exit: KeyCode::Escape, + key_menu: KeyCode::Escape, key_restart: KeyCode::F7, key_fullscreen: KeyCode::F11, key_help: KeyCode::F1, @@ -287,6 +289,10 @@ impl Settings { self.key_reply10, ]; } + + pub fn in_control(&self) -> bool { + return self.alive && !self.menu_active; + } } #[derive(Resource, Deserialize, Debug, Default)] @@ -574,3 +580,10 @@ impl GameVars { } } } + +#[derive(Resource, Default)] +pub struct CommandLineOptions { + pub window_mode_fullscreen: WindowMode, + pub window_mode_initial: WindowMode, + pub use_gl: bool, +}