Skip to content

Commit

Permalink
Chapter 34 - first part with loading prefabs.
Browse files Browse the repository at this point in the history
  • Loading branch information
thebracket committed Oct 15, 2019
1 parent 6024340 commit 2178dac
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 19 deletions.
Binary file added book/src/c34-s1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/c34-s2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added book/src/c34-s3.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
178 changes: 178 additions & 0 deletions book/src/chapter_34.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,184 @@ pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
}
```

If you `cargo run` your project now, you can run around the (otherwise deserted) demo map:

![Screenshot](./c34-s1.jpg).

## Populating the test map with prefabbed entities

Let's pretend that our test map is some sort of super-duper end-game map. We'll take a copy and call it `wfc-populated.xp`. Then we'll splat a bunch of monster and item glyphs around it:

![Screenshot](./c34-s2.jpg).

The color coding is completely optional, but I put it in for clarity. You'll see we have an `@` to indicate the player start, a `>` to indicate the exit, and a bunch of `g` goblins, `o` orcs, `!` potions, `%` rations and `^` traps. Not too bad a map, really.

We'll add `wfc-populated.xp` to our `resources` folder, and extend `rex_assets.rs` to load it:

```rust
use rltk::{rex::XpFile};

rltk::embedded_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
rltk::embedded_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp");

pub struct RexAssets {
pub menu : XpFile
}

impl RexAssets {
#[allow(clippy::new_without_default)]
pub fn new() -> RexAssets {
rltk::link_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
rltk::link_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp");

RexAssets{
menu : XpFile::from_resource("../../resources/SmallDungeon_80x50.xp").unwrap()
}
}
}
```

We also want to be able to list out spawns that are required by the map. Looking in `spawner.rs`, we have an established `tuple` format for how we pass spawns - so we'll use it in the struct:

```rust
#[allow(dead_code)]
pub struct PrefabBuilder {
map : Map,
starting_position : Position,
depth: i32,
history: Vec<Map>,
mode: PrefabMode,
spawns: Vec<(usize, String)>
}
```

Now we'll modify our constructor to *use* the new map, and initialize `spawns`:

```rust
impl PrefabBuilder {
#[allow(dead_code)]
pub fn new(new_depth : i32) -> PrefabBuilder {
PrefabBuilder{
map : Map::new(new_depth),
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-populated.xp" },
spawns: Vec::new()
}
}
...
```

To make use of the function in `spawner.rs` that accepts this type of data, we need to make it *public*. So we open up the file, and add the word `pub` to the function signature:

```rust
/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)
pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
...
```

We'll then modify our `PrefabBuilder`'s `spawn_entities` function to make use of this data:

```rust
fn spawn_entities(&mut self, ecs : &mut World) {
for entity in self.spawns.iter() {
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
}
}
```

We do a bit of a dance with references just to work with the previous function signature (and not have to change it, which would change lots of other code). So far, so good - it reads the `spawn` list, and requests that everything in the list be placed onto the map. Now would be a good time to add something to the list! We'll want to modify our `load_rex_map` to handle the new data:

```rust
#[allow(dead_code)]
fn load_rex_map(&mut self, path: &str) {
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap();

for layer in &xp_file.layers {
for y in 0..layer.height {
for x in 0..layer.width {
let cell = layer.get(x, y).unwrap();
if x < self.map.width as usize && y < self.map.height as usize {
let idx = self.map.xy_idx(x as i32, y as i32);
// We're doing some nasty casting to make it easier to type things like '#' in the match
match (cell.ch as u8) as char {
' ' => self.map.tiles[idx] = TileType::Floor,
'#' => self.map.tiles[idx] = TileType::Wall,
'@' => {
self.map.tiles[idx] = TileType::Floor;
self.starting_position = Position{ x:x as i32, y:y as i32 };
}
'>' => self.map.tiles[idx] = TileType::DownStairs,
'g' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Goblin".to_string()));
}
'o' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Orc".to_string()));
}
'^' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Bear Trap".to_string()));
}
'%' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Rations".to_string()));
}
'!' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Health Potion".to_string()));
}
_ => {
println!("Unknown glyph loading map: {}", (cell.ch as u8) as char);
}
}
}
}
}
}
}
```

This recognizes the extra glyphs, and prints a warning to the console if we've loaded one we forgot to handle. Note that for entities, we're setting the tile to `Floor` and *then* adding the entity type. That's because we can't overlay two glyphs on the same tile - but it stands to reason that the entity is *standing* on a floor.

Lastly, we need to modify our `build` function to not move the exit and the player. We simply wrap the fallback code in an `if` statement to detect if we've set a `starting_position` (we're going to require that if you set a start, you also set an exit):

```rust
fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template)
}
self.take_snapshot();

// Find a starting point; start at the middle and walk left until we find an open tile
if self.starting_position.x == 0 {
self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
}
self.take_snapshot();

// Find all tiles we can reach from the starting point
let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
self.take_snapshot();

// Place the stairs
self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();
}
}
```

If you `cargo run` the project now, you start in the specified location - and entities spawn around you.

![Screenshot](./c34-s3.gif).

**The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-34-vaults)**


Expand Down
72 changes: 54 additions & 18 deletions chapter-34-vaults/src/map_builders/prefab_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ pub struct PrefabBuilder {
starting_position : Position,
depth: i32,
history: Vec<Map>,
mode: PrefabMode
mode: PrefabMode,
spawns: Vec<(usize, String)>
}

impl MapBuilder for PrefabBuilder {
Expand All @@ -36,6 +37,9 @@ impl MapBuilder for PrefabBuilder {
}

fn spawn_entities(&mut self, ecs : &mut World) {
for entity in self.spawns.iter() {
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
}
}

fn take_snapshot(&mut self) {
Expand All @@ -57,31 +61,35 @@ impl PrefabBuilder {
starting_position : Position{ x: 0, y : 0 },
depth : new_depth,
history : Vec::new(),
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-demo1.xp" }
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-populated.xp" },
spawns: Vec::new()
}
}

fn build(&mut self) {
match self.mode {
PrefabMode::RexLevel{template} => self.load_rex_map(&template)
}
self.take_snapshot();

// Find a starting point; start at the middle and walk left until we find an open tile
self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
}
self.take_snapshot();
if self.starting_position.x == 0 {
self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
while self.map.tiles[start_idx] != TileType::Floor {
self.starting_position.x -= 1;
start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
}
self.take_snapshot();

// Find all tiles we can reach from the starting point
let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
self.take_snapshot();
// Find all tiles we can reach from the starting point
let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
self.take_snapshot();

// Place the stairs
self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();
// Place the stairs
self.map.tiles[exit_tile] = TileType::DownStairs;
self.take_snapshot();
}
}

#[allow(dead_code)]
Expand All @@ -94,10 +102,38 @@ impl PrefabBuilder {
let cell = layer.get(x, y).unwrap();
if x < self.map.width as usize && y < self.map.height as usize {
let idx = self.map.xy_idx(x as i32, y as i32);
// We're doing some nasty casting to make it easier to type things like '#' in the match
match (cell.ch as u8) as char {
' ' => self.map.tiles[idx] = TileType::Floor, // space
'#' => self.map.tiles[idx] = TileType::Wall, // #
_ => {}
' ' => self.map.tiles[idx] = TileType::Floor,
'#' => self.map.tiles[idx] = TileType::Wall,
'@' => {
self.map.tiles[idx] = TileType::Floor;
self.starting_position = Position{ x:x as i32, y:y as i32 };
}
'>' => self.map.tiles[idx] = TileType::DownStairs,
'g' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Goblin".to_string()));
}
'o' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Orc".to_string()));
}
'^' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Bear Trap".to_string()));
}
'%' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Rations".to_string()));
}
'!' => {
self.map.tiles[idx] = TileType::Floor;
self.spawns.push((idx, "Health Potion".to_string()));
}
_ => {
println!("Unknown glyph loading map: {}", (cell.ch as u8) as char);
}
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions chapter-34-vaults/src/rex_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use rltk::{rex::XpFile};

rltk::embedded_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
rltk::embedded_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp");

pub struct RexAssets {
pub menu : XpFile
Expand All @@ -12,6 +13,7 @@ impl RexAssets {
pub fn new() -> RexAssets {
rltk::link_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
rltk::link_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp");

RexAssets{
menu : XpFile::from_resource("../../resources/SmallDungeon_80x50.xp").unwrap()
Expand Down
2 changes: 1 addition & 1 deletion chapter-34-vaults/src/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub fn spawn_region(ecs: &mut World, area : &[usize], map_depth: i32) {
}

/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)
fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
let x = (*spawn.0 % MAPWIDTH) as i32;
let y = (*spawn.0 / MAPWIDTH) as i32;

Expand Down
Binary file added resources/wfc-populated.xp
Binary file not shown.

0 comments on commit 2178dac

Please sign in to comment.