Vertical Navigation
As day three drew to a close, I decided to make a start on getting staircases working. It won't be completed on day 3, but it's a good start.
Command Placeholders
A good start to allowing SecBot to climb/descend stairs is to support the commands.
Open up src/game/player.rs
and look at the player_turn
function. There's a large match
statement handling possible key-presses. You want to add two more commands to the list:
#![allow(unused)] fn main() { VirtualKeyCode::Comma => go_up(ecs, map), VirtualKeyCode::Period => go_down(ecs, map), }
You haven't written these functions yet, so it won't compile - but now you have the skeleton of making the comma key (<
with shift) indicate a desire to go up, and >
(period/full stop) indicate a desire to go down.
Let's drop-in some placeholder commands to allow the game to compile. Add two new functions to the player.rs
file:
#![allow(unused)] fn main() { fn go_up(ecs: &mut World, map: &mut Map) -> NewState { NewState::Wait } fn go_down(ecs: &mut World, map: &mut Map) -> NewState { NewState::Wait } }
Update the Instructions
It's always a good idea to give your player some idea of how to play the game. When we setup the WASM build, we included some instructions in the HTML. Now that we've added some commands, let's include them in the instructions. Open wasm_help/index.html
. Add one more sentence to the instruction text:
<p style="color: #55ff55; font-family: 'Courier New', Courier, monospace; font-size: 10pt;">< and > go up and down levels if you are on an appropriate staircase.</p>
Now that we're able to catch the up and down commands, and have told the player what to do - let's start making them do something.
Implementing Up Stairs
Open src/map/tile.rs
. In the TileType
enum, we want to add one more type of tile:
#![allow(unused)] fn main() { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TileType { Empty, Capsule, Wall, Floor, Outside, StairsDown, StairsUp, } }
We also need to make a function to build a tile of this type. In the same file, add the following builder function:
#![allow(unused)] fn main() { pub fn stairs_up() -> Self { Self { glyph: to_cp437('<'), color: ColorPair::new(YELLOW, BLACK), blocked: false, opaque: false, tile_type: TileType::StairsUp, } } }
Finally, we need to make TileType
public---we're using it outside of the map module, now. Open src/map/mod.rs
and add one line:
#![allow(unused)] fn main() { pub use tile:TileTYpe; }
Finding Stairs
Up and Down commands only work when the player (or other entity that uses stairs) is standing on a stair-case. That means we need a quick way to find out where the stairs are for a level. Open src/map/layer.rs
. Add TileType
to the list of types you are importing from super
:
#![allow(unused)] fn main() { use super::{layerbuilder::*, Tile, TileType, HEIGHT, TILES, WIDTH}; }
The layer now knows what to do with the TileType
type. Finding the downward staircase map location can be done with an iterator call. Add the following function to the layer.rs
file, as an implemented method for Layer
:
#![allow(unused)] fn main() { impl Layer { ... pub fn find_down_stairs(&self) -> Point { let idx = self .tiles .iter() .enumerate() .filter(|(_, t)| t.tile_type == TileType::StairsDown) .map(|(idx, _)| idx) .nth(0) .unwrap(); self.index_to_point2d(idx) } } }
This works by iterating through the tiles
list, adding an enumeration (for the tile index). It filters the iterator, only accepting down staircases. It then takes the first occurrence, and transforms the result to a Point
listing the staircases' map location. It would be more efficient to calculate this once and cache it - but the speed benefits are negligible, so I stuck with this method.
Setting the Current Layer
We also need a way to change the current in-play layer. Open src/map/map.rs
and add one more function to the Map
implementation:
#![allow(unused)] fn main() { pub fn set_current_layer(&mut self, new_layer: usize) { self.current_layer = new_layer; } }
You can call this function when the player changes layer, and since we're already tracking the current layer---the game will move to rendering the current map level.
Player Movement
We need a few extra mechanisms to support player movement between levels.
Going Up
In src/game/player.rs
, we can now flesh out the go_up
function. We want to check that the player is actually standing on an up staircase, and if they are change their position to the upwards-level's down staircase---and update their map position. Flesh out the function as follows:
#![allow(unused)] fn main() { fn go_up(ecs: &mut World, map: &mut Map) -> NewState { let mut find_player = <(&Player, &mut Position)>::query(); find_player.for_each_mut(ecs, |(_, pos)| { let idx = map.get_current().point2d_to_index(pos.pt); if map.get_current().tiles[idx].tile_type == TileType::StairsUp { // It really is an up staircase let new_layer = pos.layer - 1; map.set_current_layer(new_layer as usize); pos.layer = new_layer; pos.pt = map.get_current().find_down_stairs(); } }); NewState::Player } }
Going Down
We can do the same for the go_down
function stub in player.rs
. We check that the player is standing on a down staircase, and update the player's position to the layer's starting point:
#![allow(unused)] fn main() { fn go_down(ecs: &mut World, map: &mut Map) -> NewState { let mut find_player = <(&Player, &mut Position)>::query(); find_player.for_each_mut(ecs, |(_, pos)| { let idx = map.get_current().point2d_to_index(pos.pt); if map.get_current().tiles[idx].tile_type == TileType::StairsDown { // It really is a down staircase let new_layer = pos.layer + 1; map.set_current_layer(new_layer as usize); pos.layer = new_layer; pos.pt = map.get_current().starting_point; } }); NewState::Player } }
Turn Structure Adjustments
We're going to subtly change the turn structure, to give the game a chance to spend a tick processing player instructions before moving on to other tasks. Our basic flow will become:
Waiting -> PlayerTurn -> EnemyTurn -> Waiting
Open src/main.rs
and adjust TurnState
to include a new PlayerTurn
entry:
#![allow(unused)] fn main() { enum TurnState { WaitingForInput, PlayerTurn, EnemyTurn, Modal { title: String, body: String }, GameOverLeft, ... }
Since we're doing a dance of returning a NewState
and using it to set the TurnState
at the right time, you also need to add a Player
entry to NewState
in the same file:
#![allow(unused)] fn main() { pub enum NewState { NoChange, Wait, Player, Enemy, LeftMap, } }
Now scroll down to where we are handling turn states in main.rs
. You want to add two lines (the program won't compile until you do):
#![allow(unused)] fn main() { NewState::Wait } TurnState::GameOverLeft => render::game_over_left(ctx), + TurnState::PlayerTurn => NewState::Enemy, // Placeholder }; match new_state { NewState::NoChange => {} NewState::Wait => self.turn = TurnState::WaitingForInput, NewState::Enemy => self.turn = TurnState::EnemyTurn, NewState::LeftMap => self.turn = TurnState::GameOverLeft, + NewState::Player => self.turn = TurnState::PlayerTurn, } } } }
Now when it's the player's turn, we switch to Enemy
mode, and if its Player
time in NewState
we set the player appropriately.
The game is coming along nicely! You can now find up/down staircases and use them to change level, allowing you to visit the whole map. You may run into issues with the stairs being obscured by a monster---but we'll worry about that on day 4.
Wrap-Up
That concludes day 3's development. It mostly focused on map generation, with a bit of gameplay (to start using levels) thrown in. As day 4 approached, I was starting to feel the pressure - so we'll dive into a smorgasboard of game changes.
You can find the source code for
hello_modal
here.