// ▄████████▄ + ███ + ▄█████████ ███ + // ███▀ ▀███ + + ███ ███▀ + ███ + + // ███ + ███ ███ ███ █████████ ███ ███ ███ ███ // ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███ // ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███ // ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███ // ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████ // + + + ███ // + ▀████████████████████████████████████████████████████▀ // // This plugin manages game menus and the player death screen use crate::prelude::*; use bevy::prelude::*; use fastrand; pub const POEMS: &str = &include_str!("data/deathpoems.in"); pub struct MenuPlugin; impl Plugin for MenuPlugin { fn build(&self, app: &mut App) { 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 .after(game::handle_game_event) .run_if(on_event::()), ); app.add_systems(Update, handle_input.run_if(alive)); app.insert_resource(DeathScreenInputDelayTimer(Timer::from_seconds( 1.0, TimerMode::Once, ))); app.insert_resource(MenuState::default()); app.add_event::(); app.add_event::(); } } #[derive(Resource)] pub struct DeathScreenInputDelayTimer(pub Timer); #[derive(Component)] pub struct MenuElement; #[derive(Component)] pub struct MenuTopLevel; #[derive(Component)] pub struct FooterElement; #[derive(Component)] pub struct MenuAchievements; #[derive(Component)] pub struct DeathScreenElement; #[derive(Component)] pub struct DeathText; #[derive(Event)] pub struct UpdateMenuEvent; #[derive(Event, PartialEq)] pub enum DeathScreenEvent { Show, Hide, } pub const MENUDEF: &[(&str, MenuAction)] = &[ ("Phone Call", MenuAction::PhoneCall), ("", MenuAction::ToggleAR), ("", MenuAction::ChangeARAvatar), ("", MenuAction::ChangePointer), ("", MenuAction::ToggleMap), ("", MenuAction::ModLightAmp), ("", MenuAction::ModFlashlightPower), ("", MenuAction::ModThrusterBoost), ("", MenuAction::ModReactor), ("", MenuAction::ToggleSound), ("", MenuAction::ToggleMusic), ("", MenuAction::ToggleCamera), ("", MenuAction::ToggleShadows), ("Fullscreen [F11]", MenuAction::ToggleFullscreen), ("Take Off Helmet", MenuAction::Restart), ("Quit", MenuAction::Quit), ]; #[derive(Component)] pub enum MenuAction { ToggleMap, ToggleAR, ChangeARAvatar, ChangePointer, ModLightAmp, ModFlashlightPower, ModThrusterBoost, ModReactor, ToggleSound, ToggleMusic, PhoneCall, ToggleCamera, ToggleFullscreen, ToggleShadows, Restart, Quit, } pub fn setup( mut commands: Commands, asset_server: Res, achievement_tracker: Res, settings: Res, ) { commands.spawn(( DeathScreenElement, NodeBundle { style: style_fullscreen(), background_color: Color::BLACK.into(), visibility: Visibility::Hidden, ..default() }, )); let font_handle = asset_server.load(FONT); let style_death = TextStyle { font: font_handle.clone(), font_size: settings.font_size_deathtext, color: settings.hud_color_death, ..default() }; let style_death_poem = TextStyle { font: font_handle.clone(), font_size: settings.font_size_deathpoem, color: settings.hud_color_deathpoem, ..default() }; let style_death_subtext = TextStyle { font: font_handle.clone(), font_size: settings.font_size_deathsubtext, color: settings.hud_color_death, ..default() }; let style_death_subsubtext = TextStyle { font: font_handle.clone(), font_size: settings.font_size_deathsubtext * 0.8, color: settings.hud_color_death, ..default() }; let style_death_achievements = TextStyle { font: font_handle.clone(), font_size: settings.font_size_death_achievements, color: settings.hud_color_death_achievements, ..default() }; commands .spawn(( DeathScreenElement, NodeBundle { style: style_centered(), visibility: Visibility::Hidden, ..default() }, )) .with_children(|builder| { builder.spawn(( DeathText, TextBundle { text: Text { sections: vec![ TextSection::new("", style_death_poem), TextSection::new("You are dead.\n", style_death), TextSection::new("Cause: ", style_death_subtext.clone()), TextSection::new("Unknown", style_death_subtext), TextSection::new("", style_death_achievements), TextSection::new( "\n\n\n\nPress E to begin anew.", style_death_subsubtext, ), ], justify: JustifyText::Center, ..default() }, ..default() }, )); }); 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::srgba(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() }, )); }); let style_achievement_header = TextStyle { font: font_handle.clone(), font_size: settings.font_size_achievement_header, color: settings.hud_color_achievement_header, ..default() }; let style_achievement = TextStyle { font: font_handle.clone(), font_size: settings.font_size_achievement, color: settings.hud_color_achievement, ..default() }; let achievement_count = achievement_tracker.to_bool_vec().len(); commands .spawn(( MenuElement, NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), left: Val::Percent(2.0), top: Val::Percent(2.0), align_items: AlignItems::Start, justify_content: JustifyContent::Start, ..default() }, visibility: Visibility::Hidden, ..default() }, )) .with_children(|builder| { let mut sections = vec![TextSection::new( "Achievements\n", style_achievement_header.clone(), )]; sections.extend(Vec::from_iter( (0..achievement_count).map(|_| TextSection::new("", style_achievement.clone())), )); sections.push(TextSection::new( "\nPhonebook\n", style_achievement_header.clone(), )); sections.extend(Vec::from_iter((0..chat::CONTACTS.len()).map(|_| { TextSection::new( chat::CONTACTS_UNKNOWN.to_string() + "\n", style_achievement.clone(), ) }))); builder.spawn(( MenuAchievements, TextBundle { text: Text { sections, justify: JustifyText::Left, ..default() }, ..default() }, )); }); let keybindings = include_str!("data/keybindings.in"); let style_keybindings = TextStyle { font: font_handle.clone(), font_size: settings.font_size_keybindings, color: settings.hud_color_keybindings, ..default() }; commands .spawn(( MenuElement, NodeBundle { style: Style { width: Val::Percent(96.0), height: Val::Percent(96.0), left: Val::Percent(2.0), top: Val::Percent(2.0), align_items: AlignItems::Start, justify_content: JustifyContent::End, ..default() }, visibility: Visibility::Hidden, ..default() }, )) .with_children(|builder| { builder.spawn((TextBundle { text: Text { sections: vec![ TextSection::new("Controls\n", style_achievement_header), TextSection::new(keybindings, style_keybindings), ], justify: JustifyText::Right, ..default() }, ..default() },)); }); let style_version = TextStyle { font: font_handle.clone(), font_size: settings.font_size_version, color: settings.hud_color_version, ..default() }; commands .spawn(( MenuElement, NodeBundle { style: Style { width: Val::Percent(96.0), height: Val::Percent(96.0), left: Val::Percent(2.0), top: Val::Percent(2.0), align_items: AlignItems::End, justify_content: JustifyContent::End, ..default() }, visibility: Visibility::Hidden, ..default() }, )) .with_children(|builder| { builder.spawn(( FooterElement, TextBundle { text: Text { sections: vec![TextSection::new( format!("{} {}", GAME_NAME, settings.version.as_str()), style_version, )], justify: JustifyText::Right, ..default() }, ..default() }, )); }); } pub fn show_deathscreen( mut er_deathscreen: EventReader, mut q_vis: Query<&mut Visibility, With>, mut q_text: Query<&mut Text, With>, mut ew_pausesfx: EventWriter, mut ew_game: EventWriter, mut ew_sfx: EventWriter, mut ew_effect: EventWriter, mut ew_respawn: EventWriter, mut ew_respawnaudiosinks: EventWriter, mut timer: ResMut, mut menustate: ResMut, mut settings: ResMut, achievement_tracker: Res, ) { for event in er_deathscreen.read() { let show = *event == DeathScreenEvent::Show; for mut vis in &mut q_vis { *vis = bool2vis(show); } settings.deathscreen_active = show; settings.alive = !show; if show { timer.0.reset(); *menustate = MenuState::default(); ew_game.send(GameEvent::SetMenu(Turn::Off)); ew_pausesfx.send(audio::PauseAllSfxEvent); if let Ok(mut text) = q_text.get_single_mut() { let poems: Vec<&str> = POEMS.split("\n\n").collect(); if poems.len() > 0 { let poem_index = fastrand::usize(..poems.len()); let poem = poems[poem_index].to_string(); text.sections[0].value = poem + "\n\n\n\n"; } text.sections[3].value = settings.death_cause.clone(); text.sections[4].value = achievement_tracker.to_summary(); } } else { ew_respawnaudiosinks.send(audio::RespawnSinksEvent); ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::WakeUp)); ew_effect.send(visual::SpawnEffectEvent { class: visual::Effects::FadeIn(Color::BLACK), duration: 0.3, }); ew_respawn.send(world::RespawnEvent); } } } pub fn handle_deathscreen_input( time: Res