An Ink/Javascript Storylet Testbed

Ian Thomas
5 min readDec 29, 2023

--

TL;DR — a simple Storylet implementation for Ink in Javascript. You can find the code here.

I cleaned up some old Javascript code for storylets written in Ink and published it on Github.

Rather than re-invent the wheel, I write about my Ink implementation of storylets over here in my post about a Unity storylet framework. Probably best to read that first. But for a quick summary:

Storylets are individual chunks of story that have a condition attached to them saying “am I available for the player to play now?” A useful way to think about them is like a deck of cards, and the currently playable storylets as a hand of cards.

After each storylet is played, you recalculate your new hand of cards, because the storylet you just played might have affected the game state, and so might affect the condition for each storylet.

Basic Storylet Format

My core Ink implementation of storylets — as described in the Unity article — is very lean and looks something like this:

=== function _story_1() ===
~ return true

=== story_1 ===
This is story 1.
~ some_variable = true
-> END


=== function _story_2() ===
// This will only be available if some_variable is set!
~ return some_variable

=== story_2 ===
Hey, this is story 2.
-> END

A few quick things:

  • Each storylet (e.g. story_1, story_2) has a condition function above it next to it, which determines if that storylet can be played or not. It’s just the name of the storylet with an underscore in front of it.
  • It’s not obvious, but if there is no function, it’s assumed to be true.
  • If a storylet has been used up, it’s not available again. i.e. that card has been played.

Javascript Additions

The Javascript implementation fattens up that format a bit.

Repeating Storylets

In the simple version, if a storylet has been used up, it’s not available again. In the Javascript implementation if you set the tag #st-repeat to true, it will always repeat (unless the condition function is fale).

=== function _story_2() ===
// This will only be available if some_variable is set!
~ return some_variable

=== story_2 ===
#st-repeat: true
Hey, this is story 2.
-> END

Decks

In the simple version, storylets are added through Unity using a prefix e.g. storyletsManager.AddStorylets(“story_”) would add all the storylets in the example above.

In the JS version, storylets are divided into decks. Different decks just have different prefixes. For convenience, I put them in different Ink files.

// No condition function means it defaults to true
=== deck1_intro ===
Hey, this is an intro storylet.
-> END

=== function _deck1_somethingorother() ===
~ return true
=== deck1_somethingorother ===
Hi, this is a storylet.
-> END
=== deck2_intro ===
Hey, this is an intro storylet for deck2.
-> END

=== deck2_somethingorother ===
Hi, this is a storylet in deck 2.
-> END

And to add them you call the Ink/JS function to add the deck at the top of each file:

~ add_deck("deck1")

// No condition function means it defaults to true
=== deck1_intro ===
Hey, this is an intro storylet.
-> END

=== function _deck1_somethingorother() ===
~ return true
=== deck1_somethingorother ===
Hi, this is a storylet.
-> END
~ add_deck("deck2")

=== deck2_intro ===
Hey, this is an intro storylet for deck2.
-> END

=== deck2_somethingorother ===
Hi, this is a storylet in deck 2.
-> END

You can also add a condition function that applies to the whole deck, like so:

~ add_deck("deck1")

=== function _deck1() ===
// return true only if you are a priest
~ return is_class_priest;

// No condition function means it defaults to true
=== deck1_intro ===
Hey, this is an intro storylet.
-> END

=== function _deck1_somethingorother() ===
~ return true
=== deck1_somethingorother ===
Hi, this is a storylet.
-> END
~ add_deck("deck2")

=== function _deck2() ===
// return true only if you are not a wizard
~ return not is_class_wizard;

=== deck2_intro ===
Hey, this is an intro storylet for deck2.
-> END

=== deck2_somethingorother ===
Hi, this is a storylet in deck 2.
-> END

These deck condition functions are tested before everything else e.g. if a deck function is false, none of the storylets in that deck are tested. This is useful for whole swathes of content that depend on major player choices or skills or progression variables.

If there is no deck function, it is assumed to be true.

A Simple Javascript Test Harness

Take a look at the code in the repo — there’s a very lightweight Javascript implementation (via web/index.html) which is running story.js exported by Inky into the same folder.

The storylets source code are stored in the folder content/storylets. That’s where you’ll see the different decks — the filenames are arbitrary and are included in content/storylets.ink.

The main ink file is in engine/main.ink — that’s the thing to open in Inky, then export to web/story.js.

You’ll need to run it via a local web server as a web browser won’t work with local file access. Google “start a local web server for testing” if you’re not sure how to do that.

It’s very first-draft and very very simple. But will let you write sets of storylest, the logic that connects them, and see how they play together.

Under The Hood

The file engine/storylets.js does the bulk of the work and may be useful to you in other things.

The core class in Storylets. You create an instance by passing it a parsed Ink story, like so:

// Load Ink story.
var story = new inkjs.Story(storyContent);

// Set up Storylets
var storylets = new Storylets(story);

That initialises the Ink, finds all the relevant decks etc. Here are some useful snippets:

// Do this when the storylet availability check is completed
storylets.onUpdated = myCustomStoryletsReadyFunction;
// Recalculates which storylets are currently available. This
// works across several frames, instead of choking everything.
// Once done, it will call back via storylets.onUpdated, above...
storylets.StartUpdate();

Then once the update is complete:

// Once an update has finished, this returns a list of 
// storylet names as strings, which you can then choose between...
var storyletNames = storylets.GetAvailable()

// Then you can tell the storylet engine to pick one. This moves
// Ink to this path and starts executing..
storylets.ChooseStorylet(storyletName);

Then run Ink normally via story.Continue() until you run out of content or make choices or whatever you want to do… and once you’ve run out of Ink, call StartUpdate() and get a new list of available storylets.

That’s All Folks!

I hope it’s useful for something! Let me know if so.

And kudos as ever to Joseph at Inkle for all his hard work on Ink!

--

--

Ian Thomas

Ian is narrative director, coder, and writer of video games, films, larp, books, and VR/AR experiences. He has worked on well over 100 titles.