From eeeeffba53b7d3751f33e65cbf4f25ebb38363f6 Mon Sep 17 00:00:00 2001 From: Alex Bethel Date: Thu, 6 Jan 2022 13:07:53 -0600 Subject: [PATCH] Movement code --- Cargo.lock | 1 - Cargo.toml | 1 - src/components.rs | 18 ++++++++ src/main.rs | 54 +++++++++++++++++++++-- src/player.rs | 86 ++++++++++++++++++++++++++++++++++++ src/systems.rs | 109 +++++++++++++--------------------------------- 6 files changed, 185 insertions(+), 84 deletions(-) create mode 100644 src/player.rs diff --git a/Cargo.lock b/Cargo.lock index 88ad5e1..988b844 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,6 @@ version = "0.1.0" dependencies = [ "float-ord", "grid", - "lazy_static", "pancurses", "pathfinding", "rand", diff --git a/Cargo.toml b/Cargo.toml index 6e9b039..759a63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ edition = "2021" [dependencies] specs = "0.17.0" specs-derive = "0.4.1" -lazy_static = "1" pancurses = "0.17.0" rand = "0.8.4" grid = "0.6.0" diff --git a/src/components.rs b/src/components.rs index 3d04feb..0111027 100644 --- a/src/components.rs +++ b/src/components.rs @@ -30,10 +30,28 @@ pub struct TurnTaker { pub maximum: u32, } +/// Entities that can move, attack other mobile entities, use items, +/// etc. +#[derive(Component)] +pub struct Mobile { + pub next_action: MobAction, +} + /// Registers every existing component with the given ECS world. pub fn register_all(world: &mut World) { world.register::(); world.register::(); world.register::(); world.register::(); + world.register::(); +} + +/// An action that a mob can perform that takes up a turn. +#[derive(Clone, Copy)] +pub enum MobAction { + /// Do nothing. + Nop, + + /// Physically move by the given vector. + Move(i32, i32), } diff --git a/src/main.rs b/src/main.rs index 7cf710e..9b35a8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,16 @@ -use components::{register_all, CharRender, Player, Position, TurnTaker}; +use std::process::exit; + +use components::{register_all, CharRender, MobAction, Mobile, Player, Position, TurnTaker}; use game::{BranchConfig, DungeonLevel}; +use pancurses::{endwin, initscr, noecho, Window}; +use player::player_turn; use specs::prelude::*; -use systems::{IOSystem, TurnTickSystem}; +use systems::{MobSystem, TimeSystem}; mod components; mod game; +mod player; mod rooms; mod systems; mod util; @@ -25,6 +30,9 @@ fn main() { .with(Position { x: 5, y: 6 }) .with(CharRender { glyph: '@' }) .with(Player) + .with(Mobile { + next_action: MobAction::Nop, + }) .with(TurnTaker { next: 0, maximum: 10, @@ -32,11 +40,49 @@ fn main() { .build(); let mut dispatcher = DispatcherBuilder::new() - .with(TurnTickSystem, "turn_tick", &[]) - .with(IOSystem::new(), "render", &["turn_tick"]) + .with(TimeSystem, "time", &[]) + .with(MobSystem, "mobs", &[]) .build(); + let mut window = init_window(); + loop { dispatcher.dispatch(&world); + + let players = world.read_storage::(); + let turns = world.read_storage::(); + + if (&players, &turns).join().any(|(_plr, turn)| turn.next == 0) { + drop(players); + drop(turns); + player_turn(&mut world, &mut window); + } else { + drop(players); + drop(turns); + } } } + +/// Initializes the terminal to accept user input, and creates a new +/// Window. +fn init_window() -> Window { + // Create a new window over the terminal. + let window = initscr(); + + // Enable keypad mode (off by default for historical reasons), so + // we can read special keycodes other than just characters. + window.keypad(true); + + // Disable echoing so the user doesn't see flickering in the + // upper-left corner of the screen when they type a character. + noecho(); + + window +} + +/// Cleans everything up and exits the game. +fn quit() -> ! { + endwin(); + + exit(0) +} diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..fd62ad1 --- /dev/null +++ b/src/player.rs @@ -0,0 +1,86 @@ +//! Code for controlling the player, and for I/O. + +use pancurses::Window; +use specs::prelude::*; + +use crate::{ + components::{CharRender, MobAction, Mobile, Player, Position}, + game::DungeonLevel, + quit, +}; + +/// Runs a player turn on the ECS, using the given `screen` for input +/// and output. +/// +/// At some point this should maybe become a system rather than a +/// standalone function. +pub fn player_turn(ecs: &mut World, screen: &mut Window) { + render_screen(ecs, screen); + + let action = loop { + let key = screen.getch(); + + use pancurses::Input; + let action = match key { + Some(key) => match key { + Input::Character(ch) => match ch { + '.' => Some(MobAction::Nop), + + 'h' => Some(MobAction::Move(-1, 0)), + 'j' => Some(MobAction::Move(0, 1)), + 'k' => Some(MobAction::Move(0, -1)), + 'l' => Some(MobAction::Move(1, 0)), + + 'y' => Some(MobAction::Move(-1, -1)), + 'u' => Some(MobAction::Move(1, -1)), + 'b' => Some(MobAction::Move(-1, 1)), + 'n' => Some(MobAction::Move(1, 1)), + + 'q' => quit(), + + _ => None, + }, + + Input::KeyUp => Some(MobAction::Move(0, -1)), + Input::KeyLeft => Some(MobAction::Move(-1, 0)), + Input::KeyDown => Some(MobAction::Move(0, 1)), + Input::KeyRight => Some(MobAction::Move(1, 0)), + _ => None, + }, + + // User closed stdin. + None => quit(), + }; + + if let Some(action) = action { + break action; + } + }; + + let plrs = ecs.read_storage::(); + let mut mobs = ecs.write_storage::(); + for (_plr, mob) in (&plrs, &mut mobs).join() { + mob.next_action = action; + } +} + +/// Renders the state of the world onto the screen. +fn render_screen(ecs: &mut World, screen: &mut Window) { + // screen.clear(); + + // Draw the base level. + let level = ecs.fetch::(); + level.draw(screen); + + // Draw all renderable entities. + let renderables = ecs.read_storage::(); + let positions = ecs.read_storage::(); + for (render, pos) in (&renderables, &positions).join() { + screen.mvaddch(pos.y as _, pos.x as _, render.glyph); + } + + // Leave the cursor at the lower-left. + screen.mv(0, 0); + + screen.refresh(); +} diff --git a/src/systems.rs b/src/systems.rs index 80f004f..7c4d804 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -1,90 +1,15 @@ //! ECS systems. -use std::sync::atomic::{AtomicBool, Ordering}; - -use lazy_static::lazy_static; -use pancurses::{endwin, initscr, Window}; use specs::prelude::*; -use crate::{ - components::{CharRender, Player, Position, TurnTaker}, - game::DungeonLevel, -}; - -/// System for drawing the state of the game, and potentially waiting -/// (blocking) for user input. -pub struct IOSystem { - window: Window, -} - -lazy_static! { - static ref WINDOW_INITIALIZED: AtomicBool = AtomicBool::new(false); -} - -impl IOSystem { - pub fn new() -> Self { - // See the note on `impl Send for IOSystem`. - if WINDOW_INITIALIZED.swap(true, Ordering::Relaxed) { - panic!("Refusing to initialize the renderer twice"); - } - - Self { window: initscr() } - } -} - -impl Drop for IOSystem { - fn drop(&mut self) { - endwin(); - WINDOW_INITIALIZED.store(false, Ordering::Relaxed); - } -} - -// The `Window` type from pancurses contains a raw pointer, and as a -// result Rust isn't convinced that it's safe to send between threads. -// Since we guarantee that only one `Window` object is ever created at -// a time, it is in fact safe to send the render system's data between -// threads. -unsafe impl Send for IOSystem {} - -impl<'a> System<'a> for IOSystem { - type SystemData = ( - ReadExpect<'a, DungeonLevel>, - ReadStorage<'a, CharRender>, - ReadStorage<'a, Position>, - ReadStorage<'a, Player>, - ReadStorage<'a, TurnTaker>, - ); - - fn run(&mut self, (level, renderables, positions, players, turns): Self::SystemData) { - self.window.clear(); - - // Draw the base level. - level.draw(&self.window); - - // Draw all renderable entities in the ECS. - for (render, pos) in (&renderables, &positions).join() { - self.window.mvaddch(pos.y as _, pos.x as _, render.glyph); - } - - // Leave the cursor at the lower-left. - self.window.mv(0, 0); - - // On the player's turn, read input. - for (_player, turn) in (&players, &turns).join() { - if turn.next == 0 { - self.window.refresh(); - self.window.getch(); - } - } - } -} +use crate::components::{MobAction, Mobile, Position, TurnTaker}; /// System for ticking the turn counter on every entity; this system /// implements the relationship between real-world time and in-game /// time. -pub struct TurnTickSystem; +pub struct TimeSystem; -impl<'a> System<'a> for TurnTickSystem { +impl<'a> System<'a> for TimeSystem { type SystemData = WriteStorage<'a, TurnTaker>; fn run(&mut self, mut turn_takers: Self::SystemData) { @@ -93,3 +18,31 @@ impl<'a> System<'a> for TurnTickSystem { } } } + +/// System for executing actions that mobs have chosen. +pub struct MobSystem; + +impl<'a> System<'a> for MobSystem { + type SystemData = ( + WriteStorage<'a, Position>, + ReadStorage<'a, TurnTaker>, + WriteStorage<'a, Mobile>, + ); + + fn run(&mut self, (mut pos, turn, mut mob): Self::SystemData) { + for (pos, _turn, mob) in (&mut pos, &turn, &mut mob) + .join() + .filter(|(_pos, turn, _mob)| turn.next == 0) + { + match mob.next_action { + MobAction::Nop => {} + MobAction::Move(dx, dy) => { + pos.x = (pos.x as i32 + dx) as _; + pos.y = (pos.y as i32 + dy) as _; + } + } + + mob.next_action = MobAction::Nop; + } + } +}