mod bridge;
pub mod face;
mod rumble;
pub mod scheme;
mod seats;
pub use bridge::{Bridged, pad_bridge_release, pad_menu_bridge};
pub use face::{
LastPad, PadFace, hands_at_the_menu, pad_prompts_from_env, read_face, track_last_device,
};
pub use rumble::rumble_on_raid;
pub use scheme::{Face, LastDevice, PadPick};
pub use seats::{
PadSeats, SeatPad, follow_the_setup_hands, forget_the_pilot, keep_pad_order,
let_a_lone_players_claims_go, let_claims_go, pad_claim_seats,
};
use crate::app::cursor::{Cursor, HoldRepeat};
use crate::app::play_input::{SeatCommand, apply_setup, queue_versus};
use crate::app::settings::GameSettings;
use crate::app::{PendingActions, Phase, PlacementDenied, Sim};
use bevy::prelude::*;
#[derive(bevy::ecs::system::SystemParam)]
pub struct SeatPads<'w, 's> {
pads: Query<'w, 's, (Entity, &'static Gamepad)>,
face: Res<'w, PadFace>,
seats: Res<'w, PadSeats>,
}
impl SeatPads<'_, '_> {
pub fn pressed(
&self,
settings: &GameSettings,
players: &[u8],
player: u8,
does: scheme::Does,
) -> bool {
let players = seats::seated(players.iter().copied());
self.seats
.pad_for(&self.pads, settings, &players, player)
.is_some_and(|pad| scheme::pressed_on(&self.face.0, does, pad))
}
}
#[derive(Component, Default)]
pub struct PadRepeat {
repeat: HoldRepeat,
held: bool,
}
const DPAD: [GamepadButton; 4] = [
GamepadButton::DPadUp,
GamepadButton::DPadDown,
GamepadButton::DPadLeft,
GamepadButton::DPadRight,
];
fn fresh_press(was_held: bool, held: bool, dpad_just: bool) -> bool {
held && (!was_held || dpad_just)
}
fn pad_commands<'a>(face: &'a Face, pad: &'a Gamepad) -> impl Iterator<Item = SeatCommand> + 'a {
scheme::binds(face)
.into_iter()
.filter(|row| row.pad.button().is_some_and(|b| pad.just_pressed(b)))
.filter_map(|row| {
if let scheme::Does::Place(dir) = row.does {
return Some(SeatCommand::Place(dir));
}
(row.does == scheme::Does::Remove).then_some(SeatCommand::Remove)
})
}
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 players = seats::seated(cursors.iter().map(|(_, c, ..)| c.player));
for (entity, mut cursor, repeat) in &mut cursors {
let Some(pad) = seats.pad_for(&pads, &settings, &players, cursor.player) else {
continue;
};
let (dx, dy) = held_direction(pad, deadzone);
let Some(mut repeat) = repeat else {
commands.entity(entity).insert(PadRepeat::default());
continue;
};
let held = dx != 0 || dy != 0;
let dpad_just = DPAD.iter().any(|&button| pad.just_pressed(button));
let just = fresh_press(repeat.held, held, dpad_just);
repeat.held = held;
if repeat.repeat.advance(
just,
held,
time.delta(),
settings.repeat_delay,
settings.repeat_interval,
) {
cursor.step(board, dx, dy);
}
}
}
pub fn pad_versus_input(
pads: Query<(Entity, &Gamepad)>,
(face, seats): (Res<PadFace>, 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 players = seats::seated(cursors.iter().map(|c| c.player));
for mut cursor in &mut cursors {
let Some(pad) = seats.pad_for(&pads, &settings, &players, cursor.player) else {
continue;
};
let commands = pad_commands(&face.0, pad).chain(
scheme::held_on(&face.0, scheme::Does::Clear, pad).then_some(SeatCommand::ClearAll),
);
for command in commands {
queue_versus(command, &mut cursor, board, &mut pending, &mut denied);
}
}
}
pub fn pad_setup_input(
pads: Query<(Entity, &Gamepad)>,
(face, seats): (Res<PadFace>, 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 players = seats::seated(cursors.iter().map(|c| c.player));
for mut cursor in &mut cursors {
let Some(pad) = seats.pad_for(&pads, &settings, &players, cursor.player) else {
continue;
};
let face = &face.0;
let on = |does| scheme::pressed_on(face, does, pad);
let commands = pad_commands(face, pad)
.chain(on(scheme::Does::Clear).then_some(SeatCommand::ClearAll))
.chain(
(on(scheme::Does::Start) || on(scheme::Does::Run)).then_some(SeatCommand::Start),
);
for command in commands {
apply_setup(command, &mut cursor, &mut sim, &mut next_phase, &mut denied);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn a_held_pad_direction_gets_past_the_first_delay() {
let mut app = App::new();
app.insert_resource(Sim(crate::sim::Board::new(12, 9, 1)));
app.insert_resource(GameSettings {
repeat_delay: 0.28,
repeat_interval: 0.09,
..GameSettings::default()
});
app.init_resource::<PadSeats>();
app.init_resource::<Time>();
app.world_mut().spawn(Cursor::seated(0));
let pad = app.world_mut().spawn(Gamepad::default()).id();
app.add_systems(Update, pad_move_cursor);
let frame = |app: &mut App, secs: f32| {
app.world_mut()
.resource_mut::<Time>()
.advance_by(Duration::from_secs_f32(secs));
app.update();
app.world_mut()
.get_mut::<Gamepad>(pad)
.expect("the pad")
.digital_mut()
.clear();
let world = app.world_mut();
world
.query::<&Cursor>()
.single(world)
.expect("one cursor")
.x
};
let dpad = |app: &mut App, down: bool| {
let mut gamepad = app.world_mut().get_mut::<Gamepad>(pad).expect("the pad");
match down {
true => gamepad.digital_mut().press(GamepadButton::DPadRight),
false => gamepad.digital_mut().release(GamepadButton::DPadRight),
}
};
assert_eq!(frame(&mut app, 0.0), 0);
dpad(&mut app, true);
assert_eq!(frame(&mut app, 0.01), 1, "the press steps at once");
assert_eq!(frame(&mut app, 0.1), 1, "then waits out the delay");
assert_eq!(frame(&mut app, 0.2), 2, "and steps when it is out");
assert_eq!(frame(&mut app, 0.1), 3, "then at the interval");
assert_eq!(frame(&mut app, 0.1), 4);
dpad(&mut app, false);
assert_eq!(frame(&mut app, 0.1), 4, "let go, it stops");
dpad(&mut app, true);
assert_eq!(frame(&mut app, 0.01), 5, "and a new press steps at once");
}
#[test]
fn a_new_direction_on_the_pad_is_a_press() {
assert!(fresh_press(false, true, false), "from rest");
assert!(fresh_press(true, true, true), "a d-pad direction added");
assert!(!fresh_press(true, true, false), "held, the stick wobbling");
assert!(!fresh_press(true, false, false), "let go");
assert!(!fresh_press(false, false, false), "at rest");
}
}