A Javascript library for loading and executing interactive stories written in Articy. Includes full feature parity with the Unity and Unreal Articy plugins alongside additional features like more custom hooks into flow iteration and inline script calls embedded in display text.
Note that while this package features full feature parity with the Unity plguin, it doesn't share the same API.
Just install using npm
or yarn
and you're ready to go.
yarn add articy-js
npm install articy-js --save-dev
Check out the full Typedoc generated documentation here.
First we'll see how to get data from your Articy project into your Javascript application or game.
To load a story from Articy, you'll need to export it to a JSON file and store it somewhere within your project. This'll be the file loaded by the runtime.
If you're exporting a project using localization, make sure the accompanying .xlsx
files are copied alongside the JSON.
To load the data you've exported, you'll create an instance of the Database class. The Database
object is read-only and you will only ever need to create one instance of it per story. For most projects, this'll mean you only need one.
If you're using Javscript modules, the best way to manage these instances is to initialize them in their own module and export them as the default export.
// Example GameDB.ts
// Import data from the exported json
import GameData from "./exported.articy.json";
import { Database } from "articy-js";
// Create a new database
const GameDB = new Database(GameData)
// Export the database
export default GameDB;
To access it from another file, just use
import GameDB from "GameDB"
The Database
class will give you access to objects within your project such as Entities, Flow Fragments, Dialogues, etc. Objects are accessed by their Unique ID which you can find in the Properties Inspector in Articy. All IDs begin with 0x
.
To access an object, use the getObject
method.
import GameDB from "GameDB";
import { FlowFragment } from "articy-js";
// Get a flow fragment by ID
const fragment = GameDB.getObject("0x01000000000018A3", FlowFragment);
console.log("Flow fragment text: ", fragment.properties.Text);
You'll notice getObject
takes two parameters: the unique ID, and the Type. The Type is a Javascript class which helps tell the runtime how to handle the object data and gives access to both the properties
and template
data on the object.
You can create your own types to handle specific templates (see below) but included in the runtime are all the types you need for basic Articy objects like FlowFragment
, DialogueFragment
, Entity
, Location
, etc. There are also base types like BaseFlowNode
and ArticyObject
if you want to be less discerning about the type.
If there's a mismatch between the actual type of the object and the type past into getObject
, then it will return undefined
. Also, even if a base type like BaseFlowNode
is passed in as the type, getObject
will return an object of the most specific type the runtime knows about that matches the database object. So if you pass BaseFlowNode
and the real type of the object is a Flow Fragment, you'll get an object of type FlowFragment
.
Some types like will automatically fetch associated objects for your convenience. For example, DialogueFragment
will automatically fetch the associated speaker and store it in Speaker
and flow nodes will automatically fetch all their Input and Output pins.
While Articy works great as a Database, the main reason you're probably using it is to create branching story flows in the Flow Editor.
The model for flow iteration is styled after libraries like Redux, with various reducer methods which consume a GameFlowState
and return a new, updated state.
// First, we need to create a configuration to tell the runtime what nodes to 'stop' at. In most simple games, this'll be just DialogueFragment nodes.
const iterationConfig: GameIterationConfig = {
stopAtTypes: ["DialogueFragment"]
};
// Use startupGameFlowState to create a new flow state beginning at the given node
const [initialState] = startupGameFlowState(GameDB, "0x01000000000018A3", iterationConfig);
// Access information about the current state
console.log("Current node id: ", initialState.id);
console.log("Value of variable Test.X: ", initialState.variables["Test"]["X"]);
console.log("Number of branches: ", initialState.branches.length);
// Move down the first (0th) branch
const [nextState] = advanceGameFlowState(GameDB, initialState, iterationConfig, 0);
// Refresh the branch set
const stateWithRefreshedBranches = refreshBranches(GameDB, nextState, iterationConfig);
You'll notice that startupGameFlowState
and advanceGameFlowState
actually return an array, with the new state as the first element. This is because they also return a second element, the object represented by the new state's id, for convenience's sake so you don't have to call getObject
immediately after.
For example,
// Use startupGameFlowState to create a new flow state beginning at the given node
const [initialState, initialNode] = startupGameFlowState(GameDB, "0x01000000000018A3", iterationConfig);
console.log("The startup node's text is ", initialNode.properties.Text);
You can further customize flow control using the parameters in the GameIterationConfig
parameter.
Firstly, you can add additional types to the stopAtTypes
list. Unlike the Articy Unity runtime, this list can include not just base types like FlowFragment and DialogueFragment but also names of templates defined by you in Articy.
You can also define stoppage based on the presence of certain features using stopAtFeatures
. Just supply a list of Feature names and any nodes containing them will be considered stops.
For even finer grained control, you can specify a customStopHandler
function. This method is called on any node that matches stopAtTypes
or stopAtFeatures
and returns a CustomStopType
. The method is passed a reference to the given node as well as other state information by which it can make its decision to actually trigger a stop, continue, or carry out a more complex operation.
Here's a quick example that only stops on DialogueFragments but also FlowFragments if and only if their Display Text begins the string STOP:
const iterationConfig: GameIterationConfig = {
stopAtTypes: ["DialogueFragment", "FlowFragment"],
customStopHandler: node => {
// Check if the node is a flow fragment
if(node instanceof FlowFragment) {
// If so, check if it's display text begins with STOP:
if(node.properties.DisplayText.startsWith("STOP:")) {
// If so, stop as normal
return CustomStopType.NormalStop;
} else {
// Otherwise, continue past as if this wasn't marked as stop
return CustomStopType.Continue;
}
}
// We don't need to do anything for any other cases down here. If customStopHandler returns nothing, it falls back on its default behaviour.
}
}
We may have nodes in Articy we'd like to perform some kind of action without actually triggering a stop. For example, we may have a Hub template called PlayMusic
that's meant to switch the currently playing music track in our game. Or SetBackground
which changes the current scene. Neither of these contain dialogue, so we don't actually want them to appear as choices or nodes we stop on, but we do want to trigger some code whenever they're run.
We can do this by registering Template or Feature Execution handlers. These are global callbacks that are triggered whenever flow iteration passes over either a given Template or a Template with a given Feature.
/* Let's say you created a new Feature in Articy called MusicSettings. It has one string in it called SongName.
* We want to run some special music code whenever any node with that feature is executed as part of iteration.
* This will work even if the node in question is not "stopped at" in iteration, and just passed over.
*/
RegisterFeatureExecutionHandler("MusicSettings", "MyMusicSettingsHandler", (db, feature, node, state) => {
const musicToPlay = feature.SongName;
// do something with musicToPlay
alert("Now playing:" + musicToPlay);
});
Execution handlers are passed a reference to the current database, the feature or template being executed, the parent node, and the current value of the GameFlowState
during execution (where you can access visit counts, variables, etc.).
You can also register new script functions for use in Expresso scripts. They can take any number of arguments of the supported types (number, string, boolean).
// Register a new IsGreater function that takes two arguments
RegisterScriptFunction("IsGreater", (context, arg1, arg2) => {
return arg1 > arg2;
});
The first argument past to each function handler is a context
object with information about the current execution. This is useful if you want the function's behaviour to change based on parent node, for example.
This package supports all the built-in functions documented at Articy Unity Plugin with the exception of setProp due to some implementation complications (but I'm working on it). This includes the helper objects of speaker
and self
where they are appropriate.
We also include four extra built-in methods.
once()
will return true if and only if the owning node has NOT been visited. It's a great way to make choices that can only be chosen once. Simply add once()
to their input pin.limit(n)
works similarly, but only returns true if the node has been visited less than n
times.visited()
returns true if the current node has been visited before.visits()
returns the number of times the current node has been visited.Suppose you want to write a custom script function that modifies your game state in some way (say, moving the player around on the screen or something) but you're using Redux. States are immutable. What can you do?
You can use the createScriptDispatchMiddleware
included with this package. This addon not only gives script function handlers access to your current Redux store, but also gives them the ability to dispatch actions.
To use it, simply add it to your applyMiddleware
call like so:
// Make the extensions
const extensions = compose(
applyMiddleware(createScriptDispatchMiddleware())
);
// Create store
const store = createStore(reducer, initialState, extensions);
Now, script functions can not only access your application state, but they can also trigger actions using the Javascript yield
keyword.
// Make sure your script function is a _generator_ function.
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
RegisterScriptFunction('MovePlayerTo', function* (context, x, y) {
// Read your application state in context.state
// Trigger Redux actions
yield { type: 'redux/move_action', x, y };
});
This will queue a Redux of action of type redux/move_action
to be executed as soon as iteration is complete. This will only work if your iteration calls are happening during a Redux action of course.
This also works for RegisterFeatureExecutionHandler
. Simply define the handler as a generator function and yield actions just as above.
In most cases, you will not need to define new object types. This is only necessary if you want to customize the iteration functions (such as next
or execute
) or want to add new helper methods.
To register a new object type, define a new class deriving from either Entity
, DialogueFragment
or whatever base class is most appropriate. Then, mark the class with the @ArticyType
decorator (or use RegisterDatabaseTypeClass
if you don't have decorator support).
@ArticyType('MyCustomDialogueFragment') // this must match the technical name of your custom Template in Articy
class MyCustomDialogueFragment extends DialogueFragment // if using Typescript, you can add a <TemplateType> here to add type support for the template's features
{
execute(context) {
// custom logic when this node is executed in flow iteration
}
}
If you're using Typescript, you can type the feature
argument to an interface matching the spec in Articy.
To support asset loading (such as images), just add an asset resolver function to the Database constructor. This method should, given an asset reference string in Articy return the assets filename. In webpack, this is easy.
import { Database } from "articy-js";
// Import game data from json
import GameData from "./game.articy.json";
// Asset resolver method
function assetResolver(assetRef: string) {
// Articy exports all assets into an "Assets" folder relative to the exported .json file
// Use webpack's require method to bundle these and map their filenames
return require("./Assets/" + assetRef);
}
// Create database with resolver
const GameDB = new Database(GameData as ArticyData, assetResolver);
// Export
export default GameDB;
Now, given an Asset ID (from, say, a game entity) you can now get the full filename with the Database's getAssetFilename
function.
// Load an entity by ID
const entity = GameDB.getObject("0xFFFFFFFF", Entity);
// Get its preview image
const assetFilename = GameDB.getAssetFilename(entity.properties.PreviewImage.Asset);
This package supports Articy's localization system. When loading a project with localization turned on, you'll use the localization
object within the Database
class to load and manage your game's localized text.
Note that this class can't load .xlsx
files directly. Instead, it requires a JSON object that maps localization IDs to strings. You can get this by loading the xlsx file yourself using a library like xlsx or exceljs or using our articy-xlsx-loader if you're using Webpack.
Objects accessed via getObject
will automatically have their properties localized to whatever the active language is. You can change the active language at any time using the Localization objects' active
variable. This will automatically update the text in all loaded objects, meaning you don't have to worry about holding onto objects loaded via getObject
.
import { Database } from "articy-js";
// Import game data from json with localization turned on
import GameData from "./game.articy.json";
// Create database
const GameDB = new Database(GameData as ArticyData);
// Load the localizations. Uses [articy-xlsx-loader](https://www.npmjs.com/package/articy-xlsx-loader) to parse these .xlsx files into JSON objects mapping localization IDs to strings.
GameDB.localization.load('en', require('./loc_All objects_en.xlsx'));
GameDB.localization.load('fr', require('./loc_All objects_fr.xlsx'));
// Set French as the active language
GameDB.localization.active = 'fr';
// Use GameDB as usual, all localization will be carried out automatically.
Using the processInlineScripts
function, you can evaluate scripts embedded in text to create more responsive games. The syntax for inline scripts is lifted from the Lists and Variable Printing features of Inkle's Ink language.
Examples:
Some display text {show this the first time|show this after the first time}.
Print out the value of a variable: {MyNamespace.MyVariable}
Do some {~shuffling|randomizing|random rearranging} of text.
Print text conditionally to know if a variable {MyNamespace.MyBoolean:is true|is false}.
Make switch statements {
- MyNamespace.MyInteger == 3: that work.
- MyNamespace.MyInteger == 4: that work well!
- else: that are great :)
}
Constructor type for ArticyObject and any of its subclasses.
Sized box. Has position and size.
Definition of a property in a feature
Base export interface for all feature data
Type used to store Articy's GUIDs (just a string)
Preview Image Data
Represents a 2D transform
Union of types that the value of an Articy Global Variable may have. See VariableNamespace.
Empty game flow iterator. Use this to initialize your game state
Empty globals state
Null ID. Never maps to a real node in the Database.
Empty slim game flow state
Decorator alternative to @see RegisterDatabaseTypeClass Add on top of classes you want to register as Articy Database Types
Type name (must match the Template's Technical Name or Class Name in Articy)
Clears all registered feature handlers
Clears all registered template handlers
Registers a javascript class to be instantiated whenever encountering an Articy object of a given type
Type name (must match the Template's Technical Name or Class Name in Articy)
Constructor that creates objects of this type
Registers a handler function called whenever a node with a given feature is executed in advanceGameFlowState.
Technical name of the feature
Unique ID that prevents this handler from being registered twice
Function to call
Makes a Javascript function available to Expresso scripts
Function name (to be used in Articy)
Handler
Registers a handler function called whenever a node with a given template is executed in advanceGameFlowState.
Template technical name
Unique ID that prevents this handler from being registered twice
Function to call
Checks if a script method is properly registered. Logs an error if not.
Script method specification (name and return type)
If a method of that name is registered
Advances a GameFlowState along a particular branch until the next terminal node is hit.
Database
Current GameFlowState
Configuration settings which determine which nodes are considered 'terminal'.
Branch index to follow
A new game flow state with a list of available branches. Also returns the current node to avoid unncessary lookups.
Same as advanceGameFlowState but seperates out the globals of the iterator
Existing globals
Database
Current SlimGameFlowState
Configuration settings which determine which nodes are considered 'terminal'.
Branch index to follow
A new game flow state with a list of available branches. Also returns the current node to avoid unncessary lookups.
Advances a basic flow state one node down a branch.
Database
Current flow state
Branch index to follow (-1 to only follow if there is exactly one path)
The new flow state and the new current node (used to avoid unnecessary lookups)
Checks if the destination of a branch is a given type
Flow branch to check
Database
Type to check for
If the flow state has no branches, "complete" the flow by running any remaining pins or instructions after the current node.
If there are branches, this just returns a nulled out iterator with the variable and visits sets intact.
Database
Current state
a null iterator with the up-to-date variables and visits
Same as completeFlow but seperates out the globals of the iterator
Existing globals
Database
Current state
a null iterator with the up-to-date variables and visits
Creates a Redux middleware that gives script function handlers registered via RegisterScriptFunction access to your application's current Redux state (via [[Context.state]]).
Also allows script function handlers, feature handlers, etc. to yield return Redux Actions which will automatically be executed at the end of the current reducer run (or end of the next run if the reducer is not running). You can use this feature by registering your methods via RegisterScriptFunction, etc. as generator methods. Then using the yield
keyword.
Example:
RegisterScriptFunction('MovePlayerTo', function* (context, x, y) {
// Read your application state in context.state
// Trigger Redux actions (you can do more than one)
yield { type: 'redux/move_action', x, y };
yield { type: 'redux/enemies_move' };
});
You can optionally pass in a finalizeAction
parameter to this middleware. Anytime Redux actions are triggered and run from ScriptFunctions, at the end this finalizeAction will be posted to the reducer.
Resolves all branches in a list whose destination matches a given type
Flow branch list
Database
Type to check the destination again
Creates a second "thread" in a flow iterator starting at a given node.
Suppose the existing flow state is stopped at X with 3 branches A, B, and C.
If we merge this state using mergeGameFlowState
with a start
of Y is terminal and has branches M and N then we'd get a new GameFlowState with the following properties:
The two flow states, the original and the new one starting at Y, have merged. This is called 'pageing'. Your flow state now has multiple pages which are being displayed as one.
Calling refreshBranches or advanceGameFlowState work as normal along any of the sets of branches. Advancing will collapse the state into a single page again, ending up at the destination at the end of that branch.
This method is called automatically during iteration whenever the GameIterationConfig.customStopHandler returns CustomStopType.CreatePage
Articy database
Current flow iterator
Iteration configuration
Id to start the new thread from. Will iterate until it finds something that matches the stop types of config and merge branches/pages
Merged iterator
Same as mergeGameFlowState but seperates out the globals of the iterator
Existing globals
Articy database
Current flow iterator
Iteration configuration
Id to start the new thread from. Will iterate until it finds something that matches the stop types of config and merge branches/pages
Merged iterator
Takes text and parses it for inline scripts and lists that match Inkle Ink's "Lists" and "Variable Printing" syntax. See https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#1-basic-lists And https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#printing-variables
Text to process for inline scripts
Execution context (includes variables and visit counts)
ID of the node this text is from (used for script execution)
Parent database (used for script execution)
The input text where all inline scripts are replaced with their values
Refreshes the branch list of a GameFlowState.
Useful if you've manually adjusted the value of variables and you want to make sure your branches are accurate.
Database
Current game flow state
Game iteration config settings
Same as refreshBranches but seperates out the globals of the iterator
Existing globals
Database
Current game flow state
Game iteration config settings
Resolves a branch by converting all its IDs into Flow Node objects.
Flow branch
Database
Resolves a list of branches all at once using @see resolveBranch
List of flow branches
Database
Runs a condition or instruction script
Script to run
Global variable store
Visit set information
Id of the node calling this function
Database
If true, expect a boolean return. Otherwise void or a specific type like string or number.
If set, override the state context past to functions
Runs a condition or instruction script
Script to run
Global variable store
Visit set information
Id of the node calling this function
Database
If true, expect a boolean return. Otherwise void or a specific type like string or number.
If set, override the state context past to functions
If the return value of the script is truthy
Runs a condition or instruction script
Script to run
Global variable store
Visit set information
Id of the node calling this function
Database
If true, expect a boolean return. Otherwise void or a specific type like string or number.
If set, override the state context past to functions
The string return value of the script, or "" if it is not a string
Runs a condition or instruction script
Script to run
Global variable store
Visit set information
Id of the node calling this function
Database
If true, expect a boolean return. Otherwise void or a specific type like string or number.
If set, override the state context past to functions
The numerical return value of the script, or 0 if it is not a number
Runs a condition or instruction script
Script to run
Global variable store
Visit set information
Id of the node calling this function
Database
If true, expect a boolean return. Otherwise void or a specific type like string or number.
If set, override the state context past to functions
The raw result of the script executed (be it a string, number, boolean, or something else)
Creates a new flow state beginning at the first terminal node found by starting at a given ID.
Database
Starting ID. The returned state will either point to this node (if it's a terminal) or it'll find the first terminal by iterating along the first branch.
Configuration settings which determine which nodes are considered 'terminal'.
Optional existing game state to migrate variables and visits from.
A new GameFlowState ready for iteration with advanceGameFlowState.
Same as startupGameFlowState but seperates out the globals of the iterator
Existing globals
Database
Starting ID. The returned state will either point to this node (if it's a terminal) or it'll find the first terminal by iterating along the first branch.
Configuration settings which determine which nodes are considered 'terminal'.
A new Globals and SlimGameFlowState ready for iteration with advanceGameFlowStateWithGlobals.
Generated using TypeDoc
All possible values of the Class string