diff options
author | Guillaume Pasquet <dev@etenil.net> | 2020-03-14 16:53:10 +0000 |
---|---|---|
committer | Guillaume Pasquet <dev@etenil.net> | 2020-03-14 16:53:10 +0000 |
commit | 8c92d7c0aa1497e7d1a66cddcdacd04e43dba7f6 (patch) | |
tree | ffbfc495906fdb447074c1c1e3e4996432f83a14 | |
parent | 9d1de3f314a2f63a8f58e3ad2018c5a32472a67a (diff) | |
parent | 219e32ad793e66a3111465ef02f3eb677f70277d (diff) |
Merge branch 'merge-viewport'
-rw-r--r-- | .vscode/launch.json | 1 | ||||
-rw-r--r-- | TODO.md | 21 | ||||
-rw-r--r-- | src/events.rs | 10 | ||||
-rw-r--r-- | src/main.rs | 72 | ||||
-rw-r--r-- | src/state.rs | 109 | ||||
-rw-r--r-- | src/viewport.rs | 182 |
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 @@ -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(); + } +} |