beasty-visual-novel / authoring / vnbeasty-syntax.md

.vnbeasty syntax reference

Every construct of the .vnbeasty text script, for lookup. One file is one scene: every label is a node, jump wires the nodes together. If you have not used the format before, read The text script first — it covers the editor, the sync rules and the safety contract.

Contents


File structure

A file is an optional header followed by label sections. A label starts at column 0 and ends with :; its statements are indented under it.

scene "Chapter 1"        # readable scene name (optional)
start intro              # entry label / entryNodeId (optional; default = the first label)

label intro:             # a node (DialogueNode by default)
    backdrop bedroom
    juan "Hello, how are you?"
    jump cruce           # wire to another label

jump <label> sets the node’s default next node. A dialogue node with no jump simply ends.

Node kinds

A node’s header leads with its kind keyword, followed by its name. label is the plain dialogue node — the Ren’Py-familiar form — and every other kind has its own keyword:

HeaderNode typeWhat it is
label intro:DialogueNodeRuns its blocks in order, then goes to its jump target.
dialogue intro:DialogueNodeThe explicit form of label.
choice cruce:ChoiceNodeHolds choice lines. See Choices and decisions.
decision ruta:DecisionNodeHolds if / else branches. Invisible to the player.
subgraph combat:SubGraphNodeHolds outcome routes. See Subgraphs and return.
return combat/done ("win"):ReturnNodeEnds a subgraph, returning the quoted outcome key.
talkmenu charla (ana):TalkMenuNodeOpens the talk menu of the character in parentheses. See The talk menu.
flow to_town:FlowNodeA transition as its own node. See Flow and transitions.

This kind-first form is the canonical one: on the next graph sync, a linked script is rewritten to use it. A label whose only line is a -> flow exit still compiles to a FlowNode without needing the flow keyword.

The previous tag form, label <name> (choice):, still parses, so existing scripts keep working — but a tag that contradicts the keyword (choice cruce (decision):) is an import error. A trailing (...) counts as a type tag only when it names a known type: label Meeting (part 2): is a node called Meeting (part 2). Quote the node name when it contains #, ", or ends with ).

The id annotation

label intro:  #@id:8f2c1a7b-…

The #@id:<guid> comment is written automatically on every round-trip. It carries the node’s identity, so renaming a label renames the node instead of destroying it and creating a new one — which would lose the node’s position on the canvas and the wires pointing at it. Omit it when you write a new label by hand; the next sync adds it.

Dialogue and narration

juan "Hello"                  # speaker = the character id 'juan'
"The room went quiet."        # no speaker = narrator
juan (whisper) "psst..."      # delivery state
juan as "The Stranger" "..."  # one-line display-name alias

The full form is <speaker> [(state)] [as "alias"] "text". The delivery state names one of the character’s delivery styles — whisper, shout, thinking, or one of your own — and changes the font, colour and text effect of that line only. as "..." shows the line under an alias without changing the character’s name; to change the name for good, use name.

Dialogue is the only statement with free text. That text is stored in the localization table, in the authoring language selected in the Story tab.

Backdrops

backdrop bedroom              # a sprite, resolved by name
backdrop interiors/bedroom    # disambiguate by subfolder
backdrop clear                # remove the backdrop
backdrop video rain           # a video clip instead of a sprite
backdrop video rain once mute volume 0.5 manual
backdrop sky, hills parallax 0.4, street at 0 -20 order 2   # layers, back to front

A video backdrop loops, plays its audio at full volume, and starts on arrival. Each modifier overrides one of those defaults:

ModifierEffect
oncePlay once instead of looping.
muteSilence the clip’s audio.
volume <0..1>Play the clip’s audio at this volume.
manualDo not auto-play on arrival.

clear and video are keywords only when unquoted, so a sprite genuinely named video still works if you quote it.

Layers

A backdrop can stack several sprites. Write them as one comma-separated list, back layer first, each with its own options:

backdrop <sprite> [at <x> <y>] [parallax <f>] [order <n>][, <sprite> …]
OptionWhat it does
at <x> <y>Offsets the layer by these two numbers. Both are required.
parallax <f>The layer’s parallax factor.
order <n>The layer’s sorting order, a whole number. Without it, layers sort by the order you wrote them.

A single-sprite backdrop is just the one-layer case, which is why backdrop bedroom needs no punctuation. Five layers is the maximum; a sixth is an error.

Props

Props are the foreground sprites that sit over the backdrop. Same layer grammar, same options:

props crate                                   # one prop
props crate, barrel at 40 0, lamp order 3     # several, back to front
props clear                                   # remove them all

props clear empties the prop layer. Like backdrop, clear is a keyword only when unquoted.

Characters

show juan happy at left                  # expression + anchor
show maria base at right scale 1.2 flip
show juan happy portrait angry slot 1    # dialogue portrait + stage slot
expression juan sad
expression juan sad portrait             # ...and swap the portrait to the base one
hide juan
clear characters
clear characters at left                 # only that position

show <character> <expression> [at <anchor>] [scale <n>] [flip] [portrait <key>] [slot <n>]. The expression key is the one defined on the character; the default key is base.

AnchorPosition
leftFar left.
centerleftBetween left and centre.
centerCentre. This is the default, and is omitted when written back from the graph.
centerrightBetween centre and right.
rightFar right.
custom <x>A normalized X position, 0 to 1: show juan happy at custom 0.35.

scale is a multiplier (1 is unscaled, and is omitted when written back). flip mirrors the sprite horizontally.

portrait <key> also sets the portrait shown in the dialogue box, which is otherwise left as it is. slot <n> is the stage layer the sprite goes on, a whole number from 0 to 4: two characters at the same anchor on different slots overlap in a controlled order.

expression <character> <expression> [portrait [<key>]] changes the expression of a character already on stage. The portrait suffix swaps the dialogue portrait at the same time; with a key it uses that portrait, without one it goes back to the character’s base portrait.

hide <character> removes one character. clear characters removes all of them — or only one position, if you say which:

clear characters at <anchor> [layer <n>]
clear characters at custom 0.35 layer 2

The anchors are the same ones show takes, custom <x> included. layer <n> (0 to 4) narrows it to a single slot at that position; without it, every slot at that anchor is cleared.

Audio

music calm fade 2                        # loops by default
sound door
ambient forest
voice juan_l1
stop music fade 1

The form is <channel> <clip> [fade <s>] [vol <0..1>] [once] [keepbg].

ChannelWhat it plays
musicThe music channel. Loops. Pauses the background music while it plays.
ambientThe ambient channel. Loops.
soundA one-shot on the SFX channel.
voiceA voice clip on the voice channel.
ModifierEffectApplies to
fade <s>Fade in over this many seconds. Default 1.music, ambient
vol <0..1>Volume. Default 1.all four
oncePlay once instead of looping.music, ambient
keepbgDo not pause the background music.music

Note that a video backdrop spells its volume volume, while an audio cue spells it vol.

stop <channel> [fade <s>] stops a channel. The channels are music, ambient, sfx and voice.

stop ambient
stop voice fade 0.5

State and inventory

set gold = 10            # assign (also  +=  -=)
set gold += 5
toggle flag_x
dict city = "Madrid"     # a dictionary token
set juan.affection += 1  # a character variable
give 3 potion            # inventory
take 1 potion
use key
item potion = 5          # set an absolute amount
wait 2                   # wait 2 seconds
wait                     # wait for the player to click

set <key> = <value> assigns; += adds; -= subtracts. toggle <key> flips a bool.

A key containing a dot is a character variable: set juan.affection += 1 sets the affection field on the character juan. The exception is item.<id>, which is the item count: set item.potion = 5 goes through the inventory, clamping to the item’s maximum exactly as give and take do.

dict <key> = "<value>" sets a dictionary token — a player-editable piece of text.

give <amount> <item> and take <amount> <item> clamp to the item’s maximum and to 0. item <id> = <amount> sets the amount outright. use <item> runs the item’s on-use effects.

wait <seconds> pauses. wait with no number waits for the player to click — Auto stops there too, until the player advances by hand.

The names come from the Variables, Dictionary and Items tabs. The script references them; it does not create them.

Quests, screens and routines

quest ana_m1 state = active        # notstarted / active / completed / failed
quest ana_m1 stage = 2             # ordered quests: set the stage index
quest ana_m1 stage += 1            # ...or advance it
quest ana_m1 objective run = true  # mark an objective done (false clears it)
deliver ana_m1 entrega             # hand over a gather-and-deliver objective's items
screen inventory                   # open a secondary screen (by its id)
routine ana Work                   # switch a character's routine profile ("" = default)
FormWhat it does
quest <id> state = <state>Sets the quest state. The four states are notstarted, active, completed, failed.
quest <id> stage = <n>Sets the stage index of an ordered quest.
quest <id> stage += <n>Advances the stage index.
quest <id> objective <objId> = trueMarks an objective done. = false clears it.
deliver <quest> <objective>Hands over the items of a gather-and-deliver objective. Does nothing if the player does not have them.
screen <id>Opens a secondary screen.
routine <character> <profile>Switches the character’s active routine profile. Use "" for the default profile.

Game time

time +2 dayparts                   # advance the clock (also: +3 hours, +1 day)
time daypart evening               # ...or set it outright (quote names with spaces)
time hour 14                       # Clock mode only
time weekday monday

The advance forms lead with a signed amount, the set forms with the unit:

FormWhat it does
time +<n> daypartsAdvance by n dayparts.
time +<n> hoursAdvance by n hours. Clock mode only.
time +<n> daysAdvance by n days.
time daypart <name>Set the daypart.
time hour <n>Set the hour. Clock mode only.
time weekday <name>Advance to the next matching weekday. If today already matches, the date does not move.

The unit may be singular (+1 day is the same as +1 days); the canonical form written back from the graph is always the plural. The daypart and weekday names are the ones configured in the project’s time config.

Prompts

A prompt shows a line plus a text field, and writes the player’s answer somewhere.

ask gold "How much gold?" default 0 required
ask dict city "Your city?"
ask name hero "What's your name?" default "Traveler"
FormWhere the answer goes
ask <variable> "<question>"Into a variable.
ask dict <token> "<question>"Into a dictionary token.
ask name <character> "<question>"Into the character’s displayed name.

Options, in order after the question: by <character> [(state)] [as "alias"] makes a character ask it instead of the narrator, with an optional delivery state and alias; default <value> pre-fills the field; required refuses an empty answer.

ask gold "How much do you have?" by juan (whisper) as "The Stranger" default 0 required

Character names

name juan = "Don Juan"            # set the displayed name (literal text)
name juan = alias "The Stranger"  # ...from one of the character's aliases
name juan = var player_name       # ...from the value of a variable or token
name juan = key char.juan.formal  # ...from a localization key, so it translates
name juan reset                   # back to the base name

This changes the name for good, unlike the one-line as "..." on a dialogue line.

= key <locKey> names a key in the localization table, so the new name follows the player’s language. = "..." writes the text as it is, in every language.

Flow and transitions

freeroam town/square              # go to a FreeRoam room
freeroam previous                 # return to the room the player came from
freeroam choose town              # let the player choose a room on that map
goto-scene Chapter2               # go to another DialogueScene
goto-scene Chapter2 from intro    # ...starting at a given node

freeroam <map>/<room> names the map graph and the room in it.

A bare flow line like the ones above is a trailing exit block inside a dialogue node: it runs after the node’s other blocks. To make the transition its own node in the graph — a FlowNode — open a flow node whose only line is the exit, prefixed with the route arrow:

flow to_town:
    -> freeroam town/square      # a FlowNode

flow leave:
    -> freeroam previous         # any flow exit works: previous / choose <map> / goto-scene …

Other nodes reach it with jump to_town, or with -> to_town as a choice or branch target. The arrow line must be the node’s only content; a plain label whose only line is a -> exit also compiles to a FlowNode. A -> route to another label is written jump <label> instead.

Choices and decisions

Choices and decisions live in their own node and are reached with jump. One line per option. The target after -> can be another label or a flow exit (freeroam … / goto-scene …).

choice cruce:
    image crossroads                                             # the side image (see below)
    image crossroads_night if @time:daypart == Night
    choice "Go left" -> cave
    choice "Buy a sword" if gold >= 10 { gold -= 10 } -> smith   # condition + effects
    choice "Flee" -> freeroam town/square                        # flow target
    default -> alley                                             # used if everything is gated out

A choice node shows the options whose condition passes. default -> <label> is where it goes when every option is gated out.

image <sprite> [if <condition>] is the illustration shown beside the options. Write one plain image line for the default picture, and one conditional line per variant; the first variant whose condition passes wins, and the plain line is the fallback. Only one image line may go without a condition — two would mean two defaults, and that is an error. A choice node with no image line shows no picture.

decision ruta:                   # invisible router (DecisionNode)
    if gold > 100 { rich = true } -> rich_end
    if saw_intro -> chapter2
    else -> poor_end             # the fallback branch (empty condition)

A decision node routes automatically and invisibly: the first branch whose condition passes wins, otherwise the fallback. The player sees nothing. else if <condition> -> <label> is a conditional branch, not the fallback; a bare else is the fallback.

Both choice and if take an optional condition and an optional effect block, in that order, before the arrow. See Conditions and effects.

Note There is no menu: block. Write one choice "text" -> label line per option inside a choice node.

The talk menu

talkmenu charla (ana):
    default -> after_talk

A talkmenu <name> (<character>): node opens that character’s talk menu — the hub of topics you author on the character, not here. Its whole body is an optional default -> <label>: where the story goes when the player closes the menu. The character id in parentheses is required; an unknown one is reported as a warning.

See The talk menu for the topics themselves.

Subgraphs and return

A subgraph node nests a StoryGraph made of child labels named parent/child. Its body routes the nested outcomes back to the outer graph.

subgraph combat:
    outcome win -> after_win
    default -> after_combat

label combat/fight:              # a child node (the prefix is the parent label)
    "..."
    jump combat/done

return combat/done ("win"):      # a ReturnNode; effects via set / toggle
    toggle won_fight

outcome <key> -> <label> routes one outcome key; default -> <label> catches the rest. A return <name> ("<key>"): node ends the nested graph and hands that key back. Its set and toggle lines are the return node’s effects.

Subgraphs nest one level: a child label cannot itself be a subgraph.

Conditions and effects

A condition is a list of token op value clauses joined by and or or. A bare token (if flag) is shorthand for flag == true. and binds tighter than or, so a and b or c reads as (a and b) or c. An empty condition is always true.

OperatorMeaning
==Equals.
!=Not equals.
>Greater than.
<Less than.
>=Greater than or equal.
<=Less than or equal.
containsThe value contains the given text.
if gold >= 10 -> smith
if gold >= 10 and has_map -> smith
if @time:daypart == Morning or maya.affection >= 2 -> visit
if item.potion >= 2 -> heal
if saw_intro -> chapter2

A token is any of these:

TokenWhat it reads
goldOne of your own variables.
maya.affectionA character variable — the affection field of the character maya.
item.potionHow many of that item the player carries.
@time:daypartA reserved key — time, quests, routines. Type it exactly as the key column of Variable keys spells it.

A dotted token means the same key in a condition, in an effect block and in set: if maya.affection > 2 reads exactly what set maya.affection += 1 wrote, and if item.potion >= 2 reads what give, take and item potion = 5 wrote.

Note The dot is for character variables and items only. The reserved keys have no dotted form in the text script: write @time:daypart, not time.daypart — the latter would read a field called daypart on a character called time, which is not what you meant. The graph’s condition picker shows those keys with a friendly dotted label; the script wants the raw key.

See Variables and conditions and Variable keys for the full list.

An effect block is a { … } list of mutations, separated by commas, written after the condition and before the arrow. Each entry is key = value, key += n, key -= n, or toggle key.

choice "Buy a sword" if gold >= 10 { gold -= 10, has_sword = true } -> smith
if gold > 100 { rich = true, toggle celebrated } -> rich_end

Notes

  • Comments start with #. A # inside a quoted string is text, not a comment. Blank lines are ignored.
  • Indentation under a label is 4 spaces. The Text tab’s Tab key inserts 4 spaces.
  • Strings are double-quoted, with \", \\, \n, \r and \t escapes.
  • Numbers use . as the decimal separator, whatever your system locale is.
  • Asset names resolve to objects by GUID, so moving or renaming an asset does not break a synced node. Run Format to refresh the name written in the text. A name that does not resolve — a typo, or a name several assets share — is an error: the import is refused and the graph is left untouched, so a typo can never destroy a reference. Disambiguate with a subfolder: backdrop interiors/bedroom.
  • The folders listed in VN Settings keep names short and unambiguous. They are not a gate: an asset that lives outside them still resolves by name, project-wide, as a last resort.
  • A block with no asset assigned — a backdrop with no art, a music, sound or voice cue with no clip — does nothing in game: it is skipped, leaving whatever is already on screen or playing. It is not written to the script either, so saving the script removes that placeholder from the graph as well. To blank the backdrop or silence a channel on purpose, use backdrop clear or stop <channel>.
  • A name that does not exist is a warning, not an error. Condition tokens, effect keys, the keys of set, toggle and dict, item ids, quest ids, objective ids and screen ids are all checked against what the project declares. An unknown name is reported with its line number — the import still goes through, because the name may be one you are about to create, but a typo no longer reaches the graph in silence and targets a key nothing else uses. Unresolvable assets and unknown labels remain errors and refuse the import.
  • Autocompletion. On a header line — column 0 — the Text tab suggests the node-kind keywords (label, choice, decision, subgraph, return, talkmenu, flow) plus scene and start; start suggests the script’s labels, and talkmenu suggests character ids for its (<character>) argument. Header keywords are highlighted in the colour their node kind has in the graph. Inside a node, the Text tab suggests, at the start of a line: backdrop, props, image, show, expression, hide, clear characters, jump, set, toggle, dict, give, take, use, item, deliver, wait, music, sound, ambient, voice, stop, name, ask, quest, screen, routine, time, choice, if, else, default, freeroam, goto-scene — plus your character ids, since a line can start with a speaker. After the keyword it suggests what that keyword expects: characters, expressions, portrait keys, anchors, variables, dictionary tokens, items, quests and their objectives, screens, routine profiles, daypart and weekday names, asset names, and the labels already in the file. Where a condition or an effect goes, it offers the whole variable catalog — your own variables, character fields as maya.affection, item.<id> counts, dictionary tokens and the reserved @time: and @quest: keys — the same list the graph’s condition picker shows.

See also

  • The text script — the editor, the sync rules, the safety contract and the limits.
  • Blocks reference — the same vocabulary, as blocks in the graph.
  • The story graph — the node types a script compiles into.
  • Characters — ids, expressions, delivery styles and aliases.
  • Quests — quest ids, stages and objectives.
  • Game time — dayparts, the clock, and the two time modes.
  • Free-roam rooms — the maps and rooms freeroam targets.