use crate::app::cursor::Cursor;
use crate::app::settings::{GameSettings, SeatInput};
use crate::app::{PendingActions, Phase, PlacementDenied, Screen, Sim};
use crate::sim::{Direction, MAX_PLAYERS, PlayerAction};
use bevy::input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest};
use bevy::prelude::*;
#[derive(Resource, Default)]
pub struct PadSeats(pub Vec<Entity>, PlugOrder);
#[derive(Default)]
pub struct PlugOrder(Vec<Entity>);
pub fn keep_pad_order(
pads: Query<Entity, With<Gamepad>>,
screen: Res<State<Screen>>,
mut seats: ResMut<PadSeats>,
) {
let PadSeats(claims, PlugOrder(order)) = &mut *seats;
for pad in &pads {
if order.contains(&pad) {
continue;
}
if let Some(at) = claims.iter().position(|&claimed| !pads.contains(claimed)) {
let gone = std::mem::replace(&mut claims[at], pad);
if let Some(place) = order.iter().position(|&seen| seen == gone) {
order[place] = pad;
continue;
}
}
match order.iter().position(|&seen| !pads.contains(seen)) {
Some(empty) => order[empty] = pad,
None => order.push(pad),
}
}
let in_a_round = matches!(
screen.get(),
Screen::Puzzle | Screen::Versus | Screen::Interlude
);
if !in_a_round {
claims.retain(|&claimed| pads.contains(claimed));
order.retain(|&seen| pads.contains(seen));
}
}
fn nth_pad_entity(
pads: &Query<(Entity, &Gamepad)>,
claims: &PadSeats,
index: usize,
) -> Option<Entity> {
let PadSeats(claimed, PlugOrder(order)) = claims;
let fresh = pads
.iter()
.map(|(entity, _)| entity)
.filter(|pad| !order.contains(pad));
let entity = claimed
.iter()
.copied()
.chain(
order
.iter()
.copied()
.chain(fresh)
.filter(|pad| !claimed.contains(pad)),
)
.nth(index)?;
pads.contains(entity).then_some(entity)
}
fn nth_pad<'a>(
pads: &'a Query<(Entity, &Gamepad)>,
claims: &PadSeats,
index: usize,
) -> Option<&'a Gamepad> {
let entity = nth_pad_entity(pads, claims, index)?;
pads.get(entity).ok().map(|(_, pad)| pad)
}
fn pad_index_of(settings: &GameSettings, players: &[u8], player: u8) -> Option<usize> {
if let [only] = players
&& *only == player
{
return match seat_choice(settings, 0) {
Some(SeatInput::Keys) => None,
Some(SeatInput::Pad(n)) => Some(usize::from(n)),
Some(SeatInput::Auto) | None => Some(0),
};
}
match seat_choice(settings, player) {
Some(SeatInput::Keys) => return None,
Some(SeatInput::Pad(n)) => return Some(usize::from(n)),
Some(SeatInput::Auto) | None => {}
}
let mut autos: Vec<u8> = players
.iter()
.copied()
.filter(|&p| {
!matches!(
seat_choice(settings, p),
Some(SeatInput::Pad(_) | SeatInput::Keys)
)
})
.collect();
autos.sort_unstable();
let rank = autos.iter().rev().position(|&p| p == player)?;
(0..).filter(|index| !named(settings, *index)).nth(rank)
}
fn seat_choice(settings: &GameSettings, player: u8) -> Option<SeatInput> {
settings.seat_input.get(usize::from(player)).copied()
}
fn named(settings: &GameSettings, index: usize) -> bool {
settings
.seat_input
.iter()
.any(|choice| matches!(choice, SeatInput::Pad(n) if usize::from(*n) == index))
}
const KEYBOARD_SEATS: usize = 2;
const PAD_SEATS: usize = MAX_PLAYERS - KEYBOARD_SEATS;
#[derive(Component)]
pub struct PadRepeat(Timer);
const PLACES: [(GamepadButton, Direction); 4] = [
(GamepadButton::North, Direction::Up),
(GamepadButton::South, Direction::Down),
(GamepadButton::West, Direction::Left),
(GamepadButton::East, Direction::Right),
];
fn held_direction(pad: &Gamepad, deadzone: f32) -> (i16, i16) {
let mut dx = 0i16;
let mut dy = 0i16;
if pad.pressed(GamepadButton::DPadUp) {
dy -= 1;
}
if pad.pressed(GamepadButton::DPadDown) {
dy += 1;
}
if pad.pressed(GamepadButton::DPadLeft) {
dx -= 1;
}
if pad.pressed(GamepadButton::DPadRight) {
dx += 1;
}
let stick_x = pad.get(GamepadAxis::LeftStickX).unwrap_or(0.0);
let stick_y = pad.get(GamepadAxis::LeftStickY).unwrap_or(0.0);
if stick_x < -deadzone {
dx -= 1;
}
if stick_x > deadzone {
dx += 1;
}
if stick_y > deadzone {
dy -= 1;
}
if stick_y < -deadzone {
dy += 1;
}
(dx.signum(), dy.signum())
}
pub fn pad_move_cursor(
pads: Query<(Entity, &Gamepad)>,
seats: Res<PadSeats>,
time: Res<Time>,
sim: Res<Sim>,
settings: Res<GameSettings>,
mut commands: Commands,
mut cursors: Query<(Entity, &mut Cursor, Option<&mut PadRepeat>)>,
) {
let board = &sim.0;
let deadzone = settings.deadzone();
let mut players: Vec<u8> = cursors.iter().map(|(_, c, ..)| c.player).collect();
players.sort_unstable();
for (entity, mut cursor, repeat) in &mut cursors {
let Some(pad) = pad_index_of(&settings, &players, cursor.player)
.and_then(|i| nth_pad(&pads, &seats, i))
else {
continue;
};
let (dx, dy) = held_direction(pad, deadzone);
let Some(mut repeat) = repeat else {
commands
.entity(entity)
.insert(PadRepeat(Timer::from_seconds(0.0, TimerMode::Once)));
continue;
};
if dx == 0 && dy == 0 {
repeat.0 = Timer::from_seconds(0.0, TimerMode::Once);
continue;
}
repeat.0.tick(time.delta());
if !repeat.0.is_finished() {
continue;
}
let first_step = repeat.0.duration().is_zero();
repeat.0 = Timer::from_seconds(
if first_step {
settings.repeat_delay
} else {
settings.repeat_interval
},
TimerMode::Once,
);
let nx = (i16::from(cursor.x) + dx).clamp(0, i16::from(board.width()) - 1) as u8;
let ny = (i16::from(cursor.y) + dy).clamp(0, i16::from(board.height()) - 1) as u8;
cursor.x = nx;
cursor.y = ny;
}
}
pub fn pad_versus_input(
pads: Query<(Entity, &Gamepad)>,
seats: Res<PadSeats>,
settings: Res<GameSettings>,
sim: Res<Sim>,
mut pending: ResMut<PendingActions>,
mut denied: MessageWriter<PlacementDenied>,
mut cursors: Query<&mut Cursor>,
) {
let board = &sim.0;
let mut players: Vec<u8> = cursors.iter().map(|c| c.player).collect();
players.sort_unstable();
for mut cursor in &mut cursors {
let Some(pad) = pad_index_of(&settings, &players, cursor.player)
.and_then(|i| nth_pad(&pads, &seats, i))
else {
continue;
};
let p = cursor.player as usize;
for (button, dir) in PLACES {
if pad.just_pressed(button) {
if !board.can_place_signpost(cursor.player, cursor.x, cursor.y) {
cursor.flash = 0.25;
denied.write(PlacementDenied {
player: cursor.player,
out_of_signposts: board.out_of_signposts(cursor.player, cursor.x, cursor.y),
});
continue;
}
pending.0[p] = PlayerAction::Place {
x: cursor.x,
y: cursor.y,
dir,
};
}
}
if pad.just_pressed(GamepadButton::LeftTrigger) {
pending.0[p] = PlayerAction::Remove {
x: cursor.x,
y: cursor.y,
};
}
if pad.pressed(GamepadButton::RightTrigger)
&& let Some((x, y)) = board.first_signpost_of(cursor.player)
{
pending.0[p] = PlayerAction::Remove { x, y };
}
}
}
pub fn pad_setup_input(
pads: Query<(Entity, &Gamepad)>,
seats: Res<PadSeats>,
settings: Res<GameSettings>,
mut sim: ResMut<Sim>,
mut next_phase: ResMut<NextState<Phase>>,
mut denied: MessageWriter<PlacementDenied>,
mut cursors: Query<&mut Cursor>,
) {
let mut players: Vec<u8> = cursors.iter().map(|c| c.player).collect();
players.sort_unstable();
for mut cursor in &mut cursors {
let Some(pad) = pad_index_of(&settings, &players, cursor.player)
.and_then(|i| nth_pad(&pads, &seats, i))
else {
continue;
};
for (button, dir) in PLACES {
if pad.just_pressed(button) {
let spent = sim.0.out_of_signposts(0, cursor.x, cursor.y);
if !sim.0.place_signpost(0, cursor.x, cursor.y, dir) {
cursor.flash = 0.25;
denied.write(PlacementDenied {
player: 0,
out_of_signposts: spent,
});
}
}
}
if pad.just_pressed(GamepadButton::LeftTrigger) {
let _ = sim.0.remove_signpost(0, cursor.x, cursor.y);
}
if pad.just_pressed(GamepadButton::RightTrigger) {
crate::app::play_input::clear_setup(&mut sim);
}
if pad.just_pressed(GamepadButton::Start) {
next_phase.set(Phase::Running);
}
}
}
pub fn pad_menu_bridge(
pads: Query<&Gamepad>,
screen: Res<State<Screen>>,
mut keys: ResMut<ButtonInput<KeyCode>>,
mut bridged: ResMut<Bridged>,
) {
let quits = *screen.get() == Screen::Menu;
let lobby = *screen.get() == Screen::Lobby;
for pad in &pads {
for (button, key, lobby_key) in BRIDGE {
let muted = quits && button == GamepadButton::East;
if pad.just_pressed(button) && !muted {
let key = if lobby { lobby_key } else { key };
keys.press(key);
if !bridged.0.contains(&key) {
bridged.0.push(key);
}
}
}
}
}
const BRIDGE: [(GamepadButton, KeyCode, KeyCode); 6] = [
(GamepadButton::DPadUp, KeyCode::KeyW, KeyCode::ArrowUp),
(GamepadButton::DPadDown, KeyCode::KeyS, KeyCode::ArrowDown),
(GamepadButton::DPadLeft, KeyCode::KeyA, KeyCode::ArrowLeft),
(GamepadButton::DPadRight, KeyCode::KeyD, KeyCode::ArrowRight),
(GamepadButton::South, KeyCode::Enter, KeyCode::Enter),
(GamepadButton::East, KeyCode::Escape, KeyCode::Escape),
];
#[derive(Resource, Default)]
pub struct Bridged(Vec<KeyCode>);
pub fn pad_bridge_release(
pads: Query<&Gamepad>,
mut keys: ResMut<ButtonInput<KeyCode>>,
mut bridged: ResMut<Bridged>,
) {
if bridged.0.is_empty() {
return;
}
let held = |key: KeyCode| {
pads.iter().any(|pad| {
BRIDGE
.iter()
.any(|&(button, menu, lobby)| (menu == key || lobby == key) && pad.pressed(button))
})
};
let mut kept = Vec::with_capacity(bridged.0.len());
for &key in &bridged.0 {
match held(key) {
true => kept.push(key),
false => keys.release(key),
}
}
bridged.0 = kept;
}
pub fn pad_claim_seats(
pads: Query<(Entity, &Gamepad)>,
mut seats: ResMut<PadSeats>,
mut config: ResMut<crate::app::match_setup::MatchConfig>,
) {
seats.0.retain(|&entity| pads.get(entity).is_ok());
for (entity, pad) in &pads {
if pad.just_pressed(GamepadButton::Start)
&& !seats.0.contains(&entity)
&& seats.0.len() < PAD_SEATS
{
seats.0.push(entity);
if config.bots > 0 {
config.bots -= 1;
} else {
config.seats = (config.seats + 1).min(MAX_PLAYERS as u8);
}
}
}
}
const RAID_RUMBLE: GamepadRumbleIntensity = GamepadRumbleIntensity {
strong_motor: 0.85,
weak_motor: 0.45,
};
const RAID_RUMBLE_SECS: f32 = 0.32;
pub fn rumble_on_raid(
mut events: MessageReader<crate::app::sim_events::SimEvent>,
settings: Res<GameSettings>,
claims: Res<PadSeats>,
pads: Query<(Entity, &Gamepad)>,
cursors: Query<&Cursor>,
mut seated: Local<Vec<u8>>,
mut rumble: MessageWriter<GamepadRumbleRequest>,
) {
use crate::app::sim_events::SimEvent;
if !settings.rumble || events.is_empty() {
return;
}
seated.clear();
seated.extend(cursors.iter().map(|cursor| cursor.player));
seated.sort_unstable();
for event in events.read() {
let SimEvent::CastleRaided { owner, .. } = event else {
continue;
};
if seated.binary_search(owner).is_err() {
continue;
}
let Some(pad) = pad_index_of(&settings, &seated, *owner)
.and_then(|index| nth_pad_entity(&pads, &claims, index))
else {
continue;
};
rumble.write(GamepadRumbleRequest::Add {
duration: std::time::Duration::from_secs_f32(RAID_RUMBLE_SECS),
intensity: RAID_RUMBLE,
gamepad: pad,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::settings::{GameSettings, SeatInput};
use crate::app::sim_events::SimEvent;
use crate::sim::classic_arena;
fn with(choices: [SeatInput; 2]) -> GameSettings {
GameSettings {
seat_input: choices,
..GameSettings::default()
}
}
#[test]
fn pads_map_top_down() {
let auto = with([SeatInput::Auto; 2]);
assert_eq!(pad_index_of(&auto, &[0, 1], 1), Some(0));
assert_eq!(pad_index_of(&auto, &[0, 1], 0), Some(1));
assert_eq!(pad_index_of(&auto, &[0, 1, 2, 3], 3), Some(0));
assert_eq!(pad_index_of(&auto, &[0, 1, 2, 3], 0), Some(3));
assert_eq!(pad_index_of(&auto, &[2], 2), Some(0), "online single seat");
assert_eq!(pad_index_of(&auto, &[0, 1], 2), None, "no cursor, no pad");
}
#[test]
fn a_lone_cursor_plays_by_the_first_row() {
let pad_for_p1 = with([SeatInput::Auto, SeatInput::Keys]);
assert_eq!(pad_index_of(&pad_for_p1, &[1], 1), Some(0));
let keys_for_p1 = with([SeatInput::Keys, SeatInput::Auto]);
assert_eq!(pad_index_of(&keys_for_p1, &[1], 1), None);
let named = with([SeatInput::Pad(2), SeatInput::Keys]);
assert_eq!(pad_index_of(&named, &[3], 3), Some(2));
}
#[test]
fn a_named_controller_belongs_to_the_seat_that_named_it() {
let mine = with([SeatInput::Pad(0), SeatInput::Auto]);
assert_eq!(pad_index_of(&mine, &[0, 1], 0), Some(0), "P1 asked for it");
assert_eq!(
pad_index_of(&mine, &[0, 1], 1),
Some(1),
"P2 takes the next one free, not the claimed one"
);
}
#[test]
fn keyboard_only_gives_its_pad_up() {
let quiet = with([SeatInput::Auto, SeatInput::Keys]);
assert_eq!(pad_index_of(&quiet, &[0, 1], 1), None);
assert_eq!(pad_index_of(&quiet, &[0, 1], 0), Some(0));
}
#[test]
fn two_seats_may_name_the_same_controller() {
let both = with([SeatInput::Pad(1), SeatInput::Pad(1)]);
assert_eq!(pad_index_of(&both, &[0, 1], 0), Some(1));
assert_eq!(pad_index_of(&both, &[0, 1], 1), Some(1));
assert_eq!(pad_index_of(&both, &[0, 1, 2], 2), Some(0));
}
#[test]
fn a_claim_takes_a_seat_from_the_ai() {
use crate::app::match_setup::MatchConfig;
let mut app = App::new();
app.init_resource::<PadSeats>();
app.insert_resource(MatchConfig {
seats: 4,
bots: 2,
..MatchConfig::default()
});
for _ in 0..6 {
app.world_mut().spawn(Gamepad::default());
}
app.add_systems(Update, pad_claim_seats);
let pads: Vec<Entity> = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.iter(app.world())
.collect();
let start = |app: &mut App, pad: Entity| {
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("pad");
gamepad.digital_mut().press(GamepadButton::Start);
app.update();
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("pad");
gamepad.digital_mut().release(GamepadButton::Start);
gamepad.digital_mut().clear();
};
start(&mut app, pads[0]);
let config = app.world().resource::<MatchConfig>();
assert_eq!(
(config.seats, config.bots),
(4, 1),
"seat three is a person"
);
for &pad in &pads[1..] {
start(&mut app, pad);
}
assert_eq!(app.world().resource::<PadSeats>().0.len(), PAD_SEATS);
let config = app.world().resource::<MatchConfig>();
assert_eq!((config.seats, config.bots), (6, 0));
app.insert_resource(MatchConfig {
seats: 2,
bots: 1,
..MatchConfig::default()
});
app.world_mut().resource_mut::<PadSeats>().0.clear();
start(&mut app, pads[0]);
let config = app.world().resource::<MatchConfig>();
assert_eq!((config.seats, config.bots), (2, 0), "two people, no AI");
}
fn table(seats: u8, pads: usize) -> App {
let mut app = App::new();
app.insert_resource(Sim(classic_arena(false, seats.max(2))));
app.insert_resource(GameSettings::default());
app.init_resource::<PadSeats>();
app.add_message::<SimEvent>();
app.add_message::<GamepadRumbleRequest>();
for player in 0..seats {
app.world_mut().spawn(Cursor::seated(player));
}
for _ in 0..pads {
app.world_mut().spawn(Gamepad::default());
}
app.add_systems(Update, rumble_on_raid);
app
}
fn buzzed(app: &mut App) -> Vec<Entity> {
let mut messages = app
.world_mut()
.resource_mut::<Messages<GamepadRumbleRequest>>();
let out: Vec<Entity> = messages.drain().map(|request| request.gamepad()).collect();
out
}
fn raid(app: &mut App, owner: u8) -> Vec<Entity> {
app.world_mut().write_message(SimEvent::CastleRaided {
owner,
pos: Vec2::ZERO,
lost: 4,
});
app.update();
buzzed(app)
}
#[test]
fn a_raid_is_felt_by_the_seat_that_was_raided() {
let mut app = table(2, 1);
let pad = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.single(app.world())
.expect("one pad");
assert_eq!(raid(&mut app, 1), vec![pad], "seat two holds the pad");
assert!(
raid(&mut app, 0).is_empty(),
"seat one is on the keyboard and has nothing to buzz"
);
}
#[test]
fn an_unplugged_pad_does_not_move_the_others_up() {
let mut app = table(2, 2);
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.insert_resource(State::new(Screen::Versus));
app.add_systems(Update, keep_pad_order.before(rumble_on_raid));
app.update();
let top = raid(&mut app, 1);
let bottom = raid(&mut app, 0);
assert_eq!((top.len(), bottom.len()), (1, 1));
assert_ne!(top, bottom);
app.world_mut().entity_mut(top[0]).remove::<Gamepad>();
assert_eq!(raid(&mut app, 0), bottom, "P1 keeps its own pad");
assert!(raid(&mut app, 1).is_empty(), "P2's seat waits, empty");
app.world_mut()
.entity_mut(top[0])
.insert(Gamepad::default());
assert_eq!(raid(&mut app, 1), top, "and P2's pad comes back to it");
assert_eq!(raid(&mut app, 0), bottom);
}
#[test]
fn a_new_pad_takes_over_a_dead_claim() {
let mut app = table(2, 2);
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.insert_resource(State::new(Screen::Versus));
app.add_systems(Update, keep_pad_order.before(rumble_on_raid));
let plugged: Vec<Entity> = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.iter(app.world())
.collect();
app.world_mut().resource_mut::<PadSeats>().0 = plugged.clone();
app.update();
assert_eq!(
raid(&mut app, 1),
vec![plugged[0]],
"the first claim, on top"
);
app.world_mut().entity_mut(plugged[0]).remove::<Gamepad>();
let spare = app.world_mut().spawn(Gamepad::default()).id();
app.update();
assert_eq!(raid(&mut app, 1), vec![spare], "the spare takes P2");
assert_eq!(raid(&mut app, 0), vec![plugged[1]], "and P1 keeps its own");
}
#[test]
fn two_pads_are_told_apart() {
let mut app = table(2, 2);
let top = raid(&mut app, 1);
let next = raid(&mut app, 0);
assert_eq!(top.len(), 1);
assert_eq!(next.len(), 1);
assert_ne!(top, next, "one raid must not buzz both hands");
}
#[test]
fn a_claimed_pad_beats_the_order_it_was_plugged_in() {
let mut app = table(2, 2);
let plugged: Vec<Entity> = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.iter(app.world())
.collect();
assert_eq!(plugged.len(), 2);
let claimed: Vec<Entity> = plugged.iter().copied().rev().collect();
app.world_mut().resource_mut::<PadSeats>().0 = claimed.clone();
assert_eq!(
raid(&mut app, 1),
vec![claimed[0]],
"the top seat holds whichever pad claimed first"
);
}
#[test]
fn a_bound_pad_still_only_answers_for_a_seat_that_is_here() {
let mut app = table(1, 2);
app.world_mut().resource_mut::<GameSettings>().seat_input[1] = SeatInput::Pad(0);
assert!(
raid(&mut app, 1).is_empty(),
"seat two is bound to a pad here but is not sitting here"
);
assert_eq!(
raid(&mut app, 0).len(),
1,
"and seat one still feels its own, on the pad seat two did not claim"
);
}
#[test]
fn a_seat_that_is_not_here_buzzes_nothing() {
let mut app = table(2, 1);
assert!(
raid(&mut app, 3).is_empty(),
"seat four is not at this table"
);
}
#[test]
fn the_setting_turns_it_off() {
let mut app = table(2, 1);
app.world_mut().resource_mut::<GameSettings>().rumble = false;
assert!(raid(&mut app, 1).is_empty());
}
fn bridged(screen: Screen) -> App {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.insert_resource(State::new(screen));
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<Bridged>();
app.world_mut().spawn(Gamepad::default());
app.add_systems(
Update,
(
pad_menu_bridge.run_if(not(in_state(Screen::Puzzle))),
pad_bridge_release,
)
.chain(),
);
app
}
fn press(app: &mut App, button: GamepadButton) {
let pad = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.single(app.world())
.expect("one pad");
app.world_mut()
.get_mut::<Gamepad>(pad)
.expect("the pad")
.digital_mut()
.press(button);
app.update();
}
#[test]
fn east_does_not_quit_from_the_menu() {
let mut app = bridged(Screen::Menu);
press(&mut app, GamepadButton::East);
assert!(
!app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::Escape),
"B on the menu must not synthesize the key that leaves the game"
);
}
#[test]
fn east_still_goes_back_from_a_sub_screen() {
for screen in [Screen::Settings, Screen::Controls, Screen::StageSelect] {
let mut app = bridged(screen);
press(&mut app, GamepadButton::East);
assert!(
app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::Escape),
"B is the way back from {screen:?}"
);
}
}
#[test]
fn a_press_that_changes_screen_still_releases_its_key() {
let mut app = bridged(Screen::Settings);
let pad = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.single(app.world())
.expect("one pad");
app.world_mut()
.get_mut::<Gamepad>(pad)
.expect("the pad")
.digital_mut()
.press(GamepadButton::East);
app.update();
assert!(
app.world()
.resource::<ButtonInput<KeyCode>>()
.just_pressed(KeyCode::Escape),
"B goes back from a sub-screen"
);
app.insert_resource(State::new(Screen::Menu));
app.world_mut()
.get_mut::<Gamepad>(pad)
.expect("the pad")
.digital_mut()
.release(GamepadButton::East);
app.update();
assert!(
!app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::Escape),
"the synthesized Escape must not be left held on the menu"
);
}
#[test]
fn a_press_that_leaves_the_bridge_behind_still_releases_its_key() {
let mut app = bridged(Screen::StageSelect);
press(&mut app, GamepadButton::South);
assert!(
app.world()
.resource::<ButtonInput<KeyCode>>()
.just_pressed(KeyCode::Enter)
);
app.insert_resource(State::new(Screen::Puzzle));
let pad = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.single(app.world())
.expect("one pad");
app.world_mut()
.get_mut::<Gamepad>(pad)
.expect("the pad")
.digital_mut()
.release(GamepadButton::South);
app.update();
assert!(
!app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::Enter),
"let go of on a screen the bridge does not run on"
);
}
#[test]
fn an_unplugged_pad_lets_go_of_its_key() {
let mut app = bridged(Screen::StageSelect);
press(&mut app, GamepadButton::South);
let pad = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.single(app.world())
.expect("one pad");
app.world_mut().entity_mut(pad).remove::<Gamepad>();
app.update();
assert!(
!app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::Enter)
);
}
#[test]
fn the_menu_keeps_the_rest_of_the_bridge() {
for (button, key) in [
(GamepadButton::DPadUp, KeyCode::KeyW),
(GamepadButton::DPadDown, KeyCode::KeyS),
(GamepadButton::DPadLeft, KeyCode::KeyA),
(GamepadButton::DPadRight, KeyCode::KeyD),
(GamepadButton::South, KeyCode::Enter),
] {
let mut app = bridged(Screen::Menu);
press(&mut app, button);
assert!(
app.world().resource::<ButtonInput<KeyCode>>().pressed(key),
"{button:?} still drives the menu"
);
}
}
#[test]
fn the_lobby_dpad_is_the_arrows() {
for (button, key) in [
(GamepadButton::DPadUp, KeyCode::ArrowUp),
(GamepadButton::DPadDown, KeyCode::ArrowDown),
(GamepadButton::DPadLeft, KeyCode::ArrowLeft),
(GamepadButton::DPadRight, KeyCode::ArrowRight),
(GamepadButton::South, KeyCode::Enter),
(GamepadButton::East, KeyCode::Escape),
] {
let mut app = bridged(Screen::Lobby);
press(&mut app, button);
let keys = app.world().resource::<ButtonInput<KeyCode>>();
assert!(keys.pressed(key), "{button:?} is {key:?} in the lobby");
assert!(
!keys.pressed(KeyCode::KeyW),
"{button:?} must not toggle watching"
);
}
}
#[test]
fn a_dpad_press_carried_into_the_lobby_is_released() {
let mut app = bridged(Screen::Menu);
press(&mut app, GamepadButton::DPadUp);
assert!(
app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::KeyW)
);
app.insert_resource(State::new(Screen::Lobby));
let pad = app
.world_mut()
.query_filtered::<Entity, With<Gamepad>>()
.single(app.world())
.expect("one pad");
app.world_mut()
.get_mut::<Gamepad>(pad)
.expect("the pad")
.digital_mut()
.release(GamepadButton::DPadUp);
app.update();
assert!(
!app.world()
.resource::<ButtonInput<KeyCode>>()
.pressed(KeyCode::KeyW),
"the W pressed on the menu must not be left held in the lobby"
);
}
}