use super::*;
use crate::app::i18n::fill;
use crate::app::menu_ui;
use crate::app::settings::GameSettings;
#[derive(Resource, Default)]
pub struct MatchMenu {
pub selected: usize,
pub naming: Option<u8>,
}
#[derive(Component)]
pub struct MatchRow(pub usize);
#[derive(Component)]
pub struct MatchCell(pub usize, pub Row, pub menu_ui::Half);
#[derive(Component)]
pub struct MatchUi;
#[derive(Component)]
pub struct MatchPadInfo(pub bool);
#[derive(Component)]
pub struct MatchBeachNote;
pub fn enter_match_setup(
mut commands: Commands,
settings: Res<GameSettings>,
mut menu: ResMut<MatchMenu>,
mut config: ResMut<MatchConfig>,
beaches: Res<CustomBeaches>,
) {
menu.selected = 0;
menu.naming = None;
crate::app::match_setup::settle_map(&mut config, &beaches);
let tr = settings.tr();
commands
.spawn((MatchUi, menu_ui::between_bars()))
.with_children(|wrap| {
let (mut node, fill, edge, shadow) = menu_ui::screen_card();
node.height = Val::Px(
2.0 * menu_ui::CARD_PAD_Y
+ menu_ui::HEADING_H
+ ROWS as f32 * (menu_ui::ROW_H + menu_ui::ROW_GAP),
);
node.justify_content = JustifyContent::FlexStart;
wrap.spawn((node, fill, edge, shadow))
.with_children(|card| {
card.spawn(menu_ui::heading(tr.match_heading, true));
for row in 0..ROWS {
card.spawn((MatchRow(row), menu_ui::card_row()))
.with_children(|line| {
for (half, width) in [
(menu_ui::Half::Label, LABEL_W),
(menu_ui::Half::Value, VALUE_W),
] {
line.spawn((
MatchCell(row, Row::ALL[row], half),
menu_ui::cell(width, ROW_FONT),
));
}
});
}
});
wrap.spawn((
MatchBeachNote,
Node {
height: Val::Px(20.0),
..default()
},
Text::new(""),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(0.65)),
));
for is_list in [false, true] {
wrap.spawn((
MatchPadInfo(is_list),
Text::new(""),
TextFont {
font_size: FontSize::Px(15.0),
..default()
},
TextLayout::no_wrap(),
TextColor(palette::PARCHMENT.with_alpha(0.40)),
));
}
});
}
pub(super) const LABEL_W: f32 = 220.0;
pub(super) const VALUE_W: f32 = 428.0;
pub(super) const ROW_FONT: f32 = 19.0;
pub fn update_match_pad_info(
seats: Res<crate::app::gamepad::PadSeats>,
config: Res<MatchConfig>,
settings: Res<GameSettings>,
beaches: Res<CustomBeaches>,
mut rows: Query<(&MatchPadInfo, &mut Text)>,
mut note: Query<&mut Text, (With<MatchBeachNote>, Without<MatchPadInfo>)>,
) {
let tr = settings.tr();
if let Ok(mut text) = note.single_mut() {
let line = crate::app::match_setup::beaches_note(&config, tr, &beaches).unwrap_or_default();
menu_ui::set_text(&mut text, &line);
}
let humans = config.seats - config.bots;
for (info, mut text) in &mut rows {
let line = if info.0 {
if seats.0.is_empty() {
String::new()
} else {
let list: Vec<String> = (0..seats.0.len())
.map(|i| {
let seat = humans.saturating_sub(1 + i as u8);
crate::app::seat_label(tr, seat)
})
.collect();
fill(tr.match_pad_joined, &[("list", &list.join(", "))])
}
} else {
tr.match_pad_hint.to_string()
};
menu_ui::set_text(&mut text, &line);
}
}
pub(super) fn ai_seat(config: &MatchConfig, slot: u8) -> Option<u8> {
(slot < config.bots).then(|| config.seats - 1 - slot)
}
pub(super) fn cycle_ai_level(config: &mut MatchConfig, slot: u8, right: bool) {
if let Some(seat) = ai_seat(config, slot) {
config.bot_levels[seat as usize] = config.bot_levels[seat as usize].cycled(right);
}
}
pub(super) fn live_rows(config: &MatchConfig) -> [bool; ROWS] {
std::array::from_fn(|row| match Row::ALL[row] {
Row::BotLevel(slot) => ai_seat(config, slot).is_some(),
Row::Name(seat) => seat < config.seats,
Row::Players | Row::Bots | Row::Map | Row::Gulls | Row::Round | Row::Mode => true,
})
}
#[allow(clippy::too_many_arguments)]
pub fn match_setup_input(
keys: Res<ButtonInput<KeyCode>>,
beaches: Res<CustomBeaches>,
mut typed: MessageReader<bevy::input::keyboard::KeyboardInput>,
mut menu: ResMut<MatchMenu>,
mut config: ResMut<MatchConfig>,
mut settings: ResMut<GameSettings>,
mut tournament: ResMut<crate::app::tournament::Tournament>,
mut next_screen: ResMut<NextState<Screen>>,
) {
if let Some(seat) = menu.naming {
type_a_name(seat, &mut typed, &keys, &mut settings, &mut menu);
return;
}
typed.clear();
if keys.just_pressed(KeyCode::Escape) {
next_screen.set(Screen::Menu);
return;
}
if keys.just_pressed(KeyCode::Tab)
&& let Row::Name(seat) = Row::ALL[menu.selected]
{
menu.naming = Some(seat);
return;
}
if keys.just_pressed(KeyCode::Enter) {
config.armed = true;
*tournament = if config.series {
crate::app::tournament::Tournament::start()
} else {
crate::app::tournament::Tournament::default()
};
next_screen.set(Screen::Versus);
return;
}
menu.selected = menu_ui::nav_live(&keys, menu.selected, &live_rows(&config));
let Some(right) = menu_ui::left_right(&keys) else {
return;
};
match Row::ALL[menu.selected] {
Row::Players => {
let seats = i32::from(config.seats) + if right { 1 } else { -1 };
config.seats = seats.clamp(2, MAX_PLAYERS as i32) as u8;
config.bots = config.bots.min(config.seats - 1);
crate::app::match_setup::settle_map(&mut config, &beaches);
}
Row::Bots => {
let bots = i32::from(config.bots) + if right { 1 } else { -1 };
config.bots = bots.clamp(0, i32::from(config.seats) - 1) as u8;
}
Row::BotLevel(slot) => cycle_ai_level(&mut config, slot, right),
Row::Map => {
crate::app::match_setup::cycle_map(&mut config, right, &beaches);
if config.map.size().0 < WIDE_ENOUGH {
config.seats = config.seats.min(CLASSIC_SEATS);
config.bots = config.bots.min(config.seats - 1);
}
}
Row::Gulls => config.gulls = config.gulls.cycled(right),
Row::Round => config.round = config.round.cycled(right),
Row::Mode => config.series = !config.series,
Row::Name(_) => {}
}
}
fn type_a_name(
seat: u8,
typed: &mut MessageReader<bevy::input::keyboard::KeyboardInput>,
keys: &ButtonInput<KeyCode>,
settings: &mut GameSettings,
menu: &mut MatchMenu,
) {
for event in typed.read() {
if !event.state.is_pressed() {
continue;
}
let done = matches!(
event.key_code,
KeyCode::Enter | KeyCode::NumpadEnter | KeyCode::Escape | KeyCode::Tab
);
if matches!(event.key_code, KeyCode::Backspace | KeyCode::Delete) {
settings.pop_name_char(seat);
} else if !done {
for ch in event.text.iter().flat_map(|text| text.chars()) {
settings.push_name_char(seat, ch);
}
}
}
let done = keys.just_pressed(KeyCode::Enter)
|| keys.just_pressed(KeyCode::NumpadEnter)
|| keys.just_pressed(KeyCode::Escape)
|| keys.just_pressed(KeyCode::Tab);
if done {
settings.tidy_name(seat);
menu.naming = None;
}
}
pub(super) fn row_text(
tr: &crate::app::i18n::Tr,
config: &MatchConfig,
settings: &GameSettings,
beaches: &CustomBeaches,
naming: Option<u8>,
row: Row,
) -> (String, String) {
let dial = |value: &str| format!("< {value} >");
match row {
Row::Players => (
tr.match_players.to_string(),
dial(&config.seats.to_string()),
),
Row::Bots => {
let humans = config.seats - config.bots;
let rest = if humans == 1 {
tr.human_one.to_string()
} else {
fill(tr.human_many, &[("n", &humans.to_string())])
};
(
tr.match_ai.to_string(),
format!("{} ({rest})", dial(&config.bots.to_string())),
)
}
Row::BotLevel(slot) => {
let seat = ai_seat(config, slot).unwrap_or(0);
let who = crate::app::seat_label(tr, seat);
(
format!("{} {who}", tr.match_ai_level),
dial(tr.bot_levels[config.bot_levels[seat as usize].index()]),
)
}
Row::Map => (
tr.match_map.to_string(),
dial(&crate::app::match_setup::map_label(config, tr, beaches)),
),
Row::Gulls => (
tr.match_gulls.to_string(),
dial(tr.gull_names[config.gulls.index()]),
),
Row::Round => (
tr.match_round.to_string(),
dial(tr.round_names[config.round.index()]),
),
Row::Mode => (
tr.match_mode.to_string(),
dial(tr.mode_names[usize::from(config.series)]),
),
Row::Name(seat) => {
let who = crate::app::seat_label(tr, seat);
let given = settings.names[usize::from(seat)].clone();
let value = if naming == Some(seat) {
format!("{given}_ {}", tr.match_name_typing)
} else if given.is_empty() {
tr.match_name_empty.to_string()
} else {
given
};
(format!("{} {who}", tr.match_name), value)
}
}
}
pub fn update_match_ui(
config: Res<MatchConfig>,
menu: Res<MatchMenu>,
settings: Res<GameSettings>,
beaches: Res<CustomBeaches>,
mut cells: Query<(&MatchCell, &mut Text, &mut TextColor)>,
mut rows: Query<(&MatchRow, &mut BackgroundColor, &mut Node)>,
) {
let tr = settings.tr();
let live = live_rows(&config);
for (cell, mut text, mut color) in &mut cells {
if !live[cell.0] {
continue;
}
let (label, value) = row_text(tr, &config, &settings, &beaches, menu.naming, cell.1);
let half = match cell.2 {
menu_ui::Half::Label => label,
menu_ui::Half::Value => value,
};
menu_ui::set_text(&mut text, &half);
menu_ui::set_color(
&mut color,
if cell.0 == menu.selected {
Color::WHITE
} else {
palette::PARCHMENT.with_alpha(0.80)
},
);
}
for (row, mut fill, mut node) in &mut rows {
let display = if live[row.0] {
Display::Flex
} else {
Display::None
};
if node.display != display {
node.display = display;
}
let ground = menu_ui::band(row.0 == menu.selected && live[row.0]);
if fill.0 != ground {
fill.0 = ground;
}
}
}