Code Editor
The code editor lets code-savvy writers write sections, or their entire story, in code. Anywhere you see a “Code Editor” button - on a scene, on an effects panel, or inside a choice’s effects popover - clicking it opens a full-screen modal with an IDE and a live preview of how many effects you’ve written and any errors.
It’s optional. Every feature is also available in the GUI. The code editor exists for moments when clicking would be slow: for instance ten weighted or random damage rolls for a combat round, a block of scene text with inline conditions and variable references, or reorganizing a scene’s choices in one pass. You could also easily copy and paste choices with all their conditions and effects from one scene to another.
Three Scopes
The editor runs in three scopes depending on where you open it from:
- Scene scope: opened from the scene editor’s toolbar. The editor contains the whole scene: body text, on-enter effects, and every choice with its conditions and effects.
- Effects scope: opened from an effects panel (on a choice, or a scene’s on-enter block). Each line is one effect.
- Variables scope: opened from the Code Editor button at the top of the Variables panel, or from the scene Code Editor’s left-side rail (the Variables icon, next to the Scenes icon). The editor contains every variable definition: name, type, starting default, flags, folder, writer notes, and tag assignments.
The effect and scene scopes share one grammar. Variables scope uses a simpler grammar of its own (one block per variable, same value forms for defaults: literal / rand / roll / oneOf).
Effects Grammar
One effect per line. Lines starting with // are comments. Blank lines are ignored.
Note on where comments persist.
//comments only survive a save when they’re inside atext:block - there they become Writer Comment blocks in the visual editor (visible to writers, hidden from readers). Comments written between top-level blocks, insideon enter:, or inside a choice body will be lost, so the editor shows a warning and drops them on save. Move the note into atext:block if you want it to persist.
Variable Effects
PlayerDamage = 5 // set static
PlayerDamage += 5 // add (also -=, *=, /=)
EnemyHealth *= 2 // multiply
EnemyHealth /= 2 // divide
EnemyHealth -= weapon_damage // subtract a variable
PlayerDamage += weapon_damage + 5 // compound formula
reputation += npc:Mara // reference NPC / faction sentiment
PlayerDamage = rand(+1 to +6) // random range (literal bounds)
EnemyDamage = rand(minDmg to maxDmg) // random range (variable bounds)
PlayerDamage += roll(2d6+3) // dice roll
loot = oneOf(5, 10, 15) // one numeric value
Rarity = oneOf("common", "rare", "legendary") // one string value
HasKey = true // boolean
PlayerName = "Alice" // string
Greeting = "Hello, world" // literal comma string For oneOf(...), numeric options are unquoted and string options are quoted. oneOf(common, rare) is invalid because unquoted string options are ambiguous.
Use normal quoted strings for literal text that contains commas, like "Hello, world".
Formulas on the right-hand side can reference other number variables, NPC
sentiment (npc:Name), faction sentiment (faction:Name), scene visit counts
(scene:"Name"), and choice click counts (choice:"Label"), and combine them with +, -, *, /. Multi-word variable names use underscores in formulas
(weapon_damage matches a variable named weapon damage). The Code Editor
saves scene and choice formula operands with stable internal IDs, then shows
the current labels when you reopen it. If two scenes or choices share a label,
the editor adds an id suffix like scene:"Tavern"#bbbbbbbb. Formulas only
apply to number-typed targets. String and boolean variables still need a quoted
literal or true/false.
String variables also accept += for concatenation: Greeting += " world" or Greeting += oneOf("!", "?") appends the right-hand side to the existing
value. -=, *=, and /= have no string semantics and are rejected.
A literal-looking comma string with no spaces is treated as a legacy oneOf shorthand by the engine, so Greeting += "common,rare,legendary" random-picks one of those words instead of appending the comma string.
To force literal text, add a space - "common, rare, legendary" appends
verbatim. Use oneOf(...) when you want the random pick explicitly.
Probability Prefix
Wrap an effect in N%: to make it fire with a given chance.
15%: PlayerDamage += 6
50%: PlayerDamage = 0
(50 + speech * 2)%: gold += 100 // formula (base + variable * multiplier)
(25 + scene:"Bank Lobby" * 5)%: gold += 50 Conditional Effects
if EXPR: gates an effect on a condition. You can combine clauses with and, or, and parentheses.
if gold > 100: Health = 50
if gold > 100 and speech > 5: reputation += 1
if (gold > 100 and speech > 5) or charisma > 10: gold -= 50
25% if gold > 100: Health += 10 // probability + condition stacked Supported operators: =, !=, >, >=, <, <=, discovered, !discovered, at_scene, !at_scene.
Engine-tracking conditions
The engine tracks how many times each scene has been visited and each choice clicked. Reference those counts inside a condition with scene:"Name" or choice:"Label":
if scene:"Bank Lobby" >= 2: reputation += 1 // after two visits
if choice:"Attempt bribery" > 0: gold -= 50 // the player has bribed before
when scene:"Tavern" >= 1 and speech > 5 // choice visibility gate The label is always quoted because scene names and choice labels usually contain spaces or punctuation. See Variables & Effects for background on scene-visit and choice-click tracking.
Entity Effects
npc:Mara += 5 // add sentiment
npc:Mara discover // mark discovered
faction:Guild = -10 // set sentiment
persona:Hero // set the player's persona Use the npc: / faction: / persona: prefix when an entity’s name would collide with a variable name. Otherwise the bare name works when it is unambiguous.
Audio
play "TavernMusic" // default volume 100
play "TavernMusic" at 80% Dynamic Choices
Inside a dynamic choice’s effects, the right-hand side can reference the iterated entity’s tag properties via {propertyName}. At runtime the engine resolves the property for whichever entity produced this choice (e.g. when “Eat Canned Chicken” is picked, {satiety} resolves to Canned Chicken’s satiety).
Health += {satiety}
Health -= {satiety}
Health = {satiety} See Tags & Dynamic Choices for more information.
Dropdown Choices
Inside a dropdown choice, every line is one option and must end with as "Label". The left-hand side is the variable assigned when the option is picked; the label is what the player sees.
Weapon = "Sword" as "Sharp sword"
Weapon = "Bow" as "Reliable bow"
Weapon = "Staff" as "Magic staff" Probability prefixes and if are not allowed on dropdown options. They set one value per pick.
Scene Grammar
Scene scope wraps effects grammar in three block types:
text:
The guard blocks the doorway, one hand resting on his sword.
"State your business," he barks.
on enter:
reputation += 1
if gold > 100: HasReputation = true
timer 30 default "Leave quietly"
interact choice "Attempt bribery":
when gold >= 50
goes to "Bribed Outcome"
gold -= 50
50%: reputation -= 1
continue choice "Leave quietly":
goes to "Town Square"
reputation += 1
continue choice "Slip into the passage":
goes to new "Hidden Passage" text: Block
The body of the scene, as a Markdown dialect. Headings, lists, bold/italic/strikethrough/underline/subscript/superscript, images, links, inline code and code blocks, blockquotes, horizontal rules, message blocks, writer comments, inline conditional text ({gold > 20: you are rich}), multi-line conditional blocks (<if {gold > 20}>…</if>), polls, inline sound effects, text variations, and inline variable references ({gold}, {npc:Mara}) all persist changes from the code editor to the visual editor and back.
Inline formatting uses **bold**, _italic_, ~~strike~~, and `inline code`. Italic uses underscores (not single asterisks) so * is unambiguously bold.
Paragraphs and headings that are centered or right-aligned use raw HTML tags in the code editor:
<p align="center">Centered paragraph</p>
<p align="right">Right-aligned paragraph</p>
<h2 align="center">Centered heading</h2> Left/default alignment uses normal Markdown.
<image alt="Mara portrait" caption="Portrait of Mara" align="center" /> Sound effects:
<sfx name="door creak" /> The name must match one of the uploaded sound effects in the story.
Text variations pick between alternative phrasings on each render. They match the visual editor’s “Add text variation…” menu:
| Form | Behavior |
|---|---|
{random: Hello \| Hi \| Greetings} | Pick one variant uniformly at random every time the scene renders. |
{randomOnce: Red \| Blue \| Green} | Pick once on first render; keep the same choice for the rest of the playthrough. |
{cycle: First \| Second \| Third} | Advance through the variants one per visit, wrapping back to the start. |
{cycle:greeting: Hello \| Hi} | Named cycle. Multiple blocks sharing the same name advance in lockstep. |
Conditional blocks gate a multi-line region of scene text on a condition. They share the inline shorthand grammar (same operators, same and/or, same npc:/scene:/choice: prefixes), wrapped in <if {…}>…</if>:
<if {gold > 100}>
The barkeep nods at the heavy purse on your hip.
</if>
<if {choice:"Push on to the river." > 2}>
You've come this way before. The path bends just past the willow.
</if>
<if {Found Logs = false}>
There must be logs here, right? There _must_ be a record of what happened.
</if> The {…} slot uses exactly the same expression syntax as inline {condition: text}, so anything that parses inline parses inside <if> too.
on enter: Block
Effects that fire when the reader enters the scene. Uses the full effects grammar above.
timer Line
A single line at the scene’s top level that shows a countdown when the reader enters the scene.
timer 30 // 30-second timer, no fallback
timer 30 default "Run away" // auto-fires the matching choice when it expires The number is in seconds and must be a positive integer. The optional default "Choice label" clause names the choice to auto-fire when the timer expires. The label resolves against the choices in the same source, so renaming a choice in the same edit still works. Omit the line entirely to clear any existing timer (absence is “no timer”, same convention as goes to).
<type> choice "…": Block
One per choice. The header always begins with the choice’s type so the source is explicit. Choice labels can use the same inline {condition: text} shorthand as text: body, so continue choice "tap {Has Rope = false: hint}": round-trips to a variable node inside the visual editor’s choice editor.
| Header form | Choice type |
|---|---|
continue choice "Label": | Continue (default, also moves to goes to target on click) |
interact choice "Label": | Interact (info button, does not advance the scene) |
reusable interact choice "Label": | Interact, reusable across visits |
back choice "Label": | Back (returns to the previous scene) |
input choice "Label" into VarName: | Input (player types a value into VarName, which must be a string variable) |
dropdown choice "Label": | Dropdown (one option per body line, each Var = Value as "Option label") |
reusable dropdown choice "Label": | Dropdown, reusable across visits |
dynamic choice "Eat {item}": | Dynamic (label and effects iterate over a tag; effects can use {propertyName}) |
Inside the body:
when EXPR(optional) - the choice’s visibility condition. Same operators asif, withand/or/parentheses.goes to "Scene Name"(optional, continue + input choices only) - the scene this choice navigates to. Click the scene name to jump to it inside the modal. Other choice types (interact, dropdown, back, dynamic) don’t expose a writer-set scene link.goes to new "Scene Name"(continue + input choices only) - shortcut for “create a new scene with this name and link this choice to it”. The new scene starts empty and inherits the current scene’s chapter and character restrictions. Two choices that usegoes to new "Same Name"in one save share one new scene. After the save, the new scene exists, and the editor drops thenewkeyword automatically. If a scene with that name already exists,newhas no effect and the editor shows a warning asking you to remove it (the choice still links to the existing scene, so auto-save keeps working).- Any number of effect lines, using the full effects grammar. Dropdown bodies must use
as "Label"on every line; dynamic bodies may use{propertyName}on the right-hand side.
Choices reorder, add, and delete by editing the source: the order of blocks becomes the order of the choice list, removing a block deletes the choice, and writing a new <type> choice "…": block adds one. Renaming and adding choices in the same edit still works.
A bare choice "Label": (no type) is treated as a continue choice, and the editor shows a warning asking you to add an explicit type.
Variables Grammar
Variables scope has one block per variable. The header declares the name, type, and starting default; optional indented attributes follow.
gold: number = 100
public
folder "Economy"
health: number = 100
damage: number = rand(+1 to +6)
hasKey: boolean = false
playerName: string = "Adventurer" // system, name + type are read-only
weapon: string = oneOf("Sword", "Bow", "Staff")
public
party_funds: number = 0
public
shared
folder "Economy"
notes: "Shared party gold. All players contribute/spend from this."
sword: string = "Iron Sword"
folder "Equipment"
tag "Weapon":
damage: 5
weight: 3 Header: name: type = value. The three legal types are string, number, boolean. The value can be a literal (100, "Alice", true/false), rand(+a to +b) for a random starting value (each bound may be a number variable, e.g. rand(minDmg to maxDmg)), roll(NdM+K) for dice, or oneOf(...) for a pick-one-at-random starting value. Formulas aren’t allowed for variable defaults (they’d have nothing to reference at character creation time).
Attributes (indented 2 spaces, one per line):
public- the variable is visible to players in the stats page.shared- in coop multiplayer, the variable is party-level (all players read and write the same value) instead of per-character.folder "Name"- groups the variable in the Variables panel. Must match the label shown in the visual panel.notes: "…"- writer’s private notes. Not visible to players.tag "Tag name":- assigns a tag to this variable. Indented 4 spaces inside the block, one property-override line per tag property. Only override the properties you want to change. Others fall back to the tag template’s default.
A trailing // system comment marks system variables (like playerName). Their name and type can’t be changed in the editor, but the other attributes are free to edit.
Renaming a variable keeps any effects and conditions that reference it working, as long as the new name is similar enough for the editor to match the two. Clearing a variable from the source deletes it (but only when you’re editing the full variable set, not a single-variable filter view).
Tag-property values (the lines under a tag "…": block) don’t appear as their own variables in the code. They only show up as indented property lines inside their parent variable, matching how the Variables panel hides them from the main list.
Saving
The editor saves automatically as you type, on a half-second debounce. There’s no Save button. The header uses the same save indicator as the visual editor. During a normal save it moves from Saving… to Saved. If the save fails, it shows Save failed. and keeps the latest unsynced draft in this browser so you can retry. If the server version changed, it shows Review needed. and opens Unsynced Editor Changes when clicked. Click Back when you’re done; any valid pending change is committed before the modal closes.
Auto-save pauses in two cases, both shown as “Unsaved changes” in the header (hover the indicator for a tooltip explaining why):
- The source has errors - the editor won’t commit a half-broken effects list or scene body. Fix the error and the save fires automatically.
- The buffer is empty - clearing every effect (or every scene-body line and on-enter effect) is blocked, in case you cleared the editor by accident. Restore something or close the modal to keep what was there before.
Error Handling
As you type, the editor checks your code and draws red squigglies under errors. Hover the squiggly for a tooltip. The status bar at the bottom shows the total effect count and error count; the error list below it jumps you to each one.
Examples Sidebar
The right-hand sidebar has snippet examples grouped by category. Clicking one inserts it at the cursor. The search box filters snippets by title, description, or content.
Scenes Sidebar (Scene Scope)
When you open the editor in scene scope, a left-hand panel lists every scene in the story. The currently-edited scene is highlighted, and a search box at the top filters by name. Click any row to switch the editor to that scene without closing the editor. Like the examples panel, you can collapse it if not needed.
You can also rename scenes from this panel, just like in a file system: double-click any row, or click the already-selected scene a second time after a short pause. An inline text field appears with the scene’s current explicit name (the field stays empty if you’ve never set one). Press Enter or click away to save, or Escape to cancel. Clearing the name and pressing Enter removes the explicit name, so the scene falls back to its first-line label.
Create a new scene with the + New scene button just below the search input. It expands an inline name field. Type an optional name, press Enter to create and jump into the new scene, or Escape to cancel. The new scene is empty and inherits this scene’s chapter and character restrictions, same as the visual editor’s new-scene flow. Unlike the visual editor’s “Link Scene” button (which always ties the new scene to a choice), this creates an orphaned scene. You wire it up afterwards, either by typing a goes to "…" line on a choice or by using goes to new "…" from the start.
Delete a scene by hovering its row and clicking the trash icon. This deletes only that scene. Scenes it connected to stay in your story as unreachable or orphaned scenes, and any links pointing to the deleted scene are removed. Deleting the currently-edited scene automatically hops you back to the root scene. The root scene does not show a trash icon and cannot be deleted.
If your current source has unsaved edits, the editor auto-commits them before switching scenes (or creating a new one), except when there are syntax errors, or when the auto-save would wipe the scene body or on-enter effects (e.g. you’ve cleared the editor). In those cases the hop or create is blocked and you’ll see a toast explaining why.
Clickable Scene References
Anywhere a scene is referenced by name - goes to "Scene Name" on a choice, or scene:"Scene Name" inside a condition - the label gets a dotted underline. Click it to jump to that scene.
Related
- Variables & Effects - the underlying data model that effects edit.
- Scene Editor - the rich click-driven view that the code editor mirrors.
- Tags & Dynamic Choices - context for the
{propertyName}syntax.