All checks were successful
continuous-integration/drone/push Build is passing
301 lines
11 KiB
Rust
301 lines
11 KiB
Rust
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<B: Backend>(
|
|
mut self,
|
|
terminal: &mut Terminal<B>,
|
|
) -> io::Result<ScreenResult<GameRules>> {
|
|
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::<usize>().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<B: Backend>(&mut self, f: &mut Frame<B>) {
|
|
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::<Vec<_>>()
|
|
.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);
|
|
}
|
|
}
|
|
}
|