Modding:Tutorial/Building an Episode

From DoomRL Wiki

Jump to: navigation, search

The simplest of modules are built upon a single map, a single level. While this can provide for some very unique and thrilling gameplay, it is not nearly on the same scale of DoomRL itself, which is a gauntlet of many levels, one after another. If you'd prefer to have a module with the structure of something longer and more challenging than a single level can produce, you're probably looking to create an episode. The purpose of this tutorial is to explain exactly how that is done.

In order to fully explore the possibilities of an episodic module, make sure you understand the basics of modding. If nothing else, take a look at the tutorial Constructing a Map before continuing on.

Contents

Special Concept: Setting Levels

The player object comes with a custom property called "episode". This is what determines where the player will be when they reach a certain depth. In fact, this is the majority is what is necessary to string together all the levels in an episode. This is combined with an episodic engine hook known as OnCreateEpisode to build the episode's content. Here is a simple example:

core.declare( "a_mod", {} ) --initialize the module object
 
function a_mod.OnCreateEpisode()
    player.episode = {}     --initialize/reset the table
 
    --defines first floor as a special level
    player.episode[1] = { style = 1, script = "hells_arena" }
    --defines second floor as a random level
    player.episode[2] = { style = 1, number = 1, name = "Floor", danger = 1}
end

The following would create an episode with two levels. The first level is defined by calling a level "script": the script used here is actually Hell's Arena from the base game. Because the map was initialized with the rest of the game (and this is not a total conversion module), it is included as part of the potential level scripts. So long as a level object has been created and initialized in your module, you are free to include any levels you want as well. (Although style does not have an effect on the level, it is necessary to include as a formality.)

The second level is defined through level properties, rather than calling a script. When this is done, the game can initialize a level with these properties but must still produce some sort of generated content. For this, we require the OnGenerate() hook:

function a_mod.OnGenerate()
    Generator.reset()
    Generator.generate_caves_dungeon()
end

OnGenerate() controls the content of unscripted maps. This can be technically used to create an identical series of maps, but it is much more interesting to create maps using generator functions. Generator.reset() clears any leftover data in the generator, while Generator.generate_caves_dungeon() creates a random cave level (of which you are probably familiar). The properties defined from OnCreateEpisode() will come into play for the purposes of determining things such as the walls, doors, floors, danger level, and so on. See Modding:Generator for a complete list of generator functions with which to play around.

Now that we understand the basics of producing an episodic module, let us discuss a simple example that emulates that of a very old version of the game: Classic Mode.

Engine Hooks

.OnCreateEpisode()

function classic.OnCreateEpisode()
    local BOSS_LEVEL = 10
    player.episode = {}
 
    player.episode[1]     = { style = 1, script = "intro" }
    for i=2,BOSS_LEVEL-1 do
        player.episode[i] = { style = 1, number = i, name = "Phobos", danger = i}
    end
    player.episode[10]    = { style = 3, script = "phobos_arena" }
 
    statistics.bonus_levels_count = 0
end

As before, we initialize the episode table. We've also defined the end level, coined BOSS_LEVEL, as the tenth floor. The layout is fairly straightforward:

  • The very first level takes place in Phobos Base Entry. This is indicated with the script "intro".
  • Floors two through nine are set as randomly-generated levels. They will be named "Phobos 2", "Phobos 3", etc, and will have danger levels of the same number.
  • The final level is a level script called "phobos_arena". Although this is not included in the base game, we'll be providing this script ourselves for the purpose of this episode.

Finally, as a matter of completeness, since there are technically no "special" levels in this episode, we set the bonus level count to zero from the statistics object. Special levels can actually be included as part of the level initialization by defining the special key: red stairs will be automatically provided on that level in addition to everything else. In either case, however, the statistics for bonus levels must be manually set.

.OnGenerate()

function classic.OnGenerate()
    Generator.reset()
    Generator.generate_tiled_dungeon()
end

This function is almost identical to the one from the concepts example: the generator object is reset, and then a tiled dungeon is set for any and all unscripted (random) levels. The tiled dungeon is the de-facto level style found in DoomRL: it is the one that always appears on the second floor, regardless of the circumstances. Since this is a classic map, all of the levels are determined in this manner. You can, however, use some randomization within OnGenerate() to change which map is actually generated. The specifics of the randomization in the base game can be found in Level type.

.OnMortemPrint()

function classic.OnMortemPrint(killedby)
    if killedby == "defeated the Mastermind" then
        killedby = "defeated the Cyberdemon"
    elseif killedby == nil then
        killedby = "played through the classics"
    end
    player:mortem_print( " "..player.name..", level "..player.explevel.." "..klasses[player.klass].name..", "..killedby )
end

OnMortemPrint() is where all mortem-printing should be done, hence its name. It comes with the killedby parameter, which determines the status of the player at the time of the printing. Despite its name, killedby can produce a variety of messages, including those for winning the game. This mortem_print in particular is just like the one from the tutorial for Hell's Arena, so it should be familiar enough to understsand already.

Of particular note is that we require a change in the victory condition: killedby returns that the Mastermind was defeated in a standard victory, so we change it to reflect that it was the Cyberdemon instead.

.OnWinGame()

function classic.OnWinGame()
    ui.plot_screen([[
Once you beat the Cyberdemon and clean out the moon
base you're supposed to win, aren't you? Aren't you?
Where's your fat reward and ticket back home? What
the hell is this? It's not supposed to end this way!
 
It stinks like rotten meat but it looks like the
lost Deimos base. Looks like you're stuck on
The Shores of Hell. And the only way out is through...
]])
end

The OnWinGame() hook allows you to play around with some of the user interface commands upon winning the game. If you recall beating the base game, it's what happens immediately after killing the Spider Mastermind, but before the mortem appears. Our use of the hook simply displays a single screen of plot with the ui.plot_screen() function, which accepts a single string. You can play around with some properties here but that's better saved for OnMortem().

Levels: Phobos Arena

Levels("phobos_arena",{
  name = "Phobos Arena",
  entry = "Then at last he found the Phobos Arena!",
  welcome = "You enter a big arena. There's blood everywhere. You hear heavy mechanical footsteps...",
 
  Create = function ()
    Level.fill("wall")
    Level.fill("floor", area.FULL_SHRINKED )
    local scatter_area = area.new( 5,3,68,15 )
    local translation = {
        ['.'] = "blood",
        ['#'] = "gwall",
        ['>'] = "stairs",
    }
    Level.scatter_put(scatter_area,translation, [[
      .....
      .###.
      .###.
      .###.
      .....
    ]],"floor",12)
 
    Level.flags[ LF_NOHOMING ] = true
    Level.scatter(area.FULL_SHRINKED,"floor","blood",100)
    Generator.set_permanence( area.FULL )
    player.flags[ BF_ENTERBOSS1 ] = true
  end,
 
  OnEnter = function ()
    local boss = Level.summon("cyberdemon")
    boss.flags[ BF_BOSS ] = true
  end,
 
})

This is the level included with Classic Mode: the end-game level, Phobos Arena. This is, more or less, a replica of the original end-game level prior to version 0.9.9.5. Many of the things covered here have already been explained in the singular module tutorials, so they will not be described in detail here. This is a quick overview:

  • We start with a wall-bordered blank room, randomly add 12 pillars away from the borders, then randomly populate the rest of the level with blood.
  • Homing phase devices will fail to work here (though there are no stairs to phase to anyway).
  • Everything in the level is made permanent so that no walls can be destroyed
  • The player's flags are modified so that the game knows we are on a boss level.
  • Finally, the Cyberdemon is spawned at a random coordinate and given the boss flag, so that killing it ends the game.

One thing not seen in previous tutorials, however, is the Level object itself. In a singular module, the level is created with the run() function: in an episodic module, levels are defined separately and called when necessary. The basic level structure is as follows:

  • Levels are technically handled with functional syntax. The identifier is placed as the first argument in this function, and a table that defines the level's properties and hooks is given as the second argument.
  • Typical level properties are:
    • name is what the level is called, as it appears at the bottom-right.
    • entry is what appears in the mortem, signifying that you entered the level.
    • welcome is what appears in the message log as you enter the level.
  • Hooks are added to the level, just as they would be for a singular module. Of particular note is that the run() hook is replaced with Create(): they are functionally identical, but use different names due to where they are called.

Within the module's source, this level is given its own file in a separate folder called "data". In order for the module to access the file, a function called require is used:

require "classic:data/phobos_arena"

The module's name is written before the colon, and the path from the module's root folder is written after. require assumes a .lua extension, and few other files would be useful at this time.

Review

Included is the main.lua, phobos_arena.lua, and the module.lua used to run the file:

-- module.lua
 
module = {
    id = "classic",
    name = "Classic Mode",
    author = "Kornel Kisielewicz",
    webpage = "http://chaosforge.org/",
    version = {0,2,0},
    drlver = {0,9,9,5},
    type = "episode",
    description = "Episodic MOD example.",
    difficulty = true,
}
-- main.lua
 
core.declare( "classic", {} )
 
require "classic:data/phobos_arena"
 
function classic.OnMortemPrint(killedby)
    if killedby == "defeated the Mastermind" then
        killedby = "defeated the Cyberdemon"
    elseif killedby == nil then
        killedby = "played through the classics"
    end
    player:mortem_print( " "..player.name..", level "..player.explevel.." "..klasses[player.klass].name..", "..killedby )
end
 
function classic.OnCreateEpisode()
	local BOSS_LEVEL = 10
	player.episode = {}
 
	player.episode[1]     = { style = 1, script = "intro" }
	for i=2,BOSS_LEVEL-1 do
		player.episode[i] = { style = 1, number = i, name = "Phobos", danger = i}
	end
	player.episode[10]    = { style = 3, script = "phobos_arena" }
 
	statistics.bonus_levels_count = 0
end
 
function classic.OnGenerate()
	Generator.reset()
	Generator.generate_tiled_dungeon()
end
 
function classic.OnWinGame()
	ui.plot_screen([[
Once you beat the Cyberdemon and clean out the moon
base you're supposed to win, aren't you? Aren't you?
Where's your fat reward and ticket back home? What
the hell is this? It's not supposed to end this way!
 
It stinks like rotten meat but it looks like the
lost Deimos base. Looks like you're stuck on
The Shores of Hell. And the only way out is through...
]])
end
-- phobos_arena.lua
 
Levels("phobos_arena",{
  name = "Phobos Arena",
  entry = "Then at last he found the Phobos Arena!",
  welcome = "You enter a big arena. There's blood everywhere. You hear heavy mechanical footsteps...",
 
  Create = function ()
    Level.fill("wall")
    Level.fill("floor", area.FULL_SHRINKED )
    local scatter_area = area.new( 5,3,68,15 )
    local translation = {
        ['.'] = "blood",
        ['#'] = "gwall",
        ['>'] = "stairs",
    }
    Level.scatter_put(scatter_area,translation, [[
      .....
      .###.
      .###.
      .###.
      .....
    ]],"floor",12)
 
    Level.flags[ LF_NOHOMING ] = true
    Level.scatter(area.FULL_SHRINKED,"floor","blood",100)
    Generator.set_permanence( area.FULL )
    player.flags[ BF_ENTERBOSS1 ] = true
  end,
 
  OnEnter = function ()
    local boss = Level.summon("cyberdemon")
    boss.flags[ BF_BOSS ] = true
  end,
 
})

Source data: classic.module

Personal tools