Interactive narrative builder for creating branching story experiences
Most interactive fiction tools force you into rigid formats or require learning specialized scripting languages. Story-forge takes a different approach: it's a TypeScript library that treats stories as data structures. You define nodes, choices, and transitions using plain objects, then render them however you want. This makes it trivial to integrate branching narratives into games, chatbots, educational tools, or anywhere else you need interactive storytelling. The library handles state management and path validation while you focus on writing good stories.
graph TD
A[Story Definition] --> B[Story Parser]
B --> C[Story Graph]
C --> D[Runtime Engine]
D --> E[State Manager]
E --> F[Current Node]
F --> G[Choice Evaluator]
G --> H[Condition Checker]
H --> D
D --> I[Event Emitter]
I --> J[Your Renderer]
E --> K[Save/Load System]
npm install story-forge
import { Story, StoryRunner } from 'story-forge';
const story = new Story({
nodes: {
start: {
text: "You stand at a fork in the road.",
choices: [
{ text: "Go left", target: "forest" },
{ text: "Go right", target: "cave" }
]
},
forest: {
text: "The forest is dark and quiet.",
choices: [{ text: "Return", target: "start" }]
},
cave: {
text: "You find a sleeping dragon.",
choices: [{ text: "Run away", target: "start" }]
}
},
startNode: "start"
});
const runner = new StoryRunner(story);
runner.on('node', (node) => {
console.log(node.text);
node.choices.forEach((choice, i) => {
console.log(`${i + 1}. ${choice.text}`);
});
});
runner.start();
runner.choose(0); // Go leftStories are defined as graphs where nodes represent narrative moments and edges represent choices. The StoryRunner maintains a state object that tracks the current node, variables, and history. When a player makes a choice, the runner evaluates any attached conditions against the current state, updates variables, and transitions to the target node. Conditions can reference state variables, letting you create stories that remember past decisions. The library emits events at key moments (node entry, choice made, state change) so you can hook in your own rendering logic. Save states are just serialized JSON, making persistence straightforward.
Choices can include conditions that determine visibility or availability:
{
text: "Attack the dragon",
target: "battle",
condition: (state) => state.variables.hasSword === true,
visible: (state) => state.variables.dragonAwake === true
}Modify state when entering nodes or making choices:
{
text: "You pick up the sword.",
choices: [...],
onEnter: (state) => {
state.variables.hasSword = true;
state.variables.inventory.push("sword");
}
}Add validation to ensure story integrity:
const story = new Story(definition, {
validators: [
(graph) => {
// Ensure all target nodes exist
// Check for orphaned nodes
// Verify at least one path to an ending
}
]
});Can I use this with React/Vue/Svelte?
Yes. The library is framework-agnostic. Subscribe to events and render nodes in your components.
How do I handle complex game logic?
State variables can hold any JSON-serializable data. Use onEnter hooks and condition functions to implement inventory systems, stats, timers, or other mechanics.
What about procedural generation?
You can generate story definitions programmatically before passing them to Story(). The library doesn't care if nodes are handwritten or generated.
Can stories be loaded asynchronously?
Yes. Load your story JSON from a file or API, then instantiate Story() with the parsed object.
How do I implement save/load?
Call runner.serialize() to get a JSON string of the current state. Restore with runner.load(json).
MIT