Skip to content

2aronS/story-forge

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

story-forge

Interactive narrative builder for creating branching story experiences

motivation

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.

architecture

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]
Loading

getting started

install

npm install story-forge

quickstart

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 left

how it works

Stories 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.

configuration

Conditional choices

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
}

Variable mutations

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");
  }
}

Custom validators

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
    }
  ]
});

faq

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).

license

MIT

About

Interactive narrative builder for creating branching story experiences

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors