Skip to content

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.

Before the project script runs, the host builds these globals:

GlobalContents
VariableOne entry per UserVariables asset, keyed by the cleaned name, seeded from Initial Value (coerced to boolean / number / string).
Actor, Item, LocationOne 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.
ConversationConversation[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.
DialogShorthand: 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.

Conversation[c].Dialog[d].SimStatus is "Untouched", "WasOffered" or "WasDisplayed" and is both readable and assignable:

-- condition: only if the player never saw the warning
Dialog[12].SimStatus == "Untouched"
-- script: pretend the greeting already happened
Conversation[2].Dialog[3].SimStatus = "WasDisplayed"

See runtime semantics for when each status is set.

The Desktop simulator registers these; a conforming host should too. They take asset tables as arguments:

IncRelationship(Actor["Traveller"], Actor["Ferryman"], "trust") -- +1
IncRelationship(Actor["Traveller"], Actor["Ferryman"], "trust", 2) -- +2
DecRelationship(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 unset

Relationships 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.

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.

Field values are strings in the document. In Lua they become:

  • true / false for "true" / "false" (any case),
  • a number for anything tonumber accepts,
  • otherwise a string.

Assignments back to Variable[...] keep the Lua type for the rest of the playthrough.

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.

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 ;).

  • The project UserScript runs once, after the tables are built.
  • A node’s ConditionsString is evaluated when the node is a candidate (see runtime semantics), against the state at that moment — before any sibling’s script.
  • A node’s UserScript runs when the node is entered, after its status flips to WasDisplayed (unless DelaySimStatus), 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:

ConstructExample
Variable read / writeVariable["trust"], Variable["trust"] = 3
SimStatus read / writeDialog[4].SimStatus, Conversation[2].Dialog[4].SimStatus = "WasDisplayed"
Comparison== ~= < <= > >=
Booleanand or not, parentheses
Literalsnumbers, "strings", true, false, nil
Arithmetic in assignmentsVariable["trust"] = Variable["trust"] + 1
Several statementsseparated 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.

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:

-- conditions
Variable["trust"] >= 2
Variable["hasToken"] and not Variable["angry"]
Dialog[7].SimStatus ~= "Untouched"
-- scripts
Variable["trust"] = Variable["trust"] + 1
Variable["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.