Skip to content

Scripts on a node

Making something happen when the story arrives — where a script lives, a script on an item, what it may do and touch, and what a failure costs.

All documentation — Variables, logic and commands
On this page

A script runs when the story arrives at a node. It is where state changes: setting a variable, adding to an inventory, telling the game to play a cutscene.

If a condition asks may the story go this way?, a script answers and what happens now that it has?

A code node on the canvas holding an if/else block: it reads hackingstat and an interactable flag, then subtracts power cells, sets a story phase, and otherwise raises hackingstat.
A script, on the card that runs it.

The language itself is the variables reference.

Where a script lives

A script is the text of a field whose type is code, on a node. The standard pack’s Code node — runs an expression when the flow reaches it — is a node type with one such field and nothing else, but any node type you author can carry one, and can carry several.

That is worth knowing because it changes how you build: a Dialogue node with a code field runs its script and says its line. You do not need a separate Code node beside every line of dialogue.

A script on an item

A code or condition field can also live on an entity definition, so every item of that kind carries its own script — a card’s On use, a door’s Can open. Your game runs it when it decides to (play mode in the browser runs node scripts only).

self is the item the script is on

Inside a script stored on an item, self means that item:

target.hp -= self.damage + user.strength

Write self, never the item’s own code, and duplicating the item gives you a working script with nothing to edit: Iron Dagger copied from Rusty Dagger hits for its own damage. Typing self. lists the fields of the item’s kind.

self means something only there. In a node’s script it is underlined: ‘self’ only means something in a script on an item. Here, name the item by its code instead. For the same reason nothing in the project may be called self — not an item, a variable, a group, a role or a name inside a script.

Script receives — the items the game hands it

Most item scripts are about something else as well: the enemy a weapon hits, the hero using it. Name those in the field’s settings (the gear on the field) under Script receives: one row per name, and for each the kind of item it holds.

NameKind of item
targetCombatant
userCombatant

From then on the script can write target.hp and user.strength. Typing target. lists a Combatant’s fields, and a misspelling is underlined at once: ‘target’ (Combatant) has no field called ‘hpp’. A name the field does not receive is underlined too, with a pointer to Script receives.

A name there follows the same rules as any name a script makes up: letters, digits and underscores; not a word the language keeps for itself; not self; used once — and Target or tar_get count as target again, because a game engine reads them as one name; and not the code of an item or a group — inside the script, the name would hide it. Unlike a temporary name, it may not be the code of a variable, a media file or an option set either: the row lives in the field’s settings, not in the script, so a role called gold would quietly turn every gold already written on that kind of item into the role, and one called Mood every Mood.calm. Case does not tell them apart: Gold counts as gold. It works the other way round too — once a role is called target, a new variable or media file named “Target” gets the code target_1, and renaming one to target is refused (a variable inside a group is read as group.target, so it may still use the name).

In a condition field on an item, the visual builder lists these too: This item for self’s fields and Roles for the names it receives, so target.hp > 0 can be picked rather than typed.

When the game runs the script, each item it hands over must be of the kind the row names, or of a kind made from it: a Boss made from Combatant can be the target, a Potion cannot. An item of another kind, or a script on an item that calls a command the story waits for, is refused before the script changes anything, and the game is told why.

Changing every item in a list

A loop over a field that holds a list of items reaches each item in turn, and can change it:

for (w in user.weapons)
w.damage += 1
endfor

That is a whetstone sharpening every weapon its user carries. When the list field allows only one kind of item (its Allowed Entity Definitions names exactly one), typing w. lists that kind’s fields and a wrong field name is underlined. When it allows several kinds, w.anything is accepted as written — the editor will not guess which kind it is.

When it runs, and in what order

At each node, in this order:

  1. Every script slot runs, in document order.
  2. The node presents a step, if it has content.
  3. Its exits are resolved — any exit with content means choose, otherwise the first passing gated exit is taken, otherwise a single exit advances, otherwise the story ends.

So a script has already run by the time the node’s own exits are gated. A variable you set in step 1 is visible to a gate in step 3, on the same node.

Everything a script may do

Six statements. There is nothing else.

StatementWhat it does
Assignmentgold = 50, gold += 10, kim.trust -= 1. Writes a variable or an entity field.
A method callinventory.add("key"), flags.toggle("met_kim"), party.clear()
A command callplayCutscene("act2_open") — tells the game to do something
letlet total = gold + silver — a block-scoped local
forfor (item in inventory) … endfor, with break and continue
ifif (…) / elseif (…) / else / endif

What a script may touch

It canIt cannot
Read and write variablesCreate a variable, a tag or a node type
Read and write entity fieldsChange a definition’s schema
Call a commandReach the network or the file system
Read the visit count of a nodeRead a wall clock
Ask whether an entity existsChange the project
Draw from the playthrough’s seeded randomUse an unseeded random

Random is seeded, always. The playthrough owns a generator, a draw advances it, and its state is part of a save — which is what makes a save replay identically. That is also why random() is refused in a condition.

Building one without typing

A node’s script opens in Edit Code:

Build code visually using statements. The generated code is shown in the preview panel. Changes save automatically.

Unlike a condition, this surface has no Visual/Code toggle — the right-hand column shows the generated code read-only. You build with statement rows.

Add a statement offers these, searchable by What should happen?:

StatementWhat the menu says
SetPut a value into a variable — typed, another variable, or worked out.
ChangeAdd to, or take from, what a variable already holds.
LetGive a value a name, just inside this block, so you only work it out once.
CallDo something the game knows how to do. — only when the project has commands
IfDo one thing when a test passes, and something else when it does not.
ForDo the same thing once for every thing in a list.
BreakStop repeating and carry on after the block. — only inside a For
ContinueSkip the rest of this turn and go straight to the next thing. — only inside a For
NoteA line for you, ignored when the story runs.

An empty search also offers one ready-made pattern, Make a random check — Roll a die, remember it, then branch on the result — which writes let roll = roll(20) and an if roll >= 10.

A statement the builder cannot draw is kept as a raw code row rather than being lost, with its parse error inline. When any row is unhappy the dialog says Some statements need attention - delete or fix them.

The side panel has two tabs: Generated code, and What you can write — a searchable vocabulary browser grouped as Your variables, Your entities, Your commands, Your option sets, Actions on a set, Numbers, Text, Lists, The story so far, Chance and Writing a script.

A worked shape

if (gold >= 200)
gold -= 200
inventory.add("lantern")
flags.add("bought_lantern")
playSound("purchase")
else
playSound("denied")
endif

Two things to notice. inventory.add(…) and flags.add(…) are methods on a variable, not functions — every argument is flattened, so add(list) adds each member. And playSound(…) is a command: it crosses into the game, and it is the only line here that a gate could not contain.

When a script fails

The errors themselves — what each code means and the exact wording — are on the variables reference.

&& and || absorb an error on one side when the other side already decides the answer.

The budget

LimitValue
Expression size5,000 parts
Operations per invocation200,000
Nesting depth64
Items in one value100,000
Characters in one value1,000,000

Exceeding one is an error like any other: the step still happens, and you are told which limit and by how much.

The one refusal that is not a coercion

The language coerces almost everything — a list read as a number becomes its length, null read as text becomes empty. One mismatch is refused outright: assigning a list into a variable that cannot hold one.

'gold' holds a number, so it can’t be given a list. Take a single value out of it first — count(…) for how many there are, or first(…) for one of them.

The reason is where the damage would go. Every other mismatch produces a wrong value of the right shape, and it stays inside the playthrough. This one produces a wrong shape, and it travels into the export and into a typed field in your game.

for … in refuses a non-list for the same reason, rather than quietly looping once.

Visual or Code

A script has the same per-control toggle a condition does, and the same guarantee: text is the source of truth, and the rows are a projection of it, parsed by the same parser and serialised by the same encoder. Anything the builder cannot draw is preserved byte for byte, and an edit splices only the stretch of source it came from.

Code → Visual is refused while any control does not parse. Visual → Code always works.

Written for
Writers · Unity developers · Advanced integrators
Last updated