Movement code
This commit is contained in:
parent
42dc9a8625
commit
eeeeffba53
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -103,7 +103,6 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"float-ord",
|
"float-ord",
|
||||||
"grid",
|
"grid",
|
||||||
"lazy_static",
|
|
||||||
"pancurses",
|
"pancurses",
|
||||||
"pathfinding",
|
"pathfinding",
|
||||||
"rand",
|
"rand",
|
||||||
|
|
|
@ -8,7 +8,6 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
specs = "0.17.0"
|
specs = "0.17.0"
|
||||||
specs-derive = "0.4.1"
|
specs-derive = "0.4.1"
|
||||||
lazy_static = "1"
|
|
||||||
pancurses = "0.17.0"
|
pancurses = "0.17.0"
|
||||||
rand = "0.8.4"
|
rand = "0.8.4"
|
||||||
grid = "0.6.0"
|
grid = "0.6.0"
|
||||||
|
|
|
@ -30,10 +30,28 @@ pub struct TurnTaker {
|
||||||
pub maximum: u32,
|
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.
|
/// Registers every existing component with the given ECS world.
|
||||||
pub fn register_all(world: &mut World) {
|
pub fn register_all(world: &mut World) {
|
||||||
world.register::<Position>();
|
world.register::<Position>();
|
||||||
world.register::<CharRender>();
|
world.register::<CharRender>();
|
||||||
world.register::<Player>();
|
world.register::<Player>();
|
||||||
world.register::<TurnTaker>();
|
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),
|
||||||
}
|
}
|
||||||
|
|
54
src/main.rs
54
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 game::{BranchConfig, DungeonLevel};
|
||||||
|
|
||||||
|
use pancurses::{endwin, initscr, noecho, Window};
|
||||||
|
use player::player_turn;
|
||||||
use specs::prelude::*;
|
use specs::prelude::*;
|
||||||
use systems::{IOSystem, TurnTickSystem};
|
use systems::{MobSystem, TimeSystem};
|
||||||
|
|
||||||
mod components;
|
mod components;
|
||||||
mod game;
|
mod game;
|
||||||
|
mod player;
|
||||||
mod rooms;
|
mod rooms;
|
||||||
mod systems;
|
mod systems;
|
||||||
mod util;
|
mod util;
|
||||||
|
@ -25,6 +30,9 @@ fn main() {
|
||||||
.with(Position { x: 5, y: 6 })
|
.with(Position { x: 5, y: 6 })
|
||||||
.with(CharRender { glyph: '@' })
|
.with(CharRender { glyph: '@' })
|
||||||
.with(Player)
|
.with(Player)
|
||||||
|
.with(Mobile {
|
||||||
|
next_action: MobAction::Nop,
|
||||||
|
})
|
||||||
.with(TurnTaker {
|
.with(TurnTaker {
|
||||||
next: 0,
|
next: 0,
|
||||||
maximum: 10,
|
maximum: 10,
|
||||||
|
@ -32,11 +40,49 @@ fn main() {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut dispatcher = DispatcherBuilder::new()
|
let mut dispatcher = DispatcherBuilder::new()
|
||||||
.with(TurnTickSystem, "turn_tick", &[])
|
.with(TimeSystem, "time", &[])
|
||||||
.with(IOSystem::new(), "render", &["turn_tick"])
|
.with(MobSystem, "mobs", &[])
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
let mut window = init_window();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
dispatcher.dispatch(&world);
|
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
86
src/player.rs
Normal 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();
|
||||||
|
}
|
109
src/systems.rs
109
src/systems.rs
|
@ -1,90 +1,15 @@
|
||||||
//! ECS systems.
|
//! ECS systems.
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use pancurses::{endwin, initscr, Window};
|
|
||||||
use specs::prelude::*;
|
use specs::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::components::{MobAction, Mobile, Position, TurnTaker};
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// System for ticking the turn counter on every entity; this system
|
/// System for ticking the turn counter on every entity; this system
|
||||||
/// implements the relationship between real-world time and in-game
|
/// implements the relationship between real-world time and in-game
|
||||||
/// time.
|
/// time.
|
||||||
pub struct TurnTickSystem;
|
pub struct TimeSystem;
|
||||||
|
|
||||||
impl<'a> System<'a> for TurnTickSystem {
|
impl<'a> System<'a> for TimeSystem {
|
||||||
type SystemData = WriteStorage<'a, TurnTaker>;
|
type SystemData = WriteStorage<'a, TurnTaker>;
|
||||||
|
|
||||||
fn run(&mut self, mut turn_takers: Self::SystemData) {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue