Appearance
Navigation, Variables, Conditionals & Links
The core authoring primitives: moving between passages, reading and writing state, branching, and building clickable choices.
Navigation
These are the three render tiers. Every (link:) eventually calls one of these.
(goto: PassageName) / (goto: PassageName, transition)
Full scene change. Clears all zones, loads a new layout if needed, rebuilds everything from the target passage. Use this when the player moves to a new location or a major scene shift happens.
An optional second positional arg names a scene transition. If omitted, the default from (default-transition: goto, ...) is used (or none if not configured). See Transitions for the full list of valid names.
Dynamic targets: the passage name can be a variable or an expression, not just a literal. (goto: $world.nextScene), (goto: _target), and string-built names like (goto: "Chapter_" + $world.chapter) all work; the argument is evaluated, then used as the passage name. This is how loops over quests/NPCs navigate to per-id passages. (Bare names like (goto: BarScene) are treated as literals; a _name temp that resolves to nothing falls back to the literal passage name _name.)
ana
(link: "Leave the bar")[(goto: MainStreet)]
(link: "Leave the bar")[(goto: MainStreet, fade)]
(goto: BarScene, dissolve)(update: PassageName) / (update: PassageName, transition)
Partial update. Updates only the zones that the destination passage writes to, usually text and options, but also sidebar_npc or sidebar_character if the passage calls (portrait:). Zones not written to are untouched. The background image is only replaced if the new passage explicitly calls (img:). Use this for continuing a conversation, advancing a scene, or showing new dialog within the same location.
An optional second positional arg names a zone transition applied to the text and options zones.
ana
(link: "Ask about the rumors")[(update: Bar_Rumors)]
(link: "Ask about the rumors")[(update: Bar_Rumors, dissolve)](action: PassageName)
Minimal reaction. Executes the target passage and updates only the specific zones it writes to. Nothing else changes. Use this for reactions, stat changes, and item events that don't change the scene.
ana
(link: "Pocket the coin")[(action: Player_TakeCoin)]
(link: "Drink the beer")[(action: Bar_DrinkBeer)]Variables
Variable depth. A $variable path has one to three segments:
| Depth | Form | Example | What it is |
|---|---|---|---|
| 1: bare/global | $name | $gameStarted | A single top-level variable |
| 2: flat namespace | $ns.key | $player.gold, $world.days | A key on a flat namespace; the namespace exists as soon as you declare a key in it, with no registration needed |
| 3: nested namespace | $ns.id.key | $npc.bartender.name | A key on an entry of a nested namespace ($npc, $container, or one you made with (ns-nested:)) |
Flat namespaces ($player, $world, $skill, and your own $vars) need no setup; (declare: $player.gold, 0) brings $player into being. Nested namespaces hold many ids, each with its own keys, and $npc/custom ones are created/declared with (npc-define:) / (declare: $ns.id, ...).
(set: variable to value)
Assigns a value to a variable. The prefix determines scope:
$variable: global variable, persists across passages and is saved. Must be declared inGameInit. Type mismatch is a hard runtime error.$ns.id.key: nested namespace entry (3-part path). Sets a single key on an NPC, container, or other registered entry._variable: temp variable, scoped to the current passage execution. Never saved, never carries over to child passages. Use for intermediate calculations.
ana
(set: $player.name to "Avery")
(set: $player.gold to 150)
(set: $world.timeOfDay to "evening")
// Nested entries use the 3-part path
(set: $npc.bartender.location to "market")
(set: $npc.bruce.attitude to "hostile")
(set: _roll to (random: 1, 100))
(if: _roll > 80)[
You succeed brilliantly.
]
(elseif: _roll > 40)[
You manage, barely.
]
(else:)[
You fail.
](add: target, value)
Adds a value to a variable or system collection. Behavior depends on target type:
- Numeric
$variable: addsvalueto the current number. If the variable has registered bounds (via(declare:)with min/max), the result is auto-clamped. - Array
$variable: appendsvalueto the end of the array. $inv: adds an item to carry inventory.$equip.slotName: equips an item UID to the named slot.$container.id: adds an item to a named container.
System namespaces (inv, equip, container, item) accept a $-prefix for consistency with regular variable syntax. Both forms are equivalent.
ana
(add: $player.health, 10) // auto-clamped if bounds registered
(add: $player.events, "met_kyle") // array push
(add: $inv, "beer", 2) // inventory
(add: $equip.weapon, _knife) // equipment
(add: $container.wardrobe, "coat", 1) // containerFor money, prefer the currency system's (earn:) / (pay:) over (add:) / (sub:); see Currency System.
Feedback messages: (add:) emits a feedback message by default: statchanges category for numeric variables ("gold +50 → 150"), items for $inv/$container adds ("+N item"), relationships for $rel. See the feedback behavior table. To customize or silence a single call, append a feedback pair:
ana
(add: $player.xp, 50) // "xp +50 → 150"
(add: $player.xp, 50, feedback, "Hard-won experience.") // custom message
(add: $player.xp, 50, feedback, false) // silent(sub: $variable, amount)
Subtracts amount from a numeric global variable. If the variable has registered bounds, the result is auto-clamped at the minimum. Cleaner to read than (add: $var, -n) for decrements.
ana
(sub: $player.health, 15) // take damage (auto-clamped at 0 if bounds set)
(sub: $rel.bartender, 10) // relationship worsens (auto-clamped)Feedback: (sub:) emits a statchanges (or relationships) feedback message by default, just like (add:). Use the same feedback, "message" / feedback, false pair to customize or silence it. For spending money, prefer (pay:) from the Currency System.
(random: min, max)
Expression macro. Returns a random integer between min and max inclusive. Use inside (set:) or a condition.
ana
(set: _roll to (random: 1, 20))
(if: _roll >= 15)[You succeed.]
(if: (random: 1, 6) is 6)[
You get lucky.
]Conditionals
(if:) / (elseif:) / (else:)
Only the matching branch executes; other branches are completely skipped, including any macros inside them.
(elseif:) and (else:) must follow a preceding (if:) or (elseif:) block, but can be on separate lines.
ana
(if: $player.gold > 100 and $player.reputation > 50)[
The merchant gives you a discount.
]
(elseif: $player.gold > 100)[
He nods. Full price.
]
(else:)[
He eyes you suspiciously.
]Operators available in conditions:
| Operator | Meaning |
|---|---|
is | Equal (===) |
is not | Not equal (!==) |
> < >= <= | Numeric comparison |
and | Both must be true; right side skipped if left is false |
or | Either must be true; right side skipped if left is true |
not | Prefix negation; inverts a boolean |
contains | Array or string includes check |
not contains | Array or string excludes check; alias for does not contain |
does not contain | Same as not contains; preferred for readability |
Arithmetic operators are available anywhere an expression is (inside (set:), conditions, macro args):
| Operator | Meaning |
|---|---|
+ | Addition; also string concatenation when either side is a string ("HP: " + $hp) |
- | Subtraction; also unary minus (-$debt) |
* / % | Multiply, divide (float), remainder |
Precedence, tightest first: unary minus → * / % → + - → comparisons (is, >, …) → not → and → or. Use parentheses to override. / is floating-point division (7 / 2 is 3.5; wrap in (floor:)/(round:) for integers).
ana
(set: _dmg to ($player.str * 2) + 5)
(set: _half to (floor: $player.gold / 2))
(if: $player.hits % 2 is 0)[An even number of hits.]Nesting works naturally:
ana
(if: $player.health > 50)[
(if: $player.gold > 100)[
You feel good — healthy and flush.
]
(else:)[
You feel fine, if broke.
]
]
(else:)[
You're hurt and probably not thinking straight.
](cond: c1, r1, c2, r2, ..., default?)
A switch-like expression: takes condition/result pairs and returns the first result whose condition is true. Conditions are evaluated lazily, in order, stopping at the first match. A trailing odd argument is the default returned when no condition matches (with no default and no match, returns null). Use it where (if:)/(elseif:)/(else:) would be verbose, to pick a value rather than emit a branch of content.
ana
(set: $status to (cond: $cash >= 1000, "loaded", $cash >= 500, "stable", $cash >= 100, "lean", "broke"))
Your (cond: $wonTheRace, "gasps of triumph", "wheezes of defeat") drown out all other noise.Links
(link: "Label")[action]
Renders a clickable button. The action inside the brackets must be a navigation macro: (goto:), (update:), or (action:).
ana
(link: "Enter the bar")[(goto: BarScene)]
(link: "Ask about the rumors")[(update: Bar_Rumors)]
(link: "Pocket the coin")[(action: Player_TakeCoin)]Accessibility: Add aria, "label" positional pair to set an aria-label on the button for screen readers.
ana
(link: "→", aria, "Go to the bar")[(goto: BarScene)]Tooltip: add a tip, "text" pair for a hover/focus tooltip; for a rich tooltip (images, multiple lines) wrap the link in (tooltip:) instead; see Tooltips.
ana
(link: "the locket", tip, "A tarnished silver locket.")[(goto: LocketScene)]Inline reveal: omit the navigation macro for text that expands in place. The expanded text is not a new passage; it replaces the link in the current zone.
ana
(link: "Inspect the painting")[
A pastoral scene. Someone has drawn a mustache on the shepherd.
]Link body execution rules: the block inside (link:)[...] is more flexible than a single navigation macro.
(set: $global to value)preEffects are special: they run at render time (when the passage first renders), not at click time. Use them to capture temp variables from loops into globals before the click fires. They are applied to state just before navigation.- Other non-navigation macros (
(add:),(notify:), etc.) run at click time, after the link is clicked. - Temp variables are not in scope at click time. If your link body references
_tempVar, that variable no longer exists when the click fires. Use a(set: $global to _tempVar)preEffect to capture it at render time.
ana
// Pattern: capture temp var at render time, use global at click time
(link: "Equip")[
(set: $world.selected to _id)
(action: _Inv_EquipSelected)
]
// Simple case: no temp vars, macros defer naturally to click time
(link: "Buy a drink")[(pay: 5)(action: Bar_BuyDrink)]
// Inline reveal — no navigation, block executes on click in-place
(link: "Inspect painting")[
A pastoral scene. Someone has drawn a mustache on the shepherd.
]Link Variants
(link-if: condition, "text")[body]
Renders a link only when condition is truthy. If false, emits nothing: no empty button, no placeholder.
ana
(link-if: $player.level > 5, "Advanced Technique")[(goto: AdvancedFight)]
(link-if: (quest-is: "find_thief", active), "Report your findings")[(goto: Bar_QuestReturn)](link-once: "text")[body] and (link-once: "text", id, "my_id")[body]
A one-time clickable link. After it is clicked the first time, it disappears on all subsequent visits to the passage. This is persistent: it survives save/load.
Without an explicit id, the engine generates one automatically from the passage name and the occurrence order. Use an explicit id when the same link might move around, or when you need to reference it from (link-seen:).
ana
(link-once: "Read the notice")[(goto: NoticeBoard)]
(link-once: "Pick up the coin", id, "pick_coin")[(action: PickupCoin)](link-seen: "id") → boolean
Returns true if a (link-once:) with that explicit id has been clicked.
ana
(if: (link-seen: "pick_coin"))[You already picked up the coin.](link-complete: "id")
Force-marks a (link-once:) ID as seen, without requiring the player to click it. Useful for scripted completion (tutorial skips, debug tools).
ana
(link-complete: "pick_coin")Debugger: The Links tab in the dev debugger shows all seen link IDs and supports filtering.
(link-choice: "PassageName", "choice A", "choice B", ...)
Renders one link per choice string. All links navigate to the same passage. Before navigating, the engine sets $link.choice to the selected string, and the destination passage reads it to branch.
$link.choice is auto-declared as "" on boot; no GameInit entry needed.
ana
// In the current passage:
How do you respond?
(link-choice: BartenderReply, "Charm him", "Be direct", "Stay quiet")
// In BartenderReply:
(if: $link.choice is "Charm him")[You flash your best smile...]
(if: $link.choice is "Be direct")[You get straight to the point...]
(if: $link.choice is "Stay quiet")[You say nothing...]Use cases: dialogue tone selection, approach choices feeding the same scene, reusable "how do you respond?" passages.
(link-cycle: $var, "opt1", "opt2", ...)
A cycling link. Each click advances $var to the next option in the list and updates the button label. Wraps around after the last option.
If $var is unset when the passage renders, it is initialized to the first option. If its current value is not in the list, it also falls back to the first option.
ana
(link-cycle: $player.hair, "Black", "Brown", "Red", "Blonde", "White")
(link-cycle: $combat.stance, "Aggressive", "Balanced", "Defensive")Reactive zones watching $var update automatically after each click.
Use cases: character creation toggles, per-session preference selectors, repeated configuration choices that stay visible.
(link-repeat: "text")[body]
A link that stays clickable after each use. Unlike (link-once:), the link remains active after firing. The block body executes on every click.
(link-repeat:) supports two patterns:
- Navigation: include a navigation macro (
(goto:),(update:),(action:)) to navigate on each click. - Inline execution: omit the navigation macro; the body executes in-place on each click (state mutations, notifications, etc.).
ana
(link-repeat: "+1")[(add: $skill.persuasion, 1)]
(link-repeat: "-1")[(sub: $skill.persuasion, 1)]
(link-repeat: "Train")[(add: $player.xp, 10)(action: Training_SFX)]
// Navigation form — fires the action passage on each click
(link-repeat: "Rest")[(action: Player_Rest)]State mutations (set, add, sub, action) in the body execute normally. Reactive zones watching any affected variables update after each click.
Use cases: stat-allocation screens, repeated inline actions (pick up, craft, train), any button that should remain available after use.