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 crossterm::event::{Event, KeyCode};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
@ -10,15 +12,25 @@ use ratatui::{
|
|||||||
use crate::geometry::{Grid, P2, V2};
|
use crate::geometry::{Grid, P2, V2};
|
||||||
|
|
||||||
pub struct GameModel {
|
pub struct GameModel {
|
||||||
|
path: PathBuf,
|
||||||
player_pos: P2<isize>,
|
player_pos: P2<isize>,
|
||||||
room: Room,
|
room: Room,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum GameEvent {
|
pub enum GameEvent {
|
||||||
Quit,
|
Quit,
|
||||||
Nop,
|
Nop,
|
||||||
PlayerMove(V2<isize>),
|
PlayerMove(V2<isize>),
|
||||||
PlayerDash(V2<isize>),
|
PlayerDash(V2<isize>),
|
||||||
|
Interact,
|
||||||
|
Navigate(NavigationTarget),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum NavigationTarget {
|
||||||
|
Path(PathBuf),
|
||||||
|
Parent,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameEvent {
|
impl GameEvent {
|
||||||
@ -35,6 +47,8 @@ impl GameEvent {
|
|||||||
Char('K') => return GameEvent::PlayerDash(V2::new(0, -1)),
|
Char('K') => return GameEvent::PlayerDash(V2::new(0, -1)),
|
||||||
Char('H') => return GameEvent::PlayerDash(V2::new(-1, 0)),
|
Char('H') => return GameEvent::PlayerDash(V2::new(-1, 0)),
|
||||||
Char('L') => 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 {
|
struct Room {
|
||||||
tiles: Grid<Tile>,
|
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 {
|
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 {
|
Self {
|
||||||
player_pos: P2::new(1, 1),
|
player_pos: P2::new(10, 2),
|
||||||
room: Room {
|
room: Room {
|
||||||
tiles: {
|
tiles: {
|
||||||
let w = 30;
|
let w = 20 + max_entry_length;
|
||||||
let h = 12;
|
let h = entries.len() + 4;
|
||||||
let mut tiles = Grid::from_fn(w, h, |x, y| {
|
let mut tiles = Grid::from_fn(w, h, |x, y| {
|
||||||
let is_vertical_boundary = [0, w - 1].contains(&x);
|
let is_vertical_boundary = [0, w - 1].contains(&x);
|
||||||
let is_horizontal_boundary = [0, h - 1].contains(&y);
|
let is_horizontal_boundary = [0, h - 1].contains(&y);
|
||||||
|
|
||||||
if is_vertical_boundary || is_horizontal_boundary {
|
if is_vertical_boundary || is_horizontal_boundary {
|
||||||
Tile::Wall
|
Tile::new(TileStyle::Wall)
|
||||||
} else {
|
} else {
|
||||||
Tile::Floor
|
Tile::new(TileStyle::Floor)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
tiles
|
if let Some(_) = &path.parent() {
|
||||||
.get_mut(P2::new(1, 1))
|
tiles.get_mut(P2::new(2, 2)).map(|tile| {
|
||||||
.map(|tile| *tile = Tile::Ladder);
|
*tile = Tile::new(TileStyle::Portal(Portal {
|
||||||
|
kind: PortalKind::Ladder,
|
||||||
|
})).with_action(GameEvent::Navigate(NavigationTarget::Parent))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
tiles
|
for (i, entry) in entries.iter().enumerate() {
|
||||||
.get_mut(P2::new(5, 2))
|
if entry.is_dir() {
|
||||||
.map(|tile| *tile = Tile::Amphora);
|
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);
|
for (j, c) in entry.file_name().unwrap().to_string_lossy().chars().enumerate() {
|
||||||
|
tiles
|
||||||
tiles.get_mut(P2::new(7, 6)).map(|tile| *tile = Tile::Water);
|
.get_mut(P2::new(11 + j as isize, 2 + i as isize))
|
||||||
tiles.get_mut(P2::new(8, 6)).map(|tile| *tile = Tile::Water);
|
.map(|tile| *tile = Tile::new(TileStyle::Char(c)));
|
||||||
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);
|
|
||||||
|
|
||||||
tiles
|
tiles
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(self, event: Event) -> Option<Self> {
|
pub fn update(self, event: Event) -> Result<Self, PathBuf> {
|
||||||
match GameEvent::from_crossterm(event) {
|
self.update_game(GameEvent::from_crossterm(event))
|
||||||
GameEvent::Quit => None,
|
}
|
||||||
GameEvent::Nop => Some(self),
|
|
||||||
|
fn update_game(self, event: GameEvent) -> Result<Self, PathBuf> {
|
||||||
|
|
||||||
|
match event {
|
||||||
|
GameEvent::Quit => Err(self.path),
|
||||||
|
GameEvent::Nop => Ok(self),
|
||||||
GameEvent::PlayerMove(direction) => {
|
GameEvent::PlayerMove(direction) => {
|
||||||
let player_pos = self.player_pos + direction;
|
let player_pos = self.player_pos + direction;
|
||||||
if let Some(target_tile) = self.room.tiles.get(player_pos)
|
if let Some(target_tile) = self.room.tiles.get(player_pos)
|
||||||
&& target_tile.may_enter()
|
&& target_tile.may_enter()
|
||||||
{
|
{
|
||||||
Some(Self { player_pos, ..self })
|
Ok(Self { player_pos, ..self })
|
||||||
} else {
|
} else {
|
||||||
Some(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GameEvent::PlayerDash(direction) => {
|
GameEvent::PlayerDash(direction) => {
|
||||||
@ -114,16 +185,34 @@ impl GameModel {
|
|||||||
let mut player_pos = self.player_pos;
|
let mut player_pos = self.player_pos;
|
||||||
for _ in 0..max_dash_tiles {
|
for _ in 0..max_dash_tiles {
|
||||||
let next_pos = player_pos + direction;
|
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;
|
player_pos = next_pos;
|
||||||
if target_tile.may_interact() {
|
if target_tile.dashtination() {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
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);
|
frame.render_widget(camera, camera_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_tiles(&self) -> Grid<Tile> {
|
fn render_tiles(&self) -> Grid<TileStyle> {
|
||||||
let mut tiles = self.room.tiles.clone();
|
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
|
tiles
|
||||||
.get_mut(self.player_pos)
|
.get_mut(self.player_pos)
|
||||||
.map(|tile| *tile = Tile::Player);
|
.map(|tile| *tile = TileStyle::Player);
|
||||||
|
|
||||||
tiles
|
tiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Tile {
|
enum TileStyle {
|
||||||
Floor,
|
Floor,
|
||||||
Ladder,
|
|
||||||
Player,
|
Player,
|
||||||
Amphora,
|
|
||||||
Frog,
|
|
||||||
Water,
|
|
||||||
Wall,
|
Wall,
|
||||||
|
Portal(Portal),
|
||||||
|
Char(char),
|
||||||
|
Box,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Portal {
|
||||||
|
kind: PortalKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum PortalKind {
|
||||||
|
Ladder,
|
||||||
Portal,
|
Portal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tile {
|
impl TileStyle {
|
||||||
fn render(&self, cell: &mut Cell) {
|
fn render(&self, cell: &mut Cell) {
|
||||||
match self {
|
match self {
|
||||||
Tile::Floor => {}
|
TileStyle::Floor => {}
|
||||||
Tile::Ladder => {
|
TileStyle::Player => {
|
||||||
cell.set_symbol("Ħ");
|
|
||||||
}
|
|
||||||
Tile::Player => {
|
|
||||||
cell.set_symbol("▲̶͈̊");
|
cell.set_symbol("▲̶͈̊");
|
||||||
}
|
}
|
||||||
Tile::Amphora => {
|
TileStyle::Wall => {
|
||||||
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 => {
|
|
||||||
cell.set_bg(Color::DarkGray);
|
cell.set_bg(Color::DarkGray);
|
||||||
}
|
}
|
||||||
Tile::Portal => {
|
TileStyle::Portal(portal) => match portal.kind {
|
||||||
cell.set_symbol("@").set_fg(Color::LightBlue);
|
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/main.rs
39
src/main.rs
@ -1,31 +1,46 @@
|
|||||||
mod game;
|
mod game;
|
||||||
mod geometry;
|
mod geometry;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use crossterm::event;
|
use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}};
|
||||||
use ratatui::DefaultTerminal;
|
use ratatui::{
|
||||||
|
Terminal,
|
||||||
|
prelude::{Backend, CrosstermBackend},
|
||||||
|
};
|
||||||
|
|
||||||
use game::GameModel;
|
use game::GameModel;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
execute!(std::io::stderr(), EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
|
|
||||||
let mut terminal = ratatui::init();
|
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
|
||||||
let result = main_loop(&mut terminal);
|
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<()> {
|
fn main_loop<T: Backend>(terminal: &mut Terminal<T>) -> Result<PathBuf> {
|
||||||
let mut model = GameModel::new();
|
let mut model = GameModel::new(PathBuf::from("."));
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| model.render(frame))?;
|
||||||
model.render(frame)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let term_event = event::read()?;
|
let term_event = event::read()?;
|
||||||
match model.update(term_event) {
|
match model.update(term_event) {
|
||||||
Some(new_model) => model = new_model,
|
Ok(new_model) => model = new_model,
|
||||||
None => break Ok(()),
|
Err(path) => break Ok(path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user