Updating readme
This commit is contained in:
73
Readme.md
73
Readme.md
@ -1,10 +1,73 @@
|
||||
# Story Manager
|
||||
# StoryManager
|
||||
|
||||
A lightweight storylet manager for Twee and Sugarcube.
|
||||
StoryManager is a lightweight add-on for Twine + SugarCube implementing parameterized storylets. It's intended for people who are comfortable using JavaScript along with Twine, and want to use it to manage data for their interactive fiction.
|
||||
|
||||
## How to use it
|
||||
|
||||
Add `storymanager.js` (and optionally `storymanager-widgets.tw`) to your Twine project. In JavaScript, add some story data if needed, and then add a storylet with a name, some tags (optionally), and a generator function that returns a list of instantiated storylet objects, like this:
|
||||
|
||||
```javascript
|
||||
|
||||
State.variables.locations = ["Earth", "Mars", "Ganymede"];
|
||||
State.variables.currentLocation = "Deep space";
|
||||
|
||||
StoryManager.storylets["Go somewhere"] = {
|
||||
name: "Go somewhere",
|
||||
tags: ["in space"],
|
||||
generate: function() {
|
||||
let storylets = [];
|
||||
for (let loc of State.variables.locations) {
|
||||
if (loc == State.variables.currentLocation) continue;
|
||||
let storylet = {
|
||||
passage: "Orbit", // Name of the passage the storylet links to
|
||||
description: "Jump to " + loc, // Storylet link text
|
||||
planet: loc // Data associated with this storylet
|
||||
};
|
||||
storylets.push(storylet);
|
||||
}
|
||||
return storylets;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then in Twine, write one or more passages associated with your storylet:
|
||||
|
||||
```
|
||||
:: Orbit
|
||||
<<set $currentLocation = $currentStorylet.planet>>
|
||||
You orbit around $currentLocation.
|
||||
|
||||
[[Explore the surface]] or [[Jump]] somewhere else.
|
||||
|
||||
:: Explore the surface
|
||||
You take your shuttle down to the surface of $currentLocation.
|
||||
|
||||
Return to [[Orbit]]
|
||||
```
|
||||
|
||||
And finally, you need to have a passage where you query for available storylets and display them:
|
||||
|
||||
```
|
||||
:: Jump
|
||||
|
||||
You prep your ship to jump.
|
||||
<<set _possibleStorylets = window.SM.getStorylets()>>
|
||||
<<ShowStoryletLinks _possibleStorylets>>
|
||||
```
|
||||
|
||||
(This example uses the `<<ShowStoryletLinks>>` widget from `storymanager-widgets.tw`)
|
||||
|
||||
You can see this complete example in the `examples\` folder ([twee]() or [playable HTML]()).
|
||||
|
||||
### What is a parameterized storylet?
|
||||
Storylets are pieces of content for a story or game that become available based on some combination of the current state, player choice, and random chance. Parameterized storylets are storylets where some details are intended to be filled in with data from the current story state or some underlying world model. For example, your game might have different characters in different locations, each with a different piece of information to share. You can create a single dialog parameterized storylet, with the character as a parameter. Using StoryManager (or, frankly, your own storylet system) your game could look up which characters are present at the game's current location and offer one dialog storylet for each.
|
||||
|
||||
There are a bunch of great resources for learning more about storylets in general. Emily Short has written a [bunch](https://emshort.blog/2019/11/29/storylets-you-want-them/) of [blog](https://emshort.blog/2016/04/12/beyond-branching-quality-based-and-salience-based-narrative-structures/) [posts](https://emshort.blog/2019/01/06/kreminski-on-storylets/) discussing storylet-based game design. Max Kreminski's [survey paper](https://mkremins.github.io/publications/Storylets_SketchingAMap.pdf) is where I got the *parameterized storylet* terminology from. Cat Manning and Joshua Grams have a [great talk on YouTube](https://www.youtube.com/watch?v=JRKqDlAauTQ) on using storylets, built around Joshua's own Tiny-QBN system for Twine. And all of those resources should point you to plenty more if you want to learn all about storylets and how to use them.
|
||||
|
||||
### Why another storylet manager?
|
||||
If you want to do storylets in Twine and Sugarcube, you should first check out [Tiny-QBN](https://github.com/JoshuaGrams/tiny-qbn), which is more mature than this one and requires much less messing around with JavaScript. As of version 3.2.0, the Harlowe story format also has [storylets as a first-class feature](https://twine2.neocities.org/#macro_storylet).
|
||||
|
||||
|
||||
I have a few Twine hobby projects in various stage of completion, and I found myself implementing world-models and procedural generation in JavaScript and trying to tie them to passages. Instead of writing ad-hoc systems per game, I decided to try and make one unified storylet manager -- hence this project. The JavaScript that goes into the StoryManager tool itself is pretty simple (seriously, [look at it]()), but I'm hoping someone else might find this useful too.
|
||||
|
||||
## Feature list
|
||||
|
||||
@ -18,4 +81,6 @@ A lightweight storylet manager for Twee and Sugarcube.
|
||||
- [X] Widget for displaying storylet links
|
||||
- [ ] Make the widget into a macro
|
||||
- [ ] Weighted random choice
|
||||
- [ ] Explore replacing storylet generators returning arrays with the `yield` keyword? **Pro:** produces cleaner code; **Con:** requires users to understand `yield` and remember to use the function * notation.
|
||||
- [ ] Explore replacing storylet generators returning arrays with the `yield` keyword? **Pro:** produces cleaner code; **Con:** requires users to understand `yield` and remember to use the function * notation.
|
||||
- [ ] Add storylet code to passages (as comments, a-la Tiny-QBN?)
|
||||
- [ ] CSS styling (probably to go with widgets/macros?)
|
@ -100,12 +100,7 @@ var saveAs=saveAs||navigator.msSaveBlob&&navigator.msSaveBlob.bind(navigator)||f
|
||||
<div id="init-lacking">Your browser lacks required capabilities. Please upgrade it or switch to another to continue.</div>
|
||||
<div id="init-loading"><div>Loading…</div></div>
|
||||
</div>
|
||||
<!-- UUID://BE18C022-A213-466C-8DD1-DCCD5CB1DF48// --><tw-storydata name="At the Duchess's Party" startnode="2" creator="Tweego" creator-version="2.1.0+9ea2fab" ifid="BE18C022-A213-466C-8DD1-DCCD5CB1DF48" zoom="1" format="SugarCube" format-version="2.30.0" options="debug" hidden><style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css"></style><script role="script" id="twine-user-script" type="text/twine-javascript">/* twine-user-script #1: "storymanager.js" */
|
||||
|
||||
// Choose one of an array
|
||||
var randomChoice = function(vals) {
|
||||
return vals[Math.floor(Math.random() * vals.length)];
|
||||
};
|
||||
<!-- UUID://BE18C022-A213-466C-8DD1-DCCD5CB1DF48// --><tw-storydata name="At the Duchess's Party" startnode="2" creator="Tweego" creator-version="2.1.0+9ea2fab" ifid="BE18C022-A213-466C-8DD1-DCCD5CB1DF48" zoom="1" format="SugarCube" format-version="2.30.0" options="" hidden><style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css"></style><script role="script" id="twine-user-script" type="text/twine-javascript">/* twine-user-script #1: "storymanager.js" */
|
||||
|
||||
// Set up the general narrative manager
|
||||
// -----------------------------------------------------------------------
|
||||
@ -114,11 +109,17 @@ StoryManager.storylets = {};
|
||||
|
||||
StoryManager.getAllStorylets = function(tag=null) {
|
||||
let allStorylets = [];
|
||||
let storylet, storylets, boundStorylet;
|
||||
for (let key in this.storylets) {
|
||||
let storylet = this.storylets[key];
|
||||
storylet = this.storylets[key];
|
||||
if (tag === null || ("tags" in storylet && storylet.tags.includes(tag))) {
|
||||
let storylets = storylet.generate();
|
||||
for (let i in storylets) allStorylets.push(storylets[i]);
|
||||
// TODO: If using yield, this part will change
|
||||
storylets = storylet.generate();
|
||||
for (let i in storylets) {
|
||||
boundStorylet = storylets[i];
|
||||
if (!("priority" in boundStorylet)) boundStorylet.priority = 0;
|
||||
allStorylets.push(storylets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allStorylets;
|
||||
|
219
examples/simple_space_example.html
Normal file
219
examples/simple_space_example.html
Normal file
File diff suppressed because one or more lines are too long
51
examples/simple_space_example.tw
Normal file
51
examples/simple_space_example.tw
Normal file
@ -0,0 +1,51 @@
|
||||
:: StoryTitle
|
||||
Simple Space Example
|
||||
|
||||
:: StoryData
|
||||
{
|
||||
"ifid": "2EE3728E-B079-4539-9A9C-97CD8474B4C5"
|
||||
}
|
||||
|
||||
:: Story JavaScript [script]
|
||||
Config.passages.nobr = true; // No unspecified linebreaks.
|
||||
|
||||
State.variables.locations = ["Earth", "Mars", "Ganymede"];
|
||||
State.variables.currentLocation = "Deep space";
|
||||
|
||||
StoryManager.storylets["Go somewhere"] = {
|
||||
name: "Go somewhere",
|
||||
tags: ["in space"],
|
||||
generate: function() {
|
||||
let storylets = [];
|
||||
for (let loc of State.variables.locations) {
|
||||
if (loc == State.variables.currentLocation) continue;
|
||||
let storylet = {
|
||||
passage: "Orbit", // Name of the passage the storylet links to
|
||||
description: "Jump to " + loc, // Storylet link text
|
||||
planet: loc // Data associated with this storylet
|
||||
};
|
||||
storylets.push(storylet);
|
||||
}
|
||||
return storylets;
|
||||
}
|
||||
}
|
||||
|
||||
:: Start
|
||||
|
||||
You find yourself in $currentLocation. You should probably [[jump | Jump]].
|
||||
|
||||
:: Jump
|
||||
You prep your ship to jump.
|
||||
<<set _possibleStorylets = window.SM.getStorylets()>>
|
||||
<<ShowStoryletLinks _possibleStorylets>>
|
||||
|
||||
:: Orbit
|
||||
<<set $currentLocation = $currentStorylet.planet>>
|
||||
You orbit around $currentLocation.
|
||||
|
||||
[[Explore the surface]] or [[Jump]] somewhere else.
|
||||
|
||||
:: Explore the surface
|
||||
You take your shuttle down to the surface of $currentLocation.
|
||||
|
||||
Return to [[Orbit]]
|
@ -1,9 +1,4 @@
|
||||
|
||||
// Choose one of an array
|
||||
var randomChoice = function(vals) {
|
||||
return vals[Math.floor(Math.random() * vals.length)];
|
||||
};
|
||||
|
||||
// Set up the general narrative manager
|
||||
// -----------------------------------------------------------------------
|
||||
var StoryManager = {};
|
||||
@ -11,11 +6,17 @@ StoryManager.storylets = {};
|
||||
|
||||
StoryManager.getAllStorylets = function(tag=null) {
|
||||
let allStorylets = [];
|
||||
let storylet, storylets, boundStorylet;
|
||||
for (let key in this.storylets) {
|
||||
let storylet = this.storylets[key];
|
||||
storylet = this.storylets[key];
|
||||
if (tag === null || ("tags" in storylet && storylet.tags.includes(tag))) {
|
||||
let storylets = storylet.generate();
|
||||
for (let i in storylets) allStorylets.push(storylets[i]);
|
||||
// TODO: If using yield, this part will change
|
||||
storylets = storylet.generate();
|
||||
for (let i in storylets) {
|
||||
boundStorylet = storylets[i];
|
||||
if (!("priority" in boundStorylet)) boundStorylet.priority = 0;
|
||||
allStorylets.push(storylets[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return allStorylets;
|
||||
|
Reference in New Issue
Block a user