mod chat; mod controls; use crossterm::event::{read, Event, KeyCode}; use tui::{backend::Backend, Frame, layout::Layout}; use crate::{game::Game, entity::{EntityId, EntityType}, world::Terrain}; use self::{chat::{Chat, ChatLine}, controls::Controls}; /** * |........................| * | Conversations/Selection| * | | * |........................| * | Available Options | * |........................| */ enum AppStatus { Initial, GuestSelection, TalkToGuest(Option), Debug, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum DebugView { Creatures, Items, Map, } struct DebugData<'a> { list: Vec>, list_state: tui::widgets::ListState, item_list: Vec>, item_list_state: tui::widgets::ListState, selected: DebugView, map_scroll: (u16, u16), } pub struct App<'a> { game: Game, status: AppStatus, // Guest Selection guest_list: Vec>, guest_list_state: tui::widgets::ListState, // Conversation conversation: Chat, conversation_scroll: u16, // Debug debug_data: Option>, } pub struct DefaultLayout { pub status: tui::layout::Rect, pub main: tui::layout::Rect, pub controls: tui::layout::Rect, } impl DefaultLayout { pub fn default(rect: tui::layout::Rect) -> DefaultLayout { let chunks = Layout::default() .direction(tui::layout::Direction::Vertical) .constraints( [ tui::layout::Constraint::Length(1), tui::layout::Constraint::Min(3), tui::layout::Constraint::Length(2) ] .as_ref(), ).split(rect); DefaultLayout { status: chunks[0], main: chunks[1], controls: chunks[2], } } } impl<'a> App<'a> { pub fn new(state: Game) -> App<'a> { App { game: state, guest_list: vec![], guest_list_state: tui::widgets::ListState::default(), status: AppStatus::Initial, conversation: Chat::new(), conversation_scroll: 0, debug_data: None, } } pub fn step(&mut self) -> bool { match &self.status { AppStatus::Initial => { // determine guests self.guest_list = self.game.state.guests().iter().map(|creature_id| { let creature = self.game.state.get_creature(*creature_id).unwrap(); tui::widgets::ListItem::new(creature.name.clone()) }).collect(); self.guest_list_state = tui::widgets::ListState::default(); self.guest_list_state.select(Some(0)); self.status = AppStatus::GuestSelection; true }, AppStatus::GuestSelection => { match read() { Ok(Event::Key(event)) => { match event.code { KeyCode::Up => { let selected = self.guest_list_state.selected().unwrap(); if selected > 0 { self.guest_list_state.select(Some(selected - 1)); } }, KeyCode::Down => { let selected = self.guest_list_state.selected().unwrap(); if selected < self.guest_list.len() - 1 { self.guest_list_state.select(Some(selected + 1)); } }, KeyCode::Enter => { if self.game.state.guests().len() > 0 { match self.guest_list_state.selected() { Some(selected) => { let guest_id = &self.game.state.guests()[selected]; self.conversation = Chat::new(); self.greet_guest(Some(*guest_id)); self.status = AppStatus::TalkToGuest(Some(*guest_id)); }, None => {} } } }, KeyCode::Char('?') => { self.open_debug(); }, KeyCode::Char('.') => { self.game.step(); self.status = AppStatus::Initial; }, KeyCode::Esc => { return false; }, _ => {} } }, _ => {} } true }, AppStatus::TalkToGuest(guest) => { match read() { Ok(Event::Key(event)) => { match event.code { KeyCode::Esc => { self.status = AppStatus::GuestSelection; }, KeyCode::Char('a') => { self.ask_business(*guest); }, KeyCode::Up => { self.conversation_scroll += 1; }, KeyCode::Down => { if self.conversation_scroll > 0 { self.conversation_scroll -= 1; } }, _ => {} } }, _ => {} } true }, AppStatus::Debug => { match read() { Ok(Event::Key(event)) => { match event.code { KeyCode::Esc => { self.status = AppStatus::GuestSelection; }, KeyCode::Up => { if self.debug_data.as_ref().unwrap().selected == DebugView::Map { let (x, y) = self.debug_data.as_ref().unwrap().map_scroll; self.debug_data.as_mut().unwrap().map_scroll = (x.saturating_sub(1), y); } else { let ref mut state = match self.debug_data.as_ref().unwrap().selected { DebugView::Creatures => &mut self.debug_data.as_mut().unwrap().list_state, DebugView::Items => &mut self.debug_data.as_mut().unwrap().item_list_state, _ => &mut self.debug_data.as_mut().unwrap().list_state, }; let selected = state.selected().unwrap(); if selected > 0 { state.select(Some(selected - 1)); } } }, KeyCode::Down => { if self.debug_data.as_ref().unwrap().selected == DebugView::Map { let (x, y) = self.debug_data.as_ref().unwrap().map_scroll; self.debug_data.as_mut().unwrap().map_scroll = (x + 1, y); } else { let mut max = 0; let ref mut state = match self.debug_data.as_ref().unwrap().selected { DebugView::Creatures => { max = self.debug_data.as_ref().unwrap().list.len() - 1; &mut self.debug_data.as_mut().unwrap().list_state }, DebugView::Items => { max = self.debug_data.as_ref().unwrap().item_list.len() - 1; &mut self.debug_data.as_mut().unwrap().item_list_state }, _ => { max = self.debug_data.as_ref().unwrap().list.len() - 1; &mut self.debug_data.as_mut().unwrap().list_state }, }; let selected = state.selected().unwrap(); if selected < max { state.select(Some(selected + 1)); } } }, KeyCode::Left => { if self.debug_data.as_ref().unwrap().selected == DebugView::Map { let (x, y) = self.debug_data.as_ref().unwrap().map_scroll; self.debug_data.as_mut().unwrap().map_scroll = (x, y.saturating_sub(1)); } }, KeyCode::Right => { if self.debug_data.as_ref().unwrap().selected == DebugView::Map { let (x, y) = self.debug_data.as_ref().unwrap().map_scroll; self.debug_data.as_mut().unwrap().map_scroll = (x, y + 1); } }, KeyCode::Char('i') => { self.debug_data.as_mut().unwrap().selected = DebugView::Items; }, KeyCode::Char('c') => { self.debug_data.as_mut().unwrap().selected = DebugView::Creatures; }, KeyCode::Char('m') => { self.debug_data.as_mut().unwrap().selected = DebugView::Map; }, _ => {} } }, _ => {} } true } } } fn open_debug(&mut self) { let mut list = vec![]; let mut item_list = vec![]; for (id, creature) in self.game.state.creatures.iter() { list.push(tui::widgets::ListItem::new(format!("{}: {} ({}) at {}", id.1, creature.name, creature.profession, creature.loc, ))); } for (id, item) in self.game.state.items.iter() { item_list.push(tui::widgets::ListItem::new(format!("{}: {} ({}) at {}", id.1, item.name, item.item_type, item.owner, ))); } self.debug_data = Some(DebugData { list, list_state: tui::widgets::ListState::default(), item_list, item_list_state: tui::widgets::ListState::default(), selected: DebugView::Creatures, map_scroll: (0, 0), }); self.debug_data.as_mut().unwrap().list_state.select(Some(0)); self.debug_data.as_mut().unwrap().item_list_state.select(Some(0)); self.status = AppStatus::Debug; } // fn default_layout(&self) -> Layout { // DefaultLayout::default() // } pub fn draw(&mut self, f: &mut Frame) { match &self.status { AppStatus::Initial => { self.draw_initial(f); }, AppStatus::GuestSelection => { self.draw_guest_selection(f); }, AppStatus::TalkToGuest(guest_id) => { self.draw_talk_to_guest(f, *guest_id); }, AppStatus::Debug => { self.draw_debug(f); } } } fn draw_status(&self, f: &mut Frame, rect: tui::layout::Rect) { let spans = tui::text::Spans::from(vec![ tui::text::Span::raw("Date: "), tui::text::Span::raw(format!("{}", self.game.state.time)), ]); let status_text = tui::widgets::Paragraph::new(spans) .block(tui::widgets::Block::default().borders(tui::widgets::Borders::LEFT | tui::widgets::Borders::RIGHT)) .style(tui::style::Style::default().fg(tui::style::Color::White)); f.render_widget(status_text, rect); } pub fn draw_initial(&mut self, _f: &mut Frame) {} pub fn draw_guest_selection(&mut self, f: &mut Frame) { let chunks = DefaultLayout::default(f.size()); let main_window = tui::widgets::List::new(self.guest_list.clone()) .block(tui::widgets::Block::default().title("Guests").borders(tui::widgets::Borders::ALL)) .style(tui::style::Style::default().fg(tui::style::Color::White)) .highlight_style(tui::style::Style::default().add_modifier(tui::style::Modifier::ITALIC)) .highlight_symbol(">>"); let mut binding = Controls::new(); let controls = binding .add("↑↓".to_owned(), "select guest".to_owned()) .add("⏎".to_owned(), "talk to guest".to_owned()) .add(".".to_owned(), "pass one day".to_owned()) .add("Esc".to_owned(), "quit".to_owned()) .render(); self.draw_status(f, chunks.status); f.render_stateful_widget(main_window, chunks.main, &mut self.guest_list_state); f.render_widget(controls, chunks.controls); } fn draw_talk_to_guest(&self, f: &mut Frame, guest: Option) { let chunks = DefaultLayout::default(f.size()); let guest = self.game.state.get_creature(guest.unwrap()).unwrap(); let key_style = tui::style::Style::default() .add_modifier(tui::style::Modifier::BOLD) .fg(tui::style::Color::Green) .bg(tui::style::Color::Black); let mut full_text = self.conversation.to_spans(); full_text.push(tui::text::Spans::from(vec![ tui::text::Span::styled("(a) ", key_style), tui::text::Span::raw("What's your business?\n\n"), ])); full_text.push(tui::text::Spans::from(vec![ tui::text::Span::styled("(b) ", key_style), tui::text::Span::raw("Let's trade?\n\n"), ])); full_text.push(tui::text::Spans::from(vec![ tui::text::Span::styled("(c) ", key_style), tui::text::Span::raw("Heard of anything interesting?\n\n"), ])); let text = tui::text::Text::from(full_text); let scroll: u16 = ((text.lines.len() as u16) ) .checked_sub( (chunks.main.height as u16).checked_sub(2).unwrap_or(0) // 2 lines for the border ) .unwrap_or(0) .checked_sub(self.conversation_scroll) .unwrap_or(0); let main_window = tui::widgets::Paragraph::new(text) .block(tui::widgets::Block::default().title(guest.name.clone()).borders(tui::widgets::Borders::ALL)) .style(tui::style::Style::default().fg(tui::style::Color::White)) .scroll((scroll, 0)); let mut binding = Controls::new(); let controls = binding .add("a-z".to_owned(), "Talk".to_owned()) .add("Esc".to_owned(), "Back".to_owned()) .render(); self.draw_status(f, chunks.status); f.render_widget(main_window, chunks.main); f.render_widget(controls, chunks.controls); } fn draw_debug(&mut self, f: &mut Frame) { let chunks = DefaultLayout::default(f.size()); let data = self.debug_data.as_mut().unwrap(); match data.selected { DebugView::Creatures => { let main_window = tui::widgets::List::new(data.list.clone()) .block(tui::widgets::Block::default().title("Guests").borders(tui::widgets::Borders::ALL)) .style(tui::style::Style::default().fg(tui::style::Color::White)) .highlight_style(tui::style::Style::default().add_modifier(tui::style::Modifier::ITALIC)) .highlight_symbol(">>"); f.render_stateful_widget(main_window, chunks.main, &mut data.list_state); }, DebugView::Items => { let main_window = tui::widgets::List::new(data.item_list.clone()) .block(tui::widgets::Block::default().title("Items").borders(tui::widgets::Borders::ALL)) .style(tui::style::Style::default().fg(tui::style::Color::White)) .highlight_style(tui::style::Style::default().add_modifier(tui::style::Modifier::ITALIC)) .highlight_symbol(">>"); f.render_stateful_widget(main_window, chunks.main, &mut data.item_list_state); }, DebugView::Map => { let mut rows = Vec::new(); for row in self.game.state.world.map.iter() { rows.push(tui::text::Spans::from( row.iter().map(|tile| { match tile.terrain { Terrain::Void => tui::text::Span::styled(" ", tui::style::Style::default().bg(tui::style::Color::Rgb(0, 0, 0)).fg(tui::style::Color::Rgb(0, 0, 0))), Terrain::DeepOcean => tui::text::Span::styled("~", tui::style::Style::default().bg(tui::style::Color::Rgb(0, 0, 128)).fg(tui::style::Color::Rgb(32, 32, 128))), Terrain::Ocean => tui::text::Span::styled("~", tui::style::Style::default().bg(tui::style::Color::Rgb(32, 32, 128)).fg(tui::style::Color::Rgb(128, 128, 128))), Terrain::Beach => tui::text::Span::styled("-", tui::style::Style::default().bg(tui::style::Color::Rgb(200, 200, 10)).fg(tui::style::Color::Rgb(100, 100, 10))), Terrain::Flats => tui::text::Span::styled("-", tui::style::Style::default().bg(tui::style::Color::Rgb(30, 150, 30)).fg(tui::style::Color::Rgb(30, 200, 30))), Terrain::Hills => tui::text::Span::styled("~", tui::style::Style::default().bg(tui::style::Color::Rgb(120, 150, 30)).fg(tui::style::Color::Rgb(120, 150, 30))), Terrain::Mountains => tui::text::Span::styled("A", tui::style::Style::default().bg(tui::style::Color::Rgb(120,120,120)).fg(tui::style::Color::Rgb(120,120,120))), Terrain::HighMountains => tui::text::Span::styled("A", tui::style::Style::default().bg(tui::style::Color::Rgb(200,200,200)).fg(tui::style::Color::Rgb(200,200,200))), } }).collect::>() )); } let main_window = tui::widgets::Paragraph::new( tui::text::Text::from(rows) ) .block(tui::widgets::Block::default().title("Map").borders(tui::widgets::Borders::ALL)) .style(tui::style::Style::default().fg(tui::style::Color::White)) .scroll(data.map_scroll); f.render_widget(main_window, chunks.main); }, } self.draw_status(f, chunks.status); let mut binding = Controls::new(); let controls = binding .add("↑↓".to_owned(), "select guest".to_owned()) .add("c".to_owned(), "Creatures".to_owned()) .add("i".to_owned(), "Items".to_owned()) .add("m".to_owned(), "Map".to_owned()) .add("Esc".to_owned(), "back".to_owned()) .render(); f.render_widget(controls, chunks.controls); } /** * Conversation */ fn greet_guest(&mut self, guest_id: Option) { let guest = self.game.state.get_creature(guest_id.unwrap()).unwrap(); self.conversation.add_line(ChatLine::new( (EntityType::Creature, 0), "You".to_owned(), "Greetings traveller!".to_owned(), false )); self.conversation.add_line(ChatLine::new( guest_id.unwrap(), guest.name.clone(), "Hello, I'm ".to_owned() + &guest.name + ", nice to meet you! " + "I'm a " + &guest.profession.to_string() + ".", false )); self.conversation_scroll = 0; } fn ask_business(&mut self, guest: Option) { let guest = self.game.state.get_creature(guest.unwrap()).unwrap(); self.conversation.add_line(ChatLine::new( (EntityType::Creature, 0), "You".to_owned(), "What's your business?".to_owned(), false )); self.conversation.add_line(ChatLine::new( guest.entity.id, guest.name.clone(), guest.say_agenda(& self.game.state), false )); self.conversation_scroll = 0; } }