Advanced Vault Design

This document describes some advanced features of vault making. For even more advanced scripting topics, see Lua Reference.

Conditionalising levels

Crawl translates level (.des) files into Lua code chunks and runs these chunks to produce the final level that is generated. While you don't need to use Lua for most levels, using Lua allows you to conditionalise or randomise levels with greater control.

Let's take a simple example of randomisation:

NAME: random_test
# Put it on D:1 so it's easy to test.
PLACE: D:1
ORIENT: float
MAP
xxxxxxxxxxxxxxxxxxx
x........{........x
xxxAxxxxxBxxxxxCxxx
xxx.xxxxx.xxxxx.xxx
xxx@xxxxx@xxxxx@xxx
ENDMAP

Now let's say you want A, B, and C to be randomly rock or floor, but B should be floor if both A and C are rock. Here's one way to do it (add these lines to the map definition):

: local asolid, csolid
: if crawl.random2(2) == 0 then
:   asolid = true
:   subst("A = x")
: else
:   subst("A = .")
: end
: if crawl.random2(2) == 0 then
:   csolid = true
:   subst("C = x")
: else
:   subst("C = .")
: end
: if asolid and csolid then
:   subst("B = .")
: else
:   subst("B = .x")
: end

This code uses crawl.random2(N) which returns a number from 0 to N-1 (in this case, returns 0 or 1). So we give A a 50% chance of being rock, and the same for C. If we made both A and C rock, we force B to be floor, otherwise we use a subst that gives B the same 50% chance of being rock.

You can conditionalise on various factors, such as player experience level:

NAME: condition_002
DEPTH: 1-27
ORIENT: float
: if you.xl() > 18 then
MONS: greater mummy
: else
MONS: deep elf priest / deep elf sorcerer / deep elf demonologist
: end
MAP
xxxxxx
x1...x
x1...+
x1...x
xxxxxx
ENDMAP

Or based on where the map is being generated:

NAME: condition_003
DEPTH: Elf:*, Orc:*
ORIENT: float
: if you.branch() == "Orc" then
MONS: orc priest, orc high priest
: else
MONS: deep elf priest, deep elf high priest
: end
MAP
xxxxxx
x1...x
x2...+
x1...x
xxxxxx
ENDMAP

When conditionalising maps, remember that your Lua code executes in two contexts:

  1. An initial compilation phase before the game starts.
  2. The actual mapgen phase when the dungeon builder is at work.

In context 1, you will not get useful answers from the Crawl Lua API in general, because the game hasn't started. This is generally ignorable (as in the case above) because the compilation phase just checks the syntax of your Lua code. If you conditionalise your map, however, you may run into compile failures. Take this variant, which (incorrectly) attempts to conditionalise the map:

NAME: condition_004
DEPTH: Elf:*, Orc:*
ORIENT: float
: if you.branch() == "Orc" then
MONS: orc priest, orc high priest
MAP
xxxxxx
x1...x
x2.I.+
x1...x
xxxxxx
ENDMAP
: elseif you.branch() == "Elf" then
MONS: deep elf priest, deep elf high priest
MAP
xxxxxx
x1...x
x2.U.+
x1...x
xxxxxx
ENDMAP
: end

This map will break the compile with the cryptic message “Must define map.” (to compound the confusion, the line number for this error will be the first line number of the map following the buggy map).

This error is because although the map is Elf or Orc only, at compile time, the branch is *neither* Elf nor Orc, so the level-compiler thinks you've neglected to define a map.

Lua code can detect the compile phase using crawl.game_started() which returns true only when the player has started a game (and will return false when the map is being initially compiled).

For more details on the available Lua API and syntax, see the Lua reference section.

Validating levels

If you have a map with lots of transforms (SUBST and SHUFFLE), and want to guarantee that the map is sane after the transforms, you can use a validation hook.

To take a very contrived example:

NAME: contrived_001
PLACE: D:2
ORIENT: float
TAGS: no_pool_fixup
SUBST: .=.w
SUBST: c=x.
MAP
xxxxxx
x{.+.c
x..+>x
xxxxxx
ENDMAP

This map has a chance of leaving the player stuck on the upstair without access to the rest of the level if the two floor squares near the doors are substituted with deep water (from the SUBST line), or the 'c' glyph is substituted with rock. Since a cut-off vault is uncool, you can force connectedness with the rest of the level:

validate {{ return has_exit_from_glyph('{') }}

The has_exit_from_glyph() function returns true if it is possible to leave the vault (without digging, etc.) from the position of the { glyph. (This takes things like the merfolk ability to swim into account, so a merfolk character may see deep water between the stair and door.)

The validate Lua returns false (or nil) to indicate that the map is invalid, which will force the dungeon builder to reapply transforms (SUBST and SHUFFLE) and validate the map again. If the map fails validation enough times, the dungeon builder will discard the entire level and retry (this may cause a different map to be selected, bypassing the buggy map).

Going back to the example, if you just want to ensure that the player can reach the > downstair, you can use:

validate {{ return glyphs_connected('{', '>') }}

NOTE: You cannot use the colon-prefixed syntax for validation Lua. If you have a big block of code, use the multiline syntax:

validate {{
    -- This level is always cool.
    crawl.mpr("This level is guaranteed perfect!")
    return true
}}

Abyss Vaults

Abyssal vaults have more limitations than vaults in the regular dungeon, because the Abyss is constantly shifting under the player.

The abyss picks vaults by tag, using vaults tagged “abyss” as general decoration, and “abyss_rune” for abyssal rune vaults.

When designing abyssal vaults, keep in mind that:

  • Abyssal vaults must be small: no larger than 28×23. 23×23 or smaller vaults are best so that they can be rotated and fit in anywhere on the level.
  • The player may never get a chance to explore unique abyss vaults (vaults without the “allow_dup” tag). The player may be teleported away to a different area of the abyss, or never notice your vault before it shifts away.
    Unique vaults that the player never sees will be reused by the abyss builder, but it is still possible for the player to see an outer wall of your vault and not notice it, or for the player to try to explore it and be teleported away by the abyss before they can finish exploring it.
    Once any square of a unique vault has been seen by the player in the abyss, that vault cannot be reused.
  • Portal vaults that lead out of the abyss cannot return to the abyss. If you place a bazaar portal or similar in the abyss, the player will go straight back to the dungeon when exiting the bazaar.
  • Timers and other listeners in an abyssal vault may be discarded at any time when the abyss shifts and the vault is destroyed. You can still use timers to modify your vaults, but be aware that the timer may be removed at any time as the abyss shifts away from your vault.

Portal Vaults

Portal vaults are vaults accessed by portals in the dungeon (labyrinths and bazaars are special cases of portal vaults). You can create custom portal vaults in the following steps (no compilation is necessary):

  • Create a new file name.des in the dat/ folder. Rules:
    The “name” should be descriptive of the vault you're adding.
    The “name” should not exceed eight letters.
    The ending must be “des”.
  • “name.des” should contain a comment at the top, explaining flavour and gameplay goals of the portal vault (and perhaps additional ideas etc.)
  • Define at least one vault containing the portal (see below).
  • Define at least one destination map (see below).
  • Add a short in-game description for the string “desc” (see below) to dat/descript/features.txt.

Before going into the details of portal vault creation, some words about their uses: Portal vaults are different from branches in that they are not guaranteed. Also, there is only one go at a portal vault — if you leave, it's gone for good. You can apply special rules to a portal vault, like enforcing maprot.

Portal vaults can be particulary thematic, using specialised monster sets, fitting loot, coloured dungeon features etc. Avoid death traps; it is no fun to enter a vault, being unable to leave and be killed outright. In order to provide fun and reduce spoiler effects, randomise. For portal vaults, it is desirable to have several different layouts (ideally each of the maps has some randomisation on its own). Often, it is a good idea to skew the map distribution: e.g. with four destination vaults, weights like 40,30,20,10 might be more interesting than 25,25,25,25.

In order to test a portal vault, you can either use PLACE: D:2 for an entry vault, or use the wizard mode command &L for conjuring up the entry.

Define a vault to hold the portal itself
# Bare-bones portal vault entry
NAME: portal_generic_entry
TAGS: allow_dup
ORIENT: float
MARKER: O = lua:one_way_stair { desc = "A portal to places unknown", \
                                dst = "generic_portal" }
KFEAT: O = enter_portal_vault
MAP
O
ENDMAP

Portal entries must contain a portal vault entry (enter_portal_vault). This feature must always have a marker that provides the portal with a description (“A portal to places unknown”) and a destination (“generic_portal”).

In case you want to make sure that the portal vault entry is only used once, you add a TAGS: uniq_BAR line. It should be noted that the label BAR may *not* end in _entry (otherwise the level builder assumes that the vault is a branch entry).

If you want the place name displayed while in the vault to be different than the destination name, then you can give one_way_stair() a “dstname” parameter. If you want the place origin for items in a character dump to be different than the default you can give one_way_stair a “dstorigin” parameter (i.e., dstname = “garden”, dstorigin = “in the gardens”). If you want the place name abbreviation used when displaying notes to be different than than the default you can use the “dstname_abbrev” parameter.

You can dynamically change the origin string using the lua function dgn.set_level_type_origin(), and dynamically change the place name abbreviation with dgn.set_set_level_name_abbrev().

Known portal vault entries will be displayed on the overmap. By default the name shown on the overmap will be the “dstname” parameter, or if that isn't present the “dst” paremeter. It can be set to something else with the “overmap” parameter. A note can be made to accompany the portal's position on the overmap with the “overmap_note” parameter.

Bones files for characters killed in the portal vault will normally use an extension derived from the first three letters of the 'dst' property. You can override this by setting the 'dstext' property to your preferred extension.

This will produce a portal, but attempting to use it will trigger an ASSERT since there's no map for the destination. So we create a destination map like so:

Define a destination map
NAME: portal_generic_generic
# Tag must match dst value of portal in entry.
TAGS: generic_portal allow_dup
ORIENT: encompass
MONS: ancient lich
KFEAT: > = exit_portal_vault
MAP
xxxxxxxxxxx
x111111111x
x1A111111>x
x111111111x
xxxxxxxxxxx
ENDMAP

Note that the entry point into the map will be a stone arch. You must provide an exit to the dungeon explicitly (KFEAT: > = exit_portal_vault) or the player will not be able to leave.

Stairs will not work right in portal vaults, do not use them.

You can use multiple maps with the destination tag (generic_portal), and the dungeon builder will pick one at random.

The MARKER parameters

The lines

MARKER: O = lua:one_way_stair { desc = "A portal to places unknown", \
                                dst = "generic_portal" }
KFEAT: O = enter_portal_vault

ensure that an 'O' glyph will be turned into a portal. Upon leaving the portal vault, you will be placed on its entry which has been turned into a floor. You can turn it into something different (usually an empty stone arch), by adding

floor = 'stone_arch'

to the lua:one_way_stair parameters.

Note that the desc string is what you will see upon examining the portal. The dst string is used for Crawl's right hand stat area; it will show

Place: generic portal

in the above example. Here is a lost of the parameters that can be used within one_way_stair (taken from icecave.des):

desc = "A frozen archway",    # description of the portal before entry
dst = "ice_cave",             # label used for maps and entry vaults
dstname = "Ice Cave",         # used for PLACE: on the main screen
dstname_abbrev = "IceCv",     # used in the notes
dstorigin = "in an ice cave", # appendix for items picked up there
overmap = "frozen archway",   # used on the overmap (X)
floor = "stone_arch"          # feature left after escaping the portal

The dst string is also used to link the destination maps to the entry maps. In case dstname is missing, dst will be used.

You can replace lua:one_way_stair by lua:timed_marker in order to make timed portal vaults (which will disappear after some time). bazaar.des and lab.des contain examples. For timed portals, you may want to add messages to the file dat/dlua/lm_tmsg.lua.

Using lua functions as shortcuts

If you are making several entry and destination vaults, you will note a lot of duplicated header statements. This can be lessened using lua. Define a lua block right at the top (after your comments) as follows:

{{
function generic_portal(e)
  e.marker([[O = lua:one_way_stair { desc = "A portal to places unknown",
                                     dst = "generic_portal",
                                     floor = "stone_arch" }]])
  e.kfeat("O = enter_portal_vault")
  e.colour("O = magenta")
end
}}

Instead of the MARKER and KFEAT lines introduced above you now just use

:generic_portal(_G)

and the resulting portal glyphs will even be magenta!

Defining a random monster set

Portal vaults require a defined random monster set to make the Shadow Creatures spell work. This is done by calling dgn.set_random_mon_list() manually. Here's an example from ice_cave_small_02 in icecave.des:

: dgn.set_random_mon_list("ice beast w:90 / ice dragon / nothing")

You can use “nothing” to have the spell fail sometimes.

If you are using the same random monster list in several destination maps, you can define a lua block and call it from the destination map definition. This example is from sewer.des:

{{
function sewer_random_monster_list(e)
  e.set_random_mon_list("bat w:20 / giant newt w:20 / small snake / \
                         ooze / worm / snake / vampire mosquito w:15")
end
}}

You can then use this line in the map definition to execute the lua block:

: sewer_random_monster_list(_G)

You can also set env.spawn_random_rate() to have monsters generated from the list during play.

Milestones for portal vaults

This example is from icecave.des, defined in the lua preludes:

{{
function ice_cave_milestone(e)
  crawl.mark_milestone("br.enter", "entered an Ice Cave.")
end
}}

The function is called from each of the destination map definitions:

epilogue{{
  ice_cave_milestone(_G)
}}

This marks down entering the portal vault in the server milestones and causes it to be announced by the IRC bots. Notice the use of an epilogue block. The epilogues are only run after the map is validated, that is, it ensures that the map was actually used.

Feature Names

The feature names usable in MARKER and KFEAT declarations are defined in terrain.cc.

General
floor
unseen
builder_special_wall
builder_special_floor
Walls
rock_wall
stone_wall
metal_wall
wax_wall
green_crystal_wall
permarock_wall Cannot be destroyed by any means
Liquids
lava
deep_water
shallow_water
water_stuck
Doors
open_door
closed_door
secret_door
enter_shop
Stairs
stone_stairs_down_i
stone_stairs_down_ii
stone_stairs_down_iii
stone_stairs_up_i
stone_stairs_up_ii
stone_stairs_up_iii
escape_hatch_down
escape_hatch_up
Statues
granite_statue
orcish_idol
silver_statue
orange_crystal_statue
statue_reserved_1
statue_reserved_2
Altars
altar_zin
altar_the_shining_one
altar_kikubaaqudgha
altar_yredelemnul
altar_xom
altar_vehumet
altar_okawaru
altar_makhleb
altar_sif_muna
altar_trog
altar_nemelex_xobeh
altar_elyvilon
altar_lugonu
altar_beogh
Fountains
fountain_blue
fountain_sparkling
fountain_blood
dry_fountain_blue
dry_fountain_sparkling
dry_fountain_blood
permadry_fountain
Traps
undiscovered_trap
trap_mechanical
trap_magical
trap_iii
Branches
stone_arch Inactive or expired portal
exit_hell
enter_hell
enter_labyrinth
enter_dis
enter_gehenna
enter_cocytus
enter_tartarus
enter_abyss
exit_abyss
enter_pandemonium
exit_pandemonium
transit_pandemonium
enter_orcish_mines
enter_hive
enter_lair
enter_slime_pits
enter_vaults
enter_crypt
enter_hall_of_blades
enter_zot
enter_temple
enter_snake_pit
enter_elven_halls
enter_tomb
enter_swamp
enter_shoals
enter_reserved_2
enter_reserved_3
enter_reserved_4
return_from_orcish_mines
return_from_hive
return_from_lair
return_from_slime_pits
return_from_vaults
return_from_crypt
return_from_hall_of_blades
return_from_zot
return_from_temple
return_from_snake_pit
return_from_elven_halls
return_from_tomb
return_from_swamp
return_from_shoals
return_reserved_2
return_reserved_3
return_reserved_4
enter_portal_vault
exit_portal_vault

Map Statistics

Full-debug Crawl builds (this does not include normal Crawl builds that have wizard-mode - you must build Crawl with “make debug”, not “make wizard”) can produce map generation statistics. To generate statistics, run crawl from the command-line as:

crawl -mapstat

This will generate 100 Crawl dungeons and report on the maps used in a file named “mapgen.log” in the working directory.

You can change the number of dungeons to generate:

crawl -mapstat 10

Will generate 10 dungeons. If you merely want statistics on the probabilities of the random map on each level, use:

crawl -mapstat 1

Map Generation

Full-debug Crawl builds (see above for more information) include a test for generating specific vaults and outputting a copy of the map to a text file for inspection (you can also define the macro DEBUG_TESTS on an ordinary debug build, like “make EXTERNAL_DEFINES=-DDEBUG_TESTS wizard”). This is most useful for portal and other encompass vaults which use randomisation heavily.

To use the test, you must edit source/test/vault_generation.lua. Fill in the following variables:

  • map_to_test: The exact name of the vault you want to generate.
  • checks: How many times to generate the vault. Default value is 10.
  • output_to: The basic filename to output the results of generation to. This will have ”.<iteration>” appended. For example, “volcano.map” will result in files named “volcano.map.1”, “volcano.map.2”, etc.
  • need_to_load_des: If the file is not included in one of the .des files that are listed in source/dat/dlua/loadmaps.lua, this should be set to true, and the following variable should be set.
  • des_file: The name of the file to load. The file should be located in the source/dat folder.

Once you have saved your changes, run crawl:

crawl -test vault_generation

Once all of the tests have been finished successfully you should find the relevant files in your working directory.

Logged in as: Anonymous (VIEWER)
dcss/help/maps/advanced.txt · Last modified: 2012-11-10 05:15 by neil
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki