aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuillaume Pasquet <dev@etenil.net>2020-03-14 16:53:10 +0000
committerGuillaume Pasquet <dev@etenil.net>2020-03-14 16:53:10 +0000
commit8c92d7c0aa1497e7d1a66cddcdacd04e43dba7f6 (patch)
treeffbfc495906fdb447074c1c1e3e4996432f83a14
parent9d1de3f314a2f63a8f58e3ad2018c5a32472a67a (diff)
parent219e32ad793e66a3111465ef02f3eb677f70277d (diff)
Merge branch 'merge-viewport'
-rw-r--r--.vscode/launch.json1
-rw-r--r--TODO.md21
-rw-r--r--src/events.rs10
-rw-r--r--src/main.rs72
-rw-r--r--src/state.rs109
-rw-r--r--src/viewport.rs182
6 files changed, 256 insertions, 139 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e2e657d..981f449 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -22,6 +22,7 @@
"program": "${workspaceRoot}/target/debug/roguelike",
"args": [],
"cwd": "${workspaceRoot}",
+ "terminal": "external"
}
]
} \ No newline at end of file
diff --git a/TODO.md b/TODO.md
index 9289b2d..981e42b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,4 +1,23 @@
+# TODO
+
- Add unit tests
- Disassociate level to view, move view around if terminal too small
- Add equipment system
-- Remove rendering logic from State \ No newline at end of file
+- Remove rendering logic from State
+
+
+## Separate rendering logic
+
+```
+------- ---------
+| main| -----> | State |
+------- | ---------
+ |
+ | ------------
+ +-> | ViewPort |
+ ------------
+```
+
+- Main game logic mutates State in reaction to player input
+- Render is triggered by giving it a read-only reference to current state
+- Heavy use of traits in ViewPort allows multiple type of viewports (Piston etc.)
diff --git a/src/events.rs b/src/events.rs
new file mode 100644
index 0000000..0619b18
--- /dev/null
+++ b/src/events.rs
@@ -0,0 +1,10 @@
+use crate::world::Movement;
+
+#[derive(Copy, Clone, Debug)]
+pub enum ViewportEvent {
+ Quit,
+ Help,
+ MovePlayer(Movement),
+ DownStairs,
+ UpStairs,
+}
diff --git a/src/main.rs b/src/main.rs
index f88a4b1..bed0c8d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,23 +1,24 @@
mod entities;
+mod events;
mod state;
mod tiling;
+mod viewport;
mod world;
-use std::env;
-use std::fs::File;
-use std::io::{stdout, Write};
-
-use crossterm::cursor;
-use crossterm::execute;
-use crossterm::input::{input, InputEvent, KeyEvent};
-use crossterm::screen::{EnterAlternateScreen, LeaveAlternateScreen, RawScreen};
-use crossterm::terminal;
use ignore_result::Ignore;
use simplelog::*;
+use std::env;
+use std::fs::File;
use entities::Player;
+use events::ViewportEvent;
use state::State;
-use world::{Dungeon, DOWN, LEFT, RIGHT, UP};
+use viewport::{CrossTermViewPort, ViewPort};
+use world::Dungeon;
+
+const DUNGEON_SIZE_X: usize = 80;
+const DUNGEON_SIZE_Y: usize = 24;
+const DUNGEON_DEPTH: usize = 5;
fn player_name() -> String {
match env::var_os("USER") {
@@ -37,63 +38,34 @@ fn main() {
.unwrap();
}
- // Initialise the terminal, the raw alternate mode allows direct character
- // seeking and hides the prompt.
- let term_size = terminal::size().unwrap();
- execute!(stdout(), EnterAlternateScreen).unwrap();
- execute!(stdout(), cursor::Hide).unwrap();
- let _raw = RawScreen::into_raw_mode();
-
- // Initialise state, create the player and dungeon
let mut state = State::new(
Player::new(player_name(), String::from("Warrior"), 30, 10, 10, 20),
- Dungeon::new(term_size.0 as usize, (term_size.1 - 2) as usize, 5),
+ Dungeon::new(DUNGEON_SIZE_X, DUNGEON_SIZE_Y, DUNGEON_DEPTH),
);
+ let mut window = CrossTermViewPort::new();
state.init();
- let input = input();
- let mut reader = input.read_sync();
-
// Main loop, dispatches events and calls rendering routines. Don't
// add any game logic here.
loop {
- state.render_level();
- state.render_entities();
- state.render_player();
-
- state.render_ui();
+ window.render_state(&state);
- if let Some(event) = reader.next() {
+ if let Some(event) = window.wait_input() {
match event {
- InputEvent::Keyboard(KeyEvent::Char('q')) => break,
- InputEvent::Keyboard(KeyEvent::Char('?')) => {
- state.ui_help();
- }
- InputEvent::Keyboard(KeyEvent::Char('j')) => state.move_player(DOWN).ignore(),
- InputEvent::Keyboard(KeyEvent::Char('k')) => state.move_player(UP).ignore(),
- InputEvent::Keyboard(KeyEvent::Char('h')) => state.move_player(LEFT).ignore(),
- InputEvent::Keyboard(KeyEvent::Char('l')) => state.move_player(RIGHT).ignore(),
- // Arrow keys for noobs
- InputEvent::Keyboard(KeyEvent::Down) => state.move_player(DOWN).ignore(),
- InputEvent::Keyboard(KeyEvent::Up) => state.move_player(UP).ignore(),
- InputEvent::Keyboard(KeyEvent::Left) => state.move_player(LEFT).ignore(),
- InputEvent::Keyboard(KeyEvent::Right) => state.move_player(RIGHT).ignore(),
-
- // Stairs
- InputEvent::Keyboard(KeyEvent::Char('>')) => match state.down_stairs() {
+ ViewportEvent::Quit => break,
+ ViewportEvent::MovePlayer(direction) => state.move_player(direction).ignore(),
+ ViewportEvent::DownStairs => match state.down_stairs() {
Ok(()) => (),
- Err(info) => state.notify(info),
+ Err(info) => window.notify(info),
},
- InputEvent::Keyboard(KeyEvent::Char('<')) => match state.up_stairs() {
+ ViewportEvent::UpStairs => match state.up_stairs() {
Ok(()) => (),
- Err(info) => state.notify(info),
+ Err(info) => window.notify(info),
},
_ => (),
}
}
+
// actors actions (normally attack / interact if on same location as the character)
}
-
- execute!(stdout(), LeaveAlternateScreen).unwrap();
- execute!(stdout(), cursor::Show).unwrap();
}
diff --git a/src/state.rs b/src/state.rs
index 40c66f6..0badaf0 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -1,9 +1,5 @@
-use crossterm::cursor::MoveTo;
-use crossterm::{execute, queue, Output};
-use std::io::{stdout, Write};
-
-use crate::entities::{Character, Entity, Player};
-use crate::tiling::{tile_to_str, Tile, TileGrid, TileType};
+use crate::entities::{Character, Entity};
+use crate::tiling::{Tile, TileGrid, TileType};
use crate::world::{apply_movement, Dungeon, Generatable, Level, Movement};
const PLAYER_SIGHT: usize = 3;
@@ -29,87 +25,21 @@ impl State {
self.dungeon.generate();
self.switch_level(0);
self.player.place(self.current_level().start_point());
- self.clear_los()
+ self.fog_of_war();
}
- pub fn switch_level(&mut self, num_level: usize) {
- self.level = num_level;
- self.grid = Some(self.current_level().to_tilegrid().unwrap());
+ pub fn get_grid(&self) -> Option<&TileGrid> {
+ self.grid.as_ref()
}
- pub fn render_level(&self) {
- let mut sout = stdout();
- execute!(sout, MoveTo(0, 0)).unwrap();
- for (linenum, line) in self.grid.as_ref().unwrap().raw_data().iter().enumerate() {
- let linestr = line.iter().map(tile_to_str).collect::<Vec<&str>>();
- let mut linestr2 = String::from("");
- for chr in linestr {
- linestr2.push_str(chr);
- }
- queue!(sout, Output(linestr2), MoveTo(0, linenum as u16)).unwrap();
- sout.flush().unwrap();
- }
+ pub fn get_player(&self) -> &Character {
+ &self.player
}
- fn render_entity(&self, entity: &dyn Entity) {
- if !entity.is_visible() || !entity.is_dirty() {
- return;
- }
- let dirt = entity.previous_location();
- let background = self.grid.as_ref().unwrap().block_at(dirt.0, dirt.1);
- let mut sout = stdout();
- queue!(
- sout,
- MoveTo(dirt.0 as u16, dirt.1 as u16),
- Output(tile_to_str(background)),
- MoveTo(entity.location().0 as u16, entity.location().1 as u16),
- Output(tile_to_str(entity.tile()))
- )
- .unwrap();
- sout.flush().unwrap();
- }
-
- pub fn render_entities(&self) {
- for e in self.current_level().entities.iter() {
- self.render_entity(&**e);
- }
- }
-
- pub fn render_player(&mut self) {
- self.render_entity(&self.player);
- }
-
- fn ui_state_position(&self) -> MoveTo {
- MoveTo(0, (self.dungeon.ysize()) as u16)
- }
-
- fn ui_notification_position(&self) -> MoveTo {
- MoveTo(0, (self.dungeon.ysize() + 1) as u16)
- }
-
- pub fn render_ui(&self) {
- let mut sout = stdout();
- queue!(sout, self.ui_state_position(), Output(self.player.stats())).unwrap();
- sout.flush().unwrap();
- }
-
- pub fn notify(&self, message: String) {
- let mut sout = stdout();
- queue!(
- sout,
- self.ui_notification_position(),
- Output(" ".repeat(self.dungeon.xsize())),
- self.ui_notification_position(),
- Output(message)
- )
- .unwrap();
- sout.flush().unwrap();
- }
-
- pub fn ui_help(&self) {
- self.notify(String::from(
- "quit: q, movement{up(k), down(j), left(h), right(l)}",
- ))
+ pub fn switch_level(&mut self, num_level: usize) {
+ self.level = num_level;
+ self.grid = Some(self.current_level().to_tilegrid().unwrap());
+ self.fog_of_war();
}
pub fn current_level(&self) -> &Level {
@@ -161,11 +91,9 @@ impl State {
if !State::can_step_on(grid.block_at(loc.0, loc.1)) {
return Err(String::from("Can't move entity!"));
}
- self.player.move_by(dir)?;
-
- self.clear_los();
-
- Ok(())
+ let ret = self.player.move_by(dir);
+ self.fog_of_war();
+ ret
}
pub fn down_stairs(&mut self) -> Result<(), String> {
@@ -182,7 +110,6 @@ impl State {
match grid.block_at(loc.0, loc.1).get_type() {
TileType::StairsDown => {
self.switch_level(self.level + 1);
- self.render_level();
Ok(())
}
_ => Err(String::from("Not on stairs!")),
@@ -203,10 +130,16 @@ impl State {
match grid.block_at(loc.0, loc.1).get_type() {
TileType::StairsUp => {
self.switch_level(self.level - 1);
- self.render_level();
Ok(())
}
_ => Err(String::from("Not on stairs!")),
}
}
+
+ pub fn fog_of_war(&mut self) {
+ self.grid
+ .as_mut()
+ .unwrap()
+ .clear_fog_of_war(self.player.location(), PLAYER_SIGHT);
+ }
}
diff --git a/src/viewport.rs b/src/viewport.rs
new file mode 100644
index 0000000..7b4fb3e
--- /dev/null
+++ b/src/viewport.rs
@@ -0,0 +1,182 @@
+use crate::events::ViewportEvent;
+use crate::world::{DOWN, LEFT, RIGHT, UP};
+use crossterm::cursor;
+use crossterm::cursor::MoveTo;
+use crossterm::input::{input, InputEvent, KeyEvent, TerminalInput};
+use crossterm::screen::{EnterAlternateScreen, LeaveAlternateScreen, RawScreen};
+use crossterm::terminal;
+use crossterm::{execute, queue, Output};
+use log::debug;
+use std::io::{stdout, Write};
+
+use crate::entities::{Entity, Player};
+use crate::state::State;
+use crate::tiling::tile_to_str;
+
+pub trait ViewPort {
+ fn render_state(&mut self, state: &State);
+ fn wait_input(&mut self) -> Option<ViewportEvent>;
+}
+
+pub struct CrossTermViewPort {
+ xsize: usize,
+ ysize: usize,
+ raw: RawScreen,
+ input: TerminalInput,
+ start: (usize, usize),
+}
+
+impl CrossTermViewPort {
+ pub fn new() -> CrossTermViewPort {
+ // Initialise the terminal, the raw alternate mode allows direct character
+ // seeking and hides the prompt.
+ let term_size = terminal::size().unwrap();
+ execute!(stdout(), EnterAlternateScreen).unwrap();
+ execute!(stdout(), cursor::Hide).unwrap();
+ let raw = RawScreen::into_raw_mode().unwrap();
+
+ // Initialise state, create the player and dungeon
+ let xsize = term_size.0 as usize;
+ let ysize = (term_size.1 - 2) as usize;
+
+ let input = input();
+
+ CrossTermViewPort {
+ xsize,
+ ysize,
+ raw,
+ input,
+ start: (0, 0),
+ }
+ }
+
+ fn draw_level(&self, state: &State) {
+ let mut sout = stdout();
+ let grid = state.get_grid().unwrap();
+ execute!(sout, MoveTo(0, 0)).unwrap();
+ for (linenum, line) in grid.raw_data().iter().enumerate() {
+ debug!("Drawing linenum {} -- {:?}", linenum, line);
+ let linestr = line.iter().map(tile_to_str).collect::<Vec<&str>>();
+ let mut linestr2 = String::from("");
+ for chr in linestr {
+ linestr2.push_str(chr);
+ }
+ queue!(sout, Output(linestr2), MoveTo(0, linenum as u16)).unwrap();
+ sout.flush().unwrap();
+ }
+ }
+
+ fn draw_entity(&self, state: &State, entity: &dyn Entity) {
+ if !entity.is_visible() || !entity.is_dirty() {
+ return;
+ }
+ let grid = state.get_grid().unwrap();
+ let dirt = entity.previous_location();
+ let background = grid.block_at(dirt.0, dirt.1);
+ let mut sout = stdout();
+ queue!(
+ sout,
+ MoveTo(dirt.0 as u16, dirt.1 as u16),
+ Output(tile_to_str(background)),
+ MoveTo(entity.location().0 as u16, entity.location().1 as u16),
+ Output(tile_to_str(entity.tile()))
+ )
+ .unwrap();
+ sout.flush().unwrap();
+ }
+
+ fn draw_entities(&self, state: &State) {
+ for e in state.current_level().entities.iter() {
+ self.draw_entity(state, &**e);
+ }
+ }
+
+ fn draw_player(&mut self, state: &State) {
+ self.draw_entity(state, state.get_player());
+ }
+
+ fn ui_state_position(&self) -> MoveTo {
+ MoveTo(0, (self.ysize) as u16)
+ }
+
+ fn ui_notification_position(&self) -> MoveTo {
+ MoveTo(0, (self.ysize + 1) as u16)
+ }
+
+ fn draw_ui(&self, state: &State) {
+ let mut sout = stdout();
+ queue!(
+ sout,
+ self.ui_state_position(),
+ Output(state.get_player().stats())
+ )
+ .unwrap();
+ sout.flush().unwrap();
+ }
+
+ pub fn notify(&self, message: String) {
+ let mut sout = stdout();
+ queue!(
+ sout,
+ self.ui_notification_position(),
+ Output(" ".repeat(self.xsize)),
+ self.ui_notification_position(),
+ Output(message)
+ )
+ .unwrap();
+ sout.flush().unwrap();
+ }
+
+ pub fn ui_help(&self) {
+ self.notify(String::from(
+ "quit: q, movement{up(k), down(j), left(h), right(l)}",
+ ))
+ }
+}
+
+impl ViewPort for CrossTermViewPort {
+ fn render_state(&mut self, state: &State) {
+ self.draw_level(state);
+ self.draw_entities(state);
+ self.draw_player(state);
+ self.draw_ui(state);
+ }
+
+ fn wait_input(&mut self) -> Option<ViewportEvent> {
+ let mut reader = self.input.read_sync();
+
+ if let Some(event) = reader.next() {
+ return match event {
+ InputEvent::Keyboard(KeyEvent::Char('q')) => Some(ViewportEvent::Quit),
+ InputEvent::Keyboard(KeyEvent::Char('?')) => {
+ self.ui_help();
+ None
+ }
+ InputEvent::Keyboard(KeyEvent::Char('j')) => Some(ViewportEvent::MovePlayer(DOWN)),
+ InputEvent::Keyboard(KeyEvent::Char('k')) => Some(ViewportEvent::MovePlayer(UP)),
+ InputEvent::Keyboard(KeyEvent::Char('h')) => Some(ViewportEvent::MovePlayer(LEFT)),
+ InputEvent::Keyboard(KeyEvent::Char('l')) => Some(ViewportEvent::MovePlayer(RIGHT)),
+ // Arrow keys for noobs
+ InputEvent::Keyboard(KeyEvent::Down) => Some(ViewportEvent::MovePlayer(DOWN)),
+ InputEvent::Keyboard(KeyEvent::Up) => Some(ViewportEvent::MovePlayer(UP)),
+ InputEvent::Keyboard(KeyEvent::Left) => Some(ViewportEvent::MovePlayer(LEFT)),
+ InputEvent::Keyboard(KeyEvent::Right) => Some(ViewportEvent::MovePlayer(RIGHT)),
+
+ // Stairs
+ InputEvent::Keyboard(KeyEvent::Char('>')) => Some(ViewportEvent::DownStairs),
+ InputEvent::Keyboard(KeyEvent::Char('<')) => Some(ViewportEvent::UpStairs),
+
+ // No match
+ _ => None,
+ };
+ }
+ None
+ }
+}
+
+impl Drop for CrossTermViewPort {
+ fn drop(&mut self) {
+ execute!(stdout(), LeaveAlternateScreen).unwrap();
+ execute!(stdout(), cursor::Show).unwrap();
+ }
+}