1use super::*;
8use crate::app::i18n::fill;
9use crate::app::menu_ui;
10use crate::app::settings::GameSettings;
11
12#[derive(Resource, Default)]
13pub struct MatchMenu {
14 pub selected: usize,
15 pub naming: Option<u8>,
18}
19
20#[derive(Component)]
23pub struct MatchRow(pub usize);
24
25#[derive(Component)]
27pub struct MatchCell(pub usize, pub Row, pub menu_ui::Half);
28
29#[derive(Component)]
30pub struct MatchUi;
31
32#[derive(Component)]
34pub struct MatchPadInfo(pub bool);
35
36#[derive(Component)]
40pub struct MatchBeachNote;
41
42pub fn enter_match_setup(
43 mut commands: Commands,
44 settings: Res<GameSettings>,
45 mut menu: ResMut<MatchMenu>,
46 mut config: ResMut<MatchConfig>,
47 beaches: Res<CustomBeaches>,
48) {
49 menu.selected = 0;
50 menu.naming = None;
51 crate::app::match_setup::settle_map(&mut config, &beaches);
55 let tr = settings.tr();
56 commands
57 .spawn((MatchUi, menu_ui::between_bars()))
58 .with_children(|wrap| {
59 let (mark, mut node, fill, edge, shadow) = menu_ui::screen_card();
65 node.height = Val::Px(
66 2.0 * menu_ui::CARD_PAD_Y
67 + menu_ui::HEADING_H
68 + ROWS as f32 * (menu_ui::ROW_H + menu_ui::ROW_GAP),
69 );
70 node.justify_content = JustifyContent::FlexStart;
71 wrap.spawn(Node {
72 align_items: AlignItems::Center,
73 ..default()
74 })
75 .with_children(|line| {
76 line.spawn((mark, node, fill, edge, shadow))
77 .with_children(|card| {
78 menu_ui::heading_row(card, tr.match_heading, None);
79 for row in 0..ROWS {
80 card.spawn((MatchRow(row), menu_ui::card_row()))
81 .with_children(|line| {
82 for (half, width) in [
83 (menu_ui::Half::Label, LABEL_W),
84 (menu_ui::Half::Value, VALUE_W),
85 ] {
86 line.spawn((
87 MatchCell(row, Row::ALL[row], half),
88 menu_ui::cell(width, ROW_FONT),
89 ));
90 }
91 });
92 }
93 });
94 });
95 wrap.spawn((
100 MatchBeachNote,
101 Node {
106 height: Val::Px(20.0),
107 ..default()
108 },
109 Text::new(""),
110 TextFont {
111 font_size: FontSize::Px(menu_ui::type_scale::BODY),
112 ..default()
113 },
114 TextLayout::no_wrap(),
115 TextColor(palette::PARCHMENT.with_alpha(0.65)),
116 ));
117 for is_list in [false, true] {
118 wrap.spawn((
119 MatchPadInfo(is_list),
120 Text::new(""),
121 TextFont {
122 font_size: FontSize::Px(menu_ui::type_scale::BODY),
123 ..default()
124 },
125 TextLayout::no_wrap(),
126 TextColor(palette::PARCHMENT.with_alpha(0.40)),
127 ));
128 }
129 });
130}
131
132pub(super) const LABEL_W: f32 = 220.0;
150pub(super) const VALUE_W: f32 = 428.0;
151pub(super) const ROW_FONT: f32 = menu_ui::type_scale::ROW;
154
155pub fn update_match_pad_info(
158 seats: Res<crate::app::gamepad::PadSeats>,
159 config: Res<MatchConfig>,
160 settings: Res<GameSettings>,
161 beaches: Res<CustomBeaches>,
162 mut rows: Query<(&MatchPadInfo, &mut Text)>,
163 mut note: Query<&mut Text, (With<MatchBeachNote>, Without<MatchPadInfo>)>,
164) {
165 let tr = settings.tr();
166 if let Ok(mut text) = note.single_mut() {
167 let line = crate::app::match_setup::beaches_note(&config, tr, &beaches).unwrap_or_default();
168 menu_ui::set_text(&mut text, &line);
169 }
170 let humans = config.seats - config.bots;
171 for (info, mut text) in &mut rows {
172 let line = if info.0 {
173 if seats.0.is_empty() {
174 String::new()
175 } else {
176 let list: Vec<String> = (0..seats.0.len())
178 .map(|i| {
179 let seat = humans.saturating_sub(1 + i as u8);
180 crate::app::seat_label(tr, seat)
181 })
182 .collect();
183 fill(tr.match_pad_joined, &[("list", &list.join(", "))])
184 }
185 } else {
186 tr.match_pad_hint.to_string()
187 };
188 menu_ui::set_text(&mut text, &line);
189 }
190}
191
192pub(super) fn ai_seat(config: &MatchConfig, slot: u8) -> Option<u8> {
195 (slot < config.bots).then(|| config.seats - 1 - slot)
196}
197
198pub(super) fn cycle_ai_level(config: &mut MatchConfig, slot: u8, turn: Turn) {
201 if let Some(seat) = ai_seat(config, slot) {
202 config.bot_levels[seat as usize] = config.bot_levels[seat as usize].cycled(turn);
203 }
204}
205
206pub(super) fn live_rows(config: &MatchConfig) -> [bool; ROWS] {
209 std::array::from_fn(|row| match Row::ALL[row] {
210 Row::BotLevel(slot) => ai_seat(config, slot).is_some(),
211 Row::Name(seat) => seat < config.seats,
212 Row::Players | Row::Bots | Row::Map | Row::Gulls | Row::Round | Row::Mode => true,
213 })
214}
215
216#[allow(clippy::too_many_arguments)]
217pub fn match_setup_input(
218 keys: Res<ButtonInput<KeyCode>>,
219 beaches: Res<CustomBeaches>,
220 mut typed: MessageReader<bevy::input::keyboard::KeyboardInput>,
221 mut menu: ResMut<MatchMenu>,
222 mut config: ResMut<MatchConfig>,
223 mut settings: ResMut<GameSettings>,
224 mut tournament: ResMut<crate::app::tournament::Tournament>,
225 mut next_screen: ResMut<NextState<Screen>>,
226) {
227 if let Some(seat) = menu.naming {
228 type_a_name(seat, &mut typed, &keys, &mut settings, &mut menu);
229 return;
230 }
231 typed.clear();
234 if keys.just_pressed(KeyCode::Escape) {
235 next_screen.set(Screen::Menu);
236 return;
237 }
238 if keys.just_pressed(KeyCode::Tab)
244 && let Row::Name(seat) = Row::ALL[menu.selected]
245 {
246 menu.naming = Some(seat);
247 return;
248 }
249 if menu_ui::enter(&keys) {
250 config.armed = true;
251 *tournament = if config.series.is_series() {
252 crate::app::tournament::Tournament::start(config.series)
253 } else {
254 crate::app::tournament::Tournament::default()
255 };
256 next_screen.set(Screen::Versus);
257 return;
258 }
259 menu.selected = menu_ui::nav_live(&keys, menu.selected, &live_rows(&config));
260 let Some(turn) = menu_ui::left_right(&keys) else {
261 return;
262 };
263 match Row::ALL[menu.selected] {
264 Row::Players => {
265 config.seats = crate::app::cycle::dial(config.seats, turn, 1, 2..=MAX_PLAYERS as u8);
266 config.bots = config.bots.min(config.seats - 1);
267 crate::app::match_setup::settle_map(&mut config, &beaches);
271 }
272 Row::Bots => {
273 config.bots = crate::app::cycle::dial(config.bots, turn, 1, 0..=config.seats - 1);
274 }
275 Row::BotLevel(slot) => cycle_ai_level(&mut config, slot, turn),
276 Row::Map => {
277 crate::app::match_setup::cycle_map(&mut config, turn, &beaches);
278 if config.map.size().0 < WIDE_ENOUGH {
281 config.seats = config.seats.min(CLASSIC_SEATS);
282 config.bots = config.bots.min(config.seats - 1);
283 }
284 }
285 Row::Gulls => config.gulls = config.gulls.cycled(turn),
286 Row::Round => config.round = config.round.cycled(turn),
287 Row::Mode => config.series = config.series.cycled(turn),
288 Row::Name(_) => {}
290 }
291}
292
293fn type_a_name(
298 seat: u8,
299 typed: &mut MessageReader<bevy::input::keyboard::KeyboardInput>,
300 keys: &ButtonInput<KeyCode>,
301 settings: &mut GameSettings,
302 menu: &mut MatchMenu,
303) {
304 use crate::app::typing::{Keystroke, keystrokes};
305 let ends = [
306 KeyCode::Enter,
307 KeyCode::NumpadEnter,
308 KeyCode::Escape,
309 KeyCode::Tab,
310 ];
311 for stroke in keystrokes(typed, &ends) {
312 match stroke {
313 Keystroke::Erase => settings.pop_name_char(seat),
314 Keystroke::Char(ch) => settings.push_name_char(seat, ch),
315 Keystroke::Done(_) => {}
318 }
319 }
320 let done = menu_ui::enter(keys)
324 || keys.just_pressed(KeyCode::Escape)
325 || keys.just_pressed(KeyCode::Tab);
326 if done {
327 settings.tidy_name(seat);
328 menu.naming = None;
329 }
330}
331
332pub(super) fn row_text(
339 tr: &crate::app::i18n::Tr,
340 config: &MatchConfig,
341 settings: &GameSettings,
342 beaches: &CustomBeaches,
343 naming: Option<u8>,
344 row: Row,
345) -> (String, String) {
346 let dial = |value: &str| format!("< {value} >");
347 match row {
348 Row::Players => (
349 tr.match_players.to_string(),
350 dial(&config.seats.to_string()),
351 ),
352 Row::Bots => {
353 let humans = config.seats - config.bots;
354 let rest = if humans == 1 {
355 tr.human_one.to_string()
356 } else {
357 fill(tr.human_many, &[("n", &humans.to_string())])
358 };
359 (
360 tr.match_ai.to_string(),
361 format!("{} ({rest})", dial(&config.bots.to_string())),
362 )
363 }
364 Row::BotLevel(slot) => {
365 let seat = ai_seat(config, slot).unwrap_or(0);
366 let who = crate::app::seat_label(tr, seat);
367 (
368 format!("{} {who}", tr.match_ai_level),
369 dial(tr.bot_levels[config.bot_levels[seat as usize].index()]),
370 )
371 }
372 Row::Map => (
376 tr.match_map.to_string(),
377 dial(&crate::app::match_setup::map_label(config, tr, beaches)),
378 ),
379 Row::Gulls => (
380 tr.match_gulls.to_string(),
381 dial(tr.gull_names[config.gulls.index()]),
382 ),
383 Row::Round => (
384 tr.match_round.to_string(),
385 dial(tr.round_names[config.round.index()]),
386 ),
387 Row::Mode => (
388 tr.match_mode.to_string(),
389 dial(tr.mode_names[config.series.index()]),
390 ),
391 Row::Name(seat) => {
392 let who = crate::app::seat_label(tr, seat);
393 let given = settings.names[usize::from(seat)].clone();
394 let value = if naming == Some(seat) {
395 format!("{given}_ {}", tr.match_name_typing)
398 } else if given.is_empty() {
399 tr.match_name_empty.to_string()
400 } else {
401 given
402 };
403 (format!("{} {who}", tr.match_name), value)
404 }
405 }
406}
407
408pub fn update_match_ui(
409 config: Res<MatchConfig>,
410 menu: Res<MatchMenu>,
411 settings: Res<GameSettings>,
412 beaches: Res<CustomBeaches>,
413 mut cells: Query<(&MatchCell, &mut Text, &mut TextColor)>,
414 mut rows: Query<(&MatchRow, &mut BackgroundColor, &mut Node)>,
415) {
416 let tr = settings.tr();
417 let live = live_rows(&config);
420 for (cell, mut text, mut color) in &mut cells {
421 if !live[cell.0] {
422 continue;
423 }
424 let (label, value) = row_text(tr, &config, &settings, &beaches, menu.naming, cell.1);
425 let half = match cell.2 {
426 menu_ui::Half::Label => label,
427 menu_ui::Half::Value => value,
428 };
429 menu_ui::set_text(&mut text, &half);
430 menu_ui::set_color(
431 &mut color,
432 if cell.0 == menu.selected {
433 Color::WHITE
434 } else {
435 palette::PARCHMENT.with_alpha(0.80)
436 },
437 );
438 }
439 for (row, mut fill, mut node) in &mut rows {
440 menu_ui::set_shown(&mut node, live[row.0]);
444 let ground = menu_ui::band(row.0 == menu.selected && live[row.0]);
445 menu_ui::set_bg(&mut fill, ground);
446 }
447}