Add navigation
This commit is contained in:
parent
966582e0c2
commit
52b862df01
244
src/game.rs
244
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<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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
src/main.rs
43
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<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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user