Skip to content

Commit 2178dac

Browse files
committed
Chapter 34 - first part with loading prefabs.
1 parent 6024340 commit 2178dac

File tree

8 files changed

+235
-19
lines changed

8 files changed

+235
-19
lines changed

book/src/c34-s1.jpg

62.1 KB
Loading

book/src/c34-s2.jpg

250 KB
Loading

book/src/c34-s3.gif

62.1 KB
Loading

book/src/chapter_34.md

+178
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,184 @@ pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
295295
}
296296
```
297297

298+
If you `cargo run` your project now, you can run around the (otherwise deserted) demo map:
299+
300+
![Screenshot](./c34-s1.jpg).
301+
302+
## Populating the test map with prefabbed entities
303+
304+
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:
305+
306+
![Screenshot](./c34-s2.jpg).
307+
308+
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.
309+
310+
We'll add `wfc-populated.xp` to our `resources` folder, and extend `rex_assets.rs` to load it:
311+
312+
```rust
313+
use rltk::{rex::XpFile};
314+
315+
rltk::embedded_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
316+
rltk::embedded_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
317+
rltk::embedded_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp");
318+
319+
pub struct RexAssets {
320+
pub menu : XpFile
321+
}
322+
323+
impl RexAssets {
324+
#[allow(clippy::new_without_default)]
325+
pub fn new() -> RexAssets {
326+
rltk::link_resource!(SMALL_DUNGEON, "../../resources/SmallDungeon_80x50.xp");
327+
rltk::link_resource!(WFC_DEMO_IMAGE1, "../../resources/wfc-demo1.xp");
328+
rltk::link_resource!(WFC_POPULATED, "../../resources/wfc-populated.xp");
329+
330+
RexAssets{
331+
menu : XpFile::from_resource("../../resources/SmallDungeon_80x50.xp").unwrap()
332+
}
333+
}
334+
}
335+
```
336+
337+
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:
338+
339+
```rust
340+
#[allow(dead_code)]
341+
pub struct PrefabBuilder {
342+
map : Map,
343+
starting_position : Position,
344+
depth: i32,
345+
history: Vec<Map>,
346+
mode: PrefabMode,
347+
spawns: Vec<(usize, String)>
348+
}
349+
```
350+
351+
Now we'll modify our constructor to *use* the new map, and initialize `spawns`:
352+
353+
```rust
354+
impl PrefabBuilder {
355+
#[allow(dead_code)]
356+
pub fn new(new_depth : i32) -> PrefabBuilder {
357+
PrefabBuilder{
358+
map : Map::new(new_depth),
359+
starting_position : Position{ x: 0, y : 0 },
360+
depth : new_depth,
361+
history : Vec::new(),
362+
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-populated.xp" },
363+
spawns: Vec::new()
364+
}
365+
}
366+
...
367+
```
368+
369+
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:
370+
371+
```rust
372+
/// Spawns a named entity (name in tuple.1) at the location in (tuple.0)
373+
pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) {
374+
...
375+
```
376+
377+
We'll then modify our `PrefabBuilder`'s `spawn_entities` function to make use of this data:
378+
379+
```rust
380+
fn spawn_entities(&mut self, ecs : &mut World) {
381+
for entity in self.spawns.iter() {
382+
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
383+
}
384+
}
385+
```
386+
387+
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:
388+
389+
```rust
390+
#[allow(dead_code)]
391+
fn load_rex_map(&mut self, path: &str) {
392+
let xp_file = rltk::rex::XpFile::from_resource(path).unwrap();
393+
394+
for layer in &xp_file.layers {
395+
for y in 0..layer.height {
396+
for x in 0..layer.width {
397+
let cell = layer.get(x, y).unwrap();
398+
if x < self.map.width as usize && y < self.map.height as usize {
399+
let idx = self.map.xy_idx(x as i32, y as i32);
400+
// We're doing some nasty casting to make it easier to type things like '#' in the match
401+
match (cell.ch as u8) as char {
402+
' ' => self.map.tiles[idx] = TileType::Floor,
403+
'#' => self.map.tiles[idx] = TileType::Wall,
404+
'@' => {
405+
self.map.tiles[idx] = TileType::Floor;
406+
self.starting_position = Position{ x:x as i32, y:y as i32 };
407+
}
408+
'>' => self.map.tiles[idx] = TileType::DownStairs,
409+
'g' => {
410+
self.map.tiles[idx] = TileType::Floor;
411+
self.spawns.push((idx, "Goblin".to_string()));
412+
}
413+
'o' => {
414+
self.map.tiles[idx] = TileType::Floor;
415+
self.spawns.push((idx, "Orc".to_string()));
416+
}
417+
'^' => {
418+
self.map.tiles[idx] = TileType::Floor;
419+
self.spawns.push((idx, "Bear Trap".to_string()));
420+
}
421+
'%' => {
422+
self.map.tiles[idx] = TileType::Floor;
423+
self.spawns.push((idx, "Rations".to_string()));
424+
}
425+
'!' => {
426+
self.map.tiles[idx] = TileType::Floor;
427+
self.spawns.push((idx, "Health Potion".to_string()));
428+
}
429+
_ => {
430+
println!("Unknown glyph loading map: {}", (cell.ch as u8) as char);
431+
}
432+
}
433+
}
434+
}
435+
}
436+
}
437+
}
438+
```
439+
440+
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.
441+
442+
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):
443+
444+
```rust
445+
fn build(&mut self) {
446+
match self.mode {
447+
PrefabMode::RexLevel{template} => self.load_rex_map(&template)
448+
}
449+
self.take_snapshot();
450+
451+
// Find a starting point; start at the middle and walk left until we find an open tile
452+
if self.starting_position.x == 0 {
453+
self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
454+
let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
455+
while self.map.tiles[start_idx] != TileType::Floor {
456+
self.starting_position.x -= 1;
457+
start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
458+
}
459+
self.take_snapshot();
460+
461+
// Find all tiles we can reach from the starting point
462+
let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
463+
self.take_snapshot();
464+
465+
// Place the stairs
466+
self.map.tiles[exit_tile] = TileType::DownStairs;
467+
self.take_snapshot();
468+
}
469+
}
470+
```
471+
472+
If you `cargo run` the project now, you start in the specified location - and entities spawn around you.
473+
474+
![Screenshot](./c34-s3.gif).
475+
298476
**The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-34-vaults)**
299477

300478

chapter-34-vaults/src/map_builders/prefab_builder.rs

+54-18
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ pub struct PrefabBuilder {
1515
starting_position : Position,
1616
depth: i32,
1717
history: Vec<Map>,
18-
mode: PrefabMode
18+
mode: PrefabMode,
19+
spawns: Vec<(usize, String)>
1920
}
2021

2122
impl MapBuilder for PrefabBuilder {
@@ -36,6 +37,9 @@ impl MapBuilder for PrefabBuilder {
3637
}
3738

3839
fn spawn_entities(&mut self, ecs : &mut World) {
40+
for entity in self.spawns.iter() {
41+
spawner::spawn_entity(ecs, &(&entity.0, &entity.1));
42+
}
3943
}
4044

4145
fn take_snapshot(&mut self) {
@@ -57,31 +61,35 @@ impl PrefabBuilder {
5761
starting_position : Position{ x: 0, y : 0 },
5862
depth : new_depth,
5963
history : Vec::new(),
60-
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-demo1.xp" }
64+
mode : PrefabMode::RexLevel{ template : "../../resources/wfc-populated.xp" },
65+
spawns: Vec::new()
6166
}
6267
}
6368

6469
fn build(&mut self) {
6570
match self.mode {
6671
PrefabMode::RexLevel{template} => self.load_rex_map(&template)
6772
}
73+
self.take_snapshot();
6874

6975
// Find a starting point; start at the middle and walk left until we find an open tile
70-
self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
71-
let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
72-
while self.map.tiles[start_idx] != TileType::Floor {
73-
self.starting_position.x -= 1;
74-
start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
75-
}
76-
self.take_snapshot();
76+
if self.starting_position.x == 0 {
77+
self.starting_position = Position{ x: self.map.width / 2, y : self.map.height / 2 };
78+
let mut start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
79+
while self.map.tiles[start_idx] != TileType::Floor {
80+
self.starting_position.x -= 1;
81+
start_idx = self.map.xy_idx(self.starting_position.x, self.starting_position.y);
82+
}
83+
self.take_snapshot();
7784

78-
// Find all tiles we can reach from the starting point
79-
let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
80-
self.take_snapshot();
85+
// Find all tiles we can reach from the starting point
86+
let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx);
87+
self.take_snapshot();
8188

82-
// Place the stairs
83-
self.map.tiles[exit_tile] = TileType::DownStairs;
84-
self.take_snapshot();
89+
// Place the stairs
90+
self.map.tiles[exit_tile] = TileType::DownStairs;
91+
self.take_snapshot();
92+
}
8593
}
8694

8795
#[allow(dead_code)]
@@ -94,10 +102,38 @@ impl PrefabBuilder {
94102
let cell = layer.get(x, y).unwrap();
95103
if x < self.map.width as usize && y < self.map.height as usize {
96104
let idx = self.map.xy_idx(x as i32, y as i32);
105+
// We're doing some nasty casting to make it easier to type things like '#' in the match
97106
match (cell.ch as u8) as char {
98-
' ' => self.map.tiles[idx] = TileType::Floor, // space
99-
'#' => self.map.tiles[idx] = TileType::Wall, // #
100-
_ => {}
107+
' ' => self.map.tiles[idx] = TileType::Floor,
108+
'#' => self.map.tiles[idx] = TileType::Wall,
109+
'@' => {
110+
self.map.tiles[idx] = TileType::Floor;
111+
self.starting_position = Position{ x:x as i32, y:y as i32 };
112+
}
113+
'>' => self.map.tiles[idx] = TileType::DownStairs,
114+
'g' => {
115+
self.map.tiles[idx] = TileType::Floor;
116+
self.spawns.push((idx, "Goblin".to_string()));
117+
}
118+
'o' => {
119+
self.map.tiles[idx] = TileType::Floor;
120+
self.spawns.push((idx, "Orc".to_string()));
121+
}
122+
'^' => {
123+
self.map.tiles[idx] = TileType::Floor;
124+
self.spawns.push((idx, "Bear Trap".to_string()));
125+
}
126+
'%' => {
127+
self.map.tiles[idx] = TileType::Floor;
128+
self.spawns.push((idx, "Rations".to_string()));
129+
}
130+
'!' => {
131+
self.map.tiles[idx] = TileType::Floor;
132+
self.spawns.push((idx, "Health Potion".to_string()));
133+
}
134+
_ => {
135+
println!("Unknown glyph loading map: {}", (cell.ch as u8) as char);
136+
}
101137
}
102138
}
103139
}

chapter-34-vaults/src/rex_assets.rs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use rltk::{rex::XpFile};
22

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

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

1618
RexAssets{
1719
menu : XpFile::from_resource("../../resources/SmallDungeon_80x50.xp").unwrap()

chapter-34-vaults/src/spawner.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub fn spawn_region(ecs: &mut World, area : &[usize], map_depth: i32) {
9494
}
9595

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

resources/wfc-populated.xp

877 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)