Documentation menu

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 as and, or, or not
  • The in operator 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() > 0 on 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: put lastChoiceTakenCount() == 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. randomInteger and randomNumber return 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.