From 52b862df0199c743a8808fd55eb509d568bcf379 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Mon, 1 Dec 2025 08:43:41 +0100 Subject: [PATCH] Add navigation --- src/game.rs | 244 ++++++++++++++++++++++++++++++++++------------------ src/main.rs | 43 ++++++--- 2 files changed, 191 insertions(+), 96 deletions(-) diff --git a/src/game.rs b/src/game.rs index 135e93e..28f1923 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,3 +1,5 @@ +use std::{fs, path::PathBuf}; + use crossterm::event::{Event, KeyCode}; use ratatui::{ Frame, @@ -10,15 +12,25 @@ use ratatui::{ use crate::geometry::{Grid, P2, V2}; pub struct GameModel { + path: PathBuf, player_pos: P2, room: Room, } +#[derive(Clone)] pub enum GameEvent { Quit, Nop, PlayerMove(V2), PlayerDash(V2), + Interact, + Navigate(NavigationTarget), +} + +#[derive(Clone)] +pub enum NavigationTarget { + Path(PathBuf), + Parent, } impl GameEvent { @@ -35,6 +47,8 @@ impl GameEvent { Char('K') => return GameEvent::PlayerDash(V2::new(0, -1)), Char('H') => return GameEvent::PlayerDash(V2::new(-1, 0)), Char('L') => return GameEvent::PlayerDash(V2::new(1, 0)), + Char('e') => return GameEvent::Interact, + Char('r') => return GameEvent::Navigate(NavigationTarget::Parent), _ => (), } } @@ -46,67 +60,124 @@ impl GameEvent { struct Room { tiles: Grid, } -impl Room {} + +#[derive(Clone)] +struct Tile { + style: TileStyle, + action: Option, +} + +impl Tile { + fn new(style: TileStyle) -> Self { + Self { + style, + action: None, + } + } + + fn with_action(self, action: GameEvent) -> Self { + Self { + action: Some(action), + ..self + } + } + + fn may_enter(&self) -> bool { + match self.style { + TileStyle::Wall => false, + _ => true, + } + } + + fn dashtination(&self) -> bool { + self.action.is_some() + } +} impl GameModel { - pub fn new() -> Self { + pub fn new(path: PathBuf) -> Self { + let path = path.canonicalize().unwrap(); + let mut entries: Vec = fs::read_dir(&path) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect(); + entries.sort(); + let max_entry_length = entries + .iter() + .map(|entry| entry.file_name().unwrap().to_string_lossy().chars().count()) + .max() + .unwrap_or(0); + Self { - player_pos: P2::new(1, 1), + player_pos: P2::new(10, 2), room: Room { tiles: { - let w = 30; - let h = 12; + let w = 20 + max_entry_length; + let h = entries.len() + 4; let mut tiles = Grid::from_fn(w, h, |x, y| { let is_vertical_boundary = [0, w - 1].contains(&x); let is_horizontal_boundary = [0, h - 1].contains(&y); if is_vertical_boundary || is_horizontal_boundary { - Tile::Wall + Tile::new(TileStyle::Wall) } else { - Tile::Floor + Tile::new(TileStyle::Floor) } }); - tiles - .get_mut(P2::new(1, 1)) - .map(|tile| *tile = Tile::Ladder); + if let Some(_) = &path.parent() { + tiles.get_mut(P2::new(2, 2)).map(|tile| { + *tile = Tile::new(TileStyle::Portal(Portal { + kind: PortalKind::Ladder, + })).with_action(GameEvent::Navigate(NavigationTarget::Parent)) + }); + } - tiles - .get_mut(P2::new(5, 2)) - .map(|tile| *tile = Tile::Amphora); + for (i, entry) in entries.iter().enumerate() { + if entry.is_dir() { + tiles.get_mut(P2::new(10, 2 + i as isize)).map(|tile| { + *tile = Tile::new(TileStyle::Portal(Portal { + kind: PortalKind::Portal, + })).with_action(GameEvent::Navigate(NavigationTarget::Path(entry.clone()))) + }); + } else { + tiles.get_mut(P2::new(10, 2 + i as isize)).map(|tile| { + *tile = Tile::new(TileStyle::Box) + }); + } - tiles.get_mut(P2::new(8, 5)).map(|tile| *tile = Tile::Frog); - - tiles.get_mut(P2::new(7, 6)).map(|tile| *tile = Tile::Water); - tiles.get_mut(P2::new(8, 6)).map(|tile| *tile = Tile::Water); - tiles.get_mut(P2::new(9, 6)).map(|tile| *tile = Tile::Water); - - tiles.get_mut(P2::new(8, 7)).map(|tile| *tile = Tile::Wall); - tiles.get_mut(P2::new(9, 7)).map(|tile| *tile = Tile::Wall); - - tiles.get_mut(P2::new(10, 2)).map(|tile| *tile = Tile::Portal); - tiles.get_mut(P2::new(18, 2)).map(|tile| *tile = Tile::Portal); - tiles.get_mut(P2::new(10, 8)).map(|tile| *tile = Tile::Portal); - tiles.get_mut(P2::new(18, 8)).map(|tile| *tile = Tile::Portal); + for (j, c) in entry.file_name().unwrap().to_string_lossy().chars().enumerate() { + tiles + .get_mut(P2::new(11 + j as isize, 2 + i as isize)) + .map(|tile| *tile = Tile::new(TileStyle::Char(c))); + } + } tiles }, }, + + path, } } - pub fn update(self, event: Event) -> Option { - match GameEvent::from_crossterm(event) { - GameEvent::Quit => None, - GameEvent::Nop => Some(self), + pub fn update(self, event: Event) -> Result { + self.update_game(GameEvent::from_crossterm(event)) + } + + fn update_game(self, event: GameEvent) -> Result { + + match event { + GameEvent::Quit => Err(self.path), + GameEvent::Nop => Ok(self), GameEvent::PlayerMove(direction) => { let player_pos = self.player_pos + direction; if let Some(target_tile) = self.room.tiles.get(player_pos) && target_tile.may_enter() { - Some(Self { player_pos, ..self }) + Ok(Self { player_pos, ..self }) } else { - Some(self) + Ok(self) } } GameEvent::PlayerDash(direction) => { @@ -114,16 +185,34 @@ impl GameModel { let mut player_pos = self.player_pos; for _ in 0..max_dash_tiles { let next_pos = player_pos + direction; - if let Some(target_tile) = self.room.tiles.get(next_pos) && target_tile.may_enter() { + if let Some(target_tile) = self.room.tiles.get(next_pos) + && target_tile.may_enter() + { player_pos = next_pos; - if target_tile.may_interact() { - break + if target_tile.dashtination() { + break; } } else { break; } } - Some(Self { player_pos, ..self }) + Ok(Self { player_pos, ..self }) + } + GameEvent::Interact => { + let opt_action = self.room.tiles.get(self.player_pos).and_then(|tile| tile.action.clone()); + + if let Some(action) = opt_action { + self.update_game(action) + } else { + Ok(self) + } + } + GameEvent::Navigate(target) => { + let path = match target { + NavigationTarget::Path(path) => path, + NavigationTarget::Parent => self.path.parent().unwrap().to_path_buf(), + }; + Ok(Self::new(path)) } } } @@ -137,74 +226,65 @@ impl GameModel { frame.render_widget(camera, camera_area); } - fn render_tiles(&self) -> Grid { - let mut tiles = self.room.tiles.clone(); + fn render_tiles(&self) -> Grid { + let mut tiles = Grid::from_fn(self.room.tiles.dims.x, + self.room.tiles.dims.y, |x, y| { + self.room.tiles.get(P2::new(x as isize, y as isize)).unwrap().style.clone() + }); tiles .get_mut(self.player_pos) - .map(|tile| *tile = Tile::Player); + .map(|tile| *tile = TileStyle::Player); tiles } } #[derive(Clone)] -enum Tile { +enum TileStyle { Floor, - Ladder, Player, - Amphora, - Frog, - Water, Wall, + Portal(Portal), + Char(char), + Box, +} + +#[derive(Clone)] +struct Portal { + kind: PortalKind, +} + +#[derive(Clone)] +enum PortalKind { + Ladder, Portal, } -impl Tile { +impl TileStyle { fn render(&self, cell: &mut Cell) { match self { - Tile::Floor => {} - Tile::Ladder => { - cell.set_symbol("Ħ"); - } - Tile::Player => { + TileStyle::Floor => {} + TileStyle::Player => { cell.set_symbol("▲̶͈̊"); } - Tile::Amphora => { - cell.set_symbol("⚱").set_fg(Color::LightRed); - } - Tile::Frog => { - // Alternative symbol: Ö̶͈ - cell.set_symbol("ä̃").set_fg(Color::Green); - } - Tile::Water => { - cell.set_symbol("≈") - .set_bg(Color::Blue) - .set_fg(Color::White); - } - Tile::Wall => { + TileStyle::Wall => { cell.set_bg(Color::DarkGray); } - Tile::Portal => { - cell.set_symbol("@").set_fg(Color::LightBlue); + TileStyle::Portal(portal) => match portal.kind { + PortalKind::Ladder => { + cell.set_symbol("Ħ"); + } + PortalKind::Portal => { + cell.set_symbol("@").set_fg(Color::LightBlue); + } + }, + TileStyle::Char(c) => { + cell.set_symbol(&c.to_string()); + } + TileStyle::Box => { + cell.set_symbol("⬕"); } - } - } - - fn may_enter(&self) -> bool { - match self { - Tile::Wall => false, - _ => true, - } - } - - fn may_interact(&self) -> bool { - match self { - Tile::Amphora => true, - Tile::Frog => true, - Tile::Ladder => true, - Tile::Portal => true, - _ => false, } } } diff --git a/src/main.rs b/src/main.rs index aa64287..82fc8d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,46 @@ mod game; mod geometry; +use std::path::PathBuf; + use color_eyre::eyre::Result; -use crossterm::event; -use ratatui::DefaultTerminal; +use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}}; +use ratatui::{ + Terminal, + prelude::{Backend, CrosstermBackend}, +}; use game::GameModel; fn main() -> Result<()> { - - let mut terminal = ratatui::init(); + enable_raw_mode()?; + execute!(std::io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; + + let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?; let result = main_loop(&mut terminal); - ratatui::restore(); - result + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + let path = result?; + println!("{}", path.display()); + Ok(()) } -fn main_loop(terminal: &mut DefaultTerminal) -> Result<()> { - let mut model = GameModel::new(); +fn main_loop(terminal: &mut Terminal) -> Result { + let mut model = GameModel::new(PathBuf::from(".")); loop { - terminal.draw(|frame| { - model.render(frame) - })?; + terminal.draw(|frame| model.render(frame))?; let term_event = event::read()?; match model.update(term_event) { - Some(new_model) => model = new_model, - None => break Ok(()), + Ok(new_model) => model = new_model, + Err(path) => break Ok(path), } } -} \ No newline at end of file +}