Giter Site home page Giter Site logo

narrator's Introduction

logo

Narrator

Release License Website Mastodon Twitter Telegram Buy me a coffee

Overview

The Ink language parser and runtime implementation in Lua.

Ink is a powerful narrative scripting language. You can find more information about how to write Ink scripts here. There is also Inky editor with useful features to test and debug Ink scripts.

Narrator allows to convert raw Ink scripts to the book (a lua table) and play it as story.

  • 📖 A book is a passive model on the shelf like a game level.
  • ✨ A story is a runtime state of the book reading like a game process.

Quick example

local narrator = require('narrator.narrator')

-- Parse a book from the Ink file.
local book = narrator.parse_file('stories.game')

-- Init a story from the book
local story = narrator.init_story(book)

-- Begin the story
story:begin()

while story:can_continue() do

  -- Get current paragraphs to output
  local paragraphs = story:continue()

  for _, paragraph in ipairs(paragraphs) do
    local text = paragraph.text

    -- You can handle tags as you like, but we attach them to text here.
    if paragraph.tags then
      text = text .. ' #' .. table.concat(paragraph.tags, ' #')
    end

    -- Output text to the player
    print(text)
  end

  -- If there is no choice it seems like the game is over
  if not story:can_choose() then break end

  -- Get available choices and output them to the player
  local choices = story:get_choices()
  for i, choice in ipairs(choices) do
    print(i .. ') ' .. choice.text)
  end

  -- Read the choice from the player input
  local answer = tonumber(io.read())

  -- Send answer to the story to generate new paragraphs
  story:choose(answer)
end

Features

Supported

  • Comments: singleline, multiline, todo's
  • Tags: global tags, knot tags, stitch tags, paragraph tags
  • Paths and sections: inclusions, knots, stitches, labels
  • Choices: suppressing and mixing, labels, conditions, sticky and fallback choices, tags
  • Branching: diversions, glues, gathers, nesting
  • Tunnels
  • Alternatives: sequences, cycles, once-only, shuffles, empty steps, nesting
  • Multiline alternatives: all the same + shuffle options
  • Conditions: logical operations, string queries, if and else statements, nesting
  • Multiline conditions: all the same + elseif statements, switches, nesting
  • Variables: assignments, constants, global variables, temporary variables, visits, lists
  • Lists: logical operations, multivalued lists, multi-list lists, all the queries, work with numbers
  • Game queries: all the queries without TURNS() and TURNS_SINCE()
  • State: saving and loading
  • Integration: external functions, variables observing, jumping
  • Migration: the ability to implement the migration of player's saves after the book update
  • Internal functions

Unsupported

Also there is a list of known limitations on the issues page.

Alternatives

  • defold-ink — The Ink language runtime implementation in Lua based on parsing compiled JSON files.

Showcase

Installation

Common case (Löve, pure Lua, etc.)

Download the latest release archive and require the narrator module.

local narrator = require('narrator.narrator')

Narrator requires lpeg as dependency to parse Ink content. You can install it with luarocks.

$ luarocks install lpeg

In fact, you don't need lpeg in the release, but you need it locally to parse Ink content and generate lua versions of books to play in your game. Use parsing in development only, prefer already parsed and stored books in production.

Defold

Add links to the zip-archives of the latest versions of narrator and defold-lpeg to your Defold project as dependencies.

https://github.com/astrochili/narrator/archive/master.zip
https://github.com/astrochili/defold-lpeg/archive/master.zip

Then you can require the narrator module.

local narrator = require('narrator.narrator')

Documentation

narrator.parse_file(path, params)

Parses the Ink file at path with all the inclusions and returns a book instance. Path notations 'stories/game.ink', 'stories/game' and 'stories.game' are valid.

You can save a parsed book to the lua file with the same path by passing { save = true } as params table. By default, the params table is { save = false }.

-- Parse a Ink file at path 'stories/game.ink'
local book = narrator.parse_file('stories.game')

-- Parse a Ink file at path 'stories/game.ink'
-- and save the book at path 'stories/game.lua'
local book = narrator.parse_file('stories.game', { save = true })

Reading and saving files required io so if you can't work with files by this way use narrator.parse_content().

narrator.parse_content(content, inclusions)

Parses the string with Ink content and returns a book instance. The inclusions param is optional and can be used to pass an array of strings with Ink content of inclusions.

local content = 'Content of a root Ink file'
local inclusions = {
  'Content of an included Ink file',
  'Content of another included Ink file'
}

-- Parse a string with Ink content
local book = narrator.parse_content(content)

-- Parse a string with Ink content and inclusions
local book = narrator.parse_content(content, inclusions)

Content parsing is useful when you should manage files by your engine environment and don't want to use io module. For example, in Defold, you may want to load ink files as custom resources with sys.load_resource().

narrator.init_story(book)

Inits a story instance from the book. This is aclual to use in production. For example, just load a book with require() and pass it to this function.

-- Require a parsed and saved before book
local book = require('stories.game')

-- Init a story instance
local story = narrator.init_story(book)

story:begin()

Begins the story. Generates the first chunk of paragraphs and choices.

story:can_continue()

Returns a boolean, does the story have paragraphs to output or not.

while story:can_continue() do
  -- Get paragraphs?
end

story:continue(steps)

Get the next paragraphs. You can specify the number of paragraphs that you want to pull by the steps param.

  • Pass nothing if you want to get all the currently available paragraphs. 0 also works.
  • Pass 1 if you want to get one next paragraph without wrapping to array.

A paragraph is a table like { text = 'Hello.', tags = { 'tag1', 'tag2' } }. Most of the paragraphs do not have tags so tags can be nil.

-- Get all the currently available paragraphs
local paragraphs = story:continue()

-- Get one next paragraph
local paragraph = story:continue(1)

story:can_choose()

Returns a boolean, does the story have choices to output or not. Also returns false if there are available paragraphs to continue.

if story:can_choose() do
  -- Get choices?
end

story:get_choices()

Returns an array of available choices. Returns an empty array if there are available paragraphs to continue.

A choice is a table like { text = 'Bye.', tags = { 'tag1', 'tag2' } }. Most of the choices do not have tags so tags can be nil.

Choice tags are not an official feature of Ink, but it's a Narrator feature. These tags also will appear in the answer paragraph as it works in Ink by default. But if you have a completely eaten choice like '[Answer] #tag' you will receive tags only in the choice.

  -- Get available choices and output them to the player
  local choices = story:get_choices()
  for i, choice in ipairs(choices) do
    print(i .. ') ' .. choice.text)
  end

story:choose(index)

Make a choice to continue the story. Pass the index of the choice that you was received with get_choices() before. Will do nothing if can_continue() returns false.

  -- Get the answer from the player in the terminal
  answer = tonumber(io.read())

  -- Send the answer to the story to generate new paragraphs
  story:choose(answer)

  -- Get the new paragraphs
  local new_paragraphs = story:continue()

story:jump_to(path_string)

Jumps to the path. The path_string param is a string like 'knot.stitch.label'.

  -- Jump to the maze stitch in the adventure knot
  story:jump_to('adventure.maze')

  -- Get the maze paragraphs
  local maze_paragraphs = story:continue()

story:get_visits(path_string)

Returns the number of visits to the path. The path_string param is a string like 'knot.stitch.label'.

-- Get the number of visits to the maze's red room
local red_room_visits = story:get_visits('adventure.maze.red_room')

-- Get the number of adventures visited.
local adventure_visits = story:get_visits('adventure')

story:get_tags(path_string)

Returns tags for the path. The path_string param is a string like 'knot.stitch'. This function is useful when you want to get tags before continue the story and pull paragraphs. Read more about it here.

-- Get tags for the path 'adventure.maze'
local mazeTags = story:get_tags('adventure.maze')

story:save_state()

Raturns a table with the story state that can be saved and restored later. Use it to save the game.

-- Get the story's state
local state = story:save_state()

-- Save the state to your local storage
manager.save(state)

story:load_state(state)

Restores a story's state from the saved before state. Use it to load the game.

-- Load the state from your local storage
local state = manager.load()

-- Restore the story's state
story:load_state(state)

story:observe(variable, observer)

Assigns an observer function to the variable's changes.

local function x_did_change(x)
  print('The x did change! Now it\'s ' .. x)
end

-- Start observing the variable 'x'
story:observe('x', x_did_change)

story:bind(func_name, handler)

Binds a function to external calling from the Ink. The function can returns the value or not.

local function beep()
  print('Beep! 😃')
end

local function sum(x, y)
  return x + y
end

-- Bind the function without params and returned value
story:bind('beep', beep)

-- Bind the function with params and returned value
story:bind('sum', sum)

story.global_tags

An array with book's global tags. Tags are strings of course.

-- Get the global tags
local global_tags = story.global_tags

-- A hacky way to get the same global tags
local global_tags = story:get_tags()

story.constants

A table with book's constants. Just read them, constants changing is not a good idea.

-- Get the theme value from the Ink constants
local theme = story.constants['theme']

story.variables

A table with story's variables. You can read or change them by this way.

-- Get the mood variable value
local mood = story.variables['mood']

-- Set the mood variable value
story.variables['mood'] = 'sunny'

story.migrate

A function that you can specify for migration from old to new versions of your books. This is useful, for example, when you don't want to corrupt player's save after the game update.

This is the place where you can rename or change variables, visits, update the current path, etc. The default implementation returns the same state without any migration.

-- Default implementation
function(state, old_version, new_version) return state end

The old_version is the version of the saved state, the new_version is the version of the book. You can specify the verson of the book with the constant 'version' in the Ink content, otherwise it's equal to 0.

-- A migration function example
local function migrate(state, old_version, new_version)

  -- Check the need for migration
  if new_version == old_version then
    return state
  end

  -- Migration for the second version of the book
  if new_version == 2 then

    -- Get the old value
    local old_mood = state.variables['mood']

    -- If it exists then migrate ...
    if old_mood then
      -- ... migrate the old number value to the new string value
      state.variables['mood'] = old_mood < 50 and 'sadly' or 'sunny'
    end
  end

  return state
end

-- Assign the migration function before loading a saved game
story.migrate = migrate

-- Load the game
story:load_state(saved_state)

Contribution

Development

There are some useful extensions and configs for VSCode that I use in development of Narrator.

Testing

To run tests you need to install busted.

$ luarocks install busted

Don't forget also to install lpeg as described in Common case installation section.

After that you can run tests from the terminal:

$ busted test/run.lua

Third Party Libraries

narrator's People

Contributors

abadonna avatar astrochili avatar flamendless avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

narrator's Issues

String constants are not converted to lua.

Hi

Consider the following ink code :

CONST a = 1
CONST b = "myString"
CONST c = "true"
CONST d = "false"
-> knot
== knot ==
Hello
a = {a}
b = {b}
c = {c}
d= {d}
->END

As an ink file, it works fine, producing b = myString. When converted to lua, and pretty printed using the inspect.lua library produces the following output :
{ constants = { a = 1, c = true, d = false }, inclusions = {}, lists = {}, tree = { = { = { { divert = "knot" } } }, knot = { _ = { "Hello", "a = #a#", "b = #b#", "c = #c#", "d= #d#", { divert = "END" } } } }, variables = {}, version = { engine = 1, tree = 1 } }
Please note that the constant b = "myString" is not present.

It is impossible to fetch the current paragraph

Description

The only way (I know of) to fetch a paragraph from Narrator is to use story:continue. When using story:continue the story steps forward and then returns the next paragraph, meaning that there is no way to fetch the paragraph that should currently be displayed
to the user.

This is a problem when loading data. Since it is impossible to get the paragraph that currently should be displayed, the story has to step forward one paragraph and then display that, which results in an odd experience for the player, as their savefile seems to jumped a step forward without them doing anything.

Loading when at a choice works just as expected though. If the player saves at a choice, they load at a choice.

Workaround

Possible workarounds are keeping the save data from the previous step and save that if the current content is a paragraph. This does however not work if the previous content is a choice, as the player will load at the choice then. To solve that, one could also save the decision made at that choice and automatically answer it, to then load the next paragraph. Not very clean.

Proposed resolution

Currently there are two ways to fetch all the content up to the next choice: with story:continue(nil) and story:continue(0). It would make more sense for story:continue(0) to return the current paragraph, and it would solve the problem.

Threads

🧵 Threads allow to compose sections of content from multiple sources in one go.

Currently, using threads leads to stack overflow because the narrator recognizes them as a recursive divert 🙃.

Inline condition results must be not trimmed

VAR condition = false

I like^{ condition : apples | oranges }^ but you don't.
~ condition = true
I like^{ condition : apples | oranges }^ but you don't.

Inkey output:

I like^ oranges ^ but you don't.
I like^ apples ^ but you don't.

Narrator output:

I like^oranges ^ but you don't.
I like^apples ^ but you don't.

VARS declared before first knot not listed under global variables table entry

Input ink file:

VAR a = "foo"
VAR b = "bar"
CONST c = 42
CONST d = "Number 42"

-> first_knot

== first_knot ==

In the knot a = {a}
In the knot b = {b}

-> second_knot

== second_knot ==
Then we change them :
~ a = "baz"
~ b = "bazbar"

Now a = {a}
and b = {b}

->DONE

Convert to lua using narrator 1.2 produces :

return {variables={},lists={},inclusions={},constants={d="Number 42",c=42},version={tree=1,engine=1},tree={first_knot={={"In the knovt a = #a#","In the knot b = #b#",{divert="second_knot"},""}},second_knot={={"Then we change them :",{value=""baz"",var="a"},{value=""bazbar"",var="b"},"Now a = #a#","and b = #b#",{divert="DONE"}}},={={{value=""foo"",var="a"},{value=""bar"",var="b"},{divert="first_knot"}}}}}

We see at the beginnig of the output :
As expected : constants={d="Number 42",c=42}
Not expected : variables={}

We only see variables a and b being outputted as variable definitions within the second knot, en their value is set the second time. They are present at the end of the output, but not at a place we would expect them.

Greetings, and thanks for writing narrator!
Wyzoom

Evaluating a path immediately triggers any function and variables in it

Hi, I know I might be digging back this library, but I'm happy to try and explore solutions for this!

I've been working on porting this library to work on Playdate (meaning needed some simplification), but I'm also trying to get it to evaluate ink knots/paragraph line by line. While I can get the text to work as expected using the continue(n) function, the problem I have is that any variable that get set in it or any functions that get called, are triggered when the path is jumped to.

This can be tested using the example code and the readme, and calling this "call_911" knot

EXTERNAL phonebook(name,number)
EXTERNAL test(pouet)

=== function phonebook(name,number) ===
// Placeholder for phonebook function
~ return 1

=== function test(pouet) ===
// Placeholder for phonebook function
~ return 1

=== call_911 ===
{call_911 >= 2: I've seen you before... }
You've just called the emergency services
<i>Test</i>
~ test("test")
~ phonebook("Emergency","922")
[i]Test2[/i]
Please don't
We have shit to do 
And this is a very long thing
It should remove some lines from before
Let's hope so
-> DONE

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.