Story schema
| Field | Meaning |
|---|---|
id, start |
Story ID and initial node ID. |
variables |
Optional map of number, boolean or string defaults. Types stay fixed throughout the conversation. |
nodes |
Map of node IDs to {speaker?,text,choices?,next?,onEnter?}. |
choices |
Array of {id,text,target,when?,effects?,once?}. target:null ends immediately. |
next |
Next node for a line with no choices. Omit or set null to end when advanced. |
when |
All conditions must pass. Each is {variable,op,value}. |
effects, onEnter |
Array of `{op:'set' |
IDs use lowercase letters, digits, _, -, begin with a letter and have at most 64 characters. Reserved prototype keys are rejected. Story limits: 256 nodes, 64 variables, 12 choices per node, 16 conditions per choice, 32 effects per transition/entry list. Node text is up to 8,000 characters; speaker names up to 120; choice text up to 500. Numeric variables and results must be finite within ±1e9; strings up to 1,000 characters. Visits are capped at 1,000,000 per node.
Condition operators: eq, ne, gt, gte, lt, lte. Ordered comparisons and add require numbers. Comparisons and assignments must use the variable's declared type. Text interpolation supports {{variable}} and performs plain-text substitution.
{ id: 'buy', text: 'Buy tonic · 2 coins', target: 'thanks', once: true,
when: [{ variable: 'coins', op: 'gte', value: 2 }],
effects: [{ op: 'add', variable: 'coins', value: -2 }] }once is scoped to the node and choice ID for this conversation. Hidden branches are not skipped automatically. Disabled choices remain in view.choices with enabled:false; your UI decides how to present them. Always provide an exit when all choices could become unavailable. Graph cycles are allowed because progress is explicit; there is no automatic traversal loop.
Runtime API
| Method / getter | Behavior |
|---|---|
new Conversation(story) |
Validate, enter the start node and apply its entry effects once. |
validateStory(value) |
Return a normalized graph or throw. Checks targets, duplicate IDs, types and limits. |
view |
Detached current speaker/text/choices, nodeId, ended and canAdvance. |
variables, nodeId, ended |
Inspect current state; variables are copied. |
choose(choiceId) |
Recheck availability; apply choice and target entry effects together. |
advance() |
Follow next on a node without choices, or end at its final line. |
setVariables(values) |
Atomically update existing variables with values of their declared types. |
end() |
Explicitly end the conversation. |
subscribe(listener) |
Notify after a state change; returns unsubscribe. |
toSnapshot() |
Return an cranberry-forge.chatter/1 save. |
Conversation.fromSnapshot(story,save) |
Resume without replaying entry or choice effects. |
Actions return {ok:true} or {ok:false,reason} for ended, not-found, unavailable, or choice-required. Invalid schemas, argument values or overflowing effects throw. Failed effect transitions leave node, variables, visits and one-time choices unchanged. Subscriber errors are logged after commit. Avoid reentrant writes in a subscriber.