Narratomidocs
State and logic

Conditions

Reference for the condition editor, the typed expression syntax and its grammar, and how conditions evaluate when something is missing or mistyped.

A condition is a test over story state. It gates an edge in the flow view, a choice option, and a HUD widget's visibility. The same editor appears in all three places, with two faces over one underlying expression: a form builder (the default) and a typed syntax box. Switching faces does not convert anything, they edit the same thing.

An empty condition means "always". The builder says so: "No condition (always)."

The builder

Every clause has a kind, picked from one dropdown:

Clause kindWhat it asks
comparisona variable against a literal
visited / takenhow often a scene was entered, or a choice option taken
list containswhether a list holds an entry
list sizehow many entries a list holds
all of (and)every child clause is true
any of (or)at least one child clause is true
notthe child clause is false

The variable dropdown only offers variables the clause is allowed to use. A comparison never lists a list or a multiple-options variable, because comparing one directly is a compile error. contains and list size only list those two. The operator dropdown and the value editor follow the picked variable's declared type, so a flag offers == alone with a true/false control, a number offers the full ordering with a number input, and a cast variable offers your cast members by name.

The visited picker lists scenes from the chapter you have open. To test a scene in another chapter, use the syntax face and type its id.

The typed syntax

Toggle to syntax and you get one expression field. Some examples that parse:

gold >= 10 and (met_kai or not betrayed)
visited("s-dock")
takenCount("o-bribe-guard") > 1
contains(bag, "crowbar") and count(clues) >= 3
disguise == "merchant"

A bare variable reference means "this flag is true": betrayed parses as betrayed == true.

Grammar

expr       := or
or         := and ('or' and)*
and        := not ('and' not)*
not        := 'not' not | atom
atom       := '(' expr ')' | visit | list | comparison
visit      := ('visited'|'taken') '(' id ')'
            | ('visitCount'|'takenCount') '(' id ')' op number
list       := 'contains' '(' id ',' id ')'
            | 'count' '(' id ')' op number
id         := ident | string
comparison := varRef (op literal)?
op         := eq | ne | lt | lte | gt | gte
            | == | != | < | <= | > | >=
literal    := number | string | true | false

Strings take single or double quotes, with backslash escaping. visited and contains are only function calls when a ( follows, so a variable of your own named count still works as a plain reference. Whitespace is free.

The lexer accepts dots inside a name (flags.betrayed), but no such variable can be declared, so a dotted reference always resolves to nothing. Do not use one.

Formatting is canonical. Whatever you type, the box re-prints from the tree when it loses focus: word operators come back as symbols, redundant parentheses disappear, a bare betrayed comes back as betrayed == true, and visitCount("s-dock") >= 1 comes back as visited("s-dock").

Operators by type

Declared typeOffered in the builderAccepted by the compiler
flag====, !=
numberall sixall six
text==, !=all six (ordering is lexicographic)
single options==, !===, !=
cast, asset==, !===, !=
list, multiple optionsnonenone, use contains() or count()
undeclaredall sixit is an error, see below

Ordering a flag, an options, a cast or an asset variable is CONDITION_TYPE_MISMATCH. There is no useful order over those, and at runtime such a test is dead.

How a condition evaluates

The runtime tests against one flat snapshot of variable values.

  • Missing variable. A comparison on a name that is not in the snapshot is false, for every operator, including !=. A gate never opens on state that does not exist. Note the consequence: not (gold > 5) is true when gold is missing.
  • Type mismatch. == and != compare values outright, so gold == "ten" is false and gold != "ten" is true. Any ordering across different types, or any ordering of a boolean, is false.
  • Visit counters. A scene never entered, or an option never taken, reads as 0. visited(x) is exactly visitCount(x) >= 1.
  • Lists. A missing variable, or one holding something that is not a list, reads as the empty list. So contains() is false and count() is 0.

None of these silently-false cases should reach a published story: the compiler catches undeclared names first.

When the editor cannot read what you typed

The expression box validates on every keystroke. On a parse error it keeps your text, marks the field invalid, and shows the message with a position, for example unexpected "=" (did you mean == or !=?) at position 5 in "gold = 10". While the error stands, three things hold: the last valid expression is what stays saved, the builder toggle is disabled (flipping to it would adopt a tree salvaged from broken text), and blurring the field does not revert your input. Press Escape to give up and go back to the last valid expression.

A stored condition that cannot be parsed at all (hand-edited data, or an older document) is not eaten. The editor shows it read-only under the label "stored condition (unreadable)" with the parse error, and both faces stay locked until you press Discard and rebuild.

Compile errors you will meet

CodeCause
UNDECLARED_VARIABLEthe condition names a variable that is not declared
UNKNOWN_VISIT_TARGETvisited() or taken() names a scene or option id that does not exist
CONDITION_TYPE_MISMATCHliteral of the wrong type, or ordering a type that has no order
LIST_TYPE_MISMATCHcomparing a list directly, or calling contains() or count() on a non-list
UNKNOWN_OPTIONS_CHOICEthe literal is not one of the options variable's declared choices
INVALID_CONDITIONan edge's stored condition string is unparseable

On this page