use bevy::{ input_focus::{ InputFocus, InputFocusVisible, directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin}, }, prelude::*, }; use crate::{ AppState, game_settings::CommonAssets, menus::{character_select::CharacterSelectScreen, main_menu::MainMenu}, }; pub mod button; pub mod character_select; pub mod cursor; pub mod errors; pub mod input; pub mod main_menu; pub mod match_config; pub mod match_results; pub mod navigation; pub mod scaling; pub mod stage_select; pub mod style; #[derive(Component, Clone, Default)] pub struct MenuMarker {} #[derive(Component, Clone, Default)] pub struct MenuCameraMarker {} pub struct MenuPlugin; pub fn build_menu_scene(_existing_menu: Option>>) -> impl Scene where S: SceneComponent + Default + Clone + Send + Sync, { } pub fn wrap_menu(menu: impl Scene) -> impl Scene { bsn! { #MenuRoot MenuMarker Transform Node { width: percent(100), height: percent(100), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, } Children [ ( #MenuCamera Camera Camera2d MenuCameraMarker ), ( #MenuContainer Node { width: px(1920), height: px(1080), align_items: AlignItems::Center, justify_content: JustifyContent::Center, column_gap: vmin(5.0), flex_direction: FlexDirection::Column, //padding: UiRect { left: vmin(5.0), right: vmin(5.0), top: vmin(5.0), bottom: vmin(5.0) } } menu ) ] } } pub fn despawn_menu( existing_menu: Option>>, mut commands: Commands, ) { if let Some(entity) = existing_menu { commands.entity(entity.into_inner()).despawn(); } } pub fn spawn_menu( existing_menu: Option>>, mut commands: Commands, menu: impl Scene, ) { if let Some(entity) = existing_menu { commands.entity(entity.into_inner()).despawn(); } commands.queue_spawn_scene(wrap_menu(menu)); } pub fn build_menu_with_props(props: S::Props) -> impl Scene where S: SceneComponent + Default + Clone + Send + Sync, { S::scene(props) } pub fn build_menu(existing_menu: Option>>, commands: Commands) where S: SceneComponent + Default + Clone + Send + Sync, { spawn_menu( existing_menu, commands, build_menu_with_props::(S::Props::default()), ); } impl Plugin for MenuPlugin { fn build(&self, app: &mut App) { app.add_systems(OnEnter(AppState::MainMenu), build_menu::) .add_message::() .init_resource::() .init_resource::() .init_resource::() .insert_resource(InputFocusVisible(true)) .insert_resource(GlobalUiDebugOptions { ..default() }) // Configure auto-navigation behavior .insert_resource(AutoNavigationConfig { // Require at least 10% overlap in perpendicular axis for cardinal directions min_alignment_factor: 0.1, // Don't connect nodes more than 500 pixels apart between their closest edges max_search_distance: Some(500.0), // Prefer nodes that are well-aligned prefer_aligned: true, }) .add_plugins(DirectionalNavigationPlugin) .add_systems( PreUpdate, ( navigation::give_default_focus, scaling::ui_scaling_system, button::button_setup, button::button_style_system.after(bevy::picking::PickingSystems::Hover), button::button_focus_style_system, ) .run_if(resource_exists::), ) .add_systems(Startup, scaling::ui_update_scale_on_startup) // CURSOR .add_systems( PreUpdate, (cursor::cursor_input).before(bevy::picking::PickingSystems::ProcessInput), ) .add_systems( Update, (cursor::copy_mouse_input, cursor::update_cursor_transform), ) .add_systems( PreUpdate, input::sample_menu_inputs .before(cursor::cursor_input) .run_if(resource_exists::), ) //--------- // CHARACTER SELECT START //--------- .add_systems( OnEnter(AppState::CharacterSelect), character_select::setup_character_select, ) .add_systems( PostUpdate, character_select::copy_cursor_transform_to_token .run_if(in_state(AppState::CharacterSelect)) .run_if(any_with_component::), ) .add_systems( PreUpdate, ( character_select::css_auto_assign_slots, character_select::handle_fighter_slot_addition, ) .chain() .run_if(in_state(AppState::CharacterSelect)) .run_if(any_with_component::), ) .add_systems( OnExit(AppState::CharacterSelect), (cursor::despawn_cursors, character_select::despawn_tokens), ) .add_observer( character_select::select_fighter.run_if(in_state(AppState::CharacterSelect)), ) .add_observer( character_select::update_start_banner_visibility .run_if(in_state(AppState::CharacterSelect)), ) //--------- // CHARACTER SELECT END //--------- //--------- // STAGE SELECT START //--------- .add_systems( OnEnter(AppState::StageSelect), stage_select::setup_stage_select, ) .add_observer(stage_select::stage_selected.run_if(in_state(AppState::StageSelect))) .add_systems( OnExit(AppState::StageSelect), (cursor::despawn_cursors, despawn_menu), ) //--------- // STAGE SELECT END //--------- //--------- // MATCH RESULT START //--------- .add_systems( OnEnter(AppState::MatchResults), match_results::setup_post_match_menu, ) .add_systems( PostUpdate, match_results::check_return_to_character_select .run_if(in_state(AppState::MatchResults)), ) //--------- // MATCH RESULT END //--------- .add_systems( Update, ( navigation::process_inputs, navigation::navigate, navigation::interact_with_focused_button, ) .chain(), ) .init_resource::(); } }