Yarn is a language for writing dialogue trees.
YarnBound attempts to be the simplest way to use the Yarn language in the context of a javascript application. It is a wrapper around a specific forked version of bondage.js, where effort has been made to comply with the Yarn 2.0 spec.
Quality-of-life features on top of bondage.js:
- A simpler API
- History of previous Results
- Option to return text and a subsequent options block together as one result
- Option to run a custom command handler function instead of returning a CommandResult
- include an
isDialogueEnd
property with the last Result in a dialogue
Bondage.js also does not support
Character: some text
annotation[b]Markup[/b]
because these are not language features (it's confusing). YarnBound adds these things.
The only thing I know to be missing from the spec and non-unity-specific docs (at time of Yarn 2.0's release) is the built-in wait
command, because I can't tell what I would want it to do.
Install with npm i -S yarn-bound
or grab yarn-bound.js
from the /dist/
folder.
For information on how to write Yarn, visit the official documentation. Start there! YarnBound is useful after you have a yarn dialogue written and in string format. It's worth skimming the Yarn language spec as well.
To get started with YarnBound, import and create a new instance.
import YarnBound from 'yarn-bound'
// or node:
// const bondage = require('yarn-bound')
// or in a script tag:
// <script src="path-to-file/yarn-bound.min.js"></script>
const runner = new YarnBound(options)
You can then access the first Result with:
runner.currentResult
To continue the dialogue, call advance()
.
runner.advance()
runner.currentResult // is now the next Result
If you were on an Options Result, you would call advance()
with the index of the desired option.
runner.advance(2)
runner.currentResult // is now the Result after the selected option
You can also call jump()
with the title
of a node to jump directly to that node. This allows for external control over the story flow.
That's all there is to the basic operation!
dialogue (required): string - The Yarn dialogue to run. A .yarn file in string form.
startAt: string: - The title of the node to start the dialogue on.
- default: "Start"
functions: object - An object containing custom functions to run when they are called in a yarn expression.
- As the Yarn docs mention, these should not have side effects. They may execute at unexpected times.
- You can also use
runner.registerFunction(key, func)
after initialization.
variableStorage: object - A custom storage object with get()
and set()
functions (a new Map()
, for instance.)
- Unless you have a specific need you can omit this and use the built-in default.
- One use is supplying variables with initial values, though you could also do that in the dialogue.
handleCommand: function - If you provide this, YarnBound will advance()
right past Command Results, instead calling handleCommand()
with the Command Result as the single argument (see below for the data structure).
combineTextAndOptionsResults: boolean - If this is true, a Text Result followed by an Options Result will be combined into one Options Result with a text
property.
- This is convenient if you want to show prompts and responses at the same time.
locale: string - Used for pluralization markdown attributes.
Results, found on runner.currentResult
, come in three flavors:
- TextResult
- OptionsResult
- CommandResult
You can tell which kind it is by using instanceof
runner.currentResult instanceof YarnBound.TextResult
runner.currentResult instanceof YarnBound.OptionsResult
runner.currentResult instanceof YarnBound.CommandResult
A TextResult looks like this:
{
"text": "This is a line of text.",
"hashtags": ['someHashtag'],
"metadata": {/* see below */}
}
If it is the last Result in the dialogue, there will also be a isDialogueEnd
property with the value of true
.
An Options Result looks like this:
{
"options": [
{
"text": "Red",
"isAvailable": true,
"hashtags": []
},
{
"text": "Blue",
"isAvailable": true,
"hashtags": []
}
],
"metadata": {/* see below */}
}
If combineTextAndOptionsResults
is enabled, there could be a text
property on the Options Result. advance()
from it with an option's index as usual.
A Command Result looks like this:
{
"command": "someCommand",
"hashtags": [],
"metadata": {/* see below */}
}
It could also have a isDialogueEnd
property on it. Command Results are handled and skipped through when a handleCommand
function is supplied.
Every Result contains metadata
which includes node header tags including title
, and also any file tags.
{
"title": "StartingNode",
"someTag": "someTag",
"filetags": [
"someFiletag"
]
}
Let's start with this code:
import YarnBound from 'yarn-bound';
// empty lines and uniform leading whitespace are trimmed
// so you can write your nodes in template strings neatly.
const dialogue = `
title: WhereAreYou
---
Where are you?
-> Home
Nice.
<<doSomething home>>
-> Work
Rough.
<<doSomething work>>
That's it!
===
`
const runner = new YarnBound({
dialogue,
startAt: 'WhereAreYou'
})
When we log out runner.currentResult
above, we will get a TextResult with the text
"Where are you?"
to continue, we call
runner.advance()
and runner.currentResult
will be an OptionsResult with an options array with two objects in it. One object's text
property is "Nice" and one's is "Rough". We will choose "Nice" by calling:
runner.advance(0)
Now, runner.currentResult
is a CommandResult where command
is "doSomething home".
runner.advance()
One final TextResult, but this time isDialogueEnd
is true
.
If combineTextAndOptionsResults
was true, the first value for runner.currentResult
would be the same OptionsResult as above, but with a text
property that says "Where are you?".
If a handleCommand
function was supplied, it would be called and we would skip straight from the OptionsResult to the last TextResult.
If you are supplying a functions
object, it would look like this:
const runner = new YarnBound({
dialogue,
functions: {
someFunction: (arg) => {/* do stuff */},
someOtherFunction: (arg1, arg2) => {/* do other stuff */},
}
})
An array containing Results already visited is located at runner.history
.
A simple react component can be found at: react-dialogue-tree
When a handleCommand function is supplied, ending a dialogue with a command or set of commands can cause unexpected behavior. Any commands at the end of the dialogue will be handled upon reaching the last non-command result (usually, a command will only be handled when advancing past the previous non-command result). This is furthermore a problem when ending the dialogue with a command inside a conditional:
The lookahead feature won't handle commands or set variables, so a dialogue like this will behave inconsistently:
<<set $a = false>>
Hello
<<set $a = true>>
<<some command>>
<<if $a == true>>
Goodbye
<<endif>>
When processing the "Hello" TextResult, the lookahead feature will detect the end of the dialogue because it will see if $a == true
evaluate to false, since it hasn't.
Thinking that the dialogue is over, it will try to handle any trailing commands, actually doing the variable setting and this time reaching the if $a == true
block.
YarnBound will correct itself and not include the isDialogueEnd
flag prematurely, but any side effects performed by setting the variable and handling the command will have already happened. This can cause bugs!
It is suggested to avoid this pattern. But if you must use it, or prevent lookahead for any other reason, there is a special <<pause>>
command you can use:
<<set $a = false>>
Hello
<<set $a = true>>
<<some command>>
<<pause>>
<<if $a == true>>
Goodbye
<<endif>>
Note that you will have to call advance() again after reaching the pause command. Even if you have supplied a handleCommand function, runner.currentResult
will be a CommandResult, with a command of "pause".
Also note, however, that a "pause" command at the end of a dialogue will never have the isDialogueEnd
flag, as lookahead is disabled.
A minified version exists at yarn-bound/dist/yarn-bound.min.js
.
If you want to transpile for yourself, use import YarnBound from 'yarn-bound/src/index'
and make sure your transpiler isn't ignoring it. You will also need to transpile @mnbroatch/bondage
, and include both in your bundle, if necessary.
A version compatibile with internet explorer is at yarn-bound/dist/yarn-bound.ie.js
.