use crate::app::menu_ui;
use crate::app::net::Online;
use crate::app::palette;
use crate::app::settings::GameSettings;
use crate::app::{Paused, Phase, Screen};
use bevy::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PauseAction {
Continue,
ToMenu,
Quit,
}
impl PauseAction {
pub const ALL: [PauseAction; 3] = [
PauseAction::Continue,
PauseAction::ToMenu,
PauseAction::Quit,
];
}
const OPTIONS: usize = PauseAction::ALL.len();
#[derive(Resource, Default)]
pub struct PauseMenu {
pub open: bool,
held: bool,
selected: usize,
}
#[derive(Component)]
pub struct PauseUi;
#[derive(Component)]
pub struct PauseRow(usize);
fn spawn_card(commands: &mut Commands, settings: &GameSettings) {
let tr = settings.tr();
commands
.spawn((
PauseUi,
GlobalZIndex(menu_ui::layer::CARD),
menu_ui::centred_overlay(),
))
.with_children(|wrap| {
wrap.spawn(menu_ui::screen_card()).with_children(|card| {
card.spawn((
Text::new(tr.pause_title),
TextFont {
font_size: FontSize::Px(30.0),
..default()
},
TextColor(palette::GOLD),
));
card.spawn(Node {
height: Val::Px(6.0),
..default()
});
menu_ui::spawn_rows(card, OPTIONS, 24.0, PauseRow);
});
});
}
fn close(
commands: &mut Commands,
menu: &mut PauseMenu,
paused: &mut Paused,
ui: &Query<Entity, With<PauseUi>>,
) {
menu.open = false;
paused.0 = menu.held;
for entity in ui {
commands.entity(entity).despawn();
}
}
#[allow(clippy::too_many_arguments)]
pub fn pause_input(
mut commands: Commands,
keys: Res<ButtonInput<KeyCode>>,
pads: Query<&Gamepad>,
settings: Res<GameSettings>,
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
mut menu: ResMut<PauseMenu>,
mut paused: ResMut<Paused>,
mut online: ResMut<Online>,
mut next_screen: ResMut<NextState<Screen>>,
mut exit: MessageWriter<AppExit>,
ui: Query<Entity, With<PauseUi>>,
) {
let start_ok = !(*screen.get() == Screen::Puzzle && *phase.get() == Phase::Setup);
let pad_start = start_ok && pads.iter().any(|p| p.just_pressed(GamepadButton::Start));
let done = *screen.get() == Screen::Puzzle && matches!(*phase.get(), Phase::Won | Phase::Lost);
let escape = !done && keys.just_pressed(KeyCode::Escape);
let pad_up = pads.iter().any(|p| p.just_pressed(GamepadButton::DPadUp));
let pad_down = pads.iter().any(|p| p.just_pressed(GamepadButton::DPadDown));
let pad_accept = pads.iter().any(|p| p.just_pressed(GamepadButton::South));
let peer_paused = online.0.as_ref().is_some_and(|s| s.session.paused());
if !menu.open {
if escape || pad_start || peer_paused {
menu.open = true;
menu.selected = 0;
menu.held = paused.0;
match online.0.as_mut() {
Some(session) if !peer_paused => session.request_pause(),
Some(_) => {}
None => paused.0 = true,
}
spawn_card(&mut commands, &settings);
}
return;
}
if keys.just_pressed(KeyCode::Escape) || pad_start {
if let Some(session) = online.0.as_mut() {
session.request_resume();
}
close(&mut commands, &mut menu, &mut paused, &ui);
return;
}
if online.0.is_some() && !peer_paused {
close(&mut commands, &mut menu, &mut paused, &ui);
return;
}
menu.selected = menu_ui::nav(&keys, menu.selected, OPTIONS);
let pad_nav = if pad_up {
menu_ui::Nav::Up
} else if pad_down {
menu_ui::Nav::Down
} else {
menu_ui::Nav::Stay
};
menu.selected = menu_ui::step(pad_nav, menu.selected, OPTIONS);
if menu_ui::enter(&keys) || pad_accept {
match PauseAction::ALL[menu.selected] {
PauseAction::Continue => {
if let Some(session) = online.0.as_mut() {
session.request_resume();
}
close(&mut commands, &mut menu, &mut paused, &ui);
}
PauseAction::ToMenu => {
if let Some(session) = online.0.as_mut() {
session.request_resume();
}
close(&mut commands, &mut menu, &mut paused, &ui);
next_screen.set(Screen::Menu);
}
PauseAction::Quit => {
exit.write(AppExit::Success);
}
}
}
}
pub fn update_pause_rows(
menu: Res<PauseMenu>,
settings: Res<GameSettings>,
mut rows: Query<(&PauseRow, &mut Text, &mut TextColor)>,
) {
let tr = settings.tr();
let labels = [tr.pause_continue, tr.pause_to_menu, tr.pause_quit];
for (row, mut text, mut color) in &mut rows {
menu_ui::paint_row(row.0 == menu.selected, labels[row.0], &mut text, &mut color);
}
}
pub fn reset_pause(
mut commands: Commands,
mut menu: ResMut<PauseMenu>,
mut paused: ResMut<Paused>,
ui: Query<Entity, With<PauseUi>>,
) {
close(&mut commands, &mut menu, &mut paused, &ui);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::Screen;
#[test]
fn escape_opens_the_card_and_the_menu_row_leaves() {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
app.add_systems(Update, pause_input);
let tap = |app: &mut App, key: KeyCode| {
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.reset_all();
keys.press(key);
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.reset_all();
};
app.insert_resource(State::new(Screen::Versus));
tap(&mut app, KeyCode::Escape);
assert!(app.world().resource::<PauseMenu>().open, "the card opened");
assert!(
app.world().resource::<Paused>().0,
"an offline round freezes with it"
);
let row = |app: &App| PauseAction::ALL[app.world().resource::<PauseMenu>().selected];
tap(&mut app, KeyCode::KeyS);
tap(&mut app, KeyCode::KeyS);
assert_eq!(row(&app), PauseAction::Quit);
tap(&mut app, KeyCode::KeyS);
assert_eq!(row(&app), PauseAction::Continue);
tap(&mut app, KeyCode::KeyW);
assert_eq!(row(&app), PauseAction::Quit);
tap(&mut app, KeyCode::KeyW);
assert_eq!(row(&app), PauseAction::ToMenu);
tap(&mut app, KeyCode::Enter);
app.update(); assert!(!app.world().resource::<PauseMenu>().open, "the card closed");
assert!(!app.world().resource::<Paused>().0, "and unfroze");
assert_eq!(*app.world().resource::<State<Screen>>().get(), Screen::Menu);
}
#[test]
fn the_card_takes_the_keyboard_while_it_is_up() {
use crate::app::conditions::keys_are_free;
use crate::app::cursor::{Cursor, move_cursor};
use crate::app::play_input::versus_input;
use crate::app::{PendingActions, PlacementDenied, Sim, VersusPhase};
use crate::sim::classic_arena;
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.init_state::<VersusPhase>();
app.insert_resource(State::new(Screen::Versus));
app.insert_resource(Sim(classic_arena(false, 2)));
app.insert_resource(GameSettings::default());
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<Time>();
app.init_resource::<PendingActions>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.add_message::<PlacementDenied>();
app.add_message::<AppExit>();
app.world_mut()
.spawn((Cursor::seated(0), Transform::default()));
app.add_systems(
Update,
(
pause_input,
(move_cursor, versus_input).run_if(keys_are_free),
)
.chain(),
);
let tap = |app: &mut App, key: KeyCode| {
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.reset_all();
keys.press(key);
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.reset_all();
};
let cursor = |app: &mut App| {
let mut q = app.world_mut().query::<&Cursor>();
let c = q.iter(app.world()).next().expect("one cursor");
(c.x, c.y)
};
tap(&mut app, KeyCode::Escape);
assert!(app.world().resource::<PauseMenu>().open, "the card opened");
let parked = cursor(&mut app);
tap(&mut app, KeyCode::KeyS);
assert_eq!(
PauseAction::ALL[app.world().resource::<PauseMenu>().selected],
PauseAction::ToMenu
);
assert_eq!(cursor(&mut app), parked, "the cursor stayed put");
tap(&mut app, KeyCode::ArrowUp);
assert_eq!(
app.world().resource::<PendingActions>().0[0],
crate::sim::PlayerAction::None,
"no placement waiting behind the card"
);
tap(&mut app, KeyCode::Escape);
assert!(!app.world().resource::<PauseMenu>().open, "the card closed");
tap(&mut app, KeyCode::KeyS);
assert_ne!(cursor(&mut app), parked, "the cursor moves again");
}
#[test]
fn the_card_stays_shut_on_a_finished_puzzle() {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
app.add_systems(Update, pause_input);
app.insert_resource(State::new(Screen::Puzzle));
for phase in [Phase::Won, Phase::Lost] {
app.insert_resource(State::new(phase));
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.reset_all();
keys.press(KeyCode::Escape);
app.update();
assert!(!app.world().resource::<PauseMenu>().open, "{phase:?}");
assert!(!app.world().resource::<Paused>().0, "{phase:?}");
}
app.insert_resource(State::new(Phase::Running));
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.reset_all();
keys.press(KeyCode::Escape);
app.update();
assert!(app.world().resource::<PauseMenu>().open);
}
#[test]
fn continue_unfreezes_a_running_puzzle() {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
app.add_message::<crate::app::LoadLevel>();
app.add_systems(
Update,
(crate::app::play_input::running_input, pause_input).chain(),
);
app.insert_resource(State::new(Screen::Puzzle));
app.insert_resource(State::new(Phase::Running));
let tap = |app: &mut App, key: KeyCode| {
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.reset_all();
keys.press(key);
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.reset_all();
};
tap(&mut app, KeyCode::Escape);
assert!(app.world().resource::<PauseMenu>().open, "the card opened");
assert!(app.world().resource::<Paused>().0, "and froze the round");
tap(&mut app, KeyCode::Enter);
assert!(
!app.world().resource::<PauseMenu>().open,
"Continue closed it"
);
assert!(
!app.world().resource::<Paused>().0,
"and the round runs again"
);
tap(&mut app, KeyCode::Escape);
assert!(app.world().resource::<PauseMenu>().open);
tap(&mut app, KeyCode::Escape);
assert!(!app.world().resource::<PauseMenu>().open);
assert!(
!app.world().resource::<Paused>().0,
"Escape out is Continue too"
);
}
}