Giter Club home page Giter Club logo

tinta's Introduction

Tinta

This is a lua port of inkle's ink, a scripting language for writing interactive narrative.

tinta is fully compatible with the language (see missing features below for what's missing in the engine), has zero dependency and is known to work with love2d and the playdate sdk.

Installation

Clone this repository and add the tinta/source directory to your project as tinta.

Writing and compiling Ink

tinta only implements a runtime for ink, you will need to use a third party compiler (the original inklecate or the inkjs compiler) to compile your ink files to json.

Running your ink

For performance reasons, tinta is not able to run the compiled json files directly. Instead, you will need to convert the json to lua using the provided json_to_lua.sh or json_to_lua.ps1 command line tool.

json_to_lua.sh my_story.json my_story.lua

Note that you might need to change the script execution policy if you want to run the ps1 script.

json_to_lua.ps1 my_story.json my_story.lua

Once converted, you can import your story and run it.

local storyDefinition = import("my_story")

Story = import('tinta/engine/story')
story = Story(storyDefinition)

2 examples loop to run the story are provided in the run.lua file

A simple synchronous version:

    --- SIMPLE SYNC VERSION
    while story:canContinue() do
        local t = story:Continue()
        io.write(t)
        local tags = story:currentTags()
        if  #tags > 0 then
            io.write(" # tags: " .. table.concat(tags, ", "), '\n')
        end
    end

A more complex asynchronous version for limited environments (like on the playdate):

    --- ASYNC VERSION
    local textBuffer = {}
    repeat
        if not story:canContinue() then
            break
        end
        story:ContinueAsync(300)
        if story:asyncContinueComplete() then
            local currentText = story:currentText()
            local currentTags = story:currentTags()
            table.insert(textBuffer,{
                text = currentText,
                tags = currentTags
            })
        end
    until not story:canContinue()

Saving and Loading

Saving would return a lua table representing the current state of the story.

local saveData = story.state:save()

-- if on playdate
playdate.datastore.write(saveData)

Loading overwrites the current state of the story with the saved data

--if on playdate
local saveData = playdate.datastore.read()

story.state:load(saveData)

Variable Observers

Variable Observers are functions that get called whenever a variable declared in ink is changed.

You can add a variable observer like this:

-- Anonymous function (if you don't remove them later)
story:ObserveVariable("myVarDeclaredInInk", function(varName, val) 
        -- do stuff
		print(varName.." changed to ".. tostring(val))
end)


-- Named function
local function MyVarObserver(varName, val)
    -- do stuff
    print(varName.." changed to ".. tostring(val))
end
story:ObserveVariable("myVarDeclaredInInk", MyVarObserver)

Note that variable observers are identified by function addresses, adding the same observer multiple times is the same as adding it only once.

Additionally, you could add the same observer to multiple variables:

-- Anonymous function (if you don't remove them later)
story:ObserveVariables({ "myVarDeclaredInInk1", "myVarDeclaredInInk2" }, function(varName, val) 
        -- do stuff
		print(varName.." changed to ".. tostring(val))
end)

Removing variable observers:

-- Remove all observers on myVarDeclaredInInk
story:RemoveVariableObserver(nil, "myVarDeclaredInInk")

-- Remove a specific observer on all variables
story:RemoveVariableObserver(MyVarObserver, nil)

-- Remove a specific observer on myVarDeclaredInInk
story:RemoveVariableObserver(MyVarObserver, "myVarDeclaredInInk")

External Functions

External Functions are lua functions that can be called from ink.

Binding External Functions:

local MyFunc(args)
    -- do stuff
end

-- by default, external functions are not look ahead safe.
story:BindExternalFunction("functionNameDeclaredInInk", MyFunc)

-- if your function is look ahead safe:
story:BindExternalFunction("functionNameDeclaredInInk", MyFunc, true)

Fallbacks are enabled by default. Which means if an external function is called but none has been bound, we call the fallback function defined in ink.

The first Continue() call will validate all external function bindings. If you forgot to bind an external function while the function has no fallback (or fallback is disabled), it throws an error.

You can't bind multiple functions to the same external function declaration. If you try to do so, it throws an error.

Note that external function only receives a table as its first argument. If you declare your function in ink like this:

EXTERNAL someFunction(argumentA, argumentB)

Your lua function should be:

function someFunction(args)
    local argumentA = args[1]
    local argumentB = args[2]
    -- do something with argumentA and argumentB
end

You could remove bindings, but in that case you should have a fallback defined in ink or bind with another lua function immediately afterwards. Otherwise calling that function would result in error.

story:UnbindExternalFunction("functionNameDeclaredInInk")

Unbinding a function that hasn't been bound throws an error.

You could check if a function has been bound by using the following:

-- returns nil if the function hasn't been bound. 
local externalFunc = story:TryGetExternalFunction("functionNameDeclaredInInk")

Flows

Flows exist even if you don't use them. Every story has a default flow named DEFAULT_FLOW.

Some nottable getters:

story:currentFlowName()

-- true if current flow is "DEFAULT_FLOW"
story:currentFlowIsDefaultFlow()

story:aliveFlowNames()

To create or switch to a named flow:

story:SwitchFlow("MyFlow")
-- Convenient function to switch to default flow
story:SwitchToDefaultFlow()

This will create a new flow if that flow is not found. When this happens, the newly created flow doesn't know where it should be, and calling Continue would not advance the story, thus you must use story:ChoosePathString(...) to specify where that flow should start.

Note that, though temporary variables and callstacks are flow-specific, global variables are shared between flows.

You could remove a flow:

story:RemoveFlow("MyFlow")

You can't remove the default flow.

CLI Player

A fancy one-liner to run your story from the command line. From the source folder of the repository, run:

TMPSUFFIX=.lua; lua run.lua =(../json_to_lua.sh /path/to/your/game/my_story.ink.json >(cat ))

Useful commands when prompted for input are:

  • save to save the current state of the story
  • load to load the last saved state
  • -> your_knot to jump to a specific knot
  • quit or q to quit the story

Toybox

tinta is also available using the toybox package manager.

pip install toyboxpy
toybox add smwhr/tinta

then in your lua code:

import "../toyboxes/toyboxes"

local my_story = import("my_story")
local story = Story(my_story)

Löve2D

Download the full source code and copy the source folder inside your Löve2D game directory.
Rename this folder tinta.

then in your lua code:

Story = require("tinta/love")

local my_story = import("my_story")
local story = Story(my_story)

Notably missing features

  • Global event broadcasts (e.g. whenever story continues)

Feel free to contribute to the project if you need any of these features.
The lua code is a straight port of the original ink code, so it should be easy to port missing features.

tinta's People

Contributors

myonmu avatar smwhr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

myonmu

tinta's Issues

External Function Call crashes if values are string

Hello!

I've been working on porting my project from Narrator to Tinta and ran into an issue.

For info, I am developing on Playdate, with the 2.4.2 SDK as of writing, using a Windows PC and Visual Code

I have an external function that takes strings for arguments. However, when the project hits it, an exception occurs in CallExternalFunction on line 1168 because the poppedObj does not contain a valueObj.

Note that I might be wrong in my assumtion, but here is the ink I use to test it using the tinta test project:

EXTERNAL phonebook(name,number)

=== start ===
This is a test for functions

~ phonebook("Emergency","922")

This is the end of the test

-> DONE

And my only addition to make it run on the test project were:

function addPhonebook(name,number)
    print(name)
end

Then right after the story is initialised

story:BindExternalFunction("phonebook", addPhonebook, true)

Followed by a ChoosePath at the end of the setupGame() function

story:ChoosePathString("start")

Hopefully that gives you enough information, let me know if there anything else I should try

And thanks for making this library!

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.