use std::ops::DerefMut; use crate::{ GameState, config::{UI_BUTTON_HOVERED, UI_BUTTON_NORMAL, UI_BUTTON_PRESSED}, despawn, resources::{Lives, Score}, }; use bevy::{ color::palettes::css::{BLACK, DARK_GRAY, GREEN, LIGHT_BLUE, RED, WHITE}, prelude::*, }; /// Plugin for the main menu pub struct PluginGameMenu; impl Plugin for PluginGameMenu { fn build(&self, app: &mut App) { app.add_systems(OnEnter(GameState::TitleScreen), spawn_menu) .add_systems(OnExit(GameState::TitleScreen), despawn::) .add_systems( Update, (handle_spacebar, operate_buttons).run_if(in_state(GameState::TitleScreen)), ); } } /// Plugin for the "get ready" period as gameplay starts. pub struct PluginGetReady; impl Plugin for PluginGetReady { fn build(&self, app: &mut App) { app.add_systems(OnEnter(GameState::GetReady), spawn_get_ready) .add_systems(OnExit(GameState::GetReady), despawn::) .add_systems( Update, (animate_get_ready_widget).run_if(in_state(GameState::GetReady)), ) .insert_resource(ReadySetGoTimer(Timer::from_seconds(3.0, TimerMode::Once))); } } /// Plugin for the in-game HUD pub struct PluginGameHud; impl Plugin for PluginGameHud { fn build(&self, app: &mut App) { app.add_systems(OnEnter(GameState::Playing), spawn_ui) .add_systems(OnExit(GameState::Playing), despawn::) .add_systems(Update, (operate_ui).run_if(in_state(GameState::Playing))); } } /// Plugin for the game-over screen pub struct PluginGameOver; impl Plugin for PluginGameOver { fn build(&self, app: &mut App) { app.add_systems(OnEnter(GameState::GameOver), spawn_gameover_ui) .add_systems(OnExit(GameState::GameOver), despawn::) .add_systems( Update, operate_buttons.run_if(in_state(GameState::GameOver)), ); } } // Marker component for the title screen UI entity. // This way, a query for the TitleUI can be used to despawn the title screen #[derive(Component)] struct MarkerMainMenu; /// Marker component for things on the get-ready indicator #[derive(Component)] struct OnReadySetGo; /// Marker for things on the game-over screen #[derive(Component)] struct MarkerGameOver; /// Marker for things on the HUD (the in-game UI elements) #[derive(Component)] struct MarkerHUD; /// Action specifier for the game-over menu's buttons. /// /// Attach this component to a button and [`PluginGameOver`] will use it to /// decide what to do when that button is pressed. #[derive(Component)] enum ButtonMenuAction { ToMainMenu, StartGame, Quit, } /// Newtype wrapper for `Timer`. Used to count down during the "get ready" phase. #[derive(Deref, DerefMut, Resource)] struct ReadySetGoTimer(Timer); /// Marker for the counter text segment #[derive(Component)] struct CountdownText; /// Marker for the counter bar segment #[derive(Component)] struct CountdownBar; /// Utility function for creating a standard button. fn button_bundle(text: &str) -> impl Bundle { ( Button, // TODO: Generic action Node { width: Val::Px(150.0), height: Val::Px(65.0), border: UiRect::all(Val::Px(2.0)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, margin: UiRect::all(Val::Px(5.0)), ..default() }, BorderColor(Color::BLACK), BorderRadius::MAX, BackgroundColor(UI_BUTTON_NORMAL), children![( Text::new(text), TextColor(Color::srgb(0.9, 0.9, 0.9)), TextShadow::default(), )], ) } fn spawn_menu(mut commands: Commands) { commands .spawn(( MarkerMainMenu, Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, ..Default::default() }, )) .with_children(|cmds| { cmds.spawn(( Text::new("Robert's Bad Asteroids Game"), TextFont::from_font_size(50.0), TextLayout::new_with_justify(JustifyText::Center), TextShadow::default(), )); cmds.spawn(( Text::new("Press space to begin"), TextFont::from_font_size(20.0), TextColor(Color::srgb(0.7, 0.7, 0.7)), TextShadow::default(), )); cmds.spawn((button_bundle("Start Game"), ButtonMenuAction::StartGame)); cmds.spawn((button_bundle("Quit"), ButtonMenuAction::Quit)); }); } fn spawn_get_ready(mut commands: Commands, mut timer: ResMut) { timer.reset(); commands.spawn(( OnReadySetGo, // marker, so this can be de-spawned properly Node { align_self: AlignSelf::Center, justify_self: JustifySelf::Center, align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, width: Val::Percent(30.), height: Val::Percent(30.), ..default() }, BackgroundColor(LIGHT_BLUE.into()), children![ (Text::new("Get Ready!"), TextColor(BLACK.into())), ( CountdownBar, Node { width: Val::Percent(90.0), height: Val::Percent(10.), ..default() }, BackgroundColor(GREEN.into()), ), ( CountdownText, Text::new(""), TextColor(RED.into()), ) ], )); } /// Spawns the game over screen. /// /// Used by [`PluginGameOver`] when entering the [`GameState::GameOver`] state. fn spawn_gameover_ui(mut commands: Commands) { commands.spawn(( MarkerGameOver, // Marker, so `despawn` can remove this on state exit. Node { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, flex_direction: FlexDirection::Column, ..default() }, children![ (button_bundle("Main Menu"), ButtonMenuAction::ToMainMenu,), (button_bundle("Quit"), ButtonMenuAction::Quit), ], )); } fn animate_get_ready_widget( mut text_segment: Single<&mut Text, With>, mut bar_segment: Single<&mut Node, With>, time: Res