outfly/src/menu.rs

810 lines
29 KiB
Rust

// ▄████████▄ + ███ + ▄█████████ ███ +
// ███▀ ▀███ + + ███ ███▀ + ███ + +
// ███ + ███ ███ ███ █████████ ███ ███ ███ ███
// ███ +███ ███ ███ ███ ███▐██████ ███ ███ ███
// ███ + ███ ███+ ███ +███ ███ + ███ ███ + ███
// ███▄ ▄███ ███▄ ███ ███ + ███ + ███ ███▄ ███
// ▀████████▀ + ▀███████ ███▄ ███▄ ▀████ ▀███████
// + + + ███
// + ▀████████████████████████████████████████████████████▀
//
// 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::<DeathScreenEvent>()),
);
app.add_systems(Update, handle_deathscreen_input);
app.add_systems(
PostUpdate,
update_menu
.after(game::handle_game_event)
.run_if(on_event::<UpdateMenuEvent>()),
);
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::<DeathScreenEvent>();
app.add_event::<UpdateMenuEvent>();
}
}
#[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),
("⚠ FACTORY RESET ⚠", MenuAction::ResetSettings),
("⚠ 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,
ResetSettings,
Restart,
Quit,
}
pub fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
achievement_tracker: Res<var::AchievementTracker>,
settings: Res<Settings>,
) {
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<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::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<DeathScreenEvent>,
mut q_vis: Query<&mut Visibility, With<DeathScreenElement>>,
mut q_text: Query<&mut Text, With<DeathText>>,
mut ew_pausesfx: EventWriter<audio::PauseAllSfxEvent>,
mut ew_game: EventWriter<GameEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_effect: EventWriter<visual::SpawnEffectEvent>,
mut ew_respawn: EventWriter<world::RespawnEvent>,
mut ew_respawnaudiosinks: EventWriter<audio::RespawnSinksEvent>,
mut timer: ResMut<DeathScreenInputDelayTimer>,
mut menustate: ResMut<MenuState>,
mut settings: ResMut<Settings>,
achievement_tracker: Res<var::AchievementTracker>,
) {
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<Time>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut timer: ResMut<DeathScreenInputDelayTimer>,
mut ew_deathscreen: EventWriter<DeathScreenEvent>,
settings: Res<Settings>,
) {
if !settings.deathscreen_active || !timer.0.tick(time.delta()).finished() {
return;
}
if keyboard_input.pressed(settings.key_interact) {
ew_deathscreen.send(DeathScreenEvent::Hide);
}
}
#[derive(Resource, Debug, Default)]
pub struct MenuState {
cursor: usize,
}
pub fn update_menu(
mut q_text: Query<&mut Text, With<MenuTopLevel>>,
mut q_footer: Query<&mut Text, (With<FooterElement>, Without<MenuTopLevel>)>,
mut q_achievement_text: Query<
&mut Text,
(
With<MenuAchievements>,
Without<MenuTopLevel>,
Without<FooterElement>,
),
>,
mut q_vis: Query<&mut Visibility, With<menu::MenuElement>>,
id2pos: Res<game::Id2Pos>,
achievement_tracker: Res<var::AchievementTracker>,
menustate: Res<MenuState>,
settings: Res<Settings>,
prefs: Res<Preferences>,
) {
for mut vis in &mut q_vis {
*vis = bool2vis(settings.menu_active);
}
fn bool2string(boolean: bool) -> String {
if boolean { "On" } else { "Off" }.to_string()
}
// Footer
if let (Ok(mut text), Some(player_pos), Some(jupiter_pos)) = (
q_footer.get_single_mut(),
id2pos.0.get(cmd::ID_PLAYER),
id2pos.0.get(cmd::ID_JUPITER),
) {
let (clock_max, clock_current) =
nature::pos_to_orbit_time(*player_pos, *jupiter_pos, nature::JUPITER_MASS);
text.sections[0].value = format!(
"Orbital Clock:\n{} of {}\n{} {}",
nature::format_seconds_to_hour_min(clock_current),
nature::format_seconds_to_hour_min(clock_max),
GAME_NAME,
settings.version.as_str()
);
}
// Achievements
let achievement_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..achievement_bools.len() - 1 {
text.sections[i + 1].style.color = if achievement_bools[i] {
settings.hud_color_achievement_accomplished
} else {
settings.hud_color_achievement
};
text.sections[i + 1].value = rendered[i].clone();
}
// Phonebook
for (i, contact) in chat::CONTACTS.iter().enumerate() {
let text_index = i + achievement_bools.len() + 2;
let registered = prefs.contacts.contains(&contact.to_string());
text.sections[text_index].style.color = if registered {
settings.hud_color_phonebook_unlocked
} else {
settings.hud_color_phonebook_locked
};
text.sections[text_index].value = if registered {
chat::CONTACTS_PRETTY[i].to_string() + "\n"
} else {
String::from(chat::CONTACTS_UNKNOWN.to_string() + "\n")
}
}
}
// Menu
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;
}
match MENUDEF[i].1 {
MenuAction::ToggleSound => {
let noisecancel = if let Some(noisecancel) = settings
.noise_cancellation_modes
.get(settings.noise_cancellation_mode)
{
&noisecancel.0
} else {
&settings.noise_cancellation_modes[0].0
};
text.sections[i].value = format!("\nNoise Cancellation: {noisecancel}\n");
}
MenuAction::ToggleMusic => {
let station =
if let Some(station) = settings.radio_modes.get(settings.radio_mode) {
station
} else {
&settings.radio_modes[0]
};
text.sections[i].value = format!("Speakers: {station}\n");
}
MenuAction::ToggleAR => {
let onoff = bool2string(settings.hud_active);
let p = if settings.hud_active {
actor::POWER_DRAIN_AR
} else {
0.0
};
let w = if p > 0.0 {
format!(" ({p}W)")
} else {
String::from("")
};
text.sections[i].value = format!("Augmented Reality: {onoff}{w} [TAB]\n");
}
MenuAction::ModLightAmp => {
let p = actor::POWER_DRAIN_LIGHTAMP[prefs.light_amp];
text.sections[i].value = format!("\nLight Amplification: {p}W\n");
}
MenuAction::ModFlashlightPower => {
let p = actor::POWER_DRAIN_FLASHLIGHT[prefs.flashlight_power];
text.sections[i].value = format!("Flashlight Power: {p}W\n");
}
MenuAction::ModThrusterBoost => {
let state = match prefs.thruster_boost {
0 => "For braking",
1 => "Always",
2 => "Off",
_ => "ERROR",
};
let p = actor::POWER_DRAIN_THRUSTER[prefs.thruster_boost];
let w = if p > 0.0 {
format!(" ({p}W)")
} else {
String::from("")
};
text.sections[i].value = format!("Thruster Boost: {state}{w}\n");
}
MenuAction::ModReactor => {
let state = match settings.reactor_state {
0 => "Off",
1 => "On",
2 => "OVERLOAD ☢",
_ => "ERROR",
};
let p = actor::POWER_GAIN_REACTOR[settings.reactor_state];
let w = if p > 0.0 {
format!(" (+{p}W)")
} else {
String::from("")
};
text.sections[i].value = format!("Reactor: {state}{w}\n");
}
MenuAction::ChangeARAvatar => {
if let Some(ava) = hud::PLAYER_AR_AVATARS.get(settings.ar_avatar) {
let avatar_title = ava.3;
text.sections[i].value = format!("Avatar: {avatar_title}\n");
}
}
MenuAction::ChangePointer => {
for pointer in hud::POINTERS {
if pointer.0 == prefs.pointer {
text.sections[i].value = format!("Pointer: {}\n", pointer.2);
break;
}
}
}
MenuAction::ToggleMap => {
let onoff = bool2string(settings.map_active);
text.sections[i].value = format!("Map: {onoff} [M]\n");
}
MenuAction::ToggleCamera => {
let onoff = if settings.third_person {
"3rd Person"
} else {
"1st Person"
};
text.sections[i].value = format!("\nCamera: {onoff} [C]\n");
}
MenuAction::ToggleShadows => {
let onoff = if settings.shadows_sun { "High" } else { "Low" };
text.sections[i].value = format!("Shadows: {onoff}\n");
}
_ => {}
}
}
}
}
pub fn handle_input(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<Settings>,
mut prefs: ResMut<Preferences>,
mut menustate: ResMut<MenuState>,
mut app_exit_events: ResMut<Events<AppExit>>,
mut ew_game: EventWriter<game::GameEvent>,
mut ew_playerdies: EventWriter<game::PlayerDiesEvent>,
mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
mut ew_updatemenu: EventWriter<UpdateMenuEvent>,
mut ew_updateavatar: EventWriter<hud::UpdateAvatarEvent>,
mut ew_updatepointer: EventWriter<hud::UpdatePointerEvent>,
mut ew_updateoverlays: EventWriter<hud::UpdateOverlayVisibility>,
) {
let last_menu_entry = MENUDEF.len() - 1;
if keyboard_input.just_pressed(settings.key_menu)
|| keyboard_input.just_pressed(settings.key_vehicle) && settings.menu_active
{
ew_game.send(GameEvent::SetMenu(Toggle));
}
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)
{
menustate.cursor = if menustate.cursor == 0 {
last_menu_entry
} else {
menustate.cursor.saturating_sub(1)
};
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click3));
ew_updatemenu.send(UpdateMenuEvent);
}
if keyboard_input.just_pressed(settings.key_back)
|| keyboard_input.just_pressed(settings.key_mousedown)
|| keyboard_input.just_pressed(KeyCode::ArrowDown)
{
menustate.cursor = if menustate.cursor == last_menu_entry {
0
} else {
menustate.cursor.saturating_add(1)
};
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click3));
ew_updatemenu.send(UpdateMenuEvent);
}
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));
ew_game.send(GameEvent::SetMenu(Turn::Off));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ToggleAR => {
ew_game.send(GameEvent::SetAR(Toggle));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ChangeARAvatar => {
settings.ar_avatar += 1;
ew_updateavatar.send(hud::UpdateAvatarEvent);
}
MenuAction::ChangePointer => {
let mut index = 0;
for (i, pointer) in hud::POINTERS.iter().enumerate() {
if pointer.0 == prefs.pointer {
index = i;
break;
}
}
index = (index + 1) % hud::POINTERS.len();
for (i, pointer) in hud::POINTERS.iter().enumerate() {
if i == index {
prefs.pointer = pointer.0;
break;
}
}
prefs.save();
ew_updatepointer.send(hud::UpdatePointerEvent);
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ModLightAmp => {
prefs.light_amp += 1;
if prefs.light_amp > 3 {
prefs.light_amp = 0;
}
prefs.save();
ew_updateoverlays.send(hud::UpdateOverlayVisibility);
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ModFlashlightPower => {
prefs.flashlight_power += 1;
if prefs.flashlight_power > 3 {
prefs.flashlight_power = 0;
}
prefs.save();
ew_game.send(GameEvent::UpdateFlashlight);
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ModThrusterBoost => {
prefs.thruster_boost += 1;
if prefs.thruster_boost > 2 {
prefs.thruster_boost = 0;
}
prefs.save();
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ModReactor => {
settings.reactor_state += 1;
if settings.reactor_state > 2 {
settings.reactor_state = 0;
}
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ToggleMusic => {
ew_game.send(GameEvent::SetMusic(Next));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ToggleSound => {
ew_game.send(GameEvent::SetSound(Next));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ToggleCamera => {
ew_game.send(GameEvent::SetThirdPerson(Toggle));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ToggleFullscreen => {
ew_game.send(GameEvent::SetFullscreen(Toggle));
}
MenuAction::ToggleShadows => {
ew_game.send(GameEvent::SetShadows(Toggle));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::PhoneCall => {
ew_game.send(GameEvent::PhoneCall);
ew_game.send(GameEvent::SetMenu(Turn::Off));
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::ResetSettings => {
ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::PowerDown));
*settings = Settings::default();
*prefs = Preferences::default();
prefs.save();
ew_game.send(GameEvent::SetShadows(Turn::Unchanged));
ew_game.send(GameEvent::SetFullscreen(Turn::Unchanged));
ew_game.send(GameEvent::SetSound(game::Cycle::Unchanged));
ew_game.send(GameEvent::SetMusic(game::Cycle::Unchanged));
ew_game.send(GameEvent::SetAR(Turn::Unchanged));
ew_game.send(GameEvent::UpdateFlashlight);
ew_updatepointer.send(hud::UpdatePointerEvent);
ew_updateavatar.send(hud::UpdateAvatarEvent);
ew_updatemenu.send(UpdateMenuEvent);
}
MenuAction::Restart => {
settings.god_mode = false;
ew_playerdies.send(game::PlayerDiesEvent(actor::DamageType::Depressurization));
}
MenuAction::Quit => {
app_exit_events.send(AppExit::Success);
}
};
}
}