extern crate num as num_renamed; use std::io; use std::time::{Duration, Instant}; use crossterm::event; use crossterm::event::{Event, KeyCode}; use sea_battle_backend::consts::{ MAX_BOATS_NUMBER, MAX_MAP_HEIGHT, MAX_MAP_WIDTH, MAX_STRIKE_TIMEOUT, }; use tui::backend::Backend; use tui::layout::{Constraint, Direction, Layout, Margin}; use tui::style::{Color, Style}; use tui::widgets::*; use tui::{Frame, Terminal}; use sea_battle_backend::data::GameRules; use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE}; use crate::ui_screens::popup_screen::show_screen_too_small_popup; use crate::ui_screens::select_bot_type_screen::SelectBotTypeScreen; use crate::ui_screens::utils::centered_rect_size; use crate::ui_screens::ScreenResult; use crate::ui_widgets::button_widget::ButtonWidget; use crate::ui_widgets::checkbox_widget::CheckboxWidget; use crate::ui_widgets::text_editor_widget::TextEditorWidget; #[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Eq, PartialEq)] enum EditingField { MapWidth = 0, MapHeight, BoatsList, StrikeTimeout, BoatsCanTouch, PlayerContinueOnHit, BotType, Cancel, OK, } pub struct GameRulesConfigurationScreen { rules: GameRules, curr_field: EditingField, } impl GameRulesConfigurationScreen { pub fn new(rules: GameRules) -> Self { Self { rules, curr_field: EditingField::OK, } } pub fn show( mut self, terminal: &mut Terminal, ) -> io::Result> { let mut last_tick = Instant::now(); loop { terminal.draw(|f| self.ui(f))?; let timeout = TICK_RATE .checked_sub(last_tick.elapsed()) .unwrap_or_else(|| Duration::from_secs(0)); if event::poll(timeout)? { let mut cursor_pos = self.curr_field as i32; if let Event::Key(key) = event::read()? { match key.code { // Quit app KeyCode::Char('q') => return Ok(ScreenResult::Canceled), // Navigate between fields KeyCode::Up | KeyCode::Left => cursor_pos -= 1, KeyCode::Down | KeyCode::Right | KeyCode::Tab => cursor_pos += 1, // Submit results KeyCode::Enter => { if self.curr_field == EditingField::BotType { if let ScreenResult::Ok(t) = SelectBotTypeScreen::default().show(terminal)? { self.rules.bot_type = t; } } if self.curr_field == EditingField::Cancel { return Ok(ScreenResult::Canceled); } if self.curr_field == EditingField::OK && self.rules.is_valid() { return Ok(ScreenResult::Ok(self.rules)); } } KeyCode::Char(' ') => { if self.curr_field == EditingField::BoatsCanTouch { self.rules.boats_can_touch = !self.rules.boats_can_touch; } if self.curr_field == EditingField::PlayerContinueOnHit { self.rules.player_continue_on_hit = !self.rules.player_continue_on_hit; } } KeyCode::Backspace => { if self.curr_field == EditingField::MapWidth { self.rules.map_width /= 10; } if self.curr_field == EditingField::MapHeight { self.rules.map_height /= 10; } if self.curr_field == EditingField::BoatsList && !self.rules.boats_list().is_empty() { self.rules.remove_last_boat(); } if self.curr_field == EditingField::StrikeTimeout { match self.rules.strike_timeout.unwrap_or(0) / 10 { 0 => self.rules.strike_timeout = None, v => self.rules.strike_timeout = Some(v), } } } KeyCode::Char(c) if ('0'..='9').contains(&c) => { let val = c.to_string().parse::().unwrap_or_default(); if self.curr_field == EditingField::MapWidth && self.rules.map_width <= MAX_MAP_WIDTH { self.rules.map_width *= 10; self.rules.map_width += val; } if self.curr_field == EditingField::MapHeight && self.rules.map_height <= MAX_MAP_HEIGHT { self.rules.map_height *= 10; self.rules.map_height += val; } if self.curr_field == EditingField::BoatsList && self.rules.boats_list().len() < MAX_BOATS_NUMBER { self.rules.add_boat(val); } if self.curr_field == EditingField::StrikeTimeout { let mut timeout = self.rules.strike_timeout.unwrap_or(0); if timeout <= MAX_STRIKE_TIMEOUT { timeout *= 10; timeout += val as u64; self.rules.strike_timeout = Some(timeout); } } } _ => {} } } // Apply new cursor position self.curr_field = if cursor_pos < 0 { EditingField::OK } else { num_renamed::FromPrimitive::from_u64(cursor_pos as u64) .unwrap_or(EditingField::MapWidth) } } if last_tick.elapsed() >= TICK_RATE { last_tick = Instant::now(); } } } fn ui(&mut self, f: &mut Frame) { let (w, h) = (50, 23); if f.size().width < w || f.size().height < h { show_screen_too_small_popup(f); return; } let area = centered_rect_size(w, h, &f.size()); let block = Block::default() .title("📓 Game rules") .borders(Borders::ALL); f.render_widget(block, area); let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Length(3), // Map width Constraint::Length(3), // Map height Constraint::Length(3), // Boats list Constraint::Length(3), // Strike timeout Constraint::Length(1), // Boats can touch Constraint::Length(1), // Player continue on hit Constraint::Length(3), // Bot type Constraint::Length(1), // Margin Constraint::Length(1), // Buttons Constraint::Length(1), // Error message (if any) ]) .split(area.inner(&Margin { horizontal: 2, vertical: 1, })); let editor = TextEditorWidget::new( "Map width", &self.rules.map_width.to_string(), self.curr_field == EditingField::MapWidth, ); f.render_widget(editor, chunks[EditingField::MapWidth as usize]); let editor = TextEditorWidget::new( "Map height", &self.rules.map_height.to_string(), self.curr_field == EditingField::MapHeight, ); f.render_widget(editor, chunks[EditingField::MapHeight as usize]); let editor = TextEditorWidget::new( "Boats list", &self .rules .boats_list() .iter() .map(usize::to_string) .collect::>() .join("; "), self.curr_field == EditingField::BoatsList, ); f.render_widget(editor, chunks[EditingField::BoatsList as usize]); let editor = TextEditorWidget::new( "Strike timeout (0 to disable)", &self.rules.strike_timeout.unwrap_or(0).to_string(), self.curr_field == EditingField::StrikeTimeout, ); f.render_widget(editor, chunks[EditingField::StrikeTimeout as usize]); let editor = CheckboxWidget::new( "Boats can touch", self.rules.boats_can_touch, self.curr_field == EditingField::BoatsCanTouch, ); f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]); let editor = CheckboxWidget::new( "Player continue on hit", self.rules.player_continue_on_hit, self.curr_field == EditingField::PlayerContinueOnHit, ); f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]); // Select bot type let bot_type_text = format!("Bot type: {}", self.rules.bot_type.description().name); let text = Paragraph::new(bot_type_text.as_str()).style( match self.curr_field == EditingField::BotType { false => Style::default(), true => Style::default().fg(HIGHLIGHT_COLOR), }, ); f.render_widget( text, chunks[EditingField::BotType as usize].inner(&Margin { horizontal: 0, vertical: 1, }), ); // Buttons let buttons_chunk = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .split(chunks[EditingField::OK as usize]); let button = ButtonWidget::new("Cancel", self.curr_field == EditingField::Cancel); f.render_widget(button, buttons_chunk[0]); let button = ButtonWidget::new("OK", self.curr_field == EditingField::OK) .set_disabled(!self.rules.is_valid()); f.render_widget(button, buttons_chunk[1]); // Error message (if any) if let Some(msg) = self.rules.get_errors().first() { let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap()); let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red)); f.render_widget(err, area); } } }