Skip to content

Level Definition Functions

Joachim de Groot edited this page Sep 26, 2026 · 1 revision

This document describes how locations - levels, dungeons, caves, the valley - are defined. Every location is written in Lua under world/, and the engine supplies no content of its own: no tile, no creature, no map alphabet is compiled in. What is described here is the whole of the seam between the two.

Overview

The world is built from locations: distinct areas connected by stairways and teleports. world/valley.lua holds the valley and what stands in it, world/locations/ one file per dungeon, and world/init.lua calls each file's MakeXxx() function once, for a new game only - a restored game is read from the save instead.

A location definition usually runs in this order:

  1. CreateLocation() - names the location and says how its map is laid out
  2. SetPattern() - the map as ASCII, for a hand-drawn location
  3. AddTranslation() - what this location's own characters mean
  4. DrawPattern() - stamps the pattern onto the map
  5. Way(), Creature(), Settle(), Chest(), ... - what is in it

Steps 2 to 4 are skipped by a location the engine generates (a cave, a delved level, a dungeon, the open valley) and are the whole of a hand-drawn one.


Creating a Location

CreateLocation(loc_id, brief_name, full_name, generator, options)

Parameters:

  • loc_id (string): the location's id, free-form and unique - everything that refers to a location refers to it by this string ("SMALL_CAVE_2")
  • brief_name (string): short name for the status line ("SmCv:2")
  • full_name (string): full name ("Small Cave Level 2")
  • generator (XLocation): how the map is laid out, see below
  • options (table): what the generator builds with. The engine has no defaults for tiles: a generator whose options leave one out says so on stderr and builds with nothing.

Everything called after CreateLocation() applies to that location until the next CreateLocation().

Generators:

Generator Builds
XLocation.CAVE A natural cavern, dug out of the wall tile by overlapping blobs
XLocation.CHAMBERS Rough chambers joined by corridors, with optional water - caves that read as caves rather than as masonry
XLocation.DUNGEON Rooms joined by corridors, with doors and traps
XLocation.PLAIN Open country: ground, scattered cover, a ring of high ground
XLocation.DELVE Floor eaten out of solid rock a cell at a time, by a table of neighbourhood patterns
XLocation.PATTERN Nothing at all - the script's own pattern is the level

Options every generator understands:

  • width, height (number): the size of the map (default 80x20)
  • sight (number): how far can be seen here beyond what the creature's own eyes and light make out - 0 underground, 30 in open country
  • floor (XTileType): the ground a pattern may invent under something it places when no neighbour says what the ground is (see Invented floor)

XLocation.CAVE: wall, floor (tiles, both required), blobs (how many are dug, default 150), blob_radius (default 3).

XLocation.DUNGEON: wall, floor (required), cells_per_room (default 200), door_odds (a door at one junction in this many, default 3), room_chance (how often a room from world/rooms.lua is used rather than an invented one), room_width, room_height, room_exits (each a {min, max} pair), trap_odds, max_traps.

XLocation.PLAIN: ground, cover (required tiles), slope (a list of tiles from lowest to highest, used for the ring of hills), cover_odds (one cell in this many is covered, default 3), border_depth (default 4), erosion (default 2).

XLocation.PATTERN: fill (the tile the blank map starts as, before the pattern is drawn - something solid for a level that stands on its own), below and origin for a floor above another level (see below).

The option tables themselves live in world/terrain.lua, so that a level says what kind of place it is rather than repeating numbers: CAVE, Dungeon(room_chance), PLAIN, Drawn(width, height), Above(below, x, y, width, height) and SHOP.

Examples:

CreateLocation("SMALL_CAVE_1", "SmCv:1", "Small Cave Level 1", XLocation.CAVE, CAVE)
CreateLocation("DWARFCITY", "DvCty", "Dwarven City", XLocation.PATTERN, Drawn())
CreateLocation("MUSHROOMS_CAVE2", "MshCv:2", "Mushroom Cave", XLocation.DUNGEON, Dungeon(30))

Floors above another level

A location built with below is not a level of its own but a floor over another one. It shares that level's coordinate space and holds cells only for the corner of it named by origin, width and height. Everywhere its own pattern draws nothing is a hole: what shows through is the level below - its ground, its items, and the creatures walking on it, which is what makes a tower window worth looking out of.

Above() in world/terrain.lua builds the option table:

CreateLocation("WIZTOWER_TOP", "WzTwr", "Yohjishiro's Tower",
               XLocation.PATTERN, Above("MAIN", 45, 25, 21, 11))

Because the coordinate space is shared, the pattern is drawn at the same place the building stands on the level below - DrawPattern(45, 25) here, not DrawPattern(0, 0). A stairway between the two floors is an ordinary Way().


Delved Levels

Delve(pattern, cells)

Builds the options table a XLocation.DELVE location wants. Floor is eaten out of solid rock one cell at a time, and what may be eaten next is decided by a table of neighbourhood patterns - that table is the whole character of the level.

  • pattern (string): one of the tables in world/delve_patterns.lua. Add one there and it is usable here by name. Left out, a table is invented on the spot and is different every game
  • cells (number): the most floor it may dig, and so how open the level is. Left out, it takes a fifth of the map
local workings = Delve("rmaze2")
workings.width = 80
workings.height = 50

CreateLocation("FORSWORN1", "Fsw1", "Dungeon Forsworn Level 1",
               XLocation.DELVE, workings)

The returned table is ordinary and may be adjusted before use, as the width and height are above. A pattern that carries pull or store brings them with it, because a few of them only come out right one way round.

The patterns fall into families by the shape they dig: old* are caverns of galleries and halls, rmaze* dense rectangular mazes, dmaze* wandering ones. A dungeon that rolls one pattern per game and uses it for every level looks like itself all the way down; world/locations/mines.lua does exactly that for the three abandoned mines.


Patterns

SetPattern(width, height, pattern_string)

Defines the map as text, one character per cell. The string is exactly width * height characters; rows are written one per line and joined with .., which is a Lua string concatenation and not a newline - a pattern has no line breaks in it.

A pattern whose text does not match the size it declares is reported on stderr, because a single dropped character would otherwise draw everything after it shifted by one.

SetPattern(80, 20,
    "################################################################################" ..
    "######################,,,,,,,###################################################" ..
    "###################,,,,,,<,,,,,,###########################S,,,,,,,,############" ..
    -- ... 17 more rows ...
    "################################################################################")

AddTranslation(symbol, handler)

Says what one character of this pattern means.

Parameters:

  • symbol (string): the character
  • handler: either an XTileType - the character is that ground - or a function(x, y), called once per occurrence to put something there
AddTranslation("2", XTileType.GOLDEN_FLOOR)
AddTranslation("~", function(x, y) Chest(x, y) end)
AddTranslation("<", function(x, y) Way(XStairWay.UP, "MAIN", x, y) end)
AddTranslation("A", function(x, y) for i = 1, 8 do Creature('rat', x, y, 12, 4) end end)

DrawPattern(x, y)

Stamps the pattern onto the map with its top-left corner at (x, y). Tiles are laid first, and only then are the callbacks run, so a callback can look at the ground its neighbours were given.

A pattern that reaches past the part of the level the location holds is reported on stderr and those cells are left undrawn; nothing is written outside the map.

The map alphabet

world/palette.lua sets what a character means in any pattern that does not translate it itself:

  • SetDefaultTranslations{ ['#'] = XTileType.STONE_WALL, ['+'] = Door }
    • the shared alphabet, same shape as AddTranslation()'s pairs.
  • SetFloorPriority{ XTileType.GREEN_GRASS, XTileType.CAVE_FLOOR } - which tiles count as floor when one has to be invented, later entries winning over earlier ones.

Nothing is built in: a character no palette accounts for is not an error, it simply gets a floor fitting its surroundings.

Blanks and invented floor

  • A blank is a hole in the pattern: whatever is already on the map there stays: on a floor above another level, that is where the level below shows through; on an ordinary level, it is whatever the generator put there, which for XLocation.PATTERN is its fill. Blanks draw nothing, so a pattern may have a blank border hanging over the edge of its level without complaint.
  • A cell that runs a callback - a door, a chest, a shopkeeper - needs ground under whatever is placed on it. The engine copies the most preferred floor any of the eight neighbours stands on; failing that, the location's own floor option; failing that, the first entry of SetFloorPriority. A treasure alcove cut into solid rock, where every neighbour is wall, is exactly the case the location's floor answers.

DefineRoom(weight, width, height, pattern, translations, on_drawn)

Adds a room the dungeon generator may stamp into a level, instead of inventing a rectangle. weight is relative to the other rooms defined; translations is a table of glyph to tile or function(x, y), read like AddTranslation()'s pairs; on_drawn, if given, is called with (x, y, w, h) once the room is on the map. Rooms live in world/rooms.lua, and a level uses them by passing room_chance.


Connecting Locations

Way(direction, target_loc_id, x, y)

A stairway. direction is XStairWay.UP or XStairWay.DOWN; target_loc_id is the id of the location it leads to. Given x, y it stands there, which is what a pattern's translation does; without them it is placed on a free cell.

Stairways are paired after the whole world is built: each end finds the one in the target location that leads back. Both ends must exist - a way with no partner comes out nowhere, and is reported by name and position on stderr.

Way(XStairWay.UP, "MAIN")
AddTranslation("<", function(x, y) Way(XStairWay.UP, "SMALL_CAVE_1", x, y) end)

Teleport(x, y, target_loc_id, dest_x, dest_y)

A pad at (x, y) that puts whatever steps on it at (dest_x, dest_y) in the target location. Unlike a stairway it is one-way; a pair of calls makes it two-way.

SetStartLocation(loc_id, x, y, w, h)

Names the location a new hero starts in, optionally narrowed to an area of it. A world without this has nowhere to start a game, which is reported.

SetWanderingAllowed(loc_id, allowed)

Says whether AI creatures may wander into a location on their own. The hero's own stairways are unaffected, and creatures already there stay. Use it for a level that is a scene rather than a thoroughfare - a hostage's room fills up with whatever walks down from the valley otherwise.


Populating a Location

Settle(creature_classes, level, options, refresh)

Keeps the location populated. Every so often one creature is added, drawn from whichever of the named classes has fewer than the ceiling of its own here already, on a free cell; settled creatures are allowed to take stairways, so they spread into whatever a level leads to that does not refuse them (see SetWanderingAllowed()).

  • creature_classes: one class id, or a list of them - the ids declared in world/creature_classes.lua ("rat", "bat", "kobold", "undead", ...). Classes are content, not an engine enum
  • level: a CreatureTemplate level - VERY_LOW, LOW, ABOVE_LOW, AVG, ABOVE_AVG, HI, ABOVE_HI, VERY_HI, EXTREM_HI or UNIQUE. It is a ceiling, not a set: anything of that level or below may be drawn
  • options: the ceiling as a plain number, or a table (below)
  • refresh: ticks between attempts, when options was a number

The ceiling is per class, not per level: a call naming eight classes settles up to eight times as many creatures as one naming a single class.

The table form says four more things:

Key Means
max the ceiling per class (default 5)
refresh ticks between one attempt and the next (default 25000)
area {x =, y =, w =, h =}, to confine it to part of the location
on the ground it may be settled on, one tile or several
stays_on the ground it keeps to afterwards
Settle({"rat", "insect"}, CreatureTemplate.LOW)
Settle("rat", CreatureTemplate.LOW, 4, 50000)

Settle("canine", CreatureTemplate.AVG, {
    max = 3, refresh = 30000,
    area = {x = 20, y = 28, w = 40, h = 16},
    on = XTileType.GREEN_GRASS,
    stays_on = { XTileType.GREEN_GRASS, XTileType.PATH } })

Creature(name, x, y, w, h, options)

Puts one creature of a template defined in world/creatures/ on the map, and returns it - or nil if there was nowhere to put it. With no coordinates it goes on any free cell; with x, y on that cell; with w, h as well, anywhere in that rectangle.

options is the same table Guardian() takes, less the parts about guarding: it posts no guard area and joins no group, which is the whole difference between the two.

Guardian(name, group_id, x, y, w, h, options)

A creature that stays where it is put: it is given GUARD_AREA over the rectangle, joins the group group_id, and - unless its template is PEACEFUL - treats everything but humans and humanoids as an enemy. Returns the creature.

GuardianClass(creature_class, group_id, x, y, w, h, options)

The same, but the creature is drawn at random from a class rather than named - a war party of orcs rather than one particular orc.

The last argument of all three is either a number, which is extra XStandardAI flags, or a table:

Key Means
flags extra XStandardAI flags
on the ground it may be put on, one tile or several
stays_on the ground it keeps to afterwards
Guardian('sheep', "yohji_flock", x, y, 19, 9,
    { on = XTileType.GREEN_GRASS,
      stays_on = { XTileType.GREEN_GRASS, XTileType.PATH } })

on decides only where it is placed; stays_on is what holds it there afterwards. The two are deliberately separate, because the orc war party musters on the trampled earth of its camp and then marches off it.

Which leash to reach for. GUARD_AREA lets a creature leave its area to pursue an enemy and only walks it back afterwards. stays_on is absolute: it is checked after every branch of the AI has chosen its step, attacks included, so a creature can never step off the ground it keeps to. Use stays_on for "must not leave this building", GUARD_AREA for "has a post to return to". Note that a door's floor is invented from its neighbours, so check whether the ground you name reaches the doorway.

Creatures of one group come to each other's aid: attacking one makes the whole group hostile. It does not release them from their guarded areas - they answer from where they stand.


Placing Things

Call Puts
Door(x, y, opened) A door, closed unless opened is true
Trap(x, y) A trap, its kind and level chosen at random
Chest(x, y, count, item_kind, min_value, max_value) A chest, by default 5 items worth 100 to 25000
Treasure(x, y, value) Gold, roughly value and randomised
Furniture(x, y, color, view_char, description) Something that stands there and can be looked at
PlaceSpecial(class_name, x, y) A map object by engine class name ("XAltar"), returned so it can be adjusted
SetAltarDeity(altar, deity_id) Whose altar it is - an offering made on it goes to that god whatever the offerer would otherwise have chosen
SetObjectView(object, view_char, color) How a placed object is drawn
DestroyMapObject(object) Removes one

Roads

Three ways of drawing a track between two points, each with its own wander. They take the two ends, the tile to lay, and return the cells they used.

  • WindingRoad(x1, y1, x2, y2, tile, ...)
  • ZigzagRoad(x1, y1, x2, y2, tile, ...)
  • SigsagRoad(x1, y1, x2, y2, tile, ...) | OuterObject(x, y, color, view_char, description, event) | The same, with a Lua event handler bound to it | | DropItem(item, x, y) | An item made with CreateObject(); without coordinates, on a free cell | | PlaceSpecial(class_name, x, y, location) | Any registered map object by class name, returned for further use | | BuildShop(x, y, w, h, item_kind, keeper_name, options) | A shop with its keeper; options are wall, floor and door |

CreateObject(name) makes an item by class name ('XCookingSet'), by kind and value range, or by potion. Grave(x, y, inscription, event) in world/valley_extras.lua shows how a script builds its own placement helper out of these.

AddTranslation("$", function(x, y) Treasure(x, y, 20) end)
AddTranslation("S", function(x, y)
    BuildShop(x, y, 8, 2, ItemKind.FOOD, 'Nobel, the human shopkeeper', SHOP)
end)

Events

EventPlace(event) / EventPlace(x, y, w, h, event)

Binds a Lua event handler to the whole location, or to a rectangle of it: the handler is called when a creature moves into, out of, or within the area.

CreateTimerEvent(event, ttm)

Calls event every ttm ticks for as long as the location exists - the orc war party that gathers in the valley is one of these.


Map Queries

These take an optional location handle - the same one an event handler is passed - and otherwise mean the location currently being built. They exist so that scatter and decoration rules can be written in Lua: ScatterHerbBushes() in world/valley.lua is built out of exactly these.

  • GetMapSize(location) - returns width, height
  • GetTile(x, y, location) - the XTileType there
  • HasSpecial(x, y, location) - whether something already stands there
  • GetFreeXY(location) - a random walkable, unoccupied cell as x, y, or nil when the map has none

Tiles

  • DefineTile(id_name, name, view, color, movability, visibility, properties) - adds a kind of ground and publishes it as XTileType.<id_name>. properties may carry diggable_into and fertile. All of the world's tiles are defined in world/tiles.lua.
  • SetRememberedBrightness(percent) - how brightly ground the hero remembers but cannot see is drawn, against 100 for what is in sight.
  • SetTile(x, y, tile, location) - writes one cell. Answers false for a cell outside the map rather than asserting, so a script may ask about anywhere.
  • SetTileJitter(percent, hue_degrees, ...) - how much the drawn colour of a tile may vary from cell to cell, so that a field of one tile does not read as a flat block.
  • TileDiggableInto(tile) - what that tile becomes when dug out, or nothing if it cannot be dug.
  • TileFertile(tile) - whether a plant may grow there.

What the engine checks

The world is checked as it is built and once more when it is finished. Nothing here stops the game: each complaint names the location and is written to stderr, so running the game with stderr captured is the way to see them.

Message Means
... defines a WxH pattern, which wants N characters, and gives M SetPattern()'s text is not the size it declares
... draws a WxH pattern at x,y, which reaches past the ... The pattern does not fit the level; those cells were left undrawn
... is built without a 'fill' tile An XLocation.PATTERN level that stands on its own has nothing to be cut out of
... is built without a 'wall' tile A generator's options leave out a tile it cannot do without
... is built over 'X', which has to be built before it A floor above a level created later in the script
... sits at x,y on X, which reaches past its edge A floor above reaches outside the level it stands on
... leads to 'X', which is not a location A stairway or teleport naming a location that does not exist - usually a typo
the stairway in X at x,y leads to 'Y', but nothing there leads back Only one end of the pair was defined
no start location / the hero starts in 'X', which is not a location SetStartLocation() missing or wrong

Complete Example: the Small Cave

world/locations/small_cave.lua is a generated level and a hand-drawn one side by side:

function MakeSmallCave()
    -- Level 1: dug by the engine, and kept populated with vermin.
    local caverns = Chambers(60, 40)

    CreateLocation("SMALL_CAVE_1", "SmCv:1", "Small Cave Level 1", XLocation.CHAMBERS, caverns)
        Way(XStairWay.UP, "MAIN")
        Way(XStairWay.DOWN, "SMALL_CAVE_2")
        Settle({"bat", "insect"}, CreatureTemplate.ABOVE_LOW)

    -- Level 2: drawn by hand, down to the last wall.
    CreateLocation("SMALL_CAVE_2", "SmCv:2", "Small Cave Level 2", XLocation.PATTERN, Drawn())
        SetPattern(80, 20,
        "################################################################################" ..
        -- ... 18 more rows ...
        "################################################################################")

        AddTranslation("A", function(x, y) Furniture(x, y, xColor.xBROWN, '~', 'a table') end)
        AddTranslation("$", function(x, y) Treasure(x, y, 20) end)
        AddTranslation("~", function(x, y) Chest(x, y) end)
        AddTranslation("<", function(x, y) Way(XStairWay.UP, "SMALL_CAVE_1", x, y) end)
        AddTranslation("S", function(x, y) SmallCaveQuestPersons(x, y) end)
        DrawPattern(0, 0)

        -- A scene, not a thoroughfare: nothing wanders in on its own.
        SetWanderingAllowed("SMALL_CAVE_2", false)
end

Location File Structure

-------------------------------------------------------------
------------------- LOCATION NAME ---------------------------

function MakeLocationName()
    CreateLocation(...)
        SetPattern(...)         -- hand-drawn levels only
        AddTranslation(...)
        DrawPattern(...)

        Creature(...)           -- what stands here from the start
        Settle(...)             -- and what keeps arriving
        Way(...)                -- where it leads

    CreateLocation(...)         -- as many as the file holds
        ...
end

Loading Order

  1. world/init.lua's LoadScripts() reads the world's files in order: tiles, palette, terrain, rooms, creatures, uniques, the valley, locations, quests. Nothing is built yet - this only defines.
  2. InitWorld() then calls each MakeXxx() once, for a new game only.
  3. The finished world is checked, its stairways paired, and its floors linked to the levels they stand on.

A dungeon reached from the valley is built in two halves, and the order matters. A pattern can only be drawn into the location currently being built, so the ruin, hut or stair-head that holds the entrance has to be drawn while MakeAvanorValley() is running - that is, before the dungeon itself exists. The dungeon's own MakeXxx() comes afterwards and its stairway binds then, in step 3, which is why Way() may name a location that has not been created yet.

Anything the entrance needs to know must therefore be settled before the valley is drawn. world/locations/mines.lua shows the shape: an AssignMines() call at the head of InitWorld() decides which of three ruins stands on which site, MakeAvanorValley() draws them, and MakeAbandonedMines() digs the shafts afterwards.


See Also

  • Monster Definition Functions - how creatures are defined
  • world/delve_patterns.lua - the digging tables Delve() names, with a note on each describing the shape it cuts
  • world/palette.lua - the default map alphabet and the floor-priority list a door or chest invents its floor from

The AI-flag and statistics documents that used to be listed here have moved to the wiki.

Clone this wiki locally