outfly/src/menu.rs

549 lines
19 KiB
Rust
Raw Normal View History

2024-05-12 20:17:17 +00:00
// ▄████████▄ + ███ + ▄█████████ ███ +
// ███▀ ▀███ + + ███ ███▀ + ███ + +
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
// + + + ███
// + ▀████████████████████████████████████████████████████▀
//
// This plugin manages game menus and the player death screen
use crate::prelude::*;
use bevy::prelude::*;
2024-05-12 23:42:22 +00:00
use fastrand;
pub const POEMS: &str = &include_str!("data/deathpoems.in");
2024-05-12 20:17:17 +00:00
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::<DeathScreenEvent>()));
app.add_systems(Update, handle_deathscreen_input);
2024-05-13 21:50:53 +00:00
app.add_systems(PostUpdate, update_menu
.after(game::handle_game_event)
.run_if(on_event::<UpdateMenuEvent>()));
2024-05-13 18:21:56 +00:00
app.add_systems(Update, handle_input.run_if(alive));
app.insert_resource(DeathScreenInputDelayTimer(
Timer::from_seconds(1.0, TimerMode::Once)));
2024-05-13 18:21:56 +00:00
app.insert_resource(MenuState::default());
2024-05-12 20:17:17 +00:00
app.add_event::<DeathScreenEvent>();
2024-05-13 19:37:57 +00:00
app.add_event::<UpdateMenuEvent>();
2024-05-12 20:17:17 +00:00
}
}
#[derive(Resource)] pub struct DeathScreenInputDelayTimer(pub Timer);
2024-05-13 18:21:56 +00:00
#[derive(Component)] pub struct MenuElement;
#[derive(Component)] pub struct MenuTopLevel;
2024-05-14 03:17:32 +00:00
#[derive(Component)] pub struct MenuAchievements;
2024-05-12 20:17:17 +00:00
#[derive(Component)] pub struct DeathScreenElement;
#[derive(Component)] pub struct DeathText;
2024-05-13 19:37:57 +00:00
#[derive(Event)] pub struct UpdateMenuEvent;
2024-05-12 20:17:17 +00:00
#[derive(Event, PartialEq)] pub enum DeathScreenEvent { Show, Hide }
2024-05-13 18:21:56 +00:00
pub const MENUDEF: &[(&str, MenuAction)] = &[
2024-05-13 19:37:57 +00:00
("", MenuAction::ToggleMap),
("", MenuAction::ToggleAR),
("", MenuAction::ToggleSound),
("", MenuAction::ToggleMusic),
2024-05-13 21:51:18 +00:00
("", MenuAction::ToggleCamera),
2024-05-13 19:12:23 +00:00
("Toggle Fullscreen [F11]", MenuAction::ToggleFullscreen),
2024-05-13 19:37:57 +00:00
("", MenuAction::ToggleShadows),
("Take Off Helmet", MenuAction::Restart),
2024-05-13 18:21:56 +00:00
("Quit", MenuAction::Quit),
];
#[derive(Component)]
pub enum MenuAction {
ToggleMap,
ToggleAR,
ToggleSound,
ToggleMusic,
2024-05-13 21:51:18 +00:00
ToggleCamera,
2024-05-13 18:21:56 +00:00
ToggleFullscreen,
2024-05-13 19:11:27 +00:00
ToggleShadows,
2024-05-13 18:53:08 +00:00
Restart,
2024-05-13 18:21:56 +00:00
Quit,
}
2024-05-12 20:17:17 +00:00
pub fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
2024-05-14 03:33:35 +00:00
achievement_tracker: Res<var::AchievementTracker>,
2024-05-12 20:17:17 +00:00
settings: Res<Settings>,
) {
commands.spawn((
DeathScreenElement,
NodeBundle {
2024-05-12 22:44:03 +00:00
style: style_fullscreen(),
2024-05-12 20:17:17 +00:00
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,
2024-05-14 04:28:14 +00:00
color: settings.hud_color_death,
2024-05-12 20:17:17 +00:00
..default()
};
2024-05-12 23:42:22 +00:00
let style_death_poem = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_deathpoem,
color: settings.hud_color_deathpoem,
..default()
};
2024-05-12 20:17:17 +00:00
let style_death_subtext = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_deathsubtext,
2024-05-14 04:28:14 +00:00
color: settings.hud_color_death,
2024-05-12 20:17:17 +00:00
..default()
};
let style_death_subsubtext = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_deathsubtext * 0.8,
2024-05-14 04:28:14 +00:00
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,
2024-05-12 20:17:17 +00:00
..default()
};
commands.spawn((
DeathScreenElement,
NodeBundle {
2024-05-12 22:44:03 +00:00
style: style_centered(),
2024-05-12 20:17:17 +00:00
visibility: Visibility::Hidden,
..default()
},
)).with_children(|builder| {
builder.spawn((
DeathText,
TextBundle {
text: Text {
sections: vec![
2024-05-12 23:42:22 +00:00
TextSection::new("", style_death_poem),
2024-05-12 20:17:17 +00:00
TextSection::new("You are dead.\n", style_death),
TextSection::new("Cause: ", style_death_subtext.clone()),
TextSection::new("Unknown", style_death_subtext),
2024-05-14 04:28:14 +00:00
TextSection::new("", style_death_achievements),
2024-05-12 23:42:22 +00:00
TextSection::new("\n\n\n\nPress E to begin anew.", style_death_subsubtext),
2024-05-12 20:17:17 +00:00
],
justify: JustifyText::Center,
..default()
},
..default()
},
));
});
2024-05-13 18:21:56 +00:00
let style_menu = TextStyle {
font: font_handle.clone(),
font_size: settings.font_size_hud,
color: settings.hud_color,
..default()
};
let sections: Vec<TextSection> = 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()
},
));
});
2024-05-14 03:17:32 +00:00
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()
};
2024-05-14 03:33:35 +00:00
let achievement_count = achievement_tracker.to_bool_vec().len();
2024-05-14 03:17:32 +00:00
commands.spawn((
MenuElement,
NodeBundle {
style: Style {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
2024-05-14 03:22:10 +00:00
left: Val::Percent(2.0),
top: Val::Percent(2.0),
2024-05-14 03:17:32 +00:00
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())
2024-05-14 03:17:32 +00:00
];
2024-05-14 03:33:35 +00:00
sections.extend(Vec::from_iter((0..achievement_count).map(|_|
2024-05-14 03:17:32 +00:00
TextSection::new("", 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![
2024-05-14 17:40:53 +00:00
TextSection::new("Controls\n", style_achievement_header),
TextSection::new(keybindings, style_keybindings)
],
justify: JustifyText::Right,
..default()
},
..default()
},
));
});
2024-05-14 17:08:18 +00:00
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((
TextBundle {
text: Text {
sections: vec![
TextSection::new("OutFly ".to_string()
+ settings.version.as_str(), style_version),
],
justify: JustifyText::Right,
..default()
},
..default()
},
));
});
2024-05-12 20:17:17 +00:00
}
pub fn show_deathscreen(
mut er_deathscreen: EventReader<DeathScreenEvent>,
mut q_vis: Query<&mut Visibility, With<DeathScreenElement>>,
mut q_text: Query<&mut Text, With<DeathText>>,
2024-05-13 02:33:03 +00:00
mut ew_pausesfx: EventWriter<audio::PauseAllSfxEvent>,
2024-05-13 18:53:08 +00:00
mut ew_game: EventWriter<GameEvent>,
2024-05-12 20:17:17 +00:00
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_effect: EventWriter<visual::SpawnEffectEvent>,
mut ew_respawn: EventWriter<world::RespawnEvent>,
2024-05-13 02:41:17 +00:00
mut ew_respawnaudiosinks: EventWriter<audio::RespawnSinksEvent>,
mut timer: ResMut<DeathScreenInputDelayTimer>,
2024-05-13 18:53:08 +00:00
mut menustate: ResMut<MenuState>,
2024-05-12 20:17:17 +00:00
mut settings: ResMut<Settings>,
2024-05-14 04:28:14 +00:00
achievement_tracker: Res<var::AchievementTracker>,
2024-05-12 20:17:17 +00:00
) {
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;
2024-05-13 02:47:47 +00:00
settings.alive = !show;
2024-05-12 20:17:17 +00:00
if show {
timer.0.reset();
2024-05-13 18:53:08 +00:00
*menustate = MenuState::default();
ew_game.send(GameEvent::SetMenu(Turn::Off));
2024-05-13 02:33:03 +00:00
ew_pausesfx.send(audio::PauseAllSfxEvent);
2024-05-12 20:17:17 +00:00
if let Ok(mut text) = q_text.get_single_mut() {
2024-05-12 23:42:22 +00:00
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();
2024-05-14 04:28:14 +00:00
text.sections[4].value = achievement_tracker.to_summary();
2024-05-12 20:17:17 +00:00
}
} else {
2024-05-13 02:41:17 +00:00
ew_respawnaudiosinks.send(audio::RespawnSinksEvent);
2024-05-12 20:17:17 +00:00
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);
2024-05-12 20:17:17 +00:00
}
}
}
pub fn handle_deathscreen_input(
time: Res<Time>,
2024-05-12 20:17:17 +00:00
keyboard_input: Res<ButtonInput<KeyCode>>,
mut timer: ResMut<DeathScreenInputDelayTimer>,
2024-05-12 20:17:17 +00:00
mut ew_deathscreen: EventWriter<DeathScreenEvent>,
2024-05-13 15:19:07 +00:00
settings: Res<Settings>,
2024-05-12 20:17:17 +00:00
) {
if !settings.deathscreen_active || !timer.0.tick(time.delta()).finished() {
2024-05-12 20:17:17 +00:00
return;
}
if keyboard_input.pressed(settings.key_interact) {
ew_deathscreen.send(DeathScreenEvent::Hide);
}
}
2024-05-13 18:21:56 +00:00
#[derive(Resource, Debug, Default)]
pub struct MenuState {
cursor: usize,
}
pub fn update_menu(
mut q_text: Query<&mut Text, With<MenuTopLevel>>,
2024-05-14 03:17:32 +00:00
mut q_achievement_text: Query<&mut Text, (With<MenuAchievements>, Without<MenuTopLevel>)>,
2024-05-13 19:37:57 +00:00
mut q_vis: Query<&mut Visibility, With<menu::MenuElement>>,
2024-05-14 03:17:32 +00:00
achievement_tracker: Res<var::AchievementTracker>,
2024-05-13 18:21:56 +00:00
menustate: Res<MenuState>,
settings: Res<Settings>,
) {
2024-05-13 19:37:57 +00:00
for mut vis in &mut q_vis {
*vis = bool2vis(settings.menu_active);
}
fn bool2string(boolean: bool) -> String {
if boolean { "On" } else { "Off" }.to_string()
2024-05-13 18:21:56 +00:00
}
2024-05-14 03:17:32 +00:00
let bools = achievement_tracker.to_bool_vec();
let rendered = achievement_tracker.to_textsections();
if let Ok(mut text) = q_achievement_text.get_single_mut() {
for i in 0..text.sections.len() - 1 {
text.sections[i + 1].style.color = if bools[i] {
settings.hud_color_achievement_accomplished
} else {
settings.hud_color_achievement
};
text.sections[i + 1].value = rendered[i].clone();
}
}
2024-05-13 18:21:56 +00:00
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;
}
2024-05-13 19:37:57 +00:00
match MENUDEF[i].1 {
MenuAction::ToggleSound => {
let onoff = bool2string(!settings.mute_sfx);
text.sections[i].value = format!("Sound: {onoff}\n");
}
MenuAction::ToggleMusic => {
let onoff = bool2string(!settings.mute_music);
text.sections[i].value = format!("Music: {onoff}\n");
}
MenuAction::ToggleAR => {
let onoff = bool2string(settings.hud_active);
text.sections[i].value = format!("Augmented Reality: {onoff} [TAB]\n");
}
MenuAction::ToggleMap => {
let onoff = bool2string(settings.map_active);
text.sections[i].value = format!("Map: {onoff} [M]\n");
}
2024-05-13 21:51:18 +00:00
MenuAction::ToggleCamera => {
let onoff = if settings.third_person {
"3rd Person"
} else {
"1st Person"
};
text.sections[i].value = format!("Camera: {onoff} [C]\n");
}
2024-05-13 19:37:57 +00:00
MenuAction::ToggleShadows => {
let onoff = if settings.shadows_sun {
"Flashlight + Sun"
} else {
"Flashlight Only"
};
text.sections[i].value = format!("Shadows: {onoff}\n");
}
_ => {}
}
2024-05-13 18:21:56 +00:00
}
}
}
pub fn handle_input(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<Settings>,
mut menustate: ResMut<MenuState>,
mut app_exit_events: ResMut<Events<bevy::app::AppExit>>,
mut ew_game: EventWriter<game::GameEvent>,
2024-05-13 18:53:08 +00:00
mut ew_playerdies: EventWriter<game::PlayerDiesEvent>,
2024-05-13 18:21:56 +00:00
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
2024-05-13 19:37:57 +00:00
mut ew_updatemenu: EventWriter<UpdateMenuEvent>,
2024-05-13 18:21:56 +00:00
) {
2024-05-14 16:58:47 +00:00
let last_menu_entry = MENUDEF.len() - 1;
2024-05-13 18:21:56 +00:00
if keyboard_input.just_pressed(settings.key_menu)
|| keyboard_input.just_pressed(settings.key_vehicle) && settings.menu_active
{
2024-05-13 18:53:08 +00:00
ew_game.send(GameEvent::SetMenu(Toggle));
2024-05-13 18:21:56 +00:00
}
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)
{
2024-05-14 16:58:47 +00:00
menustate.cursor = if menustate.cursor == 0 {
last_menu_entry
} else {
menustate.cursor.saturating_sub(1)
};
2024-05-13 18:21:56 +00:00
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 18:21:56 +00:00
}
if keyboard_input.just_pressed(settings.key_back)
|| keyboard_input.just_pressed(settings.key_mousedown)
|| keyboard_input.just_pressed(KeyCode::ArrowDown)
{
2024-05-14 16:58:47 +00:00
menustate.cursor = if menustate.cursor == last_menu_entry {
0
} else {
menustate.cursor.saturating_add(1)
};
2024-05-13 18:21:56 +00:00
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 18:21:56 +00:00
}
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(Toggle));
2024-05-13 18:53:08 +00:00
ew_game.send(GameEvent::SetMenu(Turn::Off));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 18:21:56 +00:00
},
MenuAction::ToggleAR => {
ew_game.send(GameEvent::SetAR(Toggle));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 18:21:56 +00:00
},
MenuAction::ToggleMusic => {
ew_game.send(GameEvent::SetMusic(Toggle));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 18:21:56 +00:00
},
MenuAction::ToggleSound => {
ew_game.send(GameEvent::SetSound(Toggle));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 18:21:56 +00:00
},
2024-05-13 21:51:18 +00:00
MenuAction::ToggleCamera => {
ew_game.send(GameEvent::SetThirdPerson(Toggle));
ew_updatemenu.send(UpdateMenuEvent);
},
2024-05-13 18:21:56 +00:00
MenuAction::ToggleFullscreen => {
ew_game.send(GameEvent::SetFullscreen(Toggle));
2024-05-13 18:21:56 +00:00
},
2024-05-13 19:11:27 +00:00
MenuAction::ToggleShadows => {
ew_game.send(GameEvent::SetShadows(Toggle));
2024-05-13 19:37:57 +00:00
ew_updatemenu.send(UpdateMenuEvent);
2024-05-13 19:11:27 +00:00
},
2024-05-13 18:53:08 +00:00
MenuAction::Restart => {
settings.god_mode = false;
ew_playerdies.send(game::PlayerDiesEvent(actor::DamageType::Depressurization));
2024-05-13 18:53:08 +00:00
},
2024-05-13 18:21:56 +00:00
MenuAction::Quit => {
app_exit_events.send(bevy::app::AppExit);
},
};
}
}