use crate::prelude::*; pub(crate) struct MenuPlugin; impl Plugin for MenuPlugin { fn build(&self, app: &mut App) { app.init_state::() // Initialize menus .add_systems( // TODO: move this to game.rs or display3d.rs OnExit(GameState::Loading), init_play_menu, ) // Manage visible/hidden states .add_systems( Update, manage_state_entities::().run_if(state_changed::), ) .add_systems( PostUpdate, ( handle_button_press::, handle_button_press::, handle_button_press::, handle_button_press::, ) .run_if(any_component_changed::()), ) .add_systems( Update, manage_ai_button.run_if(state_changed::), ) .add_systems(Update, handle_escape.run_if(just_pressed(KeyCode::Escape))); } } #[derive(Debug, States, Hash, Default, PartialEq, Eq, Clone, Component)] pub(crate) enum MenuState { #[default] Off, On, } #[derive(Debug, Component)] pub(crate) struct ButtonAction(pub S); fn init_play_menu( mut commands: Commands, tweaks_file: Res, tweaks: Res>, ) { debug!("Initializing Play menu"); let tweak = tweaks.get(tweaks_file.handle.clone()).expect("Load tweaks"); let button_handle = tweak.get_handle::("buttons_image_resting").unwrap(); let font_handle = tweak.get_handle::("buttons_font").unwrap(); let title_handle = tweak.get_handle::("title_image").unwrap(); let title_width_px = tweak.get::("title_width_px").unwrap(); commands .spawn(( MenuState::On, NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), justify_content: JustifyContent::Center, align_items: AlignItems::Center, flex_direction: FlexDirection::Column, position_type: PositionType::Absolute, ..default() }, background_color: Color::NONE.into(), visibility: Visibility::Hidden, ..default() }, )) .with_children(|parent| { // Title parent.spawn(ImageBundle { style: Style { width: Val::Px(title_width_px), ..default() }, image: UiImage { texture: title_handle.clone(), ..default() }, ..default() }); // Continue button parent .spawn(( ButtonAction(GameState::Play), ButtonAction(MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(2.0)), margin: UiRect::all(Val::Px(2.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "Continue".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); // Restart button parent .spawn(( ButtonAction(GameState::Restart), ButtonAction(MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(2.0)), margin: UiRect::all(Val::Px(2.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "New Game".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); // Tutorial button parent .spawn(( ButtonAction(TutorialState::Intro), ButtonAction(MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(2.0)), margin: UiRect::all(Val::Px(2.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "Tutorial".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); // Opponent button parent .spawn(( ButtonAction(ai::PlayState::Human), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(2.0)), margin: UiRect::all(Val::Px(2.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "Human Opponent".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); // Credits button parent .spawn(( ButtonAction(GameState::Credits), ButtonAction(MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(2.0)), margin: UiRect::all(Val::Px(2.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "Credits".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); // Quit button parent .spawn(( ButtonAction(GameState::Quit), ButtonAction(MenuState::Off), ButtonBundle { style: Style { padding: UiRect::all(Val::Px(2.0)), margin: UiRect::all(Val::Px(2.0)), ..default() }, image: UiImage { texture: button_handle.clone(), ..default() }, ..default() }, )) .with_children(|parent| { parent.spawn(TextBundle { text: Text { sections: vec![TextSection { value: "Quit".into(), style: TextStyle { color: Color::WHITE, font_size: 12.0, font: font_handle.clone(), }, }], ..default() }, style: Style { margin: UiRect::all(Val::Px(10.0)), ..default() }, ..default() }); }); }); } fn handle_escape( game_state: Res>, tutorial_state: Res>, menu_state: Res>, mut next_game_state: ResMut>, mut next_menu_state: ResMut>, mut next_tutorial_state: ResMut>, ) { match game_state.get() { GameState::Loading => error!("No menu while loading!"), // State(Play): ... GameState::Play => { match tutorial_state.get() { // State(Tutorial::None): Escape -> Play Menu TutorialState::None => { match menu_state.get() { // State(Menu::None): Escape -> Show Menu MenuState::Off => next_menu_state.set(MenuState::On), // State(Menu::Play): Escape -> Hide menu MenuState::On => next_menu_state.set(MenuState::Off), } } // State(Tutorial::*): Escape -> Quit Tutorial _ => { next_tutorial_state.set(TutorialState::None); } } } // State(Endgame): Escape -> Quit game GameState::Endgame => { next_game_state.set(GameState::Quit); } // State(Credits): Escape -> Play + Menu GameState::Credits => { next_game_state.set(GameState::Play); next_menu_state.set(MenuState::On); } // State(Intro): Escape -> Play GameState::Intro => { next_game_state.set(GameState::Title); } GameState::Title => { next_game_state.set(GameState::Play); } GameState::Restart | GameState::Quit => { panic!("This shouldn't be possible!"); } } } fn handle_button_press( events: Query<(&Interaction, &ButtonAction), Changed>, mut next_state: ResMut>, ) { events .iter() .filter_map(|(interaction, button_action)| { (*interaction == Interaction::Pressed).then_some(button_action) }) .for_each(|ButtonAction(ba)| { debug!("Button press: {:?} => {:?}", next_state, ba); next_state.set(ba.clone()) }); } fn manage_ai_button( curr: Res>, mut query: Query<(&mut ButtonAction, &Children)>, mut texts: Query<&mut Text>, ) { query.iter_mut().for_each(|(mut ba, children)| { ba.0 = match curr.get() { PlayState::AiBogo => PlayState::Human, PlayState::Human => PlayState::AiBogo, }; children.iter().for_each(|c| { texts.get_mut(*c).iter_mut().for_each(|t| { t.sections.iter_mut().for_each(|s| { s.value = match ba.0 { PlayState::AiBogo => "AI Opponent".into(), PlayState::Human => "Human Opponent".into(), } }); }); }); }); }