use super::*;
use crate::app::gamepad::{LastDevice, PadSeats, SeatPad};
use crate::app::menu_ui;
use crate::app::settings::{GameSettings, SeatInput};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Step {
#[default]
Game,
Map,
Players,
}
impl Step {
pub const ALL: [Step; 3] = [Step::Game, Step::Map, Step::Players];
pub fn index(self) -> usize {
match self {
Step::Game => 0,
Step::Map => 1,
Step::Players => 2,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GameRow {
Players,
AiLevel,
Round,
Posts,
Gulls,
Scoring,
Series,
}
impl GameRow {
pub const ALL: [GameRow; 7] = [
GameRow::Players,
GameRow::AiLevel,
GameRow::Round,
GameRow::Posts,
GameRow::Gulls,
GameRow::Scoring,
GameRow::Series,
];
}
#[derive(Resource, Default)]
pub struct MatchMenu {
pub step: Step,
pub game_row: usize,
pub slot: usize,
pub seat: u8,
pub naming: Option<u8>,
pub launched: bool,
pub seed: u64,
pub scroll: f32,
pub online: bool,
}
pub fn turn_game_row(
row: GameRow,
turn: Turn,
config: &mut MatchConfig,
settings: &mut GameSettings,
beaches: &CustomBeaches,
) {
match row {
GameRow::Players => {
let seats = crate::app::cycle::dial(config.seats, turn, 1, SEATS_RANGE);
reseat(config, seats, beaches);
}
GameRow::AiLevel => {
config.ai_level = config.ai_level.cycled(turn);
let level = config.ai_level;
for controller in &mut config.controllers {
if controller.ai().is_some() {
*controller = SeatController::Ai(level);
}
}
}
GameRow::Round => config.round = config.round.cycled(turn),
GameRow::Posts => {
config.posts = crate::app::cycle::dial(config.posts, turn, 1, POSTS_RANGE);
}
GameRow::Gulls => config.gulls = config.gulls.cycled(turn),
GameRow::Scoring => settings.team_mode = settings.team_mode.cycled(turn),
GameRow::Series => config.series = config.series.cycled(turn),
}
}
pub fn game_row_text(
tr: &crate::app::i18n::Tr,
row: GameRow,
config: &MatchConfig,
settings: &GameSettings,
) -> (&'static str, String, &'static str) {
let dial = |value: &str| format!("< {value} >");
match row {
GameRow::Players => (
tr.match_players,
dial(&config.seats.to_string()),
tr.setup_players_aside,
),
GameRow::AiLevel => (
tr.match_ai_level,
dial(tr.bot_levels[config.ai_level.index()]),
tr.setup_ai_aside,
),
GameRow::Round => (
tr.match_round,
dial(tr.round_names[config.round.index()]),
"",
),
GameRow::Posts => (tr.match_posts, dial(&config.posts.to_string()), ""),
GameRow::Gulls => (
tr.match_gulls,
dial(tr.gull_names[config.gulls.index()]),
"",
),
GameRow::Scoring => (
tr.setup_scoring,
dial(&crate::app::teams::row_value(
tr,
settings.team_mode,
config.seats,
true,
)),
"",
),
GameRow::Series => (
tr.setup_series,
dial(tr.mode_names[config.series.index()]),
"",
),
}
}
pub fn game_row_note(tr: &crate::app::i18n::Tr, row: GameRow) -> String {
let index = GameRow::ALL.iter().position(|r| *r == row).unwrap_or(0);
fill(
tr.setup_game_notes[index],
&[
("a", &SEATS_RANGE.start().to_string()),
("b", &SEATS_RANGE.end().to_string()),
],
)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Card {
Map(MapChoice),
Shelf(usize),
Random,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MapSection {
BuiltIn,
Yours,
Chance,
}
impl MapSection {
pub const ALL: [MapSection; 3] = [MapSection::BuiltIn, MapSection::Yours, MapSection::Chance];
pub fn index(self) -> usize {
match self {
MapSection::BuiltIn => 0,
MapSection::Yours => 1,
MapSection::Chance => 2,
}
}
pub fn of(map: MapChoice) -> MapSection {
match map {
MapChoice::Classic
| MapChoice::GenSmall
| MapChoice::GenClassic
| MapChoice::GenLarge
| MapChoice::GenXl
| MapChoice::GenOcean
| MapChoice::TheGates
| MapChoice::TheMoat
| MapChoice::LogJam
| MapChoice::ThreeLanes
| MapChoice::RoundTable
| MapChoice::RoundTheWorld
| MapChoice::Duel
| MapChoice::TheFountain
| MapChoice::FourDoors
| MapChoice::GullRoad
| MapChoice::BackToBack => MapSection::BuiltIn,
MapChoice::Custom => MapSection::Yours,
MapChoice::Surprise => MapSection::Chance,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Slot {
Card(Card),
MakeOne,
}
pub fn map_slots(beaches: &CustomBeaches) -> Vec<(MapSection, Slot)> {
let maps_in = |section: MapSection| {
MapChoice::DIAL
.into_iter()
.filter(move |&map| map != MapChoice::Custom && MapSection::of(map) == section)
.map(move |map| (section, Slot::Card(Card::Map(map))))
};
let yours: Vec<(MapSection, Slot)> = match beaches.0.is_empty() {
true => vec![(MapSection::Yours, Slot::MakeOne)],
false => (0..beaches.0.len())
.map(|shelf| (MapSection::Yours, Slot::Card(Card::Shelf(shelf))))
.collect(),
};
maps_in(MapSection::BuiltIn)
.chain(yours)
.chain(maps_in(MapSection::Chance))
.chain(std::iter::once((
MapSection::Chance,
Slot::Card(Card::Random),
)))
.collect()
}
pub fn cards(beaches: &CustomBeaches) -> Vec<Card> {
map_slots(beaches)
.into_iter()
.filter_map(|(_, slot)| match slot {
Slot::Card(card) => Some(card),
Slot::MakeOne => None,
})
.collect()
}
pub fn card_seats(card: Card, beaches: &CustomBeaches) -> Option<(u8, u8)> {
let tables = card_tables(card, beaches);
Some((*tables.first()?, *tables.last()?))
}
pub fn card_tables(card: Card, beaches: &CustomBeaches) -> Vec<u8> {
SEATS_RANGE
.filter(|&seats| card_fits(card, seats, beaches))
.collect()
}
pub fn card_lists_its_tables(card: Card, beaches: &CustomBeaches) -> bool {
match card {
Card::Map(map) => map.tables().is_some(),
Card::Shelf(shelf) => beaches
.0
.get(shelf)
.is_some_and(|beach| !beach.level.tables.is_empty()),
Card::Random => false,
}
}
pub fn card_fits(card: Card, seats: u8, beaches: &CustomBeaches) -> bool {
match card {
Card::Map(map) => holds(map, seats),
Card::Shelf(shelf) => beaches.among_fitting(seats, shelf).is_some(),
Card::Random => true,
}
}
pub fn card_travels(card: Card, beaches: &CustomBeaches) -> bool {
match card {
Card::Shelf(shelf) => beaches
.0
.get(shelf)
.is_some_and(|beach| !beach.too_big_to_send()),
Card::Map(_) | Card::Random => true,
}
}
pub fn card_open(card: Card, seats: u8, beaches: &CustomBeaches, online: bool) -> bool {
card_fits(card, seats, beaches) && (!online || card_travels(card, beaches))
}
pub fn picked_card(config: &MatchConfig, beaches: &CustomBeaches) -> Card {
if config.random {
return Card::Random;
}
match config.map {
MapChoice::Custom => beaches
.on_shelf(config.seats, config.custom)
.map_or(Card::Map(MapChoice::GenXl), Card::Shelf),
map @ (MapChoice::Classic
| MapChoice::GenSmall
| MapChoice::GenClassic
| MapChoice::GenLarge
| MapChoice::GenXl
| MapChoice::GenOcean
| MapChoice::TheGates
| MapChoice::TheMoat
| MapChoice::LogJam
| MapChoice::ThreeLanes
| MapChoice::RoundTable
| MapChoice::RoundTheWorld
| MapChoice::Duel
| MapChoice::TheFountain
| MapChoice::FourDoors
| MapChoice::GullRoad
| MapChoice::BackToBack
| MapChoice::Surprise) => Card::Map(map),
}
}
pub fn pick_card(config: &mut MatchConfig, card: Card, beaches: &CustomBeaches) {
if !card_fits(card, config.seats, beaches) {
return;
}
match card {
Card::Map(map) => {
config.map = map;
config.random = false;
config.pick = None;
}
Card::Shelf(shelf) => {
if let Some(custom) = beaches.among_fitting(config.seats, shelf) {
config.map = MapChoice::Custom;
config.custom = custom;
config.random = false;
config.pick = None;
}
}
Card::Random => config.random = true,
}
}
pub fn card_config(
config: &MatchConfig,
card: Card,
beaches: &CustomBeaches,
) -> Option<MatchConfig> {
let seats = match card_fits(card, config.seats, beaches) {
true => config.seats,
false => card_seats(card, beaches)?.0,
};
let mut shown = MatchConfig {
seats,
random: false,
..config.clone()
};
match card {
Card::Map(MapChoice::Surprise) => return None,
Card::Map(map) => shown.map = map,
Card::Shelf(shelf) => {
shown.map = MapChoice::Custom;
shown.custom = beaches.among_fitting(seats, shelf)?;
}
Card::Random => return None,
}
Some(shown)
}
pub fn setup_teams(settings: &GameSettings, config: &MatchConfig) -> crate::app::teams::TeamMode {
settings.team_mode.played_by(config.seats)
}
pub const SEAT_KINDS: [SeatController; 5] = [
SeatController::Local,
SeatController::Ai(BotLevel::Easy),
SeatController::Ai(BotLevel::Normal),
SeatController::Ai(BotLevel::Hard),
SeatController::Bot,
];
pub fn seat_turns(seat: u8) -> bool {
seat != 0
}
pub fn turn_seat(config: &mut MatchConfig, seat: u8, turn: Turn) {
if !seat_turns(seat) || seat >= config.seats {
return;
}
let now = config.controller(seat);
let at = SEAT_KINDS.iter().position(|c| *c == now).unwrap_or(0);
let next = crate::app::cycle::dial(at as u8, turn, 1, 0..=SEAT_KINDS.len() as u8 - 1);
config.controllers[usize::from(seat)] = SEAT_KINDS[usize::from(next)];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PlayedBy {
Keys(u8),
Pad {
number: u8,
joined: bool,
},
Open {
keys: bool,
},
Ai(BotLevel),
Bot,
}
fn p2_keys(settings: &GameSettings) -> bool {
!matches!(settings.seat_input[1], SeatInput::Pad(_))
}
pub fn played_by(
config: &MatchConfig,
seat: u8,
pads: &[SeatPad],
p2_here: bool,
settings: &GameSettings,
) -> PlayedBy {
match config.controller(seat) {
SeatController::Ai(level) => return PlayedBy::Ai(level),
SeatController::Bot => return PlayedBy::Bot,
SeatController::Local | SeatController::Remote => {}
}
if let Some(pad) = pads.iter().find(|pad| pad.seat == seat) {
return PlayedBy::Pad {
number: pad.number,
joined: pad.joined,
};
}
if seat == 0 {
return PlayedBy::Keys(0);
}
let keys = seat == 1 && p2_keys(settings);
match keys && p2_here {
true => PlayedBy::Keys(1),
false => PlayedBy::Open { keys },
}
}
pub fn open_seats(
config: &MatchConfig,
pads: &[SeatPad],
p2_here: bool,
settings: &GameSettings,
) -> Vec<u8> {
(0..config.seats)
.filter(|&seat| {
matches!(
played_by(config, seat, pads, p2_here, settings),
PlayedBy::Open { .. }
)
})
.collect()
}
pub fn played_by_text(tr: &crate::app::i18n::Tr, by: PlayedBy) -> String {
match by {
PlayedBy::Keys(seat) => fill(
tr.setup_by_keys,
&[("p", &crate::app::seat_label(tr, seat))],
),
PlayedBy::Pad { number, .. } => fill(tr.setup_by_pad, &[("n", &number.to_string())]),
PlayedBy::Open { keys: false } => tr.setup_by_open.to_string(),
PlayedBy::Open { keys: true } => crate::app::name_the_pair(tr, tr.setup_by_open_keys),
PlayedBy::Ai(level) => fill(tr.setup_by_ai, &[("l", tr.bot_levels[level.index()])]),
PlayedBy::Bot => tr.setup_by_bot.to_string(),
}
}
pub fn status_text(tr: &crate::app::i18n::Tr, by: PlayedBy) -> &'static str {
match by {
PlayedBy::Keys(_) | PlayedBy::Pad { joined: false, .. } => tr.setup_status[0],
PlayedBy::Pad { joined: true, .. } => tr.setup_status[1],
PlayedBy::Open { .. } => tr.setup_status[2],
PlayedBy::Ai(_) | PlayedBy::Bot => "",
}
}
pub fn seats_line(
tr: &crate::app::i18n::Tr,
open: &[u8],
level: BotLevel,
pad_to_join: bool,
) -> (String, bool) {
if !open.is_empty() {
let list: Vec<String> = open
.iter()
.map(|&seat| crate::app::seat_label(tr, seat))
.collect();
return (
fill(
tr.setup_open_line,
&[
("list", &list.join(", ")),
("l", tr.bot_levels[level.index()]),
],
),
true,
);
}
match pad_to_join {
true => (tr.setup_join_hint.to_string(), false),
false => (String::new(), false),
}
}
pub fn seat_hint(tr: &crate::app::i18n::Tr, device: LastDevice, seat: u8) -> String {
if !seat_turns(seat) {
return String::new();
}
let keys = match device {
LastDevice::Keyboard => "A/D".to_string(),
LastDevice::Pad => crate::app::gamepad::scheme::Pad::Left.name(tr).to_string(),
};
fill(tr.setup_seat_hint, &[("k", &keys)])
}
pub fn unnamed(tr: &crate::app::i18n::Tr, device: LastDevice) -> String {
use crate::app::gamepad::scheme::{Does, button_name};
let face = crate::app::gamepad::Face::MatchSetup {
step: Step::Players,
join: true,
partners: false,
};
match (device, button_name(&face, Does::Rename, tr)) {
(LastDevice::Pad, Some(button)) => tr.match_name_empty.replacen("Tab", &button, 1),
_ => tr.match_name_empty.to_string(),
}
}
pub fn p2_pressed(keys: &ButtonInput<KeyCode>, settings: &GameSettings) -> bool {
p2_only_keys(settings)
.into_iter()
.any(|key| keys.just_pressed(key))
}
pub fn p2_only_keys(settings: &GameSettings) -> Vec<KeyCode> {
use crate::app::cursor::keymap;
let menu = [
KeyCode::Enter,
KeyCode::NumpadEnter,
KeyCode::Escape,
KeyCode::Tab,
KeyCode::Backspace,
];
let commit = settings.commit_in_play(true);
let p1: Vec<KeyCode> = keymap(settings, 0, commit).keys().collect();
keymap(settings, 1, commit)
.keys()
.filter(|key| !p1.contains(key) && !menu.contains(key))
.collect()
}
pub fn bot_seat_list(config: &MatchConfig) -> Vec<u8> {
(0..config.seats)
.filter(|&seat| config.controller(seat) == SeatController::Bot)
.collect()
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct MatchStart<'w> {
tournament: ResMut<'w, crate::app::tournament::Tournament>,
next_screen: ResMut<'w, NextState<Screen>>,
bots: ResMut<'w, crate::app::bot_seats::BotSeats>,
}
impl MatchStart<'_> {
fn launch(&mut self, config: &mut MatchConfig) {
config.armed = true;
*self.tournament = if config.series.is_series() {
crate::app::tournament::Tournament::start(config.series)
} else {
crate::app::tournament::Tournament::default()
};
self.next_screen.set(Screen::Versus);
}
fn bots_in(&mut self, config: &MatchConfig, tr: &crate::app::i18n::Tr) -> bool {
use crate::app::bot_seats::{DoorFor, Doorway};
let seats = bot_seat_list(config);
if seats.is_empty() {
self.bots.door = None;
return true;
}
if self.bots.door.is_none() {
match Doorway::open() {
Ok(door) => self.bots.door = Some(door),
Err(e) => {
self.bots.feedback = fill(tr.door_could_not_listen, &[("e", &e.to_string())]);
return false;
}
}
}
let Some(door) = &mut self.bots.door else {
return false;
};
door.seat_bots(&seats);
door.poll();
if door.ready() {
return true;
}
self.bots.door_for = DoorFor::Couch;
false
}
}
pub fn seat_the_match(
config: &mut MatchConfig,
open: &[u8],
preview_seed: u64,
beaches: &CustomBeaches,
roll: u64,
) {
for &seat in open {
config.controllers[usize::from(seat)] = SeatController::Ai(config.ai_level);
config.filled_at_start[usize::from(seat)] = true;
}
match config.random {
true => {
draw_map(config, beaches, roll);
config.seed = None;
}
false => config.seed = Some(preview_seed),
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct SetupPads<'w, 's> {
pub seats: ResMut<'w, PadSeats>,
pub live: Query<'w, 's, Entity, With<Gamepad>>,
}
impl SetupPads<'_, '_> {
pub fn seated(&self, config: &MatchConfig, settings: &GameSettings) -> Vec<SeatPad> {
let live: Vec<Entity> = self.live.iter().collect();
self.seats.seat_pads(&live, settings, &config.human_seats())
}
}
pub fn match_setup_input(
(keys, caps): (Res<ButtonInput<KeyCode>>, Res<crate::app::keycaps::KeyCaps>),
mut typed: MessageReader<bevy::input::keyboard::KeyboardInput>,
mut menu: ResMut<MatchMenu>,
dials: Dials,
mut settings: ResMut<GameSettings>,
start: MatchStart,
mut pads: SetupPads,
) {
let Dials {
mut config,
beaches,
} = dials;
let mut start = start;
let pad_start = pads.seats.take_start();
if start.bots.door_for.card_up() {
if start
.bots
.door
.as_ref()
.is_some_and(crate::app::bot_seats::Doorway::ready)
{
start.bots.door_for = crate::app::bot_seats::DoorFor::Nothing;
start.bots.feedback.clear();
launch_the_table(
&mut config,
&menu,
&mut start,
&mut pads,
&settings,
&beaches,
);
}
typed.clear();
return;
}
if let Some(seat) = menu.naming {
let row = config.name_row(seat);
type_a_name(row, &mut typed, &keys, &mut settings, &mut menu);
return;
}
typed.clear();
let p2 = p2_pressed(&keys, &settings);
if p2 {
pads.seats.p2_sat_down();
}
let partner = !p2
&& caps
.key_for(crate::app::gamepad::scheme::PARTNER_CAP)
.is_some_and(|key| keys.just_pressed(key));
let hooked = crate::app::dev::launch() && !std::mem::replace(&mut menu.launched, true);
if hooked {
start_match(
&mut config,
&menu,
&mut start,
&mut pads,
&settings,
&beaches,
);
return;
}
match menu.step {
Step::Game => game_keys(
&keys,
&mut menu,
&mut config,
&mut settings,
&beaches,
&mut start,
&mut pads,
),
Step::Map => map_keys(&keys, &mut menu, &mut config, &beaches),
Step::Players => players_keys(
(&keys, pad_start, partner),
&mut menu,
&mut config,
&settings,
&beaches,
&mut start,
&mut pads,
),
}
}
fn game_keys(
keys: &ButtonInput<KeyCode>,
menu: &mut MatchMenu,
config: &mut MatchConfig,
settings: &mut GameSettings,
beaches: &CustomBeaches,
start: &mut MatchStart,
pads: &mut SetupPads,
) {
if keys.just_pressed(KeyCode::Escape) {
start.next_screen.set(Screen::Menu);
return;
}
if menu_ui::enter(keys) {
open_map_step(menu, config, beaches);
return;
}
menu.game_row = menu_ui::nav(keys, menu.game_row, GameRow::ALL.len());
let Some(turn) = menu_ui::left_right(keys) else {
return;
};
let row = GameRow::ALL[menu.game_row];
turn_game_row(row, turn, config, settings, beaches);
if row == GameRow::Players {
pads.seats.table_set_by_hand();
}
}
fn open_map_step(menu: &mut MatchMenu, config: &MatchConfig, beaches: &CustomBeaches) {
put_the_cursor_on_the_pick(menu, config, beaches);
menu.step = Step::Map;
}
pub fn put_the_cursor_on_the_pick(
menu: &mut MatchMenu,
config: &MatchConfig,
beaches: &CustomBeaches,
) {
let picked = Slot::Card(picked_card(config, beaches));
let slots = map_slots(beaches);
menu.slot = slots
.iter()
.position(|(_, slot)| *slot == picked)
.unwrap_or(0);
menu.scroll = MapPage::of(&slots).scrolled(menu.scroll, menu.slot);
}
pub const CARD_COLUMNS: usize = 4;
pub const MOSAIC: [MapChoice; 4] = [
MapChoice::Classic,
MapChoice::GenLarge,
MapChoice::GenOcean,
MapChoice::GenXl,
];
#[derive(Clone, PartialEq, Debug)]
pub struct PageRow {
pub section: MapSection,
pub slots: std::ops::Range<usize>,
pub top: f32,
pub first: bool,
}
#[derive(Clone, PartialEq, Debug)]
pub struct MapPage {
pub rows: Vec<PageRow>,
pub headings: [f32; 3],
pub height: f32,
}
impl MapPage {
pub fn of(slots: &[(MapSection, Slot)]) -> MapPage {
use super::ui::{HEADING_H, SECTION_GAP, TILE_GAP, TILE_H};
let mut rows = Vec::new();
let mut headings = [0.0; 3];
let mut y = 0.0;
for section in MapSection::ALL {
let places: Vec<usize> = slots
.iter()
.enumerate()
.filter(|(_, (of, _))| *of == section)
.map(|(index, _)| index)
.collect();
if section.index() > 0 {
y += SECTION_GAP;
}
headings[section.index()] = y;
y += HEADING_H;
for (n, row) in places.chunks(CARD_COLUMNS).enumerate() {
if n > 0 {
y += TILE_GAP;
}
rows.push(PageRow {
section,
slots: row[0]..row[row.len() - 1] + 1,
top: y,
first: n == 0,
});
y += TILE_H;
}
}
MapPage {
rows,
headings,
height: y,
}
}
pub fn places(&self) -> usize {
self.rows.last().map_or(0, |row| row.slots.end)
}
pub fn row_of(&self, slot: usize) -> usize {
self.rows
.iter()
.position(|row| row.slots.contains(&slot))
.unwrap_or(self.rows.len().saturating_sub(1))
}
pub fn scrolled(&self, scroll: f32, slot: usize) -> f32 {
use super::ui::{TILE_H, VIEW_H};
let Some(row) = self.rows.get(self.row_of(slot)) else {
return 0.0;
};
let top = match row.first {
true => self.headings[row.section.index()],
false => row.top,
};
let bottom = row.top + TILE_H;
if top < scroll {
return top;
}
if bottom <= scroll + VIEW_H + 0.5 {
return scroll;
}
self.headings
.iter()
.copied()
.chain(self.rows.iter().filter(|row| !row.first).map(|row| row.top))
.filter(|&line| line + VIEW_H + 0.5 >= bottom)
.fold(top, f32::min)
}
pub fn whole_in_view(scroll: f32, top: f32, height: f32) -> bool {
top >= scroll - 0.5 && top + height <= scroll + super::ui::VIEW_H + 0.5
}
pub fn slot_in_view(&self, scroll: f32, slot: usize) -> bool {
self.rows
.get(self.row_of(slot))
.is_some_and(|row| Self::whole_in_view(scroll, row.top, super::ui::TILE_H))
}
pub fn heading_in_view(&self, scroll: f32, section: MapSection) -> bool {
Self::whole_in_view(scroll, self.headings[section.index()], super::ui::HEADING_H)
}
pub fn window(&self, scroll: f32) -> menu_ui::scroll_bar::ScrollWindow {
menu_ui::scroll_bar::ScrollWindow {
first: scroll,
shown: super::ui::VIEW_H,
total: self.height,
}
}
pub fn step(&self, at: usize, (dx, dy): (i32, i32)) -> usize {
let mut at = at.min(self.places().saturating_sub(1));
let moved = at as i64 + i64::from(dx);
if dx != 0 && (0..self.places() as i64).contains(&moved) {
at = moved as usize;
}
if dy != 0 {
let row = self.row_of(at);
let column = at - self.rows[row].slots.start;
let target = usize::try_from(row as i64 + i64::from(dy))
.ok()
.and_then(|target| self.rows.get(target));
if let Some(target) = target {
at = (target.slots.start + column).min(target.slots.end - 1);
}
}
at
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MapPress {
Nothing,
Back,
Chosen(Card),
}
pub fn card_to_pick(
menu: &MatchMenu,
config: &MatchConfig,
beaches: &CustomBeaches,
) -> Option<Card> {
let slots = map_slots(beaches);
match slots[menu.slot.min(slots.len() - 1)].1 {
Slot::Card(card) if card_open(card, config.seats, beaches, menu.online) => Some(card),
Slot::Card(_) | Slot::MakeOne => None,
}
}
pub fn map_page_keys(
keys: &ButtonInput<KeyCode>,
menu: &mut MatchMenu,
config: &mut MatchConfig,
beaches: &CustomBeaches,
) -> MapPress {
if keys.just_pressed(KeyCode::Escape) {
return MapPress::Back;
}
let slots = map_slots(beaches);
let at = menu.slot.min(slots.len() - 1);
let open = |card: Card| card_open(card, config.seats, beaches, menu.online);
if menu_ui::enter(keys) {
return match card_to_pick(menu, config, beaches) {
Some(card) => {
pick_card(config, card, beaches);
MapPress::Chosen(card)
}
None => MapPress::Nothing,
};
}
let pressed = |codes: [KeyCode; 2]| codes.iter().any(|&code| keys.just_pressed(code));
let dx = i32::from(pressed([KeyCode::KeyD, KeyCode::ArrowRight]))
- i32::from(pressed([KeyCode::KeyA, KeyCode::ArrowLeft]));
let dy = i32::from(pressed([KeyCode::KeyS, KeyCode::ArrowDown]))
- i32::from(pressed([KeyCode::KeyW, KeyCode::ArrowUp]));
if (dx, dy) == (0, 0) {
return MapPress::Nothing;
}
let page = MapPage::of(&slots);
menu.slot = page.step(at, (dx, dy));
menu.scroll = page.scrolled(menu.scroll, menu.slot);
if let Slot::Card(card) = slots[menu.slot].1
&& open(card)
{
pick_card(config, card, beaches);
}
MapPress::Nothing
}
fn map_keys(
keys: &ButtonInput<KeyCode>,
menu: &mut MatchMenu,
config: &mut MatchConfig,
beaches: &CustomBeaches,
) {
match map_page_keys(keys, menu, config, beaches) {
MapPress::Back => menu.step = Step::Game,
MapPress::Chosen(_) => menu.step = Step::Players,
MapPress::Nothing => {}
}
}
fn players_keys(
(keys, pad_start, partner): (&ButtonInput<KeyCode>, bool, bool),
menu: &mut MatchMenu,
config: &mut MatchConfig,
settings: &GameSettings,
beaches: &CustomBeaches,
start: &mut MatchStart,
pads: &mut SetupPads,
) {
if keys.just_pressed(KeyCode::Escape) {
open_map_step(menu, config, beaches);
return;
}
menu.seat = menu.seat.min(config.seats - 1);
if keys.just_pressed(KeyCode::Tab) {
menu.naming = Some(menu.seat);
return;
}
if menu_ui::enter(keys) || pad_start {
start_match(config, menu, start, pads, settings, beaches);
return;
}
if partner {
pick_partners(menu, config, settings, pads);
return;
}
let rows: Vec<u8> = seat_rows(setup_teams(settings, config), config.seats)
.into_iter()
.filter(|&seat| seat < config.seats)
.collect();
let at = rows.iter().position(|&seat| seat == menu.seat).unwrap_or(0);
menu.seat = rows[menu_ui::nav(keys, at, rows.len())];
if let Some(turn) = menu_ui::left_right(keys) {
turn_seat(config, menu.seat, turn);
pads.seats.table_set_by_hand();
}
}
pub fn seat_rows(mode: crate::app::teams::TeamMode, seats: u8) -> Vec<u8> {
match mode {
crate::app::teams::TeamMode::Solo => (0..MAX_PLAYERS as u8).collect(),
crate::app::teams::TeamMode::Pairs | crate::app::teams::TeamMode::Trios => {
crate::app::lineup::seat_order(mode, seats)
.into_iter()
.map(|(seat, _)| seat)
.collect()
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum P2Pin {
Keys,
Pad(u8),
KeptForKeys,
KeptForPad(u8),
}
pub fn p2_pin(
config: &MatchConfig,
settings: &GameSettings,
pads: &[SeatPad],
p2_here: bool,
) -> Option<P2Pin> {
if config.seats < 2 {
return None;
}
let by = played_by(config, 1, pads, p2_here, settings);
let seated = matches!(by, PlayedBy::Keys(_) | PlayedBy::Pad { .. });
let keys = match seated {
true => P2Pin::Keys,
false => P2Pin::KeptForKeys,
};
match settings.seat_input[1] {
SeatInput::Keys => Some(keys),
SeatInput::Pad(n) if seated => Some(P2Pin::Pad(n)),
SeatInput::Pad(n) => Some(P2Pin::KeptForPad(n)),
SeatInput::Auto if p2_here => Some(keys),
SeatInput::Auto => (by == PlayedBy::Open { keys: true }).then_some(P2Pin::KeptForKeys),
}
}
fn pinned_seats(pin: Option<P2Pin>) -> Vec<u8> {
pin.map(|_| 1).into_iter().collect()
}
fn pick_partners(
menu: &mut MatchMenu,
config: &mut MatchConfig,
settings: &GameSettings,
pads: &mut SetupPads,
) {
let mode = setup_teams(settings, config);
let pin = p2_pin(
config,
settings,
&pads.seated(config, settings),
pads.seats.p2_at_keys(),
);
let pinned = pinned_seats(pin);
let Some(pick) = crate::app::partners::next_pick(
mode,
config.seats,
menu.seat,
(&[0], &pinned),
&config.people.0,
) else {
return;
};
for &pair in &pick.swaps {
swap_seats(config, settings, pads, pair);
}
crate::app::partners::reseated(&mut config.people.0, &pick);
menu.seat = pick.moved(menu.seat);
}
pub fn swap_seats(
config: &mut MatchConfig,
settings: &GameSettings,
pads: &mut SetupPads,
(a, b): (u8, u8),
) {
let live: Vec<Entity> = pads.live.iter().collect();
pads.seats
.swap_seats(&live, settings, &config.human_seats(), (a, b));
let (a, b) = (usize::from(a), usize::from(b));
config.controllers.swap(a, b);
config.filled_at_start.swap(a, b);
}
pub fn partner_line(
tr: &crate::app::i18n::Tr,
device: LastDevice,
(config, settings): (&MatchConfig, &GameSettings),
seat: u8,
pin: Option<P2Pin>,
) -> Option<String> {
use crate::app::teams::TeamMode;
let set = settings.team_mode;
let mode = setup_teams(settings, config);
let names = config.seat_names(&settings.names);
let name = |seat: u8| crate::app::name_or_label(&names, tr, seat);
if mode == TeamMode::Solo {
return (set != TeamMode::Solo)
.then(|| crate::app::teams::row_value(tr, set, config.seats, true));
}
let (_, held) = crate::app::partners::picks(mode, config.seats, seat, &[0], &pinned_seats(pin));
if held && let Some(pin) = pin {
let n = |n: u8| (n + 1).to_string();
return Some(match pin {
P2Pin::Keys => fill(tr.setup_pinned_keys, &[("p", &name(1))]),
P2Pin::Pad(pad) => fill(tr.setup_pinned_pad, &[("p", &name(1)), ("n", &n(pad))]),
P2Pin::KeptForKeys => fill(tr.setup_kept_keys, &[("p", &name(1))]),
P2Pin::KeptForPad(pad) => fill(tr.setup_kept_pad, &[("p", &name(1)), ("n", &n(pad))]),
});
}
let face = crate::app::gamepad::Face::MatchSetup {
step: Step::Players,
join: false,
partners: true,
};
let key = crate::app::gamepad::scheme::spelled(
tr,
device,
&face,
crate::app::gamepad::scheme::Does::Partners,
&crate::app::gamepad::scheme::PARTNER_CAP.to_string(),
);
let hint = match mode {
TeamMode::Pairs => tr.setup_partner_hint,
TeamMode::Solo | TeamMode::Trios => tr.setup_teammates_hint,
};
Some(fill(hint, &[("k", &key), ("p", &name(seat))]))
}
fn start_match(
config: &mut MatchConfig,
menu: &MatchMenu,
start: &mut MatchStart,
pads: &mut SetupPads,
settings: &GameSettings,
beaches: &CustomBeaches,
) {
if start.bots_in(config, settings.tr()) {
launch_the_table(config, menu, start, pads, settings, beaches);
}
}
fn launch_the_table(
config: &mut MatchConfig,
menu: &MatchMenu,
start: &mut MatchStart,
pads: &mut SetupPads,
settings: &GameSettings,
beaches: &CustomBeaches,
) {
let open = open_seats(
config,
&pads.seated(config, settings),
pads.seats.p2_at_keys(),
settings,
);
seat_the_match(
config,
&open,
menu.seed,
beaches,
crate::app::clock::fresh_seed(),
);
if !open.is_empty() {
pads.seats.table_set_by_hand();
}
start.launch(config);
}
fn type_a_name(
row: u8,
typed: &mut MessageReader<bevy::input::keyboard::KeyboardInput>,
keys: &ButtonInput<KeyCode>,
settings: &mut GameSettings,
menu: &mut MatchMenu,
) {
use crate::app::typing::{Keystroke, keystrokes};
let ends = [
KeyCode::Enter,
KeyCode::NumpadEnter,
KeyCode::Escape,
KeyCode::Tab,
];
for stroke in keystrokes(typed, &ends) {
match stroke {
Keystroke::Erase => settings.pop_name_char(row),
Keystroke::Char(ch) => settings.push_name_char(row, ch),
Keystroke::Done(_) => {}
}
}
let done = menu_ui::enter(keys)
|| keys.just_pressed(KeyCode::Escape)
|| keys.just_pressed(KeyCode::Tab);
if done {
settings.tidy_name(row);
menu.naming = None;
}
}