Modding:Tutorial/Constructing a Map

From DoomRL Wiki

Revision as of 03:44, 16 September 2011 by Game Hunter (Talk | contribs)

Jump to: navigation, search

There are endless possibilities when it comes to modding DoomRL, whether it be custom enemies and items, or additional special levels, or maybe themes that depend on what day of the week it is. However, much of this can be difficult to understand, let alone create, if you are unfamiliar with programming techniques. For those of you simply seeking to create a single level, leaving the core elements of the game intact, there is a very easy method of doing this, especially with the advent of modules.

If you are creating a mod, the first thing necessary for DoomRL to recognize it is a directory that is the identifier of your mod, along with a .module at the end: this would be placed in the "modules" directory, which exists wherever your DoomRL directory is. For instance, if I were creating a mod that I would identify as "coolmap" (and assuming that my DoomRL directory were in something like C:/Games), the path to the module files would look like this:

C:/Games/DoomRL/modules/coolmap.module

Note that this identifier is not necessarily the NAME of your map, it is simply a way to distinguish it from other modules.

After we have created this directory, there are two necessary lua files that must be in it: main.lua, which acts as the file that initializes all of the module's data; and module.lua, which provides information about the module itself. main.lua will contain everything important to the actual modding part of this tutorial, but we shall look at module.lua first.

module.lua

module.lua is a very short script, containing only what is called a metadata table, or a table that holds information about the mod. Every module.lua should look have a base like so:

module = {
    id          = "",        --identifier of the module (same as the folder sans '.module')
    name        = "",        --name of module as it appears in the Custom Game menu
    author      = "",        --your name/gamertag/nickname/alias
    webpage     = "",        --link to your page (use 'none' if you don't have one)
    version     = {0,0,0},   --version of the mod itself (first version is typically {0,1,0})
    drlver      = {0,0,0,0}, --version of DoomRL that module was made for (e.g., {0,9,9,4})
    type        = "single",  --module type (currently only "single" works)
    description = "",        --explains mod, though you could put whatever you want here
    difficulty  = true,      --allows player to set difficulty (false forces ITYTD)
}

Feel free to copy and paste this into your module.lua, then fill in the information as necessary.

main.lua

main.lua is the core of your module, containing all of the necessary components to make the mod run as you would like it to. To start, every main.lua requires the following line:

core.declare("module_id", {} ) --replace module_id with your module's actual id

This declares a module table that you will use to store all of the module-ranging components. In the case of single levels (currently the only mod type available), this essentially replaces the Level object for the purposes of creating engine hooks that depend on events that occur within the single level. These are the following engine hooks that your module can use:

function module_id.run() end       --works just like Level.Create() (initialize map/name/spawn point/etc)
function module_id.OnEnter() end   --triggers immediately after .run(), when player enters the map
function module_id.OnTick() end    --triggers every game turn
function module_id.OnKill() end    --triggers whenever a being other than yourself dies
function module_id.OnKillAll() end --triggers whenever all beings other than yourself are dead
function module_id.OnExit() end    --triggers when player exits the map (for instance, decsending stairs)

If you choose not to use an engine hook, you don't need to include it in the script, but there's no harm in writing an empty hook in there, either.

We will now provide some very basic examples of each engine hook so you get an idea of how they can be used. The module's identifier will be "blank".

function blank.run()
    Level.name = "Blank Map" --name of map as it appears in lower-right corner of HUD
    Level.name_number = 0    --shows "LevX" where X is value: use 0 to remove entirely
    Level.fill("pwall")      --necessary step, use whatever wall you will mostly use
 
    --matches all characters on map to an object_id
    local translation = {
    ["."] = "floor",
    ["#"] = "pwall",
    [">"] = "stairs",
    }
 
    --the map itself
    local map = [[
################################################################################
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#...........................................>..................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
################################################################################
]]
 
    Level.place_tile(translation,map,1,1) --this is the map-creation step
    Level.player(39,10)                   --player is placed at this coordinate
end

.run() deals with the absolutely necessary pieces if you want the module to work at all. Here we define the level's in-game name, create the map, and add the player. There are two important variables we define:

  • translation is a table of all object ids that will be placed on the map, along with the symbols that define them on said map. You can use any symbol you want to define an object (for instance, using ["y"] = "pwall"), just remember to place it on the map with that symbol. The symbols as they are used here will not affect how the actual objects appear in-game.
  • map is a string that corresponds to what the map should look like when it is initialized. Here we use double-square brackets ("[[" and "]]"), which takes every character between them and writes into a single string. You can start new lines as much as you want, but do NOT include spaces unless the space character is one of the symbols used in translation.

You can define "translation" and "map" whatever you want so long as Level.place_tile calls them appropriately. The last two arguments define what x- and y-coordinates are used to begin the placing: since we are copying an entire map, we start from the upper-left tile (which is (1,1)).

function blank.OnEnter()
    ui.msg("Hello world!")    --appears at the top of the screen
    player.inv:add("shotgun") --puts a shotgun in the player's inventory
    player.inv:add("shell")   --puts a shotgun shell (x8) in the player's inventory
end

.OnEnter() hook has fairly limited uses, since it is only called once and doesn't do the crucial steps like making the map itself. For the most part, it is used to add messages at the start of the map, change around the player's inventory, and, depending on what else is going on in the map, initialize some timers or countdowns.

local countdown = 600   --set up a timer
local minute = 0        --set up a minute counter
local exit_check = true --set up a position check
 
function blank.OnTick()
    if countdown == 0 then
        countdown = 600
        minute = minute + 1
        ui.msg("Number of minutes passed: ".. minute)
    end
    countdown = countdown - 1
 
    if player:get_position() == coord.new(45,10) and exit_check then
        ui.msg("Leaving already?")
        exit_check = false
    end
end

.OnTick() is, by far, the most practical trigger of the module engine hooks, allowing you not only to set up time-specific events but also position-specific ones. In the example above, both cases are used:

  • A countdown variable starts at 600: whenever it reaches zero (as 60 game seconds have passed), the countdown is reset, minute is incremented by one, and the game tells you how many minutes have passed.
  • Whenever the player enters the same tile as the stairs (the stairs happen to be on (45,10)), the game sends a message, and then sets exit_check to false. Were this check not added, the message would display continuously for each turn that you stay in the tile (so it will keep displaying the message if you wait on the tile, for instance). However, this also means it will only display once, as there are no conditions in the code in which exit_check can be made true again.

Setting up too many events in the .OnTick() hook may cause some game slow-down, but it usually takes a monstrous amount of code before there is even a noticeable effect.

local killCount = 0 --set up a kill counter
 
function blank.OnKill()
    killCount = killCount + 1 --increment the kill counter
 
    --display messages dependent on kills
    if killCount == 1 then
        ui.msg("You have killed 1 enemy.")
    else ui.msg("You have killed " .. killCount .. " enemies.")
    end
    if killCount == 10 then
        ui.msg("Wow, killing spree!")
    end
end

.OnKill() lets you trigger events when an enemy is killed by you specifically. There are a number of possibilities you may want to try (adding to berserk time, increasing health, spawning new enemies, etc): this example displays a simple message whenever you kill something, and gives you a metaphorical pat on the back for ten kills.

function blank.OnKillAll()
    Level.summon("former",5)
    ui.msg("They just keep coming!!")
end

.OnKillAll() is like .OnKill() but only triggers when everything on the map has died. This is how, for instance, Hell's Arena knows when to start the next wave of enemies (or when you're completely done). In this example, whenever you kill everything on the map, the game randomly spawns five former humans, essentially allowing this trigger to occur for as many times as the player wants to do so.

function blank.OnExit()
    ui.msg_enter("Goodbye world!")
end

.OnExit(), just as with .OnEnter(), is limited in its uses when it comes to a single level. You can add some messages, maybe tidy up some variables for the mortem, but once the player is leaving there isn't much left with which to interact. (In this case, ui.msg_enter() is used in order to force a confirmation on the player: otherwise the message could very well be lost in the mortem bloodslide effect.)

Review

The following is a summation of all the lines of codes used to create the module.lua and main.lua scripts, with a few changes here and there to ensure that everything will run as intended (and a couple additions to make it more playable).

--module.lua file
 
module = {
    id          = "blank",
    name        = "A Blank Map",
    author      = "DoomRL Mod Team",
    webpage     = "http://doom.chaosforge.org/wiki/Modding:Tutorial",
    version     = {0,1,0},
    drlver      = {0,9,9,4},
    type        = "single",
    description = "This is an example mod, intended to be altered by anyone wanting " ..
                  "to test things using the DoomRL base set.",
    difficulty  = true,
}
--main.lua file
 
core.declare("blank", {} )
 
function blank.OnEnter()
    ui.msg("Hello world!")
 
    --give the man a shotty
    player.inv:add("shotgun")
    --create shotgun shell item with a modified amount of ammo
    local pack = item.new("shell")
    pack.ammo = 50
    player.inv:add(pack)
end
 
--local variables not within a loop are local to the file itself
--for global variables (across all files), you want to use core.declare()
--always remember to declare things before they are called somewhere else!
local countdown = 600
local minute = 0
local exit_check = true
 
function blank.OnTick()
    --keeps track of game minutes
    if countdown == 0 then
        countdown = 600
        minute = minute + 1
        ui.msg("Number of minutes passed: ".. minute)
    end
    countdown = countdown - 1
 
    --warns player if they are on stairs
    if player:get_position() == coord.new(45,10) and exit_check then
        ui.msg("Leaving already?")
        exit_check = false
    end
    if player:get_position() ~= coord.new(45,10) and not exit_check then
        exit_check = true
    end
end
 
local killCount = 0
 
function blank.OnKill()
    killCount = killCount + 1
 
    --display messages dependent on kills
    if killCount == 1 then
        ui.msg("You have killed 1 enemy.")
    else ui.msg("You have killed " .. killCount .. " enemies.")
    end
    if killCount == 10 then
        ui.msg("Wow, killing spree!")
    end
end
 
function blank.OnKillAll()
    --more enemies are added whenever all are defeated
    Level.summon("former",5)
    ui.msg("They just keep coming!!")
 
    --as a compensation, you are healed to 100% each time
    player.hp = player.hpmax
end
 
function blank.OnExit()
    ui.msg_enter("Goodbye world!")
end
 
--Personally I like to add this at the end, but these hooks can be placed in any order.
function blank.run()
    Level.name = "Blank Map"
    Level.name_number = 0
    Level.fill("pwall")
 
    local translation = {
    ["."] = "floor",
    ["#"] = "pwall",
    [">"] = "stairs",
    ["h"] = {"floor", being = "former"},                 --places cell with being on top
    ["]"] = {"floor", item = "garmor"},                  --places cell with item on top
    ["k"] = {"floor", being = "former", item = "phase"}, --places cell with both
    }
 
    local map = [[
################################################################################
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#...................................................h..........................#
#..............................................................................#
#......................................................h.......................#
#..............................................................................#
#......................................]....>........h.....k...................#
#..............................................................................#
#......................................................h.......................#
#..............................................................................#
#...................................................h..........................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
#..............................................................................#
################################################################################
]]
    Level.place_tile(translation,map,1,1)
    Level.player(39,10)
end

Try it out and mess around. The code used here should be easy enough to understand that a player can apply it as a base for quick-test modules. If you plan on using it yourself, be sure to create a "blank.module" folder in your modules directory and add these two scripts (as .lua files) into that folder.

Personal tools