Skip to content

04. Placing things in rooms

Now we know how to make more rooms than we probably need, but they're all empty and that's pretty boring. For testing, we'll start with this generator, which is a toned-down version of one from the previous part:

local libLevelGen = require "LibLevelGen.LibLevelGen"
local segment = require "LibLevelGen.Segment"
local room = require "LibLevelGen.Room"

local roomGenCombinations = segment.createRandLinkedRoomParameterCombinations {
    direction = {room.Direction.UP, room.Direction.DOWN, room.Direction.LEFT, room.Direction.RIGHT},
    corridorEntrance = {0.25, 0.5, 0.75},
    corridorExit = {0.25, 0.5, 0.75},
    corridorThickness = {3},
    corridorLength = {0, 1, 2},
    roomWidth = {6, 7, 8, 9},
    roomHeight = {6, 7, 8, 9},
}

local function createTwoRooms(currentRoom, roomsLeft)
    if roomsLeft > 0 then
        local newCorridor1, newRoom1 = currentRoom.segment:createRandLinkedRoom(currentRoom, false, roomGenCombinations)
        local newCorridor2, newRoom2 = currentRoom.segment:createRandLinkedRoom(currentRoom, false, roomGenCombinations)
        -- Check if the generation didn't fail:
        if newRoom1 then
            createTwoRooms(newRoom1, roomsLeft - 1)
        end
        if newRoom2 then
            createTwoRooms(newRoom2, roomsLeft - 1)
        end
    end
end

local function myGenerator(genParams)
    local instance = libLevelGen.new(genParams)

    local mainSegment = instance:createSegment()
    local startingRoom = mainSegment:createStartingRoom()

    createTwoRooms(startingRoom, 2)

    instance:finalize()
end

libLevelGen.registerGenerator("LibLevelGen Playground", myGenerator)

Room types

As mentioned earlier, a "room" in LibLevelGen is a quite wide term - it applies to actual rooms, corridors, secret rooms, vaults and even secret shops! Because of this, we need a way to know which operations we can perform in which room - and this is where room flags come into play.

If you check out the link above, you'll see that there's a lot of flags like ALLOW_ENEMY, ALLOW_SHRINE, EXIT and more. These determine what kind of things can be done to the room. For example, an exit room will have ALLOW_ENEMY but not ALLOW_SHRINE, because shrines don't spawn in exit rooms.

There's also a second enum, room types, which are just collections of flag. For example, room.Type.EXIT consists of ALLOW_ENEMY, ALLOW_TRAVELRUNE, ALLOW_TORCH, ALLOW_TILE_CONVERSION and EXIT.

When creating rooms with createRandLinkedRoom, the corridors get flags set to room.Type.CORRIDOR and the actual rooms to room.Type.REGULAR. Of course, you can then modify the flags to your liking, but these are generally pretty good defaults.

There's a method to select all rooms which match given flags: Segment:selectRooms, though often it might be more convenient to use Segment:iterateRooms, which will automatically call the given function for every matching room.

Placing entities!

Let's run some code for every room which allows enemies, like so:

local function placeEnemies(currentRoom)
    dbg("Hello from room at ", currentRoom.x, currentRoom.y)
    -- room.Flag.inspect is a useful debug method that exists for
    -- every enum automatically.
    dbg("My flags are: ", room.Flag.inspect(currentRoom.flags))
end

local function myGenerator(genParams)
    local instance = libLevelGen.new(genParams)

    local mainSegment = instance:createSegment()
    local startingRoom = mainSegment:createStartingRoom()

    createTwoRooms(startingRoom, 2)

    mainSegment:iterateRooms(room.Flag.ALLOW_ENEMY, placeEnemies)

    instance:finalize()
end
With the current generator, this should run placeEnemies for every non-corridor room except the starting room, which is where we definitely do not want any enemies. Now, let's place some entities in the room: this can be done using a handful of methods, but all of them call Tile:placeEntity in the end:
local function placeEnemies(currentRoom)
    currentRoom:getTile(1, 1):placeEntity("Skeleton")
end
This will create a little Skeleton friend in the top-left corner of every room where enemies are allowed. Now, it'd be nicer to place the enemy randomly, and there's actually an entire system for selecting tiles which we'll talk about in the next part. For now, let's just place the skeleton in a corner.

A quick reminder though: this does not actually create an actual entity yet, it creates a LibLevelGen Entity object which will then be used to construct the data needed to spawn the entity by the game.

But room:getTile(...):placeEntity(...) is a lot of typing, so there's a convenience method to do this with a few less characters:

currentRoom:placeEntityAt(1, 1, "Skeleton")
yes, I am pretty lazy

Entity levels

So in the base game, different entity levels are defined by just appending a number to the entity name: like Skeleton, Skeleton2, Skeleton3 and so on. But LibLevelGen actually handles it a bit differently - the level and the type are separate arguments to placeEntity methods.

currentRoom:placeEntityAt(1, 1, "Skeleton", 2) -- Yellow Skeleton
But why do it like that? You see, LibLevelGen is capable of automatically handling Shrine of War upgrades, Ring of Peace downgrades and other things that affect entity levels. Instead of extracting the entity level and type from the name, they are specified separately for convenience. Though spawning a Skeleton3 will also work, LibLevelGen will not actually know that it's a Skeleton - it will interpret this as an entity with base type called Skeleton3 and level 1. We'll talk more about enemy upgrades and registering your own, custom entity types in the future.

Placing torches

Our level could really use some more light, so let's add some torches to it. We could iterate over rooms that have the ALLOW_TORCH flag, but there's actually a convenience function to place a given amount of torches per room:

-- In myGenerator function, after placing the rooms...
mainSegment:placeWallTorches(2)
Suddenly it's a lot nicer in here, huh?

Making an exit room

Our level is still missing a pretty crucial part - a way to actually leave it. So first, we need to pick a room that we'll make into an exit room, and ideally it should not be directly adjacent to the starting room. We can do that with modyfing the createTwoRooms function in a clever way:

local function createExit(currentRoom)
    dbg("Create an exit room here!")
end

local function createTwoRooms(currentRoom, roomsLeft, needsExit)
    if roomsLeft > 0 then
        local newCorridor1, newRoom1 = currentRoom.segment:createRandLinkedRoom(currentRoom, false, roomGenCombinations)
        local newCorridor2, newRoom2 = currentRoom.segment:createRandLinkedRoom(currentRoom, false, roomGenCombinations)
        -- Check if the generation didn't fail:
        if newRoom1 then
            createTwoRooms(newRoom1, roomsLeft - 1, needsExit)
        end
        if newRoom2 then
            createTwoRooms(newRoom2, roomsLeft - 1, false)
        end
    elseif needsExit then
        createExit(currentRoom)
    end
end
And then we add the extra argument we just added to the initial call:
-- in myGenerator function
createTwoRooms(startingRoom, 2, true)
Essentially, the needsExit parameter will only remain true for one of the last rooms generated, so even if we were to change 2 to 5 when calling createTwoRooms we'll still get only one exit room. Of course, you can do this in any other way you'd like.

Now, how do we make this room an exit room?

local function createExit(currentRoom)
    currentRoom:makeExit { {"Dragon", 1}, {"Minotaur", 1} }
end
Aaaand that's it, this function will do everything for us. As the documentation states:

Turn the room into an exit room - set appropriate flags, place the exit stairs and the miniboss.

We just need to specify which minibosses it can pick from. It even makes sure that the same miniboss type won't appear 2 floors in a row (unless there's no other choice), and also makes sure to spawn 2 minibosses when Shrine of Boss is active. Wowie!

Our level is slowly getting better! In the next part, we'll learn the tile selection system briefly mentioned here.