Expressions
Description: The small language behind every condition and instruction in a graph.
Graphs make decisions and record what happened by running short pieces of code called expressions. There is one language for both jobs, and it is deliberately small — enough to compare a couple of variables or set a flag, not a general-purpose scripting language. If you have written any code before, the syntax will look familiar; if you haven't, most real conditions are a single comparison.
The language shows up in two forms:
- A condition is one expression that answers yes or no —
Gold >= 10 && !HasMap. A Gate waits on one, a connection out of a Hub carries one, a choice can be hidden or disabled by one, and an Event Listener watches one. - An instruction is one or more statements that change something —
JarlIsDead := true. Only an Instruction Node runs these.
Not the same as text directives. The @{PlayerName} and @{select ...} constructs that go
inside player-facing dialogue are a separate, simpler syntax with its own rules — see Text
directives. Don't put @{...} in a condition, and don't put expression syntax
inside a line of dialogue.
If you're familiar with Javascript, Narratyr's expression language is very similar. Most javascript expressions will work in Narratyr. Narratyr also includes some additional features:
&&,||, and!can also be written asand,or, ornot- The
inoperator can be used to check for list/set membership ('x' in MyList) :=can be used in place of=
What you can refer to
Expressions see two things: the variables defined in your document, and the
functions declared in it. Anything else is an error, flagged as you type rather than at runtime.
Press Ctrl+Space in any condition or instruction field to list what's in scope at that spot.
Variables are written by name, exactly as they appear in the Variables view: Gold,
Jarl_IsDead, Player_VisitedLocations. Names never contain a dot — namespacing uses underscores
— so a name in an expression is always a single word.
Values you can write directly:
| Kind | Examples |
|---|---|
| Number | 42, -1.5, 0.25 |
| Text | "MacGuffin", 'a sword' — either quote style |
| True/false | true, false |
| Enumeration entry | QuestPhaseEnum.Active — the enumeration's name, a dot, then the entry key |
| List | ["a", "b", "c"] |
| Empty | null, undefined |
An option set entry is the one place a dot appears. It's always
EnumName.EntryKey — two parts, no deeper — and it matches on the entry's key, not its
player-facing display name. So a variable typed to QuestPhaseEnum is compared with
QuestPhase == QuestPhaseEnum.Active, not with a bare Active or the words shown in a quest log.
Writing a condition
A condition is any expression, evaluated for truth. A bare true/false variable is a complete
condition on its own — JarlIsDead passes when the flag is set — and !JarlIsDead passes when it
isn't. A number counts as true when it's anything but zero, so a condition that comes out as a
count reads naturally as "did this happen at all."
Operators
| Operators | What they do |
|---|---|
== != |
Equal / not equal |
< > <= >= |
Ordering, on numbers or on text |
+ - * / % |
Arithmetic. + also joins text together |
in not in |
Is this value in that list or set? |
[ ] |
Read one element of a list by position: VisitedLocations[0] |
Conditions combine with and, or, and not, which you can spell either as words or as
the symbols &&, ||, and !. The two spellings are identical in meaning — pick whichever your
team finds easier to read and stay consistent.
Parentheses group as you'd expect, and the usual precedence applies — and binds tighter than or, comparisons bind tighter than either, arithmetic tighter still. When a condition gets long enough that you have to think about precedence, parenthesize it; the person reading it next will have the same question you did.
== compares like with like. Comparing a number to a piece of text isn't an error, it's simply
false — so a condition that never seems to fire is worth checking for a type mismatch, such as
quoting a number (Gold == "10").
Working with lists
A list or set variable answers membership directly, and three built-in functions cover the rest:
"MacGuffin" in Player_Inventory
ColorEnum.Red not in Banner_Colors
length(Player_Inventory) >= 3
count(Visited_Towns, "Riften") > 1
indexOf(Party_Members, "Sandra") != -1
length counts elements, count counts how many times one value appears (always 0 or 1 for a
set), and indexOf gives the zero-based position of the first match, or -1 when there is none.
Reading past the end of a list with [ ] yields an empty value rather than an error.
Reacting to what the player has already done
Three built-ins report how many times something has been visited, so a conversation can react to repetition without you keeping a counter variable for it. All three return a whole number, and the counts last for the whole playthrough — they're saved with the rest of the game state, and behave the same in the simulator and in every engine.
choiceTakenCount()— how many times this choice has been taken. It's only valid in a choice's own hide or disable condition. It counts past selections, so it reads 0 the first time the choice is offered:choiceTakenCount() > 0on the disable condition greys out an option the player has already used, and the same expression on the hide condition removes it entirely.lastChoiceTakenCount()— how many times the choice the player just made has been taken, counting this one. This is the routing function: putlastChoiceTakenCount() == 1,== 2, and so on onto the connections leaving a choice to give a different reaction each time the player picks the same reply.traversed(id)— how many times a specific connection, node, or choice has been traversed. The id is required; copy it from the properties panel. It reaches anything the other two don't, including across graphs, but it refers to a particular element by id, so prefer the two above where they fit.
Reacting to quest progress
questStatus("<quest id>") and objectiveStatus("<objective id>") return the current status as
text, which you compare against a fixed set of values:
questStatus("intro_quest") == "completed"
objectiveStatus("find_the_sword") == "active"
A quest is "undiscovered", "inactive", "active", "failed", or "completed"; an objective
is "locked", "active", "completed", "failed", or "not_applicable". Autocomplete offers
both the ids (listed by their readable names) and the valid status words, so neither has to be
typed from memory.
A Gate can also be authored entirely without typing: switch it to the objective wizard and pick objectives and statuses from dropdowns, and it writes the equivalent expression for you.
Writing instructions
An Instruction Node holds one or more statements, separated by semicolons or newlines. Each statement either changes a variable or calls a function.
Jarl_IsDead := true
Gold += 5
Persuasion_Attempts++
Player_Inventory += "OldMap"
addInventoryItem("Potion", 1)
| Form | Meaning |
|---|---|
Gold := 10 |
Assign. Gold = 10 means the same thing |
Gold += 5 |
Also -=, *=, /= |
Gold++, Gold-- |
Add or subtract one |
MyList += "a" |
Append to a list, or add to a set |
MyList -= "a" |
Remove — the last matching entry from a list, the value from a set |
addInventoryItem("Potion", 1) |
Call a function for its effect |
Prefer the compact forms: Gold += 5 says what it means more directly than Gold := Gold + 5,
and both produce the same result.
Note that += and -= do double duty. On a number they add and subtract; on a list or set they
add and remove an entry. Since a set never holds duplicates, adding a value it already contains
leaves it unchanged.
Calling your game's functions
Some things live outside Narratyr — granting an item, playing a cutscene, checking inventory the game already tracks. Those are declared in the Functions view as a name, parameters, and a return type, and your engine integration supplies the actual behavior — see Function definitions. Once declared, a function is called like any other:
hasInventoryItem("MacGuffin") && Gold >= 10
removeInventoryItem("OldMap", 1)
A function that returns a value can be used inside a condition; one that returns nothing is a statement on its own in an Instruction Node.
Narratyr also implements a handful of functions itself, which need no engine support: the list and
traversal functions above, questStatus / objectiveStatus, randomInteger(min, max) and
randomNumber(min, max), and debugLog(message) for writing to the engine's log while you're
tracking something down.
Things worth knowing
- Event Listener conditions can't call functions. A listener re-checks its condition whenever a variable changes, so a condition resting on something Narratyr can't see changing — an inventory check, say — would quietly never fire. Mirror the value into a variable and watch that instead. The validator reports this rather than letting it ship.
- Keep randomness out of conditions that get re-checked.
randomIntegerandrandomNumberreturn something different every call, which makes a Gate or listener that uses one behave unpredictably. Roll once in an Instruction Node, store the result, and branch on the variable. - There are no comments. Use the node's description field, or a Comment node on the canvas, to explain a condition that isn't self-evident.
- Mistakes surface in the editor. Unknown names, unbalanced parentheses, and comparisons between mismatched types are underlined as you type, and the validator collects the same problems document-wide so an unparseable expression can't reach an export.
Interpretting in-game
If you're using Unreal Engine, Godot, or Unity you don't need to worry about this -- we supply a small lightweight interpretter. However, if you're using your own engine or a different one from the ones we directly support, we haven't forgotten you. We also emit an "abstract syntax tree" that contains the parsed result. The expression AST documents the parsed form these expressions are stored and evaluated as.
Next
Function definitions covers declaring the functions above, Variables covers the state expressions read and write, Dialogues and Quests cover the nodes that hold them, and Text directives covers the separate syntax for showing a value inside a line of dialogue.