use std::io::Result; use std::io::stdout; use std::time::Instant; use crossterm::{ event::{self, KeyCode, KeyEventKind}, ExecutableCommand, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ prelude::{CrosstermBackend, Terminal}, widgets::Paragraph, }; use ratatui::prelude::*; use ratatui::widgets::{Block, Borders, BorderType, Wrap}; use ratatui::widgets::block::{Position, Title}; use whoami::realname; use crate::dungeon_slayer::PlayerStats; use crate::game::{Game, GameState}; use crate::level_widget::LevelWidget; use crate::player::Player; use crate::position::HasPosition; mod game; mod player; mod level; mod position; mod level_widget; mod level_generator; mod artifacts; mod monster; mod dungeon_slayer; /// length of a game frame in ms pub const FRAME_LENGTH: u64 = 100; // fn main() -> Result<()> { let player_stats = PlayerStats::create_random(); let mut game = Game::new(Player::new(realname().as_str(), player_stats)); stdout().execute(EnterAlternateScreen)?; enable_raw_mode()?; let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?; terminal.clear()?; let start_time = Instant::now(); let mut ticks = 0; loop { terminal.draw(|frame| { let mut area = frame.size(); frame.render_widget(Block::default().style(Style::default().bg(Color::Green)), area); // don't draw stuff except an info box if the terminal is too small (less than 80x25) // to prevent the read drawing code from crashing the game. if area.width < 80 || area.height < 25 { let block = Block::default() .title("Info") .borders(Borders::ALL) .border_style(Style::default().fg(Color::White)) .border_type(BorderType::Rounded) .style(Style::default().bg(Color::Black)); let paragraph = Paragraph::new("Terminal needs to be at leas 80x25!") .block(block) .wrap(Wrap { trim: true }); frame.render_widget(paragraph, area); return; } if area.width > 80 { area.x = (area.width - 80) / 2; area.width = 80; } if area.height > 25 { area.y = (area.height - 25) / 2; area.height = 25; } let map_area = Rect { x: area.x, y: area.y, width: level::LEVEL_WIDTH as u16, height: level::LEVEL_HEIGHT as u16, }; frame.render_stateful_widget(LevelWidget::new(game.uses_fog_of_war()), map_area, &mut game); let stats_area = Rect { x: area.x + 50, y: area.y, width: 30, height: 15, }; let block = Block::default() .title( Title::from( format!(" {} ", game.get_player().get_name())) .alignment(Alignment::Center) .position(Position::Top) ) .borders(Borders::TOP) .border_style(Style::default().fg(Color::White)) .border_type(BorderType::Rounded) .style(Style::default().bg(Color::Blue)); frame.render_widget( Paragraph::new(format!("Health: {}/{}\nExp: {}\nGold: {}\nLevel: {}", game.get_player().get_life(), game.get_player().get_max_life(), game.get_player().get_experience(), game.get_player().get_gold(), game.get_player().get_immutable_position().get_level())) .block(block).wrap(Wrap { trim: true }), stats_area, ); let messages_area = Rect { x: area.x + 50, y: area.y + 15, width: 30, height: 10, }; // Display the latest messages from the game to the user let block = Block::default() .title(Title::from(" messages ").alignment(Alignment::Center).position(Position::Top)) .borders(Borders::TOP) .border_style(Style::default().fg(Color::White)) .border_type(BorderType::Rounded) .style(Style::default().bg(Color::Blue)); let paragraph1 = if game.messages.is_empty() { "".to_string() } else { format!("> {}", game.messages.join("\n> ")) }; frame.render_widget( Paragraph::new(paragraph1).block(block).wrap(Wrap { trim: true }), messages_area, ); })?; let mut player_moved = false; if event::poll(std::time::Duration::from_millis(FRAME_LENGTH))? { if let event::Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('v') { game.messages.insert(0, format!("You are playing version '{}'.", env!("GIT_HASH")).to_string()); } // disabling the d key (disable fog of war) in release builds! #[cfg(debug_assertions)] if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('d') { game.messages.insert(0, "toggle fog of war!".to_string()); game.toggle_fog_of_war(); } if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { break; } if key.kind == KeyEventKind::Press { let new_pos = match key.code { KeyCode::Left => { player_moved = true; game.move_player(-1, 0) } KeyCode::Right => { player_moved = true; game.move_player(1, 0) } KeyCode::Up => { player_moved = true; game.move_player(0, -1) } KeyCode::Down => { player_moved = true; game.move_player(0, 1) } _ => { (0, 0) } }; if !game.player_fights_monster() { // player attacked monster but did not kill it game.move_player(new_pos.0, new_pos.1); } game.player_collects_artifact(); } } } game.update_level(ticks, player_moved); if game.get_game_state() != GameState::RUNNING { break; } ticks += 1; } let playtime = start_time.elapsed(); loop { let _ = terminal.draw(|frame| { let mut area = frame.size(); let w = area.width / 2; let h = area.height / 2; area.x += w - 20; area.y += h - 10; area.width = 40; area.height = 20; let block = Block::default() .title(Title::from(" Game ended ").alignment(Alignment::Center).position(Position::Top)) .title(Title::from("Press `q` to quit!").position(Position::Bottom)) .borders(Borders::ALL) .border_style(Style::default().fg(Color::White)) .border_type(BorderType::Rounded) .style(Style::default().bg(Color::Black)); let mut text = match game.get_game_state() { GameState::RUNNING => { "Quitting is for cowards! You'll better try again!".to_string() } GameState::LOST => { format!("Sorry, you died in the dungeon. Better luck next time!\nLast message:\n{}", game.messages[0]).to_string() } GameState::WON => { "Congratulation! You mastered your way through the dungeon and won the game.".to_string() } }; text += format!("\n\nYou gained {} experience.", game.get_player().get_experience()).as_str(); text += format!("\nYou collected {} gold.", game.get_player().get_gold()).as_str(); text += format!("\nYou played {} seconds.", playtime.as_secs()).as_str(); let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true }); frame.render_widget(paragraph, area); }); if event::poll(std::time::Duration::from_millis(16))? { if let event::Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { break; } } } } stdout().execute(LeaveAlternateScreen)?; disable_raw_mode()?; Ok(()) }