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 kind | What it asks |
|---|---|
| comparison | a variable against a literal |
| visited / taken | how often a scene was entered, or a choice option taken |
| list contains | whether a list holds an entry |
| list size | how many entries a list holds |
| all of (and) | every child clause is true |
| any of (or) | at least one child clause is true |
| not | the 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 | falseStrings 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 type | Offered in the builder | Accepted by the compiler |
|---|---|---|
flag | == | ==, != |
number | all six | all six |
text | ==, != | all six (ordering is lexicographic) |
single options | ==, != | ==, != |
cast, asset | ==, != | ==, != |
list, multiple options | none | none, use contains() or count() |
| undeclared | all six | it 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 whengoldis missing. - Type mismatch.
==and!=compare values outright, sogold == "ten"is false andgold != "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 exactlyvisitCount(x) >= 1. - Lists. A missing variable, or one holding something that is not a list,
reads as the empty list. So
contains()is false andcount()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
| Code | Cause |
|---|---|
UNDECLARED_VARIABLE | the condition names a variable that is not declared |
UNKNOWN_VISIT_TARGET | visited() or taken() names a scene or option id that does not exist |
CONDITION_TYPE_MISMATCH | literal of the wrong type, or ordering a type that has no order |
LIST_TYPE_MISMATCH | comparing a list directly, or calling contains() or count() on a non-list |
UNKNOWN_OPTIONS_CHOICE | the literal is not one of the options variable's declared choices |
INVALID_CONDITION | an edge's stored condition string is unparseable |