226 lines
7.5 KiB
Rust
226 lines
7.5 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 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::TICK_RATE;
|
|
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,
|
|
BoatsCanTouch,
|
|
PlayerContinueOnHit,
|
|
Cancel,
|
|
OK,
|
|
}
|
|
|
|
struct GameRulesConfigurationScreen {
|
|
rules: GameRules,
|
|
curr_field: EditingField,
|
|
}
|
|
|
|
pub fn configure_play_rules<B: Backend>(
|
|
rules: GameRules,
|
|
terminal: &mut Terminal<B>,
|
|
) -> io::Result<ScreenResult<GameRules>> {
|
|
let mut model = GameRulesConfigurationScreen {
|
|
rules,
|
|
curr_field: EditingField::OK,
|
|
};
|
|
|
|
let mut last_tick = Instant::now();
|
|
loop {
|
|
terminal.draw(|f| ui(f, &mut model))?;
|
|
|
|
let timeout = TICK_RATE
|
|
.checked_sub(last_tick.elapsed())
|
|
.unwrap_or_else(|| Duration::from_secs(0));
|
|
|
|
if crossterm::event::poll(timeout)? {
|
|
let mut cursor_pos = model.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 model.curr_field == EditingField::Cancel {
|
|
return Ok(ScreenResult::Canceled);
|
|
}
|
|
|
|
if model.curr_field == EditingField::OK && model.rules.is_valid() {
|
|
return Ok(ScreenResult::Ok(model.rules));
|
|
}
|
|
}
|
|
|
|
KeyCode::Char(' ') => {
|
|
if model.curr_field == EditingField::BoatsCanTouch {
|
|
model.rules.boats_can_touch = !model.rules.boats_can_touch;
|
|
}
|
|
|
|
if model.curr_field == EditingField::PlayerContinueOnHit {
|
|
model.rules.player_continue_on_hit =
|
|
!model.rules.player_continue_on_hit;
|
|
}
|
|
}
|
|
|
|
KeyCode::Backspace => {
|
|
if model.curr_field == EditingField::MapWidth {
|
|
model.rules.map_width /= 10;
|
|
}
|
|
|
|
if model.curr_field == EditingField::MapHeight {
|
|
model.rules.map_height /= 10;
|
|
}
|
|
|
|
if model.curr_field == EditingField::BoatsList
|
|
&& !model.rules.boats_list().is_empty()
|
|
{
|
|
model.rules.remove_last_boat();
|
|
}
|
|
}
|
|
|
|
KeyCode::Char(c) if ('0'..='9').contains(&c) => {
|
|
let val = c.to_string().parse::<usize>().unwrap_or_default();
|
|
|
|
if model.curr_field == EditingField::MapWidth {
|
|
model.rules.map_width *= 10;
|
|
model.rules.map_width += val;
|
|
}
|
|
|
|
if model.curr_field == EditingField::MapHeight {
|
|
model.rules.map_height *= 10;
|
|
model.rules.map_height += val;
|
|
}
|
|
|
|
if model.curr_field == EditingField::BoatsList {
|
|
model.rules.add_boat(val);
|
|
}
|
|
}
|
|
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Apply new cursor position
|
|
model.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>(f: &mut Frame<B>, model: &mut GameRulesConfigurationScreen) {
|
|
let area = centered_rect_size(50, 16, &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),
|
|
Constraint::Length(3),
|
|
Constraint::Length(3),
|
|
Constraint::Length(1),
|
|
Constraint::Length(1),
|
|
Constraint::Length(1),
|
|
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",
|
|
&model.rules.map_width.to_string(),
|
|
model.curr_field == EditingField::MapWidth,
|
|
);
|
|
f.render_widget(editor, chunks[EditingField::MapWidth as usize]);
|
|
|
|
let editor = TextEditorWidget::new(
|
|
"Map height",
|
|
&model.rules.map_height.to_string(),
|
|
model.curr_field == EditingField::MapHeight,
|
|
);
|
|
f.render_widget(editor, chunks[EditingField::MapHeight as usize]);
|
|
|
|
let editor = TextEditorWidget::new(
|
|
"Boats list",
|
|
&model
|
|
.rules
|
|
.boats_list()
|
|
.iter()
|
|
.map(usize::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join("; "),
|
|
model.curr_field == EditingField::BoatsList,
|
|
);
|
|
f.render_widget(editor, chunks[EditingField::BoatsList as usize]);
|
|
|
|
let editor = CheckboxWidget::new(
|
|
"Boats can touch",
|
|
model.rules.boats_can_touch,
|
|
model.curr_field == EditingField::BoatsCanTouch,
|
|
);
|
|
f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]);
|
|
|
|
let editor = CheckboxWidget::new(
|
|
"Player continue on hit",
|
|
model.rules.player_continue_on_hit,
|
|
model.curr_field == EditingField::PlayerContinueOnHit,
|
|
);
|
|
f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]);
|
|
|
|
// 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", model.curr_field == EditingField::Cancel);
|
|
f.render_widget(button, buttons_chunk[0]);
|
|
|
|
let button = ButtonWidget::new("OK", model.curr_field == EditingField::OK)
|
|
.set_disabled(!model.rules.is_valid());
|
|
f.render_widget(button, buttons_chunk[1]);
|
|
|
|
// Error message (if any)
|
|
if let Some(msg) = model.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);
|
|
}
|
|
}
|