ScryMaster Docs

Bindings & the data tree

Anywhere you write narration you can drop a {{binding}} that resolves, at play time, from the live game state — the hero’s name and HP, the last damage they took, how the last fight went, a story flag you set three scenes ago. One scene, many shapes. This is what lets you write reactive, “the DM remembers” prose without branching to a new node every time.

Type {{ in any narration field and an autocomplete opens, listing every path the engine can resolve, grouped by area (Character · Node · Event · Story…). You never have to guess a path — pick it from the list.

The data tree (what you can reference)

The state is a nested tree with a few roots. The common leaves:

  • character.*name, level, class, subclass, species, hp / maxHp, ac, strcha, purse.gp / purse.totalCp, inventory.count. Deep forms exist too: character.stats.hp.current, character.abilities.str.mod.
  • event.* — the most recent thing that happened: event.lastRoll.total, event.lastDamage.amount / event.lastDamage.type, event.lastHeal.amount. This is how you write “the fall opens a gash ({{event.lastDamage.amount}} damage).”
  • battle.* — post-fight stats you can react to: battle.hero_dealt, battle.ally_dealt.
  • node.*node.title, node.visits (how many times the player has been here).
  • story.* — every variable you declare (flags, counters, clocks). A flag met_mira reads as {{met_mira}} or story.met_mira.

Two kinds of binding

A plain path{{character.hp}} — prints the value. An expression — anything with an operator or a ternary — computes one:

You stand at {{character.hp}} of {{character.maxHp}} —
{{character.hp < character.maxHp ? "wounded and wary" : "hale and ready"}}.

{{met_mira ? "She nods as you enter." : "A stranger watches from the bar."}}

Expressions support ternaries (a ? b : c), comparisons (== != < > <= >=), and || / &&. They are pure — no side effects — so they’re safe and always replay the same way.

One syntax: double braces. If you open an older adventure you may see a single-brace form —{flag? A | B} for conditional text or a bare {token}. That was an earlier system; the engine still reads it so old published stories never break, but it is legacy. Write everything new as {{ flag ? "A" : "B" }} and {{token}} — the autocomplete and chips only work with double braces.

Best practice: collapse branches with expressions — until a branch needs its own bindings

Two near-identical scenes that differ by one line don’t need to be two nodes — fold them into one with a ternary:

// Before: two scene nodes (n_stays, n_parts)
// After: ONE node
{{mira_permanent ? "She clasps your arm. \"Lead on.\"" : "She melts into the dusk."}}

The limit to know: an expression’s quoted text is taken literally — a {{binding}} written inside the quotes will not resolve. So if one branch needs its own bindings (e.g. “you dealt {{battle.hero_dealt}}”), keep it as its own node, or chain several small bindings side by side instead of nesting:

// Good — small bindings in sequence, each resolves:
{{mira_debt ? "I pulled you up once today. " : ""}}{{character.name}}, {{battle.hero_dealt > battle.ally_dealt ? "you held your own." : "get some roads under you."}}

How bindings look in the editor vs. in play

In the Studio (and in Test play) a resolved binding is highlighted as a gold chip so you can see exactly which words are dynamic. In a real player’s run it renders as plain prose — it reads like the sentence around it, never like a code token. Hover any chip in the editor to see what it resolves to.

Bindings are presentation, not authority. They display state; they don’t enforce it. Gate rewards and branches with conditions and actions (which the engine resolves), not with what a binding happens to print.

Next: Triggers & actions →