use crate::app::gamepad::scheme;
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,
Restart,
ToMenu,
Quit,
}
impl PauseAction {
pub fn rows(restart: bool) -> &'static [PauseAction] {
match restart {
true => &[
PauseAction::Continue,
PauseAction::Restart,
PauseAction::ToMenu,
PauseAction::Quit,
],
false => &[
PauseAction::Continue,
PauseAction::ToMenu,
PauseAction::Quit,
],
}
}
fn label(self, tr: &crate::app::i18n::Tr) -> &'static str {
match self {
PauseAction::Continue => tr.pause_continue,
PauseAction::Restart => tr.pause_restart,
PauseAction::ToMenu => tr.pause_to_menu,
PauseAction::Quit => tr.pause_quit,
}
}
}
#[derive(Resource, Default)]
pub struct PauseMenu {
card: Option<OpenCard>,
}
#[derive(Clone, Copy, Debug)]
struct OpenCard {
held: bool,
own: bool,
restart: bool,
selected: usize,
}
impl OpenCard {
fn rows(&self) -> &'static [PauseAction] {
PauseAction::rows(self.restart)
}
fn chosen(&self) -> PauseAction {
self.rows()[self.selected.min(self.rows().len() - 1)]
}
}
impl PauseMenu {
pub fn is_open(&self) -> bool {
self.card.is_some()
}
pub fn stops_the_round(&self) -> bool {
self.card.is_some_and(|card| !card.own)
}
#[cfg(test)]
pub(crate) fn raised() -> PauseMenu {
PauseMenu {
card: Some(OpenCard {
held: false,
own: false,
restart: false,
selected: 0,
}),
}
}
}
#[derive(Component)]
pub struct PauseUi;
#[derive(Component)]
pub struct PauseRow(usize);
fn spawn_card(commands: &mut Commands, settings: &GameSettings, own: bool, rows: usize) {
let tr = settings.tr();
let title = match own {
true => tr.pause_menu_title,
false => tr.pause_title,
};
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(title),
menu_ui::display_font(30.0),
TextColor(palette::GOLD),
));
card.spawn(menu_ui::spacer(6.0));
menu_ui::spawn_rows(card, rows, 24.0, PauseRow);
});
});
}
fn close(
commands: &mut Commands,
menu: &mut PauseMenu,
paused: &mut Paused,
ui: &Query<Entity, With<PauseUi>>,
) {
paused.0 = menu.card.take().is_some_and(|card| card.held);
for entity in ui {
commands.entity(entity).despawn();
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct PauseCard<'w, 's> {
commands: Commands<'w, 's>,
menu: ResMut<'w, PauseMenu>,
paused: ResMut<'w, Paused>,
ui: Query<'w, 's, Entity, With<PauseUi>>,
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct Leaving<'w> {
next_screen: ResMut<'w, NextState<Screen>>,
exit: MessageWriter<'w, AppExit>,
again: crate::app::rematch::PlayAgain<'w>,
tournament: ResMut<'w, crate::app::tournament::Tournament>,
vphase: Res<'w, State<crate::app::VersusPhase>>,
}
pub fn pause_input(
card: PauseCard,
buttons: menu_ui::Buttons,
settings: Res<GameSettings>,
screen: Res<State<Screen>>,
phase: Res<State<Phase>>,
mut online: ResMut<Online>,
leaving: Leaving,
) {
let PauseCard {
mut commands,
mut menu,
mut paused,
ui,
} = card;
let keys = &*buttons.keys;
let Leaving {
mut next_screen,
mut exit,
mut again,
mut tournament,
vphase,
} = leaving;
let face = &buttons.face.0;
let pad = |does| scheme::pressed(face, does, buttons.pads.iter());
let pad_as = |control, does| scheme::pressed_as(face, control, does, buttons.pads.iter());
let pad_start = pad(scheme::Does::Pause) || pad(scheme::Does::OpenMenu);
let done = crate::app::conditions::puzzle_settled(screen.get(), phase.get());
let escape = !done && keys.just_pressed(KeyCode::Escape);
let pad_up = pad_as(scheme::Pad::Up, scheme::Does::Choose);
let pad_down = pad_as(scheme::Pad::Down, scheme::Does::Choose);
let pad_accept = pad(scheme::Does::Confirm);
let pad_close = pad(scheme::Does::GoOn);
let peer_paused = online.0.as_ref().is_some_and(|s| s.session.paused());
let Some(mut open) = menu.card else {
if escape || pad_start || peer_paused {
let held = paused.0;
let own = match online.0.as_mut() {
Some(session) if !peer_paused => {
let own = session.watching();
session.request_pause();
own
}
Some(_) => false,
None => {
paused.0 = true;
false
}
};
let restart = *screen.get() == Screen::Versus
&& crate::app::rematch::restart_offered(
&again.table,
&online,
&tournament,
*vphase.get(),
);
let card = OpenCard {
held,
own,
restart,
selected: 0,
};
menu.card = Some(card);
spawn_card(&mut commands, &settings, own, card.rows().len());
}
return;
};
if keys.just_pressed(KeyCode::Escape) || pad_close {
resume(&mut online);
close(&mut commands, &mut menu, &mut paused, &ui);
return;
}
if online.0.is_some() && !peer_paused && !open.own {
close(&mut commands, &mut menu, &mut paused, &ui);
return;
}
let options = open.rows().len();
let before = open.selected;
open.selected = menu_ui::nav(keys, open.selected, options);
let pad_nav = if open.selected != before {
menu_ui::Nav::Stay
} else if pad_up {
menu_ui::Nav::Up
} else if pad_down {
menu_ui::Nav::Down
} else {
menu_ui::Nav::Stay
};
open.selected = menu_ui::step(pad_nav, open.selected, options);
menu.card = Some(open);
if menu_ui::enter(keys) || pad_accept {
match open.chosen() {
PauseAction::Continue => {
resume(&mut online);
close(&mut commands, &mut menu, &mut paused, &ui);
}
PauseAction::Restart => {
close(&mut commands, &mut menu, &mut paused, &ui);
let restart = crate::app::rematch::Again::Restart;
again.ask(restart, &mut tournament, &mut next_screen);
}
PauseAction::ToMenu => {
resume(&mut online);
close(&mut commands, &mut menu, &mut paused, &ui);
next_screen.set(Screen::Menu);
}
PauseAction::Quit => {
resume(&mut online);
exit.write(AppExit::Success);
}
}
}
}
fn resume(online: &mut Online) {
if let Some(session) = online.0.as_mut() {
session.request_resume();
}
}
pub fn update_pause_rows(
menu: Res<PauseMenu>,
settings: Res<GameSettings>,
mut rows: Query<(&PauseRow, &mut Text, &mut TextColor)>,
) {
let tr = settings.tr();
let Some(card) = menu.card else {
return;
};
for (row, mut text, mut color) in &mut rows {
let Some(action) = card.rows().get(row.0) else {
continue;
};
menu_ui::paint_row(
row.0 == card.selected,
action.label(tr),
&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;
fn add_restart_sources(app: &mut App) {
app.init_resource::<crate::app::rematch::LocalTable>();
app.init_resource::<crate::app::match_setup::MatchConfig>();
app.init_resource::<crate::app::tournament::Tournament>();
if !app
.world()
.contains_resource::<State<crate::app::VersusPhase>>()
{
app.init_state::<crate::app::VersusPhase>();
}
}
#[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::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
add_restart_sources(&mut app);
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>().is_open(),
"the card opened"
);
assert!(
app.world().resource::<Paused>().0,
"an offline round freezes with it"
);
let row = |app: &App| {
app.world()
.resource::<PauseMenu>()
.card
.expect("the card is up")
.chosen()
};
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>().is_open(),
"the card closed"
);
assert!(!app.world().resource::<Paused>().0, "and unfroze");
assert_eq!(*app.world().resource::<State<Screen>>().get(), Screen::Menu);
}
#[test]
fn a_pad_works_the_card_by_its_table() {
use crate::app::gamepad::{Face, PadFace};
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::<PadFace>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
add_restart_sources(&mut app);
let face = |menu: Res<PauseMenu>, mut face: ResMut<PadFace>, mut under: Local<Face>| {
if !menu.is_open() {
*under = face.0.clone();
}
face.0 = match menu.is_open() {
true => Face::Paused,
false => under.clone(),
};
};
app.add_systems(Update, (face, pause_input).chain());
let pad = app.world_mut().spawn(Gamepad::default()).id();
let tap = |app: &mut App, button: GamepadButton| {
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("the pad");
gamepad.digital_mut().clear();
gamepad.digital_mut().press(button);
app.update();
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("the pad");
gamepad.digital_mut().release(button);
gamepad.digital_mut().clear();
app.update();
};
let open = |app: &App| app.world().resource::<PauseMenu>().is_open();
app.insert_resource(State::new(Screen::Puzzle));
app.insert_resource(PadFace(Face::PuzzleSetup {
posts: true,
hint: false,
}));
tap(&mut app, GamepadButton::Start);
assert!(!open(&app), "Start begins the run in setup");
tap(&mut app, GamepadButton::Select);
assert!(open(&app), "Select opens the card there");
tap(&mut app, GamepadButton::DPadDown);
let row = app
.world()
.resource::<PauseMenu>()
.card
.expect("up")
.chosen();
assert_eq!(row, PauseAction::ToMenu);
tap(&mut app, GamepadButton::East);
assert!(!open(&app), "B puts it away");
app.insert_resource(State::new(Screen::Versus));
app.insert_resource(PadFace(Face::Round));
tap(&mut app, GamepadButton::Start);
assert!(open(&app), "Start opens it over a round");
tap(&mut app, GamepadButton::Start);
assert!(!open(&app), "and puts it away");
app.insert_resource(PadFace(Face::Results {
door: crate::app::play_input::AfterRound::Menu,
talk: false,
rematch: None,
keep: crate::app::replays::KeepOffer::Nothing,
}));
tap(&mut app, GamepadButton::South);
assert!(!open(&app), "A on the results card is not a pause");
app.insert_resource(PadFace(Face::Replay));
tap(&mut app, GamepadButton::Start);
assert!(open(&app), "Start opens it over a replay");
}
#[test]
fn a_spectators_card_stays_up() {
use crate::app::net::OnlineSession;
use crate::sim::{DEFAULT_DELAY, Lockstep};
use crate::transport::{MatchTerms, UdpTransport};
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.insert_resource(State::new(Screen::Versus));
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
add_restart_sources(&mut app);
let watcher = OnlineSession::new(
UdpTransport::host(0).expect("socket"),
Lockstep::observer(vec![0, 1], DEFAULT_DELAY),
2,
MatchTerms::default(),
);
assert!(watcher.watching());
app.insert_resource(Online(Some(watcher)));
app.add_systems(Update, pause_input);
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(KeyCode::Escape);
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.reset_all();
for _ in 0..3 {
app.update();
}
assert!(app.world().resource::<PauseMenu>().is_open(), "still up");
}
#[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::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
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>();
add_restart_sources(&mut app);
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>().is_open(),
"the card opened"
);
let parked = cursor(&mut app);
tap(&mut app, KeyCode::KeyS);
assert_eq!(
app.world()
.resource::<PauseMenu>()
.card
.expect("the card is up")
.chosen(),
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>().is_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::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
add_restart_sources(&mut app);
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>().is_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>().is_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::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::cursor::LiveSeatKeys>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
add_restart_sources(&mut app);
app.add_message::<crate::app::LoadLevel>();
let levels = crate::sim::campaign_levels();
app.insert_resource(crate::app::Sim(levels[0].board()));
app.insert_resource(crate::app::Campaign::tide_pool_of(levels));
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>().is_open(),
"the card opened"
);
assert!(app.world().resource::<Paused>().0, "and froze the round");
tap(&mut app, KeyCode::Enter);
assert!(
!app.world().resource::<PauseMenu>().is_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>().is_open());
tap(&mut app, KeyCode::Escape);
assert!(!app.world().resource::<PauseMenu>().is_open());
assert!(
!app.world().resource::<Paused>().0,
"Escape out is Continue too"
);
}
#[test]
fn a_couch_round_can_be_restarted_from_the_card() {
for set_here in [true, false] {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.insert_resource(State::new(Screen::Versus));
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<PauseMenu>();
app.init_resource::<Paused>();
app.init_resource::<Online>();
app.insert_resource(GameSettings::default());
app.add_message::<AppExit>();
add_restart_sources(&mut app);
let config = crate::app::match_setup::MatchConfig::default();
app.world_mut()
.resource_mut::<crate::app::rematch::LocalTable>()
.note_round(set_here, &config, &Default::default());
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();
};
tap(&mut app, KeyCode::Escape);
let card = app.world().resource::<PauseMenu>().card.expect("up");
assert_eq!(card.rows().contains(&PauseAction::Restart), set_here);
if !set_here {
continue;
}
tap(&mut app, KeyCode::KeyS);
let card = app.world().resource::<PauseMenu>().card.expect("up");
assert_eq!(card.chosen(), PauseAction::Restart);
tap(&mut app, KeyCode::Enter);
assert!(!app.world().resource::<PauseMenu>().is_open(), "closed");
assert!(!app.world().resource::<Paused>().0, "and let go");
assert!(
app.world()
.resource::<crate::app::match_setup::MatchConfig>()
.armed
);
assert!(
app.world()
.resource::<crate::app::rematch::LocalTable>()
.keeps_the_table()
);
assert!(matches!(
*app.world().resource::<NextState<Screen>>(),
NextState::Pending(Screen::Versus)
));
}
}
}