-
Notifications
You must be signed in to change notification settings - Fork 1
Level Definition Functions
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.
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:
-
CreateLocation()- names the location and says how its map is laid out -
SetPattern()- the map as ASCII, for a hand-drawn location -
AddTranslation()- what this location's own characters mean -
DrawPattern()- stamps the pattern onto the map -
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.
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))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().
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 inworld/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.
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 ...
"################################################################################")Says what one character of this pattern means.
Parameters:
-
symbol(string): the character -
handler: either anXTileType- the character is that ground - or afunction(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)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.
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.
- the shared alphabet, same shape as
-
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.
- 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.PATTERNis itsfill. 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
flooroption; failing that, the first entry ofSetFloorPriority. A treasure alcove cut into solid rock, where every neighbour is wall, is exactly the case the location'sflooranswers.
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.
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)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.
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.
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.
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 inworld/creature_classes.lua("rat","bat","kobold","undead", ...). Classes are content, not an engine enum -
level: aCreatureTemplatelevel -VERY_LOW,LOW,ABOVE_LOW,AVG,ABOVE_AVG,HI,ABOVE_HI,VERY_HI,EXTREM_HIorUNIQUE. 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, whenoptionswas 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 } })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.
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.
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.
| 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 |
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 withCreateObject(); 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;optionsarewall,flooranddoor|
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)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.
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.
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)- returnswidth, height -
GetTile(x, y, location)- theXTileTypethere -
HasSpecial(x, y, location)- whether something already stands there -
GetFreeXY(location)- a random walkable, unoccupied cell asx, y, ornilwhen the map has none
-
DefineTile(id_name, name, view, color, movability, visibility, properties)- adds a kind of ground and publishes it asXTileType.<id_name>.propertiesmay carrydiggable_intoandfertile. All of the world's tiles are defined inworld/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.
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 |
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 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-
world/init.lua'sLoadScripts()reads the world's files in order: tiles, palette, terrain, rooms, creatures, uniques, the valley, locations, quests. Nothing is built yet - this only defines. -
InitWorld()then calls eachMakeXxx()once, for a new game only. - 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.
- Monster Definition Functions - how creatures are defined
-
world/delve_patterns.lua- the digging tablesDelve()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.