Under the hood, Crawl translates everything in a .des file to Lua. You don't need to know what the underlying Lua looks like to design levels, but it helps.
Crawl uses Lua 5.1 from http://www.lua.org (the site has information on the Lua language). Let's examine how Crawl converts a map definition into Lua code with an example map:
NAME: statue_in_pool TAGS: no_rotate no_pool_fixup : if you.absdepth() < 7 then MONS: plant : else MONS: oklob plant : end MAP 1...1 .www. .wGw. .www. 1...1 ENDMAP
Crawl will convert this map into the following Lua code wrapped in anonymous functions (an anonymous function that takes no parameters is called a Lua chunk):
function mapchunk() map("1...1") map(".www.") map(".wGw.") map(".www.") map("1...1") end function main() tags("no_rotate") tags("no_pool_fixup") if you.absdepth() < 7 then mons("plant") else mons("oklob plant") end end
You'll notice that these functions are not actually anonymous, but they're named just to distinguish them.
If your level defines prelude or validation Lua code, such code is extracted into separate prelude and validation chunks. The prelude and validation chunks are empty unless specified.
Apart from the special NAME map header, every map header translates to a Lua function with the same name in lowercase. For instance, KFEAT: <xyz> is translated into kfeat(”<xyz>”).
If you have a space or comma separated list (such as TAGS, MONS, ITEM, etc.), then each space/comma separated item is passed into a separate call to the corresponding Lua function. For instance:
TAGS: no_rotate no_pool_fixup
→
tags("no_rotate") tags("no_pool_fixup")
MONS: orc, gnoll
→
mons("orc") mons("gnoll")
Knowing what the generated Lua looks like under the hood is useful because it allows you to extract repeated boilerplate in similar vaults into a Lua function in the .des file's prelude. For instance, if you were planning to write a whole slew of vaults featuring statues in water guarded by plants, you could extract the common code into the top of the .des file as:
{{ -- This block has to be placed before any other vault in the .des file. function statue_pool_map(e) e.tags("no_rotate") e.tags("no_pool_fixup") if you.absdepth() < 7 then e.mons("plant") else e.mons("oklob plant") end end }} NAME: statue_in_pool # Pass in the Lua environment global _G to the prelude function. : statue_pool_map(_G) MAP 1...1 .www. .wGw. .www. 1...1 ENDMAP
You can also use arbitrary Lua directly in vault definitions, which is handy when randomizing things:
NAME: statue_in_pool : local plant_weight = crawl.random_range(1,10) : mons("plant w:" .. plant_weight .. : " / oklob plant w:" .. (10 - plant_weight)) MAP 1...1 .www. .wGw. .www. 1...1 ENDMAP
You can check what Lua code is produced when a .des file is compiled by starting Crawl with the -dump-maps option:
./crawl -dump-maps -builddb 2>compiled-maps.lua
This will only produce output for .des files that are compiled, so if Crawl has already compiled all .des files it will produce no output. You can force Crawl to recompile a .des file by updating its modification time or by deleting the $SAVEDIR/des directory.
Also note that Crawl writes -dump-maps output to stderr, not stdout, hence the use of 2> for redirection.
A map's Lua chunk consists of calls to functions such as tags(), mons(), etc. These functions are defined in the dgn table (see the Lua API reference below), and they expect to act on an instance of Crawl's C++ mapdef object. Given:
tags("no_rotate")
the actual Lua call needs to be:
dgn.tags(<map>, "no_rotate")
Where <map> is the C++ map object to which the tag should be added. Since calling dgn.<foo>(<map>, <xxx>) is tedious, dat/dlua/dungeon.lua wraps the Lua chunk for the map into an environment that defines wrappers for all the functions in 'dgn' as:
function <xyz>(...) dgn.<xyz>(<map>, ...) end
i.e. for every function <xyz> in the 'dgn' table, we define a new function <xyz> that just calls dgn.<xyz>() with the current map as the first parameter, and the other parameters as passed in. Thus Lua code that you write as:
tags("no_rotate")
is translated to the correct
dgn.tags(<map>, "no_rotate").
While this is done automatically for map code, if you need to call Lua code that was not defined in the scope of the map, as in the example statue_pool_map() function, you need to pass in the map environment to that function if you want it to modify the map. Thus the call to statue_pool_map looks like:
: statue_pool_map(_G)
Every .des file can have (at the start of the file) Lua code that is not associated with any specific map, but with all maps in the file. This is called the global prelude. The global prelude is run before running any other Lua code in the file, once during compilation, and once at start of game.
You can use the global prelude to define functions and set up globals that the rest of the maps in the .des file use. If you have a lot of common code, you should probably add it to dungeon.lua instead.
: crawl.mpr("Hello")
Colon-prefixed lines are always in the main Lua chunk, unless they occur before any map definitions, in which case they go to the global prelude.
lua {{ <code> }}
or
lua {{ <code> }}
The “lua” word is optional; you can also use:
{{ <code> }}
and
{{ <code> }}
NOTE: Colon-prefixed lines, or lua maps blocks defined before any map's NAME: directive will add the Lua code to the global prelude.
prelude {{ <code> }}
or
prelude {{ <code> }}
validate {{ <code> }}
or
validate {{ <code> }}
Since Lua action happens in the guts of Crawl, it can be hard to tell what's going on. Lua debugging involves the time-honoured method of peppering your code with print statements:
It's very important that your finished level never croaks during level-generation. A Lua error at this stage is considered a validation failure.
There are several properties a Lua marker can have which will affect the dungeon cell which they are on:
Found <whatever>.
Using the MonPropsMarker allows you to permanantly alter or mark a monster that the marker is placed upon. The options currently available are:
An example of MonPropsMarker to replace the description and quote of a monster:
MARKER: 8 = lua:MonPropsMarker:new {description="What a horrible sight!\n", \ quote='"They were filled with fear!'\n"}
Note that all of the speech related properties will be reset if the monster polymorphs.
a. The Map. b. Global game state. c. Character information.
Lua functions dealing with the map are mostly grouped under the “dgn” module. For convenience, .des file Lua chunks are run in an environment such that function calls written as:
fn(x, y, ...)
are translated to
dgn.fn(map, x, y, ...)
where “map” is the reference to the map that the currently executing Lua chunk belongs to. This is only for Lua chunks that belong to a map, Lua code in the global prelude does not get this treatment (because the global prelude is not associated with any map).
Functions in the dgn module:
default_depth, name, depth, place, tags, tags_remove, chance, weight, orient, shuffle, shuffle_remove, subst, subst_remove, map, mons, item, kfeat, kitem, kmons, grid, points_connected, gly_point, gly_points, original_map, glyphs_connected, orig_glyphs_connected, orig_gly_point, orig_gly_points, load_des_file, feature_number, feature_name, dgn_event_type, register_listener, remove_listener, remove_marker, num_matching_markers, feature_desc, feature_desc_at, item_from_index, mons_from_index, change_level_flags, change_branch_flags, set_random_mon_list
Additionally, the dgn module provides a global “mapgrd” variable that can access the current map glyphs. The top left symbol in the map can be assigned like this:
mapgrd[0][0] = 'x'
The bottom right symbol can be assigned like this:
mapgrd[width()-1][height()-1] = "."
The “you” module provides functions that describe the player character.
turn_is_over, spells, abilities, name, race, class, god, hp, mp, hunger, strength, intelligence, dexterity, xl, exp, res_poison, res_fire, res_cold, res_draining, res_shock, res_statdrain, res_mutation, res_slowing, gourmand, levitating, flying, transform, stop_activity, floor_items, where, branch, subdepth, absdepth
The “colour” module provides functions for defining new colour patterns.
<code> add_colour </code>
When the dungeon builder places a vault, it runs hooks at certain steps during vault building. These hooks are:
You may also specify hooks as “main”, “place”, “epilogue”, which will select the post_XXX hook. i.e. using “main” as the hook name is the same as writing “post_main”.
Hooks may be global, in which case they're fired for all vaults placed, or may be specific to a map. Global hooks must be defined in a Lua file, or in a .des file's global prelude.
You may use the post_place hook to take an action immediately after a vault is placed successfully. For instance, if you're designing a vault (star) that should always be accompanied by several other vaults (fan), then you might write it as:
NAME: star PLACE: D:2 {{ hook("post_place", function() -- Place ten instances of vault(s) tagged "fan" dgn.place_maps{tag="fan", count=10} end) }} MAP *** *** ENDMAP NAME: fan # Must be allow_dup since 10 instances will be used. TAGS: fan allow_dup MONS: human MAP 1 ENDMAP
Using global hooks affects every vault, so they should be used very sparingly. Global hooks must be defined in Lua files, or in .des files' global preludes (i.e. before any map definitions). As an example, here's a frivolous global hook to place a pile of gold on each floor square in vaults:
{{ dgn.global_hook("main", function () kitem(". = gold") end) }}
Note the use of the “main” hook in this example instead of “post_place”. “post_place” is too late to modify the vault definition, since the vault is already placed at that point.
Errors in hooks will usually be ignored by the level builder, although errors will still be displayed to the end user. The only exception is the “post_place” hook – errors in the post_place hook will cause the dungeon builder to veto its current level and retry level generation.