Skip to content

DSL Syntax

Dialogue files use the .dlg extension and a DSL (domain-specific language), a small scripting format for branching conversations. Its keywords define nodes, choices, conditions, and effects.

A .dlg file consists of:

  1. Optional TRIGGER declaration (auto-start on location entry)
  2. Optional top-level REQUIRE conditions (for triggered dialogues)
  3. One or more NODE blocks
TRIGGER tavern
REQUIRE notFlag seenIntro
NODE start
NARRATOR: @narrator.intro
SET flag seenIntro
CHOICE @narrator.choice.continue
END dialogue
END

Defines a dialogue node, which is a single point in the conversation.

NODE greeting
BARTENDER: @bartender.hello

The first NODE in the file is the start node (used as the dialogue’s startNode).

Closes a CHOICE or IF block:

CHOICE @option
GOTO next
END

When used as END dialogue, it’s an effect that closes the conversation:

CHOICE @goodbye
END dialogue
END

Routes to another node (within a choice or as auto-advance):

CHOICE @option
GOTO next_node
END

Or to a location (ends dialogue and moves player):

GOTO location market

GOTO location handles scripted movement by ending the dialogue and changing the current location. Map travel through travelTo() also calculates travel time and checks location triggers.

Declares that this dialogue auto-starts when the player enters a location:

TRIGGER tavern

Must be at the top of the file, before any NODE.

Condition that must pass. At the top level (for triggered dialogues) or inside choice blocks:

# Top-level: controls when the trigger fires
TRIGGER tavern
REQUIRE notFlag seenIntro
# Inside a choice: controls when the choice is visible
CHOICE @buy_drink
REQUIRE variableGreaterThan gold 4
GOTO drink
END

Condition names include hasFlag, notFlag, hasItem, variable comparisons, atLocation, questAtStage, questStatus, character and relationship checks, timeIs, itemAt, and roll.

BARTENDER: @bartender.greeting

The text before the first : is the speaker name. It is matched to a character ID (case-insensitive). The text after : is the dialogue line, which supports @key localization and may itself contain colons.

Each node has one speaker line. To let a different character speak, route to another NODE. That is how a conversation moves between speakers. A node with two speaker lines is a validation error.

Narration with no speaker:

NARRATOR: @narrator.description

In the snapshot, which contains the data sent to the renderer, the speaker is null and speakerName is "Narrator".

Optional voice audio file for the current node:

VOICE bartender_greeting.ogg

Optional portrait override (e.g., different expression):

PORTRAIT bartender_angry.png
CHOICE @choice_text
REQUIRE condition # Optional, multiple allowed
effect1 # Optional effects
effect2
GOTO target_node # Destination (see below)
END

Choices are shown to the player as clickable options. They can have:

  • Conditions: choice is hidden if any condition fails
  • Effects: run when the choice is selected
  • GOTO: required unless the choice terminates the dialogue

A choice holds button text, conditions, effects, and a route. To show narration when a choice is picked, route it to a node with GOTO and put the line in that node.

A choice terminates the dialogue (no GOTO needed) when it contains END dialogue or GOTO location:

# Terminal choice: ends the dialogue
CHOICE "Look around."
END dialogue
END
# Terminal choice: ends dialogue and travels to location
CHOICE "Head to the market."
GOTO location market
END
IF condition
GOTO target_node
END

or with effects:

IF hasFlag metBartender
SET flag returningCustomer
GOTO returning_greeting
END

or with effects that fall through to the node’s regular GOTO:

IF hasFlag metBartender
ADD relationship bartender 1
END
GOTO greeting

How IF blocks work:

  1. IF blocks are evaluated in order (top to bottom) after the node’s effects run
  2. The first condition that passes runs that IF block’s effects
  3. If the passing IF block has a GOTO, that target is used
  4. If the passing IF block has no GOTO, the node falls through to its regular GOTO (if present)
  5. If no IF conditions pass, the node falls through to its regular GOTO (if present)
  6. IF blocks are invisible to the player: they create conditional branches
  7. Multiple IF blocks can exist in a node, but only the first passing one executes

Example:

NODE check_reputation
BARTENDER: @bartender.sizing_you_up
IF variableGreaterThan reputation 50
GOTO trusted_path
END
IF variableGreaterThan reputation 20
GOTO neutral_path
END
GOTO suspicious_path

If reputation is 60, goes to trusted_path. If reputation is 30, goes to neutral_path. If reputation is 10, goes to suspicious_path.

How the engine handles nodes with no CHOICE blocks depends on whether the node has text:

The engine shows the text and waits for the player to click Continue. Only after the player clicks does the engine advance via GOTO or IF blocks.

NODE intro
BARTENDER: @bartender.welcome
GOTO main_menu

The player reads the bartender’s welcome, clicks Continue, then the engine moves to main_menu.

Text-only nodes are the natural way to present narration, character monologues, or story beats before branching.

Silent processing nodes (no text, no choices)

Section titled “Silent processing nodes (no text, no choices)”

A node with no speaker line and no text is a silent processing node. The engine processes it instantly, applying effects and evaluating IF blocks, then advances to the next node without waiting for the player.

# Roll the dice and branch. The player never sees this node as a prompt.
NODE skill_check
ROLL result 1 20
IF variableGreaterThan result 14
SET flag passedSkillCheck
ADD relationship bartender 1
GOTO success
END
GOTO failure
NODE success
BARTENDER: "Impressive. This one's on the house."
CHOICE "Cheers!"
GOTO start
END
NODE failure
BARTENDER: "Nope. Five gold."
CHOICE "Fine."
ADD variable gold -5
GOTO start
END

skill_check is silent. It runs ROLL, evaluates the IF, applies the passing branch’s effects, and jumps to success or failure without the player seeing any prompt.

Node type Has text? Has choices? Player sees
Text-only Yes No Text + Continue button
Choice node Yes or no Yes Text + choice buttons
Silent processing No No Auto-advances instantly

IF blocks (conditional branching):

  • Invisible to the player
  • The first matching branch runs
  • Effects inside the passing IF block run before its GOTO
  • Used for conditional story flow based on game state

CHOICE REQUIRE (player-facing filtering):

  • All passing choices are shown to the player
  • Player selects which one to take
  • Used for showing options only when their requirements pass (for example, “needs 50 gold”)
# IF: player never sees the branching
NODE greeting
BARTENDER: @bartender.hello
IF hasFlag metBefore
GOTO returning_customer
END
GOTO new_customer
# CHOICE REQUIRE: player sees all available options
NODE offer
BARTENDER: @bartender.what_can_i_get_you
CHOICE @buy_ale
REQUIRE variableGreaterThan gold 4
GOTO buy_ale
END
CHOICE @buy_wine
REQUIRE variableGreaterThan gold 10
GOTO buy_wine
END
CHOICE @just_browsing
GOTO leave
END

Effects modify game state. See Effects Reference for the full list.

SET flag metBartender
CLEAR flag doorLocked
SET variable gold 100
ADD variable gold -5
ADD item old_coin
REMOVE item rusty_key
MOVE item sword armory
SET questStage odd_jobs started
SET trackedQuest odd_jobs
ADD journalEntry tavern_discovery
SET characterLocation merchant tavern
ADD toParty elisa
REMOVE fromParty elisa
SET relationship bartender 5
ADD relationship bartender 1
SET characterStat elisa level 5
ADD characterStat elisa health -10
SET mapEnabled false
ADVANCE time 2
START dialogue merchant_intro
END dialogue
MUSIC tension_theme.ogg
SOUND door_slam.ogg
VIDEO intro_cinematic.mp4
INTERLUDE chapter_one
ROLL bluffRoll 1 20
NOTIFY @notification.quest_started

Dialogue text can be written in three forms:

Plain text: Just write the words. Works for most lines, including text that contains colons.

BARTENDER: Hello there, traveler!
CHOICE What's the news?
NARRATOR: The sign reads: closed until dawn.

Quoted text: Wrap in double quotes when the text contains a # (otherwise everything from the # onward is treated as a comment). Quotes are stripped before display.

BARTENDER: "Room #3 is down the hall."

Inside quoted text, write \" for a double quote and \\ for a backslash. This is how a line can contain both quotes and a #:

NARRATOR: "He said \"room #3\" and walked off."

The player sees: He said “room #3” and walked off. Doodle Studio writes these escapes for you when you type quotes in the visual editor.

Quoted speaker and narrator text can also span several lines:

NARRATOR: "The road was empty.
By morning, the snow had covered our tracks."

The line breaks are part of the displayed text, but the passage remains one dialogue entry. Use plain text without quotes for ordinary single-line dialogue.

Localization keys (prefixed with @): Reference a key from a locale file. Required for multi-language support.

BARTENDER: @bartender.greeting
CHOICE @bartender.choice.ask_news

When the engine prepares the snapshot, it looks up the @key in the current locale. A missing key appears as the raw @key string.

For single-language games, plain or quoted text is simpler. Add @keys later when you need multiple languages.

Speaker lines, narrator text, and choices support lightweight formatting:

BARTENDER: He hands you a cE5C453[*key*].
NARRATOR: _The room falls silent._
CHOICE Take the *key*.

The player sees:

  • Bartender: He hands you a key.
  • Narrator: The room falls silent.
  • Choice: Take the key.
Effect Syntax Output
Bold *key* key
Italic _The room falls silent._ The room falls silent.
Color cE5C453[key] key
Bold and color cE5C453[*key*] key

The same syntax works inside locale values:

bartender.gives_key: 'He hands you a cE5C453[*key*].'

Color codes use exactly six hexadecimal digits without a leading #. To show formatting punctuation literally, put a backslash before it:

NARRATOR: Write \*word\* to display *word* without bold formatting.

Unclosed formatting is displayed literally. Enter formatting directly in dialogue text or locale values.

Lines starting with # are ignored:

# This is a comment
NODE start
BARTENDER: @bartender.greeting # Inline comments work too

A # inside a quoted string is preserved as text. In plain text, # starts a comment and everything after it is ignored. Use quotes if you need a literal # in dialogue.

Triggered intro and character conversation live in separate files, using localization keys for every displayed line.

content/dialogues/tavern_intro.dlg:

# Plays automatically the first time the player enters the tavern
TRIGGER tavern
REQUIRE notFlag seenTavernIntro
NODE start
NARRATOR: @narrator.tavern_intro
SET flag seenTavernIntro
CHOICE @narrator.choice.look_around
END dialogue
END

content/dialogues/bartender_greeting.dlg:

# Plays when the player clicks the bartender character
NODE start
BARTENDER: @bartender.greeting
CHOICE @bartender.choice.ask_rumors
REQUIRE notFlag heardRumors
SET flag heardRumors
ADD relationship bartender 1
GOTO rumors
END
CHOICE @bartender.choice.buy_drink
REQUIRE variableGreaterThan gold 4
ADD variable gold -5
ADD variable _drinksBought 1
NOTIFY @notification.bought_drink
GOTO after_drink
END
CHOICE @bartender.choice.ask_quest
REQUIRE questAtStage odd_jobs started
GOTO quest_update
END
CHOICE @bartender.choice.goodbye
GOTO farewell
END
NODE rumors
BARTENDER: @bartender.rumors
ADD item old_coin
NOTIFY @notification.found_coin
CHOICE @bartender.choice.interesting
GOTO start
END
NODE after_drink
BARTENDER: @bartender.after_drink
# Player sees text, clicks Continue, then engine advances to start
GOTO start
NODE quest_update
BARTENDER: @bartender.quest_info
SET questStage odd_jobs talked_to_merchant
NOTIFY @notification.quest_updated
GOTO start
NODE farewell
BARTENDER: @bartender.farewell
END dialogue