Defining stories, nodes, etc.

A story.kts file is a Kotlin script file, meaning that we will be using Kotlin code throughout this entire guide.

It is recommended that you use the template for code autocompletion.

  1. Story blocks
  2. Node blocks
    1. Options
    2. Option-less nodes
  3. Full example
  4. Changing the initial node

Story blocks

A story.kts file consists in one (or more) story declarations. Stories are defined using the following syntax.

story {
    // The story's content goes here
}

The line beginning with // is a comment, meaning that it is just text we can use to explain our code.

This story { } is called a story block. We can define some story properties inside the story block.

story {
    title = "My great story!"
    author = "Me"
    id = "main"
}

This will set a title and an author for our story. You do not have to specify these, but you should give your story a title.

The id is only necessary if you plan on having multiple stories at the same time.

Node blocks

We can now define nodes inside of our story. Nodes are defined through node blocks:

story {
    title = ...
    author = ...

    node(1) {
        // The node's content goes here
    }
}

Here, we are declaring a node with the ID 1. IDs are unique within a story, meaning that two nodes inside the same story may never have the same ID. However, two nodes from two different stories may have the same ID.

IDs can be strings…

node("node id") {
    ...
}

… or integers

node(42) {
    ...
}

You can only define nodes inside story blocks. From now on, code examples of nodes will be given assuming that we are inside a story block.

Advanced

Integers are internally converted to string IDs. Make sure to not have an integer ID that corresponds to another node’s string ID (e.g. a node(1) and another node("1") are internally seen as the same nodes)

By default, stories start at the node with ID 1. That means that your story.kts file is expected to have either a node(1) or a node("1"). We’ll see how to change that in a later section.

Next, we need to add some text to our node. We do this using a body block.

node(1) {
    body {
        """
        This is my node's body text

        As you can see, it is a pretty awesome node
        """
    }
}

The body block consists in a multiline string that is formatted in Markdown. The indentation before the lines does not matter and will be removed (although it needs to be consistent).

IntelliJ IDEA will automatically generate a .trimIndent() when you write multiline strings like these. You should remove them, as this is always internally called by StoryFX.

Why?

String literals in Kotlin are the multi-line strings like the ones you write using story.kts. However, these strings are “raw”, meaning that they also include the indentation you have before them. While you could remove the indentation, it would just make your code look like crap, which is not the goal here. This is why it is necessary to, at some point, remove the indentation. This is usually done by calling .trimIndent() on the multiline string, which is why IntelliJ automatically adds it in for you. There is one issue though, and it is that it ends up adding noise to the file. In order to reduce said noise, the .trimIndent() call is automatically performed by StoryFX before showing the node. Thus, you should remove it from your story.kts file, as it is entirely useless.

Options

Options are what allows a player to go from one node to another. They are defined inside node blocks.

node(1) {
    body {
        ...
    }

    option { "Go to the second node" }
}

node(2) {
    ...
}

We are also defining a second node with id 2 to have somewhere to go to.

The option block we just defined will thus display the text “Go to the second node”. However, we have not told it what node to go to yet. This is done using the goesTo function.

option { "Go to the second node" } goesTo 2

Our option will thus go to the node which has the ID 2. goesTo is a shortcut which you can use for the simplest cases, such as when the node has a known ID, and it is in the same story as the node the option is from.

Advanced

goesTo can also take a lambda providing an ID, for example: goesTo { 2 }. This allows to choose a node depending on variables. This topic is detailed here

For full customizability, use the does block. The equivalent to the goesTo we just saw above is:

option { "Go to the second node" } does { nodeRef(2) }

The does block is a sequence of code that is executed when our option is chosen, hence its name: it’s what the option “does”. Here, the code simply returns the node in our story with id 2, just like what we had with the goesTo.

nodeRef(2) returns the node with the id 2 in our story. Be very careful not to use node(2) here, as that would define a brand new node, whereas we just want to go to a node that is already defined. nodeRef means “node reference”, you can interpret it as “reference to node with id”.

An option that does not have a does block, or that has a does block that returns null will simply re-go to the current node.

Advanced

There are multiple ways to define nodes, some better than others depending on the context. The syntax we are describing here is most appropriate when defining nodes that have options or other dynamic elements. However, nodes that only consist of text (e.g. final nodes, from which the player has nowhere to go to) should use a slightly different syntax that takes the body block out of the node block.

Option-less nodes

Option-less nodes are final nodes, meaning that they represent an “ending” in your story.

Following what you have seen above, option-less nodes would be written like this:

node(9001) {
    body {
        """
        This is my last node

        It's over **9000**!
        """
    }
}

There is actually an easier way of writing option-less nodes:

node(9001) body {
    """
    This is my last node

    It's over **9000**!
    """
}

We can put the body block right next to the node’s declaration instead of having to specify it inside the node block. Don’t forget to put the body, as otherwise you would just be throwing a string in the node block, which would have no effect!

Full example

Here is a full example of a story.kts story

story {
    title = "Sample story"
    author = "Me"

    node(1) {
        body {
            """
            This is my node's body text.

            As you can see, it is a pretty awesome node
            """
        }
        option { "Go to my second node" } goesTo 2
        option { "Go to my third node, named Three" } goesTo "Three"
        option { "Go to my third node, named 3" } goesTo 3
        option { "Let's just stay here for now" } does { null }
    }

    node(2) body {
        """
        Congrats, you reached the second node!
        """
    }

    node(3) {
        body {
            """
            Congrats, this is node 3! Like, the number 3! Hurray!
            """
        }
        option { "Go to node Three but with letters" } goesTo "Three"
    }

    node("Three") body {
        """
        This is node Three, with letters instead of a number!
        """
    }

}

Note

Simple stories like the one above should never be written in the story.kts format, because it is completely overkill. They should instead be written using the much simpler and writer-oriented story.txt format.

Changing the initial node

If you want your story to start at a node other than the node with id 1, you can use the following in your story block to change the initial node.

story {
    initialNode { ... }
    ...
}

The initialNode is expected to be the same thing as a does block: it has to return a node in the end. For almost all cases, you’re good to go with something like this, where id is either a string or an integer:

initialNode { nodeRef(id) }