We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Placing a Guaranteed Item with a Prefab (Chapter 13 follow-up)
Somewhere after Chapter 13 of Hands on Rust, I wanted the Dungeon Map item (the one that reveals the whole level, ProvidesDungeonMap) to always spawn in the fortress prefab room instead of showing up as a random drop anywhere on the level.
The starting point
The book’s spawn_entity function rolls a die for every room center and monster-spawn point, and one of the outcomes was a magic mapper:
pub fn spawn_entity(ecs: &mut World, rng: &mut RandomNumberGenerator, pos: Point) {
let roll = rng.roll_dice(1, 6);
match roll {
1 => spawn_healing_potion(ecs, pos),
2 => spawn_magic_mapper(ecs, pos),
_ => spawn_monster(ecs, rng, pos),
}
}
That meant the map item could appear anywhere, or nowhere, purely by chance. I wanted it tied specifically to the fortress prefab — the hand-authored ASCII room with the { glyph baked into its layout.
Following the amulet’s pattern
The amulet of Yala already does something similar: MapBuilder carries a single amulet_start: Point, set once by find_most_distant(), and main.rs spawns it directly with its own dedicated call — no dice roll involved. That was the model to copy for the map item.
I added a matching field:
pub struct MapBuilder {
// ...
pub amulet_start: Point,
pub map_start: Point,
}
Every architect (DrunkardsWalkArchitect, RoomsArchitect, CellularAutomataArchitect, EmptyArchitect) initializes it to Point::zero() as a placeholder, same convention already used for amulet_start and player_start before they’re set for real.
Setting it inside the prefab
The fortress prefab is applied after the base map, monsters, and amulet are already placed, via apply_prefab(&mut mb, rng). It uses a Dijkstra map from the player’s start to find a valid spot for the fortress, tries up to 10 times, and — if successful — stamps the ASCII layout onto the map tile by tile. The M characters already fed into mb.monster_spawns, so the { character just needed the same treatment, but for a single point instead of a vec:
'{' => {
mb.map.tiles[idx] = TileType::Floor;
mb.map_start = Point::new(tx, ty)
}
Spawning it — and guarding the “no fortress” case
Because apply_prefab only has 10 attempts and requires the fortress to land somewhere clear of the amulet, at a reasonable Dijkstra distance from the player, placement can fail. When it does, map_start is left at its Point::zero() sentinel.
Rather than checking for that at every call site, I pushed the guard into spawn_magic_mapper itself:
pub fn spawn_magic_mapper(ecs: &mut World, pos: Point) {
if pos != Point::zero() {
ecs.push((
Item,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('{'),
},
Name("Dungeon Map".to_string()),
ProvidesDungeonMap,
));
}
}
Then in both State::new() and reset_game_state(), right alongside the amulet spawn:
spawn_amulet_of_yala(&mut ecs, map_builder.amulet_start);
spawn_magic_mapper(&mut ecs, map_builder.map_start);
Finally, I commented out the old dice-roll branch in spawn_entity so the map item no longer spawns randomly — it’s fortress-only now.
A limitation I’m leaving alone for now
Testing across a few runs turned up something worth noting: RoomsArchitect levels can never contain the fortress. Its rooms cap out at 10×10 tiles and are deliberately non-overlapping, connected only by single-tile corridors — so there’s never a contiguous 12×11 open block for the fortress to occupy, regardless of how many placement attempts run. DrunkardsWalkArchitect and CellularAutomataArchitect both carve out large open blobby regions, so they can fit it, just not guaranteed every time.
For now I’m treating this as an accepted, architect-dependent quirk rather than a bug — a “fortress ruins don’t appear in every kind of dungeon” flavor of limitation. Possible future fixes if I revisit it: allow larger individual rooms in RoomsArchitect, or explicitly skip fortress attempts for room-based layouts.
Takeaways
-
When you want a single guaranteed entity rather than a random chance-based one, look at how the book already solves that for the amulet — a dedicated
Pointfield onMapBuilder, set once, spawned with its own direct call. - Push “don’t spawn this” guard logic into the spawn function itself when a sentinel value can mean “no valid placement,” rather than repeating the check at every call site.
- Prefabs applied after procedural generation inherit whatever constraints that generator’s layout imposes — a placement algorithm being technically correct doesn’t guarantee it’s satisfiable for every dungeon type.