Skip to content

Quality of life wrapper around bondage.js, a yarn language parser.

Notifications You must be signed in to change notification settings

mnbroatch/yarn-bound

Repository files navigation

What is YarnBound?

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.

Usage

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!

Available Options

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 Data

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"
    ]
  }

Full example

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.

Functions

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 */},
  }
})

History

An array containing Results already visited is located at runner.history.

React Component

A simple react component can be found at: react-dialogue-tree

Caveats

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:

Dialogue Ending With 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.

Other included versions

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.

About

Quality of life wrapper around bondage.js, a yarn language parser.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published