Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

VN-Mikro

Write a visual novel the way you would write a play. VN-Mikro turns your script, pictures and music into a finished game — with menus, saves, rollback and builds for every platform already done.

This handbook matches runtime 0.1.0.

Two ways in

Making a game? Start at What VN-Mikro is and read the sidebar top to bottom. Each part adds one layer to your game:

PartYou learn to…
WelcomeWhat the player does for you, JSON and folders, author tools, first project
Writing your storyScenes, speakers, stage directions, choices and pacing
Setting up your gameName, text speed, sound, saves, Back, layout, multi-story packs
Directing a scenePrecise direction, live flags, sticky / clear
The stagePlanes and slots, backgrounds, props, music
CharactersCast, layered packs, talking mouths, expression fades, places
Dialogue panelsRouting, presets, height/scroll, several panels, append, presence, art
Look and menusTheme, name plate, choices/inputs, free choices, title/pause/HUD
PolishTransitions, animation, shaders, cursor, notifications, keys, translations
ShipIdentity, platforms, Build, runtime version, native extensions

Looking for something? Open Reference:

Worked projects live in the examples/ folder next to the player.

What VN-Mikro is

VN-Mikro is a player for visual novels. You give it a folder — your script, your pictures, your music — and it plays that folder as a finished game.

my-game/
  game.json          your game's name and settings
  story/script.txt   the story, written like a play
  characters/        character pictures
  backgrounds/       scene pictures
  audio/             music and sound effects

There is no editor to learn and no project file to keep in sync. You write in any text editor, press one key, and the player shows the change.

What you write

Most of your time goes into script.txt. It reads like a script for the stage or screen:

[bg street fade 0.5]
[hana left]
HANA:
You came back.

> Of course I did. -> stay
> I'm just passing through. -> leave

Who speaks, what they say, where the scene goes next. The bracket lines are stage directions: show this place, bring in this character.

When a scene needs more — a precise camera move, a custom menu, a character with blinking eyes — you add a small settings file next to the script. The script stays readable.

What the player does

Everything a player expects from a visual novel is already there: text that types out, a name plate, choices, save and load, stepping back through lines, a history log, title and pause menus, a settings screen, and builds for desktop, web and phones.

You do not build these. You decide how they look and when they appear. The next page lists them: What VN-Mikro handles for you.

Who it is for

VN-Mikro is made for story-first games: kinetic novels, dialogue-heavy stories, branching romances, mysteries. If your plan is “write the scenes, then dress them”, you are in the right place.

It is not a general game engine. If your game is mostly a custom minigame with some dialogue around it, a full engine suits you better.

The spirit is closer to classic Japanese script engines (think KiriKiri / KAG) than to a full game IDE: plain script, data beside it, player runs the folder. Those tools went quiet; the habit of writing VNs as text didn’t.

Built on a proven engine

The player is built with the Godot engine, so it runs smoothly on Windows, macOS, Linux, the web and Android. You never open Godot yourself — you only work with your project folder.

Next

What VN-Mikro handles for you

What VN-Mikro handles for you

Every visual novel needs the same hundred small things before a single player enjoys it. Most of them are invisible when they work and painful when they do not. VN-Mikro ships them ready, so your work is the story.

Below: what the player does on its own, and what you can change when you want to.

Reading the text

Handled for youYou can change
Text types out letter by letterSpeed, or other ways to reveal text (soft fade, pop, rise)
Click finishes the line; next click moves on—
Holding the advance key speeds text upThe fast speed
Pauses inside a line, then continueWhere the pauses go
A short voice beep while text types, with slight pitch changes so it never sounds roboticThe sound, pitch range and volume — or turn it off
Bold, italic and coloured words—
Auto-advance after a lineDelay per line, or a default
Readers can change text speed, auto-advance and text size in SettingsWhether those rows appear

Who is speaking

Handled for youYou can change
The name plate shows the speaker’s nameIts look, position, or a custom name like “???”
Text appears in a dialogue panelExtra panels, whisper looks, what quiet panels do — Dialogue panels
The speaker is highlighted; others step backHow strong the effect is
Mouths move while a character talks, eyes can blinkWhich pictures move, and how fast

Characters and scenes

Handled for youYou can change
Characters get a place on stage (left, centre, right) without setupNumber of places, their size and position
Moving a character frees the old spot—
Changing an expression blends smoothlyBlend style
Characters and backgrounds fade in and outFade, slide, timing
The picture scales to any window sizeBase resolution

Choices and input

Handled for youYou can change
Choice menu with numbers and keyboard / mouse controlLook, numbers on or off, order shuffled
Choices that show or grey out depending on earlier decisionsThe conditions
Timed choicesThe timer
Asking the player to type a nameLook and default text

Saving and going back

This is where most homemade visual novels break. VN-Mikro keeps it stable.

Handled for youYou can change
Autosave, and Continue on the title screen—
Save and load in slotsNumber of slots (save_slots); you build the slot menu — polished slot UI still early — Save, Back and story end
Loading brings back the whole scene: background, characters, music, effects, looks—
Back steps through earlier lines, and the scene rewinds tooOn or off, how far back, whether text retypes
History log of everything saidWhere it appears
Settings (volume, fullscreen, language…) are kept separately from savesDefault values on first launch
Old saves that no longer match the story are detected, not silently brokenThe message shown
Save files cannot be casually edited in released builds—
“Saved” and “Loaded” messagesText and duration
Handled for youYou can change
Title, pause and settings screensButtons, layout, pictures — or your own screens
Settings rows for music, sound, voice beep, text speed, auto-advance, text size, fullscreen, languageWhich rows appear and how they look
A small in-game bar (pause button, etc.)Its contents and position
Credits after the last line, then back to titleWhich screen, and whether to return
Music on the title screen that fades in and out, and does not restart when the same track plays againTrack, volume, fade times
Button click and hover soundsSounds and volume
A custom mouse cursor that hides while the player readsPictures, when it hides
All keys can be remappedDefault keys

More than one language

Handled for youYou can change
Language switch in SettingsWhich languages are offered
Text and character looks per languageThe translations

Checking your work

While you write, the author player helps you catch mistakes before players do:

Handled for youYou can change
Reload the story with one key and stay where you are—
Check the whole project: broken jumps, branches that can never happen, missing characters, backgrounds, sprites or screens—
Jump to any scene, with the right flags set on the wayWhich flags to set first
See and edit flags while playing—

Shipping the game

Handled for youYou can change
Builds for Windows, macOS, Linux, web, Android (install file and Play Store)Which platforms you build
Your icon converted to each platform’s formatThe icon picture
Game name on the window, the app file and the build folderThe name and version
Start-up splash on desktop, web and AndroidLogo, background, colours
Author tools and debug keys removed from player builds—
Files your game does not use are left outTurn it off if you need them

What it does not do (yet)

A few things other tools offer are not in VN-Mikro today: skipping already-read text, gamepad controls, and picture thumbnails on save slots.

Multi-scripting (several story packs / scripts in one project) and the Save/Load menu polish (slots work; finished UI still early) — details: In development.

Next

In development · How you work

In development

These are planned or only partly finished. Do not build your whole game around them yet.

Save and load menus

Saving, loading, autosave, and Continue already work. session.save_slots sets how many manual slots you get (1–99). You build Save / Load screens yourself in screens.json.

What is not ready: a polished, finished slot UI (layout, picture thumbnails, the overall Save/Load experience authors expect out of the box). Slot count and early screens are usable tooling; the menu finish may change.

Until that lands, keep Save/Load screens simple. Details while setting the count: Save, Back and story end.

Multi-scripting (several stories in one project)

One project can already list more than one story pack in game.json (stories: each pack has its own folder and script.txt). Saves know which pack you were in. Unlock flags can gate the next episode.

What is not ready: a polished story-select / campaign hub. Today you wire that menu yourself in screens.json. Labels, hub layout, and the overall flow may change.

Until that settles, prefer one story/ folder and one script for a shipped game. Treat multi-pack as early tooling, not a finished feature. Keys and hub wiring: Several stories in one project.

3D models (Blender → the stage)

The direction: use Blender models (GLTF) as characters, props, and eventually backgrounds — not only flat PNGs. Named animation clips play from the story (idle loops, one-shot gestures).

Parts of this already exist in experiments (characters and sprites can mount a model; clips can run and stop). What is not ready for handbook authors: a stable, documented way to export, place, and light models in a normal project folder; full background / room integration; and a finish that matches the 2D path for save, shaders, and interaction.

Until that lands, ship with PNG and layered 2D characters. Treat 3D as upcoming, not as something to depend on for a release.

Character idle (2D layered packs)

The direction: soft breathe / sway / bob on layered character packs so standing characters feel alive without a new expression every beat. Pack settings would choose an idle motion; talking and focus reactions stay separate.

This may already appear as early pack options in some builds. What is not ready: a calm, final set of motions, tuning that looks good at every resolution, and clear docs for when idle fights with expression swaps or talking mouths.

Prefer still poses and your own blink / mouth layers today. Idle motion is polish on top — not required to finish a story.

Stage locations (richer rooms)

The direction: backgrounds that behave more like places than single pictures — when the room resets between beats, layered background packs, clickable hotspots in the room, and a simple room camera (pan / focus) driven from the story.

Schema and tests for a stage-style flow are under work. What is not ready: author handbook pages, a finished Mode A spelling for everyday use, and a player-facing room editor. Clickable props today are still normal sprites / objects / screen hooks.

Keep using [bg …] and stage sprites for shipped games. Stage locations will extend that world later, not replace your current backgrounds overnight.

Video on the stage

The direction: play video as stage pictures — sprites / layers you place like any other prop — with simple controls from the story (play, pause, seek, loop, stop).

Nothing here is handbook-ready yet. Ship with still images and flipbooks (frames) for motion. Treat video layers as upcoming polish, not a release dependency.

Blend modes (sprites and layers)

The direction: choose how a sprite or character layer blends with what is behind it. A small set of common modes ships first (the usual multiply / add / screen family). Extra Photoshop-style looks would come from small built-in shaders you pick by name, not from a free-form editor.

What is not ready: author keys, a finished way to write it in the script, and docs for when blend mode fights with existing shaders or save/restore. Today, tint and opacity are the safe tools — Sprites and objects · Shaders.

Not scheduled here (but missing today)

Skipping already-read text, gamepad controls, and picture thumbnails on save slots — listed under What VN-Mikro handles for you. Thumbnails sit with the unfinished Save/Load UI above.

Panel height ceiling and body scroll for dialogue text are available now (theme max_height / sticky panel max_height). Choice-list scrolling inside a capped plate is still not a separate feature — a choice list taller than the ceiling can still clip.

Next

How you work

How you work

Making a game with VN-Mikro is a short loop you repeat all day:

write  →  play  →  reload  →  check  →  …  →  build

The two players

Author playerYour released game
Who uses itYouYour readers
Opens any project folderYesNo — it contains one game
Reload and debug keysYesNo
Build buttonYesNo

You write and test in the author player. When the story is ready, you build your released game from it.

Write

Open your project folder in any text editor. Almost everything is in:

  • story/script.txt — the story itself
  • game.json — your game’s name and settings
  • picture and sound folders

Save the file. That is all “writing” means here — there is nothing to export.

Play and reload

KeyWhat it does
F5Reload the story and pictures, and keep your place (also reloads native extensions in author mode)
F6Reload only the look (theme, menus)
F2Open a different project folder
F12Open the author tools

Edit a line, press F5, see it. You do not restart the game to test a change.

Check

F12 opens the author tools beside the game. Full tab list (Meta, Nodes jump modes, Vars, Validation, Prefs, Build): Author tools.

Run Validation before you share a build.

Build

When you are ready, the Build tab packs your game for Windows, macOS, Linux, the web or Android. You get one folder per platform to give to players. Details: Build and hand off · Author tools.

How to read these docs

  • Parts 1–7 in the sidebar follow the order you build a game in. Read them top to bottom the first time.
  • Reference at the bottom is for looking things up once you know what you want.

You do not need to read everything. Most games use only a small part of what is here.

Next

What is JSON — then Reusing settings — then Your first ten minutes

Author tools (F12)

While you are writing, the author player includes a debug overlay. Use it to inspect the running story, jump around, edit flags, validate the project, and open Build.

Player builds you ship do not include this overlay (and they strip the F1–F12 author shortcuts).

Open and close

Press F12 (default; remappable in keybindings.json as the debug toggle action).

Prefs and Build choices are remembered in vn-mikro-debug.cfg next to the player.

Quick keys outside the overlay: How you work (F5 / F6 / F2).

Tabs

Meta

Current node summary, optional raw JSON, and quick controls. The compact status line starts with vn-mikro and the runtime version (separate from your project’s game.json → version).

ControlWhat it does
Prev / NextRollback one history step / leave this node like normal play
Force prevJump to the previous node in the graph, ignoring play history
AdvanceSame as Continue (finish typing or a pause tag)

Nodes

List of story nodes. Sort by story order or id. Jump with:

ModeBehaviour
simulateWalk a path to the target and rebuild flags/visuals along the way. Falls back to force if no path is found
forceReconstruct along a path when possible; otherwise enter the node on a blank slate

You can seed flags before a jump. Jump history backs and forwards through recent targets.

Vars

Live flags: sort by name, type, or recent change. Set, add, remove, or reset values while playing.

Validation

Run project checks and browse warnings/errors with detail text. Useful before a build. Refresh includes broken links, flag-aware reachability (branches that can never run), and missing character / background / screen / sprite refs for the active story pack. Activate a row with a node id to jump there.

When a check fails, the Validation tab label turns red and shows the error count, so you can spot a broken project without opening the tab. F5 re-reads your project files and reruns the checks — fix a missing comma or bracket, press F5, and the error and the red label clear. The Refresh button reruns the same checks without reloading gameplay.

Prefs

OptionEffect
Pause while openFreeze gameplay while the overlay is open
Show FPSFPS readout
Perf logPerformance logging
Sprite TracerDraw outlines on sprites, character slots, choices, and screen sprites. Kind filters (Sprites / Chars / Choices / Screen sprites) sit under this toggle. Outlines stay on; labels appear on hover. Overlapping same-size layers (for example expression sheets) list under the pointer, top layer first. A sprite or object with shader.margin shows the authored rect solid and the padded glow area dashed
Composition LayersExtra composition-layer outlines on the tracer (needs Sprite Tracer on)
Switch ProjectOpen the project picker (F2 by default)
Author RefreshReload story/assets and restore current play state (F5). Also reloads declared native extensions in author mode
UI Refresh OnlyReload themes/screens/panel without rebuilding gameplay (F6)
Width / DockNarrow or normal (default); dock left or right

While the overlay is open, the software cursor is suppressed so the OS pointer stays usable on the tools.

Build

Visible only in the author player. Packaging steps, modes, and what to ship: Build and hand off.

Author vs player

Author playerPlayer build
Open any project folderYes (picker)No — ships with one game
Debug overlay (F12)YesNo
Hot reload (F5 / F6)YesNo
Build tabYesNo

You write and test in the author player. When the story is ready, build from the Build tab and distribute that output.

How these docs work

Three layers, one sidebar. You do not need every layer on day one.

Guide (Welcome through Ship)

Short pages in story order: write → set up → direct → stage → look → polish → ship. Read top to bottom the first time. Later, jump in from search or the sidebar.

Reference

When you already know what you want:

LayerJob
File mapsWhole file at a glance; every key clickable
Detail pagesFull tables (commands, story, screens, theme, …)
Shared shapesTransitions, textures, binds, hooks — once, linked everywhere
Everything A–ZFlat name list — browser search (Ctrl+F)

Guide pages teach when; maps and detail teach every key.

Examples vs docs

examples/Playable, incomplete on purpose — teach a slice
DocsComplete — teach the surface

If an example does not use a key, the key still exists. Check the map or detail page for that surface.

Author tools

Overlay, jumps, flags, Validation, Build: Author tools · Build and hand off.

Checking a project

The author kit can validate your folder (broken jumps, missing assets, schema). Prefer that over guessing from memory — Reference.

What is JSON

JSON is a way to write structured notes in a plain text file. The player reads those notes. You do not “run” JSON like a program — you fill in names and values that the docs list.

If you already know forms and spreadsheets, you are closer than you think: names are column headers, values are cells, and groups of fields are like a row under a heading.

A tiny example

{
  "title": "Cold Down",
  "window": {
    "width": 1280,
    "height": 720
  }
}

In words:

  • Curly braces { } wrap a group of named fields.
  • "title" is a name (always in double quotes).
  • "Cold Down" is the value for that name.
  • "window" is another group nested inside the first one.

Commas separate fields. The last field in a group has no comma after it.

The shapes you will see

ShapeLooks likeMeans
Text"hana"Names, paths, dialogue keys
Number0.5, 14Amounts, seconds, sizes
On / offtrue / falseA switch (always lowercase)
Empty / clearnullNone — for example stop music, or clear a setting
Group{ "a": 1, "b": 2 }Named fields together
List[ "a", "b" ] or [ {…}, {…} ]Items in order

Colours are usually text like "#343a52e0". Paths are text relative to your project folder.

Why VN-Mikro uses it

The script is great for lines and flow. JSON is better when many small settings must stay together and stay checkable:

  • Your game’s name and defaults (game.json)
  • How the dialogue box and menus look (themes/)
  • Title, pause and settings screens (screens.json)
  • Precise direction on one moment (story/story.json)
  • Layered characters, choice looks, cursors

You do not write JSON for every line of dialogue. Most of the story stays in script.txt. Settings files hold everything that is not a spoken line.

The file maps show every allowed name in a file. Stick to those names so projects stay stable and easy to edit later — including with help from an assistant.

Common mistakes (and how they feel)

MistakeWhat happens
A missing comma, or a comma after the last fieldThe file is skipped and defaults apply; author tools report the file and line of the problem
Missing quotes around a nameInvalid JSON — same report with file and line
True / False (capital T/F)Use true / false
Comments (// or #)Standard JSON has no comments — put notes in a README instead
Single quotes '…'Use double quotes "…"

A good editor underlines broken JSON. The author tools (F12 → Validation) and project check also name the file and line for broken settings JSON (game.json, themes, screens, characters, story overlays).

You do not need to memorise it

Copy an example from these docs or from examples/, change the values, reload (F5). Open a settings page only when you need a key.

When one look or expression should start from another and change only a few things, see Reusing settings.

Next

Reusing settings · then Pictures, sound, and folders · then Your first ten minutes

Reusing settings

You often want this: start from something that already works, change only what differs. VN-Mikro does that in several places. The idea is the same; the words and rules are not. This page keeps them apart.

When a later chapter says extends or source, it will point at one row of the table below — not at “inheritance” in general.

The kinds (start here)

What you reuseWhere you write itHow you say itWhat happens
A character expressioncharacters/<id>/*.json"extends", "override", "add"Copy another expression’s layers, then patch layers or append new ones
A dialogue-box lookthemes/… → panel.<name>"extends"Merge another panel preset’s keys
A button / choice / name-entry looka pack file under themes/"source" or "style"Load that pack, then merge your local keys on top
Splash per platformgame.json → splash.web / android / desktopnested blocksPlatform block merges over the shared splash root
A whole mood for a scenestory command theme_styleprofile nameThat theme file merges over the base theme
Text or looks per languagelocale overlays / string catalogslocale filesLanguage layer sits on top of the base

Objects under sprites/*.obj.json do not reuse another object this way. They are a flat list of parts; you change an instance with update and live links to flags. See Sprites and objects.

One example per kind

Character expression (layers)

"default": [
  { "path": "base.png" },
  { "path": "mouth.png" }
],
"happy": {
  "extends": "default",
  "override": [{ "layer": 1, "path": "mouth_happy.png" }]
}

happy starts as default, then replaces layer 1. "add" would append extra layers. This is not the same tool as panel "extends".

Dialogue-box look (keys)

"panel": {
  "default": { "width": "100%", "bg": "#1e2133e0" },
  "whisper": {
    "extends": "default",
    "width": "60%",
    "bg": "#00000099"
  }
}

whisper keeps everything from default except the keys you set. Full story: Theme and the dialogue box.

Pack look (source / style)

{ "style": "choice_plain" }
{ "source": "choice_plain", "gap": 12 }

"style": "name" means the same as "source": "name". The pack loads from themes/<name>.json, then your local keys merge on top. Used for buttons, choices and name-entry — not for character packs, cursors, or name-plate style keys.

Splash per platform

"splash": {
  "logo": "splash/logo.png",
  "web": { "logo_max_width": 280 },
  "desktop": { "min_seconds": 0.6 }
}

Shared keys at the root; web / android / desktop only change what that platform needs. Details: Title, icon, splash.

Shared merge rule (deep merge)

These places merge key by key (panel extends, pack source, splash platforms, scene mood files):

In your fileEffect
Key omittedKeep the base value
Key presentUse yours
nullClear the base value
A list (array)Replace the whole list — not item by item

Character expression "extends" is different: it works on layers (override / add), not this key table alone.

Do not mix these

Easy mix-upReality
Character "extends" vs panel "extends"Same English word; different tools. Characters = layers; panels = keys.
"source" / "style" vs character packsPack compose is for controls. Character art uses its own pack files and expression extends.
Sticky story overrides (theme_style, choice_style, node panel)“Use this until I clear it” during play — not a JSON extends between files.
Defaults in game.jsonProject defaults (typing, layout, session). They are not inheritance between files.
Object partsNo object-to-object extends. Parts are edited with update / binds.

When you need this page

  • First time you copy a look and change one field → read the matching row above.
  • Later chapters link here instead of re-explaining merge rules.

Glossary: Words used in these docs.

Next

Pictures, sound, and folders · then Your first ten minutes

Pictures, sound, and folders

Lives in: your project folder

The script names people and places. Those names only work if the files exist. The simplest game still needs pictures (and usually a little sound) on disk — not only script.txt.

This page is the map. Deeper character packs come under Characters; props under The stage.

A tiny project that can run

my-game/
  game.json                 required — name and defaults
  story/
    script.txt              your play
  characters/
    hana/
      hana.png              default look
      hana_happy.png        optional expression
  backgrounds/
    street.jpg              a place
  audio/                    optional at first
    bgm/
    sfx/
  themes/
    default.json            dialogue-box look (copy from an example)
  screens.json              title / pause (copy from an example)

Copy examples/example-00-boilerplate or example-01-hello-vn and replace pictures — you do not need to invent game.json from scratch on day one.

Characters = folders of pictures

On diskIn the script
Folder characters/hana/Speaker HANA: and direction [hana left]
hana.png (or hana_default.png)Default look
hana_happy.pngHANA [happy]: or [hana happy left]

Rules that matter:

  • One folder per character id — folder name is the id (hana), case-insensitive in the script.
  • File name = id or id_expression, plus an image extension.
  • You do not need a JSON pack for a first game. One PNG per expression is enough.
  • Layered packs (blinking, talking mouths) are optional later — Characters, Reusing settings.

People often call these “sprites.” In VN-Mikro, character pictures live under characters/. The separate sprites/ folder is for props and gadgets on top of the scene — optional; skip it until you need a lamp, phone overlay, or HUD widget.

Places (background pictures)

On diskIn the script
backgrounds/street.jpg[bg street] or [bg street fade 0.5]

Write the name without the extension. Formats: .png, .jpg / .jpeg, .webp, .svg.

Sound (when you want it)

On diskIn the script / settings
audio/bgm/theme.ogg[bgm theme]
audio/sfx/door.mp3[sfx door]
same sfx/ foldertyping beeps and button clicks in game.json

Formats: .ogg or .mp3. Name without extension in commands.

Title music can also be set once in game.json — Music and sounds by default.

Formats at a glance

KindFolderFormatsNamed in script as
Charactercharacters/<id>/.png .jpg .jpeg .webp .svgid / expression
Placebackgrounds/.png .jpg .jpeg .webp .svg[bg name]
Musicaudio/bgm/.ogg .mp3[bgm name]
Sound effectaudio/sfx/.ogg .mp3[sfx name]

What can wait

Folder / fileWhen you need it
sprites/Props, meters, clickable objects — Sprites and objects
fonts/, richer themes/Custom look — Look and menus
Character *.json packsLayered art, drive — Characters
shaders/, animations/Polish
extensions/Custom extension packs — Native extensions
Extra story packsEarly multi-scripting — In development

Next

Your first ten minutes if you have not run an example yet · otherwise What a script is

Your first ten minutes

You will open a small example, change it, and see your change play.

1. Open the example

  1. Start the author player.
  2. When it asks for a project, pick examples/example-01-hello-vn.
  3. Click through a few lines. Space, a mouse click, or the Continue button moves on.

2. Open the script

Open examples/example-01-hello-vn/story/script.txt in any text editor. You will recognise what you just saw:

[bg street fade 0.5]
Hello! This is the smallest story you can run.

[hana left fade 0.45]
HANA:
Hi - I'm Hana.
  • A line in square brackets is a stage direction: show the street, bring Hana in on the left.
  • HANA: means Hana speaks the lines below it.
  • A line with no speaker is the narrator.
  • An empty line starts the next moment of the story.

3. Change something

Change Hana’s line:

HANA:
Hi! I rewrote this line myself.

Save the file, go back to the player, press F5. Hana now says your line.

Other keys you will use (F2, F6, F12): How you work.

4. Add a moment

Add these lines at the end of the file, after an empty line:

[bg parknchairs fade 0.5]
[hana left fade 0.4]
HANA [closed]:
The park is quieter than I thought.

Press F5 and click through to the end. You changed the place, brought Hana back, and gave her a different expression (closed uses the picture hana_closed.png).

Those pictures live under characters/ and backgrounds/ — Pictures, sound, and folders.

5. Break something on purpose

Change parknchairs to parkchairs (a typo), save, press F5, then press F12 and open Validation. The missing background is listed. Fix the typo and reload.

That is the whole loop: write, reload, check.

Where next

If settings files are new to you: What is JSON and Reusing settings. What those [bg] / [hana] lines load from disk: Pictures, sound, and folders.

Continue with What a script is. The rest of the examples in examples/ add one idea each:

ExampleAdds
example-00-boilerplateAn empty project to copy for your own game
example-02-choices-and-flagsChoices and remembering decisions
example-03-character-compositionCharacters built from layers (blinking, talking)
example-04-presentation-toolboxPer-character panels, focus dimming, a clickable hotspot
example-05-ui-and-polishMenus, HUD, look
example-06-native-extensionCustom extension packs (parallax, click particles)
example-07-phone-and-terminalPhone chat + terminal panels (append, typing, input)
cold-downA complete tiny visual novel

What a script is

Lives in: story/script.txt

A VN-Mikro script is meant to read like a play or screenplay: who speaks, what they say, and short stage directions for the scene. The player turns that text into the game.

You write it in any text editor. Empty lines matter. Square brackets and a few marks do the rest.

Scenes, moments, and lines

WordIn the fileWhat the player does
MomentOne block of text between empty linesRuns that block’s directions, types the text, then waits for a click (or a choice / timer)
SceneA moment with a name: :: parkSomething you can jump to later
LineText under a speaker, or narrator textWhat appears in the dialogue box

An empty line ends the current moment and starts the next. That is the main “cut” in the script.

A short scene, read top to bottom

:: street
[bg street fade 0.5]
[hana left fade 0.4]
HANA:
You came back.

> Of course. -> stay
> Just passing through. -> leave

:: stay
HANA:
Good. Stay a while.
-> END

:: leave
HANA:
Safe travels.
-> END

What happens:

  1. :: street names this moment so other moments can jump here.
  2. [bg …] and [hana …] are stage directions — they run when the moment starts.
  3. HANA: marks who speaks; the next lines are her dialogue.
  4. > lines are choices. Each can jump with -> ….
  5. -> END ends the story.

The marks you will use most

MarkExampleJob
Empty lineNew moment
## noteComment — ignored by the player
:::: parkName this moment
:: start:: startOptional New Game entry (first moment if omitted)
NAME:HANA:Speaker
[…][bg street]Stage direction or flag
>> Stay. -> stayChoice
->-> park or -> ENDJump / end
{…}{score} / {@w}Flag in text, or a pause mark

Everything else builds on these.

Dialogue, narration, directions — together

[bg park fade 0.5]
The park is quiet.

[hana left]
HANA:
Sit with me.
  • No speaker → narrator (or the story’s voice).
  • HANA: → her name plate and focus.
  • Bracket lines → change the stage; they are not spoken aloud.

Those names only work if the pictures exist on disk (characters/, backgrounds/, …) — Pictures, sound, and folders.

What you do not put in the script

Looks (fonts, dialogue-box colour), title menus, and dense production usually live in other files (game.json, themes/, screens.json, sometimes story/story.json). The script stays readable on purpose.

One project normally has one story folder and one script.txt. Several scripts in one game (episodes / packs) is early — In development.

When a moment needs more than brackets can say: When the script is not enough.

Next

Lines and speakers

Open examples/example-01-hello-vn/story/script.txt while you read.

Lines and speakers

Lives in: story/script.txt

Who talks, how their name appears, and how each line waits for a click.

Speaker marks at a glance

WriteEffect
HANA:Speaker = folder characters/hana/
HANA [happy]:Same + stage that expression
???|HANA:Plate shows ???; talking still hana
HANA & SORA:Both in focus
HANA (silent):Same speaker; typing mouth / beep off (see below)
HANA [happy] & SORA (silent):Per name: expression and/or silent

These can combine, for example ???|HANA [happy] (silent): or ???|HANA [happy] & SORA (silent):.

Speakers

HANA:
Hello.

HANA: (any case) maps to the folder characters/hana/. The name plate usually shows that character’s display name. What to put in that folder: Pictures, sound, and folders.

Several lines under the same speaker stay in the same moment until an empty line:

HANA:
First click.
Still Hana — same moment, another click.

Expression while speaking

HANA [happy]:
Good morning.

That looks for characters/hana/hana_happy.png (or the pack’s happy look). Folders and naming: Pictures, sound, and folders. Details later: Characters.

Narrator

A line with no NAME: is narrator voice — no name plate from a character:

The street is empty.

Unknown name / custom plate

Show a different name on the plate while still using a character folder:

???|HANA:
Who's there?

Left of | is the plate text; right is who is talking (for focus and assets).

Two speakers at once

HANA & SORA:
Together:

Both are in focus for that line. One name plate covers the group (auto names, or a custom Label|… for the whole line). The line uses the routed panel for this moment — usually the first speaker’s preferred panel, unless you set panel_id in data — How text finds a panel.

Each name can take its own [expression] and/or (silent):

HANA [happy] & SORA (silent) & DADDY [sad] (silent):
They speak as a group.
SegmentEffect
HANA [happy]In the group; stage happy; mouth on while text types
SORA (silent)In the group; mouth off
DADDY [sad] (silent)In the group; stage sad; mouth off

To silence everyone, mark each name: HANA (silent) & SORA (silent):.

Silent line (no talking mouth)

HANA (silent):
(She doesn't answer.)

Same speaker and plate; typing mouth (and beep) stay off. Only the literal (silent) mark — not free-form (anything). Mouths in depth: Talking and focus.

Style inside a line

BBCode works in dialogue:

HANA:
That was [b]not[/b] the plan.
[color=#ff8888]Careful.[/color]

Unknown [word] tags that are not commands stay visible as text. To show real brackets: [lb] / [rb].

HANA:
Your score is {score}.

Curly braces insert the current flag value. To print a literal {, write {{.

Usually you need

WantWrite
Someone speaksNAME: then lines
ExpressionNAME [happy]:
Narratorplain lines
Mystery plate???|NAME:
Joint + per-name modsNAME [happy] & NAME (silent):
Silent (no mouth)NAME (silent):
Style[b], [i], [color=#…]
Remembered value{flag}

Next

Stage directions

Stage directions

Lives in: story/script.txt

Bracket lines change the scene. They are not spoken. They run when the moment starts — unless they appear after dialogue has already begun, in which case they fire mid-line as {@cN} cues; see Pacing.

[bg street fade 0.5]
[hana left fade 0.4]
[bgm city_loop]
[sfx door]

Places

[bg street]
[bg street fade 0.5]
[bg park slide_left 0.6]

Loads a picture from backgrounds/ (extension resolved for you). Optional fade / slide and duration. Naming: Pictures, sound, and folders.

More: Backgrounds.

Characters

[hana left]
[hana happy left fade 0.4]
[hana hide]
[sora right fade 0.3]
  • First word is the character folder name (characters/hana/).
  • left / center / right are places on stage (defaults; you can rename them later).
  • An expression word (happy) and a fade are optional.
  • hide removes them from their place.

Speaking with HANA [happy]: also stages that expression. Bracket lines are for show / move / hide without a line of dialogue.

Pictures on disk: Pictures, sound, and folders. Packs and places: Characters.

Music and sound

[bgm theme]
[bgm theme fadein 1.5]
[sfx door]

Files live under audio/bgm/ and audio/sfx/. Title music defaults belong in game.json — Music and sounds by default. Mid-story cues: Music and sound.

Flags (also brackets)

[score = 0]
[helped = true]
[score += 1]

Same bracket shape, different job: memory, not staging. Full use: Choices and memory.

Why several directions feel “instant”

All enter directions in one moment run as a batch when that moment starts. That is why two [hana …] and [sora …] lines can appear together. To time something with the spoken line, put the bracket after text has started — Pacing ({@cN}).

Comments

# This is only for you — the player ignores it

Usually you need

WantExample
Change place[bg street fade 0.5]
Show character[hana left fade 0.4]
Hide character[hana hide]
Music / SFX[bgm theme] / [sfx door]
Set a flag[helped = true]

Keyword directions also include things like panel, screen, animate — look them up in Reference when you need them. Character folder names without a keyword are treated as show/move/hide for that character.

Next

Scenes and jumps

Scenes and jumps

Lives in: story/script.txt

Name moments and move between them so the story can branch and meet again.

Name a scene

:: park
HANA:
We're here.

:: park sets this moment’s id to park. Use letters, numbers, and underscores. Every jump target must match an id (or the special end).

If you omit ::, the player still builds a moment — it just has no stable name to jump to. Named scenes are how you build routes.

Where New Game starts

New Game looks for a moment named start (JSON: "id": "start"). In the script that is:

:: start
[bg street fade 0.5]
HANA:
Good morning.

You can omit :: start. The player then uses the first moment in the file. Small examples do that on purpose. Name it when you care about a stable entry (saves, jumps to START, or a pack that sets a different entry).

For several stories in one project, each pack can set its own entry id with stories[].start — Several stories.

Jump

-> park

alone on a line (or after text in some setups) sets where to go when this moment finishes. Common pattern:

HANA:
Let's go.
-> park

End the story

-> END

Ends play (then credits / title follow your session settings — Save, Back and story end).

Branch and merge

:: choice
> Left path. -> left
> Right path. -> right

:: left
HANA:
Left.
-> after

:: right
HANA:
Right.
-> after

:: after
HANA:
Both roads meet here.

Both branches jump to the same after moment. That is how routes rejoin.

Visit a beat and come back

For a reusable insert (a menu of facts, a short cutaway), use call and return:

[call encyclopedia]

…and at the end of that scene:

[return]

call remembers where you were; return goes back. Prefer plain -> for normal story flow; reserve call/return for side visits.

Conditional jump

[if helped -> thank_you]

If the expression is true, go there instead of the normal next moment. Expressions use flag names and && / || / ! — Choices and memory.

Order tips

  • Give every important moment a :: id early — renaming later can break old saves.
  • Keep merge points (:: after) obvious in the file.
  • One -> per moment for the default exit; choices carry their own targets.

Usually you need

WantWrite
Name:: park
New Game entry:: start (optional — first moment if omitted)
Go there-> park
End-> END
Choice target> Text. -> park
Side visit[call id] … [return]
If flag[if helped -> thank_you]

Next

Choices and memory

Try examples/example-02-choices-and-flags/story/script.txt.

Choices and memory

Lives in: story/script.txt

Let the reader decide, and remember what they decided.

Open examples/example-02-choices-and-flags/story/script.txt while you read.

Flags

A flag is something the story remembers for the rest of the playthrough (and in saves).

[score = 0]
[helped = false]
[player_name = 'Hero']

Change later:

[score += 1]
[helped = true]

Print in dialogue:

HANA:
Score is {score}. Helped is {helped}.

Strings use single quotes inside the bracket: [some_text = 'hello'].

Choice menus

:: park
HANA:
What do you do?
> Help her. -> help_branch
> Stay back. -> skip_branch

:: help_branch
[helped = true]
HANA:
Thanks.
-> after

:: skip_branch
HANA:
Alright.
-> after

:: after
HANA:
helped is {helped}.
  • Each > line is one option.
  • -> label is where that option goes.
  • A bare > with no arrow continues to the next moment in file order (NEXT).

Look and shuffle defaults live in game.json — Choice and character layout. Rich choice rules (show only if…, disable if…) need the direction file — Adding direction.

Branch on a flag

[if helped -> thank_you]

or, with more cases in data form, jump_if — same expression language. Use &&, ||, ! (not the words and / or).

[if score > 3 && helped -> good_ending]

Ask for a name

[input player_name "Your name?" default "Hero"]

The field starts empty with ghost text Hero. Enter with nothing typed stores Hero; or the reader types a name. On confirm the flag player_name is set. To start with real text the reader can edit, use prefill instead of default:

[input player_name "Your name?" prefill "Hero"]

Then:

HANA:
Nice to meet you, {player_name}.

More options (max_length, pattern, …): Reference when you need them. Look of the field: Choice and input looks.

Usually you need

WantWrite
Remember[flag = value] / += / -=
Show value{flag} in text
Menu> Caption. -> target
Gate a jump[if expr -> target]
Name entry[input flag "Prompt?"]

Try this

  1. Add a third choice that does [score += 2].
  2. Print {score} after the branches meet.
  3. Play twice and pick different options.
  4. After the branches meet, ask for a name with [input] and use {player_name} in an ending.

Next

Pacing

Pacing

Lives in: story/script.txt

Control the rhythm of reading: pauses, speed, auto-advance, and fades when leaving a moment.

Soft pause in the line

HANA:
Wait…{@w} there.

{@w} waits for a click (skipped when the reader holds fast-forward). Timed pause:

HANA:
One second…{@w=1.0} go.

Clear the page, then continue

HANA:
First page.{@p}Second page.

{@p} pauses and clears the dialogue box on resume. It is not skipped by fast-forward.

Change speed mid-line

HANA:
Slow.{@cps=8} Faster now.{@cps}

{@cps=N} sets letters per second; {@cps} resets to your project default (Text speed).

Auto-advance a moment

After the text finishes, move on without a click:

HANA:
No click needed.
[auto 2]

[auto 2] means “auto-advance after 2 seconds.” Ignored when the moment is a choice menu.

Exit directions (leave cleanly)

Run directions when leaving this moment — useful for fades out before the next place enters:

:: goodbye
HANA:
See you.
[exit: hana hide fade 0.4]
[exit_delay 0.5]
-> street
  • [exit: …] — a stage direction that runs on leave (same language as enter brackets).
  • [exit_delay 0.5] — wait after those exit directions before the next moment.

Enter directions still run at the start of the next moment as usual.

Directions in the middle of a line

A bracket after spoken text has already started fires when typing reaches that word — not when the moment begins:

HANA:
I was fine.
[sfx click]
Then I wasn't.

The script compiler inserts an invisible {@c1} (then {@c2}, …) at that spot and stores the direction in flow_commands. Typing does not pause; the command runs as the cursor passes the mark.

Prefer {@w} / {@cps} for timing-only. Use a mid-line bracket when something must fire at a word. In Mode B / JSON you can write {@c1} in text yourself and fill flow_commands — Command flow. Dense cases: Directing.

Usually you need

WantWrite
Click pause{@w}
Timed pause{@w=1.0}
New page{@p}
Speed change{@cps=12} / {@cps}
Fire a direction at a wordMid-line […] (becomes {@cN})
Auto-advance[auto 1.5]
Fade out on leave[exit: …] + [exit_delay …]

Try this

  1. Put {@w} in the middle of a line; click to resume.
  2. Put [sfx …] after a spoken line has started; hear it fire mid-type.
  3. Add [auto 1.5] on a short narrator moment.
  4. Hide a character with [exit: …] into the next scene.

Next

Writing techniques

Writing techniques

Lives in: story/script.txt

Common visual-novel patterns, written in the script. Steal these shapes; change the words.

Inner voice vs spoken line

Use a plain line (no NAME:) for narrator voice and thoughts; a normal speaker line for speech:

The letter feels heavier than it should.

HANA:
I'm fine.

Or keep the same character, silent, so the mouth does not move (how (silent) works):

HANA (silent):
(Don't say it.)

HANA:
Everything's fine.

Flashback with a different look

A simple script-only flashback: change place, maybe hide the present cast, mark the voice, then jump back.

:: present
[bg street]
HANA:
I keep thinking about that day.
-> flashback

:: flashback
[bg park fade 0.8]
# Optional: switch dialogue-box look for this stretch — see Look / theme_style
Three years earlier.

HANA [closed]:
We should have left sooner.
-> present_return

:: present_return
[bg street fade 0.8]
HANA:
…Anyway.

For a lasting mood change (dimmer panel, different colours), use a second panel preset, a sticky panel command, or a theme_style mood file — Theme and the dialogue box · How a panel looks.

Phone or chat messages

Treat the phone as narrator lines or a dedicated “speaker” folder with its own pictures:

[phone center]
PHONE:
hey, you free?

HANA:
In a minute.

Or plain narrator with style:

[color=#88aaff]hey, you free?[/color]

Free-floating bubbles and custom layouts need direction data — Sprites and objects, Adding direction.

Split into routes, then join

:: fork
> Trust her. -> route_a
> Walk away. -> route_b

:: route_a
[trusted = true]
HANA:
Thank you.
-> join

:: route_b
[trusted = false]
HANA:
…Alright.
-> join

:: join
HANA:
Either way, we keep moving.
[if trusted -> trusted_extra]
-> after_join

:: trusted_extra
HANA:
I'm glad you stayed.
-> after_join

:: after_join
…

One merge moment (join) keeps the file readable. Optional extras use [if …].

A scene the reader can revisit

Share one labeled moment from several places with call / return:

:: hub
> Ask about the park. -> ask_park
> Leave. -> leave_hub

:: ask_park
[call park_info]
-> hub

:: park_info
HANA:
It's quieter after dark.
[return]

Normal story flow still uses ->. Call/return is for side visits that should resume where you left off.

Name entry at the start

:: start
[bg street fade 0.5]
What should we call you?
[input player_name "Your name?" default "Hero"]
-> intro

:: intro
HANA:
Nice to meet you, {player_name}.

Put name entry before the first long scene so {player_name} works everywhere after.

Where to go next

You can draft a whole game with only this part. When you need project defaults (text speed, saves, Back): Setting up your game. When brackets are not enough for one moment: Directing a scene.

The game.json file

Lives in: game.json

Every project has one settings file in the project root. The player reads it when the game starts. Anything you leave out uses a sensible default.

A minimal file

{
  "title": "My Novel",
  "window": { "width": 1280, "height": 720 }
}

That is enough to open a named window at a base size. Story text, choices and pictures still come from story/ and your asset folders.

A fuller starter (still small):

{
  "title": "My Novel",
  "version": "0.1",
  "window": { "width": 1280, "height": 720 },
  "typing": { "cps": 28 },
  "session": { "allow_back": true }
}

See examples/example-00-boilerplate/game.json and examples/example-01-hello-vn/game.json.

What most games touch

BlockJobPage
title, window, version, authorsName and windowName, window, language
typingHow text appearsText speed and voice beeps
soundsTitle music, beeps, UI clicksMusic and sounds by default
sessionSaves, Back, title / endSave, Back and story end
layoutChoice menus and character placesChoice and character layout

Later parts of the handbook cover the rest (transitions, cursor, splash, screens look, and so on). Those keys still live in this same file. Advanced: custom extension packs under extensions.

Full map

Every allowed key in one view: game.json file map. Click a key to open the page that teaches it.

Usually you need

WantDo
Name the game"title": "…"
Set design size"window": { "width", "height" }
Faster / slower text"typing": { "cps": … }
Allow rollback"session": { "allow_back": true }

Next

Name, window, language

Name, window, language

Lives in: game.json

What the window is called, how large the picture is drawn, and which language the game starts in.

Title and version

{
  "title": "My Novel",
  "version": "1.0.0",
  "authors": ["Your Studio"]
}
KeyEffect
titleName on the window, app file, and build folder
versionYour game’s version (used in build names)
authorsCredits; also the Windows app’s company field

To take the title from a translations file instead, use title_key — Translations.

Window size

"window": {
  "width": 1280,
  "height": 720
}

This is the base size your art and layouts are written for. The player scales the picture to whatever window or screen the reader uses.

Starting language

"locale": "en"

That is the language the game opens in. Offering several languages in Settings is i18n — again Translations.

App icon

"icon": "icon.png"

A picture in the project folder, converted for each platform when you build. Details: Title, icon, splash.

Usually you need

WantWrite
Window name"title": "…"
Design size"window": { "width": 1280, "height": 720 }
Credits / company"authors": ["…"]

Full key list: game.json map.

Next

Text speed and voice beeps

Text speed and voice beeps

Lives in: game.json → typing

How dialogue letters appear, and the optional voice beep while they type. Mid-line pauses and speed tags stay in the script — Pacing. A panel preset can override most of these keys for one surface — Panel.

Letters per second

"typing": {
  "cps": 28,
  "cps_fast": 100
}
KeyEffect
cpsLetters per second (0 = whole line at once)
cps_fastSpeed while the reader holds advance / fast-forward
instantAlways show whole lines (true wins over a positive cps)
speed_scaleOverall multiplier (readers can change this in Settings)
auto_advance_defaultSeconds after a line before moving on; 0 = wait for a click

If you omit cps, the player can use delay / delay_fast (seconds per letter) instead. speed_scale and auto_advance_default are Settings-owned — they cannot be set on a panel preset.

How letters look

"typing": {
  "cps": 28,
  "reveal": "typewriter"
}
revealLook
typewriterEach letter snaps on (default)
softLetters fade in
fade_lineThe current clause fades as a whole
pop / riseFade plus a small scale or lift

Optional extras (pending_alpha, reveal_soft, caret mark, …) are on the game.json map.

Voice beep

"typing": {
  "beep": {
    "sample": "typing_beep",
    "volume": 0.5,
    "pitch_min": 0.9,
    "pitch_max": 1.1
  }
}
KeyEffect
enabledPlay the beep (false silences this surface / the project default)
sampleSound id under audio/sfx/
volume0–1, multiplied by master × SFX
pitch_min / pitch_maxRandom pitch range

Ids point at files in your audio folders — Pictures, sound, and folders. Readers can turn the beep off in Settings (sounds.defaults.typing_enabled seeds the first launch). Script [typing_sfx …] overrides the sample until reset — typing_sfx.

Line start / end sounds

Optional one-shots when a whole line begins or finishes typing. Useful for phone chat (“incoming message”) without writing [sfx] on every node. Not fired on {@w} pauses, and not on Back / load restore. Uses the SFX bus (master × SFX × volume) — the Typing Sound toggle does not mute these.

"typing": {
  "start_sfx": { "sample": "msg_in", "volume": 0.6 },
  "end_sfx": { "sample": "msg_done", "volume": 0.3 }
}

Same objects work on a panel preset — Typing per preset.

Mouth motion while typing is a character pack feature, not a typing key — Talking and focus. Script (silent) turns it off for a speaker — Lines and speakers.

Usually you need

WantWrite
Comfortable speed"typing": { "cps": 28 }
Instant lines"typing": { "cps": 0 } or "instant": true
Soft fade"reveal": "soft"
Beep sample"typing": { "beep": { "sample": "…" } }
Incoming message ping"typing": { "start_sfx": { "sample": "msg_in" } }

Next

Music and sounds by default

Music and sounds by default

Lives in: game.json → sounds

Title music and UI clicks. The typing beep lives under typing.beep — Text speed and voice beeps. Scene music and one-shot effects in the script are separate — Music and sound.

Title music

"sounds": {
  "main_bgm": "title_theme",
  "main_bgm_volume": 0.8,
  "main_bgm_fade_in": 1.5,
  "main_bgm_fade_out": 1.5
}

main_bgm is a music id from your audio folders. Fade keys are in seconds; volume is 0–1.

UI clicks

"sounds": {
  "button_click": "click",
  "button_hover": "hover",
  "button_volume": 0.5
}

Used for menus, Continue / Back on the dialogue panel, and normal choice buttons (unless a look pack overrides them).

Typing beep

Set typing.beep — Text speed and voice beeps.

First-launch volumes

"sounds": {
  "defaults": {
    "master": 1.0,
    "bgm": 1.0,
    "sfx": 1.0,
    "typing_enabled": true
  }
}

Seeds Settings the first time someone plays. After they save prefs, those win.

Usually you need

WantWrite
Music on the title"main_bgm": "…"
Menu click"button_click": "…"
Quieter UI"button_volume": 0.25

Ids and folders: Pictures, sound, and folders. Full key list: game.json map.

Next

Save, Back and story end

Save, Back and story end

Lives in: game.json → session · save files next to the player · screen actions

A session is one playthrough from New Game / Continue / Load until you return to title (or quit). Progress lives next to the player as manual slots, one autosave, and a separate prefs file.

Early — not final UX. Multi-slot save/load grids and multi-story packs are shipped and usable, but menus, labels, hub screens, and overall player comfort are still a first cut. Prefer the simple one-slot "save" / "load" wiring unless you need the grid or packs — In development.

Most stories

  • Keep pause/title hooks as "save" / "load" for a simple one-slot feel (always slot 1).
  • Or point menus at screen:save / screen:load for a multi-slot picker — define your own save / load screens with the save_slots block; none ship built-in.
  • Title Continue resumes the autosave.
  • Set session.allow_back if you want Back / rollback.
  • Set session.post_story_screen (and usually post_story_return_to_title) for credits after the last line.
  • Changing node ids or restructuring the story can invalidate old saves — that is intentional.

Where this lives

IdeaFile / surfacePath
Back + story-end + slot countgame.jsonsession
Continue / Load visibilityscreens.jsonshow_if: has_autosave / has_save
Slot picker UIscreens.jsonyour own save / load screens + save_slots block
Files on diskNext to the playersee below
{
  "session": {
    "allow_back": true,
    "back_retype": false,
    "back_limit": 0,
    "save_slots": 9,
    "boot": "title",
    "hud_screen": "hud",
    "post_story_screen": "credits",
    "post_story_return_to_title": true
  }
}

"hud_screen": "hud" (default) auto-shows that screens.json entry with role: "hud" when a session starts. Set "hud_screen": "" to disable. Extra HUD mid-story: screen command.

{ "label": "Save", "interaction": { "hooks": { "on_click": "save" } } }
{ "label": "Save game", "interaction": { "hooks": { "on_click": "screen:save" } } }

Players never write save files by hand. Authors configure session and the screen actions that read those files.

session keys

KeyTypeDefaultEffect
allow_backboolfalseEnable Back / history. History is only stored in saves when this is true
back_retypeboolfalseAfter Back: false finishes typing instantly; true retypes the line
back_limitint0Max Back steps per forward stretch; 0 = unlimited
save_slotsint9Manual slot count (clamped 1–99)
default_storystringfirst pack / mainPack id for bare new_game — Several stories
bootstring"title"Cold start: "title" or "new_game" (skip title)
post_story_screenstring""Screen id after the story ends (e.g. "credits")
post_story_return_to_titlebooltrueEnd session and show title (then optionally the post-story screen)
hud_screenstring"hud"HUD id at session start; "" disables

If post_story_screen is empty and post_story_return_to_title is false, the runtime still returns to title.

Map: game.json → session.

Files on disk

FileRole
save-1.json … save-N.json (author) or save-N.sav (player build)Manual slots
autosave.json / autosave.savContinue only
prefs.jsonVolumes, display, UI prefs, last_used_slot, and (when using multi-story) last_story_id / campaign (always plain JSON)
Manual slotAutosave
Written byPlayer Save / save_slot:N / key SRuntime after progress; also after successful Back, Title/Quit flush, window close
Read byLoad / load_slot:N / key L (in play)Continue only
PreferencesNo — see prefs.jsonNo

Immediate actions: "save" / "load" always use slot 1.
Keyboard: S quick-saves the last-used slot (fallback 1); L in play loads that slot. Title L still prefers Continue when an autosave exists.
New Game starts a fresh session; it does not delete these files.

Title buttons typically use show_if: "has_save" / "has_autosave". Those checks also require a compatible file, so broken or outdated saves hide Continue/Load. has_save is true if any slot is occupied and compatible.

Author vs player encode

ModeWhenSlot / autosave format
Plain JSONEditor and author-mode player.json — readable for debugging
ObfuscatedProduction game build.sav — not casually readable in a text editor

Obfuscation is comfort (raise the bar vs peeking), not DRM. Prefs stay plain so volumes/locale remain easy to inspect.

Where files live

ContextLocation
Player build (one shipped game)saves/ next to the executable
Author player (many projects)saves/<project-id>/ — id is a slug from game.json title
Web buildBrowser storage — survives reload when storage is allowed; private mode and some iframe blocks do not

Saves are player progress, not part of your project folder.

What a save holds

Story cursor: current node id, flags, shuffle seed, plus save_version, story_id (pack), and story_hash for compatibility.

Stage and chrome (restored in a fixed order): background, character layout profile, on-stage characters, BGM, sprites, objects, screen shader, running animations, panels, theme_style / choice_style overrides, cursor profile.

Also: rollback history (only if allow_back), dialogue backlog. Preferences live only in prefs.json (volumes, display, UI, last_used_slot, last_story_id, campaign unlock flags).

Node-local overrides are not kept; drivers re-sync after load or Back.

Multiple stories (early)

One shared slot list + one autosave for the whole game. Each save stamps story_id. Hub screen: new_game:<id> and optional show_if: story_unlocked:<id>. Slot labels can use {story} / {story_title}. Full contract: Several stories in one project.

Back / rollback

Requires allow_back, enough history, and room under back_limit.

  • History snapshots are taken after a node’s command batch finishes.
  • Back restores the previous snapshot and skips “transit” nodes (no text and no choices) so the player lands on something interactive.
  • Successful Back also writes a live autosave.
  • Backlog trims to the restored node.

Default Back key is remappable (dialogue_back in keybindings.json) — Keys.

Story end

When there is no valid next (or a choice targets null):

  1. Fade out.
  2. Clear autosave — Continue is gone for that finished run.
  3. End session (reset stage/flags; title if configured).
  4. Fade in.
  5. Optionally push post_story_screen.

Manual slots and prefs are not cleared on story end.

Usually you need

WantWrite
Allow Back"allow_back": true
Skip title (test)"boot": "new_game"
Credits after ending"post_story_screen": "credits"
More save slots"save_slots": 12

Title, pause and settings screens · Gameplay HUD · save_slots content block · Sticky state and clear · Several stories

Choice and character layout

Lives in: game.json → layout

Project defaults for choice menus, name entry, and where characters stand. A story moment can override these for one scene; this file sets the usual look.

Choice menus

"layout": {
  "choices": {
    "show_numbers": true,
    "shuffle": false,
    "gap": 10,
    "style": "choice_button"
  }
}
KeyEffect
show_numbersPrefix choices with 1., 2., …
shuffleMix the order (still stable for a given play)
gapSpace between stacked choices
styleButton look pack under themes/

container and item hold where the menu sits and per-button defaults — shapes shared with other UI: Shared shapes. Visual packs: Choice and input looks. Writing the options themselves stays in the script — Choices and memory.

Name entry

"layout": {
  "text_input": {
    "style": "input_default",
    "confirm": { "label": "OK" }
  }
}

Defaults for [input …] moments. Field placement and chrome use the same container shapes as choices.

Character places

"layout": {
  "characters": {
    "mode": "adaptive",
    "adaptive": {
      "slot_count": 3,
      "slot_width": 400,
      "slot_height": 720
    }
  }
}
ModeMeaning
adaptiveEven row of places (left / center / right by default)
explicitYou name each place with your own positions

Script stage directions like [hana left] use those place names. Full explanation: Planes and slots · Places on stage.

Usually you need

WantWrite
Numbered choices"choices": { "show_numbers": true }
Wider choice buttons"choices": { "item": { "layout": { "width": 480 } } }
Three standing places"characters": { "mode": "adaptive", "adaptive": { "slot_count": 3 } }

Full key list: game.json map.

Next

When a line of script is not enough: When the script is not enough. Or jump to The stage for pictures and music in scenes.

Several stories in one project

Lives in: game.json → stories

Early — not final UX. Multi-story packs are implemented (catalog, save story_id, unlock / clear_flag, new_game:<id>). There is no polished shipped story-select / campaign UI — you wire a hub in screens.json yourself. Labels, hub layout, and comfort flows may change.

Optional catalog of story packs in one project (episodes or separate stories). Omit the key for a single pack at story/ (id main).

"stories": [
  {
    "id": "main",
    "title": "Episode 1",
    "path": "story",
    "start": "start",
    "mode": "episode",
    "unlock": { "always": true },
    "clear_flag": "ep1_cleared"
  },
  {
    "id": "ep2",
    "path": "stories/ep2",
    "mode": "episode",
    "unlock": { "flag": "ep1_cleared" }
  }
],
"session": {
  "default_story": "main"
}

Usually you need

IdeaHow
One storyOmit stories — same as today
Hub menu (DIY)screens.json screen whose button hook is "on_click": "new_game:ep2"
Lock episode 2"unlock": { "flag": "ep1_cleared" } + "clear_flag" on episode 1
Slot labelsoccupied_label: "Slot {n} — {story_title} — {node}"

All pack keys

KeyTypeDefaultEffect
idstringrequiredStable pack id (saved as story_id)
pathstring"story"Folder under the project root with script.txt and/or story.json
startstring"start"Entry node id for New Game; if that id is missing, the first moment is used (Scenes and jumps)
modestring"anthology"episode or anthology
titlestring""Display name (slot labels); fallback = id
title_keystring""Locale key for title when set
unlockobject{always:true}{ "always": true } or { "flag": "<name>" } (reads prefs.campaign)
clear_flagstring""On story end in episode mode, set this key true in prefs.campaign

Session

KeyTypeDefaultEffect
session.default_storystringfirst pack / mainPack for bare new_game and boot graph

Actions / show_if

SurfaceEffect
new_gameStart default_story
new_game:<id>Start that pack if unlocked
continue / load_slot:NActivate pack from the save’s story_id, then restore
story_unlocked:<id>True when unlock passes against prefs.campaign

Saves

All packs share one slot list and one autosave (same early multi-slot surface). Each save stores story_id. Load grid tokens: {story}, {story_title} (plus {n}, {node}).

In development · Save, Back and story end · Screens · game.json map

When the script is not enough

Lives in: story/script.txt and (when needed) story/story.json

Most of a game stays in the script: speakers, lines, jumps, choices, and short stage directions. Add a direction file only when a moment needs more than brackets can say cleanly.

What the script is good at

JobIn the script
Who speaks, what they sayHANA:, lines, (silent), joints
Simple stage[bg …], [hana left], [bgm …]
Flow:: id, ->, >, [if …]
Pacing{@w}, {@cps=…}, [auto …], mid-line […] / {@cN}

That is enough for a whole first draft — Writing your story.

When brackets get awkward

Reach for story/story.json when you need things like:

  • An object or widget tied to a flag (a meter, a lamp)
  • Choice menus with custom layout or look packs in detail
  • Dense animation, shaders, or panel chrome on one beat
  • A command that should run only if a condition is true (when)
  • Something that sticks for the session, or lasts only this moment (apply)

If you can say it in one clear bracket line, keep it in the script.

Three ways to work

WayFilesUse when
Script onlyscript.txtMost games, most moments
Script + directionscript.txt + story.jsonDialogue in the script; heavy staging in data
Data onlystory.json aloneRare — whole story as data entries

With script + direction, each direction entry matches a scene by id (the same name as :: park). Dialogue and jumps stay in the script; the data file adds or overrides what that moment needs.

Start in the script

  1. Write and play the scene as script.
  2. Give important moments a stable :: id.
  3. Only then add a story.json patch for the moments that need it.

Next: see one moment in both shapes — The same beat as data.

The same beat as data

Lives in: story/script.txt and story/story.json

A moment is the same thing whether you write it as script or as a data entry. The player runs one shape either way.

Side by side

Script:

:: park
[bg street fade 0.5]
[hana left]
HANA [happy]:
Watch the meter.
-> after

Same moment as data (fields the player cares about):

{
  "id": "park",
  "talking": "hana",
  "text": "Watch the meter.",
  "commands": [
    { "type": "bg", "value": "street", "transition": "fade", "duration": 0.5 },
    { "type": "char", "name": "hana", "position": "left" },
    { "type": "char", "name": "hana", "expression": "happy" }
  ],
  "next": "after"
}

You rarely type the whole data form by hand. The script is sugar for this. Direction files usually hold only the extra fields you could not say in brackets.

Fields most moments use

FieldMeaning
idScene name (:: park)
talkingWho is speaking (one id, or several)
labelCustom name-plate text
textDialogue / narrator lines
commandsStage directions at the start of the moment
choicesChoice menu
nextWhere to go after (or null to end)
autoAuto-advance seconds ([auto N])
driver_typing / driver_typing_targetsMouth drive on/off or limited to some speakers

Mid-line tags ({@w}, {@cN}, {@talk:…}) live inside text. Batches for {@cN} sit in flow_commands; leave actions use exit_commands — Adding direction to a script scene · Command flow.

Watch it live

In the author player, F12 → Nodes lists every moment. Open one to see the live fields after script and direction are merged. Vars shows flags; Validation catches broken jumps and missing assets — How you work.

Usually you need

WantDo
Read what a moment becameF12 → Nodes
Add only stagingPatch in story.json by id — next page
Invent a whole game in JSONPossible, but script stays easier for dialogue

Full map

Every moment field in one view: story/story.json file map. Full tables for choices, script sugar, and Mode B: Story detail.

Next

Adding direction to a script scene

Adding direction to a script scene

Lives in: story/script.txt + story/story.json

Keep dialogue and flow in the script. Put precise staging in a direction entry with the same id.

Name the scene

:: park
HANA:
Watch the meter.
-> after

The :: park name is the link. Without a stable id, the direction file cannot find the moment.

Add a patch

story/story.json is a list of objects. Each object matches one script moment:

[
  {
    "id": "park",
    "commands": [
      {
        "type": "obj",
        "object": "meter",
        "instance_id": "score_meter",
        "show": true,
        "x": 16,
        "y": 16
      }
    ]
  }
]

How merge works

RuleEffect
Match by idDirection entry attaches to that script moment
Most fieldsDirection replaces what the script had
commandsDirection commands are appended after script brackets
Fields you omitScript values stay

So the line and HANA: stay in the script; the meter object is only in the direction file.

A worked meter / flag bind lives in the presentation examples under examples/ (search for story.json next to a script.txt).

When commands run

WhenWhere you put them
Start of the momentScript brackets, or commands in the direction entry
Middle of the textMid-line […] → {@cN} + flow_commands, or write those fields in direction / data
Leave the momentexit_commands / exit_delay — fade or cleanup on the way out

Everyday timing stays in the script: Pacing. Use direction when the mid-line or leave action is too heavy for a bracket. Batches and indexing: Command flow.

Choices and other overlays

You can also replace or flesh out choices, panel fields, auto, and similar on the matched id. Prefer replacing only what you must; leave speakers and text in the script when they are already clear.

Usually you need

WantDo
Extra staging on one beat{ "id": "…", "commands": [ … ] }
Keep script stage tooRely on append — do not duplicate every [bg]
Whole new textPut text in the direction entry (it replaces the script text)
Every command’s fieldsCommands

Next

Reacting to flags live · Commands

Reacting to flags live

Lives in: story/story.json (and object / sprite packs)

Make something on screen follow what the story remembers — a meter, a lamp, a command that only runs if a condition is true.

A flag drives a picture

Set a flag in the script:

[score = 3]
HANA:
Watch the meter.

An object (or sprite part) can bind a property to that flag — position, colour, and similar — so it updates when the flag changes. The bind lives on the object file; the moment only shows the object:

{
  "id": "park",
  "commands": [
    {
      "type": "obj",
      "object": "meter",
      "instance_id": "score_meter",
      "show": true
    }
  ]
}

Building the meter parts and bind map: Sprites and objects. Characters can bind opacity, modulate, scale, and shader uniforms the same way — Expressions and packs. Worked examples also live under examples/.

Flags themselves: Choices and memory.

Run a command only if…

On a command in the direction file, add when with an expression:

{
  "type": "sfx",
  "value": "chime",
  "when": "score >= 3"
}

If the expression is false (or errors), that command is skipped; the rest of the moment continues. Expressions use the same language as [if …] — Choices and memory.

when is data-only (no script sugar for it yet).

Things that stick, things that last one beat

Some commands change the stage for the whole play until you change them again. Others should apply only this moment.

MetaEffect
omit / "apply": "persistent"Stays until override, reset, or new game (default where supported)
"apply": "node_local"Reverts after this moment

Used on surfaces such as panel look, name plate, objects, and some layouts. To clear a sticky override, send that command family again or use "reset": true where the command allows it.

For many shaders: leave the key out = keep what is there; set the key to null = clear. Check the stage / polish page for that surface when you use it.

Usually you need

WantDo
Meter follows a flagObject/sprite bind + show the object on the moment
SFX only if helped"when": "helped" on the command
Panel look for one whisper"apply": "node_local" on that panel command

Next

Stage pictures and sound in depth: Backgrounds. Or return to Setting up your game for project defaults.

Sticky state and clear

Some story commands change the stage for the whole session. Others last one moment. Omitting a key often means “leave as-is”; null often means “clear.” Getting that wrong is how backgrounds and shaders “stick forever.”

Most stories

  • Default: stage changes persist until you change them again (or reset / new session).
  • Use "apply": "node_local" when a panel / layout / labelplate should revert after this beat.
  • Use "reset": true (or an explicit clear) when a sticky override must go away.
  • For shaders and similar: omit = keep current; null = clear.
  • Character modulate / opacity / scale are not sticky — set them again on each char command when you want them to carry on — Characters on stage.

Where this lives

IdeaSurface
Sticky vs one-momentCommand meta apply
Undo stickyreset: true on that command family
Keep vs clearAbsent key vs null (per field)
Session endNew game clears sticky stage
{ "type": "panel", "preset": "whisper", "apply": "persistent" }
{ "type": "panel", "preset": "whisper", "apply": "node_local" }
{ "type": "labelplate", "reset": true }
{ "type": "bg", "value": "street", "shader": null }

apply

ValueEffect
"persistent" (default where supported)Sticks until reset, overridden, or new session
"node_local"Applies for this moment’s cycle, then reverts

Used on (among others): panel, labelplate, char_layout, obj, screen_shader. Shape: Shared shapes.

when is separate: skip this command if the expression is falsy (JSON / Mode B only).

Absent vs null (common sticky fields)

Field familyOmit keyExplicit null
bg / char / sprite shaderKeep currentClear shader
Node panel_shaderClear (unlike sticky stage shaders)Clear
bgm / typing_sfx value—Stop / restore default
screen_shader value—Clear post-process

Always check the command’s detail page when unsure — a few surfaces treat absent differently (panel shader on the node is the common surprise).

How to undo

Sticky thingUndo
Panel preset overrideAnother panel command, or reset where supported
Labelplate override{ "type": "labelplate", "reset": true }
Char layout profile{ "type": "char_layout", "reset": true }
Choice pack from choice_style{ "type": "choice_style", "reset": true }
Theme mood from theme_style{ "type": "theme_style", "reset": true }
Shader on stageSame surface with "shader": null

Save / back: sticky stage is part of cumulative save; node_local is not meant to outlive the moment. Save, Back and story end.

Command flow · Shared shapes · Shaders · Commands

Planes and slots

Two ideas decide where things appear on screen. A render plane decides what draws on top of what. A slot is a named place on the character plane where one character stands.

Everything on one screen: planes stack from back to front, slots hold characters left to right.

Most stories

You can ignore most of this page until you need it.

  • Background at the back, characters in the middle, dialogue panel in front.
  • Default layout gives you three slots: left, center, right. Write [hana left] and you are done.
  • Put a character over the panel with draw_layer: "above_panel" on the char command (or in their pack / slot).
  • Switch arrangements mid-scene with char_layout once you have named profiles in game.json.

Where this lives

IdeaFile / surfacePath
Slot layoutgame.jsonlayout.characters
Character over/under panelchar command or packdraw_layer / ui.draw_layer
Screen stackingscreens.jsonrole (scene / hud / modal)
Sprite bandsprite / obj commandlayer (bg_front / fg / overlay)
Panel chrome slotsthemes/default.jsonpanel.<preset>.slots
{
  "layout": {
    "characters": {
      "mode": "adaptive",
      "adaptive": { "slot_count": 3, "slot_width": 600 }
    }
  }
}
{ "title": { "role": "modal" } }
{ "type": "sprite", "value": "alert", "layer": "overlay" }

Keys: Choice and character layout · Theme: panel for panel slots.

Render planes

Planes are fixed. You cannot add or renumber them; you choose which plane your content goes to.

PlaneLayerWhat lives thereHow you put something there
bg0Background image, bg_front spritesbg, sprite with layer: "bg_front"
scene_ui0Screens with role: "scene"screens.json
chars0Character slots, fg spriteschar, sprite with layer: "fg"
panel0The dialogue panelpanel command, node panel* fields
above_panel1Characters lifted over the panelchar with draw_layer: "above_panel"
overlay3overlay spritessprite with layer: "overlay"
hud10Screens with role: "hud"screens.json
notify50Toast notificationsnotifications in game.json
modal75Screens with role: "modal" (the default)screens.json
cursor90The software cursorcursors.json / cursor
transition100Full-screen fade between scenestransitions

Higher layer numbers draw on top. Four planes share layer 0, so their order comes from scene tree order instead, bottom to top: background, scene UI, characters, panel. That is why a role: "scene" screen sits behind your characters while a role: "hud" screen sits in front of everything in the story.

Where the screen shader goes

screen_shader is not on the ladder above. It moves itself based on target. Details: Shaders.

targetLayerEnds up covering
"game" (default)2Background, scene UI, characters, panel
"scene"74Everything below modal screens
"all"99Everything except the transition fade

Sprite layers

The sprite command has three layers, and they are not one contiguous band — they interleave with characters and the panel:

layerDraws aboveDraws below
"bg_front"Background imageCharacters, panel
"fg"BackgroundPanel, above_panel characters
"overlay"Panel and above_panel charactersHUD screens

Character slots

A slot is a rectangle on the chars plane that holds one character. Each slot has two names:

  • A slot id (slot_0, slot_left, …) — internal, used inside a layout profile.
  • An alias (left, center, right, …) — what you write in a story: position: "left".

You always write aliases. The alias-to-slot mapping belongs to a layout profile, and switching profiles is how you rearrange the stage mid-scene.

Profiles come from layout.characters in game.json, in one of two modes.

Adaptive mode (default)

You state how many characters can be on screen and how big they are; slot rectangles are generated to fit the window.

{
  "layout": {
    "characters": {
      "mode": "adaptive",
      "base_texture_size": 600,
      "adaptive": {
        "slot_count": 3,
        "slot_width": 520,
        "snap": "bottom",
        "margin": 0
      }
    }
  }
}
KeyTypeDefaultEffect
slot_countint ≥ 23How many slots are generated
slot_widthint ≥ 1base_texture_sizeSlot width in pixels
slot_heightint ≥ 1slot_width if width was set, else base_texture_sizeSlot height in pixels
side_marginint ≥ 020Inset from the left and right window edges
slot_gapint or nullnullPixels between slots. null divides the remaining width evenly. Negative values overlap slots on purpose
snap"bottom" | "top""bottom"Which window edge slots align to
marginint0Distance from the snapped edge
z_patternstring"center_front""center_front" gives the middle slot z: 2 and the rest z: 1. Any other value leaves every slot at z: 1
auto_selection_policystring"nearest_center_out"Order used when placing a character that has no position
alias_strategy"canonical" | "indexed" | "custom""canonical"How aliases are named
aliasesarray of strings—Required when alias_strategy is "custom"; must have exactly slot_count unique, non-empty entries

base_texture_size sits one level up, directly under layout.characters, and defaults to 600.

Canonical alias names depend on the slot count:

slot_countAliases, left to right
2left, right
3left, center, right
4far_left, left, right, far_right
5far_left, left, center, right, far_right
6 or moreslot_1 … slot_N

"indexed" always gives slot_1 … slot_N regardless of count. "custom" uses your aliases array in left-to-right order; if the array is the wrong length or has duplicates, it falls back to canonical names and warns.

Explicit mode

You place every slot by hand. Use this when characters should not be evenly spaced, when slots overlap deliberately, or when different scenes need different arrangements.

{
  "layout": {
    "characters": {
      "mode": "explicit",
      "explicit": {
        "active_profile": "default",
        "profiles": {
          "default": {
            "aliases": { "left": "slot_left", "right": "slot_right" },
            "auto_selection_policy": "nearest_center_out",
            "slots": [
              { "id": "slot_left",  "x": 0,   "y": 20, "width": 700, "height": 700, "z": 1, "order": 0 },
              { "id": "slot_right", "x": 580, "y": 20, "width": 700, "height": 700, "z": 1, "order": 1 }
            ]
          },
          "closeup": {
            "aliases": { "center": "slot_big" },
            "slots": [
              { "id": "slot_big", "x": 290, "y": 0, "width": 900, "height": 900, "z": 2, "order": 0 }
            ],
            "transition": { "kind": "fade", "duration": 0.35 }
          }
        }
      }
    }
  }
}

A profile has four keys:

KeyTypeEffect
slotsarrayThe slot rectangles
aliasesobjectMaps alias to slot id. An alias pointing at a slot id that does not exist is dropped with a warning
auto_selection_policystring"nearest_center_out" (default) orders aliases by slot order, breaking ties alphabetically. "center_priority" tries the alias literally named center first, then falls back to that same order
transitionobjectDefault { "kind", "duration" } used when a char_layout command switching to this profile does not name its own

A slot has these keys:

KeyTypeDefaultEffect
idstringrequiredSlot id referenced by aliases; must be unique in the profile
x, ynumber0Top-left corner, in window pixels
width, heightnumberbase_texture_sizeSlot size. The character’s art is composed inside this box
znumber0Draw order within the character plane; higher is in front
ordernumberindexTie-breaker for z, and the sequence used by auto placement and remapping
draw_layer"behind_panel" | "above_panel"inheritedOverrides the plane for characters in this slot
modulateHTML color#ffffffSlot tint, multiplied with the char command tint and any pack drive tint
scalenumber or [sx, sy]1Slot scale, multiplied the same way

Which plane a character ends up on

draw_layer is resolved in this order, first match wins:

  1. draw_layer on the char command
  2. ui.draw_layer in the character’s pack JSON
  3. draw_layer on the slot in the layout profile
  4. "behind_panel"

Placing a character without naming a slot

When a char command omits position:

  1. If that character is already on screen, they stay in their current slot.
  2. Otherwise the first free alias in auto_selection_policy order is used.
  3. If every alias is taken, nothing is shown and a warning is logged.

Showing a character in a slot also removes them from any other slot, instantly and without a transition, so a character can never be in two places.

Switching profiles mid-scene

The char_layout command activates a different profile.

[char_layout closeup fade 0.35]
[char_layout reset]

Characters keep their alias across the switch. If the new profile still defines that alias, the character moves to the new rectangle. If it does not, the character overflows: they are hidden with a 0.2 second fade, and a warning names the alias and character. Two characters resolving to the same slot id is also an overflow for the second one, ordered by the old slot order.

policy decides what happens when anything overflows:

policyBehaviour
"best_effort_remap" (default)Apply the new profile, hide the overflowing characters
"strict"Roll back to the previous profile and change nothing

transition decides how slots move. "instant" snaps. "fade" glides surviving slots to their new position over duration seconds, easing out. If the command omits transition, the target profile’s own transition block is used; if that is also missing, the switch is instant. A fade with no duration anywhere uses 0.3 seconds.

Only slots that exist before and after the switch glide. Characters appearing or disappearing are handled by their own char show and hide transitions.

Panel slots

The dialogue panel has its own small slot system for chrome such as a speaker portrait pinned to the panel. Those are declared in the theme under panel.<preset>.slots, not in layout.characters, and they follow the panel rather than the window. See Theme: panel. Stage props: Sprites and objects. UI screens: Title, pause and settings screens.

Backgrounds

Lives in: backgrounds/

Places your story happens in. Folders and formats: Pictures, sound, and folders. Script sugar: Stage directions.

Pictures on disk

On diskIn the story
backgrounds/street.jpg[bg street] or [bg street fade 0.5]

Write the name without the extension. Formats: .png, .jpg / .jpeg, .webp, .svg.

Change place

[bg street]
[bg street fade 0.5]
[bg park slide_left 0.6]
[bg clear]
PieceEffect
NameLoad that picture
fade / slide_*How it enters (default for a plain [bg name] is a short fade)
DurationSeconds
clearRemove the background

As data:

{ "type": "bg", "value": "street", "transition": "fade", "duration": 0.5 }

value: null clears. Same-track music rule does not apply here — each bg sets the place.

Effects on a background

You can attach a shader (and optional parameters) on the background command. Leave the shader key out to keep what is there; set it to null to clear. Full shader authoring: Shaders. Sticky omit vs null: Sticky state and clear.

Command fields opacity, modulate, and visible set how the plate looks; live binds can drive the same look from flags — including image to swap the picture (same stems as value, under backgrounds/). Bind image swaps are instant; use [bg … fade …] when you want a transition.

Day / night from a flag:

{
  "type": "bg",
  "value": "street",
  "bind": {
    "image": {
      "flag": "time",
      "map": { "day": "street", "night": "street_night" },
      "default": "street"
    }
  }
}

Prefer a sprite/object for small overlays; keep bg for the full scene picture. Show/hide hooks on the command still work as elsewhere.

Richer rooms

Backgrounds that behave like multi-layer places (hotspots, room camera) are still early — In development. Ship with single pictures and [bg …] today.

Usually you need

WantWrite
Show a place[bg street fade 0.5]
Clear[bg clear]
Slide in[bg park slide_left 0.6]

Next

People on stage: What a character is · then Sprites and objects

Sprites and objects

Lives in: sprites/

Props and gadgets on top of the scene — not character folders. Skip this folder until you need a chair, lamp, phone overlay, or flag-driven meter.

A picture on the scene (sprite)

Sprites are data-only (direction file / story data — no [sprite …] sugar in the script today):

{
  "type": "sprite",
  "id": "chair",
  "value": "chair",
  "layer": "bg_front",
  "x": 625,
  "y": 494
}
FieldEffect
idName of this prop instance
valuePicture name under sprites/ (or null to hide)
layerDraw band: bg_front, fg, or overlay
x / yPosition

You can also draw a plain rect or polygon (kind) instead of an image. Optional fade, tint, shader (one per sprite), and live binds work here too.

Reusable objects (*.obj.json + obj)

A catalog object is a file like sprites/meter.obj.json: an id, a default layer, and a list of parts (images or shapes).

{
  "id": "meter",
  "layer": "overlay",
  "parts": [
    { "id": "bg", "kind": "rect", "width": 40, "height": 120, "color": "#343a52e0" },
    {
      "id": "ptr",
      "kind": "polygon",
      "points": [[12, 0], [0, 10], [12, 20]],
      "bind": {
        "y": { "flag": "score", "map": { "1": 90, "3": 50, "5": 10 }, "default": 50 }
      }
    }
  ]
}

Show an instance on a moment:

{
  "type": "obj",
  "object": "meter",
  "instance_id": "score_meter",
  "show": true,
  "x": 16,
  "y": 16
}
IdeaMeaning
objectWhich catalog file
instance_idThis copy on stage (you can show the same def twice with different ids)
updatePatch parts on an existing instance
bindLive link from a flag to a property (on the def, the instance, or a part)
applySticky for the session, or node_local for one moment

A shader on an object applies to the whole instance (all parts composited), not per part.

Objects do not use character-style extends. Parts are a flat list; change them with update and binds — Reusing settings.

Putting the obj on a script scene: Adding direction to a script scene. Flag-driven examples: Reacting to flags live.

Usually you need

WantDo
One prop picturesprite with value + position
Meter / widget*.obj.json parts + obj show
Follow a flagbind on a part

Full map

objects file map · Objects detail

Next

Music and sound

Music and sound

Lives in: audio/bgm/ and audio/sfx/

Music beds and one-shot effects during the story. Title defaults and UI clicks live in game.json — Music and sounds by default. Folders: Pictures, sound, and folders.

Names and formats

On diskIn the story
audio/bgm/theme.ogg[bgm theme]
audio/sfx/door.mp3[sfx door]

Formats: .ogg or .mp3. Use the name without the extension.

Music

[bgm theme]
[bgm theme fadein 1.5]
[bgm stop]

As data:

{ "type": "bgm", "value": "theme", "volume": 0.7, "fadein": 1.0 }
{ "type": "bgm", "value": null, "fadeout": 1.0 }
KeyEffect
valueTrack name; null / stop clears
volumeLoudness 0–1
fadein / fadeoutSeconds
fadein_from / fadeout_toVolume fractions at the ends of the fade

Playing the same track again does not restart it (volume can still update). Different track with both fade-in and fade-out → crossfade.

Title screen music uses sounds.main_bgm* in game.json, not these command defaults.

Sound effects

[sfx door]
{ "type": "sfx", "value": "door" }

One-shot. Optional volume and similar fields exist in data form. Button clicks and typing beeps are separate defaults under sounds — Text speed and voice beeps.

On a moment vs on a button

WhereTypical use
Script / moment commandsStory stings, room ambience changes
game.json → soundsTitle BGM, UI click / hover, typing beep
Screen / choice packsPer-button overrides when you customise menus

Usually you need

WantWrite
Start music[bgm theme fadein 1.5]
Stop music[bgm stop]
Door sting[sfx door]

Next

Look of the dialogue box and menus: Theme and the dialogue box. Or directing heavy staging: When the script is not enough.

What a character is

Lives in: characters/<id>/

A character is a folder whose name is the id (hana). The script uses that id for speakers (HANA:) and stage directions ([hana left]). Folders and file names: Pictures, sound, and folders.

The simple path

characters/hana/
  hana.png
  hana_happy.png
[hana left fade 0.4]
[hana happy left]
[hana hide]
HANA [happy]:
Good morning.
RuleMeaning
Folder nameCharacter id (case-insensitive in the script)
hana.png / hana_default.pngDefault look
hana_happy.pngExpression happy
No JSON requiredEnough for a first game

HANA [happy]: both speaks and stages that expression. Bracket lines show, move, or hide without a line of dialogue — Stage directions.

What comes next in this part

PageWhen you need it
Expressions and packsLayered art, pack root / ui, char_pack, tint / shader / bind
Expression fadesSoft swaps between looks (vs show/hide fade)
Talking and focusMouths, silent lines, focus / choice drive
Places on stageSlots, layouts, above/behind the dialogue box
Motion and framesFlipbook layers; idle / 3D still early

Props and backgrounds are not characters — The stage.

Usually you need

WantDo
Add a personFolder + default PNG
Show them[hana left]
Change lookhana_happy.png + HANA [happy]: or [hana happy left]

Next

Expressions and packs

Expressions and packs

Lives in: characters/<id>/*.json

When one PNG per look is not enough, a pack lists stacked pictures (layers) for each expression. Same folder as the PNGs; often named default.json.

A small pack

{
  "display_name": "Hana",
  "default": [
    { "path": "base.png" },
    { "path": "mouth.png" }
  ],
  "happy": {
    "extends": "default",
    "override": [{ "layer": 1, "path": "mouth_happy.png" }]
  }
}
IdeaMeaning
Expression keydefault, happy, … — what [hana happy] and HANA [happy]: ask for
LayersStacked pictures (body, face, mouth…) drawn bottom → top
"layer": 1 in overrideIndex into that stack (0 = first layer)
extendsCopy another expression’s layers, then change them
overrideReplace layers by index
addAppend extra layers

This is layer reuse, not the same as panel "extends" — Reusing settings.

Unknown expression names fall back to default.

Pack root (not layers)

Keys at the top of the file describe the character, not one expression:

KeyEffect
display_nameName shown on the name plate (usual case)
display_name_keyTranslation catalog key instead of a hard-coded string
uiPreferred panel / name-plate / draw plane — see Pack ui below
driveLive reactions while talking or focused — Talking and focus
composition_transitionsHow expression swaps fade — Expression fades
<expression>Layer list or extends / override / add object

Reserved filenames (not packs): sprites.json, objects.json, metadata.json, profiles.json.

Pack ui

KeyEffect
draw_layer"behind_panel" (usual) or "above_panel" — Places on stage
default_panel_idPreferred dialogue panel for this character — How text finds a panel
default_panel_presetPanel look preset name — How a panel looks
default_labelplate_style_keyName-plate look for this speaker — Name plate
panel_slotsPortrait / art inside the box — Speaker art on the panel

Layer keys

A layer is usually a picture; it can also be a flipbook — Motion and frames.

KeyEffect
pathImage in the character folder (__blank = empty slot)
x / yOffset in pixels
w / hSize override
modulate / scale / opacityTint, size, alpha for that layer
driveShow only while a driver is on — e.g. "typing", "!typing", "focus" — Talking and focus
frames + fps / loop / …Flipbook instead of a single path

Several pack files

Keep a simple default.json and a richer full.json (or similar). Switch with:

[char_pack hana full]
{ "type": "char_pack", "name": "hana", "pack": "full" }

Or set "pack": "full" on a single char command. char_pack alone does not redraw — the next show / expression change uses the new pack.

If the named pack is missing, the player tries default, then the first pack it finds.

Locale overlays

For a translated art set, add default_ja.json (or <pack>_<locale>.json) beside the base pack. The player picks the overlay that matches the active language when it exists.

Simple PNGs still work

If there is no pack, the player keeps using hana.png / hana_happy.png. Adding a pack is optional polish, not a rewrite of the script.

Story look (tint, shader, bind)

Separate from the pack file — on a char command (direction file or data):

WantField
Tint / fade / size for this beatmodulate, opacity, scale (repeat on later commands if you want them to stick)
Effect on the charactershader / shader_params (or null to clear) — one shader over the whole slot, not per layer — Shaders
Follow a flag while on stagebind for opacity, modulate, scale, shader:… — Reacting to flags live
{
  "type": "char",
  "name": "hana",
  "position": "left",
  "opacity": 0.85,
  "bind": {
    "opacity": { "flag": "trust", "linear": [0, 0.3, 10, 1.0] }
  }
}

Show/hide fades in the script ([hana left fade 0.4]) are stage enter/leave — expression-to-expression blends live in the pack — Expression fades.

Usually you need

WantDo
Body + mouthPack with two layers
Happy mouth only"extends": "default" + override on the mouth index
Second wardrobeAnother *.json pack + char_pack
Name on the platedisplay_name (and optional ui.default_labelplate_style_key)

Full map

Pack keys at a glance: characters pack map. Full tables: Character pack detail.

Next

Expression fades · Talking and focus

Expression fades

Lives in: character pack → composition_transitions

Two different fades:

KindWhat it isWhere you set it
Show / hideCharacter enters or leaves a placeScript sugar / char — [hana left fade 0.4]
Expression swapSame person, new look while already on stagePack composition_transitions (or swap_mode on the command)

This page is the second kind.

Pack policy

"composition_transitions": {
  "default": { "kind": "fade", "duration": 0.2, "swap_mode": "auto" },
  "pairs": {
    "happy->angry": { "kind": "fade", "duration": 0.4, "swap_mode": "crossfade" }
  }
}
KeyEffect
defaultUsed when no more specific rule matches
pairs"from->to" overrides (also wildcards like happy->*, *->angry, *->*)
from / toMaps keyed by expression id → same payload shape

Payload

FieldValues
kindinstant, fade, slide_up / down / left / right
durationSeconds
swap_modeauto, in_only, crossfade, out_in, cross_slide

auto: fade → crossfade; slide → in_only; otherwise in_only.

Resolve order: exact pair → from-wildcard → to-wildcard → *->* → from map → to map → default → instant.

When the command wins

If the char command names a transition (transition, transition_in / transition_out, or a duration), that command wins for that beat — including show/hide.

If the command says nothing about transitions:

  • Expression change on someone already visible → pack composition_transitions
  • Otherwise → instant

Force blend style with "swap_mode": "crossfade" (and friends) on the command when you need a one-off.

Usually you need

WantDo
Soft look changes by defaultPack default fade ~0.2s
Dramatic one pairpairs entry for that from->to
Instant this beatCommand transition: "instant" or omit pack policy

Next

Talking and focus

Talking and focus

Lives in: character packs (drive on layers / pack root) and the story’s speakers

While text types, the player can drive mouth (and similar) layers. Focus can emphasise who the line is about. Speakers and (silent): Lines and speakers.

Drive reacts to live UI state (typing, focus, open choices). It is not the same as a bind, which follows a story flag — Reacting to flags live.

Mouth layers

"default": [
  { "path": "base.png" },
  { "path": "mouth_closed.png", "drive": "!typing" },
  {
    "frames": ["m1.png", "m2.png"],
    "fps": 10,
    "loop": true,
    "drive": "typing"
  }
]
Layer driveWhen that layer shows
"typing"While the typing mouth driver is on for this character
"!typing"While it is off
"focus" / "!focus"While this character is / is not in the current focus set
"choice_open" / "!choice_open"While an enabled choice menu is waiting

Text still types either way. (silent) on a speaker (or on one name in a joint line) turns that character’s typing driver off — mouths stay on the closed layer.

Voice beep is separate (game.json sounds) — Text speed and voice beeps.

Pack-root drive (focus and more)

At the pack root, drive can change the whole look while a driver is on — swap expression, tint, scale, nudge:

"drive": {
  "focus": {
    "use": "happy",
    "priority": 10,
    "scale": 1.05
  },
  "choice_open": {
    "modulate": "#ddddff",
    "priority": 5
  }
}
FieldEffect
useSwitch to another expression while the predicate holds
modulate / scaleTint / size while active
offset / rotation_degreesNudge / rotate while active
priorityWhen several root rules match, higher wins for use; modulate/scale multiply; offsets sum
transition (optional)How the drive look eases in/out
You want…Prefer
Mouth open/closed while typingLayer "drive": "typing" / "!typing"
Slight “I’m the focus” scale / tintRoot drive.focus with scale / modulate (same expression)
Different art while focused / choosingRoot drive with "use": "other_expression"
A lasting expression for the sceneStory char / HANA [happy]: (command wins until you change it)

Drivers are recomputed after load or Back — they are not stored in the save. Flag overlays stay on bind.

Joint lines

HANA [happy] & SORA (silent): — both in the group; only non-silent names get the typing mouth. One name plate for the group — Lines and speakers.

Usually you need

WantDo
Talking mouthLayer with "drive": "typing" (+ closed with "!typing")
No mouth this lineNAME (silent):
Only one mouth in a groupMark the others (silent)
Focus popRoot "drive": { "focus": { "scale": 1.05 } }

Next

Places on stage

Places on stage

Lives in: game.json → layout.characters, plus char / char_layout in the story

Where people stand, and whether they draw under or over the dialogue box.

Default places

[hana left]
[sora center]
[emi right]

By default the project offers an even row of places — usually left, center, right. Counts, sizes, and names come from:

"layout": {
  "characters": {
    "mode": "adaptive",
    "adaptive": {
      "slot_count": 3,
      "slot_width": 400,
      "slot_height": 720
    }
  }
}
ModeMeaning
adaptiveEven row; player names places for you (or you set aliases)
explicitYou define each place’s position in a named profile

Project defaults: Choice and character layout. Full key list: game.json map.

Change arrangement mid-story

Use a char_layout command (usually from a direction file) to switch to another profile — close-up, three-wide, off-stage row, and so on — when you have named those profiles in layout.characters.

Behind or above the dialogue box

Characters usually draw behind the dialogue panel so text stays readable. Panel system: What a panel is.

SettingEffect
behind_panelDefault — under the dialogue box
above_panelOver the box (rare; big portraits, special beats)

Set it on the pack (ui.draw_layer) or on a single char command.

This is not the same as sprite/object layer bands (bg_front / fg / overlay) — those are for props — Sprites and objects.

A character shader applies to the whole slot (all layers composited), not to one layer.

Usually you need

WantDo
Three standing spotsadaptive + slot_count: 3
Custom names / positionsexplicit profiles
Portrait over the boxdraw_layer: "above_panel"

Next

Motion and frames

Motion and frames

Lives in: character pack layers

How a layer can animate while someone is on stage. Soft idle sway and 3D models are still early — see below.

Flipbook layers

A layer can be one picture (path) or a short flipbook:

{
  "frames": ["blink1.png", "blink2.png", "blink1.png"],
  "fps": 8,
  "loop": true
}
KeyEffect
framesPictures in order (paths in the character folder)
fpsFrames per second
loopRepeat, or play once

Combine with drive so a mouth flipbook runs only while typing — Talking and focus.

What is not ready yet

IdeaStatus
Soft idle breathe / sway on layered packsEarly — In development
Blender / GLTF characters on stageEarly — In development

Ship with still poses and flipbooks you control. Treat idle and 3D as upcoming, not release blockers.

Story animation presets

Separate from pack flipbooks: story animate commands and shared presets — Animation. Use those for scripted moves; use layer frames for blink / mouth cycles on the character itself.

Usually you need

WantDo
Blink or mouth cycleLayer frames + fps
Mouth only while talkingSame + "drive": "typing"

Next

Back to the room: Backgrounds · Sprites and objects · Music and sound.

What a panel is

Lives in: theme panel.<preset> + story routing

A panel is the dialogue box — the place text, the name plate, and on-panel choices appear. Most games need only one. Extra panels are for side commentary, whisper boxes, or a second speaker’s sticky line.

Two names that matter

WordMeaning
Panel idWhich instance on screen (main, side, …). main always exists.
PresetWhich look from the theme (default, whisper, …) that instance uses

Id = which box. Preset = how that box is drawn. You can put look whisper on id main, or look default on id side.

One box is enough

Ship with a single main panel and a default preset. Speakers write into main unless you say otherwise. Name plate and Continue / Back live on that box.

HANA:
Good morning.

No panel fields required.

Where the pieces live

PieceWhere
Look (size, height ceiling, body scroll)themes/default.json → panel.<preset> — How a panel looks
Which box gets this lineStory node / character pack / panel command — How text finds a panel
Second boxes, stacking, side logsSeveral panels
What quiet boxes doPresence and quiet panels
Lines that stack in the boxAppending text
Portrait inside the boxSpeaker art on the panel

Fonts and whole-theme colours sit under Look. Character preference for a panel: pack ui — Expressions and packs.

Usually you need

WantDo
Normal dialogueNothing — use main + default
A quieter lookTheme preset whisper + node "panel": "whisper"
A second boxNew panel id — Several panels
A tall scrolling logPreset max_height + sticky append — Several panels

Next

How text finds a panel

How text finds a panel

Lives in: story nodes, character packs, and the panel command

Each dialogue moment draws text on one panel — the routed panel. Routing picks the id and the preset.

Which panel id gets the text

First match wins:

  1. Node field panel_id
  2. Pack ui.default_panel_id for the first speaker on the line (talking[0])
  3. Otherwise main
{
  "id": "side_note",
  "panel_id": "side",
  "talking": ["hana"],
  "text": "She muttered aside."
}

With several speakers and no panel_id, only the first speaker drives routing (and panel speaker art). Joint lines: Lines and speakers.

If the id is not main and that panel does not exist yet, the player creates it (there is a small cap on extra panels). main is never destroyed.

Which preset (look) it uses

First match wins:

  1. Node field panel (preset name)
  2. preset on a panel command aimed at that panel
  3. Character ui.default_panel_preset, if that preset exists in the theme
  4. A preset whose name matches the panel id (e.g. id side → panel.side)
  5. default

Unknown names fall back to default.

{
  "id": "soft",
  "panel": "whisper",
  "talking": ["hana"],
  "text": "…"
}

Character pack defaults:

"ui": {
  "default_panel_id": "main",
  "default_panel_preset": "whisper"
}

Sticky vs one-shot

SurfaceEffect
Node panel / panel_id / panel_* policy fieldsThis moment only
{ "type": "panel", "preset": "whisper" }Stays until you change it (or "apply": "node_local")
Pack ui.default_*Whenever that character is first speaker and the node omits overrides

Script sugar [panel whisper] sets the preset on main. Direction files can set panel_id and policy fields when the script is not enough — Adding direction.

Usually you need

WantDo
Force this line to sideNode "panel_id": "side"
Always whisper for HanaPack ui.default_panel_preset: "whisper"
Change look for a stretch of scenespanel command with "apply": "persistent"

Next

How a panel looks

How a panel looks

Lives in: themes/default.json → panel.<preset>

Presets are named looks. The story picks a preset; the theme decides size, colour, padding, name-plate docking, and how tall the box may grow before the text scrolls.

{
  "panel": {
    "default": {
      "width": "100%",
      "anchor_v": "bottom",
      "min_height": 200,
      "padding": { "top": 20, "left": 24, "right": 24, "bottom": 24 },
      "bg": "#1e2133e0",
      "corner_radius": 12
    },
    "whisper": {
      "extends": "default",
      "width": "60%",
      "anchor_h": "center",
      "bg": "#00000099"
    },
    "side_log": {
      "extends": "default",
      "width": "36%",
      "anchor_h": "right",
      "offset_x": -16,
      "anchor_v": "stretch",
      "max_height": "full",
      "scrollbar_layout": "reserve"
    }
  }
}

default is the fallback preset. Other names are yours. "extends" merges another preset’s keys — Reusing settings.

Same box, two moments. The plate grows with its text, holds the name plate and portrait, and hosts choices when they appear. Continue is hidden while choices are up.

Same stage, two presets. Only the preset name changed — size, colour, and where the plate sits come from panel.<preset> in the theme.

Usually you tune

KeyEffect
width"NN%", "100%", "stretch", or pixels
anchor_h / anchor_vleft / center / right / stretch and top / center / bottom / stretch
offset_x / offset_yNudge from the anchors (negative to move inward from right/bottom)
min_height / max_heightFloor and ceiling — pixels, "NN%", or "full"
paddingInner margins
bgFlat plate colour
bg_texture / bg_9sliceNine-slice plate image (wins over flat bg)
corner_radiusRounded corners on the flat plate
speaker_gap / choices_gap / choices_separationGaps around name and choices
scrollbar_styleOptional element pack for the body scrollbar (else theme scrollbar.*)
scrollbar_layoutHow the scrollbar column is laid out — see below
scroll_pin_durationSeconds to ease to the newest line after height settles (0 = snap)
labelplate_style_keyWhich name-plate style — Name plate
labelplate_layoutDock the name plate to a panel edge (omit = inline above the text)

Height and body scroll

The plate grows with text (and panel-hosted choices) up to max_height. Past that ceiling, the body text scrolls inside the plate. Name plate, panel-placed Continue / Back, and choices stay outside the scroll area.

Height valueMeaning
numberPixels
"40%"Share of window height
"full"Window height
omit min_height160
omit max_heightDefault share of the window (same idea as before: about 65%, with a floor from min_height)

A sticky { "type": "panel", "max_height": … } overrides the theme preset until reset.

Scrollbar column

scrollbar_layoutEffect
default (omit)Bar appears when needed and takes width from the text when it does
reserveKeep a scrollbar column so wrap width stays stable; track stays hidden until there is something to scroll
overlayNot available yet — falls back to default

Use reserve on logs, phone panels, or any fixed-height plate that scrolls often. Leave normal ADV boxes on default.

Panel-hosted choices (host: "panel") share the plate and shrink the body. Overlay choices sit on the screen and do not take body space. A choice list taller than the ceiling can still clip — body scroll is for dialogue text, not a separate choice scroller.

Mood for one scene

A scene can load another theme/mood file that overrides panel presets for that stretch — Theme and the dialogue box. Otherwise, prefer a second preset (whisper) or a sticky panel command.

Bind and shader

A panel command can tint or hide from flags (bind on opacity / visible) and attach a panel shader. Flag binds in depth: Reacting to flags live.

Usually you need

WantDo
Full-width bottom boxdefault with anchor_v: "bottom", width: "100%"
Narrow centre whisperPreset with smaller width + anchor_h: "center"
Tall side logmax_height: "full" + scrollbar_layout: "reserve" — Several panels
Reuse most of default"extends": "default" then override a few keys

Next

Several panels

Several panels

Lives in: theme presets + story panel_id / panel commands

Use more than one panel when two conversations should stay visible, or a side box holds a note while main advances.

Create another box

Ask for a new id on a node or command:

{ "id": "aside", "panel_id": "side", "panel": "side", "talking": ["hana"], "text": "…" }
{ "type": "panel", "panel_id": "side", "preset": "side", "show": true }

If theme panel.side exists, routing can pick that preset by matching the id name. Otherwise set "panel": "…" or pack default_panel_preset explicitly.

main always exists. Extra ids are created when first used (capped — keep the cast of boxes small).

Stacking

FieldEffect
z on a panel commandDraw order among panels; also feeds which visible panel gets input

Higher z draws above. Keep the active reading box easy to see.

Patterns that work

PatternIdea
WhisperSame main id, different preset — no second box
Side commentarypanel_id: "side" + smaller preset; main keeps the primary line
Sticky noteKeep side visible while focus returns to main — Presence and quiet panels
Side / phone logTall preset + sticky append — see below

Stage character places (left / center) are a different system — Places on stage. Drawing characters above/behind the box is draw_layer, not a second panel.

Side log (append + scroll)

A common second box: full-height (or tall) side panel that collects lines and scrolls inside the plate.

  1. Theme preset with "max_height": "full" (or "80%") and "scrollbar_layout": "reserve" — How a panel looks
  2. Sticky { "type": "panel", "panel_id": "side", "preset": "side_log", "text_mode": "append" }
  3. Moments that write to "panel_id": "side" (or pack default_panel_id)
  4. Optional [panel panel_id side clear_append] when the thread should restart — Appending text

Keep main on normal replace dialogue while the log accumulates.

Usually you need

WantDo
Soft look onlyOne id, second preset
Two texts at onceTwo ids + presence / readonly policy
Force a side box uppanel command show: true on that id
Scrollable logTall max_height + sticky text_mode: append

Next

Presence and quiet panels

Presence and quiet panels

Lives in: panel policy on pack / panel command / node fields

The routed panel is the one receiving this moment’s text. Other panels are non-talking — policy decides whether they hide or stay as a dim reminder.

Policy fields

FieldValuesDefaultEffect
presence_modetalk_only, visible_with_char, always_visibletalk_onlyHow strongly a panel stays when it is not routed
non_talking_modehide, readonly_last_texthideWhat a non-routed panel shows
readonly_style_keystyle name—Look for readonly text

Practical recipe: keep a side box around without the speaker → always_visible + readonly_last_text. Plain hide when unused → leave defaults.

Today talk_only and visible_with_char do not differ for show/hide; prefer always_visible when you need “stay up.”

Where policy is set

Order (later wins for sticky layers as noted in the live rules):

  1. Built-in defaults
  2. Character pack ui.panel_policy
  3. Sticky fields from a panel command (presence_mode, non_talking_mode, …) — beat the pack
  4. Node fields panel_presence_mode, panel_non_talking_mode, panel_readonly_style_key — this node only

policy_reset or full reset on a panel command clears sticky policy.

Show hold

show: true on an off-route panel sticks: policy cannot instantly hide it until show: false, reset, or policy_reset. Use that for a deliberate “pin this box.”

Node hide_panel hides the routed panel for that node only — different from non_talking_mode: "hide" on other panels.

Text modes

Default is replace — each line clears the panel. To stack lines in the same box, use append — full fields and sticky vs node: Appending text.

Usually you need

WantDo
Hide unused boxesDefaults (hide)
Leave last side line visiblenon_talking_mode: "readonly_last_text" (+ often always_visible)
Pin a box against policyOff-route panel with show: true

Next

Appending text · Speaker art on the panel

Appending text

Lives in: story moment fields and the panel command (text_mode)

By default each new moment replaces what was in the panel. Append keeps earlier text and adds this moment’s text — useful for a running log, a chat-style beat, or a sticky side panel that accumulates notes.

A moment is one click-through step of the story (also called a node in tools and data) — Words used in these docs.

Replace vs append vs plates

text_modeEffect
replace (default)This moment’s text becomes the whole panel contents
appendThis moment’s text is added to what is already there (one label)
platesEach beat is a separate plate from the preset’s plates map — Theme: panel → Plates

Per-plate transition_in / transition_duration animate the live plate chrome inside the body scroll on each new beat (see Theme: panel → Plates).

plates uses the same append_direction, append_gap_px, append_push_duration, and clear_append keys as append.

Not mid-line typing: while one moment’s text is revealing, that is still one block. Append decides what happens when the next moment writes to the same panel.

{
  "id": "log_1",
  "panel_id": "side",
  "text_mode": "append",
  "talking": ["system"],
  "text": "Door unlocked."
}

For a lasting chat, prefer a sticky command so later moments keep appending:

{ "type": "panel", "panel_id": "side", "text_mode": "append", "max_height": "full" }
[panel panel_id side text_mode append]
[panel clear_append]

Direction and gaps

FieldValuesDefaultEffect
append_directiondown, updownNew text below or above the existing block
append_gap_linesnumber ≥ 00Blank lines between old and new
append_gap_pxnumber ≥ 00Extra pixel gap

When append fills past the panel height ceiling, the body scrolls and keeps the newest edge in view (down → bottom, up → top). Set the ceiling with theme max_height or a sticky panel max_height — How a panel looks.

Changing append_direction on a panel that already has a thread keeps that history and re-pins it for the new direction (newest at the bottom for down, at the top for up). It does not start a fresh log — use clear_append when you want a wipe.

Editing the script in author mode while you are mid-thread re-reads sticky panel keys (append_direction, text_mode, gaps) along the moments you already played — including the moment that opened the thread — then reorients the live thread. You do not need to be on the same moment as the sticky command. Going back after that keeps the rehydrated direction (it does not restore the pre-edit value).

Sticky vs one moment

SurfaceEffect
Moment fields text_mode / append_*This moment only
{ "type": "panel", "text_mode": "append", … }Stays for later moments on that panel until you change it

A moment’s text_mode overrides a sticky panel setting for that moment only.

SituationWhat happens
That panel was replace, then this moment is appendStarts a new append log from this moment
That panel was sticky append, even while another panel was talkingKeeps collecting when you write to it again
Sticky text_mode: replace, clear_append, or panel resetClears the log on that panel
One moment uses replace while sticky is still appendShows that line alone; the log stays for later append moments
Only a moment field sets append (no sticky command)That one moment appends; lasting chat needs the sticky command

Clear the thread, keep the mode

clear_append wipes history on that panel but keeps sticky append mode, direction, preset, and policy. Use it for a fresh chat on the same box without a full reset.

[panel panel_id side clear_append]

Same-moment clear_append applies before that moment’s first paint.

Script sugar for everyday append is limited — use the direction file / moment data, or a panel command before the stretch of moments ([panel … text_mode append] / [panel clear_append] exist).

With several panels

Append is per panel id. main can stay on replace while side or a phone panel appends. A quiet append panel keeps its history until you write to it again. Presence / readonly on quiet panels is separate — Presence and quiet panels. Tall log recipe: Several panels.

Usually you need

WantDo
Normal dialogueLeave text_mode off (replace)
One log panelSticky panel with "text_mode": "append" (+ max_height if it should fill the side)
Chat-style threadSticky panel with "text_mode": "plates" + preset plates / character ui.default_plate
Fresh thread, same modeclear_append
Single appended beat"text_mode": "append" only on that moment

Next

Speaker art on the panel

Speaker art on the panel

Lives in: theme panel slots + character pack ui.panel_slots

Some panel presets reserve a slot for a picture — a bust, icon, or object that follows who is talking. That art is not a stage character place; it is chrome inside the box.

Theme slot

The preset declares a slot id (for example speaker_art) with position and size. Details grow with the theme map; copy a working preset from examples/ when you want a portrait strip.

Pack map

Point the slot at an object (or sprite) id the character owns:

"ui": {
  "panel_slots": {
    "speaker_art": "hana_portrait"
  }
}

When this character is first speaker / routed, the panel can show that art. Theme binding "bind": "talking" on panel chrome is speaker art, not a story-flag bind — Expressions and packs · Reacting to flags live.

With joint lines

Only the first speaker in talking drives panel routing and slot art unless you set panel_id yourself — Lines and speakers.

Usually you need

WantDo
No portrait in the boxLeave panel_slots empty
Bust beside the textTheme slot + pack panel_slots map
Full-body on stageStage character — Places on stage

Next

Look chrome around the box: Theme and the dialogue box · Name plate

Theme and the dialogue box

Lives in: themes/default.json

Fonts, colours, and shared chrome for the whole game. The dialogue box (presets, routing, height, scroll) lives in its own part of the handbook:

→ What a panel is · How a panel looks

Where the theme lives

PieceIn themes/default.json
Fonts and sizestypography
Fallback text / empty-stage colourscolors
Dialogue box presetspanel.<name> — How a panel looks
Name-plate styleslabelplate.<name> — Name plate
Scrollbarsscrollbar (menus and panel body)
Menu / screen buttonsbuttons — Buttons and actions

Put font files under fonts/. Locale overlays can sit beside the base theme as default_<locale>.json. Pack files for choices and inputs are separate stems under themes/ — Choice and input looks.

"extends" on a panel preset merges keys — Reusing settings.

Fonts and sizes

"typography": {
  "font": "Jupiteroid",
  "font_menu": null,
  "font_dialogue": null,
  "font_label": "Jupiteroid",
  "size_scale": 1.0,
  "sizes": {
    "speaker": 45,
    "dialogue": 30,
    "choice": 30,
    "button": 30,
    "notification": 30
  }
}
KeyEffect
fontBase face under fonts/ (variants like Regular / Bold when present)
font_dialogueDialogue + choices (null → font)
font_labelSpeaker name plate
font_menuMenus / settings buttons (null → font)
sizes.*Pixel sizes for speaker, dialogue, choice, button, notification
size_scaleMultiplies all sizes.* (player text-size setting builds on this)

Change sizes.dialogue or font, reload (F5 / F6 for look), and play a line.

Colours

"colors": {
  "background_default": "#dfdcd7",
  "speaker_text": "#c4693a",
  "dialogue_text": "#e8e5e0",
  "choice_text": "#c5cad5",
  "notification_text": "#dfdcd7"
}
KeyEffect
dialogue_textBody text fallback
speaker_textName-plate text fallback
choice_textChoice caption fallback
background_defaultFill when no background picture is up
notification_textToast / some heading fallbacks

Panel plate colour is usually panel.<preset>.bg, not colors — How a panel looks.

Full map

Every allowed key in one view: themes/default.json file map. Panel, buttons, and choice/input packs: Theme detail.

Mood for one stretch of story

A mood profile is another theme file (for example themes/flashback.json) that merges over default.json for a while:

{ "type": "theme_style", "profile": "flashback" }
{ "type": "theme_style", "reset": true }

Sticky until you reset or start a new game. It does not switch choice packs — use a choice style command / pack for that — Choice and input looks.

Until you add a mood file, a second panel preset or a sticky panel command is enough for a quieter box.

Usually you need

WantDo
Bigger dialogue textRaise typography.sizes.dialogue
Warm name colourcolors.speaker_text or a labelplate style
Dimmer platepanel.default.bg
Whole scene moodtheme_style profile file

Next

Name plate

Name plate

Lives in: themes/default.json → labelplate.*, plus character packs (display_name / ui)

The box that shows who is speaking.

What the character pack sets

Pack keyEffect
display_nameText on the plate for that speaker (usual)
display_name_keyCatalog key when you translate names
ui.default_labelplate_style_keyWhich name-plate style from the theme this character prefers
ui.default_panel_id / default_panel_presetWhich dialogue panel this character prefers

Packs: Expressions and packs. Which box shows the name: How text finds a panel. Speakers and joint labels: Lines and speakers.

Theme styles

Named looks under labelplate.<style_key>:

"labelplate": {
  "default": {
    "font_color": "#ffffff",
    "font_size": 20,
    "bg": "#00000099",
    "padding_left": 12,
    "padding_right": 12,
    "padding_top": 6,
    "padding_bottom": 6
  },
  "whisper": {
    "font_color": "#cccccc",
    "bg": "#00000066"
  }
}
KeyEffect
font_color / font_size / fontName text
boldBold face when the font provides it
bgFlat plate colour
bg_texture / bg_9sliceImage plate
padding_*Inner margins

Resolution: start from labelplate.default (if present), then overlay labelplate.<style_key>.

Who picks the style

First match wins:

  1. Explicit style_key on a labelplate command / moment field
  2. Panel preset labelplate_style_key
  3. Character pack ui.default_labelplate_style_key

Inline vs docked

By default the name sits inline above the dialogue text. If the panel preset sets labelplate_layout (anchors + offsets), the plate can dock to a panel edge instead — How a panel looks.

Story override

A direction labelplate command (or moment field) can change text, style, or visibility for a beat:

{
  "type": "labelplate",
  "panel_id": "main",
  "text": "???",
  "style_key": "whisper",
  "apply": "persistent"
}
FieldEffect
text / text_keyPlate body ({@auto} expands to the speaker)
style_keyTheme style name
visibleShow / hide
mode"inline" or "docked"
apply"persistent" (sticky) or "node_local" (this moment)
resetClear sticky override

Prefer pack display_name for everyday cast names; use the command for mystery labels and one-off looks.

Usually you need

WantDo
Show “Hana”Pack "display_name": "Hana"
Special colour for one cast memberTheme style + ui.default_labelplate_style_key
Hide the plate for a beatlabelplate with "visible": false
“???” then revealSticky text: "???", later reset or new text

Next

Choice and input looks

Choice and input looks

Lives in: themes/<pack>.json, plus layout / story fields

How choice buttons and the name-entry field look. Story choices and input flow: Choices and memory. Layout defaults: Choice and character layout.

Choice packs

A pack is a JSON file under themes/. The name is the file stem: "style": "choice_button" → themes/choice_button.json.

Two common roles:

RoleHow you point at itWhat’s inside
Button looklayout.choices.style, node choices.style, or sticky choice_styleTextures / flat colours / fonts for stacked text buttons
Group composechoices.source / container.source / item.sourceLayout + item chrome for free / sprite / overlay menus

"style" / "source" merge rules: Reusing settings. Locale overlays: themes/<name>_<locale>.json.

Button pack (usual ADV)

{
  "min_width": 220,
  "padding_left": 18,
  "padding_right": 18,
  "padding_top": 4,
  "padding_bottom": 6,
  "normal_texture": "sprites/button-normal.png",
  "hover_texture": "sprites/button-hover.png",
  "pressed_texture": "sprites/button-down.png",
  "texture_9slice": [6, 6, 6, 6],
  "font_color": "#3a2a1a",
  "font_hover_color": "#6b4420"
}

Flat colour keys (normal_bg, borders, …) work when textures are omitted or empty. Optional click_sfx / hover_sfx override default UI sounds for button choices.

Choice packs do not restyle screen/menu buttons (those use theme buttons) or the dialogue panel plate.

Sticky choice look

{ "type": "choice_style", "style": "choice_plain" }

Stays until you change it. Separate from theme_style mood profiles — Theme and the dialogue box.

Text input packs

Optional pack for story name-entry / text fields. No pack required — omit style and the field uses dialogue colours and flat fills.

PieceKeys (examples)
Fieldfield_font_color, field_normal_bg / textures, padding, nine-slice
Confirm buttonSame family as choice/nav packs (normal_texture, font_color, …)

Precedence: moment input.style → sticky input_style → layout.text_input.style → defaults.

theme_style does not restyle input chrome (same rule as choice packs).

Usually you need

WantDo
Default stacked choices"style": "choice_plain" (or your pack) in layout.choices
Different look this sceneSticky choice_style or node choices.style
Custom name fieldA text-input pack + layout.text_input.style

Next

Free choices · Title, pause and settings screens · Choice packs · Text input packs

Free choices

Most menus are a stack of buttons. Free choices place options at absolute positions — hotspots on the stage, sprite faces, inspect scenes. Same story choices object; different container.layout.

Most stories

  • Keep script > lines for ordinary menus (stack).
  • For clickable art: write the choice as JSON in story.json with "mode": "free", item x / y, often "render_mode": "sprite".
  • Do not combine shuffle or stack gap with free.

Where this lives

IdeaPath
Group modechoices.container.layout.mode: "stack" | "free"
Hostpanel (dialogue slot) or overlay (full screen)
FacesItem / pack style.sprite + state_visual
Defaultsgame.json → layout.choices
"choices": {
  "show_numbers": false,
  "container": {
    "layout": { "mode": "free", "host": "overlay", "render_mode": "sprite" }
  },
  "items": [
    { "text": "Door", "target": "enter", "layout": { "x": 420, "y": 280 } },
    { "text": "Leave", "target": "NEXT", "layout": { "x": 900, "y": 600 } }
  ]
}

Worked pack: examples/example-05-ui-and-polish/themes/choice_yes_no.json.

Same items, same mode: free and render_mode: sprite. The only difference is host: overlay leaves them floating at their absolute x / y, while panel puts them inside the dialogue plate, which grows to fit.

Stack vs free

stackfree
PlacementVertical listAbsolute x / y
gap / shuffleOKForbidden
Typical lookTheme buttonsSprites + state_visual

Hover / pressed looks: Shared shapes → state_visual. Full keys: Choices (story). Ordinary menus: Choices and memory · Choice and input looks.

Title, pause and settings screens

Lives in: screens.json

Menus and boards around the story — title, pause, settings, credits, custom galleries. Separate from the dialogue panel and from stage sprites.

Most stories

ScreenRoleJob
titlemodalNew Game, Continue, Settings, Quit
pausemodal (often pauses the tree)Resume, save/load, settings, title
settingsmodalVolume, text speed, and similar rows
hudhudPause button (and friends) while playing
{
  "defaults": { "transition_duration": 0.3 },
  "title": {
    "role": "modal",
    "layers": [
      { "type": "background", "order": 0 },
      {
        "type": "menu",
        "order": 10,
        "buttons": [
          { "label": "New Game", "interaction": { "hooks": { "on_click": "new_game" } } },
          { "label": "Continue", "interaction": { "hooks": { "on_click": "continue" } }, "show_if": "has_autosave" }
        ]
      }
    ]
  }
}

Copy a working screens.json from examples/ and change labels first — Your first ten minutes.

Full map

Every allowed key in one view: screens.json file map. Layers, blocks, and actions: Screens detail.

Roles

RoleEffect
modal (default)Full menu; blocks the world; not persistent
hudStays up during play; never pauses; input passes through by default
sceneUnder characters (special boards)

Auto HUD id comes from game.json → session.hud_screen (default "hud"; "" turns it off) — Save, Back and story end.

Layers

Each screen is a list of layers drawn in order:

typeJob
backgroundFull-rect plate / dimmer (often click-outside → close)
menuButton column (title, pause, HUD)
contentCard of blocks (text, sliders, history, save slots, …)
spritesAnchored UI pictures with optional hooks
chromeExtra controls (Close), often docked to a content card

Open and close

FromHow
Button / hookinteraction.hooks.on_click holds the action: new_game, screen:pause, close, …
Story{ "type": "screen", "value": "pause" } · "show": false to remove · "mode": "replace"

Screens stack. Top modal usually owns input; HUDs can stay underneath. Return to title clears persistent HUD strips.

show_if on buttons (and some blocks) hides them unless a condition is true — e.g. "has_autosave".

Save / load and history

Slot UI and history logs are content blocks inside your screens. Slot count is session.save_slots — Save, Back and story end. Polished slot chrome is still early — In development.

Usually you need

WantDo
Change title buttonsEdit screens.json → title menu labels / actions
Pause from HUDHUD button "screen:pause" — Gameplay HUD
Open a board mid-storyscreen command with that id

Next

Gameplay HUD · Buttons and actions

Gameplay HUD

Lives in: screens.json → screen with role: "hud"

A small bar (or corner controls) shown while the story plays — pause, backlog, and similar.

Default strip

"hud": {
  "role": "hud",
  "layers": [
    {
      "type": "menu",
      "order": 10,
      "layout": {
        "host": "panel",
        "anchor_h": "right",
        "anchor_v": "top",
        "offset_x": 8
      },
      "buttons": [
        {
          "label": "",
          "icon": "sprites/ico-pause.png",
          "interaction": { "hooks": { "on_click": "screen:pause" } }
        }
      ]
    }
  ]
}
IdeaEffect
role: "hud"Persistent during play; does not pause; clicks pass through empty areas
layout.host: "panel"Dock relative to the dialogue panel
host: "screen"Dock to the window instead
Auto-showsession.hud_screen names which HUD id appears when play starts

Several different HUD ids can be up at once. Story can push another strip:

{ "type": "screen", "value": "minimap" }
{ "type": "screen", "value": "minimap", "show": false }

Usually you need

WantDo
Pause buttonHUD menu button → "screen:pause"
Hide HUD entirely"hud_screen": "" in session
Second strip mid-storyAnother role: "hud" screen + screen command

Menus around the story: Title, pause and settings screens. Button actions: Buttons and actions.

Next

Buttons and actions

Buttons and actions

Lives in: screens.json buttons · theme buttons / named packs

What menu and HUD buttons do, and how they look.

Actions you use most

ActionEffect
new_gameStart from the beginning (optional new_game:<story_id> for multi-pack)
continueResume autosave / continue slot
screen:<id>Open that screen
closePop the top screen
quitLeave the game
save / loadOpen your save/load screens (when you built them)

Hooks can use the same strings, e.g. "on_click": "screen:pause". Conditions: "show_if": "has_autosave" (and similar) hide a button until it makes sense.

Full screen structure: Title, pause and settings screens.

How buttons look

SurfaceWhere
Title / pause / settings / HUDTheme root buttons (and optional named button packs)
Story choice rowsChoice packs — Choice and input looks

Typical theme keys (flat or textured): normal / hover / pressed / focus colours or textures, corner radius, padding, font colours. Named packs under themes/ can override a screen’s button look the same way choice packs do — Reusing settings.

Default click / hover sounds come from game.json → sounds unless the pack sets click_sfx / hover_sfx — Music and sounds by default.

Usually you need

WantDo
Title “New Game”Menu button hook "on_click": "new_game"
Open pause"on_click": "screen:pause"
Hide Continue until a save exists"show_if": "has_autosave"
Match choice buttonsChoice pack — not theme buttons

Next

Polish for menus: Transitions · Cursor · or back to Theme and the dialogue box

Transitions

Lives in: commands · game.json → transitions · packs / screens

How pictures and menus appear, leave, and swap. Three related systems use the word “transition” — keep them apart.

Kinds (stage and chrome)

KindEffect
instantSnap
fadeOpacity / crossfade
slide_up / slide_down / slide_left / slide_rightSlide

Fields: transition (or transition_in / transition_out) and duration (or transition_duration).

[bg street fade 0.5]
[hana left slide_left 0.3]
{ "type": "bg", "value": "street", "transition": "fade", "duration": 0.5 }
{ "type": "char", "name": "hana", "transition_in": "slide_left", "duration": 0.3 }

Defaults (when you omit fields)

SurfaceDefault kindDefault duration
Backgroundfade0.5
Character show/hideinstant0
Sprite / objectinstant0
Screensoften fade from fade_default~0.2 in samples

Do not fight opacity with animate on the same surface at the same time — Animation.

Scene fade (full-screen veil)

Separate from per-command fades. Timing in game.json:

"transitions": { "fade_default": 0.35 }

Used for the full-screen scene veil and as a fallback when a screen has no own transition. Not the same as the bg command’s 0.5s default.

Expression swaps (characters)

Same person, new look while already on stage → pack composition_transitions, not show/hide sugar — Expression fades.

Screens

Screen push/pop can set transition_in / transition_out / transition_duration on screens.json defaults or per screen — Title, pause and settings screens.

Panel plates (thread chrome)

Named plates under panel.<preset>.plates can set their own transition_in / transition_duration for each new beat. That motion stays inside the panel body scroll and is separate from whole-panel show/hide — Theme: panel → Plates.

Usually you need

WantDo
Soft place change[bg name fade 0.5] (or omit — fade is default)
Soft character enterName a kind + duration on char
Faster scene veilLower transitions.fade_default
Soft expression changePack composition_transitions
Chat plate enterPer-plate transition_in on the theme preset

Next

Animation

Animation

Lives in: story animate / stop_animate · optional animations/presets.json

Tweens (or steps) a property on something already on stage. Show/hide fades are a different system — Transitions. Character flipbooks live in the pack — Motion and frames.

Most stories

{
  "type": "animate",
  "target": "char:left",
  "property": "position_x",
  "to": 20,
  "duration": 0.3,
  "relative": true
}
{ "type": "animate", "preset": "shake", "intensity": 8, "duration": 0.5 }
{ "type": "animate", "preset": "bob", "target": "sprite:fairy", "distance": 8 }
{ "type": "stop_animate", "target": "char:left" }
{ "type": "stop_animate", "all": true }

Built-in presets: shake, pulse, flash, bob. Author presets: files under animations/presets.json (locale overlays allowed).

Targets

TargetWhat it drives
bgBackground plate
char:<place>Character in that place (left, center, …)
sprite:<id> / obj:<instance_id>Stage prop
panelDialogue panel
screen_shaderFull-screen shader rect
choice_item:… / ui_button:… / cursorUI / cursor

Common properties: opacity, position_x / position_y, scale, modulate, shader:<uniform>. Give an id when you need a precise stop_animate.

Do not animate opacity on a surface while a transition is also fading that surface.

Usually you need

WantDo
Nudge a characteranimate on char:… + position_x / y
Hit feedback"preset": "shake"
Soft idle on a sprite"preset": "bob"
Stopstop_animate on the target (or "all": true)

Full map

animations/presets.json map · Animation presets · animate

Next

Shaders

Shaders

Lives in: shaders/ · commands (char, bg, sprite, obj, panel, screen_shader) · plate shader (target: "bg" only)

Visual filters from .gdshader files. Attach by name (no extension), or clear with null.

On a character or prop

{
  "type": "char",
  "name": "hana",
  "position": "left",
  "shader": "drop_shadow",
  "shader_params": { "blur": 2.0 }
}
ValueEffect
key absentKeep whatever is already on
nullClear
stringApply shaders/<name>.gdshader
objectFull form — see below

Pack files do not list shaders — the story (or a bind) does. Drive a uniform from a flag with bind and a shader:… key — Reacting to flags live.

Backgrounds: Backgrounds.

Full object

{
  "name": "hotspot_glow",
  "params": { "glow_radius": 45 },
  "target": "full",
  "pass": "subviewport",
  "margin": 48
}
KeyEffect
nameFile stem under shaders/
paramsUniforms (numbers, #hex, vectors)
targetbg | full — allowed values differ by surface (see below)
passdirect or subviewport
marginExtra pixels so glow is not clipped (forces subviewport when > 0)

Which forms each surface accepts, its target values, and where the effect sits is the per-surface matrix: only char / bg / stage take a bare string; sprite / obj / panel / screens need the object. Every effect is one shader per slot / instance, never per layer or part.

On a named plate (panel.<preset>.plates.<name>.shader), only target: "bg" applies — Theme: panel → Plates.

Whole-screen effect

{ "type": "screen_shader", "value": "glitch", "target": "game", "shader_params": { "noise_intensity": 0.2 } }
{ "type": "screen_shader", "value": null }

target here is the screen-shader plane (game / scene / all) — coverage map: Planes and slots. Clear with null.

Usually you need

WantDo
Soft shadow on a character"shader": "drop_shadow" (+ params)
Clear it"shader": null on a later char
Room-wide vibescreen_shader
Uniform follows a flagbind → shader:…

Next

Cursor

Cursor

Lives in: game.json → cursor · optional cursors.json

Optional custom (software) pointer, or the normal OS cursor.

Most stories

Leave cursor out (or "enabled": false) → OS cursor. Soft cursor:

"cursor": {
  "enabled": true,
  "default": "default",
  "config": "cursors.json",
  "touch_pointer": false,
  "idle_hide": {
    "enabled": true,
    "delay": 2.5,
    "fade": 0.2,
    "keep_visible": ["choices", "screens"]
  }
}
// cursors.json
{
  "default": "default",
  "profiles": {
    "default": {
      "kind": "image",
      "image": "sprites/pointer.png",
      "hotspot": [2, 2],
      "scale": 1.0
    }
  }
}

Soft cursor follows mouse pointing. On touch it stays hidden unless touch_pointer is true.

From the story

{ "type": "cursor", "profile": "inspect" }
{ "type": "cursor", "push": "inspect" }
{ "type": "cursor", "pop": true }
{ "type": "cursor", "reset": true }

Hover hotspots can use hook actions cursor:push:… / cursor:pop. Idle-hide still works with the OS cursor (instant hide/show; fade is soft-cursor only).

Usually you need

WantDo
Normal pointerOmit soft cursor
Custom lookenabled: true + cursors.json
Hide while readingidle_hide
Inspect cursor on a hotspotcursor:push / pop hooks

Full map

cursors.json file map · cursors detail

Next

Notifications

Notifications

Lives in: game.json → notifications · theme notification

Short toasts such as “Saved.” The player shows them on save/load success and failures — you rarely fire them from the story.

Copy and timing

"notifications": {
  "duration": 2.0,
  "labels": {
    "saved": { "text": "Saved." },
    "loaded": { "text": "Loaded." }
  }
}
KeyEffect
durationHow long the toast stays (seconds)
labels.<id>.textCopy for that event (catalog keys where supported)

Look

Skin under themes/default.json → notification (colours, padding, and similar). Global text colour fallback: colors.notification_text — Theme and the dialogue box.

Usually you need

WantDo
Change “Saved.”notifications.labels.saved.text
Longer toastRaise duration
Match your themeTheme notification block

Next

Keys

Keys

Lives in: optional keybindings.json

Which keys (and mouse buttons) fire which actions. Skip the file until you need different confirm keys, a custom Back key, or controller-friendly remaps.

Remap file

Arrays you list for an action replace the defaults for that action:

{
  "dialogue_advance": ["Space", "Enter", "MouseLeft"],
  "dialogue_back": ["Backspace"],
  "pause": ["Escape"]
}

Common actions: dialogue_advance, dialogue_back, pause, plus choice numbers and menu navigation. Locale overlay: keybindings_<locale>.json.

Author builds keep debug F-keys; released player builds strip author-only shortcuts — How you work.

Back behaviour (what Back does in the story): Save, Back and story end.

Usually you need

WantDo
DefaultsOmit keybindings.json
Space to advanceSet dialogue_advance
Esc to pauseSet pause

Full map

keybindings.json file map · keybindings detail

Next

Translations

Translations

Lives in: game.json → locale / i18n · overlay files · optional story/strings.json

VN-Mikro ships one base language as the unsuffixed files, then optional locale overlays and an optional strings catalog for keyed text.

Most stories

  1. Set "locale" in game.json to the language of your base files (the ones without a suffix).
  2. List switcher locales under i18n.locales if settings should offer a language row.
  3. Add script_en.txt (or whatever locale id) when you translate the story; keep the same moment ids.
  4. Use text_key / label_key / display_name_key only when you want a shared catalog; literals still work without one.

Where this lives

IdeaFile / surfacePath
Base locale + switcher listgame.jsonlocale, i18n.locales
Overlay filesProject tree*_<locale>.* next to each base file
Key → stringstory/strings.json (+ strings_<locale>.json)catalog object
Keyed UI copyScreens, choices, buttons, packstext_key, label_key, suffix_key (sliders), display_name_key, title_key
Settings switcher rowscreens.jsoncontent { "bind": "locale", "label_key": "…" }
{
  "locale": "uk",
  "title": "Мій візуальний роман",
  "title_key": "game.title",
  "i18n": {
    "locales": [
      { "id": "uk", "label": "UK", "label_key": "screen.settings.locale_uk" },
      { "id": "en", "label": "EN", "label_key": "screen.settings.locale_en" }
    ]
  }
}
project/
  game.json
  game_en.json
  themes/default.json
  themes/default_en.json
  screens.json
  screens_en.json
  story/
    script.txt
    script_en.txt
    strings.json
    strings_en.json
  characters/hana/default.json
  characters/hana/default_en.json
{
  "game.title": "Мій візуальний роман",
  "screen.settings.locale_uk": "Українська",
  "choice.help": "Допомогти Хані"
}
{
  "settings": {
    "content": {
      "blocks": [
        {
          "bind": "locale",
          "label": "Language",
          "label_key": "screen.settings.language",
          "style": "locale_button",
          "active_style": "locale_button_active"
        },
        {
          "type": "button",
          "label": "Back",
          "label_key": "screen.settings.back"
        }
      ]
    }
  }
}

Catalog shape: Strings catalog. Map: game.json.

Effective locale

  1. Player / settings override (set_locale), if any.
  2. Else game.json → locale.

That value picks which overlay files load. An overlay may contain a "locale" field, but it does not re-choose which files were loaded in that pass.

File naming

The locale id is inserted before the extension: foo.json → foo_uk.json, script.txt → script_uk.txt.

AssetBaseLocale fileHow it combines
Gamegame.jsongame_<locale>.jsonDeep merge
Themethemes/default.jsondefault_<locale>.jsonDeep merge
Style packsthemes/<name>.json<name>_<locale>.jsonDeep merge
Screensscreens.jsonscreens_<locale>.jsonScreens merge (below)
Keybindingskeybindings.jsonkeybindings_<locale>.jsonDeep merge
Cursorscursors.jsoncursors_<locale>.jsonDeep merge
Character packscharacters/<id>/<pack>.json<pack>_<locale>.jsonDeep merge
Animation presetsanimations/presets.jsonpresets_<locale>.jsonDeep merge
Scriptstory/script.txtscript_<locale>.txtWhole-file pick (no merge)
Story JSONstory/story.jsonstory_<locale>.jsonWhole-file pick
Stringsstory/strings.jsonstrings_<locale>.jsonCatalog pick (below)

Missing overlay → keep the base file. No crash.

Merge rules

FamilyRule
Most JSON overlaysDeep-merge objects; arrays replace wholesale; null deletes a key
ScreensSame, but arrays of objects merge by id if present, else by order. Patch only what changed
Script / story JSONPrefer the locale file if it exists; otherwise the base file

Strings catalog

Optional. Used when fields carry *_key.

SituationCatalog used
Locale equals project base localestrings.json only
Other locale, strings_<locale>.json existsThat file
Other locale, postfix missingFall back to strings.json if present
Nothing foundEmpty catalog — keys miss, literals win

Missing key → use the literal field (or the surface’s default). Games can ship with no catalog at all.

Key fields vs literals

SurfaceKeyLiteral
game.jsontitle_keytitle
Screens (heading, text, links, …)text_keytext
Buttons / menus / chromelabel_keylabel
Settings sliderssuffix_keysuffix
Choicestext_keytext
Character packsdisplay_name_keydisplay_name

Prefer key-only: set the *_key and put the copy in story/strings.json. Omit the sibling literal so the catalog is the single source. Keep a literal only as a standalone fallback when the key is missing.

Usual order: non-empty key and found in catalog → catalog value; else literal; else surface default.

check reports a keyed field that has no literal and no entry in the base catalog (i18n.missing_key).

Special cases:

  • Button label: "" stays blank even if label_key is set (icon-only).
  • Labelplate prefers a non-empty literal text / prefix / suffix before keys.
  • Character display name: catalog → display_name → capitalized folder name.

Settings locale switcher

A settings { "bind": "locale" } block expands into the locales in i18n.locales as chips. The active one uses active_style (default locale_button_active) and is not clickable. Locales not listed in i18n.locales are rejected by set_locale.

Mid-session language switch

When the player picks another locale:

  1. Config and theme reload for the new overlays.
  2. Story reloads (script_<locale> / story_<locale> + strings).
  3. Play restores the same node id and flags without replaying commands.
  4. If that node id is missing in the new story, the previous locale is restored and a locale_unavailable notification shows.
  5. On success: backlog relocalizes and open screens rebuild.

Keep node ids aligned across locale scripts if players can switch mid-game.

Usually you need

WantDo
One languageSet locale; skip overlays
Second languageOverlay files + i18n.locales
Shared UI stringsstory/strings.json + *_key fields

Strings catalog · Title, pause and settings screens · Theme and the dialogue box · Pictures, sound, and folders

Title, icon, splash

Lives in: game.json

How the game presents itself before the first line — window name, shell icon, and boot splash. In-game panel and title-menu chrome are separate — Theme and the dialogue box · Title, pause and settings screens.

Title, version, authors, and window size: Name, window, language.

Checklist

Set thisWhereWhat players see
Titletitle (or title_key)Window title; web page title; build folder slug; desktop app names
VersionversionBuild folder (…-v1.0); store / changelog label
AuthorsauthorsWindows EXE company field; optional credits
IconiconDesktop / window / web favicon / Android launcher
SplashsplashBoot / loading look on web, Android, and delivered desktop builds
Android packageandroid.package_idRequired for Android builds — Platforms
{
  "title": "My Novel",
  "version": "0.1",
  "icon": "ui/icon.png",
  "splash": {
    "bg_color": "#1a1a2e"
  }
}

Icon

"icon": "ui/icon.png"

Prefer a square PNG (≥256 px). One path; Build stamps each platform. Empty icon keeps the VN-Mikro player mark.

TargetResult
LinuxIcons + desktop entry beside the player
WindowsStamps the .exe (Explorer icon + version strings from title / authors)
macOS.icns in the .app; Finder name from title
WebFavicon / apple-touch icons next to index.html
AndroidLauncher icons inside the APK / AAB

If Windows stamp tools are missing on your machine (Wine / rcedit), Build can still finish with the default icon — Build and hand off.

Splash

Shared colours and logo at the root; optional web / android / desktop blocks override what that platform needs. Omitted colours are filled from your theme at Build time.

"splash": {
  "bg_color": "#1a1a2e",
  "logo": "sprites/logo.png",
  "logo_max_width": 300,
  "progress_bar": {
    "width": "50%",
    "height": 12,
    "background": "#343a52",
    "fill": "#7a8a9e"
  },
  "desktop": { "min_seconds": 0.6 }
}
KeyEffect
bg_colorSolid splash fill (else theme panel / background colour)
logoCentered brand mark
backgroundOptional full-bleed art under the logo
logo_offset_x / yNudge from centre
progress_barLoading bar (web + Android overlay; not desktop)
desktop.min_secondsMinimum brand hold on desktop production builds (0 = as soon as ready)

Platform nested blocks merge key-by-key over the root — Reusing settings.

Usually you need

WantDo
Named buildSet title + version
Your mark on the EXE / appSet icon
Matching boot flashSet splash.bg_color (and optional logo)

Next

Platforms

Platforms

Lives in: game.json · Build tab targets

What each Build target needs. How to run a build: Build and hand off. Identity (title, icon, splash): Title, icon, splash.

Desktop (Windows, macOS, Linux)

NeedNotes
title / version / optional iconNames the output folder and stamps the binary / app
OutputFolder with player + game data side by side

Windows EXE company string comes from authors. On Linux/macOS hosts, Windows icon stamping may need Wine so the kit’s stamp tool can run — Build still succeeds without it.

Web

NeedNotes
Same identity keysPage title + favicons from title / icon
SplashHTML loading shell uses splash (+ optional web overrides)
ModeAlways packed (pck)

Zip the build folder for itch / itch-like hosts; leave out BUILD_INFO.json.

Android

"android": {
  "package_id": "com.studio.mygame",
  "version_code": 1
}
KeyEffect
package_idReverse-DNS application id. Required for Android Build. Never change after the first Play upload for that listing
version_codeInteger for Play / versionCode — raise for every Play upload (Build does not auto-bump)

Root title, version, and icon become the store label, version name, and launcher icon.

TargetSigning
Android APK (itch / sideload)Optional in Build prefs; empty → debug-signed installable
Android Play (AAB)Release keystore required in Build prefs (machine-local, not in game.json)

Needs JDK 17 or 21 with java on PATH. The Build tab lists what is missing. Stamp tools ship in the author kit — you do not install an Android SDK.

One output file (.apk or .aab) with game data inside.

Usually you need

WantDo
Desktop zipBuild that OS target; ship the folder minus BUILD_INFO.json
Web uploadBuild web; set splash colours that match the game
Play Storeandroid.package_id + rising version_code + Play AAB + keystore

Next

Build and hand off

Build and hand off

Lives in: author player → F12 → Build

Turn the open project into a folder (or install file) you can give to players. Player builds do not include the author overlay or F1–F12 author shortcuts.

Overlay tabs and prefs: Author tools.

Before you build

  1. Set identity in game.json — title, version, optional icon / splash / android — Title, icon, splash · Platforms
  2. Press F12 → Validation and fix errors
  3. Play through critical paths once

Build tab

  1. Choose Target (self / linux / windows / macos / web / Android APK / Android Play — whatever your author kit ships).
  2. Choose Mode: pck (default) or folder where offered. Web and Android always use pck (Android embeds the pack inside the APK/AAB).
  3. Set an output folder (absolute path), or Use CWD / Pick….
  4. Leave Filter unused assets and Run prebuild checks on unless you have a reason not to.
  5. For Android Play (AAB): set signing fields (release keystore). For Android APK (itch/sideload), signing can stay empty — Build uses a debug keystore automatically. Install JDK 17 or 21 so java works on PATH; the Build tab warns if it is missing.
  6. Press Run Build.
  7. Use Open Folder, Copy Path, or Copy Log as needed.

Output lands in {game-slug}-{platform}-v{version}/ under the path you chose (title and version come from game.json). That folder is flat: player binary + game data side by side. Building again to the same name replaces the folder, so you never end up with both data.pck and assets/ from two modes.

Prefs are remembered in vn-mikro-debug.cfg next to the player.

What to give players

Give players the build folder contents except BUILD_INFO.json. That file is an author working receipt (kit version, mode, paths) — leave it out of zips, itch uploads, and store packages.

In the build folderShip to players?
Player binary (.exe / binary / {slug}.app / web shell)Yes — named from game.json → title (slug). macOS Finder label uses the title
{slug}.apk or {slug}.aab (Android)Yes — one file; game data is already inside
data.pck or assets/Yes (desktop/web; not left beside Android APK/AAB after a successful stamp)
Linux icons/ + .desktop (Linux builds)Yes
Leftover macOS player zip / extensionless zip copyNo — Build removes it after expanding {slug}.app
BUILD_INFO.jsonNo — author-only working file
ModeResult
pck (default)Player binary + a packed data.pck (game data in one file)
folderPlayer binary + an assets/ tree (readable files next to the exe)

Web: copies the kit’s web shell into the build folder and always writes sibling data.pck. Serve that folder over HTTP (not file://). Splash / favicon: Title, icon, splash. Omit BUILD_INFO.json when you upload the web folder too.

Android: needs game.json → android (package_id, optional version_code) and JDK 17 or 21 with java on PATH — Platforms. APK (itch/direct) can omit Build signing prefs (debug-signed). Play AAB needs your release keystore in prefs. Two targets write separate folders (…-android-v… and …-android-play-v…). Keystore passwords stay in local prefs — do not commit them with the story.

Filter and checks

Prebuild checks fail early if there is no story (script.txt or story.json) or other required pieces are missing.

Filter unused assets (on by default) keeps known runtime folders (themes/, story/, characters/, audio/, backgrounds/, sprites/, animations/, shaders/, ui/, fonts/, extensions/) plus root config JSON and any folder that holds a declared native extension entry. It is not a full “referenced file” scan — turn the filter off if something you need is still getting skipped.

A successful build is a player package: no debug overlay, no project picker, no author F-keys. Players run the binary; they do not edit your source folder.

Usually you need

WantDo
Desktop demoBuild your OS → zip folder minus BUILD_INFO.json
itch webBuild web
Sideload AndroidBuild Android APK (JDK on PATH)
Play ConsoleBuild Android Play + keystore + bump version_code

Author tools · Title, icon, splash · Platforms · Native extensions · Runtime version

Runtime version

This handbook matches runtime 0.1.0.

That number is the VN-Mikro player / author kit version — the app you write and build with. It is not the version of your novel.

Runtime vs game version

VersionWhere it livesWho cares
RuntimeAuthor kit MANIFEST.json → runtime_version; debug overlay Meta line (vn-mikro …)Matching kit, players, and this handbook
GameYour project game.json → versionStores, changelogs, Build output folder names

Mixing kits from different runtime versions can break Build targets or behaviour. Prefer one kit; check Meta or MANIFEST.json if something looks off.

Native extensions may set runtime_min so an older player skips a pack that needs a newer kit — extensions.

Where to see it

  1. Debug overlay (F12) → Meta — starts with vn-mikro and the runtime version.
  2. Author kit root — MANIFEST.json field runtime_version.
  3. This sidebar — the top link shows the same number when the handbook was built for that kit.

Related: Author tools · Build and hand off · Name, window, language (version).

Native extensions

Advanced. Most projects should stay on script.txt / JSON. Use a native extension only when you need custom systems the declarative surface does not cover (inventory, custom parallax math, a one-off minigame).

A native extension is a GDScript entry file your project declares in game.json. The player compiles and loads it at startup. The runtime is built with Godot; this is the one handbook place that expects you to write GDScript against a small host API.

This page is the runtime integration map — how your Node lives inside the player. Method tables live on extensions (game.json).

When to use

PreferWhen
Script / JSON / screensDialogue, flags, layouts, most FX (shake, flash, shaders)
First-class runtime feature (later)Patterns many games would copy the same way
Native extensionGame-specific logic you will maintain yourself

Mental model

Your entry Node  →  parented under reserved "Extensions" as ext_<id>
Visuals          →  children of host.get_plane("…") CanvasLayers
Story            →  host (flags, commands, jump, signals, blocker)
Screens / toast  →  host.push_screen / notify, or authored screens.json
Godot power      →  only on nodes you create (process, input, particles, …)
  • Host is the stable contract. Prefer it over digging into private player nodes.
  • Full engine power on your subtree is intentional (unsandboxed). That does not mean every scene path is supported.
  • There is no long-term stability promise beyond the documented host API. Pin runtime_min to the player version you tested.

Trust model

  • Unsandboxed: files, network, process — anything the OS user can do.
  • Author mode: toast after title listing loaded id(s) (“unrestricted code”).
  • Production / shipped: silent load; pack is part of the product you publish.
  • Author Refresh (F5) reloads extensions in author mode. Production still loads once at boot.

Example: examples/example-06-native-extension/.

Lifecycle

MomentWhat happens
Cold bootAfter core wiring: load every extensions[] entry; call vn_extension_setup(host) once per success
TitlePhase "title"; modal title is up; your nodes already exist unless you hid them
PlayPhase "playing"; story flags/commands/signals fire
Session end → titleInput blocker cleared; story reloads; your extension Node is not torn down — reset on connect_story_ended if needed
F5 (author)Free all Extensions children; unregister ext.* saves and ext_* command handlers; load_all again; toast
ProductionNo F5 reload — edit .gd → rebuild / restart the player

There is no teardown / unload API beyond F5’s full reload. Design for long-lived nodes.

Where visuals and UI sit

Planes are fixed. You choose which plane your content attaches to via host.get_plane(id).

PlaneTypical use from an extension
bgParallax / under-character FX
overlayOver panel, under HUD/modals
hudAlways-visible chrome (still under modals)
notifyCustom toasts / banners above most UI
modalDraw over title/pause (use carefully — don’t steal all input)
cursor / transitionRare; easy to break chrome

Full plane list for get_plane: extensions (game.json). Authored menus: Title, pause and settings screens + host.push_screen(id).

Title and pause are modal screens (high layer). Children on overlay stay under them. To appear above title, parent to modal or notify, or use host.notify.

Pause vs dialogue blocker

Two different gates:

MechanismWhat it doesExtension tip
Screen pauses_tree (pause menu)Tree pauses — nodes with default process mode freezeSet process_mode = Node.PROCESS_MODE_ALWAYS on your extension root and any child that must animate or take input while paused
host.set_input_blocker(true)Blocks dialogue advance and choice confirm onlyUI screens and pause still work; call host.resume() or set_input_blocker(false) to clear

host.resume() clears the input blocker only — not story history, not pause, not a return from jump.

Input

  • Dialogue UI often consumes clicks for advance. Prefer _input (not only _unhandled_input) when you need clicks during play — see ex06 click_particles.
  • While a modal screen is up, world input is usually blocked; your node still needs a process mode that runs if you want events under pause.
  • Gate by phase when FX should not run on title: host.get_phase() → "title" | "playing" | "exit_commands" | "story_ending".

Story integration

NeedUse
React to nodes / flags / endconnect_node_entered / connect_flag_changed / connect_story_ended
Private state in savesregister_save → stored as ext.<id>.<key>; restore runs after core
Drive from scriptregister_command("verb", …) → type ext_<id>_verb in Mode A or JSON
Rich command fieldsPrefer JSON / Mode B objects; Mode A mainly gives "args" plus key=value tokens
Soft lock the lineset_input_blocker(true) then jump / resume when done
Toastnotify(message) (draws above modal)

Flags are shared with the story — prefer names like ext_<id>_score. Check allowlists ext_* only for ids declared in game.json.

Recipes

Always-on FX (runs under pause)

func vn_extension_setup(host) -> void:
	process_mode = Node.PROCESS_MODE_ALWAYS
	var plane := host.get_plane("overlay")
	var fx := CPUParticles2D.new()
	fx.process_mode = Node.PROCESS_MODE_ALWAYS
	plane.add_child(fx)

Visible over title

Parent to host.get_plane("modal") or "notify", or call host.notify("…"). Still set PROCESS_MODE_ALWAYS if the title path ever pauses the tree.

Minigame beat

  1. host.set_input_blocker(true)
  2. Show your UI on a plane (or push_screen an authored screen id)
  3. On success: host.set_flag(…), host.jump("next_node"), host.resume()

Title-only decoration

func _process(_dt: float) -> void:
	visible = (host.get_phase() == "title")

(Keep a host reference from setup; or poll only when you care.)

Command-driven effect

host.register_command("burst", func(cmd: Dictionary) -> void:
	# cmd.type == "ext_my_id_burst"; Mode A may set cmd.args
	pass
)

Story: [ext_my_id_burst power=2] or { "type": "ext_my_id_burst", "power": 2 }.

Do / don’t

DoDon’t
Parent visuals via get_planeAssume deep paths into private player UI are stable
Use Host for story, save, screens, blockerExpect Host to wrap every engine API
Reset on connect_story_endedAssume session end frees your extension
Author new menus in screens.jsonClone title by digging screen-manager internals
Namespace flags ext_<id>_*Stomp story flags by accident
Pin runtime_minRely on undocumented private methods

Failure modes (short)

Missing file, bad GDScript, missing vn_extension_setup, duplicate id, or runtime_min too new → that entry is skipped; siblings still load. Details: extensions API.

extensions (game.json) · Screens · Save, Back and story end · Build and hand off · game.json map

How to use the reference

The guide parts teach one idea at a time. The reference is for when you already know what you want and need the exact name.

You want to…Open
See everything a file can hold, at a glanceFile maps
Find something by nameEverything A–Z — use your browser’s search (Ctrl+F / ⌘F)
Find where the full option tables liveDetail pages
Understand a shape that appears in many placesShared shapes
Look up a word these docs useWords used in these docs

You never need to read the reference top to bottom.

Guide vs map vs detail: How these docs work.

Check your project

Do not rely on memory (or an AI guessing) for whether a project is valid. With the author kit:

  1. Open the project in the author player.
  2. F12 → Validation — broken jumps, unreachable branches, missing assets for the active story pack.
  3. Fix what the player reports when it loads the project; a kit may also ship a separate check tool.

Fix what Validation reports, then Build. Overlay details: Author tools.

File maps

A file map shows a whole settings file as it would look written out. Overview links jump to sections. Each section has a plain key tree, a Keys list (type, If omitted, values, note), and one Learn more link to the guide.

The tree shows an example write. If omitted is what the player uses when you leave that key out. You only write the keys you want to change.

FileWhat it holdsMap
game.jsonName, window, text speed, sound, saving, layout, splashgame.json
story/story.jsonMoments as data (array of nodes)story/story.json
screens.jsonScreens you name (title, pause, …) plus optional defaultsscreens.json
themes/default.jsonFonts, colours, dialogue box, buttons, controlsthemes/default.json
characters/<id>/<pack>.jsonLayered character packscharacters
cursors.jsonPointer picturescursors.json
keybindings.jsonKeyskeybindings.json
sprites/<id>.obj.jsonReusable objectsobjects
animations/presets.jsonYour own animation presetsanimations

game.json — file map

Every game has one game.json in the project folder. Most games set only title, window and a few session keys — everything else has a sensible default.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "title": "My Novel",         // game name
  "title_key": "game_title",   // translated game name
  "version": "1.0.0",          // your version label
  "authors": ["Your Studio"],  // credits / company
  "icon": "icon.png",          // app icon path
  "window": { … },             // base screen size; the picture scales to any window
  "locale": "en",              // starting language
  "i18n": { … },               // languages offered in Settings
  "typing": { … },             // how dialogue text appears
  "sounds": { … },             // title music, voice beeps, button sounds
  "transitions": { … },        // default fade between scenes and screens
  "notifications": { … },      // short messages like Saved
  "session": { … },            // saving, Back, start and end of the story
  "layout": { … },             // where choices, name entry and characters go on screen
  "cursor": { … },             // custom mouse pointer
  "splash": { … },             // boot / loading look before the title on web, Android, and delivered desktop. Draw order: bg_color → background → logo → progress bar. Root keys are shared; web / android / desktop only override that platform
  "android": { … },            // Android store identity for builds
  "stories": [ { … } ],        // several story packs in one game (early)
  "extensions": [ { … } ]      // advanced: your own code packs
}

Root keys

Top-level keys that are not objects.

title string
If omitted empty — window/build name fall back
Values string
Name on the window, web tab, app file and build folder.
title_key string
If omitted use title instead
Catalog key for the title when you use translations.
version string
If omitted empty string ""
Shown in build folder names (e.g. …-v1.0).
authors array
If omitted empty list
Values list of strings
Credits text; also Windows EXE company field.
icon path
If omitted engine default icon
Values project path
Square PNG stamped for every platform (desktop, web favicon, Android launcher).
locale string
If omitted en
Values language code
Language code the game starts in.

Learn more: title · title_key · icon

window

Base screen size; the picture scales to any window.

{
  "window": {       // base screen size; the picture scales to any window
    "width": 1280,  // design width (px)
    "height": 720   // design height (px)
  }
}

Keys

width int
If omitted 1280
Values pixels ≥ 1
Base design width in pixels. The picture scales to any window.
height int
If omitted 720
Values pixels ≥ 1
Base design height in pixels.

Learn more: window

i18n

Languages offered in Settings.

{
  "i18n": {                   // languages offered in Settings
    "locales": [ {            // one entry per language
      "id": "en",             // language code
      "label": "English",     // name in Settings
      "label_key": "lang_en"  // translated language name
    } ]
  }
}

Keys

locales[].id string
If omitted required in each entry
Locale id (e.g. en, uk). Required in each entry.
locales[].label string
If omitted show id
Human name shown in the language list.
locales[].label_key string
If omitted use label
Catalog key for the language name instead of label.

Learn more: i18n

typing

How dialogue text appears.

{
  "typing": {                     // how dialogue text appears
    "cps": 30,                    // letters per second
    "cps_fast": -1,               // speed while advance held
    "delay": 0.07,                // seconds per letter
    "delay_fast": 0.01,           // fast seconds per letter
    "instant": false,             // always show whole lines
    "speed_scale": 1.0,           // overall speed multiplier
    "auto_advance_default": 0.0,  // auto-advance delay
    "reveal": "typewriter",       // how letters appear
    "pending_alpha": 0.0,         // untyped letter opacity
    "reveal_soft": {              // settings for the soft reveal
      "fade_chars": 2.5           // soft fade width
    },
    "reveal_pop": {               // settings for the pop reveal
      "pop_scale": 1.2,           // pop start scale
      "fade_chars": 1.5           // pop fade width
    },
    "reveal_rise": {              // settings for the rise reveal
      "rise_px": 6.0,             // rise distance
      "fade_chars": 1.5           // rise fade width
    },
    "caret": {                    // a blinking mark where typing is
      "enabled": true,            // show typing caret
      "blink": 0.5,               // blink period
      "glyph": "▋",               // caret character
      "color": "#ffffff",         // caret colour
      "offset_x": 0,              // nudge right
      "offset_y": 0,              // nudge down
      "height": "line",           // caret height
      "scale": 1.0,               // extra size
      "texture": "ui/caret.png"   // caret picture
    },
    "beep": {                     // voice beep while text types
      "enabled": true,            // play typing beep
      "sample": "typing_beep",    // beep sample id
      "volume": 0.5,              // beep volume
      "pitch_min": 0.9,           // beep pitch low
      "pitch_max": 1.1            // beep pitch high
    },
    "start_sfx": {                // sound when a line starts typing
      "enabled": true,            // play start sound
      "sample": "msg_in",         // start sample id
      "volume": 1.0               // start volume
    },
    "end_sfx": {                  // sound when a line finishes typing
      "enabled": true,            // play end sound
      "sample": "msg_done",       // end sample id
      "volume": 1.0               // end volume
    }
  }
}

Keys

cps number
If omitted use delay
Values ≥ 0 · 0 = instant
Characters per second. 0 = whole line at once. Omit to use delay instead.
cps_fast number
If omitted -1
CPS while the reader holds fast-forward. -1 means use delay_fast.
delay number
If omitted 0.07
Values seconds
Seconds per character when cps is omitted.
delay_fast number
If omitted 0.01
Values seconds
Seconds per character for fast-forward when cps_fast is unused.
instant bool
If omitted false
Values true · false
Force every line fully visible (wins over a positive cps).
speed_scale number
If omitted 1.0
Multiplies effective typing speed. Readers can change this in Settings.
auto_advance_default number
If omitted 0.0
Values seconds
Seconds after a finished line before moving on. 0 = wait for click.
reveal enum
If omitted typewriter
Values typewriter · soft · fade_line · pop · rise
Reveal style while the typing clock runs.
pending_alpha number
If omitted 0.0
Values 0–1
How visible not-yet-typed letters are (0–1). Ignored for fade_line.
reveal_soft.fade_chars number
If omitted 2.5
How many letters fade at once in soft reveal.
reveal_pop.pop_scale number
If omitted 1.2
Starting size of each letter in pop reveal.
reveal_pop.fade_chars number
If omitted 1.5
Fade width for pop reveal.
reveal_rise.rise_px number
If omitted 6.0
Values pixels
How far letters rise (px) in rise reveal.
reveal_rise.fade_chars number
If omitted 1.5
Fade width for rise reveal.
caret.enabled bool
If omitted true
Values true · false
On/off for the blinking mark at the typed frontier.
caret.blink number
If omitted 0.5
Values seconds
Seconds per blink. 0 = solid (no blink).
caret.glyph string
If omitted ▋
Character drawn when no texture is set.
caret.color color
If omitted #ffffff
Values #RRGGBB
Colour of the caret glyph.
caret.offset_x number
If omitted 0
Values pixels
Horizontal nudge in pixels.
caret.offset_y number
If omitted 0
Values pixels
Vertical nudge in pixels.
caret.height string or number
If omitted line
Values line · pixels
"line" (match line height) or a pixel number.
caret.scale number
If omitted 1.0
Scale multiplier for the caret.
If omitted draw glyph instead
Values project path
Image instead of glyph. Path under the project.
beep.enabled bool
If omitted true
Values true · false
On/off for the typing beep. Panel presets may set this to silence one surface.
beep.sample string
If omitted typing_beep
Values audio id
Sound id under audio/sfx/ played while text types.
beep.volume number
If omitted 0.5
Values 0–1
Typing beep volume (0–1), multiplied by master × SFX.
beep.pitch_min number
If omitted 0.9
Lowest random pitch for the typing beep.
beep.pitch_max number
If omitted 1.1
Highest random pitch for the typing beep.
start_sfx.enabled bool
If omitted true
Values true · false
On/off for the line-start sound. Omit the whole object for silence.
start_sfx.sample string
If omitted no start sound
Values audio id
Sound id under audio/sfx/ played once when the line begins (not on Back/load).
start_sfx.volume number
If omitted 1.0
Values 0–1
Volume (0–1), multiplied by master × SFX. Not gated by Typing Sound.
end_sfx.enabled bool
If omitted true
Values true · false
On/off for the line-end sound. Omit the whole object for silence.
end_sfx.sample string
If omitted no end sound
Values audio id
Sound id under audio/sfx/ played once when the whole line finishes (not each pause, not on Back/load).
end_sfx.volume number
If omitted 1.0
Values 0–1
Volume (0–1), multiplied by master × SFX. Not gated by Typing Sound.

Learn more: typing

sounds

Title music, voice beeps, button sounds.

{
  "sounds": {                      // title music, voice beeps, button sounds
    "main_bgm": "title_theme",     // title music id
    "main_bgm_volume": 1.0,        // title music volume
    "main_bgm_fade_in": 1.5,       // fade-in seconds
    "main_bgm_fade_in_from": 0.0,  // fade-in from volume
    "main_bgm_fade_out": 1.5,      // fade-out seconds
    "main_bgm_fade_out_to": 0.0,   // fade-out to volume
    "button_click": "click",       // button click sound
    "button_hover": "hover",       // button hover sound
    "button_volume": 1.0,          // button sound volume
    "defaults": {                  // Settings values on first launch
      "master": 1.0,               // first-launch master
      "bgm": 1.0,                  // first-launch music
      "sfx": 1.0,                  // first-launch SFX
      "typing_enabled": true       // first-launch beep on/off
    }
  }
}

Keys

main_bgm string
If omitted no title music
Values audio id
BGM id for the title screen.
main_bgm_volume number
If omitted 1.0
Values 0–1
Volume of title BGM (0–1).
main_bgm_fade_in number
If omitted 1.5
Values seconds
Seconds to fade title music in.
main_bgm_fade_in_from number
If omitted 0.0
Values 0–1
Volume the fade-in starts from (0–1).
main_bgm_fade_out number
If omitted 1.5
Values seconds
Seconds to fade title music out.
main_bgm_fade_out_to number
If omitted 0.0
Values 0–1
Volume the fade-out ends at (0–1).
button_click string
If omitted no click sound
Values audio id
Sound id when a UI button is pressed.
button_hover string
If omitted no hover sound
Values audio id
Sound id when the pointer enters a UI button.
button_volume number
If omitted 1.0
Values 0–1
Volume for button click/hover (0–1).
defaults.master number
If omitted 1.0
Values 0–1
Settings master volume on first launch (0–1).
defaults.bgm number
If omitted 1.0
Values 0–1
Settings music volume on first launch (0–1).
defaults.sfx number
If omitted 1.0
Values 0–1
Settings SFX volume on first launch (0–1).
defaults.typing_enabled bool
If omitted true
Values true · false
Whether the typing beep starts enabled in Settings.

Learn more: sounds

transitions

Default fade between scenes and screens.

{
  "transitions": {        // default fade between scenes and screens
    "fade_default": 0.35  // default fade seconds
  }
}

Keys

fade_default number
If omitted 0.35
Values seconds
Default seconds for the full-screen scene veil and screen push/pop when a screen has no own duration.

Learn more: transitions

notifications

Short messages like Saved.

{
  "notifications": {      // short messages like Saved
    "duration": 2.0,      // toast hold time
    "labels": {           // text per message: saved · loaded · save_incompatible
      "saved": {          // any name you choose
        "text": "Saved!"  // message text
      }
    }
  }
}

Keys

duration number
If omitted 2.0
Values seconds
Seconds a short message like Saved stays on screen.
labels.*.text string
If omitted built-in label for that message
Text for one label key (saved, loaded, save_incompatible, …).

Learn more: notifications

session

Saving, Back, start and end of the story.

{
  "session": {                           // saving, Back, start and end of the story
    "boot": "title",                     // cold start screen
    "allow_back": false,                 // enable Back
    "back_retype": false,                // retype when going Back
    "back_limit": 0,                     // Back step limit
    "save_slots": 9,                     // manual save slots
    "hud_screen": "hud",                 // in-game HUD id
    "post_story_screen": "credits",      // screen after last line
    "post_story_return_to_title": true,  // return to title after end
    "default_story": "main"              // New Game story pack
  }
}

Keys

boot enum
If omitted title
Values title · new_game
"title" shows the title screen; "new_game" skips it and starts play.
allow_back bool
If omitted false
Values true · false
Let readers step back through earlier lines (scene rewinds too).
back_retype bool
If omitted false
Values true · false
Retype text when stepping back.
back_limit int
If omitted 0
Values 0 = unlimited · ≥ 1
How many steps Back may go. 0 = no limit.
save_slots int
If omitted 9
Values 1–99
Number of manual save slots (1–99).
hud_screen string
If omitted hud
Values screen id · ""
Screen id shown during play. "" turns the HUD off.
post_story_screen string
If omitted ""
Values screen id · ""
Screen when the story ends (e.g. credits). Empty = none.
post_story_return_to_title bool
If omitted true
Values true · false
After the post-story screen, go back to the title.
If omitted first story pack / main
Values story id
Which story pack bare New Game starts when you have several.

Learn more: session

layout

Where choices, name entry and characters go on screen.

{
  "layout": {                                 // where choices, name entry and characters go on screen
    "choices": {                              // choice menu defaults
      "show_numbers": true,                   // number choices
      "shuffle": false,                       // shuffle choice order
      "gap": 10,                              // space between choices
      "style": "choice_button",               // choice button pack
      "container": {},                        // menu placement box
      "item": {}                              // per-choice defaults
    },
    "text_input": {                           // name-entry defaults
      "style": "input_default",               // input look pack
      "source": "input_extra",                // extra input look
      "container": {},                        // field placement
      "field": {},                            // field chrome
      "confirm": {"label": "OK"}              // confirm button
    },
    "characters": {                           // places for characters on stage
      "mode": "adaptive",                     // place layout engine
      "base_texture_size": 600,               // art height basis
      "active_profile": "default",            // active arrangement
      "adaptive": {                           // an even row of places
        "slot_count": 3,                      // how many places
        "slot_width": 400,                    // place width
        "slot_height": 720,                   // place height
        "side_margin": 20,                    // side inset
        "slot_gap": null,                     // gap between places
        "snap": "bottom",                     // vertical snap
        "margin": 0,                          // edge margin
        "auto_selection_policy": "nearest_center_out",  // free-place pick order
        "alias_strategy": "canonical",        // how places are named
        "aliases": ["left", "center", "right"],  // custom place names
        "z_pattern": "center_front"           // draw-order pattern
      },
      "explicit": {                           // your own arrangements (same as profiles below)
        "active_profile": "default",          // active explicit profile
        "profiles": {}                        // named arrangements
      },
      "profiles": {                           // arrangements by name
        "closeup": {                          // any name you choose
          "aliases": {},                      // extra names for places
          "auto_selection_policy": "nearest_center_out",  // free-place pick order
          "transition": {},                   // move between arrangements
          "slots": [ {                        // the places
            "id": "left",                     // place id
            "x": 320,                         // x position
            "y": 720,                         // y position
            "width": 400,                     // place width
            "height": 720,                    // place height
            "z": 0,                           // draw order
            "order": 0,                       // auto-pick order
            "draw_layer": "behind_panel",     // behind or above panel
            "modulate": "#ffffff",            // tint
            "scale": 1.0                      // size multiplier
          } ]
        }
      }
    }
  }
}

Keys

choices.show_numbers bool
If omitted true
Values true · false
Prefix choices with "1.", "2.", …
choices.shuffle bool
If omitted false
Values true · false
Mix up choice order.
choices.gap number
If omitted -1 (use theme spacing)
Gap between choice rows. -1 means use theme spacing.
If omitted built-in choice look
Values pack name
Theme pack stem for stacked choice buttons.
If omitted default placement
Where the choice menu sits (anchors, size).
If omitted default button chrome
Default chrome for each choice item.
If omitted dialogue colours / flat field
Values pack name
Pack for the name-entry field look.
If omitted none
Values pack name
Optional extra compose pack layered on the input.
If omitted default placement
Where the text-input block sits.
text_input.field object
If omitted default field chrome
Look of the text field itself.
text_input.confirm object
If omitted default confirm button
Confirm button defaults (e.g. { "label": "OK" }).
characters.mode enum
If omitted adaptive
Values adaptive · explicit
"adaptive" = even row; "explicit" = your named profiles.
characters.base_texture_size number
If omitted 600
Values pixels ≥ 1
Picture height your art is drawn for; sizes places when width/height omitted.
characters.active_profile string
If omitted default
Which named profile is in use.
characters.adaptive.slot_count int
If omitted 3
Values 2–…
Number of places in the row (≥ 2).
characters.adaptive.slot_width number
If omitted same as base_texture_size
Width of each place in pixels.
characters.adaptive.slot_height number
If omitted same as slot_width, or base_texture_size
Height of each place in pixels.
characters.adaptive.side_margin number
If omitted 20
Values pixels
Space at the left/right screen edges.
characters.adaptive.slot_gap number or null
If omitted null — auto gap to fit the window
Values null · pixels
null = auto gap to fit the window. A number sets an exact gap (negative overlaps).
characters.adaptive.snap enum
If omitted bottom
Values bottom · top
Stand on the bottom edge or hang from the top.
characters.adaptive.margin number
If omitted 0
Values pixels
Space from the snapped edge (top or bottom).
characters.adaptive.auto_selection_policy string
If omitted nearest_center_out
Values nearest_center_out · policy name
How a free place is chosen when the script gives none.
characters.adaptive.alias_strategy enum
If omitted canonical
Values canonical · indexed · custom
Naming for script places: canonical, indexed, or custom aliases.
characters.adaptive.aliases array
If omitted generated from strategy (left/center/right for 3 slots)
Values list of names
Your names for the places. Required when strategy is custom.
characters.adaptive.z_pattern string
If omitted center_front
Values center_front · pattern name
Which places draw in front (e.g. center_front).
characters.explicit.active_profile string
If omitted default
Which arrangement under explicit.profiles is active.
characters.explicit.profiles object
If omitted built-in three-place profile
Your own place layouts by name.
characters.profiles.*.aliases object
If omitted empty — use slot ids only
Map of alias → slot id for this profile.
characters.profiles.*.auto_selection_policy string
If omitted nearest_center_out
Pick order when no place is given.
If omitted {"kind":"instant","duration":0}
How characters move when switching profiles.
characters.profiles.*.slots[].id string
If omitted required on each place you define
Stable name of this place (required).
characters.profiles.*.slots[].x number
If omitted required on each place you define
Values pixels
Horizontal position in design pixels (required).
characters.profiles.*.slots[].y number
If omitted required on each place you define
Values pixels
Vertical position in design pixels (required).
characters.profiles.*.slots[].width number
If omitted required on each place you define
Values pixels
Width in design pixels (required).
characters.profiles.*.slots[].height number
If omitted required on each place you define
Values pixels
Height in design pixels (required).
characters.profiles.*.slots[].z number
If omitted 0
Higher draws in front.
characters.profiles.*.slots[].order number
If omitted 0
Order in the auto-selection list.
characters.profiles.*.slots[].draw_layer enum
If omitted behind_panel
Values behind_panel · above_panel
Draw behind the dialogue box or above it.
characters.profiles.*.slots[].modulate color
If omitted #ffffff
Values #RRGGBB
Colour multiply tint for characters in this place.
characters.profiles.*.slots[].scale number
If omitted 1.0
Scale for characters in this place.

Learn more: layout

cursor

Custom mouse pointer.

{
  "cursor": {                                 // custom mouse pointer
    "enabled": true,                          // use custom pointer
    "default": "arrow",                       // starting pointer profile
    "config": "cursors.json",                 // pointer catalog file
    "touch_pointer": false,                   // show on touch
    "idle_hide": {                            // hide the pointer while reading
      "enabled": true,                        // hide while idle
      "delay": 2.5,                           // idle seconds
      "fade": 0,                              // fade-out seconds
      "keep_visible": ["choices", "screens"]  // never hide during…
    }
  }
}

Keys

enabled bool
If omitted false
Values true · false
Software cursor on/off. Off or omitted → OS pointer.
default string
If omitted "default", or the default in cursors.json
Profile id from cursors.json to start with.
config string
If omitted cursors.json
Values project path
Path to the pointer pictures file.
touch_pointer bool
If omitted false
Values true · false
Show the soft cursor while touch-pointing.
idle_hide.enabled bool
If omitted false unless you enable idle-hide
Values true · false
Hide the pointer after idle time while reading.
idle_hide.delay number
If omitted 2.5
Values 0.5–30 seconds
Seconds without moving before hide.
idle_hide.fade number
If omitted 0
Values seconds
Soft fade when hiding. OS cursor ignores fade.
idle_hide.keep_visible array
If omitted ["choices","screens"] when idle_hide is set
Values choices · screens · hotspots
Contexts that keep the pointer visible.

Learn more: cursor

splash

Boot / loading look before the title on web, Android, and delivered desktop. Draw order: bg_color → background → logo → progress bar. Root keys are shared; web / android / desktop only override that platform.

{
  "splash": {                          // boot / loading look before the title on web, Android, and delivered desktop. Draw order: bg_color → background → logo → progress bar. Root keys are shared; web / android / desktop only override that platform
    "bg_color": "#1a1a2e",             // solid fill behind everything
    "background": "ui/splash_bg.png",  // full-bleed picture under the logo
    "logo": "sprites/logo.png",        // centred brand mark (prefer PNG)
    "logo_max_width": 300,             // max logo width
    "logo_offset_x": 0,                // nudge logo right (+)
    "logo_offset_y": 0,                // nudge logo down (+)
    "boot": {                          // logo drawing on the web shell
      "fullsize": false,               // stretch logo to fill (web)
      "use_filter": true               // smooth logo scaling (web)
    },
    "progress_bar": {                  // loading bar (web + Android; not desktop)
      "background": "#2a3060",         // track (empty bar) colour
      "fill": "#6a75b0",               // progress fill colour
      "width": "50%",                  // bar width (px or %)
      "height": 6,                     // bar height
      "border_radius": 3,              // bar corner radius
      "bottom_percent": 10             // distance from bottom (%)
    },
    "web": {                           // web-only overrides (merged over root; any splash key may live here)
      "notice": {                      // error plate if the web pack fails
        "background": "#5b3943",       // error plate fill (web)
        "border_color": "#9b3943",     // error plate border (web)
        "text_color": "#e0e0e0"        // error plate text (web)
      }
    },
    "android": {},                     // Android-only overrides
    "desktop": {                       // delivered desktop builds only
      "min_seconds": 0.6               // min brand hold (desktop builds)
    }
  }
}

Keys

bg_color color
If omitted theme panel.default.bg, else colors.background_default; no theme → kit #000000
Values #RRGGBB · null
Solid fill on all platforms (under picture + logo).
background path
If omitted no picture — solid bg_color only
Values project path · null
Cover image under the logo. Web + in-engine Android/desktop overlays. Not used by the Android OS splash API.
logo path
If omitted no logo (null also hides it)
Values project path · null
Centred brand mark. Android OS splash uses this; if missing, root icon is used for the OS splash icon.
logo_max_width number or size
If omitted web shell uses 300; in-engine overlays leave width unconstrained
Values px · CSS size
Max logo width in px; web also accepts CSS sizes like "40%".
logo_offset_x number
If omitted 0
Values pixels
Horizontal nudge from centre; + = right.
logo_offset_y number
If omitted 0
Values pixels
Vertical nudge from centre; + = down, − = up.
boot.fullsize bool
If omitted false
Stretch the logo to fill the web overlay.
boot.use_filter bool
If omitted true
Smooth logo scaling on web; false = sharp / pixel art.
progress_bar.background color
If omitted darkened splash/panel bg from theme; no theme → kit #2a3060
Values #RRGGBB
Colour of the empty loading-bar track. Web + Android in-engine only.
progress_bar.fill color
If omitted theme colors.speaker_text, else choice_text; no theme → kit #6a75b0
Values #RRGGBB
Colour of the loading-bar fill. Web + Android in-engine only.
progress_bar.width number or size
If omitted web shell "50%"
Values px · CSS size
Bar width as pixels or CSS size. Mainly web shell.
progress_bar.height number or size
If omitted 6
Values pixels
Bar height in px (web CSS / overlay).
progress_bar.border_radius number or size
If omitted 3
Values pixels
Rounded corners of the bar (web).
progress_bar.bottom_percent number
If omitted 10
Values 0–100 % of screen height
How far the bar sits from the bottom edge, as % of screen height (web).
web.notice.background color
If omitted darkened splash bg from theme; no theme → kit #5b3943
Values #RRGGBB
Fill of the web error plate when the pack fails to load.
web.notice.border_color color
If omitted same as progress fill from theme; no theme → kit #9b3943
Values #RRGGBB
Border of the web error plate.
web.notice.text_color color
If omitted theme notification_text / dialogue_text; no theme → kit #e0e0e0
Values #RRGGBB
Text colour of the web error plate.
android object
If omitted use root splash keys for Android too
Same keys as root splash, deep-merged for Android only (e.g. a different logo_max_width).
desktop.min_seconds number
If omitted 0.6
Values 0–… seconds
Shortest time the brand splash stays on delivered desktop builds. 0 = dismiss when ready. Not used on web/Android. Author Open does not show this overlay.

Learn more: splash

android

Android store identity for builds.

{
  "android": {                          // Android store identity for builds
    "package_id": "com.studio.mygame",  // Android app id
    "version_code": 1                   // Play versionCode
  }
}

Keys

package_id string
If omitted required for Android builds
Values reverse-DNS id
Reverse-DNS id (e.g. com.studio.game). Required for Android builds. Never change after first Play upload.
version_code int
If omitted 1
Values 1–…
Integer for store uploads. Raise by one every Play upload.

Learn more: android

stories

Several story packs in one game (early).

{
  "stories": [ {               // several story packs in one game (early)
    "id": "main",              // story pack id
    "title": "Chapter One",    // display name
    "title_key": "ch1_title",  // translated title
    "path": "story",           // story folder
    "start": "start",          // first scene id
    "mode": "anthology",       // episode or anthology
    "unlock": {                // when it becomes available
      "always": true,          // always unlocked
      "flag": "ch1_done"       // unlock flag
    },
    "clear_flag": "ch1_done"   // flag set on finish
  } ]
}

Keys

id string
If omitted required on each story entry
Stable id saved with progress (required).
title string
If omitted use id
Name shown to readers / slot labels.
title_key string
If omitted use title
Catalog key for the story title.
path string
If omitted story
Values project folder
Folder with script.txt / story.json.
start string
If omitted start
Entry node id for New Game. Default looks for a moment named start; if that id is missing, the first moment in the story is used. Script sugar: :: start.
mode enum
If omitted anthology
Values episode · anthology
"episode" unlocks in order; "anthology" is free pick.
unlock.always bool
If omitted true when unlock is omitted entirely
Values true · false
Available from the start. Default when unlock is omitted.
unlock.flag string
If omitted ignored unless set
Values flag name
Available once this campaign flag is set.
clear_flag string
If omitted none
Values flag name
Set this campaign flag when the story ends (episode mode).

Learn more: stories

extensions

Advanced: your own code packs.

{
  "extensions": [ {                          // advanced: your own code packs
    "id": "parallax",                        // extension id
    "entry": "extensions/parallax/main.gd",  // main script path
    "runtime_min": "0.1.0"                   // oldest player version
  } ]
}

Keys

id string
If omitted required on each extension entry
Values lowercase_id
Unique id ([a-z0-9_]+). Required.
entry path
If omitted required on each extension entry
Values project path
Extension entry file relative to the project root. Required.
runtime_min string
If omitted no minimum check
Values semver
Skip load if the player is older than this version.

Learn more: extensions

screens.json — file map

Optional defaults, then screen ids you choose — any names you need. title, pause, settings, and hud are common starters, not a closed list. Every screen uses the same shape (documented below under the example id title). Copy from examples/ and change labels first.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "defaults": { … },  // shared enter/exit fade for every screen that does not set its own
  "title": { … },     // example id only — use any name (`pause`, `credits`, `gallery`, …); every screen uses this shape
  "…": { … }          // any other screen ids you invent — same shape
}

defaults

Shared enter/exit fade for every screen that does not set its own.

{
  "defaults": {                 // shared enter/exit fade for every screen that does not set its own
    "transition_in": "fade",    // how screens appear
    "transition_out": "fade",   // how screens leave
    "transition_duration": 0.3  // fade/slide length
  }
}

Keys

transition_in enum
If omitted instant
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Enter transition kind when a screen does not set its own.
transition_out enum
If omitted instant
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Exit transition kind when a screen does not set its own.
transition_duration number
If omitted 0
Values seconds
Seconds for enter/exit when a screen does not set its own duration. 0 = instant.

Learn more: defaults

title

Example id only — use any name (pause, credits, gallery, …); every screen uses this shape.

{
  "title": {                                  // example id only — use any name (`pause`, `credits`, `gallery`, …); every screen uses this shape
    "role": "modal",                          // how the screen sits in the world
    "layers": [ {                             // ordered plates drawn bottom → top (required)
      "type": "menu",                         // what this layer is
      "order": 10,                            // draw order
      "layout": {"host": "screen", "anchor_h": "center", "anchor_v": "center"},  // where the layer sits
      "style": {"bg_color": "#101018"},       // look of this layer
      "buttons": [ {                          // menu layer only — the button column
        "label": "New Game",                  // caption
        "label_key": "screen.title.new_game",  // translated caption
        "interaction": {"hooks": {"on_click": "new_game"}},  // what the button does
        "show_if": "has_autosave",            // when to show
        "icon": "sprites/ico-pause.png"       // optional glyph
      } ],
      "blocks": [ {                           // content layer only — rows inside the card
        "type": "heading",                    // block kind
        "bind": "bgm",                        // settings preference id
        "label": "Music",                     // row caption
        "text": "Audio",                      // copy for heading/text
        "interaction": {"hooks": {"on_click": "close"}}  // hooks for buttons/controls
      } ],
      "items": [ {                            // sprites layer only — UI pictures
        "image": "ui/logo.png",               // picture path
        "interaction": {"hooks": {"on_click": "close"}}  // click / hover hooks
      } ],
      "interaction": {"hooks": {"on_click": "close"}},  // layer hooks (background / chrome)
      "modulate": "#ffffff",                  // layer tint
      "scale": 1.0                            // layer scale
    } ],
    "pauses_tree": false,                     // pause the game while open
    "transition_in": "fade",                  // this screen's enter
    "transition_out": "fade",                 // this screen's exit
    "transition_duration": 0.3,               // this screen's fade length
    "interaction": {},                        // screen-level hooks
    "shader": {},                             // screen shader
    "action_context": "",                     // hook allowlist context
    "modulate": "#ffffff",                    // whole-screen tint
    "scale": 1.0,                             // whole-screen scale
    "plane": "",                              // draw plane override
    "persistent": false,                      // stay when others stack
    "input_passthrough": false                // clicks pass empty areas
  }
}

Keys

role enum
If omitted modal
Values modal · hud · scene
modal = full menu (default). hud = stays up during play. scene = under characters.
layers[].type enum
If omitted required on each layer
Values background · menu · content · sprites · chrome
background plate · menu button column · content card of blocks · sprites UI pictures · chrome extra controls (e.g. Close).
layers[].order number
If omitted draw order 0
Higher draws above. Also used when merging locale overlays.
layers[].layout object
If omitted theme / type defaults
Placement. Menu often uses host: screen or panel (dock to dialogue box).
layers[].style object
If omitted theme defaults
Colours / textures for the plate. May include source pack compose.
layers[].buttons[].label string
If omitted empty / icon-only
Button text. Prefer label_key when translating.
layers[].buttons[].label_key string
If omitted use label
Catalog key for the caption.
layers[].buttons[].interaction object
If omitted required for a working button
hooks.on_click is a verb string (new_game, close, screen:pause), a command object, or an array of those.
layers[].buttons[].show_if string
If omitted always show
Hide unless a condition is true (e.g. has_autosave for Continue).
layers[].buttons[].icon path
If omitted text-only button
Image path for an icon button (common on HUD).
layers[].blocks[].type enum
If omitted required unless bind is set
Values heading · text · separator · link · spacer · image · button · button_row · slider · toggle · history · save_slots · scroll · table
Omit type when using preference bind instead. Common: heading, bind (as field), history, save_slots.
layers[].blocks[].bind string
If omitted not a preference row
Values master · bgm · sfx · typing_enabled · typing_speed_scale · auto_advance_default · locale · …
Preference row (volume, language, …). Do not set type when bind is set.
layers[].blocks[].label string
If omitted full-width control / no caption
When set with bind, builds a settings row (label | control).
layers[].blocks[].text string
If omitted use text_key or none
text / heading body. text accepts BBCode.
layers[].blocks[].interaction object
If omitted none
Same action payload shapes as menu buttons.
layers[].items[].image path
If omitted depends on sprite kind
For kind: image sprites.
layers[].items[].interaction object
If omitted decorative only
Optional interaction on a UI sprite.
layers[].interaction object
If omitted none
Background often uses on_click: close for click-outside. Chrome uses the same hook shape.
layers[].modulate color
If omitted no tint
Colour multiply for the whole layer.
layers[].scale number
If omitted 1
Scale for the layer (number or [x,y]).
pauses_tree bool
If omitted false
Values true · false
When true, the story/world pauses (typical for pause). HUD screens never pause.
transition_in enum
If omitted use defaults
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Overrides defaults.transition_in for this screen only.
transition_out enum
If omitted use defaults
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Overrides defaults.transition_out.
transition_duration number
If omitted use defaults
Values seconds
Overrides defaults.transition_duration.
interaction object
If omitted none
Rare. Prefer hooks on layers / buttons.
shader object
If omitted none
Optional fullscreen/bg shader while this screen is up — [Shaders](../../polish/shaders.md).
action_context string
If omitted from role / built-in
Advanced: which action policy context this screen uses.
modulate color
If omitted no tint
Colour multiply for the screen root.
scale number
If omitted 1
Scale for the screen root.
plane string
If omitted from role
Advanced. Prefer role.
persistent bool
If omitted from role (hud is persistent)
Advanced. HUD role already implies persistence.
input_passthrough bool
If omitted from role (hud passes through)
Advanced. HUD role passes input through empty space by default.

Learn more: title

themes/default.json — file map

Fonts, colours, dialogue-box presets, menus, and control chrome for the whole game. Most games start from an example themes/default.json and change colours and sizes. Choice and text-input packs are separate files under themes/ — see the Look pages. Extra root objects (or themes/<name>.json) are named button packs with the same keys as buttons.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "typography": { … },       // fonts and sizes
  "colors": { … },           // fallback text and empty-stage colours
  "panel": { … },            // named dialogue-box presets — any names you choose
  "labelplate": { … },       // named name-plate styles
  "buttons": { … },          // default menu / screen button chrome
  "scrollbar": { … },        // menus and panel body scrollbars
  "slider": { … },           // settings volume and similar rows
  "toggle": { … },           // on/off settings controls
  "card": { … },             // content cards on screens
  "separator": { … },        // thin rules between blocks
  "notification": { … },     // toast chrome (Saved, Loaded, …)
  "settings_layout": { … },  // label / control columns on settings screens
  "readonly_styles": { … },  // looks when a panel is readonly / not talking
  "…": { … }                 // named button packs you add — same keys as buttons
}

typography

Fonts and sizes.

{
  "typography": {                // fonts and sizes
    "font": "Jupiteroid",        // base face under fonts/
    "font_menu": null,           // menus and settings
    "font_dialogue": null,       // dialogue and choices
    "font_label": "Jupiteroid",  // speaker name plate
    "size_scale": 1.0,           // size multiplier
    "sizes": {                   // pixel sizes
      "speaker": 45,             // name plate
      "dialogue": 30,            // body text
      "choice": 30,              // choice captions
      "button": 30,              // menu buttons
      "notification": 30         // toasts
    }
  }
}

Keys

font string
If omitted player default font
Stem under fonts/ (variants -Regular / -Bold when present).
font_menu string
If omitted use font
Menus / settings buttons. null or omit → font.
font_dialogue string
If omitted use font
Dialogue body and choice captions. null or omit → font.
font_label string
If omitted use font
Name-plate face.
size_scale number
If omitted 1.0
Multiplies all sizes.*. Player text-size builds on this.
sizes.speaker number
If omitted engine fallback
Speaker name size.
sizes.dialogue number
If omitted engine fallback
Dialogue body size.
sizes.choice number
If omitted engine fallback
Choice button captions.
sizes.button number
If omitted 20
Screen / menu button captions.
sizes.notification number
If omitted engine fallback
Notification toast text.

Learn more: typography

colors

Fallback text and empty-stage colours.

{
  "colors": {                         // fallback text and empty-stage colours
    "background_default": "#dfdcd7",  // empty stage fill
    "speaker_text": "#c4693a",        // name plate text
    "dialogue_text": "#e8e5e0",       // body text
    "choice_text": "#c5cad5",         // choice captions
    "notification_text": "#dfdcd7"    // toast text
  }
}

Keys

background_default color
If omitted #1e2347
Fill when no background picture is up.
speaker_text color
If omitted #b3bfe6
Speaker name fallback.
dialogue_text color
If omitted #ffffff
Dialogue body fallback.
choice_text color
If omitted #c8d0ff
Choice caption fallback.
notification_text color
If omitted #e6f0ff
Toast / some heading fallbacks.

Learn more: colors

panel

Named dialogue-box presets — any names you choose.

{
  "panel": {                                  // named dialogue-box presets — any names you choose
    "default": {                              // any name you choose
      "extends": "default",                   // inherit another preset
      "width": "80%",                         // panel width
      "anchor_h": "center",                   // horizontal place
      "anchor_v": "bottom",                   // vertical place
      "offset_x": 0,                          // nudge X
      "offset_y": -32,                        // nudge Y
      "min_height": 160,                      // height floor
      "max_height": "50%",                    // height ceiling
      "padding": {"top": 15, "left": 20, "right": 20, "bottom": 20},  // inner margins
      "speaker_gap": 6,                       // name to text gap
      "choices_gap": 2,                       // text to choices gap
      "choices_separation": 4,                // between choices
      "bg": "#343a52e0",                      // flat plate colour
      "bg_texture": "sprites/panel-bg.png",   // plate image
      "bg_9slice": [15, 15, 15, 15],          // nine-slice margins
      "corner_radius": 0,                     // flat corner radius
      "resize_duration": 0,                   // height ease
      "scrollbar_layout": "default",          // body scrollbar column
      "typography": {"font": "JetBrainsMono", "sizes": {"dialogue": 18, "choice": 18}},  // panel font / sizes overlay
      "colors": {"dialogue_text": "#c8ffd0", "speaker_text": "#7CFF8A"},  // panel body / speaker colours
      "labelplate_style_key": "default",      // name plate style
      "labelplate_layout": {"anchor_h": "left", "anchor_v": "top", "offset_y": -47},  // docked name plate
      "continue_button": {"visible": true},   // next / continue control
      "back_button": {"visible": false},      // back control
      "slots": [],                            // avatar / art slots
      "default_plate": "them",                // fallback plate name
      "plates": {                             // named per-beat plates for text_mode plates
        "them": {                             // any name you choose
          "extends": "them",                  // inherit another plate
          "anchor_h": "left",                 // side of text column
          "width": "88%",                     // plate width
          "speaker": "on_change",             // in-plate speaker header
          "speaker_gap": 4,                   // header to body gap
          "bg": "#2b3552",                    // flat plate colour
          "corner_radius": 14,                // flat corner radius
          "bg_texture": "sprites/plate-bg.png",  // plate image
          "bg_9slice": [12, 12, 12, 12],      // nine-slice margins
          "shader": {"name": "drop_shadow", "target": "bg"},  // background shader
          "padding": {"top": 8, "left": 12, "right": 12, "bottom": 8},  // inner margins
          "margin": {"top": 3, "bottom": 3, "left": 0, "right": 0},  // outer spacing
          "group_gap": 6,                     // extra gap on speaker change
          "typography": {"sizes": {"speaker": 13}},  // plate font / sizes overlay
          "colors": {"dialogue_text": "#e8ecff", "speaker_text": "#8aa0c8"},  // plate body / speaker colours
          "transition_in": "slide_right",     // live plate enter
          "transition_out": "fade",           // validated; unused on fold
          "transition_duration": 0.22,        // enter duration
          "transition": "fade",               // enter fallback
          "duration": 0.22                    // duration alias
        }
      }
    }
  }
}

Keys

extends string
If omitted no parent
Merge another preset first, then apply this one's keys.
width string
If omitted 100%
"NN%", "100%", "stretch", or a pixel number.
anchor_h enum
If omitted stretch
Values left · center · right · stretch
Where the plate sits horizontally.
anchor_v enum
If omitted bottom
Values top · center · bottom · stretch
Where the plate sits vertically.
offset_x number
If omitted 0
Pixels from the horizontal anchor.
offset_y number
If omitted 0
Pixels from the vertical anchor (negative moves up from bottom).
min_height number
If omitted 160
Number, "NN%", or "full".
max_height number
If omitted legacy formula
Overflow scrolls the body. Number, "NN%", or "full".
padding object
If omitted 15 / 20 / 20 / 20
top / left / right / bottom pixels.
speaker_gap number
If omitted 6
Gap between speaker name and body.
choices_gap number
If omitted 2
Gap between body and choice list.
choices_separation number
If omitted 4
Gap between choice rows.
bg color
If omitted #343a52e0
Used when no usable bg_texture.
bg_texture path
If omitted flat bg only
Nine-slice plate image.
bg_9slice array
If omitted [0,0,0,0]
[left, top, right, bottom] for bg_texture.
corner_radius number
If omitted 0
Ignored once a texture is used.
resize_duration number
If omitted 0
Values seconds
Seconds to ease plate height when content grows/shrinks. 0 snaps.
scrollbar_layout enum
If omitted default
Values default · reserve · overlay
reserve keeps wrap width stable on scrolling plates.
typography object
If omitted project typography
Allowlisted overlay onto project typography for body, labelplate, panel-hosted choices and input. Keys: font, font_dialogue, font_label, sizes.speaker/dialogue/choice. Not size_scale / font_menu / button / notification.
colors object
If omitted project colors
Allowlisted overlay onto project colors: dialogue_text, speaker_text only. Choice packs own caption colours; notifications stay global.
labelplate_style_key string
If omitted labelplate.default
Which labelplate.<name> styles the speaker.
labelplate_layout object
If omitted inline above text
Present → dock to panel edge instead of inline.
continue_button object
If omitted shown with defaults
Visibility, label, placement, and button style.
back_button object
If omitted hidden
Same shape as continue_button.
slots array
If omitted none
Panel slots (e.g. talking art). See panels guide.
If omitted none
Which plates.<name> to use when the beat does not pick one. Must name a key in plates.
plates.*.extends string
If omitted no parent
Merge another plate in the same preset first.
plates.*.anchor_h enum
If omitted left
Values left · center · right · stretch
Where the plate sits in the body column.
plates.*.width string
If omitted 80%
"NN%" of the text column, a pixel number, or "stretch".
plates.*.speaker enum
If omitted false
Values false · true · on_change
on_change shows the header when the speaker changes.
plates.*.speaker_gap number
If omitted panel speaker_gap
Gap under the in-plate speaker header. Omit to inherit the preset.
plates.*.bg color
If omitted transparent
Used when no usable bg_texture.
plates.*.corner_radius number
If omitted 0
Ignored once a texture is used.
plates.*.bg_texture path
If omitted flat bg only
Nine-slice plate image.
plates.*.bg_9slice array
If omitted [0,0,0,0]
[left, top, right, bottom] for bg_texture.
plates.*.shader object
If omitted none
target: "bg" only on plates.
plates.*.padding object
If omitted 8 / 12 / 12 / 8
top / left / right / bottom pixels.
plates.*.margin object
If omitted 3 / 3 / 0 / 0
Space around the plate in the column.
plates.*.group_gap number
If omitted 0
Extra space when the speaker changes between beats.
plates.*.typography object
If omitted preset / project
Same allowlist as the panel preset overlay.
plates.*.colors object
If omitted preset / project
Same allowlist as the panel preset overlay.
plates.*.transition_in enum
If omitted instant
Values instant · fade · slide_up · slide_down · slide_left · slide_right
How the live plate chrome enters on a new beat. Motion is clipped to the body scroll. slide_right enters from the left; slide_left from the right.
plates.*.transition_out enum
If omitted unused in v1
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Accepted when present; fold commits plates instantly (no exit tween in v1).
plates.*.transition_duration number
If omitted 0
Values seconds
Seconds for transition_in. 0 or instant = no motion.
plates.*.transition enum
If omitted use transition_in
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Used when transition_in is omitted.
plates.*.duration number
If omitted use transition_duration
Values seconds
Same as transition_duration when that key is absent.

Learn more: panel

labelplate

Named name-plate styles.

{
  "labelplate": {                            // named name-plate styles
    "default": {                             // any name you choose
      "bg_texture": "sprites/label-bg.png",  // plate image
      "bg_9slice": [7, 7, 7, 17],            // nine-slice margins
      "padding_left": 12,                    // inner left
      "padding_right": 12,                   // inner right
      "padding_top": 2,                      // inner top
      "padding_bottom": 12,                  // inner bottom
      "font_color": "#3a2a1a"                // name colour
    }
  }
}

Keys

bg_texture path
If omitted flat colour / none
Nine-slice background for the name.
bg_9slice array
If omitted [0,0,0,0]
Margins for bg_texture.
padding_left number
If omitted 0
Content margin.
padding_right number
If omitted 0
Content margin.
padding_top number
If omitted 0
Content margin.
padding_bottom number
If omitted 0
Content margin.
font_color color
If omitted colors.speaker_text
Overrides colors.speaker_text for this style.

Learn more: labelplate

buttons

Default menu / screen button chrome.

{
  "buttons": {                                // default menu / screen button chrome
    "spacing": 12,                            // menu separation
    "min_width": 220,                         // minimum width
    "corner_radius": 0,                       // corner radius
    "border_width": 0,                        // border width
    "padding_left": 18,                       // padding left
    "padding_right": 18,                      // padding right
    "padding_top": 4,                         // padding top
    "padding_bottom": 6,                      // padding bottom
    "normal_bg": "#d8c9a8",                   // normal fill
    "hover_bg": "#e4d6b4",                    // hover fill
    "pressed_bg": "#c4b48e",                  // pressed fill
    "font_color": "#3a2a1a",                  // label colour
    "font_hover_color": "#6b4420",            // hover label
    "font_pressed_color": "#8a5a28",          // pressed label
    "normal_texture": "sprites/button-normal.png",  // idle image
    "hover_texture": "sprites/button-hover.png",  // hover image
    "pressed_texture": "sprites/button-down.png"  // pressed image
  }
}

Keys

spacing number
If omitted engine fallback
Gap between stacked menu buttons.
min_width number
If omitted 0
Minimum width when greater than 0.
corner_radius number
If omitted 0
Flat plate corner radius.
border_width number
If omitted 0
Flat border width.
padding_left number
If omitted 0
Content margin.
padding_right number
If omitted 0
Content margin.
padding_top number
If omitted 0
Content margin.
padding_bottom number
If omitted 0
Content margin.
normal_bg color
If omitted engine grey
Flat fill for the idle state.
hover_bg color
If omitted engine grey
Flat fill on hover.
pressed_bg color
If omitted engine grey
Flat fill while pressed.
font_color color
If omitted theme default
Idle caption colour.
font_hover_color color
If omitted font_color
Caption on hover.
font_pressed_color color
If omitted font_color
Caption while pressed.
normal_texture path
If omitted flat colours
Texture for the idle state.
hover_texture path
If omitted flat colours
Texture on hover.
pressed_texture path
If omitted flat colours
Texture while pressed.

Learn more: buttons

scrollbar

Menus and panel body scrollbars.

{
  "scrollbar": {                         // menus and panel body scrollbars
    "width": 10,                         // bar width
    "corner_radius": 4,                  // corner radius
    "content_separation": 18,            // gap from content
    "track_color": "#8a6a4a40",          // track colour
    "grabber_color": "#6b4420",          // grabber
    "grabber_hover_color": "#8a5a28",    // grabber hover
    "grabber_pressed_color": "#3a2a1a",  // grabber pressed
    "track_9slice": [0, 0, 0, 0],        // track nine-slice
    "grabber_9slice": [0, 0, 0, 0]       // grabber nine-slice
  }
}

Keys

width number
If omitted 10
Scrollbar thickness.
corner_radius number
If omitted 4
Track / grabber radius.
content_separation number
If omitted caller fallback
Space between scrollable content and the bar.
track_color color
If omitted #4a559060
Track fill.
grabber_color color
If omitted #5b6a82
Thumb colour.
grabber_hover_color color
If omitted #6a75b0
Thumb on hover.
grabber_pressed_color color
If omitted #a8b8ef
Thumb while dragged.
track_9slice array
If omitted [0,0,0,0]
Margins when a track texture is set.
grabber_9slice array
If omitted [0,0,0,0]
Margins when a grabber texture is set.

Learn more: scrollbar

slider

Settings volume and similar rows.

{
  "slider": {                                 // settings volume and similar rows
    "track_height": 9,                        // track height
    "corner_radius": 4,                       // corner radius
    "grabber_size": 19,                       // knob size
    "track_color": "#4a559060",               // track colour
    "fill_color": "#c4693a90",                // filled track
    "fill_hover_color": "#d4794ab0",          // fill hover
    "grabber_color": "#c5cad5",               // knob colour
    "grabber_hover_color": "#dfdcd7",         // knob hover
    "grabber": "sprites/slider-picker.png",   // knob image
    "grabber_hover": "sprites/slider-picker.png",  // knob hover image
    "track_texture": "sprites/slider-bg.png",  // track image
    "fill_texture": "",                       // fill image
    "fill_hover_texture": "",                 // fill hover image
    "track_9slice": [4, 6, 4, 6],             // track nine-slice
    "fill_9slice": [4, 6, 4, 6]               // fill nine-slice
  }
}

Keys

track_height number
If omitted 8
Track thickness.
corner_radius number
If omitted 4
Track / fill radius.
grabber_size number
If omitted 20
Grabber diameter when drawn as a circle.
track_color color
If omitted #4a559060
Empty track fill.
fill_color color
If omitted #c4693a90
Filled portion colour.
fill_hover_color color
If omitted fill_color
Filled portion on hover.
grabber_color color
If omitted #c5cad5
Circle grabber when no texture.
grabber_hover_color color
If omitted #dfdcd7
Circle grabber on hover.
grabber path
If omitted drawn circle
Texture spec for the grabber.
grabber_hover path
If omitted grabber
Texture spec on hover.
track_texture path
If omitted flat track_color
Optional track texture.
fill_texture path
If omitted flat fill_color
Optional fill texture.
fill_hover_texture path
If omitted fill_texture / flat
Optional fill texture on hover.
track_9slice array
If omitted [0,0,0,0]
Margins for track_texture.
fill_9slice array
If omitted [0,0,0,0]
Margins for fill_texture.

Learn more: slider

toggle

On/off settings controls.

{
  "toggle": {                                 // on/off settings controls
    "checked": "sprites/checked.png",         // on image
    "unchecked": "sprites/unchecked.png",     // off image
    "checked_hovered": "sprites/checked.png",  // on hover image
    "unchecked_hovered": "sprites/unchecked.png",  // off hover image
    "icon_size": 0,                           // icon size
    "track_width": 48,                        // switch width
    "track_height": 26,                       // switch height
    "knob_size": 20,                          // knob size
    "corner_radius": 13,                      // track radius
    "track_on_color": "#c4693a90",            // on track
    "track_off_color": "#4a559060",           // off track
    "knob_color": "#dfdcd7",                  // switch knob
    "knob_hover_color": "#dfdcd7",            // knob hover
    "track_on_hover_color": "#d4794acc",      // on track hover
    "track_off_hover_color": "#6a75a8a0"      // off track hover
  }
}

Keys

checked path
If omitted drawn switch
Texture when on (checkbox mode).
unchecked path
If omitted drawn switch
Texture when off.
checked_hovered path
If omitted checked
On texture while hovered.
unchecked_hovered path
If omitted unchecked
Off texture while hovered.
icon_size number
If omitted 0
Force icon size; 0 uses the texture size.
track_width number
If omitted 48
Drawn switch track width.
track_height number
If omitted 26
Drawn switch track height.
knob_size number
If omitted 20
Drawn switch knob size.
corner_radius number
If omitted 13
Drawn switch corner radius.
track_on_color color
If omitted #c4693a90
Track colour when on.
track_off_color color
If omitted #4a559060
Track colour when off.
knob_color color
If omitted #dfdcd7
Drawn switch knob colour.
knob_hover_color color
If omitted knob_color
Drawn switch knob on hover.
track_on_hover_color color
If omitted track_on_color
On track colour while hovered.
track_off_hover_color color
If omitted track_off_color
Off track colour while hovered.

Learn more: toggle

card

Content cards on screens.

{
  "card": {                                 // content cards on screens
    "max_width": 520,                       // card width cap
    "padding_left": 36,                     // padding left
    "padding_right": 36,                    // padding right
    "padding_top": 28,                      // padding top
    "padding_bottom": 28,                   // padding bottom
    "spacing": 14,                          // inner gap
    "bg": "#dfdcd7e0",                      // card fill
    "corner_radius": 0,                     // corner radius
    "bg_texture": "sprites/screen-bg.png",  // card image
    "bg_9slice": [7, 7, 7, 7]               // nine-slice margins
  }
}

Keys

max_width number
If omitted 500
Maximum card width.
padding_left number
If omitted 40
Inner margin.
padding_right number
If omitted 40
Inner margin.
padding_top number
If omitted 30
Inner margin.
padding_bottom number
If omitted 30
Inner margin.
spacing number
If omitted 20
Separation between card children.
bg color
If omitted #1e234790
Flat card colour.
corner_radius number
If omitted 12
Flat card radius.
bg_texture path
If omitted flat bg
Optional nine-slice background.
bg_9slice array
If omitted [0,0,0,0]
Margins for bg_texture.

Learn more: card

separator

Thin rules between blocks.

{
  "separator": {          // thin rules between blocks
    "height": 2,          // rule thickness
    "color": "#8a6a4a60"  // rule colour
  }
}

Keys

height number
If omitted 2
Separator height in pixels.
color color
If omitted #ffffff60
Separator colour.

Learn more: separator

notification

Toast chrome (Saved, Loaded, …).

{
  "notification": {             // toast chrome (Saved, Loaded, …)
    "bg": "#1e2347cc",          // toast fill
    "bg_texture": "",           // toast image
    "bg_9slice": [0, 0, 0, 0],  // nine-slice margins
    "corner_radius": 10,        // corner radius
    "padding_left": 24,         // padding left
    "padding_right": 24,        // padding right
    "padding_top": 10,          // padding top
    "padding_bottom": 12,       // padding bottom
    "min_width": 0,             // minimum width
    "offset_y": 0,              // vertical nudge
    "fade_in": 0.15,            // fade in
    "fade_out": 0.2             // fade out
  }
}

Keys

bg color
If omitted #1e2347cc
Flat toast colour.
bg_texture path
If omitted flat bg
Optional nine-slice background.
bg_9slice array
If omitted [0,0,0,0]
Margins for bg_texture.
corner_radius number
If omitted 10
Flat toast radius.
padding_left number
If omitted 24
Inner margin.
padding_right number
If omitted 24
Inner margin.
padding_top number
If omitted 10
Inner margin.
padding_bottom number
If omitted 12
Inner margin.
min_width number
If omitted content width
Optional minimum toast width.
offset_y number
If omitted 0
Pixels to shift the toast.
fade_in number
If omitted engine default
Values seconds
Appear fade length.
fade_out number
If omitted engine default
Values seconds
Dismiss fade length.

Learn more: notification

settings_layout

Label / control columns on settings screens.

{
  "settings_layout": {        // label / control columns on settings screens
    "title_font_size": 28,    // section heading size
    "label_min_width": 260,   // label column
    "slider_min_width": 200,  // slider column
    "value_min_width": 60,    // value readout
    "item_spacing": 16        // row gap
  }
}

Keys

title_font_size number
If omitted 28
Heading size inside settings cards.
label_min_width number
If omitted 180
Minimum width for preference labels.
slider_min_width number
If omitted 200
Minimum width for slider controls.
value_min_width number
If omitted 60
Minimum width for value text.
item_spacing number
If omitted 16
Gap inside a labeled row.

Learn more: settings_layout

readonly_styles

Looks when a panel is readonly / not talking.

{
  "readonly_styles": {             // looks when a panel is readonly / not talking
    "default": {                   // any name you choose
      "text_color": "#aaaaaa",     // dimmed body
      "speaker_color": "#888888",  // dimmed name
      "bg_opacity": 0.7,           // plate alpha
      "font_scale": 1.0            // size scale
    }
  }
}

Keys

text_color color
If omitted #ffffff
Dialogue colour in readonly mode.
speaker_color color
If omitted #b3bfe6
Speaker colour in readonly mode.
bg_opacity number
If omitted 1.0
Multiplies panel background alpha.
font_scale number
If omitted 1.0
Scales dialogue font size (about 0.5–2).

Learn more: readonly_styles

story/story.json — file map

A JSON array of moments. With script.txt, each entry patches the matching script scene by id (direction only — dialogue usually stays in the script). Without a script, the array is the whole story. You rarely type every field by hand; start from The same beat as data.

The whole file

The file is a JSON array. Each { … } is one moment. Linked entries jump to that section. Under each section, Keys lists type, If omitted, allowed values, and a short note.

[
  { … },  // one moment — any id; with a script, match :: id
  { … }   // more moments — same shape
]

moment

One moment — any id; with a script, match :: id.

{
  "id": "park",                               // moment id
  "talking": "hana",                          // who speaks
  "talking_anchor": "hana",                   // initial focus
  "label": "Hana",                            // name-plate text
  "text": "Watch the meter.",                 // dialogue body
  "text_ref": "line.park.1",                  // strings catalog key
  "text_from": "earlier_node",                // copy text from node
  "set": {"met_hana": true},                  // flags on enter
  "commands": [{"type": "bg", "value": "street"}],  // enter commands
  "flow_commands": [[{"type": "sfx", "value": "click"}]],  // mid-line batches
  "exit_commands": [{"type": "char", "name": "hana", "show": false}],  // leave commands
  "exit_delay": 0.3,                          // pause after exit
  "next": "after",                            // linear target
  "next_if": [{"expr": "score > 5", "then": "good"}],  // conditional next
  "auto": 2.0,                                // auto-advance
  "choices": {                                // choice menu — full tables in Story detail
    "items": [ {                              // the options (required)
      "text": "Stay",                         // caption
      "text_key": "choice.stay",              // translated caption
      "target": "stay",                       // where it goes
      "set": {"chose_stay": true},            // flags on pick
      "commands": [{"type": "sfx", "value": "click"}],  // commands on pick
      "id": "stay",                           // option id
      "rules": {"show_if": "has_key"},        // show / disable
      "style": {},                            // this option's look
      "layout": {"x": 100, "y": 80},          // free placement
      "source": "choice_plain",               // pack stem
      "interaction": {},                      // hotkey / hooks
      "transition": {},                       // option motion
      "shader": {},                           // option shader
      "audio": {},                            // option sounds
      "state_visual": {},                     // state look
      "debug": {}                             // author debug
    } ],
    "source": "choice_yes_no",                // menu pack
    "style": "choice_button",                 // button pack
    "item": {},                               // shared item defaults
    "shuffle": false,                         // reshuffle options
    "show_numbers": true,                     // number prefix
    "container": {},                          // menu plate / layout
    "timer": {"seconds": 30, "on_end": "explode"}  // choice deadline
  },
  "input": {                                  // text field wait
    "flag": "player_name",                    // where to store
    "prompt": "Your name?",                   // prompt copy
    "default": "",                            // ghost + empty confirm
    "placeholder": "Type a name…",            // hint only
    "prefill": "Hero",                        // editable starter
    "prefill_from_flag": true,                // seed from flag
    "max_length": 24,                         // character cap
    "pattern": "^[A-Za-z ]+$",                // validation
    "style": "name_entry",                    // chrome pack
    "source": "name_entry",                   // pack stem
    "container": {},                          // field host
    "field": {},                              // field chrome
    "confirm": {}                             // confirm button
  },
  "text_metas": {"help": "url:https://example.com"},  // body link actions
  "panel": "default",                         // panel preset
  "panel_id": "main",                         // which panel
  "text_mode": "replace",                     // body write mode
  "append_direction": "down",                 // append / plates direction
  "append_gap_lines": 0,                      // append line gap
  "append_gap_px": 0,                         // append pixel gap
  "plate": "them",                            // plate name this moment
  "hide_panel": false,                        // hide dialogue box
  "hide_continue": false,                     // hide continue
  "hide_back": false,                         // hide back
  "driver_typing": true,                      // mouth / beep drive
  "driver_typing_targets": ["hana"],          // who gets drive
  "panel_shader": {},                         // panel shader
  "panel_presence_mode": "talk_only",         // when panel shows
  "panel_non_talking_mode": "hide",           // when not talking
  "panel_readonly_style_key": "default",      // readonly look
  "labelplate": {"visible": true}             // name plate override
}

Keys

id string
If omitted auto _N from file order
Matches :: park in the script. Required for direction patches.
talking string
If omitted narrator / none
Speaker id, or an array of ids.
talking_anchor string
If omitted first talking id
Which speaker starts focused when several talk.
label string
If omitted character display name
Overrides the plate caption for this moment.
text string
If omitted none / from text_ref
BBCode, {@…} tags, and {flag} inserts.
text_ref string
If omitted use text
Pull body from the strings catalog.
text_from string
If omitted none
Reuse another moment's text by id.
set object
If omitted none
Flag updates when the moment starts. Values may be "= expr".
commands array
If omitted none
Stage directions when the moment starts. Each item is a command object — [Commands](../commands/index.md).
If omitted none
Batches fired by {@cN} markers in text (1-based). Command shapes — [Commands](../commands/index.md).
If omitted none
Run when leaving the moment. Command shapes — [Commands](../commands/index.md).
exit_delay number
If omitted 0
Values seconds
Seconds after exit_commands before the next moment.
next string
If omitted next in file / end
Next moment id, or null / END / NEXT / START.
next_if array
If omitted use next
First truthy { expr, then }; else next.
auto number
If omitted wait for continue
Values seconds
Advance after N seconds ([auto N]).
choices.items[].text string
If omitted use text_key
Choice label.
choices.items[].text_key string
If omitted use text
Catalog key for the caption.
choices.items[].target string
If omitted NEXT
Moment id, or NEXT / END / START.
choices.items[].set object
If omitted none
Flags applied when this option is chosen.
If omitted none
Run on confirm (no jump/input waits). Command shapes — [Commands](../commands/index.md).
choices.items[].id string
If omitted auto
Stable id for this option.
choices.items[].rules object
If omitted always available
show_if / disable_if expressions.
choices.items[].style object
If omitted group / game default
Per-option chrome (icon, colours, sprite).
choices.items[].layout object
If omitted list layout
Position when the menu uses free layout.
choices.items[].source string
If omitted none
Optional per-item pack under themes/.
choices.items[].interaction object
If omitted none
Focus, hotkey, and hooks.
choices.items[].transition object
If omitted none
Show / focus / confirm motion.
choices.items[].shader object
If omitted none
Optional shader on the option.
choices.items[].audio object
If omitted none
Focus / confirm SFX.
choices.items[].state_visual object
If omitted none
Visual tweaks by interaction state.
choices.items[].debug object
If omitted none
Debug-only metadata.
choices.source string
If omitted none
Pack stem under themes/ for the whole menu.
choices.style string
If omitted game / choice_style
Choice button look (themes/<name>.json).
choices.item object
If omitted none
Defaults merged into every item.
choices.shuffle bool
If omitted false
Deterministic shuffle. Forbidden in free layout mode.
choices.show_numbers bool
If omitted true
Prefix captions with N. .
choices.container object
If omitted game layout.choices
Host, anchors, plate style, transitions.
choices.timer object
If omitted none
Countdown while the menu is open.
input.flag string
If omitted required for a working input
Flag that receives the typed value.
input.prompt string
If omitted none
Label above the field.
input.default string
If omitted empty
Ghost text; empty Enter/OK uses this when there is no seed. Supports {flag}.
input.placeholder string
If omitted none
Ghost hint when default is empty; never submitted. Supports {flag}.
input.prefill string
If omitted empty field
Real editable text in the field. Supports {flag}.
input.prefill_from_flag bool
If omitted false
Editable seed from the current flag value when the flag exists (wins over prefill).
input.max_length number
If omitted unlimited
Maximum length.
input.pattern string
If omitted any text
Regex the value must match.
input.style string
If omitted code fallbacks
Pack under themes/ for the field look.
input.source string
If omitted none
Alternate pack resolve key.
input.container object
If omitted defaults
Layout / plate around the field.
input.field object
If omitted defaults
Textures and colours for the input box.
input.confirm object
If omitted defaults
Confirm control chrome.
text_metas object
If omitted none
Map [url=key] markers to a url string or jump/set_flag object.
panel string
If omitted routing chain
Dialogue-box preset from the theme.
panel_id string
If omitted from talking → main
Target panel instance id.
If omitted replace
Values replace · append · plates
Replace the panel body, append as one label, or stack named plates.
append_direction enum
If omitted down
Values down · up
Where new lines or plates attach.
append_gap_lines number
If omitted 0
Blank lines between append chunks (ignored in plates).
append_gap_px number
If omitted 0
Pixel gap between append chunks or plates.
plate string
If omitted sticky / character / preset default
Which plates.<name> to use for this moment when text_mode is plates. Prefer character ui.default_plate for chat sides.
hide_panel bool
If omitted false
Hide the routed panel for this moment.
hide_continue bool
If omitted false
Hide the continue control.
hide_back bool
If omitted false
Hide the back control.
driver_typing bool
If omitted true
Typing driver for speakers. False for silent lines.
driver_typing_targets array
If omitted all talking
Limit typing drive to these speaker ids.
panel_shader object
If omitted clear sticky shader
Per-moment panel shader. Absent clears a sticky one.
panel_presence_mode enum
If omitted routing default
Values talk_only · visible_with_char · always_visible
One-cycle presence override.
panel_non_talking_mode enum
If omitted routing default
Values hide · readonly_last_text
One-cycle non-talking behaviour.
panel_readonly_style_key string
If omitted theme default
Which readonly_styles.<name> to use.
labelplate object
If omitted theme / routing
Same keys as the labelplate command.

Learn more: moment

characters/{id}/{pack}.json — file map

One art pack for a character. Folder characters/<id>/; pack file e.g. default.json. Expression names are free. Reserved filenames (not packs): sprites.json, objects.json, metadata.json, profiles.json.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "display_name": "Emily",                // speaker label
  "display_name_key": "char.emily.name",  // translated label
  "ui": { … },                            // panel and draw defaults for this pack
  "drive": {},                            // mouth / focus rules
  "composition_transitions": {},          // expression swap fades
  "default": [ { … } ],                   // example expression — layers bottom → top
  "…": { … }                              // more expression ids — layer arrays or inherit objects
}

Root keys

Top-level keys that are not objects.

If omitted folder id
Name on the plate when this character talks.
display_name_key string
If omitted use display_name
Catalog key for the display name.
drive object
If omitted none
Talking and focus drive — [Drive rules](../character/drive-rules.md).
If omitted none
How layers crossfade — [Composition transitions](../character/composition-transitions.md).

Learn more: display_name · drive · composition_transitions

ui

Panel and draw defaults for this pack.

{
  "ui": {                                     // panel and draw defaults for this pack
    "default_panel_id": "main",               // panel instance
    "default_panel_preset": "default",        // panel preset
    "default_plate": "them",                  // plates mode side
    "default_labelplate_style_key": "default",  // name plate style
    "draw_layer": "behind_panel",             // draw plane
    "panel_slots": {}                         // slot art map
  }
}

Keys

default_panel_id string
If omitted routing default
Which panel this character prefers.
default_panel_preset string
If omitted routing default
Theme panel.<name> preset.
If omitted preset default_plate
Which plates.<name> this character prefers under text_mode plates.
default_labelplate_style_key string
If omitted theme default
labelplate.<name> style.
draw_layer enum
If omitted behind_panel
Values behind_panel · above_panel
In front of or behind the dialogue box.
panel_slots object
If omitted none
Which pack layers fill panel slots.

Learn more: ui

default

Example expression — layers bottom → top.

{
  "default": [ {         // example expression — layers bottom → top
    "path": "base.png",  // layer image
    "z": 0,              // draw order
    "offset": [0, 0],    // pixel nudge
    "frames": [],        // flipbook frames
    "fps": 12            // flipbook rate
  } ]
}

Keys

path path
If omitted required for still layer
Path relative to the character folder / pack.
z number
If omitted order in array
Optional explicit z.
offset array
If omitted [0,0]
[x, y] offset.
frames array
If omitted still path
Frame list instead of a single path.
fps number
If omitted pack default
Frames per second.

Learn more: default

cursors.json — file map

Soft pointer profiles. Pick a default id, then define each look under profiles. Locale overlay: cursors_<locale>.json.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "default": "default",  // active profile id
  "profiles": { … }      // named pointer looks — any ids you choose
}

Root keys

Top-level keys that are not objects.

default string
If omitted built-in / first profile
Which profile is used at start and after reset.

Learn more: default

profiles

Named pointer looks — any ids you choose.

{
  "profiles": {                        // named pointer looks — any ids you choose
    "default": {                       // any name you choose
      "kind": "image",                 // what draws the pointer
      "image": "sprites/pointer.png",  // pointer picture
      "object": "custom_cursor",       // object id
      "hotspot": [2, 2],               // click point
      "scale": 1.0,                    // size
      "modulate": "#ffffff",           // tint
      "opacity": 1.0,                  // alpha
      "shader": {}                     // pointer shader
    }
  }
}

Keys

kind enum
If omitted image
Values image · obj
image = PNG path · obj = object definition id.
image path
If omitted required for kind image
Path under the project.
object string
If omitted required for kind obj
Stem of sprites/<id>.obj.json.
hotspot array
If omitted [0,0]
[x, y] hotspot in the image.
scale number
If omitted 1.0
Scale multiplier.
modulate color
If omitted no tint
Colour multiply.
opacity number
If omitted 1.0
0–1 opacity.
shader object
If omitted none
Optional shader object — [Shared shapes](../shared-shapes.md#effects-shaders).

Learn more: profiles

keybindings.json — file map

Action → list of keys. You only override the actions you care about; each listed action fully replaces the built-in keys for that name. Locale overlay: keybindings_<locale>.json.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "dialogue_advance": ["space", "enter", "mouse_left"],  // advance / confirm
  "dialogue_back": ["backspace"],             // rollback
  "pause": ["escape"],                        // pause screen
  "save": ["s"],                              // quick save
  "load": ["l"],                              // quick load
  "quit": ["q"],                              // quit
  "choice_1": ["1"],                          // choice 1
  "choice_2": ["2"],                          // choice 2
  "choice_3": ["3"],                          // choice 3
  "choice_4": ["4"],                          // choice 4
  "choice_5": ["5"],                          // choice 5
  "choice_prev": ["up"],                      // previous choice
  "choice_next": ["down"],                    // next choice
  "choice_cycle": ["space"],                  // cycle choices
  "choice_confirm": ["enter", "kp_enter"],    // confirm choice
  "debug_toggle": ["f12"],                    // debug overlay
  "debug_author_refresh": ["f5"],             // reload project
  "debug_ui_refresh": ["f6"],                 // reload look
  "debug_switch_project": ["f2"],             // switch project
  "…": { … }                                  // other actions you map the same way
}

Root keys

Top-level keys that are not objects.

If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
pause array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
save array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
load array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
quit array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
choice_1 array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
choice_2 array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
choice_3 array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
choice_4 array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
choice_5 array
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.
If omitted engine default for this action
Key / mouse names for this action. Your array **replaces** the default list for that action.

Learn more: dialogue_advance

sprites/{id}.obj.json — file map

Reusable object definitions. One file per id under sprites/. Instances use the obj command. See Objects.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "id": "lamp_widget",         // definition id
  "layer": "overlay",          // default draw plane
  "transition_in": "fade",     // default show
  "transition_out": "fade",    // default hide
  "transition_duration": 0.3,  // default fade length
  "modulate": "#ffffff",       // root tint
  "scale": 1.0,                // root scale
  "rotation_degrees": 0,       // root rotation
  "parts": [ { … } ]           // pieces of the object (required)
}

Root keys

Top-level keys that are not objects.

id string
If omitted required
Must match the file stem.
layer enum
If omitted overlay
Values bg_front · fg · overlay
Where instances draw unless the command overrides.
transition_in enum
If omitted instant
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Default enter transition for instances.
transition_out enum
If omitted instant
Values instant · fade · slide_up · slide_down · slide_left · slide_right
Default leave transition.
transition_duration number
If omitted 0
Values seconds
Seconds for default transitions.
modulate color
If omitted no tint
Colour multiply for the whole object.
scale number
If omitted 1
Number or [sx, sy].
rotation_degrees number
If omitted 0
Degrees.

Learn more: id

parts

Pieces of the object (required).

{
  "parts": [ {                                // pieces of the object (required)
    "id": "ptr",                              // part id
    "kind": "polygon",                        // shape
    "x": 0,                                   // local x
    "y": 0,                                   // local y
    "width": 40,                              // width
    "height": 20,                             // height
    "order": 0,                               // draw order
    "points": [[0, 0], [10, 0], [5, 8]],      // polygon points
    "color": "#343a52e0",                     // fill colour
    "opacity": 1.0,                           // alpha
    "visible": true,                          // shown
    "path": "sprites/face.png",               // image path
    "text": "OK",                             // caption
    "bind": {"y": {"flag": "score", "map": {"5": 23}}},  // live flag links
    "bbcode_enabled": false,                  // rich text
    "font_size": 16,                          // text size
    "align_h": "left",                        // text align
    "autowrap": false                         // wrap text
  } ]
}

Keys

id string
If omitted required
Used by obj update patches.
kind enum
If omitted required
Values image · rect · polygon · text
What this part draws.
x number
If omitted 0
Offset inside the object.
y number
If omitted 0
Offset inside the object.
width number
If omitted from art
For rect / text / sized images.
height number
If omitted from art
For rect / text / sized images.
order number
If omitted 0
Higher draws above.
points array
If omitted polygon only
[[x,y], …] for kind: polygon.
color color
If omitted white / theme
Rect / polygon / text colour.
opacity number
If omitted 1
0–1.
visible bool
If omitted true
Hide a part without removing it.
path path
If omitted image kinds
For kind: image.
text string
If omitted text kind
For kind: text.
bind object
If omitted none
Flag → property — [Live flags](../../directing/live-flags.md).
bbcode_enabled bool
If omitted false
For kind: text.
font_size number
If omitted theme default
For kind: text.
align_h enum
If omitted left
Values left · center · right
For kind: text.
autowrap bool
If omitted false
For kind: text.

Learn more: parts

animations/presets.json — file map

Named animation presets. Reference with "preset": "nudge" on an animate command. Built-ins (shake, pulse, …) work without this file. Locale overlay: presets_<locale>.json.

The whole file

Keys with { … } open below. Linked names jump to that section. Grey notes say what each key does. Under each section, Keys lists type, If omitted, allowed values, and a short note.

{
  "nudge": { … },  // example preset id — any name you choose
  "…": { … }       // more preset ids — same fields as animate
}

nudge

Example preset id — any name you choose.

{
  "nudge": {                   // example preset id — any name you choose
    "property": "position_y",  // what to tween
    "from": 1.0,               // start
    "to": -8,                  // end
    "duration": 0.12,          // length
    "ease": "in_out",          // ease
    "trans": "sine",           // transition curve
    "loop": "ping_pong",       // repeat
    "count": 2,                // repeat count
    "relative": true,          // relative to
    "keyframes": [],           // keyframe list
    "frames": [],              // frame list
    "fps": 12,                 // frame rate
    "intensity": 1.0,          // shake strength
    "distance": 8,             // travel distance
    "start_frame": 0,          // first frame
    "color": "#ffffff",        // tint end
    "opacity": 1.0,            // alpha end
    "scale_from": 1.0,         // scale start
    "scale_to": 1.05,          // scale end
    "decay": true              // shake decay
  }
}

Keys

property string
If omitted required for tween
e.g. position_x, position_y, scale, opacity, modulate.
from number
If omitted current value
Start value (or current if omitted).
to number
If omitted required for tween
End value.
duration number
If omitted 0
Values seconds
Seconds.
ease enum
If omitted out
Values in · out · in_out · out_in
Ease curve.
trans enum
If omitted cubic
Values linear · sine · cubic · quad · quint · expo · elastic · back · bounce
Tween transition.
loop enum
If omitted false
Values true · false · ping_pong
true, false, or ping_pong.
count number
If omitted forever if loop
How many loops (0 = forever when looping).
relative bool
If omitted false
Treat to / from as deltas.
keyframes array
If omitted none
For keyframe mode.
frames array
If omitted none
For frames mode.
fps number
If omitted mode default
For frames mode.
intensity number
If omitted preset default
Built-in / preset knobs.
distance number
If omitted preset default
For some presets.
start_frame number
If omitted 0
Start index for frames mode.
color color
If omitted unchanged
For modulate / color tweens.
opacity number
If omitted unchanged
For opacity tweens.
scale_from number
If omitted from
Alternate scale start knob.
scale_to number
If omitted to
Alternate scale end knob.
decay bool
If omitted false
Whether shake intensity falls off.

Learn more: nudge

Everything A–Z

One flat list of common names: commands, script marks, important keys, and actions. One line each — use your browser search (Ctrl+F / ⌘F).

This list gathers the words used across the handbook. Full command tables: Commands. Words these docs use: Glossary.

Script marks

NameWhat it doesWhere
NAME:Speaker lineLines and speakers
NAME [expr]:Speak + stage expressionLines and speakers
(silent)No typing mouth / beep for that speakerLines and speakers
Label|…Custom name-plate text for the lineLines and speakers
[…]Stage direction / keyword commandStage directions
:: idScene / moment idScenes and jumps
-> idJumpScenes and jumps
> / choice linesChoice menuChoices and memory
{@w} / {@cps} / {@p}Mid-line pacing marksPacing
{@cN}Mid-line command cue (flow_commands)Pacing · Command flow
[if …] / [flag = …]Branch / set memoryChoices and memory

Commands

NameWhat it doesWhere
animate / stop_animateTween or stop motion on stage / UIanimate · stop_animate
bgBackground picture / clearbg
bgm / sfx / typing_sfxMusic and soundsbgm · sfx · typing_sfx
call / returnCall a scene as a subroutinecall · return
cameraPan / zoom (in-dev stage camera)camera
charShow / move / hide / look a characterchar
char_layoutSwitch character place profilechar_layout
char_packSwitch active art packchar_pack
choice_styleSticky choice look packchoice_style
cursorSoft pointer profilecursor
input / input_styleAsk for typed textinput · input_style
jump / jump_ifGo to a momentjump · jump_if
labelplateOverride speaker name platelabelplate
obj / spriteStage propsobj · sprite
panelDialogue box look / append / policypanel
screenOpen / close a menu or HUDscreen
screen_shaderFull-screen effectscreen_shader
set_flagSet memoryset_flag
stageLocation beat (in-dev)stage
theme_styleMood theme overlaytheme_style
timerTimed wait / choice timertimer
All commandsIndexCommands

Moment / direction fields (common)

NameWhat it doesWhere
text / talkingWhat is said / whoThe same beat as data
nextWhere to go afterScenes and jumps
choices / inputBranch or typeChoices · input
commandsEnter directionsCommand flow · Commands
flow_commandsMid-line batches fired by {@cN}Command flow · Flow tags
exit_commands / exit_delayLeave directions + waitCommand flow · Pacing
panel / panel_id / text_modeWhich box / appendNode fields · How text finds a panel
when / applySkip or stickyShared shapes
bindLive flag → propertyReacting to flags live

game.json (common)

NameWhat it doesWhere
title / version / authorsIdentityName, window, language · Ship identity
windowBase resolutionName, window, language
locale / i18nLanguageTranslations
icon / splash / androidShell brand / Android idTitle, icon, splash · Platforms
typing / soundsText speed / default SFXText speed · Default sounds
sessionSave slots, HUD id, ending, BackSave, Back and story end
locale / i18nBase language + switcherTranslations
layoutChoices / character placesChoice and character layout
transitions / notifications / cursorVeil / toasts / pointerTransitions · Notifications · Cursor
extensionsNative extension packs (advanced)Native extensions · extensions API
storiesSeveral story packs in one project (early)Several stories
Full mapEvery documented keygame.json map

Screens and actions

NameWhat it doesWhere
role: modal / hud / sceneScreen kindScreens
Layers background / menu / content / sprites / chromeScreen structureScreens detail
Content blocks / bindSettings rows and cardsContent blocks · Widget bindings
new_game / continue / quitTitle actionsActions and hooks · Buttons and actions
screen:<id> / closeOpen / popActions and hooks
show_ifConditional buttonScreens
has_autosaveCommon continue gateSave, Back and story end

Theme blocks (common)

NameWhat it doesWhere
typography / colorsFonts and fallback coloursthemes map
panel.<preset>Dialogue box looktheme panel
labelplate.<style>Name-plate looktheme labelplate
buttons / choice packsMenu vs story choice looksbuttons · Choice packs
scrollbar / notificationBars and toastsHow a panel looks · Notifications

Character pack (common)

NameWhat it doesWhere
display_name / uiLabel and preferred panelPack root · characters map
Expression keys / extendsLayered looksExpressions and layers
driveTalking / focus reactionsDrive rules
composition_transitionsExpression fade policyComposition transitions
Layer frames / pathStill or flipbookExpressions and layers

Other project files

NameWhat it doesWhere
screens.jsonTitle, pause, HUD, …screens map · Screens detail
keybindings.jsonRemap actionskeybindings map
cursors.jsonSoft cursor profilescursors map
animations/presets.jsonAuthor animate presetsanimations map
sprites/<id>.obj.jsonObject definitionsobjects map
story/strings.jsonTranslation catalogStrings · Translations

Next

Detail pages · Shared shapes

Detail pages

One page per command or settings group, with every option: type, default, what it does, and the rules that decide between them.

Commands

→ Commands — every stage direction (bg, char, panel, jump, …)

Story

→ Story detail — node fields, choices, script sugar, flow tags, Mode B
Overview: story/story.json map

Screens

→ Screens detail — layers, content blocks, binds, actions
Overview: screens.json map

Theme

→ Theme detail — typography, colours, panel, widgets, choice & input packs
Overview: themes/default.json map

Characters

→ Character pack detail — pack root, expressions, drive, composition fades
Overview: characters pack map

Other files

→ Other files — cursors, keys, objects, animation presets, strings, extensions
Maps: cursors · keybindings · objects · animations · game.json → extensions

Guide: Native extensions

File maps

FileMap
game.jsongame.json
screens.jsonscreens.json
themes/default.jsonthemes/default.json
story/story.jsonstory/story.json
characters/<id>/<pack>.jsoncharacters
cursors.jsoncursors.json
keybindings.jsonkeybindings.json
sprites/<id>.obj.jsonobjects
animations/presets.jsonanimations

Shared contracts

→ Shared shapes — transitions, texture specs, binds, hooks, state_visual, source, shaders

Use Everything A–Z to jump by name.

Next

Commands · Story · Screens · Theme · Characters · Other · File maps

Commands

Every stage direction and data command — full fields, types, and defaults. Guide pages teach when to use each; these pages list every key.

Commands live in:

  • Script brackets: [bg street fade 0.5]
  • Moment fields: commands, flow_commands, exit_commands — story/story.json map
  • Some also as sticky / node fields (input, labelplate, …)

Stage

CommandJob
bgBackground picture / clear
stageLocation beat (in development)
cameraPan / zoom (in development)
charShow / move / hide / expression
char_packSwitch art pack
char_layoutSwitch place profile
spriteLayered stage picture
objReusable object instance

Dialogue box and name

CommandJob
panelDialogue box look / append / policy
labelplateSpeaker name plate

Sound and motion

CommandJob
bgmMusic
sfxOne-shot sound
typing_sfxTyping beep override
animateTween / keyframes / frames
stop_animateStop a running animation

Flow and memory

CommandJob
jumpGo to a moment
jump_ifBranch on expression
call / returnSubroutine
set_flagSet memory
timerTimed wait / fire

Menus, look, and chrome

CommandJob
screenOpen / close a screen
screen_shaderFull-screen shader
choice_styleSticky choice pack
inputAsk for typed text
input_styleSticky text-input pack
theme_styleMood theme overlay
cursorSoft pointer profile

Shared fields that appear on many commands (when, transitions, texture specs, binds): Shared shapes.

Detail pages · Everything A–Z · Adding direction

bg

Shows or clears the stage background image.

{ "type": "bg", "value": "street", "transition": "fade", "duration": 0.5 }
[bg street fade 0.5]
[bg clear]

Usually you need

FieldTypeDefaultEffect
valuestring or null—Image name under backgrounds/; null clears
transitiontransition kindfade if omittedEnter / leave
durationnumber ≥ 00.5 if omitted with default fadeSeconds

All fields

FieldTypeDefaultEffect
valuestring or null—Image / clear
transition / transition_in / transition_outkindomit → fadeKinds: instant, fade, slide_*
transition_duration / durationnumber ≥ 0omit → 0.5Seconds
opacitynumber 0–11 when omitted on a showPlate alpha baseline (binds compose on top)
modulatecolor stringwhite when omitted on a showTint baseline
visiblebooltrue when omitted on a showShow/hide baseline
shaderstring or object or nullabsent = keep; null = clearStem under shaders/, or an object with name / params / target / pass / margin — Effects
shader_paramsobject—Uniforms when setting shader
bindobject—image, opacity, modulate, visible, plus shader:…
interactionobject—hooks.on_show / on_hide / on_change
whenexpression—Skip when falsy

Bind notes

  • bind.image uses the same stems as value (files under backgrounds/). Swaps are instant — use an explicit bg command when you want a fade.
  • Bind look keys compose with the command baselines above (opacity / modulate / visible).
  • While a model background or an active background pack is showing, image / opacity / modulate / visible binds are ignored (shader:… still applies when a shader is set).

Clearing the image also clears shader and bind. Guide: Backgrounds, Transitions, Binds, Shaders.

stage

Stage locations (in development).

This command exists in the runtime schema but is not ready for shipping stories yet.

→ In development

Commands · Everything A–Z

camera

Camera (in development).

This command exists in the runtime schema but is not ready for shipping stories yet.

→ In development

Commands · Everything A–Z

char

Shows, changes, moves, or hides a character.

{ "type": "char", "name": "hana", "expression": "happy", "position": "left", "transition": "fade", "duration": 0.4 }

In script.txt:

[hana happy left fade 0.4]
[hana]                        # default expression, current or auto-assigned slot
[hana hide]
Hana [happy]: Good morning.   # implicit char command before the line

Usually you need

FieldTypeDefaultEffect
namestring—Character folder under characters/. Empty or missing hides whoever is in the resolved slot
expressionstring"default"Expression key in the pack. Unknown keys fall back to default
positionstringsee slot resolutionSlot alias (left, center, right, …)
showbooltruefalse hides the character in the resolved slot
transitiontransition kind"instant"Enter and leave
durationnumber ≥ 00Seconds when transition_duration is absent

That is enough for most stories. Everything below is the full surface.

All fields

FieldTypeDefaultEffect
namestring—Character folder under characters/. An empty or missing name hides whoever is in the resolved slot
expressionstring"default"Expression key in the pack. Unknown keys fall back to default
packstringactive packPack file to read the expression from, without .json
positionstringsee belowSlot alias from the active layout profile
showbooltruefalse hides the character in the resolved slot
draw_layer"behind_panel" | "above_panel"pack, then slot, then behind_panelWhich plane the character draws on
transitiontransition kind"instant"Applies to both entering and leaving
transition_intransition kind—Overrides transition when appearing
transition_outtransition kind—Overrides transition when hiding
transition_durationnumber ≥ 00Seconds. Takes priority over duration
durationnumber ≥ 00Seconds, when transition_duration is absent
swap_modeswap modefrom the packForces how an expression change is blended
modulateHTML color#ffffffTint
opacitynumber 0–11Alpha
scalenumber or [sx, sy]1Scale
shaderstring, null, or shader objectkeep currentShader for this character
shader_paramsobject{}Uniform values, when shader is a plain name
bindobject—Live property binding to flags
interactionobject—Hooks fired when this command runs
whenexpression—Skip the whole command when the expression is falsy

Transition kinds are instant, fade, slide_up, slide_down, slide_left, slide_right, or "" to mean “not specified”. Swap modes are auto, in_only, crossfade, out_in, cross_slide.

Which slot the character lands in

position takes a slot alias — whatever the active layout profile defines, such as left or center by default, or your own names in a custom profile. See Planes and slots.

Omit position and the engine resolves it:

  1. The character’s current slot, if they are already on screen.
  2. Otherwise the first free alias, in the profile’s auto_selection_policy order.
  3. If every slot is taken, nothing happens and a warning is logged.

Showing a character also clears them from any other slot immediately, so the same character can never occupy two slots.

Visual fields are not sticky

modulate, opacity, and scale apply only to commands that mention them. Any char command that leaves one out clears the previous value for that slot.

[hana happy]                  # tinted
[hana sad]                    # tint is gone

Repeat the value on every command that should keep it, or move it into a pack drive rule instead.

The final look of a slot multiplies four sources in this order: the slot’s own modulate/scale from the layout profile, the command’s values, any matching pack drive rules, and finally bind. opacity and a bound opacity replace the alpha rather than multiplying it.

Transitions and expression changes

The command’s transition is used whenever the command names one, meaning a non-empty transition, transition_in, or transition_out, or any transition_duration or duration.

When the command says nothing about transitions:

  • Changing expression on a character who is already on screen uses the pack’s composition_transitions policy for that expression pair.
  • Anything else is instant.

swap_mode decides how the outgoing and incoming art overlap during an expression change. Without it, the pack policy chooses, and auto resolves to crossfade for a fade and in_only for a slide.

Shaders

shader valueResult
absentKeeps whatever shader the slot already has
nullClears the shader
"drop_shadow"Loads shaders/drop_shadow.gdshader, with shader_params as uniforms
{ "name": …, "params": …, "target": …, "pass": …, "margin": … }Full shader object; use margin to grow the render area so effects are not clipped

Binding to flags

bind drives a property from a flag while the character is on screen.

{
  "type": "char",
  "name": "hana",
  "bind": {
    "opacity": { "flag": "trust", "linear": [0, 0.3, 10, 1.0] },
    "modulate": { "flag": "mood", "map": { "angry": "#ffbbbb" }, "default": "#ffffff" }
  }
}

Bindable properties are opacity, modulate, scale, and shader:<uniform> for any uniform of the current shader. Full modes and surfaces: Binds.

Hooks

interaction.hooks fires when the command runs, not when the player clicks the character. Available events are on_show, on_hide, and on_change as a fallback for either. on_hide is chosen when show is false, otherwise on_show. Actions run in the gameplay UI context.

{ "type": "char", "name": "hana", "show": false,
  "interaction": { "hooks": { "on_hide": { "type": "set_flag", "values": { "hana_gone": true } } } } }

char_pack

Sets the active composition pack for a character. Does not redraw alone — later char commands use this pack.

{ "type": "char_pack", "name": "emily", "pack": "full" }
[char_pack emily full]

Usually you need

FieldTypeDefaultEffect
namestring—Character folder id
packstring—Pack file stem (full → full.json)

All fields

FieldTypeDefaultEffect
namestring—Character id
packstring—Pack id
whenexpression—Skip when falsy

char_layout

Switches the active layout.characters profile (slot geometry).

{ "type": "char_layout", "profile": "closeup", "apply": "persistent", "transition": "fade", "duration": 0.35 }
[char_layout closeup fade 0.35]
[char_layout reset]

Usually you need

FieldTypeDefaultEffect
profilestring—Profile id (unless reset)
resetboolfalseBack to "default"
transition"" | "instant" | "fade""" → profileSlot glide
durationnumber ≥ 00 (fade with no duration → profile or 0.3)Seconds

All fields

FieldTypeDefaultEffect
profilestring—Target profile
resetboolfalseReset to default
apply"persistent" | "node_local""persistent"Scope
policy"best_effort_remap" | "strict""best_effort_remap"Occupancy remap
transition"" | "instant" | "fade""" → profileVisual
durationnumber ≥ 00Seconds
whenexpression—Skip when falsy

Only instant / fade (no slides). See Planes and slots, Transitions, layout.characters.

sprite

Places a free prop (image, rect, or polygon) on a stage layer. JSON / Mode B only (no script sugar).

{
  "type": "sprite",
  "id": "chair",
  "value": "chair",
  "layer": "bg_front",
  "x": 625,
  "y": 494
}

Usually you need

FieldTypeDefaultEffect
idstring—Named sprite
valuestring or null—Asset name; null hides
layer"bg_front" | "fg" | "overlay""overlay"Draw band
x / ynumber—Position

All fields

FieldTypeDefaultEffect
idstring—Named id
layerenum"overlay"Plane band
kind"image" | "rect" | "polygon" | "model""image"Shape (model is in development)
valuestring or null—Asset / hide
x / y / width / height / ordernumber—Geometry
offsetnumber or array—Additive px
rotation_degreesnumber—Angle
color / corner_radius / points…—Primitives
opacity0–11.0Alpha
modulate / scalecolor / number or [sx,sy]—Tint / scale
transition / transition_in / transition_outkind"instant"Show/hide
transition_duration / durationnumber ≥ 00 (fade often 0.5 in comments)Seconds
sfxstring—Loop until hide
shaderobject or null—Shader; uniforms go in the object’s params (a top-level shader_params is not a sprite field)
bind / interaction / state_visual / layoutobject—Live / hooks / states / host
clearlayer name or "all"—Clear sprites
whenexpression—Skip when falsy

Sprites and objects · Planes · Binds.

obj

Shows, updates, or destroys an instance of a catalog object (sprites/<id>.obj.json).

{
  "type": "obj",
  "object": "lamp_widget",
  "instance_id": "score_lamp",
  "show": true,
  "x": 16,
  "y": 16
}

Usually you need

FieldTypeDefaultEffect
instance_idstring—Runtime instance id
objectstring—Def id (required on first show)
showbool—Show / hide (true + object to create)
x / ynumber0 on createPosition

All fields

FieldTypeDefaultEffect
instance_idstring—Instance
objectstring—Definition id
showbool—Visibility
x / ynumber0Position
transition / durationkind / number"instant" / 0Show/hide
color / opacity / sfx…—Look / loop sfx
shader / shader_params——Shader
bind / layout / interactionobject—Binds / host / hooks
apply"persistent" | "node_local""persistent"Scope
updateobject—Part patch
destroyboolfalseDestroy instance
whenexpression—Skip when falsy

Def shape: Objects. Guide: Sprites and objects.

panel

Controls dialogue panel instance look, visibility, content mode, and presence policy.

{ "type": "panel", "preset": "whisper", "panel_id": "main", "apply": "persistent" }
[panel whisper]
[panel reset]
[panel clear_append]
[panel panel_id phone clear_append]

Usually you need

FieldTypeDefaultEffect
presetstring—Theme preset name
panel_idstring"main"Panel instance
text_mode / clear_append / plate——Append or plates mode, wipe history, or sticky plate name
showbool—Visibility (true on an off-route panel sticks against policy hide until cleared)
apply"persistent" | "node_local""persistent"Scope

All fields

FieldTypeDefaultEffect
panel_idstring"main"Instance
presetstring—Layout preset
showbool—Visibility. Off-route true sticks against policy hide until show: false, reset, or policy_reset
apply"persistent" | "node_local""persistent"Scope
resetbool—Reset panel (clears sticky max_height, append state, policy, shaders)
transition / transition_in / transition_outkindper contractShow/hide
transition_duration / durationnumber ≥ 0—Seconds
text_mode"replace" | "append" | "plates""replace"Content mode
clear_appendbool—Wipe append/plates history only (keeps text_mode, direction, preset, policy)
append_direction"down" | "up""down"Append / plates direction
append_gap_lines / append_gap_px≥ 00Gaps (append_gap_lines ignored in plates)
platestring or null—Sticky plate name for text_mode plates (null / clear clears)
max_heightnumber, "NN%", or "full"theme / legacySticky height ceiling for this panel
znumber0Draw order
mode"adv"—Panel mode
shaderobject or nullomit = keepPanel shader
presence_mode"talk_only" | "visible_with_char" | "always_visible""talk_only"Persistent policy
non_talking_mode"hide" | "readonly_last_text""hide"
readonly_style_keystring—Readonly style
policy_resetbool—Clear policy to defaults
linked_char_idstring—Explicit character link
bindobject—Flag binds
interactionobject—Hooks
whenexpression—Skip when falsy

Node panel_* fields are one render cycle; this command is sticky unless node_local. Same-node text_mode / preset / append_* / clear_append also apply to that node’s first paint (explicit node fields win). Sticky text_mode: replace, clear_append, or reset clears that panel’s append history; a node-only text_mode: replace does not. Presets: Theme: panel. Routing: Panel routing.

labelplate

Overrides the speaker nameplate on a panel. Same dictionary may appear as node field labelplate.

{
  "type": "labelplate",
  "panel_id": "main",
  "text": "???",
  "style_key": "default",
  "apply": "persistent"
}

Usually you need

FieldTypeDefaultEffect
text / text_keystring—Plate body ({@auto} expands to speaker)
style_keystring—Theme labelplate.<key>
visiblebool—Show / hide
apply"persistent" | "node_local""persistent"Scope

All fields

FieldTypeDefaultEffect
panel_idstring"main"Host panel
apply"persistent" | "node_local""persistent"Sticky vs this node only
resetbool—Clear override
mode"inline" | "docked"—Placement
visiblebool—Visibility
text / text_keystring—Body
prefix / suffixstring—Affixes
prefix_key / suffix_keystring—Affixes from strings catalog
gap≥ 0—Spacing
style_keystring—Named style
whenexpression—Skip when falsy

Sticky vs node field

  • Command with apply: "persistent" sticks until reset or new session.
  • node_local / node field applies for that node’s render cycle.
  • Style resolution: explicit style_key → panel preset labelplate_style_key → character ui.default_labelplate_style_key.

Sticky overrides: Sticky state and clear. No dedicated script keyword — JSON / Mode B / node field. Theme: labelplate. Speakers: Labelplate and speakers. when and apply.

bgm

Plays or stops looping music from audio/bgm/.

{ "type": "bgm", "value": "main_theme", "volume": 0.7, "fadein": 1.0 }
{ "type": "bgm", "value": null, "fadeout": 1.0 }
[bgm main_theme fadein 1.0 volume 0.7]
[bgm stop]

Usually you need

FieldTypeDefaultEffect
valuestring or null—Track name; null / stop clears
volume0–11.0Track ceiling
fadein≥ 01.0 when playingNew track fade-in
fadeout≥ 00 play; 1.0 if stopping and omitted/0Old track fade-out

All fields

FieldTypeDefaultEffect
valuestring or null—Track / stop
volume0–11.0Ceiling
fadein≥ 01.0 (play)Fade in
fadein_from0–10.0Start fraction of ceiling
fadeout≥ 0see aboveFade out
fadeout_to0–10.0End fraction
whenexpression—Skip when falsy

Same track again does not restart; volume may update. Both fadein + fadeout on play → crossfade. Title default BGM uses sounds.main_bgm* (often 1.5s fades) — not these command defaults. Audio · sounds.

sfx

Plays a one-shot from audio/sfx/.

{ "type": "sfx", "value": "door_open", "volume": 0.5 }
[sfx door_open volume 0.5]

Usually you need

FieldTypeDefaultEffect
valuestring—Sample name
volume0–11.0Linear scale

All fields

FieldTypeDefaultEffect
valuestring—Sample; empty/null is a no-op
volume0–11.0Scale
whenexpression—Skip when falsy

Audio.

typing_sfx

Overrides the typing beep sample until reset. Not stored in the story save.

{ "type": "typing_sfx", "value": "typing_alt" }
{ "type": "typing_sfx", "value": null }
[typing_sfx typing_alt]
[typing_sfx off]

Usually you need

FieldTypeDefaultEffect
valuestring or null—Sample under audio/sfx/; null / off → the speaking panel’s typing.beep.sample

All fields

FieldTypeDefaultEffect
valuestring or null—Override / reset
whenexpression—Skip when falsy

Reveal speed is separate — Typing. Beep defaults: typing.beep · Audio.

animate

Tweens or steps a property on something already on stage. Not the same as show/hide Transitions.

{
  "type": "animate",
  "target": "char:left",
  "property": "position_x",
  "to": 40,
  "duration": 0.4,
  "relative": true
}

Usually you need

FieldTypeDefaultEffect
targetstring—bg, char:slot, sprite:id, obj:id, panel, …
propertystring—opacity, position_x / y, scale, modulate, shader:u, …
toany—End value
duration≥ 00.5 (tween)Seconds
presetstring—shake / pulse / flash / bob or author preset

All fields

FieldTypeDefaultEffect
idstringautoFor stop_animate
presetstring—Built-in or animations/presets.json
mode"tween" | "keyframes" | "frames"inferredMode
targetstring—Target grammar
layerstring—Optional layer hint for some targets
propertystring—Property path
from / toanyfrom = currentEnds
duration≥ 00.5Seconds
easein | out | in_out | out_in"in_out"Ease
translinear | sine | …"linear"Curve
loopbool | "ping_pong"falseLoop
count≥ 00 (= ∞ when looping)Iterations
relativeboolfalseOffsets
keyframes / frames / fps / start_frame…fps min 0.001Advanced
intensity / decay / scale_from / scale_to / distance / color / opacitypreset extrasshake 5 / true; pulse 0.95–1.05; bob 10Preset knobs
whenexpression—Skip when falsy

Preset duration defaults: shake 0.4, pulse 0.6, flash 0.3, bob 2.0.

Do not animate opacity while a fade transition runs on the same node. Animations · presets · stop_animate.

stop_animate

Stops running animations and restores pre-animation property values.

{ "type": "stop_animate", "id": "shake_1" }
{ "type": "stop_animate", "target": "char:left" }
{ "type": "stop_animate", "all": true }

Usually you need

FieldTypeEffect
idstringStop by animate id
targetstringStop all on that target
allboolStop everything

All fields

FieldTypeEffect
allboolStop all
idstringBy id
targetstringBy target
whenexpressionSkip when falsy

animate · Animations.

jump

Jumps to a node after the current command batch (first jump in the batch wins).

{ "type": "jump", "to": "after" }
[jump after]

Usually you need

FieldTypeDefaultEffect
tostring—Target node id (END / START / NEXT keywords apply)

All fields

FieldTypeDefaultEffect
tostring—Target
whenexpression—Skip when falsy

Linear -> / node next are not this command. Batch timing: Command flow. Expressions.

jump_if

Conditional jump: first matching case, else optional fallback.

{
  "type": "jump_if",
  "cases": [
    { "expr": "score > 5", "to": "good" },
    { "expr": "score > 0", "to": "ok" }
  ],
  "else": "bad"
}
[jump_if score > 5 -> good else bad]

Usually you need

FieldTypeDefaultEffect
casesarray { expr, to }—First truthy wins
elsestring—Fallback node id

All fields

FieldTypeDefaultEffect
casesarray—{ "expr", "to" }
elsestring—Fallback
whenexpression—Skip whole command when falsy

Expressions · jump.

call

Jumps to a subroutine node after the current command batch and pushes a return frame. Resume is the caller’s resolved next (via next_if / next) — not the rest of the same node.

{ "type": "call", "to": "phone_ring" }
:: hallway
You're in the hallway.
[call phone_ring]
-> hallway_after

:: phone_ring
Your phone rings.
[return]

:: hallway_after
You're still in the hallway.

Usually you need

FieldTypeDefaultEffect
tostring—Subroutine entry node id

All fields

FieldTypeDefaultEffect
tostring—Target
whenexpression—Skip when falsy

Rules

  • Prefer calling from nodes without pending choices; resume uses linear next only.
  • Mid-node resume (text after [call] in the same node) is not supported — put continuation on a following node.
  • Nesting depth is capped (64). A jump / jump_if while a call is active clears the stack (returns abandoned).
  • First control command in a batch wins (call / return / jump / story end).

Batch timing: Command flow. Pair with return.

return

Pops the call stack and jumps to the saved resume node after the current command batch.

{ "type": "return" }
[return]

Usually you need

No fields — empty object / bare [return].

All fields

FieldTypeDefaultEffect
whenexpression—Skip when falsy

Rules

  • Empty stack → warning, no-op (stay on the current node).
  • Empty resume id (caller had no valid next) → story ends after the batch.
  • If a subroutine node has no next and no choices, Continue / advance auto-returns when the stack is non-empty (same resume as an explicit [return]).
  • Prefer an explicit [return] at the end of reusable cutaways.

See call. Batch timing: Command flow.

set_flag

Sets one or more flags from a command (same semantics as node set / [flag = …]).

{ "type": "set_flag", "values": { "helped": true, "score": "= score + 1" } }
[set_flag helped = true]

(Script also uses bare [score += 1] as node set.)

Usually you need

FieldTypeDefaultEffect
valuesobject—Flag map; "= expr" for expressions

All fields

FieldTypeDefaultEffect
valuesobject—Assignments
whenexpression—Skip when falsy

Expressions · Binds (live property updates after flag change).

timer

Starts a named countdown. When it reaches zero, the story navigates to on_end (a node id, or NEXT / END / START). Use pause/resume/stop to control it. There is no built-in countdown display — signal urgency with dialogue, SFX, or your own UI later.

{ "type": "timer", "id": "bomb", "seconds": 30, "on_end": "explode" }
[timer id=bomb seconds=30 on_end=explode]
… later …
[timer id=bomb stop]

Usually you need

FieldTypeEffect
idstringTimer name (restarting the same id replaces it)
secondsnumber > 0Duration (start only)
on_endstringWhere to go when time runs out
actionpause / resume / stopControl an existing timer (instead of start)

Mode A

[timer id=bomb seconds=30 on_end=explode]
[timer id=bomb pause]
[timer id=bomb resume]
[timer id=bomb stop]

Spaced keys also work: [timer id bomb seconds 30 on_end explode].

Rules

  • Start shape: id + seconds + on_end. Control shape: id + action.
  • on_end resolves like choice targets: node id, NEXT, END, or START.
  • Timers freeze while a blocking screen is open (pause menu, etc.) and while the story is not in play. Author pause / resume is separate.
  • Remaining time is saved with the play session. New Game / return to title clears all timers.
  • For a menu that times out, use choices.timer on the choice object — not a separate wait.
  • To stop a timer only when a flag is set, use command meta when on the stop command.

All fields

FieldTypeDefaultEffect
idstring—Required
secondsnumber—Start only; must be > 0
on_endstring—Start only
actionstring—pause, resume, or stop
whenexpression—Skip when falsy

See also

screen

Shows or hides a screen defined in screens.json from the story — any role (hud, modal, scene).

{ "type": "screen", "value": "minimap" }
{ "type": "screen", "value": "minimap", "show": false }
{ "type": "screen", "value": "credits", "mode": "replace" }

In script.txt:

[screen minimap]
[screen minimap hide]
[screen credits replace]

Usually you need

FieldTypeDefaultEffect
valuestring—Screen id (required)
showbooltruetrue pushes the screen; false removes that id from the stack

All fields

FieldTypeDefaultEffect
valuestring—Screen id from screens.json
showbooltrueShow or hide
mode"push" | "replace""push"How to show. "replace" pops the current top screen, then pushes. Ignored when show is false
whenexpression—Skip the command when falsy (JSON / Mode B meta)

Unknown ids error at runtime (same path as a bad screen:id button). Hiding an id that is not stacked is a no-op.

Auto HUD vs this command

game.json → session.hud_screen (default "hud") still auto-shows one HUD when a play session starts. Use screen to show extra HUDs, modals, or scene boards mid-story, or to hide them again.

Several role: "hud" screens can stay up together; each push is independent. Persistent HUDs with the same id replace themselves on push.

screen_shader

Full-screen (or scoped) post-process shader.

{ "type": "screen_shader", "value": "glitch", "target": "game", "shader_params": { "noise_intensity": 0.2 } }
{ "type": "screen_shader", "value": null }

Usually you need

FieldTypeDefaultEffect
valuestring or null—Shader stem; null clears
target"game" | "scene" | "all""game"Coverage plane
shader_paramsobject—Uniforms

All fields

FieldTypeDefaultEffect
valuestring or null—Apply / clear
targetenum"game"Layers 2 / 74 / 99
shader_paramsobject—Uniforms
bindobject—shader:… binds only
apply"persistent" | "node_local""persistent"Scope
resetbool—Clear (alone)
whenexpression—Skip when falsy

Shaders · Planes · Binds.

choice_style

Sets or clears the runtime button choice pack override for the play session. Does not merge into the mood theme and does not change sprite choice groups.

{ "type": "choice_style", "profile": "choice_plain" }
{ "type": "choice_style", "reset": true }

Script sugar:

[choice_style choice_plain]
[choice_style reset]

Usually you need

FieldTypeDefaultEffect
profilestring—Use themes/<profile>.json as the button pack until reset
resetbool—Clear the override → fall back to layout.choices.style

Exactly one of profile or reset should drive the change.

All fields

FieldTypeDefaultEffect
profilestring—Pack stem under themes/
resetbool—Clear sticky override
whenexpression—Skip the command when falsy

Precedence

Button groups only:

  1. Node choices.style
  2. This command’s active override
  3. game.json → layout.choices.style

Does not affect groups with render_mode: "sprite". Pack file keys: Choice packs. Node fields: Choices.

Save / session

Sticky until reset or a new session. Saved and restored with chrome (Save and session).

input

After the line finishes, show a text field, write a flag, then advance. Hosted like choices (stack/free, panel/overlay, packs). Optional pattern gates confirm.

{
  "id": "ask_name",
  "text": "What should I call you?",
  "input": {
    "flag": "player_name",
    "prompt": "Your name?",
    "default": "Hero",
    "max_length": 24,
    "pattern": "^.+$",
    "style": "name_entry",
    "container": { "layout": { "mode": "stack", "host": "panel" } },
    "field": { "layout": { "width": 480 } },
    "confirm": { "label": "OK" }
  },
  "next": "after_name"
}

Script (before or after the line text):

:: ask_name
What is your name?
[input player_name]

:: ask_name_rich
What is your name?
[input player_name "Your name?" default "Hero" max_length 16]

:: ask_hint
What is your name?
[input player_name placeholder "Type a name…" pattern "^.+$"]

:: ask_prefill
What is your name?
[input player_name prefill "Hero" max_length 16]

:: ask_name_again
Edit your name.
[input player_name "Your name?" default "Hero" prefill_from_flag]

:: ask_age
How old are you?
[input age "Age?" pattern "^[0-9]+$"]

Put flag/prompt in the script; put free/overlay positions and rich chrome in story.json on the same node’s input (JSON merges over the script fields). Sticky pack override: input_style.

Usually you need

FieldTypeDefaultEffect
flagstring—Destination flag (required)
promptstring""Label above the field
defaultstring""Ghost text; empty Enter/OK uses this when there is no seed
placeholderstring""Ghost hint only (never submitted); used when default is empty
prefillstring""Editable text already in the field
prefill_from_flagboolfalseIf the flag exists, seed from it (wins over prefill)
max_lengthnumber ≥ 132Clamp on submit
patternstring—Regular expression; fail keeps the field open (no flag write). Prefer ^…$

Ghost and seed

Non-empty keysGhost in the fieldEmpty Enter/OK
neither default nor placeholder(none)""
default onlydefaultdefault (when no seed)
placeholder onlyplaceholder""
bothdefault winsdefault (when no seed)
Seed sourceEditable textClear field then Enter/OK
noneemptysee table above
prefillthat string"" (does not fall back to default)
prefill_from_flag and flag existsflag value (wins over prefill)""

default, placeholder, and prefill support {flag} like dialogue lines (one pass). A flag seed from prefill_from_flag is used as stored.

All fields

FieldTypeDefaultEffect
flagstring—Required
promptstring""Prompt label
defaultstring""Ghost + empty-Enter fallback when no seed
placeholderstring""Ghost hint only
prefillstring""Authored editable seed
prefill_from_flagboolfalseSeed from existing flag when present
max_lengthnumber ≥ 132Clamp
patternstring—Confirm gate
stylestring—Pack under themes/
sourcestring—Compose pack (container / field / confirm / style)
containerobject—Host layout (same shape as choice containers)
fieldobject—Text-field chrome / layout
confirmobject—Confirm button (label, visible, layout)

Timing and keys

  • Appears after the line finishes. If the same node also has choices, choices are shown instead.
  • Enter / Confirm → submit (strip + empty→default when no seed + clamp; pattern may reject).
  • Esc → submit "" (no default substitution). If pattern rejects empty, stay open. Pause does not open while the field is up.
  • Continue / Back are blocked while the field is open.
  • After Back to this node, the field shows again with the same ghost/seed rules (prefill_from_flag uses the flag if it is still set).
  • Confirm label: confirm.label (default OK). Hide the button with confirm.visible: false — Enter / Esc still work.

Validation

Encode rules in pattern (e.g. ^.{3,}$, ^[0-9]+$). Invalid pattern → warn and accept. Fail keeps the field open. Use jump_if for checks regex cannot express. Keep default / prefill values pattern-safe if you use pattern.

Precedence (chrome)

Node input.style → sticky input_style → layout.text_input.style → code fallbacks (no pack file required).

At show: node input overlays layout.text_input. Packs: Text input packs.

Save

The flag is written only after a successful confirm. Sticky input_style is session chrome.

input_style

Sets or clears the session text-input pack override (same role as choice_style for choices). Does not change the mood theme.

{ "type": "input_style", "profile": "name_entry" }
{ "type": "input_style", "reset": true }

Script sugar:

[input_style name_entry]
[input_style reset]

Usually you need

FieldTypeDefaultEffect
profilestring—Use themes/<profile>.json until reset
resetbool—Clear override → fall back to layout.text_input.style

Exactly one of profile or reset should drive the change.

All fields

FieldTypeDefaultEffect
profilestring—Pack stem under themes/
resetbool—Clear sticky override
whenexpression—Skip when falsy

Precedence

  1. Node input style
  2. This command’s active override
  3. game.json → layout.text_input.style
  4. Code fallbacks (no pack file required)

Pack file keys: Text input packs.

Save / session

Sticky until reset or a new session. Registered and cleared with other cumulative chrome (Save and session).

theme_style

Applies a mood theme profile over themes/default.json, or resets to default.

{ "type": "theme_style", "profile": "flashback" }
{ "type": "theme_style", "reset": true }

Usually you need

FieldTypeDefaultEffect
profilestring—Load themes/<profile>.json over default
resetbool—Back to default.json

All fields

FieldTypeDefaultEffect
profilestring—Profile stem
resetbool—Clear override
whenexpression—Skip when falsy

Does not change choice packs — use choice_style. Styling model. Sticky; saved with chrome.

cursor

Changes the software cursor profile. Exactly one of profile / push / pop / reset.

{ "type": "cursor", "profile": "inspect" }
{ "type": "cursor", "push": "inspect" }
{ "type": "cursor", "pop": true }
{ "type": "cursor", "reset": true }

No script sugar. Hook actions: cursor:<id>, cursor:push:<id>, cursor:pop, cursor:reset.

Usually you need

FieldTypeEffect
profilestringSet sticky profile
pushstringPush profile
popboolPop
resetboolBack to game.json default

All fields

FieldTypeEffect
profilestringSet
pushstringPush
popboolPop
resetboolReset
whenexpressionSkip when falsy

Push stack is not saved. Cursor · cursors.json · game cursor.

Story detail

Fields and script sugar for moments. The whole file at a glance: story/story.json map.

PageJob
Node fieldsEvery field on one moment
ChoicesChoice menu object (items, container, style, …)
Command flowEnter / mid-line / exit command batches
Script syntaxscript.txt sugar for the same nodes
Flow tags{@…} marks and body links
ExpressionsFlag expressions (next_if, when, show_if, …)
Mode BScript + story.json overlay merge

Command object shapes inside commands arrays: Commands.

Detail pages · Adding direction · Beat as data

Node fields

Fields on one dialogue node. Script and JSON describe the same object — Story nodes.

{
  "id": "park",
  "talking": "hana",
  "text": "Hello.",
  "commands": [{ "type": "bg", "value": "street" }],
  "next": "after"
}

Typical node

Common shape authors actually use (omit unused keys):

{
  "id": "park",
  "talking": "hana",
  "label": "Hana",
  "text": "Hello.{@c1} Ready?",
  "set": { "met_hana": true },
  "commands": [
    { "type": "bg", "value": "street", "transition": "fade", "duration": 0.5 },
    { "type": "char", "name": "hana", "position": "left" }
  ],
  "flow_commands": [
    [{ "type": "sfx", "value": "click" }]
  ],
  "exit_commands": [
    { "type": "char", "name": "hana", "show": false }
  ],
  "exit_delay": 0.3,
  "next": "after",
  "choices": {
    "items": [
      { "text": "Stay", "target": "stay" },
      { "text": "Go", "target": "leave" }
    ]
  }
}
ZoneKeys
Identity / speechid, talking, label, text
Enterset, commands
Mid-lineflow_commands (+ {@cN} in text)
Leaveexit_commands, exit_delay
Branchnext, next_if, choices, input
Body linkstext_metas
Timing / panel extrasauto, panel, panel_id, … (below)

Script side-by-side: Story nodes · script.txt syntax. Batches: Command flow.

Usually you need

KeyTypeDefaultEffect
idstringauto _NNode id (start = New Game entry; omit/_N → first moment is used)
textstring—Dialogue body
talkingstring or string[]—Who is speaking this moment (one id or several)
commandsarray—Run on enter — Command flow
nextstring or nullnext in file / nullLinear target
choicesobject—Menu — Choices
inputobject—Text field — input
setobject—Flags on enter

All fields

KeyTypeDefault / omitEffect
idstringauto _NNode id; start is the default New Game entry (Scenes and jumps)
textstring—Dialogue (BBCode + {@…} + {flag})
labelstring—Speaker plate override
talkingstring or array—Speaker id(s)
talking_anchorstring—Initial focus when multi-speaker
setobject—Flags on enter; "= expr" = expression
commandsarray—Enter commands
flow_commandsarray—Batches indexed by {@cN} (1-based)
exit_commandsarray—Run on leave
nextstring or nullautoLinear target; NEXT / END / START keywords
next_ifarray { expr, then }—First truthy, else next
choicesobject—Choices
inputobject—input
autonumber ≥ 0—Auto-advance seconds ([auto N])
exit_delaynumber ≥ 00Seconds after exit_commands before next node
text_metasobject—Map [url=key] → action — Flow tags
panelstringresolve chainPreset override
panel_idstringfrom talking → "main"Target panel instance
text_mode"replace" | "append" | "plates""replace"Content mode — Appending text
append_direction"down" | "up""down"Append / plates direction
append_gap_lines / append_gap_pxnumber ≥ 00Append gaps (append_gap_lines ignored in plates)
platestring—Plate name for text_mode plates this moment — Theme: panel → Plates
hide_panelboolfalseHide the routed dialogue panel for this node (wins over routing show)
hide_continue / hide_backboolfalseHide nav chrome
driver_typingbooltrue (unless silent)Typing driver / beep
panel_shaderobject or nullabsent = clearPer-node panel shader
panel_presence_modeenum—One-cycle presence
panel_non_talking_mode"hide" | "readonly_last_text"—One-cycle
panel_readonly_style_keystring—One-cycle readonly style
labelplateobject—Same keys as labelplate command
text_refstring—Strings catalog key
text_fromstring—Copy text from another node id

Panel routing for presets / presence: Panel routing. panel_shader absent clears (unlike sticky panel / bg shaders).

Choices (story node)

A choice menu on a dialogue node. Script > lines compile into this object. Full presentation keys live here — the opening sketch is just the idea.

"choices": {
  "shuffle": false,
  "show_numbers": true,
  "style": "choice_button",
  "items": [
    {
      "text": "Take the key",
      "target": "vault",
      "set": { "has_key": true },
      "commands": [{ "type": "sfx", "value": "pickup" }]
    },
    { "text": "Leave", "target": "NEXT" }
  ]
}

Script sugar:

> Help her. -> help_branch
> Stay back. -> skip_branch
> Continue

Per-item set / commands are Mode B (JSON) only — not Mode A > sugar.

Usually you need

KeyTypeDefaultEffect
itemsarray—Required. Choice entries
stylestring or { "source" }game / choice_styleButton pack → themes/<name>.json
shuffleboolfrom layout.choices → falseDeterministic reshuffle. Forbidden when mode is free
show_numbersboolfrom layout → truePrefix N. on captions
containerobject{}Group layout / plate / host
itemobject{}Defaults merged into every item

That covers stacked button menus. Floating sprite picks and full chrome are below.

All group keys

KeyTypeDefaultEffect
itemsarray of objects—Choice entries
containerobject{}Group presentation — container
itemobject{}Defaults for each item root (layout, style, interaction, …)
stylestring or { "source" }see precedenceButton element pack name. Empty "" falls through to game
sourcestring—Pack compose → themes/<name>.json. Marks group pack-backed (sprite inheritance)
shuffleboollayout → falseDisplay shuffle. Forbidden with free
show_numbersboollayout → trueNumber labels
timerobject—Optional countdown — Timed choices

Not allowed on the group root: gap (use container.layout.gap or layout.choices.gap), top-level layout / transition / shader / audio / interaction (those live under container).

Style name precedence (button groups): node choices.style → active choice_style → layout.choices.style. See Styling model.

Timed choices

Optional choices.timer starts when the menu is shown. A player pick cancels it. On timeout, either jump elsewhere or auto-pick an item (that item’s set / commands run).

"choices": {
  "timer": { "seconds": 8, "on_end": "too_slow" },
  "items": [
    { "text": "Defuse", "target": "safe" },
    { "text": "Run", "target": "hall" }
  ]
}

Auto-pick (uses the chosen item’s body):

"timer": { "seconds": 5, "on_end": { "pick": 0 } }
KeyTypeDefaultEffect
secondsnumber > 0—Required
on_endstring or { "pick": int } or { "pick_id": string }—Timeout jump, or auto-pick by source index / item id
idstring__choice_<node_id>Timer id (saved with the session)

Mode B only — no Mode A sugar for choices.timer. Background timers use the timer command.

Container

Allowed roots: layout, style, transition, shader, audio, interaction, source.

container.layout

KeyTypeDefaultEffect
mode"stack" | "free""stack"Stack = vertical list; free = absolute item x/y
host"panel" | "overlay""panel"Sit in the panel choice slot vs full-screen overlay
render_mode"button" | "sprite""button"Group-wide. Sprite uses item style.sprite faces
anchor_h"left" | "center" | "right"overlay: "left"Overlay horizontal anchor
anchor_v"top" | "center" | "bottom"overlay: "top"Overlay vertical anchor
offset_x / offset_ynumber0Nudge the host
x / ynumber—Absolute group position when used
width / heightnumber ≥ 0—Group plate size only (not items)
min_width / min_height / max_width / max_heightnumber ≥ 0—Plate clamps
padding{ left, top, right, bottom }0Plate padding
gapnumber ≥ 0from layout.choices.gapStack item separation. Forbidden in free

Free vs stack

Rulestackfree
Item placementVBox + align_hAbsolute x / y
container.layout.gapAllowedForbidden
shuffleAllowedForbidden
Item align_hAllowedForbidden
Item x / yForbiddenAllowed

container.style

KeyTypeEffect
sourcestringPack pull for the plate
bg_colorcolor stringPlate fill
bg_texturetexture specPlate texture
border_colorcolor stringBorder colour
border_widthnumberBorder width
corner_radiusnumberCorner radius
layout / shader / interactionobjectNested same shapes as container roots

style_key is rejected — use source.

container.transition

KeyTypeEffect
kindtransition kindDefault kind
durationnumber ≥ 0Seconds
on_show / on_hidetransition kindEnter / exit
transition / transition_in / transition_outtransition kindLegacy aliases
transition_durationnumber ≥ 0Legacy duration

Kinds: instant, fade, slide_up, slide_down, slide_left, slide_right. Group-scoped only — not copied onto items. on_hide plays for host: "overlay"; a host: "panel" group leaves with the line change so the plate never keeps the old choice height. See Transitions.

container.audio

KeyTypeEffect
on_show / on_hidestring (sfx name)Group show / hide sound

container.interaction

KeyTypeEffect
focusableboolGroup focus hint
hooksobjecton_show, on_hide → action payloads
hooks_overrideobjectOverride or clear hook payloads

Not copied onto items.

container.shader

KeyTypeDefaultEffect
namestring—Shader stem
paramsobject{}Uniforms
target"bg" | "full""bg"Apply target
pass"direct" | "subviewport""subviewport"full + direct forbidden
marginnumber or sides object0Expand host

container.source

String pack ref; deep-merged under the container.

Items

Each entry in items:

KeyTypeDefaultEffect
idstring""Stable id (unique in the node); used by animate / choice_item:…
textstring—Caption. Need text or text_key
text_keystring—Catalog key (Localization)
targetstring or omitNEXTJump target — targets
setobject—Flags applied on pick before navigating (same as node set)
commandsarray—Commands run after set, before enter target. No jump / call / return / story input — use target to navigate. Meta when skips individual pick commands the same as enter batches
rulesobjectvisible + enabledVisibility
layoutobjectfrom item / gameItem layout
styleobjectfrom item / gameItem style
transitionobject—Item enter / focus / confirm FX
shaderobject—Same shader shape as container
audioobject—on_focus / on_confirm sfx names (prefer on_confirm for a one-off pick sound)
interactionobjectfocusable trueHooks, hotkey, hit_test
state_visualobject—Per-state look / animation
sourcestring—Per-item pack compose
debugany—Passthrough

On pick

  1. Menu node exit_commands (if any)
  2. Item set
  3. Item commands
  4. Enter target → that node’s set → its enter commands

Omit target (or "NEXT") to continue to the next node after the menu. For a loop that re-shows the same menu without re-typing a line, use a menu-only node (empty text + choices) and point target back at that node — not at a say+choices node.

Rules

KeyTypeEffect
show_ifexpression stringHidden when falsy
disable_ifexpression stringShown but disabled when truthy

Item layout

KeyTypeModeEffect
align_h"left" | "center" | "right"stack onlyHorizontal align
x / ynumberfree onlyAbsolute position
width / heightnumber ≥ 0bothItem size; omit in sprite for native face size
rotation_degreesnumberbothRotation

Item style

KeyTypeEffect
sourcestringElement pack pull
text_colorcolor stringCaption colour (sprite packs often transparent)
icon / hover_icon / pressed_icon / focus_iconpath stringGlyph + text; ignored when sprite theme applies
icon_max_widthnumberIcon width cap
spriteobjectOnly when group render_mode is "sprite"

style.sprite

KeyTypeEffect
normal / hover / focus / pressed / disabledtexture specState faces
scale_mode"stretch" | "fit" | "cover" | "keep"Face scaling

Item transition

KeyTypeEffect
kind / durationkind / number ≥ 0Defaults
on_show / on_focus / on_confirmtransition kindEvent kinds

Item interaction

KeyTypeDefaultEffect
hotkeystring—Custom action; not reserved (choice_1…choice_5, choice_prev / next / cycle / confirm, dialogue_advance); unique per node
focusablebooltrueFocus eligibility
hit_test"rect" | "alpha" | "polygon""rect"Hit mode; alpha / polygon need sprite render
hooksobject—on_show, on_focus, on_blur, on_confirm, on_hover_enter, on_hover_exit
hooks_overrideobject—Override or clear payloads

Item state_visual

KeyTypeEffect
normal / hover / focus / pressed / disabledentry objectPer-state look
fallback_orderstring[]Resolve order

Entry keys: source, shader (null to clear — unlike shader_params, which is kept when present), shader_params, animation or animations (not both). Animation keys: preset, property, to, from, duration (> 0), ease, trans, relative.

Targets

KeywordResolves to
NEXTNode next, else next array node
ENDStory end (null)
STARTPack entry node (start by default), else the first moment in the file

Omitted target = NEXT. Resolved at load. Avoid node ids named NEXT / END / START (keyword wins; warning). Name your opening moment :: start when New Game and START should agree — Scenes and jumps.

Sprite vs button inheritance

ConditionInherits from layout.choices
render_mode: "sprite" without choices.sourceNo button chrome — author container + item style.sprite on the node
Sprite with choices.sourcePack supplies chrome; pack item is the item base
Eithershuffle / show_numbers / gap fallbacks still apply if unset on the node

Example pack: examples/example-05-ui-and-polish/themes/choice_yes_no.json (free + sprite + overlay).

Command flow

When enter, mid-line, and exit command batches run on a dialogue node. Shared idea: Command flow (concept).

{
  "id": "park",
  "text": "Hello.{@c1} Nice day.",
  "commands": [{ "type": "bg", "value": "street" }],
  "flow_commands": [[{ "type": "sfx", "value": "click" }]],
  "exit_commands": [{ "type": "char", "name": "hana", "show": false }],
  "exit_delay": 0.5,
  "next": "after"
}

Usually you need

SurfaceTypeEffect
commandsarray of commandsRun on enter (after set)
flow_commandsarray of batches{@cN} runs batch N-1 while typing
exit_commandsarray of commandsRun on leave (advance or choice)
exit_delaynumber ≥ 0Seconds after exit_commands, before next node
{@cN}text tagTrigger flow batch (1-based)

Lifecycle order

StepWhat runs
1Node set flags
2Enter commands (top → bottom)
3Line display / typing
4Each {@cN} → that flow_commands batch (typing does not pause)
5Continue / auto-advance / choice pick
6exit_commands
7exit_delay wait (if > 0)
8Enter next node (next, choice target, or end)

Leave also runs when the player picks a choice (exit, then the branch node).

Batch fields

KeyTypeDefaultEffect
commandsarray—Enter batch; omitted = no enter commands
flow_commandsarray—Each element is a command array (a batch)
exit_commandsarray—Leave batch
exit_delaynumber ≥ 00Wait after exit batch before advancing
setobject—Flags applied on enter, before commands

Related node timing (not command batches): auto / [auto N] — Node fields, Typing.

flow_commands shape

RuleDetail
Outer arraySlots indexed by {@cN}
Each slotA command array (may hold several commands)
{@c1}flow_commands[0]
Missing slotWarning; no run
Script sugarMid-line […] appends a one-command batch and inserts {@cN}
"flow_commands": [
  [{ "type": "sfx", "value": "click" }],
  [
    { "type": "char", "name": "hana", "expression": "happy" },
    { "type": "sfx", "value": "whoosh" }
  ]
]

Text: "A.{@c1} B.{@c2} C."

Script forms

ConstructMaps to
[bg …] before spoken textEnter commands
[…] after text has startedNew flow_commands slot + {@cN} in assembled text
[exit: …]Append to exit_commands
[exit_delay N]exit_delay
[flag = …]set (not a command)
-> targetnext (not a command)

Full script map: script.txt syntax.

jump and batches

RuleDetail
WhenAfter the current batch finishes
WinnerFirst jump / effective jump_if in that batch
Enter jumpLeaves before the rest of the line plays
Flow jumpStops further typing on that line; transfers immediately
Exit jumpSkips normal advance / choice target after the exit batch
Linear nextNot a command — use only outside batches

jump · jump_if · call · return.

Command meta when

FieldTypeEffect
whenexpressionSkip that command if falsy / eval error; rest of batch continues

JSON / Mode B overlay only — no [when …] in script.txt. Same expression language as next_if / expressions.

vs other conditionals

ToolJob
Choice rules.show_if / disable_ifMenu option visibility / enable
next_if / jump_ifWhere the story goes
Command whenRun or skip that side effect

Cases

Conditional chrome (mutually exclusive packs):

{ "type": "choice_style", "profile": "choice_plain", "when": "chose_skin == \"plain\"" }
{ "type": "choice_style", "profile": "choice_button", "when": "chose_skin == \"textured\"" }

Conditional SFX / character / flags:

{ "type": "sfx", "value": "alarm", "when": "bomb_armed" }
{ "type": "char", "name": "rival", "show": true, "when": "rival_present" }
{ "type": "set_flag", "values": { "hint": true }, "when": "score < 3" }

Stop a timer only if it was armed (exit_commands or enter batch):

{ "type": "timer", "id": "bomb", "action": "stop", "when": "bomb_armed" }

Pick-item commands use the same meta (after item set, before target):

{
  "text": "Take",
  "target": "NEXT",
  "commands": [
    { "type": "sfx", "value": "pickup", "when": "!quiet_mode" }
  ]
}

Mode B

Overlay commands are appended after script enter commands. Other batch keys follow normal overlay replace rules. Mode B.

Save note

During exit_delay (after exit commands, before the next node), a save uses the pre-exit snapshot so reload does not land mid-teardown. Save and session.

script.txt syntax

Mode A stories are plain text under story/script.txt. Blank lines separate nodes. The parser builds the same objects as JSON — script is sugar, not a second runtime. Story nodes.

:: park
[bg street fade 0.5]
HANA [happy]:
Hello.
> Help. -> help
> Leave. -> END

→ roughly:

{
  "id": "park",
  "talking": "hana",
  "text": "Hello.",
  "commands": [
    { "type": "bg", "value": "street", "transition": "fade", "duration": 0.5 },
    { "type": "char", "name": "hana", "expression": "happy" }
  ],
  "choices": {
    "items": [
      { "text": "Help.", "target": "help" },
      { "text": "Leave.", "target": "END" }
    ]
  }
}

Usually you need

ConstructFormEffect
Label:: idNode id (:: start = New Game entry; omit → first moment)
SpeakerNAME: / NAME [expr]:Talking + optional enter char
Textfollowing linesDialogue (joined with {@w})
Choice> text / > text -> targetChoice item; bare > → NEXT
Jump-> target / -> ENDNode next
Flag[flag = …] / += / -=Node set
Command[bg …], [hana left], …Enter commands

Full construct list

ConstructFormEffect
Blank lineempty lineEnds the current node
Comment# …Ignored
Label:: idSets node id
SpeakerNAME:talking = character id (folder name)
ExpressionNAME [expr]:Also queues enter char with that expression
Custom labelLabel|NAME:label + talking
JointNAME & NAME: / NAME [expr] & NAME (silent):Multi-speaker talking; per-segment expr / silent
SilentNAME (silent):That speaker’s mouth drive off; on joints, only names marked (silent)
Textnon-special linesJoined; speaker change inserts {@talk:id}
Mid-line cmds[…] after text startedflow_commands + {@cN} markers
Choice> text / > text -> targetchoices.items[]
Text input[input flag …]input on the node
Jump-> targetnext
End-> ENDnext: null
Flag[name = lit|expr] / += / -=set
Branch[if expr -> target]next_if[]
Auto-advance[auto N]auto: N
Panel sugar[panel …], [panel_id …], [hide_panel], …Node fields or panel commands
Exit[exit: …], [exit_delay N]exit_commands / exit_delay
Locale helpers[text_ref key], [text_from id]Those node fields
Commands[keyword …] / [charname …]Enter commands (or flow if after text)

Keyword commands: bg, bgm, sfx, panel, typing_sfx, jump, jump_if, call, return, set_flag, char_layout, char_pack, choice_style, input_style, screen, plus bare character folder names → char. ([input …] writes the node’s input field, like > writes choices.)

A line that is a whole BBCode wrap ([b]…[/b], [right]…[/right], [color=…]…[/color], …) stays as text, not a command.

Line breaks inside text: a new line in the file is a pause (the player advances). To break the line on screen without a pause, write \n in the text — [right]You\nOn my way.[/right]. Write \\n for a literal \n.

Flow tags and markup

Inline markers in dialogue text. {@…} tags are processed by the typewriter; BBCode stays for rich text.

Slow.{@cps=5} Fast.{@cps}
Wait…{@w=1.0} Go.
Page break.{@p}
Fine.{@c1} Then not.

Usually you need

TagEffect
{@w}Soft pause (skipped in fast / hold)
{@w=N}Timed pause N seconds
{@cps=N} / {@cps}Mid-line speed / reset
{@cN}Run flow batch N while typing (1-based)
{flag}Print a flag value

All {@…} tags

TagEffect
{@w}Pause; click resumes; skipped in fast/hold
{@w=N}Timed pause N seconds
{@p}Pause + clear page on resume (not skipped by fast)
{@cN}Run flow_commands[N-1] when typing reaches this mark (1-based; does not pause). Script mid-line […] inserts these for you — Command flow, Pacing
{@cps}Reset CPS to theme / typing default
{@cps=N}Override characters per second
{@talk:id}Switch talking / focus (parser also inserts on speaker change)
{@phN}Mode B: phrase from script line N (1-based)
{@auto}Labelplate speaker expansion

Flag interpolate: {flag}. Escape braces with {{ / }}.

Typing defaults: Typing · typing.

BBCode

Known tags are markup (skipped by the typing clock). Common ones: b, i, u, s, color, url, code, center, right, left, img, font, font_size, cell, table, ol, ul, p, br, indent, bgcolor, fgcolor, outline_color, outline_size, opentype_features.

Unknown [word] (for example [alt]) stays literal on the panel so typing stays in sync — it is not treated as a command or as hidden markup.

To show real square brackets (or to print a tag name literally), use [lb] / [rb] → [ / ]. Example: [lb]b[rb]bold[lb]/b[rb] displays [b]bold[/b].

Clickable [url] (body metas)

Authors write [url=META]label[/url] in dialogue text. Clicks run only on the primary panel, after the line is waiting (typing finished or paused on {@w} / {@p}), and only when the press was a tap (not a scroll drag). Wheel and drag still scroll; empty body taps still Continue.

Map keys with node text_metas (Mode B / JSON; Mode A overlay on a script node):

{
  "id": "inspect_desk",
  "text": "See the [url=codex]Codex[/url].",
  "text_metas": {
    "codex": "url:https://example.com/codex",
    "door": { "type": "jump", "to": "shed" }
  }
}
ResolutionResult
text_metas has key METARun that payload
Else META is http:// or https://Open as url:META
ElseIgnore (warning once per key)

Allowed map values: url:… strings, or a single { "type": "jump", "to": "…" } / { "type": "set_flag", "values": { … } } object. No command arrays.

PayloadAfter a successful click
url:… / bare http(s)Stay waiting — Continue still needed
{ "type": "set_flag", … }Stay waiting
{ "type": "jump", … } (or a screen push that leaves the beat)Consume wait — no second body tap

[hint] stays visual-only (no click wiring). Append history / readonly panels do not fire metas.

Expressions

Flag expressions gate jumps, choice visibility, and command when. Evaluated with flag names as identifiers. Use &&, ||, ! — not and / or / not.

{ "type": "jump_if", "cases": [{ "expr": "score > 5", "to": "good" }], "else": "bad" }
[if helped -> thank_you]
> Secret. -> vault

(with item rules.show_if: "has_key")

Usually you need

SurfaceShape
Choice visibilityrules.show_if / rules.disable_if
Branchnext_if[] or [if expr -> target]
Command skip"when": "expr" on any command (JSON / Mode B) — Command flow
Flag math"score": "= score + 1" in set / set_flag

Where expressions appear

SurfaceShape
set / set_flag.valuesLiteral, or string "= score + 1"
next_if[]{ "expr", "then" } — first truthy wins, else next
jump_if.cases[]{ "expr", "to" } + optional else
Choice rules.show_if / disable_ifexpr string
Command whenexpr string; no script sugar
Screen / block show_ifsame grammar

Truthiness

ValueResult
nullfalse
numberfalse only if 0
string "false" / "0" / ""false
string "true" / "1" / other non-emptytrue

Mode B overlay

Mode B = story/script.txt plus story/story.json. Script is primary; JSON overlays complex nodes. Mode C is JSON only (no script).

// story/story.json
[
  {
    "id": "park",
    "commands": [
      { "type": "sprite", "id": "chair", "value": "chair", "layer": "bg_front" }
    ]
  }
]

Usually you need

RuleBehavior
MatchOverlay id (or _ref) → script node id
Most keysOverlay replaces the script value
commandsOverlay commands are appended after script commands
Absent keysScript values kept

_ref is an alternative to id in an overlay entry when you prefer not to repeat the node id; it matches a script node the same way.

Merge rules

RuleBehavior
MatchBy id or _ref
OverrideAll overlay keys replace script values
Exceptioncommands: appended to script commands
text + {@phN}Substitute from script line N before erase
Absent keysScript kept (talking, etc.)
Localescript_<locale>.txt / story_<locale>.json

Modes

ModeFiles
Ascript.txt only
Bscript.txt + story.json overlay
Cstory.json only

Screens detail

Layers, content blocks, preference binds, and button actions. The whole file at a glance: screens.json map.

PageJob
Screen objectOne screen id: role, transitions, layers
Layer: backgroundFull-screen plate
Layer: menuButton column
Layer: contentCard of blocks
Layer: spritesUI pictures
Layer: chromeExtra controls (e.g. Close)
Content blocksheading, text, bind rows, history, slots, …
Widget bindingsPreference bind ids
Actions and hookson_click verbs and payloads

Guide: Title, pause and settings screens · Buttons and actions.

Detail pages · screens.json map

Screen object

One entry in screens.json (plus optional top-level defaults).

{
  "title": {
    "role": "modal",
    "layers": [
      { "type": "background", "style": { "bg_color": "#101018" } },
      { "type": "menu", "buttons": [{ "label": "Start", "interaction": { "hooks": { "on_click": "start" } } }] }
    ]
  }
}

Usually you need

KeyTypeDefaultEffect
layersarray—Required. Ordered layers
role"modal" | "hud" | "scene""modal"Plane / input
defaults (file root)object—Shared transition / etc.

All keys

KeyTypeDefaultEffect
layersarray—Layers
roleenum"modal"Plane policy
shadershader object—Screen shader
transition / transition_in / transition_out / transition_duration / duration—instant / 0Enter/exit
pauses_treebool—Pause game tree
action_contextstring—Hook allowlist context
interactionobject—Screen-level hooks
modulate / scalehex / number or array—Tint / scale
plane / persistent / input_passthroughadvanced—Plane overrides

Layers: background · sprites · content · menu · chrome. Guide: Screens. Story control: screen.

Layer: background

Full-bleed plate behind other screen layers.

{
  "type": "background",
  "style": { "bg_color": "#101018" },
  "interaction": { "hooks": { "on_click": "close" } }
}

Usually you need

KeyTypeEffect
type"background"Required
style.bg_color / texture—Plate look
interaction.hooks.on_clickactione.g. "close" on dimmed overlays

All keys

KeyTypeEffect
type"background"Required
ordernumberZ / locale merge
layout / styleobjectMay include source
modulate / scale—Tint / scale
interactionobjecthooks (typically on_click)

Screen object · Actions and hooks · source.

Layer: menu

Vertical column of buttons (title, pause, …).

{
  "type": "menu",
  "layout": { "host": "screen" },
  "buttons": [
    { "label": "New game", "interaction": { "hooks": { "on_click": "new_game" } } },
    { "label": "Quit", "interaction": { "hooks": { "on_click": "quit" } } }
  ]
}

Usually you need

KeyTypeEffect
type"menu"Required
buttonsarrayMenu entries
layout.host"screen" | "panel"Where the column sits

All keys

KeyTypeEffect
type"menu"Required
ordernumberZ
layoutobjecthost, spacing via theme
style / sourcepackButton chrome
buttonsarraySee below
modulate / scale—Layer tint

Button keys

KeyTypeEffect
idstringLocale merge / identity
label / label_keystringCaption
icons / icon fields—Glyphs
interactionobjecthooks.on_click …
state_visualobjectPer-state
show_ifexpressionVisibility
style / sourcepackElement pack

Default chrome: buttons. Actions and hooks.

Layer: content

Card of content blocks (settings, credits, …).

{
  "type": "content",
  "layout": { "host": "screen" },
  "blocks": [
    { "type": "heading", "text": "Settings" },
    { "bind": "master", "label": "Master" },
    { "bind": "fullscreen", "label": "Fullscreen" }
  ]
}

Usually you need

KeyTypeEffect
type"content"Required
blocksarrayContent blocks

All keys

KeyTypeEffect
type"content"Required
ordernumberZ
layout / styleobjectCard chrome (source ok). With layout.mode: "scroll", players can scroll with wheel, keys, or touch drag
modulate / scale—Tint / scale
blocksarrayNested blocks

Card look: Theme: card. Screen object.

Layer: sprites

Decorative or interactive sprites on a screen.

{
  "type": "sprites",
  "items": [
    {
      "id": "logo",
      "kind": "image",
      "image": "sprites/logo.png",
      "interaction": {
        "hit_test": "rect",
        "hooks": { "on_click": "url:https://example.com" }
      }
    }
  ]
}

Usually you need

KeyTypeEffect
type"sprites"Required
itemsarraySprite entries

Layer keys

KeyTypeEffect
type"sprites"Required
ordernumberZ
layout / style / modulate / scale—Layer chrome

Item keys

KeyTypeDefaultEffect
idstring—Locale merge key
kind"image" | "rect" | "polygon""image"Shape
image / pointspath / [[x,y],…]—Kind-specific
interactionobject—hooks, focusable, focus_order, hit_test, hooks_override
state_visualobject—Per-state look
modulate / scale / show_if—Tint, scale, condition

Root on_click etc. are invalid — use interaction.hooks.*. hit_test: rect (default), alpha (image), polygon (polygon only).

Actions and hooks · state_visual.

Layer: chrome

Floating chrome control — usually a close button on a card corner.

{
  "type": "chrome",
  "id": "close",
  "layout": { "host": "content", "anchor_h": "right", "anchor_v": "top" },
  "button_style": "icon_close_button",
  "interaction": { "hooks": { "on_click": "close" } }
}

Usually you need

KeyTypeEffect
type"chrome"Required
interaction.hooks.on_clickactionOften "close"
button_stylestringPack id; empty → info_close_button

All keys

KeyTypeEffect
type"chrome"Required
idstringIdentity
label / label_keystringCaption (optional for icon-only)
button_stylestringNamed button pack
icons / icon fields—Glyphs
layoutobjecthost: "screen" (window, default — stays fixed while content scrolls) | "content" (overlay first content card; scrolls with layout.mode: scroll) ; anchors / offsets
interactionobjectHooks
state_visualobjectPer-state
order / style / modulate / scale—Layer fields

Packs: Named button packs. Actions and hooks · Layout and host.

Content blocks

Blocks inside a content layer (or nested scroll).

"blocks": [
  { "type": "heading", "text": "Audio" },
  { "bind": "bgm", "label": "Music", "label_key": "screen.settings.bgm_volume", "min": 0, "max": 1, "step": 0.05 },
  { "bind": "locale", "label": "Language", "label_key": "screen.settings.language", "style": "locale_button", "active_style": "locale_button_active" },
  { "type": "separator" },
  {
    "type": "table",
    "columns": [30, 70],
    "rows": [
      [
        { "type": "text", "text": "Go" },
        { "type": "button_row", "buttons": [{ "label": "A", "interaction": { "hooks": { "on_click": "close" } } }] }
      ]
    ]
  }
]

Usually you need

Type / shapeRole
heading / textCopy
bindPreference row — Widget bindings
slider / toggleFree controls with hooks (not prefs)
button / button_rowActions
tableMulti-row percent columns
historyBacklog log
save_slotsManual save/load slot grid
separatorRule — separator

Common fields

KeyTypeEffect
typestringBlock kind (required unless bind is set)
bindstringPreference id — no type when set
idstringLocale merge / identity
stylestring packElement pack sugar
sourcestringPack compose
show_ifexpressionVisibility
interactionobjectHooks
state_visualobjectPer-state look

Types

bind (preference)

KeyTypeEffect
bindstringKnown preference id (master, locale, …)
label / label_keystringWhen set → settings row (label | control); omit → full-width control. Labels accept BBCode ([b], [color=…], …)
format / min / max / step / suffix—Slider display (when kind is slider)
style / active_stylepack idsLocale chips

Do not set type on bind blocks; only listed keys are allowed.

KeyTypeEffect
text / text_keystringCopy. text blocks accept BBCode; heading is plain

separator / spacer

Theme separator supplies colour/height for separator. spacer is empty space (optional size fields via style/layout).

image

KeyTypeEffect
image path / texture fields—Displayed image
alignstringleft | center | right

button

KeyTypeEffect
label / label_keystringCaption
icon fieldspathGlyphs
interaction.hooksobjectTypically on_click

button_row

KeyTypeEffect
buttonsarrayExplicit row buttons (required)
stylepack idDefault button pack
align_hstringleft | center | right (default center)

slider / toggle (free UI)

KeyTypeEffect
label / label_keystringWhen set → settings-style row; omit → full width. Labels accept BBCode
interaction.hooksobjectRequire on_change and/or on_commit
min / max / step / format fields—Slider only

Only listed keys are allowed — use bind for preferences.

table

KeyTypeDefaultEffect
columnsnumber[2..6]—Percent weights; must sum to 100
rowsarray of cell arrays—Each row length = columns.length
align_hstring or arrayleftCell default; array = per column
align_vstringcentertop | center | bottom
spacing / row_gapint—Gaps

Cells are normal blocks (text, bind unlabeled, free controls, …). No nested table or scroll. Bind/slider/toggle inside cells must omit label/label_key (put copy in an adjacent cell). Empty {} or null = spacer. Cell blocks may override align_h / align_v.

history

KeyTypeEffect
max_itemsintCap
show_speaker / speaker_on_own_line / reverse_orderboolLayout
empty_text / empty_text_keystringEmpty state
colours / sizes / item_separator—Look

No interaction.hooks / state_visual on history.

save_slots

Early — not final UX. The slot grid works; layout, labels, and comfort (filters, previews, per-story views) are not polished yet.

Slot grid for opt-in save / load screens. Clicks emit save_slot:N or load_slot:N.

KeyTypeDefaultEffect
mode"save" | "load""save"Save writes a slot; load opens occupied compatible slots
columnsint3Buttons per row (1–6)
stylestring"menu_button"Button theme prefix
empty_label / empty_label_keystringSlot {n} — EmptyEmpty cell; {n} = slot number (literal token, not a story flag)
occupied_label / occupied_label_keystringSlot {n} — {node}Occupied; {n}, {node}, and (multi-story) {story} / {story_title}
spacing / row_gapint10Gaps

Slot count comes from session.save_slots (see Save and session). Empty load cells are disabled. No built-in save / load screens ship — build them yourself with this block, then point menus at screen:save / screen:load (immediate "save" / "load" stay slot 1).

scroll

KeyTypeEffect
blocksarrayNested blocks (no nested scroll)
height or height_percentnumberWindow height
min_height / max_height / separationnumberClamps / gap

Widget bindings · Actions and hooks · Named button packs · Theme: card · settings_layout.

Widget bindings

bind on a content block maps to a registered player preference. Control kind comes from the engine registry — do not set type on bind blocks.

{ "bind": "master", "label": "Master", "label_key": "screen.settings.master_volume", "min": 0, "max": 1, "step": 0.05 }
{ "bind": "typing_enabled", "label": "Typing sound", "label_key": "screen.settings.typing_sound" }
{ "bind": "locale", "label": "Language", "label_key": "screen.settings.language", "style": "locale_button", "active_style": "locale_button_active" }

Omit label / label_key for a full-width control. With a label, the row uses settings_layout (label | control [| value]). Labels accept BBCode ([b], [color=…], …).

Usually you need

BindControlEffect
master / bgm / sfxsliderVolumes
fullscreentoggleDisplay mode
typing_enabledtoggleTyping beep
localelocale chipsLanguage from i18n.locales

All bindings

BindControlEffect
mastersliderMaster volume
bgmsliderMusic volume
sfxsliderSFX volume
typing_enabledtoggleTyping beep on/off
fullscreentoggleFullscreen
typing_speed_scaleslidertyping.speed_scale
auto_advance_defaultsliderAuto-advance default
font_size_scaleslidertypography.size_scale (+ commit)
localelocale chipsset_locale:<id> from game.i18n.locales

Free slider / toggle blocks (with type and hooks, no bind) are for custom UI — see Content blocks.

Slider/toggle chrome: slider · toggle. Layout: settings_layout. Content blocks · Typing.

Actions and hooks

Interactive screen surfaces fire interaction.hooks with action payloads.

"interaction": {
  "hooks": {
    "on_click": "close",
    "on_hover_enter": { "type": "ui_sfx", "name": "hover" }
  }
}

Usually you need

PayloadExample
Verb string"close", "new_game", "screen:settings"
Command object{ "type": "set_flag", "values": { "seen": true } }
Array[ "close", { "type": "app_action", "name": "resume" } ]

Payload shapes

FormExamples
String verb"close", "start", "continue", "save", "load", "save_slot:1", "load_slot:1", "new_game", "new_game:ep2" (early multi-story), "quit", "title", "screen:save", "screen:load", "screen:pause", "cursor:push:inspect", "url:https://…"
Command objectset_flag, app_action, ui_sfx, ui_transition, jump, ui_advance, cursor, ui_animate
ArrayMix of the above

Events by surface

SurfaceEvents
Backgroundon_click
Buttons / chromeon_click, on_focus, on_blur, on_hover_enter / exit, on_show, on_hide
Slider / toggleon_change, on_commit, focus/hover/show/hide
Spritesclick / hover / focus / show / hide

hooks_override with null removes an inherited event. Shape: Interaction hooks. Concept: Buttons and actions.

Theme detail

Deep tables for packs and chrome that outgrow the overview map. Whole themes/default.json: themes map.

PageJob
typographyFonts and sizes
colorsFallback text / stage colours
panelDialogue-box presets
labelplateName-plate styles
buttonsRoot menu / screen button chrome
Named button packsExtra root packs / themes/<name>.json
separatorHorizontal rule
slider / toggleSettings controls
scrollbarScroll bars
notificationToast plate
cardContent card chrome
settings_layoutLabeled settings row columns
readonly_stylesQuiet / non-talking panel looks
Choice packsChoice menu look files
Text input packsText-field chrome files

Guide: Theme and the dialogue box · Choice and input looks.

Detail pages · themes map

Theme: typography

"typography": {
  "font": "Jupiteroid",
  "font_menu": null,
  "font_dialogue": null,
  "font_label": "Jupiteroid",
  "size_scale": 1.0,
  "sizes": {
    "speaker": 45,
    "dialogue": 30,
    "choice": 30,
    "button": 30,
    "notification": 30
  }
}

Usually you need

KeyTypeDefaultEffect
fontstring or null—Base font under fonts/
sizes.dialogueint—Body size
size_scalenumber1.0Scales all sizes.* (player settings too)

All keys

KeyTypeDefaultEffect
fontstring or null—Base; variants -Regular / -Bold / …
font_menustring or nullnull → fontMenus / settings buttons
font_dialoguestring or nullnull → fontDialogue + choices
font_labelstring or null—Speaker nameplate
size_scalenumber1.0Global multiplier
sizes.speakerint—Nameplate size
sizes.dialogueint—Body
sizes.choiceint—Choice captions
sizes.buttonint—Screen/menu buttons
sizes.notificationint—Toast text

A panel preset may overlay a subset of these keys (font, font_dialogue, font_label, sizes.speaker / dialogue / choice) — see Panel → Typography / colors per preset. size_scale, font_menu, and menu/notification sizes stay project-global.

Colors · Panel · Theme and the dialogue box · themes map.

Theme: colors

Fallback colours when a control does not set its own.

"colors": {
  "background_default": "#dfdcd7",
  "speaker_text": "#c4693a",
  "dialogue_text": "#e8e5e0",
  "choice_text": "#c5cad5",
  "notification_text": "#dfdcd7"
}

Usually you need

KeyTypeDefault (code)Effect
dialogue_textcolor string#ffffffBody text
speaker_textcolor string#b3bfe6Nameplate
background_defaultcolor string#1e2347Empty-stage fill

All keys

KeyTypeShipped / code fallbackEffect
background_defaultcolor#dfdcd7 / #1e2347Fill when no background image
speaker_textcolor#c4693a / #b3bfe6Nameplate
dialogue_textcolor#e8e5e0 / #ffffffDialogue body
choice_textcolor#c5cad5 / #c8d0ffChoice caption pool default
notification_textcolor#dfdcd7 / #e6f0ffToast text (+ some heading fallbacks)

A panel preset may overlay dialogue_text and speaker_text only — see Panel → Typography / colors per preset. Choice caption colours stay on choice packs; notification_text stays project-global.

Typography · Panel · Theme and the dialogue box · themes map.

Theme: panel

panel in themes/default.json holds presets — named looks for the dialogue panel. A story picks a preset per node or per character; the theme decides what each one looks like.

{
  "panel": {
    "default": {
      "width": "100%",
      "anchor_v": "bottom",
      "min_height": 200,
      "padding": { "top": 20, "left": 24, "right": 24, "bottom": 24 },
      "bg": "#1e2133e0",
      "corner_radius": 12
    },
    "whisper": {
      "extends": "default",
      "width": "60%",
      "anchor_h": "center",
      "bg": "#00000099"
    }
  }
}

Preset names are yours. default is the only one the engine falls back to.

Usually you need

KeyTypeDefaultEffect
width"NN%", "100%", "stretch", or a number"100%"Panel width
anchor_h / anchor_vsee size and position"stretch" / "bottom"Where the panel sits
offset_x / offset_ynumber0Nudge from the anchors (negative to move inward from right/bottom)
min_heightnumber, "NN%", or "full"160Floor for height
max_heightnumber, "NN%", or "full"legacy formulaCeiling; overflow scrolls the body text
paddingobjectsee belowInner margins
bgHTML color#343a52e0Flat plate colour
corner_radiusint0Rounded corners on the flat plate
extendsstring—Inherit another preset, then override
scrollbar_stylestring—Optional element pack for the body scrollbar (same overlay as screen scroll "style")
scrollbar_layout"default" | "reserve" | "overlay""default"How the body scrollbar column is laid out — see Body scroll
scroll_pin_durationnumber ≥ 00Seconds to ease the body to the new append edge after settle; 0 snaps
append_push_durationnumber ≥ 00Seconds to ease lines when an append beat adds text. append_direction: "up": earlier lines slide down as the new top line comes in. "down": the body eases to the newest line when it scrolls. 0 snaps
typingobjectinherit game.jsonPer-preset text reveal / caret / beep — Typing per preset
typographyobjectinherit project typographyPer-preset font/sizes for body, labelplate, panel-hosted choices/input — Typography / colors per preset
colorsobjectinherit project colorsPer-preset body / speaker colours — Typography / colors per preset

Height grows with text and choices up to max_height; past that the body scrolls. Floor + ceiling are enough for a first theme. Everything below is the full surface.

Size and position

KeyTypeDefaultEffect
width"NN%", "100%", "stretch", or a number"100%"Panel width. "stretch" and "100%" fill the window, minus offset_x on both sides when anchor_h is "stretch"
min_heightnumber, "NN%", or "full"160Floor for the panel height
max_heightnumber, "NN%", or "full"see belowCeiling for the panel height
anchor_h"left" | "center" | "right" | "stretch""stretch"Horizontal anchor
anchor_v"top" | "center" | "bottom" | "stretch""bottom"Vertical anchor
offset_xnumber0Pixels from the horizontal anchor
offset_ynumber0Pixels from the vertical anchor
xnumber—Absolute left edge; ignores anchor_h and offset_x
ynumber—Absolute top edge; ignores anchor_v and offset_y
resize_durationnumber0Seconds to ease plate height when the same preset gains/loses content (new line or choices). Preset or panel switches snap. Typing growth snaps each frame. Not show/hide — that is transition_*. 0 snaps
scroll_pin_durationnumber ≥ 00Seconds to ease body scroll to the append edge after the plate height is stable. 0 snaps
append_push_durationnumber ≥ 00Seconds to ease lines when an append beat adds text. append_direction: "up": earlier lines slide down as the new top line comes in. "down": the body eases to the newest line when it scrolls. 0 snaps
scrollbar_stylestring—Element style pack name for the body scrollbar; omit to use global scrollbar.*
scrollbar_layout"default" | "reserve" | "overlay""default"Body scrollbar column layout (see Body scroll)
typographyobject—Overlay onto project typography for this preset (see below)
colorsobject—Overlay onto project colors for body / speaker only (see below)
modulateHTML color#ffffffTint for the whole panel
scalenumber or [sx, sy]1Scale for the whole panel

Offsets are measured from the anchored edge, so a right- or bottom-anchored panel needs a negative offset to move inward:

{ "anchor_h": "right", "offset_x": -20, "anchor_v": "bottom", "offset_y": -20 }

Height units and ceiling

min_height and max_height accept a pixel number, a percent of the window height ("40%"), or "full" (window height). Unset min_height uses 160. Unset max_height uses min(65% of the window, max(5 × min_height, 320)). If min ends up larger than max, max is raised to match (and a warning is logged).

A sticky { "type": "panel", "max_height": … } overrides the theme preset until reset — panel command.

Body scroll

Dialogue text lives in a body scroll area. When content would push the plate past max_height, the plate stops growing and the text scrolls. Name plate, continue/back with placement: "panel", and choices stay outside that scroll. Continue/back with placement: "text_end" (and the typing caret) hide while the body is scrolled away from the newest line, and return when pinned back to that edge.

Panel-hosted choices and text input (host: "panel") share the plate and shrink the body; the body stays on the newest line (the top line for append_direction: "up") until the player scrolls away. Overlay-hosted choices sit on the screen and do not take body space. There is no scroll on/off switch — the ceiling alone decides.

The body scrollbar uses global theme scrollbar.* unless the preset sets scrollbar_style to an element pack (same overlay path as screen scroll blocks — Theme: scrollbar).

scrollbar_layout controls the body scrollbar column:

ValueEffect
default (omit)Bar appears when needed and takes width from the text when it does
reserveAlways keep a scrollbar column (bar width + scrollbar.content_separation gap) so wrap width stays stable when the track appears; the plate keeps its preset width and the text column is narrower by that column. The track stays hidden until there is something to scroll
overlayNot available yet — falls back to default

Use reserve on plates that often scroll (phone, log, fixed height). Leave normal ADV presets on default.

Spacing

KeyTypeDefaultEffect
padding.topint15Inner top margin
padding.leftint20Inner left margin
padding.rightint20Inner right margin
padding.bottomint20Inner bottom margin
speaker_gapint6Gap between the speaker name and the text
choices_gapint2Gap between the text block and the choice list
choices_separationint4Gap between individual choices

Background

KeyTypeDefaultEffect
bgHTML color#343a52e0Flat plate colour, used when no usable texture is set
bg_texturetexture spec—Nine-slice plate image. Takes priority over bg
bg_9slice[left, top, right, bottom][0, 0, 0, 0]Stretch margins for bg_texture
corner_radiusint0Rounded corners on the flat plate. Has no effect once a texture is used

A texture spec is either "panels/plate.png", { "file": "…", "modulate": "#hex" }, or a flipbook { "frames": [...], "fps": 10 } — see Pictures (texture specs).

Speaker nameplate

KeyTypeDefaultEffect
labelplate_style_keystring—Which labelplate.<key> block styles the speaker name
labelplate_layoutobject—Present means the nameplate is docked to the panel edge instead of sitting inline above the text

labelplate_layout takes anchor_h (left, center, right; default left), anchor_v (top, center, bottom; default top), offset_x, and offset_y. Offsets apply only to the anchored side, so a plate hanging above a bottom panel uses anchor_v: "top" with a negative offset_y.

The mode is resolved per node: an explicit labelplate command mode wins, otherwise the preset is docked if labelplate_layout exists and inline if it does not. Asking for docked on a preset with no labelplate_layout falls back to inline and warns.

Panel slots

slots pins extra art to the panel — most often a portrait of whoever is speaking.

{
  "slots": [
    { "id": "speaker_art", "bind": "talking", "layout": { "anchor_h": "left" } },
    { "id": "corner", "object": "panel_corner_deco" }
  ]
}
KeyTypeEffect
idstringRequired, unique within the preset
bind"talking"Fills the slot with the current speaker’s panel art. "talking" is the only supported binding
objectstringFills the slot with an object from sprites/<id>.obj.json
layoutobjectAnchors within the panel

Each slot needs exactly one of bind or object. Which art a character supplies for a bound slot comes from ui.panel_slots in their pack.

Continue and back buttons

Each preset carries its own navigation buttons under continue_button and back_button. Both take the same keys.

KeyTypeDefaultEffect
visiblebooltrue for continue, false for backWhether the button is shown
labelstring—Caption. An empty string gives an icon-only button
label_keystring—Localisation key, used instead of label when set
align"left" | "center" | "right"—Corner placement inside the panel
placement"panel" | "text_end""panel""panel" parks it in a corner; "text_end" follows the last typed character
offset_xnumber0Nudge from the resolved position
offset_ynumber0Nudge from the resolved position
stylestring or objectfallback packElement style pack name, or an inline style object with an optional "source"
click_sfx / hover_sfxstring or nullinherit sounds.button_*Per-channel UI SFX override. Omit = inherit; "" or null = silent
interactionobject—hooks and hooks_override for on_show, on_hide, on_hover_enter, on_hover_exit, on_focus, on_blur

click_sfx / hover_sfx may also sit on the nested style object or pack. Button cfg keys win over style, then sounds.button_click / sounds.button_hover. If on_hover_enter already runs ui_sfx, inherited hover SFX is skipped for that button.

Any other key produces an “unknown panel button field” warning at load. There is no on_click hook: advancing and going back are wired to the buttons already.

When no style is given, the buttons fall back to the panel_continue_button_default and panel_back_button_default packs. They deliberately do not inherit the global buttons block, so a wide menu button style will not stretch your continue arrow.

Both were top-level theme keys in earlier versions. continue_button and back_button at the root of the theme file now warn and do nothing — move them inside a preset.

Inheritance with extends

extends names another preset to build on. Resolution is recursive, so a preset can extend a preset that extends another.

Merging is deep: nested objects such as continue_button or padding merge field by field rather than replacing wholesale. Setting a key to null deletes it from the inherited result.

{
  "quiet": {
    "extends": "default",
    "padding": { "top": 8 },
    "continue_button": { "visible": false },
    "corner_radius": null
  }
}

quiet keeps default’s left, right, and bottom padding, keeps the rest of its continue button configuration, and drops the inherited corner radius.

Typing per preset

A preset may set typing with the same keys as game.json → typing (except Settings-owned speed_scale and auto_advance_default). Set keys override; unset keys inherit through extends, then the project default. Nested objects (caret, beep, reveal_soft, …) merge key by key.

"panel": {
  "phone": {
    "extends": "default",
    "typing": {
      "reveal": "fade_line",
      "caret": { "enabled": false },
      "beep": { "enabled": false },
      "start_sfx": { "sample": "msg_in", "volume": 0.6 }
    }
  },
  "terminal": {
    "extends": "default",
    "typography": { "font": "JetBrainsMono" },
    "typing": {
      "cps": 45,
      "caret": { "glyph": "█", "color": "#7CFF8A" },
      "beep": { "sample": "key_click", "volume": 0.3 }
    }
  }
}

The player’s Text Speed slider still multiplies each panel’s cps. Typing Sound off silences the per-letter beep only — start_sfx / end_sfx still play (SFX bus). Full key list: Text speed and voice beeps.

Typography / colors per preset

A preset may nest typography and colors overlays. Set keys override; unset keys inherit through extends, then the project theme. Nested sizes merge key by key. null removes an inherited key.

Allowed on typography: font, font_dialogue, font_label, sizes.speaker, sizes.dialogue, sizes.choice.
Not allowed: size_scale, font_menu, sizes.button, sizes.notification (stay project / player global).

Allowed on colors: dialogue_text, speaker_text.
Not allowed: choice_text, notification_text (choice packs own caption colours; toasts stay global).

ChromeFontSizeColour
Dialogue bodyfont_dialogue → fontsizes.dialogue × live size_scaledialogue_text
Labelplatefont_label → fontsizes.speaker × scalespeaker_text
Choices (host: panel)pack font else panel dialoguepack font_size else sizes.choice × scalepack colours
[input] (host: panel)same as bodypack field_font_size else sizes.dialogue × scalepack / field colours

Overlay-hosted choices and input keep project typography (plus their packs). Project size_scale always applies; presets must not set it.

"panel": {
  "terminal": {
    "extends": "default",
    "typography": {
      "font": "JetBrainsMono",
      "sizes": { "dialogue": 18, "choice": 18, "speaker": 14 }
    },
    "colors": {
      "dialogue_text": "#c8ffd0",
      "speaker_text": "#7CFF8A"
    }
  }
}

Full project keys: Typography · Colors.

Plates

A preset may define named plates — per-beat plates inside the text column. Use them with text_mode: "plates" (plain append stays a single label). Keys mirror the panel plate and the shared shapes (shared shapes).

"panel": {
  "phone": {
    "extends": "default",
    "default_plate": "them",
    "plates": {
      "them": {
        "anchor_h": "left",
        "width": "80%",
        "speaker": "on_change",
        "bg": "#2b3552",
        "corner_radius": 14,
        "padding": { "top": 8, "left": 12, "right": 12, "bottom": 8 },
        "margin": { "top": 3, "bottom": 3 },
        "group_gap": 6,
        "transition_in": "slide_right",
        "transition_duration": 0.22,
        "colors": { "dialogue_text": "#e8ecff", "speaker_text": "#8aa0c8" }
      },
      "me": {
        "extends": "them",
        "anchor_h": "right",
        "bg": "#3f6fd8",
        "transition_in": "slide_left"
      }
    }
  }
}
KeyTypeDefaultEffect
platesobject—Named plate looks (same shape as nested panel presets)
default_platestring—Fallback name when the beat does not pick one
extendsstring—Another plate in the same preset
anchor_h"left" | "center" | "right" | "stretch""left"Side of the text column — same values as panel anchor_h
width"NN%", number, or "stretch""80%"Plate width; % is of the text column
speakerfalse | true | "on_change"falseIn-plate speaker header; gap uses speaker_gap
bg / corner_radius / bg_texture / bg_9sliceas panel—Same plate builder as the panel
shaderstring, object, or null—Background effect only (target: "bg") — Shaders
padding / marginsides objectsee exampleInner padding; outer spacing (same margin shape as choices)
group_gapnumber0Extra space when the speaker changes
typography / colorsoverlay—Same allowlists as the preset overlay
transition_instringinstantHow the live plate chrome enters on a new beat (fade, slide_up, slide_down, slide_left, slide_right, instant)
transition_outstring—Validated when present; not used on fold in v1 (committed plates spawn instantly)
transition_duration / durationnumber0Enter duration in seconds; 0 or instant = no motion (duration only when transition_duration is omitted)
transitionstring—Fallback when transition_in is omitted (same kinds as transition_in)

Motion is clipped inside the panel body scroll (not the whole panel). Enter can run at the same time as line typing (for example fade_line). Theme reload (F6) restyles plates without replaying enter. Preset-root transition_in / transition_out still control whole-panel show/hide only — do not reuse them for plates.

Slide kinds name the travel direction (same as stage chrome): slide_right enters from the left edge; slide_left enters from the right; slide_up from below; slide_down from above. Left-anchored chat plates usually use slide_right; right-anchored plates use slide_left.

Selection (first match): node plate → sticky [panel … plate NAME] → character ui.default_plate → preset default_plate. Clear the sticky name with plate null. Prefer character defaults for chat sides.

Panel labelplate vs plate speaker: they are independent. Hide the panel nameplate and use speaker: "on_change" for chat, or keep both. Set an explicit bg on each plate so it does not blend into the panel plate.

append_direction, append_gap_px, append_push_duration, and clear_append work with plates the same way they do with append. Use clear_append at scene boundaries on long threads.

How a preset gets chosen

For each node, the first of these that exists wins:

  1. The node’s panel field
  2. preset on a panel command
  3. While the panel’s sticky mode is already a thread (append or plates) and this line stays in a thread mode (same-node text_mode replace ends the hold), the panel’s sticky layout preset (speaker switches do not yank to a character default; Back/load restore that preset)
  4. ui.default_panel_preset from the speaking character’s pack, if a preset by that name exists
  5. A preset named after the resolved panel id, so panel id side uses panel.side
  6. default

An unknown preset name falls back to default too, warning when the panel id is not main.

Readonly appearance

When a panel stays on screen for a character who is not speaking, non_talking_mode: "readonly_last_text" keeps the last line visible in a dimmed state. Those colours come from readonly_styles.<key> at the top level of the theme, not from the preset:

KeyTypeEffect
text_colorHTML colorDialogue text colour while readonly
speaker_colorHTML colorSpeaker name colour while readonly
bg_opacitynumber 0–1Multiplies the panel alpha
font_scalenumber 0.5–2Scales dialogue text from its normal size

readonly_styles.default is used when the requested key is missing.

Theme: labelplate

Named styles under labelplate.<style_key> in themes/default.json. Placement and speaker text resolution: Labelplate and speakers.

"labelplate": {
  "default": {
    "font_color": "#ffffff",
    "font_size": 20,
    "bg": "#00000099",
    "padding_left": 12,
    "padding_right": 12,
    "padding_top": 6,
    "padding_bottom": 6
  },
  "whisper": {
    "font_color": "#cccccc",
    "bg": "#00000066"
  }
}

Usually you need

KeyTypeEffect
font_color / font_sizecolor / numberNameplate text
bgcolorFlat plate
padding_*numberInner margins
Named <style_key>objectVariant referenced by style_key

All style keys

KeyTypeEffect
font_colorhexText colour
font_sizenumberText size
fontstringFont under fonts/
boldboolBold face when available
bghexFlat fill
bg_texturetexture specImage plate — Texture specs
bg_9sliceobjectNine-slice plate (same idea as panel)
padding_left / right / top / bottomnumberContent margins

Resolution when a style is picked: start from labelplate.default (if any), then overlay labelplate.<style_key>.

Key on panel.<preset>Effect
labelplate_style_keyDefault style when node/command omits style_key
labelplate_layoutDock geometry (anchor_h / anchor_v / offset_*); presence implies docked when mode omitted

Command / node field: labelplate. Panel inventory: panel.

Theme: buttons

Root menu / screen button chrome in themes/default.json. Choice menus use Choice packs, not this block (unless a pack inherits it).

"buttons": {
  "min_width": 400,
  "corner_radius": 8,
  "border_width": 2,
  "padding_left": 16,
  "padding_right": 16,
  "padding_top": 6,
  "padding_bottom": 10,
  "normal_bg": "#c5cad5b0",
  "hover_bg": "#dfdcd7c0",
  "pressed_bg": "#a8afc290",
  "font_color": "#1e2133",
  "font_hover_color": "#9a4e2a"
}

Usually you need

KeyTypeDefaultEffect
normal_bg / hover_bg / pressed_bgcolorcode greysFlat fills
font_color / font_hover_colorcolor—Label colours
font_sizeinttypography.sizes.buttonPack face size (× typography.size_scale)
min_widthint0 if omitMinimum width when > 0
padding_*int—Content margins

All keys

KeyTypeEffect
spacingintMenu button separation
corner_radiusintUniform flat corner radius
corner_radius_top_left / top_right / bottom_right / bottom_leftintPer-corner radius (any present → per-corner mode; missing sides fall back to corner_radius)
border_widthintUniform flat border width
border_width_left / top / right / bottomintPer-side border (any present → per-side mode; missing sides fall back to border_width)
min_width / min_heightintMin size when > 0
padding_left / padding_right / padding_top / padding_bottomintContent margins
normal_bg / hover_bg / pressed_bg / focus_bg / disabled_bgcolorFlat fills (focus → hover if omit)
normal_border / hover_border / pressed_border / focus_border / disabled_bordercolorFlat borders
normal_texture / hover_texture / pressed_texture / focus_texture / disabled_texturetexture spec or nullPer-state faces
texture_9slice[L,T,R,B]Nine-slice margins
fontstringFamily base under fonts/ (same as typography.font); omit → font_menu → font
font_variantstringregular | bold | italic | bold_italic (default regular)
font_sizeintFace size in px × typography.size_scale; omit → typography.sizes.button
font_color / font_hover_color / font_pressed_color / font_focus_color / font_disabled_colorcolorLabel colours
font_outline_size / font_outline_colorint / colorLabel outline
font_shadow_offset_x / font_shadow_offset_y / font_shadow_colorint / colorLabel shadow
align_h or alignmentstringleft | center | right (horizontal only)
icon / hover_icon / pressed_icon / focus_iconpathGlyphs (avoid dumping defaults on root)
icon_max_widthintIcon width cap
states.<state>.paddingsides objectPer-draw-state padding (normal / hover / pressed / focus / disabled)

Named packs (locale_button, info_close_button, hud_menu_button, …) use the same key set — Named button packs. Textures: Texture specs. Styling model · Typography.

Named button packs

Button chrome that is not the root buttons block: a file themes/<name>.json, and/or a same-named object inside themes/default.json. Same keys as buttons.

// themes/default.json (root objects)
"info_close_button": {
  "min_width": 0,
  "border_width": 0,
  "corner_radius": 6,
  "normal_bg": "#343a5280",
  "hover_bg": "#5b6a82c0"
},
"locale_button": {
  "min_width": 0,
  "corner_radius": 6,
  "padding_left": 10,
  "padding_right": 10
},
"locale_button_active": {
  "min_width": 0,
  "normal_bg": "#c4693ad0",
  "font_color": "#ffffff"
}

Usually you need

Pack idRole
info_close_buttonDefault chrome close control when button_style is empty
locale_buttonInactive locale chip
locale_button_activeActive locale chip
icon_close_button / hud_icon_button / hud_menu_buttonOptional pack files for icon chrome / HUD glyph buttons

How resolve works

  1. Start from root buttons (unless the call skips it).
  2. Load themes/<prefix>.json if present.
  3. Else merge root object default.json[<prefix>] if present.
  4. Merge inline style last.

Locale switcher (screens)

{
  "bind": "locale",
  "label": "Language",
  "label_key": "screen.settings.language",
  "style": "locale_button",
  "active_style": "locale_button_active"
}

Built from game.json → i18n.locales. Active locale uses active_style and is non-interactive.

Chrome close

Screen chrome with empty button_style uses info_close_button. Authored screens often set "button_style": "icon_close_button" (pack file) instead.

Keys

Identical to buttons. Styling model · Localization · Layer: chrome.

Theme: separator

Horizontal rule used by content { "type": "separator" }.

"separator": {
  "height": 2,
  "color": "#4a559060"
}

Usually you need

KeyTypeDefaultEffect
colorcolor#ffffff60Fill
heightint2Thickness (px)

All keys

KeyTypeDefaultEffect
colorcolor#ffffff60Colour
heightint2Min height

Screens may override per block via style (separator_color, history item_separator, …). Content blocks · themes map.

Theme: slider

Settings and content slider blocks.

"slider": {
  "track_height": 8,
  "grabber_size": 20,
  "corner_radius": 4,
  "track_color": "#4a559060",
  "fill_color": "#c4693a90",
  "fill_hover_color": "#d4794ab0",
  "grabber_color": "#c5cad5",
  "grabber_hover_color": "#dfdcd7"
}

Usually you need

KeyTypeDefaultEffect
track_color / fill_colorcolor—Track and fill
grabber_sizeint20Knob diameter
track_heightint8Track thickness

All keys

KeyTypeDefaultEffect
track_heightint8Track thickness
corner_radiusint4Flat corners
grabber_sizeint20Generated circle size
track_colorcolor—Flat track
fill_color / fill_hover_colorcolor—Fill
grabber_color / grabber_hover_colorcolor—Generated grabber
grabber / grabber_hovertexture spec or nullnullIcon grabber
track_texture / fill_texture / fill_hover_texturetexture spec—Textured track/fill
track_9slice / fill_9slice[L,T,R,B][4,4,4,4]Nine-slice

Widget bindings · Shared shapes · themes map.

Theme: toggle

Settings and content toggle blocks.

"toggle": {
  "track_width": 48,
  "track_height": 26,
  "knob_size": 20,
  "corner_radius": 13,
  "track_off_color": "#4a559060",
  "track_on_color": "#c4693a90",
  "knob_color": "#dfdcd7"
}

Usually you need

KeyTypeEffect
track_off_color / track_on_colorcolorGenerated track
knob_colorcolorGenerated knob
checked + uncheckedtexture specsIf both set, use PNGs instead of generated look

All keys

KeyTypeDefaultEffect
checked / uncheckedtexture or nullnullPNG mode when both set
checked_hovered / unchecked_hoveredtexture or nullnullHover swap
icon_sizeint0Fit PNGs if > 0
track_width / track_heightint48 / 26Generated track
knob_size / corner_radiusint20 / 13Generated knob
track_off_color / track_on_colorcolor—Generated
track_off_hover_color / track_on_hover_colorcolor—Generated hover
knob_color / knob_hover_colorcolor—Generated knob

Widget bindings · themes map.

Theme: scrollbar

Scrollbars for content scroll blocks and history. Optional in default.json — code defaults apply if omitted.

"scrollbar": {
  "width": 10,
  "corner_radius": 4,
  "track_color": "#4a559060",
  "grabber_color": "#5b6a82",
  "grabber_hover_color": "#6a75b0",
  "grabber_pressed_color": "#a8b8ef"
}

Usually you need

KeyTypeDefaultEffect
widthint10Bar thickness
track_color / grabber_colorcolorsee aboveFlat look

All keys

KeyTypeDefaultEffect
widthint10Thickness
corner_radiusint4Corners
content_separationint= widthGap content ↔ bar
track_colorcolor#4a559060Track (same family as slider.track_color)
grabber_color / grabber_hover_color / grabber_pressed_colorcolor#5b6a82 / #6a75b0 / #a8b8efGrabber
track_texture / grabber_texture / grabber_hover_texture / grabber_pressed_texturetexture—Textured
track_9slice / grabber_9slicearray[4,4,4,4]Nine-slice

Overlayable via a scroll block’s style pack. Dialogue panel presets may also set scrollbar_style to an element pack so a soft ADV plate can use a different bar than a dense settings screen, and scrollbar_layout (default / reserve) for whether the body keeps a stable scrollbar column — Theme: panel. Content blocks · themes map.

Theme: notification

Toast plate look. Hold time and copy live in notifications.

"notification": {
  "bg": "#1e2133cc",
  "corner_radius": 10,
  "min_width": 200,
  "padding_left": 24,
  "padding_right": 24,
  "padding_top": 10,
  "padding_bottom": 12,
  "offset_y": 20,
  "fade_in": 0.2,
  "fade_out": 0.3
}

Usually you need

KeyTypeDefaultEffect
bgcolor#1e2347ccPlate fill
offset_yint20Distance from top
fade_in / fade_outnumber0.2 / 0.3Motion

All keys

KeyTypeDefaultEffect
bgcolor—Fill
bg_texturetexture or nullnullTextured plate
bg_9slice[L,T,R,B][16,16,16,16]Nine-slice
corner_radiusint10Corners
min_widthint200Width floor
padding_left / padding_right / padding_top / padding_bottomint24 / 24 / 10 / 12Margins
offset_yint20Top offset
fade_in / fade_outnumber0.2 / 0.3Tweens

Text colour / size: colors.notification_text, typography.sizes.notification. Notifications · themes map.

Theme: card

Content-layer card chrome (settings, credits body, …).

"card": {
  "max_width": 500,
  "padding_left": 40,
  "padding_right": 40,
  "padding_top": 30,
  "padding_bottom": 30,
  "corner_radius": 12,
  "bg": "#1e2133c0",
  "spacing": 20
}

Usually you need

KeyTypeDefaultEffect
bgcolor#1e234790Fill
max_widthint500Width cap
padding_*int—Inner margins

All keys

KeyTypeDefaultEffect
max_widthint500Cap
padding_left / padding_right / padding_top / padding_bottomint40 / 40 / 30 / 30Margins
corner_radiusint12Corners
bgcolor—Fill
bg_texture / bg_9slicetexture / arrayoptionalTextured card
spacingint20Inner VBox separation

Layer: content · themes map.

Theme: settings_layout

Column widths and spacing for labeled preference / free-control rows (bind or slider/toggle with a label). Unlabeled controls are full width. table blocks use their own columns percents instead.

"settings_layout": {
  "title_font_size": 28,
  "label_min_width": 260,
  "slider_min_width": 200,
  "value_min_width": 60,
  "item_spacing": 16
}

Usually you need

KeyTypeDefaultEffect
label_min_widthint180 (code; samples often 260)Label column
slider_min_widthint200Slider column
item_spacingint16Row gap

All keys

KeyTypeDefaultEffect
title_font_sizeint28Section heading size
label_min_widthint180Label column
slider_min_widthint200Slider column
value_min_widthint60Value readout column
item_spacingint16Gap inside the row

Content blocks · slider · toggle · Widget bindings · themes map.

Theme: readonly_styles

Named looks when a panel is in readonly / non-talking mode. Optional in default.json; missing keys fall back to code defaults via readonly_styles.default.

"readonly_styles": {
  "default": {
    "text_color": "#aaaaaa",
    "speaker_color": "#888888",
    "bg_opacity": 0.7,
    "font_scale": 1.0
  }
}

Usually you need

KeyTypeDefaultEffect
text_colorcolor#ffffffDimmed dialogue
speaker_colorcolor#b3bfe6Dimmed speaker
bg_opacity0–11.0Panel alpha multiply

All keys (per named style)

KeyTypeDefaultEffect
text_colorcolor#ffffffBody
speaker_colorcolor#b3bfe6Nameplate
bg_opacity0–11.0Panel modulate alpha
font_scale0.5–21.0Scales dialogue font size

Pick with panel / node readonly_style_key. Not an element source pack. Presence and quiet panels · How text finds a panel · themes map.

Choice packs

A choice pack is a JSON file under themes/ that skins choice buttons or supplies group/item chrome for compose (source). The pack name is the file stem: "style": "choice_button" → themes/choice_button.json.

// themes/choice_button.json — button element pack
{
  "min_width": 220,
  "corner_radius": 0,
  "padding_left": 18,
  "padding_right": 18,
  "padding_top": 4,
  "padding_bottom": 6,
  "normal_texture": "sprites/button-normal.png",
  "hover_texture": "sprites/button-hover.png",
  "pressed_texture": "sprites/button-down.png",
  "focus_texture": "sprites/button-hover.png",
  "texture_9slice": [6, 6, 6, 6],
  "font_color": "#3a2a1a",
  "font_hover_color": "#6b4420",
  "font_pressed_color": "#8a5a28",
  "font_focus_color": "#6b4420"
}

Usually you need

ApproachWhenWhat you put in the file
Button element packStacked text buttonsTextures / flat colours / fonts (table below)
Group compose packFree / sprite / overlay menuscontainer + item (+ optional shuffle / show_numbers) — same shapes as story choices
Name onlyMost stories"style": "choice_plain" in layout.choices or on the node

How style / source resolve: Styling model. Node fields: Choices.

Two pack roles

RoleReferenced byTypical contents
Button elementlayout.choices.style, node choices.style, choice_styleFlat button chrome keys (this page)
Group / composechoices.source, container.source, item.sourcecontainer, item, toggles — story Choices shapes; usually omit items

Nested source is allowed (depth capped). Locale overlays: themes/<name>_<locale>.json.

Button element pack keys

KeyTypeEffect
min_widthnumberMinimum button width
corner_radiusnumberRounded corners on flat faces
border_widthnumberFlat border width
padding_left / padding_right / padding_top / padding_bottomnumberContent margins
normal_texture / hover_texture / pressed_texture / focus_texturetexture spec or ""Nine-slice / image faces; empty string = flat fill
texture_9slice[L, T, R, B]Slice margins for textures
normal_bg / hover_bg / pressed_bg / focus_bgcolor stringFlat fills
normal_border / hover_border / pressed_border / focus_bordercolor stringFlat borders
font_color / font_hover_color / font_pressed_color / font_focus_colorcolor stringCaption colours
states.<state>.paddingsides objectPer-draw-state padding override (pressed, …)
interaction.hooksobjectDefault item hooks merged under items
icon / hover_icon / pressed_icon / focus_iconpath stringOptional glyphs (same as item style)
icon_max_widthnumberIcon width cap
click_sfx / hover_sfxstring or nullPer-channel UI SFX for button (non-sprite) choices. Omit = sounds.button_*; "" / null = silent

Texture specs: string path, or richer texture objects — Texture specs. Empty texture strings keep flat colour chrome (see choice_plain.json).

Same click_sfx / hover_sfx keys are allowed on item style overlays (merged with the pack). If on_hover_enter / on_confirm already includes ui_sfx, that channel’s inherited theme SFX is skipped. Sprite render_mode does not inherit button SFX — add ui_sfx hooks when you want sound.

Does not restyle screen/menu buttons (those use theme root buttons) or the dialogue panel.

Group compose pack example

// themes/choice_yes_no.json — free + sprite + overlay
{
  "show_numbers": false,
  "shuffle": false,
  "container": {
    "layout": {
      "mode": "free",
      "host": "overlay",
      "render_mode": "sprite",
      "anchor_h": "center",
      "anchor_v": "center",
      "width": 750,
      "height": 288
    },
    "style": { "bg_texture": "sprites/choice_bg.png" },
    "transition": { "on_show": "slide_down", "on_hide": "fade", "duration": 0.3 }
  },
  "item": {
    "style": { "text_color": "#00000000" },
    "state_visual": {
      "hover": { "animation": { "property": "position_y", "to": 73, "duration": 0.08 } }
    }
  }
}

Node then: "choices": { "source": "choice_yes_no", "items": [ … ] } with per-item layout.x / y and style.sprite faces. Full key lists: Choices (story node).

Text input packs

Optional element and compose packs for story input chrome. Files live under themes/<name>.json (same folder as choice packs).

No pack is required. Omit style / input_style / layout.text_input.style and the field/confirm use dialogue colours plus flat fills. Ship a pack only for a custom look.

Confirm button

Same keys as choice / nav packs (normal_bg, normal_texture, hover_texture, texture_9slice, font_color, …).

With no pack, confirm uses the theme buttons colours/corners but keeps a compact width (menu min_width is not applied) and matches the field’s font size. Named packs can set their own font_size.

Author chrome: confirm.label (default OK), confirm.visible (false hides the button; Enter / Esc still confirm).

Field

Colors inherit dialogue text by default. Optional overrides:

KeyRole
field_font_colorTyped text (caret follows unless overridden)
field_font_sizeTyped text size (default: typography.sizes.dialogue)
field_caret_colorCaret only
field_placeholder_colorPlaceholder
field_normal_bg / field_focus_bg / field_read_only_bgFlat fill when no texture
field_borderFlat border color
field_padding_left / _right / _top / _bottomContent margins
field_normal_texture / field_focus_texture / field_read_only_textureFace textures (Texture specs)
field_texture_9slice or texture_9slice[L, T, R, B] nine-slice margins

Empty / omitted textures → flat colors. Focus without its own texture reuses field_normal_texture. Flipbook frames animate while the field is shown.

Example (still field + textured confirm):

{
  "field_normal_texture": "ui/field_normal.png",
  "field_focus_texture": { "file": "ui/field_focus.png", "modulate": "#ffffff" },
  "field_texture_9slice": [12, 8, 12, 8],
  "field_padding_left": 14,
  "field_padding_right": 14,
  "normal_texture": "ui/ok_normal.png",
  "hover_texture": "ui/ok_hover.png",
  "texture_9slice": [8, 8, 8, 8],
  "font_color": "#e8e6e1"
}

Prompt label uses the same font color/size as dialogue (typography.sizes.dialogue) unless you only care about field chrome.

theme_style does not restyle input chrome (same rule as choice packs).

Compose pack (source)

A compose pack may include container, field, confirm, and optional style. Merged under node / layout defaults at show time.

Precedence

Node input.style → sticky input_style → layout.text_input.style → code fallbacks.

Character pack detail

Pack JSON under characters/<id>/. File map: characters pack map.

PageJob
Pack rootdisplay_name, ui, reserved files
Expressions and layersExpression arrays, inherit, layer keys
Drive rulesTalking / focus mouth drive
Composition transitionsExpression swap fades

Guide: What a character is · Expressions and packs.

Detail pages · characters map

Character pack root

Pack file: characters/<id>/<pack>.json (e.g. default.json).

{
  "display_name": "Emily",
  "ui": { "draw_layer": "behind_panel" },
  "default": [{ "path": "base.png" }],
  "drive": {},
  "composition_transitions": {}
}

Usually you need

KeyTypeEffect
display_namestringSpeaker label
<expression>array or inherit objectLayers — Expressions and layers

All root keys

KeyTypeEffect
display_name / display_name_keystringSpeaker label / catalog
uiobjectdefault_panel_id, default_panel_preset, default_plate, default_labelplate_style_key, panel_slots, draw_layer
driveobjectDrive rules
composition_transitionsobjectComposition transitions
<expr>array | inheritExpression layers

Reserved filenames (not packs): sprites.json, objects.json, metadata.json, profiles.json.

Characters and composition.

Expressions and layers

Expression values in a character pack: a layer array, or an inherit object.

"default": [
  { "path": "base.png" },
  { "path": "mouth.png", "drive": "!typing" },
  { "frames": ["m1.png", "m2.png"], "fps": 10, "loop": true, "drive": "typing" }
],
"happy": {
  "extends": "default",
  "override": [{ "layer": 1, "path": "mouth_happy.png" }]
}

Usually you need

FormWhen
Array of layersA full expression
{ "extends", "override" }Variant of another expression

Layer array entries

KeyTypeEffect
pathstringImage (__blank = empty)
x / y / w / hnumberOffset / size
modulate / scale / opacity—Look
drivestringVisibility: typing, !typing, focus, !focus, choice_open, …
framesstring[]Animated layer
fps / loop / loop_count / hide_when_done / shuffle / idle_frame—Animation
interval_min / interval_max—Frame intervals

Inherit object

KeyEffect
extendsBase expression id
override[{ "layer": N, …fields }] patches
addExtra layers appended
driveOptional variant guard (mismatch → fall back via extends)

Pack root · Drive rules · Characters.

Drive rules

Pack-root drive: predicate → rule. Runtime auto-drivers: typing, focus, choice_open (not saved).

"drive": {
  "focus": { "use": "happy", "priority": 10, "scale": 1.05 },
  "typing": { "modulate": "#ffffff", "priority": 5 }
}

Usually you need

FieldEffect
useSwitch to another expression while the predicate holds
modulate / scaleTint / size while active

All rule fields

FieldTypeEffect
useexpression idSwitch variant
modulatehex colorTint
scalenumber or [sx,sy]Scale
offsetnumber or [dx,dy]Offset
rotation_degreesnumberRotate
prioritynumberHigher wins when several match
transition / _in / _outobjectTween (duration, ease, curve, optional kind)

Matching rules: modulate/scale multiply; offset/rotation sum.

Layer drive strings only gate visibility — Expressions and layers. Distinct from flag Binds. Characters.

Composition transitions

Pack policy for same-slot expression swaps (not stage show/hide). Command transition* / swap_mode on char overrides when set.

"composition_transitions": {
  "default": { "kind": "fade", "duration": 0.2, "swap_mode": "auto" },
  "pairs": {
    "happy->angry": { "kind": "fade", "duration": 0.4, "swap_mode": "crossfade" }
  }
}

Usually you need

KeyEffect
defaultFallback { kind, duration, swap_mode }
pairs"from->to" overrides

All keys

KeyEffect
default{ kind, duration, swap_mode }
from / toMaps expression id → payload
pairs"a->b", wildcards a->*, *->b, *->*

Payload

FieldValues
kindinstant, fade, slide_up / down / left / right
durationseconds
swap_modeauto | in_only | crossfade | out_in | cross_slide

Resolve order: exact pair → from-wildcard → to-wildcard → *->* → from map → to map → default → instant.

auto: fade→crossfade; slide→in_only; else in_only.

Transitions · char · Pack root.

Other files detail

PageJobMap
cursors.jsonSoft pointer profilescursors map
keybindings.jsonAction → keyskeybindings map
Objectssprites/<id>.obj.jsonobjects map
Animation presetsanimations/presets.jsonanimations map
Strings catalogstrings.json / locale catalogs—
extensions (game.json)Native extension packsgame.json → extensions

Detail pages · File maps

cursors.json

{
  "default": "default",
  "profiles": {
    "default": {
      "kind": "image",
      "image": "sprites/pointer.png",
      "hotspot": [2, 2],
      "scale": 1.0
    }
  }
}
KeyEffect
defaultProfile id
profiles.<id>Profile object

Profile

KeyEffect
kind"image" | "obj"
image / objectAsset / obj id
hotspot[x, y]
scale / modulate / opacityLook
shaderSprite-shaped shader

Locale: cursors_<locale>.json. Cursor · game cursor · cursor command.

keybindings.json

Action → array of key / mouse names. Author arrays replace the default list for that action (merge is per-action overlay).

{
  "dialogue_advance": ["Space", "Enter", "MouseLeft"],
  "dialogue_back": ["Backspace"],
  "pause": ["Escape"]
}

Usually you need

ActionRole
dialogue_advanceAdvance / confirm
dialogue_backRollback
pausePause screen

Canonical actions

ActionRole
dialogue_advanceAdvance / confirm
dialogue_backRollback
pause / save / load / quitSession
choice_1 … choice_5Numbered picks
choice_prev / choice_next / choice_cycle / choice_confirmChoice nav
debug_*Debug overlay

Locale overlay: keybindings_<locale>.json. Concept: Keybindings. Save and session (dialogue_back).

Objects (*.obj.json)

Catalog definition under sprites/<id>.obj.json. Instances use the obj command.

{
  "id": "lamp_widget",
  "layer": "overlay",
  "parts": [
    { "id": "bg", "kind": "rect", "width": 40, "height": 283, "color": "#343a52e0" },
    {
      "id": "ptr",
      "kind": "polygon",
      "points": [[20, 0], [0, 13], [20, 26]],
      "bind": { "y": { "flag": "score", "map": { "5": 23 }, "default": 23 } }
    }
  ]
}

Usually you need

KeyTypeEffect
idstringRequired definition id
partsarrayRequired parts
layerstringDefault plane (overlay, …)

Root keys

KeyTypeEffect
idstringDefinition id
partsarrayPart list
layerstringDefault draw layer
transition / transition_in / transition_outkindDefault show/hide
duration / transition_durationnumberSeconds
modulatecolorRoot tint
scalenumber or [sx,sy]Root scale
rotation_degreesnumberRoot rotation

Parts

Kinds: image | rect | polygon | text.

Meters and HUD gadgets in the root demo are mostly rect + polygon (and bind) — you do not need sprite art for a score lamp. Use image when you have a PNG face.

Shared

KeyTypeEffect
idstringPart id (for update)
kindenumShape
x / y / width / height / ordernumberGeometry / draw order
points[[x,y],…]Polygon
color / opacity / visible—Look
path / value / image fields—Image source
frames / fps / loop / intervals—Flipbook
bindobjectFlag → property — Binds

Text parts

KeyTypeEffect
textstringCaption
bbcode_enabledboolRich text
font_sizenumberSize
align_hstringAlign
autowrapboolWrap

No shader / sfx patch on text parts.

Sprites and objects · obj · Transitions.

Animation presets

File: animations/presets.json (+ presets_<locale>.json).

Named presets are dictionaries of animate fields. Reference with "preset": "nudge" on an animate command.

{
  "nudge": {
    "property": "position_y",
    "to": -8,
    "duration": 0.12,
    "relative": true,
    "loop": "ping_pong",
    "count": 2
  },
  "pulse_soft": {
    "property": "scale",
    "from": 1.0,
    "to": 1.05,
    "duration": 0.5,
    "ease": "in_out",
    "loop": "ping_pong"
  }
}

Usually you need

Key on presetEffect
propertyWhat to tween
to / fromEnds
durationSeconds

Allowed fields

Same as animate (minus requiring type / target on the preset itself — target stays on the command): property, from, to, duration, ease, trans, loop, count, relative, keyframes, frames, fps, optional sequence[], preset knobs (intensity, …).

Built-in names (shake, pulse, flash, bob) work without this file. Animations.

Strings catalog

story/strings.json (+ strings_<locale>.json): flat key → string map for UI and keyed dialogue.

{
  "game.title": "My Novel",
  "screen.common.next": "Next",
  "choice.yes": "Yes"
}

Usually you need

IdeaHow
Literal still worksMost fields accept plain text / label without a catalog
Shared copySet text_key / label_key / title_key / display_name_key — no sibling text / label needed
Standalone fallbackKeep a literal next to a key only when you want a fallback if the key is missing
LocaleOverlay strings_<locale>.json

Some keys live only in the catalog: panel nav labels, notification.* toasts, and other runtime show_key strings. They do not need a matching *_key in your JSON.

Shape

FormMeaning
Object of string valuesCatalog
Key with dotsConventional namespaces (game., screen., choice.) — not enforced

Missing keys fall back per Translations. check reports a keyed field with no literal and no catalog entry (i18n.missing_key). Used by screens, choices, notifications, window title, character display_name_key, and more.

Translations · Flow tags (literals in script stay in the script file).

extensions (game.json)

Declare native extensions. Each entry is a GDScript file under your project that the player loads at startup (and reloads on Author Refresh in author mode).

How the extension lives in the player (pause, planes, input, lifecycle, recipes): Native extensions. This page is the declaration + Host method reference.

{
  "extensions": [
    {
      "id": "mouse_parallax",
      "entry": "extensions/mouse_parallax/main.gd",
      "runtime_min": "0.1.0"
    }
  ]
}

Usually you need

KeyTypeDefaultEffect
idstring—Unique id ([a-z0-9_]+)
entrypath string—Path relative to the project root
runtime_minstring—Optional player version floor; skip load if player is older

Where this lives

IdeaFilePath
Declarationgame.jsonextensions (array)
Entry scriptproject treepath in entry

Concepts: Native extensions. Example: examples/example-06-native-extension/. Map: game.json → extensions.

Entry script

extends Node

func vn_extension_setup(host) -> void:
	# required — host is the ExtensionHost API (untyped)
	pass
  • Cold boot loads every declared entry once.
  • Author Refresh (F5) reloads extensions in author mode (frees prior instances, clears ext.* save keys and ext_* command handlers, then loads again). Production / shipped player builds still load once at boot — restart if you only ship a player without author tools.
  • Player Build (Filter unused assets on) ships the entry script and the other files in that entry’s folder (and everything under extensions/ by convention). Keep companion textures/sfx next to main.gd, or turn the filter off.
  • Reset your own state on story end via host.connect_story_ended (session end does not free your Node).
  • Parent is a reserved Extensions node; your instance is named ext_<id>.

ExtensionHost methods

MethodEffect
extension_id() -> StringThis entry’s id
runtime_version() -> StringPlayer / kit version
get_plane(plane_id: String) -> CanvasLayerNamed draw plane, or null
register_save(key, save_fn, restore_fn)Registers under ext.<id>.<key>
register_command(verb, handler)Registers story command ext_<id>_<verb>
get_flag(name) -> VariantRead a story flag
set_flag(name, value)Write a story flag (shared with the story)
connect_node_entered(cb)Story entered a node
connect_flag_changed(cb)A flag changed
connect_story_ended(cb)Story ended
set_input_blocker(enabled)Block dialogue advance / choice confirm (not tree pause)
get_phase() -> String"title" / "playing" / "exit_commands" / "story_ending"
jump(node_id)Request a story jump (same path as jump command)
resume()Clear the input blocker only
notify(message)Toast above modal screens
push_screen(screen_id) -> boolPush an id from screens.json, or false
pop_screen()Pop the current modal screen if showing
log(message)print with [ext:<id>] prefix
warn(message)Warning with the same prefix

Custom commands

Call host.register_command("burst", handler) to handle Mode A / JSON commands whose type is ext_<id>_burst (host builds the type string; verb must match [a-z0-9_]+). Schema/check allows that type only when id is declared in game.json → extensions[].id. Remaining Mode A tokens land in "args" (and key=value tokens are also copied onto the command object). For structured fields prefer JSON / Mode B.

Plane ids

bg, scene_ui, chars, panel, above_panel, overlay, hud, notify, modal, cursor, transition.

Draw order and title/pause stacking: Native extensions · Screens.

Save order

Core systems register first; extensions register afterward (load order). Restore runs in registration order, so extension state restores after core state. F5 unregisters all ext.* keys before reload.

Using several extensions

There is no limit on how many entries you declare.

RuleMeaning
Declared array orderLoad order and save-registration order
Unique idSchema rejects duplicates
Save keysAlways ext.<id>.… — isolated per extension
Shared planesChildren stack in load order; use z_index or reorder the array
FlagsNot namespaced — shared with the story. Prefer ext_<id>_* for private state
One failureA skipped entry (missing file, bad script, runtime_min) does not block later entries
CooperationNo inter-extension handles — use flags

Failure modes

ProblemResult
Missing / empty entry fileSkip + error log
GDScript parse / reload errorSkip + error log
Missing vn_extension_setup or not a NodeSkip + error log
Duplicate idKeep first; skip later
runtime_min newer than playerSkip + error log
Author modeAfter title is up: toast above the title listing loaded id(s) (“unrestricted code”)
Production / shippedSilent load

Native extensions · game.json map · Screens · Build and hand off

Shared shapes

Some settings look the same wherever they appear; others share only a name. Each shape below is tagged:

  • shared — one shape, one meaning, everywhere.
  • per-surface — same name, different body. The table says which surface accepts what.
ShapeTag
Fades and slidesShared vocabulary
Pictures (texture specs)Per-surface
Position on screenPer-surface (host varies)
Conditions and “only when”Split — three different shapes
Sticky vs this moment (apply)Shared
Live links to flags (bind)Shared (surface list varies)
Click, hover, focus (hooks)Shared vocabulary, per-surface events
Looks per state (state_visual)Shared
Layering one look on anotherShared
Effects (shaders)Per-surface
modulate and shader_paramsPer-surface

Fades and slides

Shared vocabulary. Kinds: instant, fade, slide_up / down / left / right.

FieldMeaning
transitionSame kind for enter and leave
transition_in / transition_outSeparate enter / leave
duration / transition_durationSeconds (0 = instant)

Used on backgrounds, characters, sprites, objects, panels, screens. The default differs by surface (e.g. bg fades by default; char is instant), so check the command page.

One exception: char_layout accepts only "" / instant / fade (no slides).

→ Transitions · character expression swaps: Expression fades

Pictures (texture specs)

Per-surface. Two shapes share the word “picture”:

Chrome faces — panel plates, buttons, choice faces, text-input fields, sliders:

FormMeaning
string pathStill picture in the project
{ "file", "modulate"? }Still + optional tint
{ "frames", "fps"?, "modulate"? }Flipbook
""Flat / no image (where allowed)

Character pack layers — one picture inside an expression — are a different shape: path (not file), plus frames, fps, drive, x / y / w / h, and per-layer modulate / scale / opacity. A layer that uses file is dropped — use path. → Expressions and layers

Where "" is allowed: chrome faces ("" = flat / no image); source ("" = keep empty); char.transition ("" = not specified); an SFX name ("" = silent). It is not a universal value — check the field.

→ Theme and the dialogue box · Choice and input looks

Position on screen

Per-surface. Either absolute x / y, or anchor_h / anchor_v plus offset_x / offset_y. Optional width / height, min/max, padding.

Host — where the surface offers it — differs: dock to the window (screen), beside the dialogue box (panel), or overlay for free choice menus.

→ Places on stage · Gameplay HUD · Sprites and objects

Conditions and “only when”

Split — three unrelated shapes share only the word “condition”:

ShapeWhereEffect
whenCommands (data / direction)Skip this command if the expression is falsy
show_if / disable_ifScreen buttons / choice rulesHide or disable until the condition is true
jump_if / [if …] / next_ifStoryBranch on flags

→ Reacting to flags live · Choices and memory · Expressions

Sticky vs this moment (apply)

Shared.

ValueEffect
persistent (usual default)Stays until reset, override, or new game
node_localReverts after this moment

Used on panel, labelplate, obj, and similar command families.

→ Sticky state and clear · Reacting to flags live

Shared — the shape is the same; the surfaces that offer it differ.

"bind": {
  "opacity": { "flag": "lamp_on", "map": { "true": 1, "false": 0.2 } }
}

Surfaces: sprites, objects, characters, backgrounds, panels, and others that list bind keys in their command page.

ModeMeaning
mapDiscrete flag value → property
linearNumeric range remap
directPass the flag value through

→ Reacting to flags live · character look: Expressions and packs

Click, hover, focus (hooks)

Shared vocabulary, per-surface events. The shape is the same; which events exist depends on the surface (screen button vs sprite vs choice item).

"interaction": {
  "focusable": true,
  "hit_test": "rect",
  "hooks": {
    "on_click": "screen:pause"
  },
  "hooks_override": {}
}
KeyMeaning
hooksEvent → string verb, command object, or array of those
hooks_overrideOverride / clear inherited events (null removes)
focusable / focus_orderFocus
hit_testrect | alpha | polygon

A menu button carries its action here — interaction.hooks.on_click. Verb inventory: Actions and hooks.

→ Buttons and actions · Sprites and objects

Looks per state (state_visual)

Shared. Per-interaction-state faces: normal, hover, focus, pressed, disabled, optional fallback_order.

Each state may set textures / modulate, source pack compose, shader_params, or a short animation / animations block (not both). shader on a state is clear-only (null to clear) — shader_params is kept when present.

→ Choice and input looks · Buttons and actions

Layering one look on another

Shared.

ToolMeaning
"extends"Merge keys (panel presets) or layers (character expressions) — different tools
"style": "name"Sugar for { "source": "name" }
"source": "name"Load themes/<name>.json (+ locale), deep-merge local keys
path with /Under the themes tree

Nested source up to depth 3. Merge: missing = inherit; null = delete; "" = keep empty; present = override.

→ Reusing settings · Choice packs

Effects (shaders)

Per-surface. Accepts a bare string (a stem under shaders/) or an object, and null clears — but which forms and target values each surface allows differs:

SurfaceAcceptstarget allowedDefault
charstring or objectfullfull
bg / stagestring or objectfullfull
spriteobject onlyfullfull
objobject onlyfullfull
panelobject onlybg | fullbg
screens / choicesobject onlybg | fullbg
theme plate (panel face)string or objectbgbg

Only char / bg / stage (and theme plates) take a bare string; the rest need the object.

Object keys:

KeyMeaning
nameStem under shaders/ (required)
paramsUniforms
targetbg | full (see table)
passdirect | subviewport (default subviewport)
marginNumber or { left, top, right, bottom } — needs a model background on bg / stage

panel and screens forbid full + direct.

Where an effect sits: one shader per slot / instance — the whole composited result — never per layer or per part:

  • Character: one shader over the whole slot (all layers composited). Not per layer.
  • Object: one shader over the whole instance; text parts take no shader.
  • Sprite / background: one each.

Full-screen sticky: screen_shader.

→ Shaders

modulate and shader_params

Per-surface. modulate is the most inconsistent shared-looking key:

SurfacemodulateTypeScope
character layeryeshex stringper layer
character slotyescolorwhole slot
spriteyescolorwhole sprite
bg / stageyeshex stringthe plate
objyeshexwhole instance
panelno——
state_visual entryyesper stateper state

shader_params (uniforms) is a sibling of shader on char / bg / stage / obj; sprite and panel have no shader_params — put uniforms in the shader object’s params.

Next

Everything A–Z · Detail pages

Words used in these docs

These docs try to use plain words. Where the player, error messages or older pages use a technical name, it is listed here too.

In these docsAlso calledMeaning
Momentnode, beatOne click-through step of the story: what happens, then what is said
TalkingtalkingWho is speaking this moment — one id or several (may differ from who is on screen)
ScenelabelA named moment you can jump to (:: park)
Stage directioncommandA bracket line that changes the scene ([bg street])
The short way to write itscript sugar, Mode A syntaxWriting a command as a bracket line instead of data
Script onlyMode AThe whole story in story/script.txt
Script + direction fileMode BDialogue in the script, extra direction in story/story.json
All in story.jsonMode CEvery moment written as data
Flagvariable, varSomething the story remembers (helped, score)
Native extensionextensions[]Custom code the player loads from game.json — Native extensions
SessionplaythroughNew Game / Continue / Load until title — Save, Back and story end
Locale overlay*_uk.json, script_en.txt, …Language file next to a base file — Translations
Place on stageslotWhere a character stands (left, center, right) — Planes and slots · Places on stage
Draw stackplaneFixed layer that decides what sits in front — Planes and slots
Arrangementlayout profileA named set of places
Layered characterpack, compositionA character built from several pictures — Expressions and packs
Expression swap fadecomposition_transitionsSoft blend when the look changes on stage — Expression fades
Talking and focus reactionsdrivePictures or effects that change while a character speaks or is in focus — Talking and focus
Composition layerpack layerOne stacked picture inside a character expression
Draw band (props)sprite/obj layerbg_front / fg / overlay — Sprites and objects
Silent line(silent), driver_typing: falseSpeaker line with typing mouth / beep off — Lines and speakers
Dialogue boxpanelThe box the text appears in — What a panel is
Panel instancepanel idWhich box on screen (main, side, …) — How text finds a panel
Panel lookpanel presetNamed look under panel.<name> in the theme — How a panel looks
Stacked panel linestext_mode appendKeep earlier text and add the next moment’s text — Appending text
Panel height ceilingmax_heightHow tall the plate may grow before body text scrolls — How a panel looks
Name platelabelplateThe speaker’s name box — Name plate
Theme moodtheme_styleOverlay theme file for a stretch of play — Theme and the dialogue box
InterfacechromeMenus, buttons and boxes around the story
ScreenscreenA full menu or overlay: title, pause, settings — Title, pause and settings screens
In-game barHUDA small screen shown during play — Gameplay HUD
Stays until you change itstickyA setting that carries on to later moments
Live link to a flagbindA value on screen that follows a flag by itself
Lookstyle, pack, themeHow something is drawn
Reuse / build oninheritance, extendsStart from an existing look or expression; change only what differs — kinds differ: Reusing settings
Pack looksource, styleLoad a theme pack onto a button, choice or name-entry field
Expression variantcharacter extends / override / addOne expression built from another’s layers
Layer (front / back)planeWhich things draw in front of which
Shared shapecontractA settings shape used in many places
EffectshaderA visual filter on a picture or the whole screen — Shaders
Show/hide blendtransitionHow something appears or leaves — Transitions
Tween / preset motionanimateMove or pulse something already on stage — Animation
Soft pointercursorCustom mouse look — Cursor
Language overlaylocale, i18nExtra language files and Settings switch — Translations
Author playerauthor modeThe player you write and test in — How you work
Released gameplayer buildWhat you give to readers — Build and hand off
Boot brandsplashLoading / boot look — Title, icon, splash