Movement code

master
Alex Bethel 2022-01-06 13:07:53 -06:00
parent 42dc9a8625
commit eeeeffba53
6 changed files with 185 additions and 84 deletions

1
Cargo.lock generated
View File

@ -103,7 +103,6 @@ version = "0.1.0"
dependencies = [
"float-ord",
"grid",
"lazy_static",
"pancurses",
"pathfinding",
"rand",

View File

@ -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"

View File

@ -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::<Position>();
world.register::<CharRender>();
world.register::<Player>();
world.register::<TurnTaker>();
world.register::<Mobile>();
}
/// 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),
}

View File

@ -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::<Player>();
let turns = world.read_storage::<TurnTaker>();
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)
}

86
src/player.rs Normal file
View File

@ -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::<Player>();
let mut mobs = ecs.write_storage::<Mobile>();
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::<DungeonLevel>();
level.draw(screen);
// Draw all renderable entities.
let renderables = ecs.read_storage::<CharRender>();
let positions = ecs.read_storage::<Position>();
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();
}

View File

@ -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;
}
}
}