Every file in scripts/dungeons describes one instanced dungeon: a private copy of a map that a party opens with a key item, fights through, and gets paid for. The files are Lua, written as lists and tables.
What a Dungeon Is Made Of
- The dungeon is the whole run: which map, who may enter, which key opens it, and what it pays.
- A mission is one stage of the run, roughly one room or one wave: how long the party has, what to put on the map, and what ends the stage. Missions run in the order they are listed.
- A goal is one thing the party has to do inside a mission: kill twenty of something, kill one boss, collect seven of an item. A mission can have several.
- Reactions happen while a mission runs: reinforcements when somebody walks into a room, a kill that buys more time, a gate that opens when a boss falls.
A Worked Example
This is hora_infernal_50.lua, cut down: five bosses in a row, seven minutes each and ten for the last.
dungeon "hora_infernal_50" {
title = "Hora Infernal",
keys = { "ixkey27" }, -- the key item that opens it
map = "Dungeon00",
spawn = "dpstart", -- where the party lands
party_only = true,
max_party = 8,
level = { min = 50, max = 55 },
blocks = {
-- a pool of markers; block("BOSS") picks one of these at random
BOSS = { "dm00m50_0", "dm00m54_0", "dm00m56_0", "dm00m53_0" },
PitBOSS = { "dm00m51_0", "dm00m52_0", "dm00m55_0", "dm00m57_0" },
},
reward = {
exp = 218039766451,
dalant = 150000,
items = {
{ to = "one", code = "iyust03", count = 1, chance = 0.3 },
{ to = "other", code = "iyust03", count = 1, chance = 0.3 },
},
},
missions = {
{
name = "m01_hora_infernal_lava",
description = "a50", -- the message the client shows
time_limit = 420, -- seconds
result = "next", -- when it is done, run the next mission
spawn = {
{ monster = "05BA3", count = 1, at = block("BOSS") },
},
add_time = {
{ monster = "05BA3", seconds = 0 },
},
goals = {
hunt("05BA3", 1), -- kill one of it
},
},
{
name = "m05_hora_infernal_draco",
description = "a51",
complete_msg = "a52", -- shown when the whole run is won
time_limit = 600,
result = "complete", -- last stage: the run is over
spawn = {
{ monster = "0BBA3", count = 1, at = block("PitBOSS") },
},
goals = {
hunt("0BBA3", 1),
},
},
},
}
Adding a Dungeon
- Copy the closest file.
hora_infernal_50.luais the simplest complete one, anddark_hole_01_4549.luauses every feature. - Rename the file and change the name in the
dungeon "..."line to match. The name is unique in the folder, lower case, with words joined by underscores. - Point
mapat a map andspawnat a marker on it. Reuse the marker names of a dungeon on the same map, in lower case. The load refuses a marker that is not on that map. - Add the key to
_index.luaif a key item should open it. Without a key, nothing in a player's bag opens it. - Fill in the missions.
Codes and marker names belong to your version's content and change between RF versions, so check any value you copy from another version.
Checking and Reloading
Everything is checked when the server loads it. An unknown monster or item code, a marker that is not on the map, a mission nothing can reach, a next that names no mission, a gate that leads nowhere or an empty goal list each name the file in the log and skip that dungeon. The others keep working.
There is no build step. Change a file, then restart the server. On Dragonborn and Golden Age you can instead send reload-content from GoMMO Manager, and a load that fails keeps the dungeons already loaded.
Field Reference
Dungeon
| Field | Type | Meaning |
|---|---|---|
title | text | Display name for logs and the admin panel. Players do not see it. |
keys | list of text | Key item codes that open this dungeon. |
map | text | Map code, for example Dungeon00. |
spawn | text | Marker where entrants land. |
party_only | true or false | Whether a party is required to open it. |
max_party | number | How many members may be inside. |
level | { min =, max = } | Level band allowed in. Omit for no limit. |
description | text | A client message code shown when the run starts. Usually absent. |
blocks | table of lists | Named pools of markers. |
reward | table | What the run pays out. |
missions | list | The stages, in run order. |
Blocks
A block is a named pool of markers. at = block("BOSS") puts the monster at one of the pool's markers at random, and at = "dm00m50_0" puts it at exactly that marker.
Reward
| Field | Type | Meaning |
|---|---|---|
exp | number | Experience for the whole run. |
dalant | number | Money. |
pvp | number | Contribution points. |
items | list | Weighted item rolls. |
exp, dalant and pvp are usually one number that everyone gets. When the finisher and the rest differ, write exp = { one = 10, other = 4 }: one is the player who finished the run, and other is everyone else.
Each row of items is { to = "one", code = "iyust03", count = 1, chance = 0.3 }. The engine picks one row, weighted by chance, and grants that item, so a chance of 1 is not a guarantee when the list holds other rows.
Mission
| Field | Type | Meaning |
|---|---|---|
name | text | Unique within the file. Other missions refer to it by this name. |
description | text | Client message code shown when the stage starts. |
complete_msg | text | Client message code shown when the stage is won. |
when_morphed | table | Replaces description and complete_msg when a morphs roll fired. |
time_limit | number | Seconds. 0 means untimed. |
accept_at | text | The room this stage belongs to, in the dungeons built around gates. |
start | text | Marker where the party is placed for this stage, when it differs from spawn. |
result | text | What finishing does. |
next | text | The mission to run next, written only when it is not the next one listed. |
goal_mode | text | "any" means one goal is enough. Absent means all are needed. |
gates | list of text | Gates this stage opens when it is done. |
spawn | list | What to put on the map when the stage starts. |
repop | list | A population that keeps refilling while the stage runs. |
add_time | list | Seconds a kill of a given code adds to the stage's clock. |
triggers | list | What happens when a player walks into a marker. |
responds | list | What happens when a particular monster is killed. |
morphs | list | A spawned monster may turn out to be a different one. |
drops | list | An extra item drop bound to one monster code. |
goals | list | What the party has to do. Required. |
result is one of three words:
"complete"ends the whole run as a win. The last stage of a linear dungeon has this."next"moves on to the following mission."portal"opens this stage'sgatesand waits for the party. The Dark Hole dungeons are built this way: rooms behind gates rather than a queue of waves.
Gates
A gate is one of the map's own portals. Each gate leads to a spot on the same map, and the stage that runs next is the first one listed whose accept_at holds that spot. So you choose where a gate goes by where its room is, and next does not come into it.
When a "portal" stage ends, nothing else counts until the party moves on. The leader takes the gate, and every member has to be standing at it. The stage's clock keeps running while they walk, so time left over is time to reach the gate. When a gate is taken, everyone arrives in the next room together.
The stage's monsters leave a few seconds after it ends.
A stage does not have to open everything. Leave gates out, give each goal its own opens, and the party's choice of what to kill decides which way they go. This is how the last room of a Dark Hole picks its final boss.
A file that opens no gate at all, names a marker that is not a portal, or has a gate leading to no stage's room is refused when the server loads it.
Spawning
spawn = {
{ monster = "05BA3", count = 1, at = block("BOSS") },
{ monster = "001A0", count = 40, at = "dm00m50_0" },
},
monster is a monster code, count is how many, and at is a marker name or a block(...).
repop rows carry two more numbers, period and limit. Their units are not established, so leave those values as they are.
add_time rows give the party more time. Each kill of that code adds that many seconds to the stage's clock, and the party sees the clock change:
add_time = {
{ monster = "06A03", seconds = 2 },
},
A negative number takes time away, and zero changes nothing. Files that still say respawn load with a warning and act the same as add_time.
Triggers and Responds
A trigger fires the first time a player walks into a named marker:
triggers = {
{ at = "dq_event01",
spawn = {
{ monster = "06203", count = 5, at = "dmm01_0" },
},
},
},
A respond fires when a named monster is killed. at = HERE puts the reinforcements where that kill happened:
responds = {
{ on_kill = "0A50B",
spawn = {
{ monster = "06403", count = 2, at = HERE },
},
},
},
Goals
goals = {
hunt("05BA3", 1), -- kill 1 of monster 05BA3
hunt(ANY, 220), -- kill 220 of anything
hunt("006A3", ALL), -- kill every one of them on the map
take("evddd01", 7), -- pick up 7 event objects, dropped through `drops`
loot("iyust03", 3), -- collect 3 of an item
},
ANY and ALL are words the scripting layer provides, so do not put them in quotes.
A goal can carry an options table as a third argument:
hunt("0A70B", 1, { opens = { "dp_gate04" } }),
hunt(ANY, ALL, { description = "a8" }),
opens is a list of gates that unlock the moment that one goal is met. description is a client message code for that goal on its own.
The Key Table
_index.lua records which key item opens which dungeon, and lists every key on your server. Each dungeon file repeats its own keys in keys, and that field is the authority: if the two disagree, the dungeon file wins and the index is stale.
return {
keys = {
["ixkey25"] = "dark_hole_01_4549",
["ixkey27"] = "hora_infernal_50",
},
unkeyed = { "_template" },
}
Files Beginning With an Underscore
_index.lua is the key table and declares no dungeon. _template.lua is a complete example to copy; no key opens it and its rewards are placeholders. The loader skips both, so start the name of anything in this folder that is not a real dungeon with _.