@esaevian/genmo-v2
v2.7.1
Published
## New, updated, sleeker, sexier, adjectivier, and cheesier than ever! 🧀
Downloads
21
Readme
📕 Genmo v2 📗
New, updated, sleeker, sexier, adjectivier, and cheesier than ever! 🧀
Genmo is a text narrative engine that is meant to be pluggable into any sort of frontend.
Table of Contents
Installation / Quick Start
Install
yarn add @esaevian/genmo-v2
# or
npm install @esaevian/genmo-v2
Quick Start
- Install Twine and the Twison story format
- Write your story in Twine, save the JSON within your project.
- Import Genmo, initialize it with your story data, and define an
outputFunction
to display the current passage:
import { Genmo } from "@esaevian/genmo-v2";
import MyStoryJSON from "./MyStory.json";
const story = new Genmo(MyStoryJSON, {
outputFunction(passage) {
// Display `passage.text` for user, and `passage.links` for navigation
},
errorFunction(err) {
// Deal with `err`
},
});
Generating Stories
Stories are in JSON format, as written in Twine using the Twison story format. Create your story in Twine as usual, then use the Twison story format to export your story to a .json
file.
Currently, no Harlowe functions are available (unless you implement them in your outputFunction
, see below). Any directives will be output in the JSON with zero processing.
If you do not want your links to show up in the body text, you can separate them with a newline, 3 dashes, and another newline:
Normal passage text.
---
[[Page 2]]
[[Page 3]]
This will generate a passageText
variable on each passage that only has the text above that divider.
Story Data
You can update story data upon entering a passage by including a JSON object describing the data you want to change under the links with a similar separator for the link
Normal passage text.
---
[[Page 2]]
[[Page 3]]
---
{
"hp":10,
"wizard_staff": true,
"player_name": "Steve"
}
Data is cumulative, so you don't need to specify the entire data object in each passage. You can use ++
or --
prefixes to increment/decrement a variable by a value:
The orc hits you for 2 damage!
---
[[Fight back]]
---
{
"hp":"--2"
}
After the last two example blocks, hp
will be set to 8
You can also define story data using a Handlebars helper. Click here to read more about the helpers
Inserting data into text
Genmo uses Handlebars to replace variables from your data within the text. For example:
The orc hits you for 2 damage!
Your HP is {{hp}}
---
[[Fight back]]
---
{
"hp":"--2"
}
passageText
will be:
The orc hits you for 2 damage!
Your HP is 8
Prompting for data
Data can also be prompted for by the player. Any data keys that need to be prompted can be indicated in the data JSON with a >>
. For example:
Normal passage text.
---
[[Page 2]]
[[Page 3]]
---
{
"hp":10,
"wizard_staff": true,
"player_name": ">>"
}
This will cause Genmo to return a passage object that will indicate that player_name
needs input from the player. Otherwise, any instance of #{player_name}
in passageText
will not be replaced.
See respondToPrompt for the API on how to properly log player responses.
Conditional Links
You can use named links (using ->
) in order to specify conditions for the link to appear, separated from the name with ||
:
Normal passage text
---
[[Go home]]
[[Go to the hospital||hp lt 10->Go to the hospital]]
This will only display Go to the hospital
if the variable hp
is less than 10
The operators available are:
lt
: less thangt
: greater thanlte
: less than or equalgte
: greater than or equaleq
: equalsseq
: strict equals (===
)
lt
, gt
, lte
, and gte
will convert any variable to a Number before comparison.
The displayed name will also have anything after ||
removed before display/outputFunction
Managing Player Inventory
Genmo can keep track of a player's inventory as a set of key/value pairs where the key is the name of the item, and the value is how many they have. Currently, Genmo only supports adding one of an individual item at a time (i.e. per passage). For player resources like currencies, use data instead.
Inventory is indicated in the story text via the inventory_add
and inventory_remove
keys:
Normal passage text.
---
[[Page 2]]
[[Page 3]]
---
{
"inventory_add": "wand_of_gamelon"
}
This will add one (1) wand_of_gamelon
to the player's inventory. Conversely:
Normal passage text.
---
[[Page 2]]
[[Page 3]]
---
{
"inventory_remove": "broken_sword"
}
Will remove one (1) broken_sword
from the inventory. Note that inventory items cannot go negative.
Recommendation: Genmo's inventory is a simple key/value pair, looking something like this:
{
"bounce_bracelet": 1,
"magic_ring": 2,
"bread_loaves": 5
}
Naturally, there isn't a lot of information in this about what the inventory items actually are. The keys in this object can (should?) be used as keys in your application's item database so details about items (descriptions, images, flavor text) can be kept in application code.
Recommendation: Keep in mind that inventory items are usually only added or removed one at a time. While Genmo does offer the ability to add multiples of an item, consider whether you want to use the inventory system for this, or if you just want to use a standard data. For example, while "The Haunted Coin of Gorgnax" would be a good inventory item, "coins" used as currency (which the player can have tens, hundreds, or even thousands of), would be better used as a standard piece of data.
The inventory can also be handled via Helper blocks. Click here to read about inventory helpers
Passage Helpers
Some helpers are available to modify your text on the fly. Shortcodes are typically follow the following format:
{{#shortcodeName arg="argument1"}Text content here to be modified by the shortcode{{/shortcodeName}}
If you are familiar with Handlebars, you'll notice that shortcodes look a lot like block helpers. That's because they are! That means the built-in Handlebars helpers are available for you to use, as well as custom shortcodes mentioned below.
Note: While not yet available, I'm hoping to add the ability to supply your own custom helpers in the initialization of Genmo.
inventory_has
and inventory_not_has
These shortcodes will show/hide the text content based on whether the player has (or doesn't have) a set of specified items in their inventory.
{{#inventory_has items="coin"}}You have a coin!{{/inventory_has}}
In the items
argument, you can supply a space-separate list of items that the player must have ALL of in order for the text to show:
{{#inventory_has items="coin book"}}You have a coin and a book{{/inventory_has}}
inventory_not_has
works the same, but asserts that a player doesn't have an item. If multiple items are specified, the player must not have any of the items in the list:
{{#inventory_not_has items="tea coffee"}}You don't have tea OR coffee{{/inventory_not_has}}
changed
This will let you show/hide text based on whether a piece of data change on the last link navigation (i.e. usage of JSON data/inventory management, the {{#data_set}}
helper, or the {{inventory_add/remove}}
helpers). Manually updated data/inventory (as in, using updateData
or updateInventory
directly) will not be counted as a valid change for this.
{{#changed keys="age"}}Your age changed!{{/changed}}
---
[[Next]]
---
{
"age": 17
}
This will show Your age changed!
if, upon entering this passage, the age
was updated to 17. However if the player leaves this passage and returns (without age
changing somewhere else), the message will not show.
Note that you can also specify an inventory
attribute to check if something in the user's inventory has changed. However, if both inventory
and keys
are present, only keys
will be checked.
{{#changed inventory="coin"}}The number of coins you have changed{{/changed}}
Right now, there isn't a way to determine whether the change was positive or negative (or neither, in the case of a string change).
NB: While you can store complex objects in your data, the {{#changed}}
helper may not be super helpful, due to how Javascript compares objects, they will always have changed.
Data Helpers
Much like the passage helpers above, there are other sets of helpers that allow you to modify the data of your story, if you don't want to use the JSON method outlined above.
data_set
and passage_data_set
These are helpers to set data and passage data, like you would in JSON. The following passages are equivalent:
This Passage is using JSON data setting
---
[[Next]]
---
{
"fruit": "orange",
"passage_data": {
"fridge_open": true
}
}
This passage is using data setting via helpers.
{{#data_set fruit="orange}}{{/data_set}}
{{#passage_data_set fridge_open="true"}}{{/passage_data_set}}
---
[[Next]]
inventory_add
and inventory_remove
Like data_set
and passage_data_set
, these are analogous to using inventory_add
and inventory_remove
JSON keys.
I giveth a bike and a shirt and taketh away a Playstation.
{{#inventory_add items="bike shirt"}}{{/inventory_add}}
{{#inventory_remove items="playstation"}}{{/inventory_remove}}
You can also specify a condition under which the inventory change will be made, similar to Conditional Links:
{{#inventory_add items="alcohol cigarettes" condition="age gte 18"}}{{/inventory_add}}
Comments
You can insert comments into your Twine story (in you need to notate some particularly complex story decision) using standard Handlebars comments.
{{! NOTE: Zebulon wouldn't do this, rewrite this passage with a Hoobastank song instead}}
Zebulon steps up on stage and blasts out a perfect rendition of Bohemian Rhapsody.
Usage
import { Genmo } from "genmo-v2";
// Load in your JSON however you like
import StoryJSON from "story.json";
const story = new Genmo(StoryJSON, {
outputFunction: (passage) => {
// output passage
},
errorFunction: (err) => {
// deal with error
},
});
Note that Genmo
is not a default export, but a named one. In CommonJS:
const Genmo = require("genmo-v2").Genmo;
Options
outputFunction
const outputFunction = (passage) => {};
You will receive the entire "passage" object from the StoryJSON generated by Twine:
text
: Main passage text (including links and unprocessed Harlowe directives)links
: Array of links in this passagename
: The display text for this linklink
: The name of the passage this link links topid
: The Passage Id of the passage this link links to
name
: The name of this passagepid
: The Passage Id of this passageposition
: The position of this passage within Twinex
: x position of the passage. String.y
: y position of the passage. String.
needsPrompt
: An array of variables that require user input.key
: The name of the variablecomplete
: (Boolean) Whether we already have data for this key.
In addition you will recieve passageText
(passage.passageText
) which is a subset of the text above the link divider (see Generating Stories above)
Returning any value from the outputFunction
will also return that value from functions that "output" (i.e. Genmo.outputCurrentPassage
)
errorFunction
const errorFunction = (err) => {};
This will be called if you do anything illegal with Genmo (except call its constructor without storyData, in which case an Error
will be thrown).
If your errorFunction
returns a value, any function that calls your errorFunction
will also return that value.
Possible errors are available as an export named ERRORS
customHelpers
{
helper_name: (handlebarsOptions, options) => {
},
another_helper: (handlebarsOptions, options) => {
}
}
Supply your own set of Handlebars helpers for Genmo to use. handlebarsOptions
is the options object from Handlebars helpers, while options
contains genmo
, for you to access directly. Be sure to pass genmo.getData()
into handlebarsOptions.fn()
in order to properly interpolate any data within your custom block.
Ex.
new Genmo(StoryData, {
customHelpers: {
bold: (handlebarsOptions, options) => {
return `<strong>${handlebarsOptions.fn(
options.genmo.getData()
)}</strong>`;
},
},
});
import { ERRORS } from "genmo-v2";
API
new Genmo(storyData, options = {})
Constructs a new Genmo Object. Options are detailed above.
outputCurrentPassage()
story.outputCurrentPassage();
This function will call your outputFunction
with the current passage.
If your outputFunction
returns a value, it will be returned here as well.
getCurrentPassage()
story.getCurrentPassage();
Returns the raw passage object for the current passage. This is the same object that is passed to your outputFunction
.
followLink(Object|String)
story.followLink(link);
This will set the currentPassage
to the passage linked to in the provided link
. This will only work if the link you are trying to follow is on the currentPassage
. You may pass in the entire object from the links
array in the passage, or just the pid
of the passage that link goes to.
Attempting to follow an invalid link (the link doesn't exist, is not a part of this passage, or does not link to a passage that exists, etc.) will call your errorFunction
and return any value you had set there.
respondToPrompt(Object)
story.respondToPrompt({ variableName: "receivedValue" });
See Prompting for data.
If, in outputFunction
, passage
has a key called needsPrompt
, you should query the user for the keys involved (using passage.needsPrompt[i].key
). Once the user has provided an answer, you can call respondToPrompt
with an object mapping the variable name (key
) to the user's answer (using whatever method of input you desire).
Note that the object in respondToPrompt
will only handle one variable at a time. So if you have multiple prompts, you will call respondToPrompt
multiple times for each.
Once you call respondToPrompt
for a given key
, it will still show up in the passage.needsPrompt
array, however it will have a property complete
set to true
(i.e. passage.needsPrompt[i].complete
). It is up to you if you wish to re-prompt the user or leave the data as is.
Note this function will throw an error if you are attempting to respond to a prompt that does not exist on the current passage (story.getCurrentPassage()
).
getInventory()
story.getInventory();
This simply returns the key/value pairs to indicate quantities in the player inventory. This is the same as genmo.state.data.inventory
.
updateInventory(Object)
story.updateInventory(inventoryUpdateObject);
This will allow you to add items outside of specifying them in Twine. An object is provided which indicates which items are being updated, as well as a delta of their quantities:
genmo.updateInventory({
broom: 1,
fishhook: -2,
});
The above call will add one (1) broom to the inventory, and remove two (2) fishhooks.
setData(Object)
story.setData(myDataObject);
Allows you to set arbitrary data to the story's data.
getData()
story.getData();
Returns the current data object for the story
getPassageData(passage)
story.getPassageData(story.getCurrentPassage());
Gets the passage_data
object for the current passage i.e. transient data that is only set on the passage given.
getRawPassageData(passage)
story.getRawPassageData(story.getCurrentPassage()); // Or any other passage
Gets the parsed data for the current passage. Note that any deltas (i.e. { s: "--2"}
) will be returned unchanged, that is, as literal strings. Will return null if there is no data for the current passage. This includes passage_data
, if set. If you just want just passage_data
use getPassageData(passage)
addHelper(helperName, helperFn)
story.addHelper(
"bold",
(handlebarsOptions, options) =>
`<strong>${handlebarsOptions.fn(options.genmo.getData())}</strong>`
);
Adds a helper that will be used throughout the rest of the rendering of the story. See options.customHelpers
removeHelper(helperName)
story.removeHelper("bold");
Removes a helper with the name given, meaning blocks using this helper will no longer be rendered (their content will not display at all).
state
story.state;
story.state.storyData;
story.state.currentPassage;
story.state.data;
Genmo keeps track of its state in state
, there are several properties:
storyData
: The complete storyData passed in the Genmo constructorpassages
: Array of passage objectsname
: Name of the Story (as set in Twine)startnode
: Thepid
of the starting passagecreator
: The name of the app that created the story (usually "Twine")creator-version
: Version of Twine when this story was madeifid
: String representing an ID to be used in the Interactive Fiction Database
currentPassage
: The current passage set. Passage properties are detailed in theoutputFunction
section above
story.state.data
is the list of custom data applied by inline JSON (or Handlebars helpers) in Twine.
As a general rule: state
should only be read from, not written to. Writing to state outside of the provided functions (followLink()
, etc) may cause inconsistent behavior.