Add navigation

This commit is contained in:
Paul Brinkmeier 2025-12-01 08:43:41 +01:00
parent 966582e0c2
commit 52b862df01
2 changed files with 191 additions and 96 deletions

View File

@ -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<isize>,
room: Room,
}
#[derive(Clone)]
pub enum GameEvent {
Quit,
Nop,
PlayerMove(V2<isize>),
PlayerDash(V2<isize>),
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<Tile>,
}
impl Room {}
#[derive(Clone)]
struct Tile {
style: TileStyle,
action: Option<GameEvent>,
}
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<PathBuf> = 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<Self> {
match GameEvent::from_crossterm(event) {
GameEvent::Quit => None,
GameEvent::Nop => Some(self),
pub fn update(self, event: Event) -> Result<Self, PathBuf> {
self.update_game(GameEvent::from_crossterm(event))
}
fn update_game(self, event: GameEvent) -> Result<Self, PathBuf> {
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<Tile> {
let mut tiles = self.room.tiles.clone();
fn render_tiles(&self) -> Grid<TileStyle> {
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,
}
}
}

View File

@ -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<T: Backend>(terminal: &mut Terminal<T>) -> Result<PathBuf> {
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),
}
}
}
}