use crate::app::cursor::Cursor;
use crate::app::layout::{self};
use crate::app::settings::{GameSettings, SeatInput};
use crate::app::{PendingActions, Phase, PlacementDenied, Sim};
use crate::sim::{Direction, MAX_PLAYERS, PlayerAction};
use bevy::prelude::*;
#[derive(Resource, Default)]
pub struct PadSeats(pub Vec<Entity>);
fn nth_pad<'a>(
pads: &'a Query<(Entity, &Gamepad)>,
claims: &PadSeats,
index: usize,
) -> Option<&'a Gamepad> {
if claims.0.is_empty() {
return pads.iter().nth(index).map(|(_, pad)| pad);
}
let entity = *claims.0.get(index)?;
pads.get(entity).ok().map(|(_, pad)| pad)
}
fn pad_index_of(settings: &GameSettings, players: &[u8], player: u8) -> Option<usize> {
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, &mut Transform, 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, mut transform, 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.elapsed() == repeat.0.duration();
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;
transform.translation = layout::tile_center(board, nx, ny).extend(layout::z::CURSOR);
}
}
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,
});
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();
let Some(pad) = pad_index_of(&settings, &players, 0).and_then(|i| nth_pad(&pads, &seats, i))
else {
return;
};
let Some(mut cursor) = cursors.iter_mut().find(|c| c.player == 0) else {
return;
};
for (button, dir) in PLACES {
if pad.just_pressed(button) && !sim.0.place_signpost(0, cursor.x, cursor.y, dir) {
cursor.flash = 0.25;
denied.write(PlacementDenied { player: 0 });
}
}
if pad.just_pressed(GamepadButton::LeftTrigger) {
let _ = sim.0.remove_signpost(0, cursor.x, cursor.y);
}
if pad.just_pressed(GamepadButton::Start) {
next_phase.set(Phase::Running);
}
}
pub fn pad_menu_bridge(pads: Query<&Gamepad>, mut keys: ResMut<ButtonInput<KeyCode>>) {
const MAP: [(GamepadButton, KeyCode); 6] = [
(GamepadButton::DPadUp, KeyCode::KeyW),
(GamepadButton::DPadDown, KeyCode::KeyS),
(GamepadButton::DPadLeft, KeyCode::KeyA),
(GamepadButton::DPadRight, KeyCode::KeyD),
(GamepadButton::South, KeyCode::Enter),
(GamepadButton::East, KeyCode::Escape),
];
for pad in &pads {
for (button, key) in MAP {
if pad.just_pressed(button) {
keys.press(key);
}
if pad.just_released(button) {
keys.release(key);
}
}
}
}
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() < KEYBOARD_SEATS + PAD_SEATS
{
seats.0.push(entity);
let needed = (KEYBOARD_SEATS + seats.0.len()) as u8;
config.seats = config.seats.max(needed).min(MAX_PLAYERS as u8);
config.bots = config.bots.min(config.seats - 1);
}
}
}
#[cfg(test)]
mod tests {
use super::pad_index_of;
use crate::app::settings::{GameSettings, SeatInput};
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_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));
}
}