Inkling
Partial implementation of the Ink
markup language for branching game stories
and dialogue in Rust.
Ink
is a creation of Inkle. For more information
about the language, see their website.
This book is a user’s guide to the language. It is written with the intent to be a mostly friendly and accessible explanation of getting it up and running. The more advanced documentation of the API is available at docs.rs.
License
inkling
is copyleft, licensed under the Parity License. The code is available
on Github. Contact us about the possibility
of acquiring a private license.
Introduction
When writing a story for a game one has to consider how to get the script into it. This is simple if the script is in plain text, but depending on our game we might need to account for many possibilities, including but not limited to:
- Using variables for names or items which are declared elsewhere
- Branching the story into different paths
- Testing conditions for presenting certain parts
- Marking up content for emphasis or effect
Ink
is a scripting language which implements many of these features in its design.
inkling
is a library which can read this language and present the final text
to the reader. This chapter will introduce what the language is and how inkling
works with it.
What is Ink?
Ink
is a scripting language created by game developer Inkle
to use in their many narrative driven games, including 80 Days,
Heaven’s Vault
and Pendragon.
It is designed to make it easy to write a script in regular text files, without the need for complicated tools (although Inkle does provide such tools). Since their games are narrative heavy, with dialogue and paths which change as the player moves through them, it has excellent support for branching the story into different parts. All this while still being very easy to read.
More information about the language can be found on Inkle’s web site. This includes several tutorials for getting familiar with both basic and advanced features of the language.
What is Inkling?
inkling
is a library which reads stories written in Ink
and presents their content
to the user. It is an interface, not a game engine: it validates the script and
returns the text, but it is up to the caller to take that text and use it however
they want in their game.
Why use inkling
?
- Simple interface
- Rust native
- Support for non-latin alphabets in identifiers
- Few dependencies: optional
serde
dependency for easy saving and loading, and optionalrand
dependency for adding randomized features
Why not?
- Fewer features than Inkle’s implementation of the language
- Untested in serious work loads and large scripts: expect bugs
- Written by a hobbyist, who cannot promise quick fixes or support
Short example
This section provides an example of a script and how to read it into your program
with inkling
.
Script
// This is an `ink` script, saved as 'story.ink'
A single candle flickered by my side.
Pen in hand I procured a blank letter.
* "Dear Guillaume"
Sparing the more unfavorable details from him, I requested his aid.
-> guillaume_arrives
* "To the Fiendish Impostor"
-> write_to_laurent
=== guillaume_arrives ===
A few days later my servant informed me of Guillaume's arrival.
I met with him in the lounge.
=== write_to_laurent ===
The letter was spiked with insults and veiled threats.
Rust code
extern crate inkling;
use std::fs::read_to_string;
use inkling::read_story_from_string;
// Read the script into memory
let story_content = read_to_string("story.ink").unwrap();
// Read the story from the script
let mut story = read_story_from_string(&story_content).unwrap();
The next chapter will explain how to proceed from here.
Using Inkling
This chapter goes into more detail how to use inkling
in your program. It also comes
with a couple of examples of how run it.
If you want to dive even deeper into the functions, structures and methods, please check out the full documentation.
Set-up
To use inkling
in your Rust project, add this line to your dependencies in Cargo.toml
:
[dependencies]
#
inkling = "1.0.0-pre.1"
By default, inkling
has no additional dependencies. Extra features which carry dependencies
can be opted in to.
Adding serde
support
The serde
library is widely used to serialize and deserialize data, which can be used
to save and restore an object. Support for this can be added to inkling
by enabling the serde_support
feature. This adds serde
as a dependency.
[dependencies]
#
inkling = { version = "1.0.0-pre.1", features = ["serde_support"] }
Randomization support
The Ink
language supports a few randomized features like shuffle sequences.
These are optional and can be enabled with the random
feature. This adds
a dependency to rand
and its sub project rand_chacha
.
[dependencies]
#
inkling = { version = "1.0.0-pre.1", features = ["random"] }
If this feature is not enabled, shuffle sequences will behave as cycle sequences.
Reading a script
Let us now introduce how to move through a script with inkling
. We will reuse the script from the short introductory example:
#![allow(unused)] fn main() { let content = r#" A single candle flickered by my side. Pen in hand I procured a blank letter. * "Dear Guillaume" Sparing the more unfavorable details from him, I requested his aid. * "To the Fiendish Impostor" "#; }
Reading text content
To parse your script into a story, inkling
provides the read_story_from_string
function. It takes the script (as a string) and from that returns a Story
object in a Result
.
Again, we parse it into a story:
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Story}; let content = r#" A single candle flickered by my side. Pen in hand I procured a blank letter. * "Dear Guillaume" Sparing the more unfavorable details from him, I requested his aid. * "To the Fiendish Impostor" "#; let mut story: Story = read_story_from_string(&content).unwrap(); }
Aside: The Story
object
Story
contains the entire parsed script in a form that is ready to be used.
It implements a wealth of methods which can be used to go through it or modify its
state by setting variables and changing locations. Look through the documentation
for the object for more information about these methods.
Starting the story
To start the story we must supply a buffer which it can add text lines into. The story will then proceed until a set of choices is encountered, which the user has to select from.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Story}; let content = r#" A single candle flickered by my side. Pen in hand I procured a blank letter. * "Dear Guillaume" Sparing the more unfavorable details from him, I requested his aid. * "To the Fiendish Impostor" "#; let mut story: Story = read_story_from_string(&content).unwrap(); use inkling::Line; // Buffer which the text lines will be added to let mut line_buffer: Vec<Line> = Vec::new(); // Begin the story by calling `resume` let result = story.resume(&mut line_buffer).unwrap(); // The two first lines have now been added to the buffer assert_eq!(line_buffer[0].text, "A single candle flickered by my side.\n"); assert_eq!(line_buffer[1].text, "Pen in hand I procured a blank letter.\n"); assert_eq!(line_buffer.len(), 2); }
Note that the lines end with newline characters to denote that they are separate
paragraphs. Oh, and the text lines are of type Line
, which contains
two fields: text
(seen above) and tags
for tags
which are associated with the line.
Encountering choices
The story returned once it encountered the choice of whom to pen a letter to.
This set of choices is present in the returned object, which is an enum
of type Prompt
. We can access the choices (which are of type
Choice
) through this object.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Story, Prompt}; let content = r#" A single candle flickered by my side. Pen in hand I procured a blank letter. * "Dear Guillaume" Sparing the more unfavorable details from him, I requested his aid. * "To the Fiendish Impostor" "#; let mut story: Story = read_story_from_string(&content).unwrap(); let mut line_buffer = Vec::new(); let result = story.resume(&mut line_buffer).unwrap(); match result { Prompt::Choice(choices) => { assert_eq!(choices[0].text, r#""Dear Guillaume""#); assert_eq!(choices[1].text, r#""To the Fiendish Impostor""#); assert_eq!(choices.len(), 2); } Done => (), } }
To continue the story we use the make_choice
method with an index corresponding
to Choice
made.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Story, Prompt}; let content = r#" A single candle flickered by my side. Pen in hand I procured a blank letter. * "Dear Guillaume" Sparing the more unfavorable details from him, I requested his aid. * "To the Fiendish Impostor" "#; let mut story: Story = read_story_from_string(&content).unwrap(); let mut line_buffer = Vec::new(); let result = story.resume(&mut line_buffer).unwrap(); story.make_choice(0).unwrap(); let result = story.resume(&mut line_buffer).unwrap(); assert!(line_buffer[2].text.starts_with(r#""Dear Guillaume""#)); assert!(line_buffer[3].text.starts_with("Sparing the more unfavorable details")); }
Note that inkling
does not clear the supplied buffer when resuming the story.
That task is trusted to you, if you need to, by running line_buffer.clear()
.
Summary
- Parse the story using
read_story_from_string
- Move through it with
resume
, which adds text to a buffer - Use
make_choice
to select a choice when hitting a branch, thenresume
again - Key objects:
Story
,Line
,Choice
andPrompt
Design intent
This library has been written with the intent to create a simple usage loop.
while let Prompt::Choice(choices) = story.resume(&mut buffer)? {
// Process text, show it to the player, then present the encountered
// choices to them and resume.
let i = select_choice(&choices)?;
story.make_choice(i)?;
}
The loop will finish when Prompt::Done
is returned from the resume
call,
signaling the end of the story. Here errors are returned through the standard
?
operator, which further simplifies the loop.
Of course, this pattern may not suit your application.
Helper functions
This page lists miscellaneous functions to deal with inkling
data.
Text handling
copy_lines_into_string
takes a buffer ofLine
objects and joins the text into a single string which is returned
Read error handling
print_read_error
creates a string with the information of all errors that were encountered when parsing a story
Inspecting the log
When the Story
is parsed it goes through all lines and scenes in the script,
inspecting them for inconsistencies and notes. If any errors are encountered the parsing fails,
and an error is returned.
However, some inconsistencies are not very serious and do not raise an error. Such issues are instead added to a log, which you can inspect after the parsing has finished. You can then decide whether any yielded warning is sufficient for further investigation.
We recommend that you always check this log and inspect its messages after parsing a story. Besides regular warnings, it will also contain reminders of any to-do comment you have added to the story, which may merit having a look at.
The log supports standard iterator operations, which makes it simple to walk through all warnings and to-do comments at once.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Story}; let content = r#" TODO: Should these names be in variables? A single candle flickered by my side. Pen in hand I procured a blank letter. * "Dear Guillaume" Sparing the more unfavorable details from him, I requested his aid. * "To the Fiendish Impostor" "#; let mut story: Story = read_story_from_string(&content).unwrap(); // Print all warnings and comments to standard error for inspection for message in story.log.iter() { eprintln!("{}", message); } assert_eq!(story.log.todo_comments.len(), 1); }
Dealing with errors
Errors from reading the Story
When parsing the Story
using read_story_from_string
, its content is validated.
This means that inkling
goes through it and looks for errors. These errors include
things like invalid knot or variable declarations, using invalid names for variables in
assignments and conditions, wrongly typed conditions, and much more.
If any error is encountered during this validation step, the function returns
a ReadError
which contains a list of all the errors it found. The helper
function print_read_error
exists to write a description of all
errors and where they were found into a single string, which can be written to a log file.
Runtime errors
Once a story is started, returned errors will be of InklingError
type.
Saving and loading
Saving and loading the state of a story can be done using the serialization and
deserialization methods of serde
.
If the serde_support
feature is enabled (see Set-up
for more information), inkling
derives the required method for all of its objects.
It is then possible to use any compatible serializer and deserializer to save the
state into some object on disk, and restore the data from that object.
Some supported data formats are listed on this page.
Example: using JSON
The serde_json
crate uses JSON text files as storage.
In Cargo.toml
, add
serde_json = "1.0"
to your dependencies and ensure that the serde_support
feature
is enabled for inkling
.
Converting from Story
to String
use serde_json;
let serialized_story: String = serde_json::to_string(&story).unwrap();
// write `serialized_story` to a text file
Restoring a Story
from String
use serde_json;
// `serialized_story` is read from a text file
let story: Story = serde_json::from_str(&serialized_story).unwrap();
Example: Text adventure
This page contains a sample implementation of a simple story reader using inkling
.
It only uses plain text and a terminal.
The full code can be found as the player.rs
example on Github.
Design
The player requires this functionality:
- Reading a story from a file
- A game loop
- Presenting the text to the player
- Asking the player for a choice at branches
Reading
A simple function that attempts to read the story from a file at a given path. If errors are encountered they should be handled.
#![allow(unused)] fn main() { extern crate inkling; use std::io::Write; use inkling::{error::parse::print_read_error, read_story_from_string, Story}; fn read_story(path: &std::path::Path) -> Result<Story, std::io::Error> { let content = std::fs::read_to_string(path)?; match read_story_from_string(&content) { Ok(story) => Ok(story), Err(error) => { // If the story could not be parsed, write the list of errors to stderr write!( std::io::stderr(), "{}", print_read_error(&error).unwrap() ) .unwrap(); std::process::exit(1); } } } }
Game loop
The main loop implements the standard pattern.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{InklingError, Prompt, Story}; fn play_story(mut story: Story) -> Result<(), InklingError> { let mut line_buffer = Vec::new(); while let Prompt::Choice(choices) = story.resume(&mut line_buffer)? { print_lines(&line_buffer); line_buffer.clear(); let choice = ask_user_for_choice(&choices).unwrap_or_else(|| { println!("Exiting program."); std::process::exit(0); }); println!(""); story.make_choice(choice)?; } Ok(()) } // Mock the following functions fn print_lines(buffer: &inkling::LineBuffer) { unimplemented!(); } fn ask_user_for_choice(choices: &[inkling::Choice]) -> Option<usize> { unimplemented!(); } }
Printing story text
Simply iterate through the list of lines and print the text. Add an extra newline if there is a paragraph break.
#![allow(unused)] fn main() { extern crate inkling; use inkling::LineBuffer; fn print_lines(lines: &LineBuffer) { for line in lines { print!("{}", line.text); if line.text.ends_with('\n') { print!("\n"); } } } }
Asking the player for a choice
Print the available choices one by one, then ask for a selection.
#![allow(unused)] fn main() { extern crate inkling; use std::io; use inkling::Choice; fn ask_user_for_choice(choices: &[Choice]) -> Option<usize> { println!("Choose:"); for (i, choice) in choices.iter().enumerate() { println!(" {}. {}", i + 1, choice.text); } println!(" ---"); println!(" 0. Exit story"); println!(""); let index = get_choice(choices.len())?; Some(index) } fn get_choice(num_choices: usize) -> Option<usize> { loop { let mut input = String::new(); std::io::stdin().read_line(&mut input).unwrap(); match input.trim().parse::<usize>() { Ok(0) => { return None; } Ok(i) if i > 0 && i <= num_choices => { return Some(i - 1); } _ => { println!("Not a valid option, try again:"); } } } } }
Implemented Ink features
This chapter contains a list of the Ink
features which are available in inkling
.
More information about these features can be found in Inkle’s guide to writing with Ink, which is a better guide showing how to write a script.
However, not everything in that guide can be done with inkling
, since it is not
completely compatible with the original implementation. This is partly
the reason for this document, which shows which features are guaranteed to work.
All examples shown here are accompanied under the hood by tests which assert that
the result is what it should be.
Examples in this chapter show how the features are written in plain .ink
text files (although the file names do not have to end with .ink
). Text inside of
these files will appear like this:
#![allow(unused)] fn main() { let content = " Example text in a file to be read. "; }
Major features which are not yet implemented are listed on the missing features page.
Basic elements
These are the basic features needed to write a story with Ink
and inkling
.
Text
Plain text is the most basic element of a story. It is written in the story text as regular lines.
#![allow(unused)] fn main() { let content = r" I opened my notebook to a blank page, pen in hand. "; }
Text is separated into paragraphs by being on different lines.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" My hand moved towards the canvas. The cold draft made a shudder run through my body. A dark blot spread from where my pen was resting. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(buffer[0].text, "My hand moved towards the canvas.\n"); assert_eq!(buffer[1].text, "The cold draft made a shudder run through my body.\n"); assert_eq!(buffer[2].text, "A dark blot spread from where my pen was resting.\n"); }
Those three lines will be returned from inkling
as separate lines, each ending with
a newline character.
Glue
If you want to remove the newline character from in between lines, you can use the <>
marker which signifies glue. This:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" This line will <> be glued to this, without creating a new paragraph. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert!(!buffer[0].text.ends_with("\n")); }
Becomes:
This line will be glued to this, without creating a new paragraph.
as will this, since glue can be put at either end:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" This line will <> be glued to this, without creating a new paragraph. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert!(!buffer[0].text.ends_with("\n")); }
For these examples glue doesn’t do much, but it will be more useful once we introduce story structure features. Keep it in mind until then.
Comments
The text file can contain comments, which will be ignored by inkling
as it parses the story.
To write a comment, preceed the line with //
.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" The cold could not be ignored. // Unlike this line, which will be As will the end of this. // removed comment at end of line "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(buffer[0].text, "The cold could not be ignored.\n"); assert_eq!(buffer[1].text, "As will the end of this.\n"); }
Note that multiline comments with /*
and */
are not currently supported.
Branching story paths
To mark a choice in a branching story, use the *
marker.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" * Choice 1 * Choice 2 "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices[0].text, "Choice 1"); assert_eq!(choices[1].text, "Choice 2"); } _ => unreachable!() } }
(The +
marker can also be used, which results in a different behavior
if the tree is visited again. More on this later.)
When inkling
encounters one or more lines beginning with this marker, the options will
be collected and returned to the user to make a choice.
After making a choice, the story proceeds from lines below the choice. So this story:
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r#" A noise rang from the door. * "Hello?" I shouted. "Who's there?" * I rose from the desk and walked over. "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices[0].text, r#""Hello?" I shouted."#); assert_eq!(choices[1].text, r"I rose from the desk and walked over."); } _ => unreachable!() } story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert!(buffer[0].text.starts_with(r#"A noise rang from the door."#)); assert!(buffer[1].text.starts_with(r#""Hello?" I shouted."#)); assert!(buffer[2].text.starts_with(r#""Who's there?""#)); }
results in this “game” for the user (in this case picking the first option):
A noise rang from the door.
1: "Hello?" I shouted.
2: I rose from the desk and walked over.
> 1
"Hello?" I shouted.
"Who's there?"
Removing choice text from output
As the previous example show, by default, the choice text will be added to the
text presented to the user. Text encased in square brackets []
will, however,
be ignored. Building on the previous example:
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r#" * ["Hello?" I shouted.] "Who's there?" "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices[0].text, r#""Hello?" I shouted."#); } _ => unreachable!() } story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert!(buffer[0].text.starts_with(r#""Who's there?""#)); }
1: "Hello?" I shouted.
> 1
"Who's there?"
Note how the choice text is not printed below the selection.
Advanced: mixing choice and presented text
The square brackets also acts as a divider between choice and presented text. Any text after the square brackets will not appear in the choice text. Text before the brackets will appear in both choice and output text. This makes it easy to build a simple choice text into a more presentable sentence for the story:
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r#" * "Hello[?"]," I shouted. "Who's there?" "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices[0].text, r#""Hello?""#); } _ => unreachable!() } story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert!(buffer[0].text.starts_with(r#""Hello," I shouted. "Who's there?""#)); }
1: "Hello?"
> 1
"Hello," I shouted. "Who's there?"
Nested dialogue options
Dialogue branches can be nested, more or less infinitely. Just add extra *
markers
to specify the depth.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" * Choice 1 * * Choice 1.1 * * Choice 1.2 * * * Choice 1.2.1 * Choice 2 * * Choice 2.1 * * Choice 2.2 "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(&choices[0].text, "Choice 1.1"); assert_eq!(&choices[1].text, "Choice 1.2"); story.make_choice(1).unwrap(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(&choices[0].text, "Choice 1.2.1"); } _ => unreachable!() } } _ => unreachable!() } }
Any extra whitespace is just for readability. The previous example produces the exact same tree as this, much less readable, example:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content_nowhitespace = r" *Choice 1 **Choice 1.1 **Choice 1.2 ***Choice 1.2.1 *Choice 2 **Choice 2.1 **Choice 2.2 "; let content_whitespace = r" * Choice 1 * * Choice 1.1 * * Choice 1.2 * * * Choice 1.2.1 * Choice 2 * * Choice 2.1 * * Choice 2.2 "; let story_nowhitespace = read_story_from_string(content_nowhitespace).unwrap(); let story_whitespace = read_story_from_string(content_whitespace).unwrap(); assert_eq!(format!("{:?}", story_nowhitespace), format!("{:?}", story_whitespace)); }
Story structure
Story scripts can be divided into different sections, to which the story can diverge. This section introduces how to create these sections and move to them in the text.
Knots
A story can be divided into different sections, called knots in Ink
. This division
is invisible to the user but makes it easier to write and reason about the story
in production.
A knot is denoted by beginning the line with at least two (2) =
signs followed by
a name for the knot. On the following lines, the story text can resume.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location}; let content = r" == stairwell I made my way down the empty stairwell. "; let mut story = read_story_from_string(content).unwrap(); assert!(story.move_to(&Location::from("stairwell")).is_ok()); }
The name (’stairwell’ in the previous example) cannot contain spaces or non-alphanumeric
symbols. Optionally, it may be followed by more =
signs, which are not necessary but may
make it easier to identify knots in the document. This is identical to the previous
example:
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location}; let content = r" === stairwell === I made my way down the empty stairwell. "; let mut story = read_story_from_string(content).unwrap(); assert!(story.move_to(&Location::from("stairwell")).is_ok()); }
Non-latin characters
Knot names support any character as long as they are
alphanumeric
according to the Rust
language specification. This seems to include all languages
which are recognized by UTF-8. Thus, knots (and any identifer) may contain e.g.
Chinese, Japanese, Arabic, Cyrillic and other characters. Do let us know if you
find any exceptions.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location}; let content = r" === عقدة === These === 매듭 === are === गांठ === all === 結 === allowed. "; let mut story = read_story_from_string(content).unwrap(); assert!(story.move_to(&Location::from("عقدة")).is_ok()); assert!(story.move_to(&Location::from("매듭")).is_ok()); assert!(story.move_to(&Location::from("गांठ")).is_ok()); assert!(story.move_to(&Location::from("結")).is_ok()); }
Stitches
Knots may be further subdivided into stitches. These are denoted by single =
markers.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location}; let content = r" === garden === = entrance A pale moonlight illuminated the garden. = well The well stank of stagnant water. Is that an eel I see at the bottom? "; let mut story = read_story_from_string(content).unwrap(); assert!(story.move_to(&Location::from("garden.entrance")).is_ok()); assert!(story.move_to(&Location::from("garden.well")).is_ok()); }
Diverts
Diverts are used to move to different parts of the story. A divert to a knot moves
the story to continue from there. They are designated with the ->
marker followed
by the destination.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location}; let content = r" -> stairwell === stairwell === The stairs creaked as I descended. -> lower_floor === garden === A pale moonlight illuminated the garden as I entered it. -> END === lower_floor === On the bottom I found an unlocked door. -> garden "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(story.get_current_location(), Location::from("garden")); }
Diverts are automatically followed as they are encountered.
Diverts to stitches
Stitches inside knots can be diverted to using knot.stitch
as a destination:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> garden.entrance === garden === = well Unreachable. = entrance A pale moonlight illuminated the garden. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert!(buffer[0].text.starts_with("A pale moonlight illuminated the garden.")); }
Stitches within the same knot can be diverted to with only the stitch name:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> garden === garden === -> well = entrance A pale moonlight illuminated the garden. = well The well stank of stagnant water. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert!(buffer[0].text.starts_with("The well stank of stagnant water.")); }
Ending the story with -> END
END
is a destination that signifies that the story has come to, well, an end. Use
-> END
diverts for such occasions. An ink
story is not complete unless all
branches one way or another leads to an -> END
divert: ending a story should
be intentional.
Diverts in choices
A common use of branches is to divert to other knots.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location}; let content = r" * [Descend stairs] -> lower_floor * [Return to desk] I sighed, wearily, and returned to my room. -> desk === desk === As I sat by my desk, I noticed that my notebook had gone missing. === lower_floor === On the bottom I found an unlocked door. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); story.make_choice(1).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(story.get_current_location(), Location::from("desk")); }
Revisiting content and choices
With diverts we can easily return to previously visited knots and stitches. When this happens, the text is reevaluated to reflect the current state of the story (see the sections on conditional content and alternating sequences for more information).
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" === table === You are seated at the table. * [Order a cup of tea] A waiter returns with a steaming hot cup of tea. -> table * [Leave] You leave the café. "; assert!(read_story_from_string(content).is_ok()); }
Once-only and sticky choices
Any set of branching choices will also be reevaluated. There are two types of
choices, denoted by if they begin with *
or +
markers:
*
marks once-only choices, which can only be picked once+
marks sticky choices, which can be picked any number of times
In short, once-only choices are removed from the choice list if they are picked. Sticky choices will remain. This has to be kept in mind if the branch might be revisited during the story.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" -> loop === loop === * This choice can only be picked once -> loop + This choice is always here -> loop "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 2); assert_eq!(&choices[0].text, "This choice can only be picked once"); } _ => unreachable!() } story.make_choice(0).unwrap(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 1); assert_eq!(&choices[0].text, "This choice is always here"); } _ => unreachable!() } story.make_choice(0).unwrap(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 1); assert_eq!(&choices[0].text, "This choice is always here"); } _ => unreachable!() } }
Running out of choices
Since once-only choices are removed it is possible for a branching choice point
to run out of choices. This will result in an
error
being returned from inkling
at runtime.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" -> thrice_fail === thrice_fail === The third time we visit this we are out of choices and an error is returned. * First choice -> thrice_fail * Second choice -> thrice_fail "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); assert!(story.resume(&mut buffer).is_err()); }
So be careful when writing branching choices using only once-only markers. Is there a risk that you will return to it multiple times?
Fallback choices
There is a fallback option available for running out of choices. If no regular (sticky
or once-only) choices are left to present for the user, inkling
will look for a
fallback choice and automatically follow it.
This can only be a single choice and is marked by being a choice without choice text,
which is to say that it starts with a divert ->
marker.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" -> twice_fail === twice_fail === The second time we visit this we are out of regular choices. We then use the fallback. * First choice -> twice_fail * -> fallback === fallback === We escaped the loop! "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 1); assert_eq!(&choices[0].text, "First choice"); } _ => unreachable!() } story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "We escaped the loop!\n"); }
The fallback content can contain text by putting it on a new line directly after the divert marker.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" -> write_article === write_article === * [Write abstract] -> write_article * [Write main text] -> write_article * [Write summary] -> write_article * -> The article is finished. -> submit_article === submit_article === You submit it to your editor. Wow, writing is easy! "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "The article is finished.\n"); assert!(&buffer[1].text.starts_with("You submit it to your editor.")); }
Fallback choices can also be sticky. If they are not they will also be consumed after use. Again, ensure that you are sure that branches with non-sticky fallback choices will not be returned to multiple times.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location, Prompt}; let content = r" === once_only_fallback === This will return an error if the fallback choice is used twice. * -> once_only_fallback === sticky_fallback === {sticky_fallback > 4 : -> END} // exit once we have returned here a few times This sticky fallback choice can be use any number of times. + -> sticky_fallback "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.move_to(&Location::from("once_only_fallback")).unwrap(); assert!(story.resume(&mut buffer).is_err()); story.move_to(&Location::from("sticky_fallback")).unwrap(); story.resume(&mut buffer).unwrap(); assert!(story.resume(&mut buffer).is_ok()); }
Gather points
When creating a set of choices, you can return (or, gather) all of the branches to a single path after they have gone through their content. This is done using gather points.
To return the branches, add a gather marker -
at a new line after the branches.
In the following example, regardless of whether the player heads to the garden or the kitchen, they return to their room. There, they are presented with the next choice.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" * [Head into the garden] The chirp of crickets greet you as you enter the garden. * [Move to the kitchen] A crackling fireplace illuminates the dark room. - A while later, you return to your room. * [Lay in bed] * [Sit at table] "; let mut story = read_story_from_string(content).unwrap(); let mut story_other = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(buffer.len(), 2); assert_eq!(&buffer[0].text, "The chirp of crickets greet you as you enter the garden.\n"); assert_eq!(&buffer[1].text, "A while later, you return to your room.\n"); buffer.clear(); story_other.resume(&mut buffer).unwrap(); story_other.make_choice(1).unwrap(); story_other.resume(&mut buffer).unwrap(); assert_eq!(buffer.len(), 2); assert_eq!(&buffer[0].text, "A crackling fireplace illuminates the dark room.\n"); assert_eq!(&buffer[1].text, "A while later, you return to your room.\n"); }
Nested gather points
Gathers can be performed for any nested level of choices. Simply add the corresponding
number of gather markers -
below.
In this example, both inner choices 1.1 and 1.2 will gather at 1.1. Inner choices 2.1 and 2.2 at gather 2.1. Then finally, both outer choices 1 and 2 at gather point 1.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" * Choice 1 * * Choice 1.1 * * Choice 1.2 - - Gather 1.1 * Choice 2 * * Choice 2.1 * * Choice 2.2 - - Gather 2.1 - Gather 1 "; let mut story = read_story_from_string(content).unwrap(); let mut story_other = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); story.make_choice(1).unwrap(); story.resume(&mut buffer).unwrap(); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(buffer.len(), 4); assert_eq!(&buffer[0].text, "Choice 2\n"); assert_eq!(&buffer[1].text, "Choice 2.1\n"); assert_eq!(&buffer[2].text, "Gather 2.1\n"); assert_eq!(&buffer[3].text, "Gather 1\n"); buffer.clear(); story_other.resume(&mut buffer).unwrap(); story_other.make_choice(0).unwrap(); story_other.resume(&mut buffer).unwrap(); story_other.make_choice(1).unwrap(); story_other.resume(&mut buffer).unwrap(); assert_eq!(buffer.len(), 4); assert_eq!(&buffer[0].text, "Choice 1\n"); assert_eq!(&buffer[1].text, "Choice 1.2\n"); assert_eq!(&buffer[2].text, "Gather 1.1\n"); assert_eq!(&buffer[3].text, "Gather 1\n"); }
Preamble
The script is divided into a preamble and the story content. The preamble contains variable declarations, metadata and inclusions of other documents. The content comes afterwards and can refer to declarations in the preamble.
The end of the preamble in a script is marked by the first line of text or story content. This can be a divert to the introductory scene.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r#" // Global story tags are declared in the preamble # title: Inkling # author: Petter Johansson // ... as are global variables CONST name = "d'Artagnan" VAR rank = "Capitaine" // First line of story content comes here, which ends the preamble declaration -> introduction === introduction === I opened my notebook to a blank page, pen in hand. "#; let story = read_story_from_string(content).unwrap(); let tags = story.get_story_tags(); assert_eq!(&tags[0], "title: Inkling"); assert_eq!(&tags[1], "author: Petter Johansson"); assert!(story.get_variable("name").is_some()); assert!(story.get_variable("rank").is_some()); }
Variables
Throughout a story it can be useful to introduce variables, which can be declared and used in the story text. This makes it easy to keep a story consistent and track a story state.
Declaring variables
Global variables can be declared in the script using the VAR
keyword. They
must be declared in the preamble: before the first knot.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Variable}; let content = r#" VAR a_float = 1.0 VAR a_int = 2 VAR a_bool = false VAR a_string = "A String" VAR a_destination = "-> stairwell" "#; let story = read_story_from_string(content).unwrap(); assert_eq!(story.get_variable("a_float").unwrap(), Variable::Float(1.0)); assert_eq!(story.get_variable("a_int").unwrap(), Variable::Int(2)); assert_eq!(story.get_variable("a_bool").unwrap(), Variable::Bool(false)); assert_eq!(story.get_variable("a_string").unwrap(), Variable::String("A String".to_string())); assert!(story.get_variable("a_destination").is_some()); }
As shown in this example, the variable type is automatically assigned from the given value. Once assigned, a variable’s type cannot be changed.
Using variables in text
Variables can be inserted into text by enclosing them in curly braces.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Variable}; let content = r#" VAR time = 11 VAR moon = "gloomy" The time was {time}. A {moon} moon illuminated the room. "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(buffer[0].text, "The time was 11. A gloomy moon illuminated the room.\n"); }
Variable assignment
It is not currently possible to assign variables in the script. Use Story::set_variable
.
Constant variables
Constant variables, whose values cannot be changed, are declared using the CONST
keyword.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Variable}; let content = r#" CONST name = "d'Artagnan" // Constant variable, cannot be modified VAR rank = "Capitaine" // Non-constant variable, can be changed "#; let mut story = read_story_from_string(content).unwrap(); assert_eq!(story.get_variable("name").unwrap(), Variable::from("d'Artagnan")); assert!(story.set_variable("name", "Aramis").is_err()); }
Variable mathematics
Variable comparisons
Conditional content
Ink
provides many methods for varying the text content of a line or the choices
presented to a user.
Choice conditions
The easiest way to gate which choices are presented to the user is to check if they have
visited a knot in the story. This is done by preceding the choice with the knot name
enclosed by curly braces, for example {knot}
.
In the following example, the first choice is only presented if the player has previously
visited the knot with name tea_house
.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location, Prompt}; let content = r#" -> choice === choice === + {tea_house} "Yes, I saw them at 'Au thé à la menthe.'" + "No, I have not met them." === tea_house === -> choice "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 1); assert_eq!(choices[0].text, r#""No, I have not met them.""#); } _ => unreachable!() } story.move_to(&Location::from("tea_house")).unwrap(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 2); assert_eq!(choices[0].text, r#""Yes, I saw them at 'Au thé à la menthe.'""#); } _ => unreachable!() } }
Under the hood, inkling
resolves this by translating {tea_house}
as a variable
whose value is the number of times the knot has been visited. It then asserts
whether that value is “true”, which in Ink
is whether it is non-zero. Thus, {tea_house}
is an implicit form of writing the explicit condition {tea_house != 0}
.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Location, Prompt}; let content = r#" -> choice === choice === + {tea_house != 0} "Yes, I saw them at 'Au thé à la menthe.'" + "No, I have not met them." === tea_house === -> choice "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 1); assert_eq!(choices[0].text, r#""No, I have not met them.""#); } _ => unreachable!() } story.move_to(&Location::from("tea_house")).unwrap(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 2); assert_eq!(choices[0].text, r#""Yes, I saw them at 'Au thé à la menthe.'""#); } _ => unreachable!() } }
Knowing this, we can of course also test these conditions using other types of variables.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" VAR visited_château = true VAR coins = 3 + {visited_château} You recognize the bellboy. + {coins > 2} [Tip the bellboy] + {coins <= 2} [You cannot afford entry] "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 2); assert_eq!(choices[0].text, "You recognize the bellboy."); assert_eq!(choices[1].text, "Tip the bellboy"); } _ => unreachable!() } }
Multiple conditions
Multiple conditions can be tested at once by supplying them one after another. All must be true for the choice to be presented. In this example, the first and third choices will be presented.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" VAR visited_château = true VAR coins = 6 + {visited_château} {coins > 5} Purchase the painting + {not visited_château} {coins > 5} Your wallet itches but you see nothing of interest. + Leave the exhibit "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices.len(), 2); assert_eq!(choices[0].text, "Purchase the painting"); assert_eq!(choices[1].text, "Leave the exhibit"); } _ => unreachable!() } }
Beginning choices with variables instead of conditions
Finally, in case you want the choice text to begin with a variable, “escape” the first
curly brace by prepending it with a \
character. This is so that inkling
will know
to write the variable as text, not evaluate it as a condition. This is an unfortunate
quirk of the very compact Ink
scripting language, where curly braces play many roles.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r#" VAR mentor = "Evan" + \{mentor}, your mentor, greets you "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices[0].text, "Evan, your mentor, greets you"); } _ => unreachable!() } }
For more information about which types of comparisons are supported, see the section on variable comparisons.
Text conditions
Conditions for displaying text are very similar to how conditions work for choices,
but work on this format: {condition: process this if true | otherwise process this}
.
A colon :
follows the condition, the content is inside the braces and an optional
|
marker marks content to show if the condition is not true.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" VAR visited_château = false VAR coins = 3 You {visited_château: recognize a painting | see nothing of interest}. {coins < 5: You cannot afford anything.} "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "You see nothing of interest.\n"); assert_eq!(&buffer[1].text, "You cannot afford anything.\n"); }
You see nothing of interest.
You cannot afford anything.
Again, see the section on variable comparisons for more information about how conditions can be tested.
Nesting conditions
Conditions can naturally be nested inside of conditional content:
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" VAR met_evan = true VAR met_austin = false {met_evan: Yes, I met with Evan {met_austin: and | but not} Austin}. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "Yes, I met with Evan but not Austin.\n"); }
Yes, I met with Evan but not Austin.
Diverts inside conditions
Content inside of conditions can divert to other knots.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r" VAR met_evan = true {met_evan: Evan takes you to his home. -> château | -> END } === château === The car ride takes a few hours. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "Evan takes you to his home.\n"); assert_eq!(&buffer[1].text, "The car ride takes a few hours.\n"); }
Evan takes you to his home.
The car ride takes a few hours.
Alternating sequences
Ink
comes with several methods which vary the content of a line every time it is
seen. These are known as alternating sequences of content.
Sequences
Alternative sequences can be declared in a line using curly braces and |
separators.
Every time the line is revisited, the next piece will be presented, allowing us to
write something like this:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> continue === continue === The train had arrived {in Mannheim|in Heidelberg|at its final stop}. + [Continue] -> continue "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "The train had arrived in Mannheim.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "The train had arrived in Heidelberg.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "The train had arrived at its final stop.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "The train had arrived at its final stop.\n"); }
When revisiting this line, it will go through the
alternatives in order, then repeat the final value after reaching it. In Ink
terms,
this is called a sequence.
The train had arrived in Mannheim.
The train had arrived in Heidelberg.
The train had arrived at its final stop.
The train had arrived at its final stop.
Cycle sequences
Cycle sequences repeat the entire sequence after reaching the final piece. They are
denoted by starting the first alternative with a &
marker.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> continue === continue === Today is a {&Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday}. + [Continue] -> continue "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); for day in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", "Monday"].iter() { story.resume(&mut buffer).unwrap(); assert_eq!(buffer.last().unwrap().text, format!("Today is a {}.\n", day)); story.make_choice(0).unwrap(); } }
Today is a Monday.
Today is a Tuesday.
Today is a Wednesday.
[...]
Today is a Sunday.
Today is a Monday.
Once-only sequences
Once-only sequences goes through all alternatives in order and then produce nothing.
They are denoted by starting the first alternative with a !
marker.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> continue === continue === I met with Anirudh{! for the first time| for the second time}. + [Continue] -> continue "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I met with Anirudh for the first time.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I met with Anirudh for the second time.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I met with Anirudh.\n"); }
I met with Anirudh for the first time.
I met with Anirudh for the second time.
I met with Anirudh.
Shuffle sequences
Shuffle sequences go through the alternatives in a random order, then shuffle the
alternatives and deal them again. They are denoted by starting the first alternative
with a ~
marker.
Note that these are only random if inkling
has been compiled with the random
feature.
Otherwise they mimic the behavior of cycle sequences.
#![allow(unused)] fn main() { let content = r" -> continue === continue === I was dealt a Jack of {~hearts|spades|diamonds|clubs}. + [Continue] -> continue "; }
I was dealt a Jack of diamonds.
I was dealt a Jack of spades.
I was dealt a Jack of hearts.
I was dealt a Jack of clubs.
Nested alternatives
Alternatives can of course hide even more alternatives. How would we otherwise have any fun in life?
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> continue === continue === I {&{strode|walked} hastily|waltzed {gracefully|clumsily}} into the room. + [Continue] -> continue "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I strode hastily into the room.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I waltzed gracefully into the room.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I walked hastily into the room.\n"); story.make_choice(0).unwrap(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer.last().unwrap().text, "I waltzed clumsily into the room.\n"); }
I strode hastily into the room.
I waltzed gracefully into the room.
I walked hastily into the room.
I waltzed clumsily into the room.
Diverts in alternatives
We can use diverts inside of alternatives to alternatively trigger different parts of the story.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> continue === continue === The {first|next} time I saw the door it was {locked. -> locked_door|open. -> open_door} === locked_door === I had to return another day. -> continue === open_door === In the doorway stood a thin figure. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "The first time I saw the door it was locked.\n"); assert_eq!(&buffer[1].text, "I had to return another day.\n"); assert_eq!(&buffer[2].text, "The next time I saw the door it was open.\n"); assert_eq!(&buffer[3].text, "In the doorway stood a thin figure.\n"); }
The first time I saw the door it was locked.
I had to return another day.
The next time I saw the door it was open.
In the doorway stood a thin figure.
Here’s where glue can come in handy. By adding glue before the divert we can continue the same paragraph in a new knot. Building on the previous example:
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r" -> continue === continue === The {first|next} time I saw the door it was {locked. -> locked_door|open. -> open_door} === locked_door === <> I had to return another day. -> continue === open_door === <> In the doorway stood a thin figure. "; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "The first time I saw the door it was locked. "); assert_eq!(&buffer[1].text, "I had to return another day.\n"); assert_eq!(&buffer[2].text, "The next time I saw the door it was open. "); assert_eq!(&buffer[3].text, "In the doorway stood a thin figure.\n"); }
The first time I saw the door it was locked. I had to return another day.
The next time I saw the door it was open. In the doorway stood a thin figure.
Story metadata
These features do not impact the story flow but contain additional information.
Tags
Information about the story, knots or even individual lines can be marked with tags. All tags
begin with the #
marker.
Tags are stored as pure strings and can thus be of any form you want. inkling
assigns no
meaning to them on its own, it’s for you as the user to decide how to treat them.
Global tags
Tags in the preamble are global story tags. Here you can typically mark up metadata for the script.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Variable}; let content = r#" # title: Inkling # author: Petter Johansson "#; let story = read_story_from_string(content).unwrap(); let tags = story.get_story_tags(); assert_eq!(&tags[0], "title: Inkling"); assert_eq!(&tags[1], "author: Petter Johansson"); }
Knot tags
Tags encountered in a knot before any content is parsed as tags belonging to that knot.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r#" === stairwell === # sound: blowing_wind.ogg # dark, quiet, dangerous I made my way down the empty stairwell. "#; let story = read_story_from_string(content).unwrap(); let tags = story.get_knot_tags("stairwell").unwrap(); assert_eq!(&tags[0], "sound: blowing_wind.ogg"); assert_eq!(&tags[1], "dark, quiet, dangerous"); }
Line tags
Lines can be tagged by adding the tag after the line content. Multiple tags can be set, separated by additional ‘#’ markers.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r#" A pale moonlight illuminated the garden. # sound: crickets.ogg The well stank of stagnant water. # smell, fall # sound: water_drip.ogg "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); let tags1 = &buffer[0].tags; let tags2 = &buffer[1].tags; assert_eq!(&tags1[0], "sound: crickets.ogg"); assert_eq!(&tags2[0], "smell, fall"); assert_eq!(&tags2[1], "sound: water_drip.ogg"); }
Tags can also be added to choice lines.
#![allow(unused)] fn main() { extern crate inkling; use inkling::{read_story_from_string, Prompt}; let content = r#" * I made my way to the well. # sound: footsteps.ogg "#; let mut story = read_story_from_string(content).unwrap(); let mut buffer = Vec::new(); match story.resume(&mut buffer).unwrap() { Prompt::Choice(choices) => { assert_eq!(choices[0].tags[0], "sound: footsteps.ogg"); } _ => unreachable!() } }
To-do comments
To-do comments are lines which start with TODO:
, including the colon. When the script
is parsed, these comments are removed from the text and added to
the log as reminders.
#![allow(unused)] fn main() { extern crate inkling; use inkling::read_story_from_string; let content = r#" -> fireworks === fireworks === TODO: Make this more snappy. Emtithal woke up to the sound of fireworks. "#; let mut story = read_story_from_string(content).unwrap(); assert_eq!(story.log.todo_comments.len(), 1); let mut buffer = Vec::new(); story.resume(&mut buffer).unwrap(); assert_eq!(&buffer[0].text, "Emtithal woke up to the sound of fireworks.\n"); }
Missing features
This page lists notable features of Ink
which are currently missing in inkling
.
Some may be implemented, others will be more difficult.
Variable assignment
Assigning new values to variables in the script.
~ rank = "Capitaine"
~ coins = coins + 4
Including other files
Dividing the script into several files and including them in the preamble of the main script.
INCLUDE château.ink
INCLUDE gloomwood.ink
Multiline comments
Using /*
and */
markers to begin and end multiline comments.
Line one /* We can use multiline comments
to split them over several lines,
which may aid readability. */
Line two
Multiline conditionals
Using multiline blocks to create larger if-else or switch statements.
{condition:
if condition content
- else:
else this content
}
{
- condition1:
if condition 1 content
- condition2:
else if condition 2 content
- else:
else this content
}
Labels
Add labels to choices and gather points to refer and divert to them.
* (one) Choice
* (two) Choice
- (gather) Gather
Functions
Calling various types of functions from the script.
Built-in functions
Support for the pre-defined functions of Ink
.
~ result = RANDOM(1, 6)
~ result = POW(3, 2)
Definining functions
Defining functions in the script to print text, modify variables and return calculations.
// Modifying referenced values
=== function add(ref x, value) ===
~ x = x + value
// Modifying global variables
VAR coins = 0
=== function add_wealth(v) ===
~ coins = coins + v
// Writing text
=== function greet(person) ===
Greetings, {person}!
External functions
Begin able to call external Rust functions from the script.
Threads
Tunnels
Advanced state tracking
In regards to compatibility
The authors have made a best effort attempt to replicate the behavior of Ink
in
most respects. However, since this is a completely separate implementation, there
are surely certain features which even if implemented
(and many aren’t), result in different output
from the same input .ink
script. Edge cases may be a-plenty.
We do care about making Ink
and inkling
as similar as possible, to the extent
that we can. If you find cases where the results differ, please let us know by
opening an issue on the Github repository.
In the end, inkling
cannot be considered a drop-in replacement for Inkle’s own
implementation of the language. More realistically, it’s inspired by it, sharing
most features but with results which may differ. Keep this in mind while writing
a script.
I’d like to end this note by thanking Inkle for designing the language and being the inspiration for this project. And of course for their many fantastic games and stories, which is some of the best work out there.
— Petter Johansson, 2020