Descending into the depths

In this chapter we’re going to add stairs and create a new Level when the player uses our new Descend action on the stairs.

The stairs themselves

Navigate to modules/game/components/ and create a new file called stair.lua. This will be a simple tag component to indicate the actor can be descended.

--- @class Stair : Component
local Stair = prism.Component:extend("Stair")

return Stair

Next we’ll register a Stairs actor in modules/game/actors/stairs.lua with the new component.

prism.registerActor("Stairs", function()
   return prism.Actor.fromComponents {
      prism.components.Name("Stairs"),
      prism.components.Position(),
      prism.components.Drawable { char = ">" },
      prism.components.Stair(),
      prism.components.Remembered(),
   }
end)

Note

Actors with a Remembered component will remain in an actor’s Senses after being seen. Handy for actors you want other actors to, well, remember.

Placing the stairs on the map

We’ve defined our stairs, it’s time to place them on the map. Navigate to levelgen.lua and head to the bottom of the function right above return.

--- @type Rectangle[]
local availableRooms = {}
for _, room in pairs(rooms) do
   if room ~= startRoom then
      table.insert(availableRooms, room)
   end
end

local stairRoom = availableRooms[rng:random(1, #availableRooms)]
local corners = stairRoom:toCorners()
local randCorner = corners[rng:random(1, #corners)]

builder:addActor(prism.actors.Stairs(), randCorner.x, randCorner.y)

We collect all the rooms the player didn’t spawn in into a table, and then choose a random room. We place the stairs in a random corner of that room for now.

Descending

Navigate to modules/game/messages and define a new message in descend.lua. For now we don’t need anything inside.

--- @class DescendMessage : Message
--- @overload fun(): DescendMessage
local DescendMessage = prism.Object:extend("DescendMessage")

return DescendMessage

Next, we’ll define the Descend action.

local DescendTarget = prism.Target()
   :with(prism.components.Stair)
   :range(1)

---@class Descend : Action
---@overload fun(owner: Actor, stairs: Actor): Descend
local Descend = prism.Action:extend("Descend")
Descend.targets = { DescendTarget }

function Descend:perform(level)
   level:removeActor(self.owner)
   level:yield(prism.messages.Descend())
end

return Descend

First we create a target that targets actors with the Stair component within range 1. Then we create our Descend action, which is similar to Die but yields a different message.

Now let’s add some code to GameLevelState:keypressed. After we figure out which direction the user just pressed we’ll add the following. Make sure this is checked before the Move action is considered.

if keybindOffsets[action] then
   local destination = owner:getPosition() + keybindOffsets[action]

   -- add this
   local descendTarget = self.level:query(prism.components.Stairs)
      :at(destination:decompose())
      :first()

   local descend = prism.actions.Descend(owner, descendTarget)
   if self.level:canPerform(descend) then
      decision:setAction(descend)
      return
   end

Creating the next floor

Now that we’ve got everything set up we need to actually handle the descend message. In GameLevelState:handleMessage we’ll add the following message handling.

if prism.messages.Descend:is(message) then
   self.manager:enter(GameLevelState(self.display))
end

If we run the game and find ourselves a staircase we’ll be able to go down to a new floor!

There are a couple of problems, though. The new level has a completely new player on it and we’re not tracking depth anywhere.

In the next chapter

We’ve created a Stairs actor that takes us down an infinite amount of levels. In the next chapter, we’ll set up a Game object that tracks what depth we’re on and manages level generation. We’ll pass the player to the new level so that we’re playing as the same actor all the way down.