Lua scripting
ConditionsString and UserScript on every node, and UserScript on the
project, are Lua. This page defines the environment they run in. The
editor’s simulator runs real Lua 5.4 (via WebAssembly); the standalone HTML
player runs a documented subset. A host that provides the tables below runs
every script the editor can.
The environment
Section titled “The environment”Before the project script runs, the host builds these globals:
| Global | Contents |
|---|---|
Variable | One entry per UserVariables asset, keyed by the cleaned name, seeded from Initial Value (coerced to boolean / number / string). |
Actor, Item, Location | One entry per asset, keyed by cleaned Name. Each is a table of the asset’s fields keyed by cleaned field title, plus Status (a free-text tag every asset has, "" by default — Actor["Ferryman"].Status = "paid") and TableName. Actor / Conversant fields hold the actor’s name, not its ID. |
Conversation | Conversation[id] is a table of the conversation’s fields plus Dialog, where Conversation[id].Dialog[nodeId] is a table of that node’s fields plus SimStatus. |
Dialog | Shorthand: Dialog[nodeId] is Conversation[<the evaluating node's conversation>].Dialog[nodeId]. |
Every table is pre-created, so a reference to an existing asset or node is
never nil.
SimStatus
Section titled “SimStatus”Conversation[c].Dialog[d].SimStatus is "Untouched", "WasOffered" or
"WasDisplayed" and is both readable and assignable:
-- condition: only if the player never saw the warningDialog[12].SimStatus == "Untouched"
-- script: pretend the greeting already happenedConversation[2].Dialog[3].SimStatus = "WasDisplayed"See runtime semantics for when each status is set.
Relationship and status helpers
Section titled “Relationship and status helpers”The Desktop simulator registers these; a conforming host should too. They take asset tables as arguments:
IncRelationship(Actor["Traveller"], Actor["Ferryman"], "trust") -- +1IncRelationship(Actor["Traveller"], Actor["Ferryman"], "trust", 2) -- +2DecRelationship(Actor["Traveller"], Actor["Ferryman"], "trust")SetRelationship(Actor["Traveller"], Actor["Ferryman"], "trust", 5)GetRelationship(Actor["Traveller"], Actor["Ferryman"], "trust") >= 3 -- 0 when unset
SetStatus(Actor["Traveller"], Item["Brass_Token"], "held")GetStatus(Actor["Traveller"], Item["Brass_Token"]) == "held" -- "" when unsetRelationships are directional: (a, b) and (b, a) are separate.
TrackVariable, TrackRelationship, TrackStatus and their Un… /
Update… partners exist as no-ops for compatibility with Desktop scripts
that call them.
Name cleaning
Section titled “Name cleaning”Asset names and field titles become Lua table keys after cleaning:
- remove
"[].\/ - replace
-, spaces and newlines with_
So the field Menu Text is Menu_Text, the actor Harbour Master is
Actor["Harbour_Master"], and a variable created as My Score is
Variable["My_Score"]. The editor shows the cleaned name where it matters.
Coercion
Section titled “Coercion”Field values are strings in the document. In Lua they become:
true/falsefor"true"/"false"(any case),- a number for anything
tonumberaccepts, - otherwise a string.
Assignments back to Variable[...] keep the Lua type for the rest of the
playthrough.
Conditions fail open
Section titled “Conditions fail open”A ConditionsString that raises an error — a typo, an undefined function —
must be treated as true, and the error logged. Failing open keeps content
reachable; failing closed hides it silently, which is far harder to debug.
Both the simulator and the HTML player behave this way.
Line breaks
Section titled “Line breaks”A condition may be written over several lines for readability; it is
evaluated as one expression. A script is several statements separated by
newlines (or ;).
Evaluation timing
Section titled “Evaluation timing”- The project
UserScriptruns once, after the tables are built. - A node’s
ConditionsStringis evaluated when the node is a candidate (see runtime semantics), against the state at that moment — before any sibling’s script. - A node’s
UserScriptruns when the node is entered, after its status flips toWasDisplayed(unlessDelaySimStatus), and after any enclosing group’s script.
The supported subset (HTML player, Yarn export)
Section titled “The supported subset (HTML player, Yarn export)”The standalone HTML player deliberately does not ship a Lua interpreter (it would break single-file portability), and the Yarn exporter can only translate what Yarn can express. Both support exactly:
| Construct | Example |
|---|---|
| Variable read / write | Variable["trust"], Variable["trust"] = 3 |
| SimStatus read / write | Dialog[4].SimStatus, Conversation[2].Dialog[4].SimStatus = "WasDisplayed" |
| Comparison | == ~= < <= > >= |
| Boolean | and or not, parentheses |
| Literals | numbers, "strings", true, false, nil |
| Arithmetic in assignments | Variable["trust"] = Variable["trust"] + 1 |
| Several statements | separated by ; or newlines |
Anything outside this — function calls, Actor[...] field reads, the
relationship helpers, string operations — is reported at export time in
the export’s warnings (and, for Yarn, left as an // UNTRANSLATED: comment
beside the line) rather than failing silently at play time. If a project must
run in the HTML player, keep its scripts inside the subset; the export tells
you which nodes do not.
Writing scripts a writer can maintain
Section titled “Writing scripts a writer can maintain”The editor exposes a Lua console in the simulator, so anything in this page can be tried against a live playthrough. In practice almost all game dialogue needs only:
-- conditionsVariable["trust"] >= 2Variable["hasToken"] and not Variable["angry"]Dialog[7].SimStatus ~= "Untouched"
-- scriptsVariable["trust"] = Variable["trust"] + 1Variable["lastTopic"] = "storm"Keep engine-specific calls (StartQuest(...), PlayCutscene(...)) in a
custom field the engine reads (Dialogue System’s Sequence, for
instance) rather than in the Lua, so the project still simulates in the
editor and exports to other targets.