Creating continuity

In this chapter, we’ll create a Game object and store it in a global called GAME to track the overall state of the game. This includes managing a random number generator (RNG) that we’ll use to seed level generation, as well as keeping track of the dungeon depth the player is currently exploring.

Getting the message

First, let’s update our DescendMessage to include the actor that’s descending.

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

--- @param descender Actor
function DescendMessage:__new(descender)
   self.descender = descender
end

return DescendMessage

Next, let’s modify the Descend action so that it populates the message with the descending actor.

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

Creating the game

Now we’ll create a new class Game. This will hold an RNG and the current depth, and handle our level generation. Having a centralized RNG for generating seeds for level generation ensures that the game will be repeatable given the same seed.

local levelgen = require "levelgen"

--- @class Game : Object
--- @field depth integer
--- @field rng RNG
--- @overload fun(seed: string): Game
local Game = prism.Object:extend("Game")

--- @param seed string
function Game:__new(seed)
   self.depth = 0
   self.rng = prism.RNG(seed)
end

--- @return string
function Game:getLevelSeed()
   return tostring(self.rng:random())
end

--- @param player Actor
--- @return MapBuilder builder
function Game:generateNextFloor(player)
   self.depth = self.depth + 1

   local genRNG = prism.RNG(self:getLevelSeed())
   return levelgen(genRNG, player, 60, 30)
end

return Game(tostring(os.time()))

This class will eventually track everything we need for the overall game. There should only be one instance of a Game, so we return a seeded Game instance rather than the prototype.

Modifying the levelstate

The first thing we’ll do is remove the levelgen require:

-local levelgen = require "levelgen"

Next we’ll change GameLevelState’s constructor.

--- @param display Display
--- @param builder MapBuilder
--- @param seed string
function GameLevelState:__new(display, builder, seed)
   -- Build the map and instantiate the level with systems
   local map, actors = builder:build()
   local level = prism.Level(map, actors, {
      prism.systems.Senses(),
      prism.systems.Sight(),
      prism.systems.Fall(),
   }, nil, seed)

   -- Initialize with the created level and display, the heavy lifting is done by
   -- the parent class.
   spectrum.LevelState.__new(self, level, display)
end

This sets up our level with the map we build and the seed we’ll pass from the Game. Let’s change our overload here as well to reflect the new arguments.

--- @overload fun(display: Display, builder: MapBuilder, seed: string): GameLevelState
local GameLevelState = spectrum.LevelState:extend "GameLevelState"

Now modify our message handler so it passes the player into the next level:

if prism.messages.Descend:is(message) then
   --- @cast message DescendMessage
   self.manager:enter(
      GameLevelState(
         self.display,
         Game:generateNextFloor(message.descender),
         Game:getLevelSeed()
      )
   )
end

To indicate what level we’re on, add another call to Display:putString() below our health display:

if health then self.display:putString(1, 1, "HP: " .. health.hp .. "/" .. health.maxHP) end

self.display:putString(1, 2, "Depth: " .. Game.depth)

Finally, head over to main.lua and require the class right below where we’re loading all our modules.

...
prism.loadModule("modules/game")

local Game = require("game")

In love.load(), we’ll generate the first level and pass a seed for the level to our GameLevelState.

local builder = Game:generateNextFloor(prism.actors.Player())
manager:push(GameLevelState(display, builder, Game:getLevelSeed()))

Launch the game, and your health should be maintained between floors!

Moving along

We’ve created a Game class to maintain some global game state and now pass our player to the next level. In the next section, we’ll start working on an inventory system, along with a few items.