use crate::app::editor::Command;
use crate::app::i18n::{Tr, fill};
use crate::app::menu_ui::{NUMBER_KEYS, Nav};
use crate::app::play_input::AfterRound;
use crate::sim::Direction;
use bevy::prelude::*;
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum LastDevice {
#[default]
Keyboard,
Pad,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Pad {
A,
B,
X,
Y,
LB,
RB,
LT,
RT,
Start,
Select,
RS,
Up,
Down,
Left,
Right,
Move,
Flick(Direction),
}
impl Pad {
pub fn button(self) -> Option<GamepadButton> {
Some(match self {
A => GamepadButton::South,
B => GamepadButton::East,
X => GamepadButton::West,
Y => GamepadButton::North,
LB => GamepadButton::LeftTrigger,
RB => GamepadButton::RightTrigger,
LT => GamepadButton::LeftTrigger2,
RT => GamepadButton::RightTrigger2,
Start => GamepadButton::Start,
Select => GamepadButton::Select,
RS => GamepadButton::RightThumb,
Pad::Up => GamepadButton::DPadUp,
Pad::Down => GamepadButton::DPadDown,
Pad::Left => GamepadButton::DPadLeft,
Pad::Right => GamepadButton::DPadRight,
Pad::Move | Pad::Flick(_) => return None,
})
}
pub fn name(self, tr: &Tr) -> &'static str {
match self {
A => "A",
B => "B",
X => "X",
Y => "Y",
LB => "LB",
RB => "RB",
LT => "LT",
RT => "RT",
Start => "Start",
Select => "Select",
RS => "RS",
Pad::Up | Pad::Down | Pad::Left | Pad::Right | Pad::Move => tr.pad.dpad,
Pad::Flick(_) => tr.pad.right_stick,
}
}
pub fn held(self, pad: &Gamepad, deadzone: f32) -> bool {
if let Pad::Flick(dir) = self {
return flick(pad, deadzone) == Some(dir);
}
self.button().is_some_and(|button| pad.pressed(button))
}
}
pub fn flick(pad: &Gamepad, deadzone: f32) -> Option<Direction> {
let x = pad.get(GamepadAxis::RightStickX).unwrap_or(0.0);
let y = pad.get(GamepadAxis::RightStickY).unwrap_or(0.0);
if x.abs().max(y.abs()) <= deadzone {
return None;
}
Some(match x.abs() > y.abs() {
true if x > 0.0 => Direction::Right,
true => Direction::Left,
false if y > 0.0 => Direction::Up,
false => Direction::Down,
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Key {
Code(KeyCode),
Shifted(KeyCode),
Cap(char),
Wall(Direction),
Command(Command),
ShiftedCommand(Command),
Pick,
Step(Nav),
Direct,
Layer,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Does {
Choose,
ChooseAdjust,
Confirm,
Next,
Play,
Back,
Menu,
SaveBack,
PasteRound,
Mute,
Rebind,
Defaults,
StartMatch,
Join,
Partners,
Rename,
Coop,
Watch,
CopyCode,
PasteCode,
Delete,
Scroll,
NotNow,
GoOn,
Cancel,
NextPhrase,
Copy,
LanOpen,
LanClose,
Pick,
AsBot,
Host,
ByAddress,
PlayInstead,
SetRound,
Talk,
AskToLeave,
More,
NeverMind,
Move,
Place(Direction),
Remove,
Clear,
Start,
Run,
PrevLevel,
NextLevel,
Hint,
Reset,
Pause,
OpenMenu,
PlayPause,
NextStage,
TryAgain,
Stages,
BackToMenu,
Speed,
Vote,
PickWinner,
Close,
Lobby,
NextRound,
PickMap,
Rematch(crate::app::rematch::Rematch),
KeepReplay,
Paint,
PrevBrush,
NextBrush,
Walls,
SettingDown,
SettingUp,
Turn,
Undo,
Redo,
Open,
Edit,
Check,
Test,
HoldMore,
Save,
Name,
ShareLevel,
LoadLevel,
Size,
Kind,
PrevSetting,
NextSetting,
EndTest,
Help,
Quit,
}
impl Does {
pub fn label(self, tr: &Tr) -> &'static str {
let p = &tr.pad;
match self {
Does::Choose => p.choose,
Does::ChooseAdjust => p.choose_adjust,
Does::Confirm => p.confirm,
Does::Next => p.next,
Does::Play => p.play,
Does::Back => p.back,
Does::Menu | Does::OpenMenu => p.menu,
Does::SaveBack => p.save_back,
Does::PasteRound => p.paste_round,
Does::Mute => p.mute,
Does::Rebind => p.rebind,
Does::Defaults => p.defaults,
Does::StartMatch => p.start_match,
Does::Join => p.join,
Does::Partners => p.partners,
Does::Rename => p.rename,
Does::Coop => p.coop,
Does::Watch => p.watch,
Does::CopyCode | Does::ShareLevel => p.copy_code,
Does::PasteCode => p.paste_code,
Does::LoadLevel => p.my_levels,
Does::Delete => p.delete,
Does::Scroll => p.scroll,
Does::NotNow => p.not_now,
Does::GoOn => p.go_on,
Does::Cancel => p.cancel,
Does::NextPhrase => p.next_phrase,
Does::Copy => p.copy,
Does::LanOpen => p.lan_open,
Does::LanClose => p.lan_close,
Does::Pick => p.pick,
Does::AsBot => p.as_bot,
Does::Host => p.host,
Does::ByAddress => p.by_address,
Does::PlayInstead => p.play_instead,
Does::SetRound => p.set_round,
Does::Talk => p.talk,
Does::AskToLeave => p.ask_to_leave,
Does::More => p.more,
Does::NeverMind => p.never_mind,
Does::Move => p.moves,
Does::Place(_) => p.place,
Does::Remove => p.remove,
Does::Clear => p.clear,
Does::Start => p.start,
Does::Run => p.run,
Does::PrevLevel | Does::NextLevel => p.level,
Does::Hint => p.hint,
Does::Reset => p.reset,
Does::Pause | Does::PlayPause => p.pause,
Does::NextStage => p.next_level,
Does::TryAgain => p.try_again,
Does::Stages => p.stages,
Does::BackToMenu => p.back_to_menu,
Does::Speed => p.speed,
Does::Vote => p.vote,
Does::PickWinner => p.pick_winner,
Does::Close => p.close,
Does::Lobby => p.lobby,
Does::NextRound => p.next_round,
Does::PickMap => p.pick_map,
Does::Rematch(crate::app::rematch::Rematch::Round) => p.rematch,
Does::Rematch(crate::app::rematch::Rematch::Series) => p.rematch_series,
Does::Rematch(crate::app::rematch::Rematch::SeriesAsked) => p.rematch_series_confirm,
Does::KeepReplay => p.keep_replay,
Does::Paint => p.paint,
Does::PrevBrush | Does::NextBrush => p.brush,
Does::Walls => p.walls,
Does::SettingDown | Does::SettingUp => p.setting,
Does::Turn => p.turn,
Does::Undo | Does::Redo => p.undo,
Does::Open => p.open,
Does::Edit => p.edit,
Does::Check => p.check,
Does::Test => p.test,
Does::HoldMore => p.hold_more,
Does::Save => p.save,
Does::Name => p.name,
Does::Size => p.size,
Does::Kind => p.kind,
Does::PrevSetting | Does::NextSetting => p.pick_setting,
Does::EndTest => p.end_test,
Does::Help => p.help,
Does::Quit => p.quit,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Bind {
pub pad: Pad,
pub key: Key,
pub does: Does,
}
const fn bind(pad: Pad, key: Key, does: Does) -> Bind {
Bind { pad, key, does }
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Picks {
pub list: PickList,
pub rows: Vec<(u8, String)>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PickList {
Kick,
Vote,
Winner,
Partners,
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct PadPick {
list: Option<PickList>,
at: usize,
}
impl PadPick {
pub fn row(&self, picks: &Picks) -> usize {
match self.list == Some(picks.list) {
true => self.at.min(picks.rows.len().saturating_sub(1)),
false => 0,
}
}
pub fn stand_on(&mut self, picks: &Picks, number: u8) {
if let Some(at) = picks.rows.iter().position(|(row, _)| *row == number) {
self.list = Some(picks.list);
self.at = at;
}
}
pub fn step(&mut self, picks: &Picks, nav: Nav) {
let at = self.row(picks);
let len = picks.rows.len().max(1);
self.list = Some(picks.list);
self.at = crate::app::menu_ui::step(nav, at, len);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum TextBox {
Chat,
Cancels,
Keeps,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub enum Face {
#[default]
Menu,
Quitting,
Settings,
Controls,
MatchSetup {
step: crate::app::match_setup::Step,
join: bool,
partners: bool,
},
StageSelect {
custom: bool,
},
Replays,
Achievements,
Language,
NewVersion,
Interlude {
keep: crate::app::replays::KeepOffer,
},
Door {
lan: bool,
},
Typing(TextBox),
Browse {
hosts: bool,
watching: bool,
},
Hosting {
partners: bool,
},
HostMap {
pick: bool,
},
NextMap {
pick: bool,
},
Kicking {
picks: Picks,
pages: bool,
},
AtBeach,
PuzzleSetup {
posts: bool,
hint: bool,
},
PuzzleRunning,
PuzzleWon {
last: bool,
},
PuzzleLost {
hint: bool,
},
Paused,
Round,
Replay,
Watching {
vote: bool,
pick: bool,
},
Card(Picks),
Results {
door: AfterRound,
talk: bool,
rematch: Option<crate::app::rematch::Rematch>,
keep: crate::app::replays::KeepOffer,
},
Editor {
layer: bool,
},
LevelList,
EditorCard,
LevelCard,
Playtest(crate::app::editor::TestPhase),
}
use Pad::{A, B, LB, LT, RB, RS, RT, Select, Start, X, Y};
pub const PARTNER_CAP: char = 'P';
fn keep_bind(keep: crate::app::replays::KeepOffer) -> Option<Bind> {
match keep {
crate::app::replays::KeepOffer::Offered => Some(bind(
X,
Key::Cap(crate::app::replays::KEEP_CAP),
Does::KeepReplay,
)),
crate::app::replays::KeepOffer::Nothing | crate::app::replays::KeepOffer::Kept => None,
}
}
const UP_DOWN: [Bind; 2] = [
bind(Pad::Up, Key::Code(KeyCode::KeyW), Does::Choose),
bind(Pad::Down, Key::Code(KeyCode::KeyS), Does::Choose),
];
const DIALS: [Bind; 4] = [
bind(Pad::Up, Key::Code(KeyCode::KeyW), Does::ChooseAdjust),
bind(Pad::Down, Key::Code(KeyCode::KeyS), Does::ChooseAdjust),
bind(Pad::Left, Key::Code(KeyCode::KeyA), Does::ChooseAdjust),
bind(Pad::Right, Key::Code(KeyCode::KeyD), Does::ChooseAdjust),
];
pub const PLACES: [Bind; 4] = [
bind(A, Key::Direct, Does::Place(Direction::Down)),
bind(B, Key::Direct, Does::Place(Direction::Right)),
bind(X, Key::Direct, Does::Place(Direction::Left)),
bind(Y, Key::Direct, Does::Place(Direction::Up)),
];
const MOVE: Bind = bind(Pad::Move, Key::Direct, Does::Move);
const BOARD: [Bind; 7] = [
MOVE,
PLACES[0],
PLACES[1],
PLACES[2],
PLACES[3],
bind(LB, Key::Direct, Does::Remove),
bind(RB, Key::Direct, Does::Clear),
];
fn map_page_binds(pick: bool, picked: Does) -> Vec<Bind> {
let mut out: Vec<Bind> = DIALS
.map(|row| Bind {
does: Does::Choose,
..row
})
.to_vec();
if pick {
out.push(bind(A, Key::Code(KeyCode::Enter), picked));
}
out.push(bind(B, Key::Code(KeyCode::Escape), Does::Back));
out
}
pub fn binds(face: &Face) -> Vec<Bind> {
let code = Key::Code;
let esc = code(KeyCode::Escape);
let enter = code(KeyCode::Enter);
let mut out = Vec::new();
match face {
Face::Menu => {
out.extend(UP_DOWN);
out.extend([
bind(A, enter, Does::Play),
bind(X, Key::Cap('V'), Does::PasteRound),
bind(Y, Key::Cap('M'), Does::Mute),
bind(Start, Key::Direct, Does::Quit),
]);
}
Face::Quitting => out.extend([
bind(A, Key::Direct, Does::Quit),
bind(B, Key::Direct, Does::Back),
bind(Start, Key::Direct, Does::Back),
]),
Face::Settings => {
out.extend(DIALS);
out.extend([bind(A, enter, Does::Confirm), bind(B, esc, Does::SaveBack)]);
}
Face::Controls => {
out.extend(DIALS);
out.extend([
bind(A, enter, Does::Rebind),
bind(X, code(KeyCode::Backspace), Does::Defaults),
bind(B, esc, Does::Back),
]);
}
Face::MatchSetup {
step: crate::app::match_setup::Step::Game,
..
} => {
out.extend(DIALS);
out.extend([bind(A, enter, Does::Next), bind(B, esc, Does::Back)]);
}
Face::MatchSetup {
step: crate::app::match_setup::Step::Map,
..
} => {
out.extend(DIALS.map(|row| Bind {
does: Does::Choose,
..row
}));
out.extend([bind(A, enter, Does::Next), bind(B, esc, Does::Back)]);
}
Face::MatchSetup {
step: crate::app::match_setup::Step::Players,
join,
partners,
} => {
out.extend(DIALS);
if !*join {
out.push(bind(Start, Key::Direct, Does::StartMatch));
}
out.extend([
bind(A, enter, Does::StartMatch),
bind(X, code(KeyCode::Tab), Does::Rename),
]);
if *partners {
out.push(bind(Y, Key::Cap(PARTNER_CAP), Does::Partners));
}
if *join {
out.push(bind(Start, Key::Direct, Does::Join));
}
out.push(bind(B, esc, Does::Back));
}
Face::StageSelect { custom } => {
out.extend(DIALS.map(|row| Bind {
does: Does::Choose,
..row
}));
out.push(bind(A, enter, Does::Play));
if *custom {
out.extend([
bind(X, Key::Cap('E'), Does::Edit),
bind(Select, code(KeyCode::F3), Does::CopyCode),
]);
}
out.extend([bind(Y, Key::Cap('C'), Does::Coop), bind(B, esc, Does::Menu)]);
}
Face::Replays => {
out.extend(UP_DOWN);
out.extend([
bind(A, enter, Does::Watch),
bind(X, Key::Cap('C'), Does::CopyCode),
bind(Y, Key::Cap('V'), Does::PasteCode),
bind(Select, Key::Cap('D'), Does::Delete),
bind(B, esc, Does::Menu),
]);
}
Face::Achievements => {
out.extend(UP_DOWN.map(|row| Bind {
does: Does::Scroll,
..row
}));
out.push(bind(B, esc, Does::Menu));
}
Face::Language => {
out.extend(UP_DOWN);
out.push(bind(A, enter, Does::Confirm));
}
Face::NewVersion => {
out.extend(UP_DOWN);
out.extend([bind(A, enter, Does::Confirm), bind(B, esc, Does::NotNow)]);
}
Face::Interlude { keep } => {
out.push(bind(A, enter, Does::GoOn));
out.extend(keep_bind(*keep));
}
Face::Door { lan } => out.extend([
bind(X, Key::Cap('C'), Does::Copy),
bind(
Y,
Key::Cap('L'),
match lan {
true => Does::LanClose,
false => Does::LanOpen,
},
),
bind(B, esc, Does::Back),
]),
Face::Typing(ends) => {
out.push(bind(A, enter, Does::Confirm));
match ends {
TextBox::Chat => out.extend([
bind(Y, Key::Direct, Does::NextPhrase),
bind(B, esc, Does::Cancel),
]),
TextBox::Cancels => out.push(bind(B, esc, Does::Cancel)),
TextBox::Keeps => {}
}
}
Face::Browse { hosts, watching } => {
if *hosts {
out.extend([
bind(Pad::Up, code(KeyCode::ArrowUp), Does::Pick),
bind(Pad::Down, code(KeyCode::ArrowDown), Does::Pick),
]);
}
match watching {
true => out.extend([
bind(A, enter, Does::Watch),
bind(RB, Key::Cap('W'), Does::PlayInstead),
]),
false => {
if *hosts {
out.extend([
bind(A, enter, Does::Join),
bind(X, Key::Cap('B'), Does::AsBot),
]);
}
out.extend([
bind(Y, Key::Cap('H'), Does::Host),
bind(Select, Key::Cap('J'), Does::ByAddress),
bind(RB, Key::Cap('W'), Does::Watch),
]);
}
}
out.push(bind(B, esc, Does::Menu));
}
Face::Hosting { partners } => {
out.extend([
bind(Pad::Up, code(KeyCode::ArrowUp), Does::SetRound),
bind(Pad::Down, code(KeyCode::ArrowDown), Does::SetRound),
bind(Pad::Left, code(KeyCode::ArrowLeft), Does::SetRound),
bind(Pad::Right, code(KeyCode::ArrowRight), Does::SetRound),
bind(A, enter, Does::StartMatch),
bind(Y, Key::Direct, Does::Talk),
bind(X, Key::Cap('K'), Does::AskToLeave),
]);
if *partners {
out.push(bind(Select, Key::Cap(PARTNER_CAP), Does::Partners));
}
out.push(bind(B, esc, Does::Menu));
}
Face::HostMap { pick } => out.extend(map_page_binds(*pick, Does::StartMatch)),
Face::NextMap { pick } => out.extend(map_page_binds(*pick, Does::NextRound)),
Face::Kicking { pages, .. } => {
out.extend([
bind(Pad::Up, Key::Step(Nav::Up), Does::Choose),
bind(Pad::Down, Key::Step(Nav::Down), Does::Choose),
bind(A, Key::Pick, Does::AskToLeave),
]);
if *pages {
out.push(bind(Select, code(KeyCode::Tab), Does::More));
}
out.push(bind(B, esc, Does::NeverMind));
}
Face::AtBeach => out.extend([bind(Y, Key::Direct, Does::Talk), bind(B, esc, Does::Menu)]),
Face::PuzzleSetup { posts, hint } => {
match posts {
true => out.extend(BOARD),
false => out.push(MOVE),
}
out.push(bind(
Start,
Key::Direct,
match posts {
true => Does::Start,
false => Does::Run,
},
));
out.extend([
bind(LT, Key::Direct, Does::PrevLevel),
bind(RT, Key::Direct, Does::NextLevel),
]);
if *hint {
out.push(bind(RS, Key::Direct, Does::Hint));
}
out.push(bind(Select, Key::Direct, Does::OpenMenu));
}
Face::PuzzleRunning => out.extend([
bind(Y, Key::Direct, Does::Reset),
bind(Start, Key::Direct, Does::Pause),
]),
Face::PuzzleWon { last } => out.extend([
match last {
true => bind(A, enter, Does::BackToMenu),
false => bind(A, enter, Does::NextStage),
},
bind(Y, Key::Cap('R'), Does::Reset),
bind(B, esc, Does::Stages),
]),
Face::PuzzleLost { hint } => {
out.push(bind(A, enter, Does::TryAgain));
if *hint {
out.push(bind(RS, Key::Direct, Does::Hint));
}
out.push(bind(B, esc, Does::Stages));
}
Face::Paused => out.extend([
bind(Pad::Up, Key::Direct, Does::Choose),
bind(Pad::Down, Key::Direct, Does::Choose),
bind(A, Key::Direct, Does::Confirm),
bind(Start, Key::Direct, Does::GoOn),
bind(B, Key::Direct, Does::GoOn),
]),
Face::Round => {
out.extend(BOARD);
out.push(bind(Start, Key::Direct, Does::Pause));
}
Face::Replay => out.extend([
bind(A, Key::Direct, Does::PlayPause),
bind(X, Key::Direct, Does::Speed),
bind(Start, Key::Direct, Does::OpenMenu),
]),
Face::Watching { vote, pick } => {
out.push(bind(Y, Key::Cap('T'), Does::Talk));
if *vote {
out.push(bind(X, Key::Cap('E'), Does::Vote));
}
if *pick {
out.push(bind(RB, Key::Cap('P'), Does::PickWinner));
}
out.push(bind(Start, Key::Direct, Does::OpenMenu));
}
Face::Card(picks) => out.extend([
bind(Pad::Up, Key::Step(Nav::Up), Does::Choose),
bind(Pad::Down, Key::Step(Nav::Down), Does::Choose),
bind(
A,
Key::Pick,
match picks.list {
PickList::Winner => Does::PickWinner,
PickList::Vote | PickList::Kick => Does::Vote,
PickList::Partners => Does::Partners,
},
),
bind(B, esc, Does::Close),
]),
Face::Results {
door,
talk,
rematch,
keep,
} => {
out.push(bind(
A,
enter,
match door {
AfterRound::NextRound => Does::NextRound,
AfterRound::NextMap => Does::PickMap,
AfterRound::Lobby => Does::Lobby,
AfterRound::Menu => Does::Menu,
},
));
if *talk {
out.push(bind(Y, Key::Cap('T'), Does::Talk));
} else if let Some(again) = rematch {
out.push(bind(Y, Key::Cap('R'), Does::Rematch(*again)));
}
out.extend(keep_bind(*keep));
}
Face::Editor { layer: false } => out.extend([
MOVE,
bind(
Pad::Flick(Direction::Up),
Key::Wall(Direction::Up),
Does::Walls,
),
bind(
Pad::Flick(Direction::Down),
Key::Wall(Direction::Down),
Does::Walls,
),
bind(
Pad::Flick(Direction::Left),
Key::Wall(Direction::Left),
Does::Walls,
),
bind(
Pad::Flick(Direction::Right),
Key::Wall(Direction::Right),
Does::Walls,
),
bind(A, code(KeyCode::Space), Does::Paint),
bind(LB, Key::Shifted(KeyCode::Tab), Does::PrevBrush),
bind(RB, code(KeyCode::Tab), Does::NextBrush),
bind(LT, code(KeyCode::Minus), Does::SettingDown),
bind(RT, code(KeyCode::Equal), Does::SettingUp),
bind(Y, Key::Command(Command::Turn), Does::Turn),
bind(X, Key::Command(Command::Check), Does::Check),
bind(Start, enter, Does::Test),
bind(B, esc, Does::Menu),
bind(Select, Key::Layer, Does::HoldMore),
]),
Face::Editor { layer: true } => out.extend([
bind(A, code(KeyCode::F2), Does::Save),
bind(Y, code(KeyCode::F1), Does::Name),
bind(X, code(KeyCode::F3), Does::ShareLevel),
bind(B, code(KeyCode::F4), Does::LoadLevel),
bind(RB, code(KeyCode::F6), Does::Kind),
bind(
LT,
Key::Shifted(crate::app::editor::NEXT_SETTING),
Does::PrevSetting,
),
bind(
RT,
code(crate::app::editor::NEXT_SETTING),
Does::NextSetting,
),
bind(
Pad::Flick(Direction::Left),
Key::Command(Command::Undo),
Does::Undo,
),
bind(
Pad::Flick(Direction::Right),
Key::ShiftedCommand(Command::Undo),
Does::Redo,
),
bind(Pad::Flick(Direction::Up), code(KeyCode::F5), Does::Size),
bind(
Pad::Flick(Direction::Down),
Key::Shifted(KeyCode::F5),
Does::Size,
),
bind(
Start,
Key::Shifted(crate::app::editor::HELP_KEY),
Does::Help,
),
]),
Face::EditorCard => out.extend([bind(A, enter, Does::Close), bind(B, esc, Does::Close)]),
Face::LevelCard => out.extend([
bind(A, enter, Does::Play),
bind(X, Key::Cap('E'), Does::Edit),
bind(Y, Key::Cap('S'), Does::Save),
bind(B, esc, Does::Back),
]),
Face::LevelList => {
out.extend(UP_DOWN);
out.extend([
bind(A, enter, Does::Open),
bind(X, Key::Cap('C'), Does::CopyCode),
bind(Select, Key::Cap('D'), Does::Delete),
bind(B, esc, Does::Close),
]);
}
Face::Playtest(phase) => {
use crate::app::editor::TestPhase;
match phase {
TestPhase::Setup => {
out.extend(BOARD[..6].iter().copied());
out.push(bind(Start, Key::Direct, Does::Start));
}
TestPhase::Running | TestPhase::Won | TestPhase::Lost => {
out.push(bind(Y, Key::Direct, Does::Reset));
}
TestPhase::Match => {
out.extend(BOARD[..6].iter().copied());
out.push(bind(RB, Key::Direct, Does::Reset));
}
TestPhase::MatchOver => out.push(bind(RB, Key::Direct, Does::Reset)),
}
out.push(bind(Select, Key::Direct, Does::EndTest));
}
}
out
}
pub fn picks(face: &Face) -> Option<&Picks> {
if let Face::Kicking { picks, .. } | Face::Card(picks) = face {
return Some(picks);
}
None
}
pub fn pressed<'a>(face: &Face, does: Does, pads: impl IntoIterator<Item = &'a Gamepad>) -> bool {
let buttons: Vec<GamepadButton> = binds(face)
.iter()
.filter(|row| row.does == does)
.filter_map(|row| row.pad.button())
.collect();
pads.into_iter()
.any(|pad| buttons.iter().any(|&button| pad.just_pressed(button)))
}
pub fn pressed_as<'a>(
face: &Face,
control: Pad,
does: Does,
pads: impl IntoIterator<Item = &'a Gamepad>,
) -> bool {
let bound = binds(face)
.iter()
.any(|row| row.pad == control && row.does == does);
let Some(button) = control.button().filter(|_| bound) else {
return false;
};
pads.into_iter().any(|pad| pad.just_pressed(button))
}
pub fn held_on(face: &Face, does: Does, pad: &Gamepad) -> bool {
binds(face)
.iter()
.filter(|row| row.does == does)
.filter_map(|row| row.pad.button())
.any(|button| pad.pressed(button))
}
pub fn pressed_on(face: &Face, does: Does, pad: &Gamepad) -> bool {
pressed(face, does, [pad])
}
pub fn button_name(face: &Face, does: Does, tr: &Tr) -> Option<String> {
let row = binds(face).into_iter().find(|row| row.does == does)?;
Some(match *face == (Face::Editor { layer: true }) {
true => format!("{}+{}", Select.name(tr), row.pad.name(tr)),
false => row.pad.name(tr).to_string(),
})
}
pub fn spelled(tr: &Tr, device: LastDevice, face: &Face, does: Does, keys: &str) -> String {
match (device, button_name(face, does, tr)) {
(LastDevice::Pad, Some(button)) => button,
_ => keys.to_string(),
}
}
pub fn legend(tr: &Tr, face: &Face) -> String {
let mut slots: Vec<(Vec<&'static str>, &'static str)> = Vec::new();
for row in binds(face) {
let label = row.does.label(tr);
let name = row.pad.name(tr);
match slots.iter_mut().find(|(_, said)| *said == label) {
Some((names, _)) => {
if !names.contains(&name) {
names.push(name);
}
}
None => slots.push((vec![name], label)),
}
}
slots
.iter()
.map(|(names, label)| fill(tr.pad.bind, &[("b", &names.join("/")), ("a", label)]))
.collect::<Vec<_>>()
.join(" | ")
}
pub fn pick_key(picks: &Picks, pick: &PadPick) -> Option<KeyCode> {
let (number, _) = picks.rows.get(pick.row(picks))?;
NUMBER_KEYS
.get(usize::from(*number).checked_sub(1)?)
.copied()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::i18n::EN;
#[test]
fn the_legend_shares_a_slot_between_rows_with_the_same_words() {
assert_eq!(
legend(&EN, &Face::Menu),
"D-pad: choose | A: play | X: paste a code | Y: mute | Start: quit"
);
assert_eq!(legend(&EN, &Face::Quitting), "A: quit | B/Start: back");
assert_eq!(
legend(
&EN,
&Face::PuzzleSetup {
posts: true,
hint: false
}
),
"D-pad: move | A/B/X/Y: place | LB: remove | RB: clear | Start: start \
| LT/RT: level | Select: menu"
);
}
#[test]
fn a_round_presses_no_keys() {
for face in [
Face::Round,
Face::PuzzleSetup {
posts: true,
hint: true,
},
Face::PuzzleRunning,
Face::Paused,
Face::Replay,
Face::Playtest(crate::app::editor::TestPhase::Match),
Face::Playtest(crate::app::editor::TestPhase::Setup),
] {
for row in binds(&face) {
assert_eq!(row.key, Key::Direct, "{face:?}: {row:?}");
}
}
}
#[test]
fn no_control_does_two_things_on_one_face() {
for face in super::super::face::every_face() {
let rows = binds(&face);
for (i, a) in rows.iter().enumerate() {
for b in &rows[i + 1..] {
assert_ne!(a.pad, b.pad, "{face:?}: {a:?} and {b:?}");
}
}
}
}
#[test]
fn the_pick_walks_its_list() {
let list = |n: u8, kind| Picks {
list: kind,
rows: (1..=n).map(|i| (i, i.to_string())).collect(),
};
let votes = list(3, PickList::Vote);
let mut pick = PadPick::default();
assert_eq!(pick_key(&votes, &pick), Some(KeyCode::Digit1));
pick.step(&votes, Nav::Up);
assert_eq!(pick_key(&votes, &pick), Some(KeyCode::Digit3), "wraps");
pick.step(&votes, Nav::Down);
assert_eq!(pick.row(&votes), 0);
pick.step(&votes, Nav::Down);
pick.step(&votes, Nav::Down);
assert_eq!(pick.row(&list(2, PickList::Vote)), 1, "held to the end");
assert_eq!(pick.row(&list(3, PickList::Winner)), 0, "a new list");
}
}