Skip to content

07. A variety of small things #1

So generally, the most important things have been discussed in the previous part already. Yay!
Still, that doesn't mean that there's nothing left to talk about - in fact, there's a whole bunch of small but neat things!

Randomness

So far, the randomness in our level has been determined by built-in functions, but that doesn't mean we can't generate random numbers ourselves. The code we'll start with is this.

Let's try randomizing the levels of the Skeletons we spawn: we'll use instance:randIntRange for this.

You should not use the game's built-in RNG module for level generation in LibLevelGen! While it will work, it will make the levels different even on the same seed, unless you specifically set up proper RNG channels youself.

local function placeEnemies(currentRoom)
    -- For convenience, objects have a reference to the instance they belong to.
    local instance = currentRoom.instance

    -- randIntRange is not inclusive for the 2nd number, so we get numbers from 1 to 3 here.
    currentRoom:placeEntityRand(tr.Enemy.Generic, "Skeleton", instance:randIntRange(1, 4))
    currentRoom:placeEntityRand(tr.Enemy.OnWall, "Spider")
end

Pretty simple so far. Another function we can use is instance:randChoice:

local function placeEnemies(currentRoom)
    local instance = currentRoom.instance

    currentRoom:placeEntityRand(tr.Enemy.Generic, "Skeleton", instance:randIntRange(1, 4))
    currentRoom:placeEntityRand(tr.Enemy.OnWall, "Spider")

    local enemyType = instance:randChoice {"Monkey", "Bat", "Ghost"}
    currentRoom:placeEntityRand(tr.Enemy.Generic, enemyType)
end
Now we have 3 enemies per room: a Skeleton, a Spider and a random enemy that's either a Monkey, a Bat or a Ghost. There's also instance:randChance, which returns a boolean:
local function placeEnemies(currentRoom)
    local instance = currentRoom.instance

    currentRoom:placeEntityRand(tr.Enemy.Generic, "Skeleton", instance:randIntRange(1, 4))
    currentRoom:placeEntityRand(tr.Enemy.OnWall, "Spider")

    local enemyType = instance:randChoice {"Monkey", "Bat", "Ghost"}
    currentRoom:placeEntityRand(tr.Enemy.Generic, enemyType)

    if instance:randChance(0.5) then
        currentRoom:placeEntityRand(tr.Enemy.MovingSlime, "Slime", 2)
    end
end
Our rooms now have a 50% to chance to also contain a blue Slime. Notice how tr.Enemy.MovingSlime is used here - it's essentialy the same as Generic, but does not place the entity next to a wall. After all, our Slime would be pretty sad if we spawned it in a way that makes it unable to move, and we do not want to make the Slime sad.

Exit rooms typically have extra enemies, so we can throw in some extra ones too:

local function placeEnemies(currentRoom)
    local instance = currentRoom.instance

    currentRoom:placeEntityRand(tr.Enemy.Generic, "Skeleton", instance:randIntRange(1, 4))
    currentRoom:placeEntityRand(tr.Enemy.OnWall, "Spider")

    local enemyType = instance:randChoice {"Monkey", "Bat", "Ghost"}
    currentRoom:placeEntityRand(tr.Enemy.Generic, enemyType)

    if instance:randChance(0.5) then
        currentRoom:placeEntityRand(tr.Enemy.MovingSlime, "Slime", 2)
    end

    if currentRoom:checkFlags(room.Flag.EXIT) then
        currentRoom:placeEntityRand(tr.Enemy.Generic, "Monkey", 2)
        currentRoom:placeEntityRand(tr.Enemy.Generic, "Slime")
    end
end
As mentioned in one of the earlier parts, room flags can be used to check for various properties of the room, including whether the room is an exit room. Anyhow, let's leave the placeEnemies function for now.

Acquiring entity names

Okay, this section is a bit of a detour, but it's worth talking about. So far the entities we placed had fairly intuitive names, but it might not always be the case so it's worth knowing how to find the name of the entity you want. The built-in level editor is of great help here - and you can actually enable it mid-run!

To enable the level editor for live-editing, open the menu and go to Customize -> Downloadable Content. Then, use the icon shown below to enable display of individual feature packs:
menu1
Once you switch this option to "on", the feature packs will be displayed:
menu2
And you can activate the level editor! After doing that, exit the menus and right-click anywhere in the level to open the editor (and press escape to close it): menu3
But we're not done yet - to show internal entity names, we need to activate this option in the editor settings: menu4
menu5

And now finally, we can search for entities. To toggle the category list in the right panel, either press [Tab] on the keyboard or right click that panel. For example, let's go to the Traps category and check the entity name of the all-directional bounce trap:
menu6
There's a small subtitle telling us that it's called BounceTrapOmni when we hover over it. Cool!

The level editor can also be used to preview the level sequence if you go to Dungeon Settings (the Stairs icon in the toolbar), which may be useful for debugging and skipping to given levels.

Placing traps

Traps are placed in the same way as enemies - let's go ahead and create a placeTraps function:

-- in myGenerator function, below the iterateRooms call that calls placeEnemies...
mainSegment:iterateRooms(room.Flag.ALLOW_TRAP, placeTraps)
-- above the myGenerator function
local function placeTraps(currentRoom)
    local instance = currentRoom.instance
end
Similarly to ALLOW_ENEMY, the ALLOW_TRAP flag can be used to check whether the room is suitable for trap placement. In this case it's similar to enemies, but the exit room will not have this flag set because exit rooms normally do not have traps. Either way, time to place some traps; the entity names can be acquired with the method explained above.
local function placeTraps(currentRoom)
    local instance = currentRoom.instance

    local trapTypes = {"BombTrap", "BounceTrapOmni", "SpikeTrap", "Sync_DiceTrap"}
    local trapCount = instance:randIntRange(1, 4)
    for i = 1, trapCount do
        currentRoom:placeEntityRand(tr.Trap.Generic, instance:randChoice(trapTypes))
    end
end
...to be honest, this is not exactly ideal - I think the trap count should instead be determined by the size of the room, to prevent small rooms from getting too many traps. I'm not going to do this here to keep things simple, but it's worth keeping room size in mind when placing things in it.

Making later floors harder

This is actually pretty simple to do - the instance:getFloor() method returns the current floor number, and you can use it to determine various things - for example, let's head back to placeEnemies for a moment and modify it a bit:

local function placeEnemies(currentRoom)
    local instance = currentRoom.instance
    -- !
    local floor = instance:getFloor()

    -- !
    currentRoom:placeEntityRand(tr.Enemy.Generic, "Skeleton", instance:randIntRange(math.min(floor, 3), 4))
    currentRoom:placeEntityRand(tr.Enemy.OnWall, "Spider")

    local enemyType = instance:randChoice {"Monkey", "Bat", "Ghost"}
    currentRoom:placeEntityRand(tr.Enemy.Generic, enemyType)

    -- !
    if instance:randChance(0.5) or floor > 2 then
        currentRoom:placeEntityRand(tr.Enemy.MovingSlime, "Slime", 2)
    end

    -- !
    if floor > 1 then
        currentRoom:placeEntityRand(tr.Enemy.Generic, "Bat", 2)
    end

    if currentRoom:checkFlags(room.Flag.EXIT) then
        currentRoom:placeEntityRand(tr.Enemy.Generic, "Monkey", 2)
        currentRoom:placeEntityRand(tr.Enemy.Generic, "Slime")
    end
end
I marked the modified parts with a ! comment. While changing just the enemies is pretty basic, this is just an example - there is a whole lot more you can do across the entire level!

A bunch of automatic things

LibLevelGen has a few quite useful options that you can enable: let's add a little thing to our generator configuration...

libLevelGen.registerGenerator("LibLevelGen Playground", myGenerator, {
    placeShrines = true, -- !!!
    levelsPerZone = 4,
    zones = 4,
    sequenceClb = levelSequence.templateAllZones()
})
And just like that, LibLevelGen will place shrines automatically in every zone: autoshrine

Note: this feature had a bug in versions 1.0.0 and 1.1.0, update to 1.1.1 or later should issues occur.

The same can be done with secret shops:

libLevelGen.registerGenerator("LibLevelGen Playground", myGenerator, {
    placeShrines = true,
    placeSecretShops = true, -- !!!
    levelsPerZone = 4,
    zones = 4,
    sequenceClb = levelSequence.templateAllZones()
})
autoshop

You can even define your own shrines and shop types to be placed, but we'll talk about it later (since it's a bit more advanced).

There's more things like this, but I think this part is long enough by now - to be continued...