Open-source interactive fiction engine, powered by React, Redux and TypeScript. _
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Dale b80596279a
misc: Set correct value for linguist-vendored
1 year ago
src fix: Add index to options key in renderer 1 year ago
.envrc feat: Working engine, nix package 1 year ago
.eslintrc.json feat: Rework engine and renderer - stage 1 1 year ago
.gitattributes misc: Set correct value for linguist-vendored 1 year ago
.gitignore feat: Use emotion for css 1 year ago misc: Better dependencies in README 1 year ago
default.nix feat: Working engine, nix package 1 year ago
package.json 2.1.1 1 year ago
shell.nix feat: Rework engine and renderer - stage 1 1 year ago
tsconfig.json feat: Rework engine and renderer - stage 1 1 year ago
webpack.config.js feat: Rework engine and renderer - stage 1 1 year ago
yarn.lock fix: Use relative path import 1 year ago
yarn.nix feat: Status bar improvements 1 year ago


Text-based interactive ficiton with RPG elements

Allows you to create simple RPG-esque text stories that follow basic principles.

  1. You can read text that is spoken by characters or just presented as is ("narration").
  2. You can navigate branching dialogue.
  3. You can distribute and use skills to pass dialogue checks and gain them.
  4. State can be used to track progress / remember decisions / set "VN-like" flags.
  5. Time passes with each line

Full Demo (Source Code for Demo)


With seed

Just clone example seed project, then edit game.ts, skills.ts and store.ts to develop your game.

git clone my-game
cd my-game
yarn start

With create-react-app as template

(Optional, but provides for stable webpack set up that we're not covering here)

Start with

npx create-react-app my-app --template typescript

Remove all contents from src/

Install following pre-requisits

yarn add discoteque react-toastify redux emotion @emotion/core
yarn add -D @types/react-redux

Follow these steps

Create game store & reducer

Create a store and reducer to process in-game actions and change state

// store.ts
import { createStore, Action } from 'redux';
import { resetGame } from 'discoteque/lib/engine/lib/utils';

export interface IGameState {
    myCheck: boolean;

const defaultState: IGameState = {
  myCheck: false,

type ACTION = 'INIT' | 'set-check';
interface IGameAction extends Action {
    type: ACTION;

interface ISetCheckAction extends IGameAction {
    check: boolean;

interface InitAction extends IGameAction {
    data: { gameState: IGameState };

export const setCheck = (check: boolean): ISetCheckAction => ({
    type: 'set-check', check,

export const reducer = (state: IGameState = defaultState, action: IGameAction): IGameState => {
  switch (action.type) {
      case 'set-check':
        const checkAction = action as ISetCheckAction;
        return ({ ...state, myCheck: checkAction.check });
      // 'INIT' is a reserved Engine action type, used to initialize both game and engine state from save
      // Since game state is defined along with game's script and not included with engine, we have to perform reseting ourselves
      // `resetGame` is a supplied utility function that lets us perform necessary reduce without cluttering the rest of reducer
      case 'INIT':
        const initAction = action as InitAction;
        return resetGame(state, initAction);
        return state;

export default createStore(reducer);

Create skills

Create default skills

// skills.ts
import { SkillSet } from "discoteque/lib/engine/types";

export type GameSkills = 'test';

export const skills: SkillSet<GameSkills> = {
  test: 5,

export const skillDescriptions: Record<GameSkills, string> = {
  test: 'This is a test skill',

export default skills;

Create script

// game.ts
import { INode, IActor, ILocation } from 'discoteque/lib/engine/types';
import { setState } from 'discoteque/lib/engine/lib/store';
import { IGameState, setCheck } from './store';
import { awardSkill, toast } from 'discoteque/lib/engine/lib/utils';

// Create Nodes
export const nodes: INode<IGameState>[] = [
    { id: 'beginning', kind: 'node', next: 'choice', lines: [
        // Lines are basically objects which specify how line should look
        { actorId: 'char_exampler', text: 'Hi! This is an example of Discoteque!' },
        { actorId: 'char_exampler', text: 'Let\'s try picking options' },
    ] },
    { id: 'choice', kind: 'node', next: 'choice', lines: [
        // Make player pick an answer
        { text: 'Your choice?', options: [
            // This answer is gated behind a skill check (dice throw)
            // name and difficulty are self-explanatory, failTo specifies node to fallback to if check is failed
            // failTo is REQUIRED
            { text: 'Let\'s test a skill', value: 'test_success', skill: { name: 'test', difficulty: 15, failTo: 'test_fail' } },
            // This one has no costs or prerequisites!
            { text: 'Let me out!', value: 'exit' },
        ] }
    ] },
    { id: 'test_success', kind: 'node', next: 'choice', lines: [
        // Line can be a funciton which takes a number of parameters and returns a line object
        (_, { myCheck }, dispatch) => {
            if (!myCheck) {
                // We can dispatch redux events to modify game's store!
                return { actorId: 'char_exampler', text: "Yay! You passed a check!" }
            } else {
                return { text: 'You think you\'ve already passed this check, so no need to do it again' }
    { id: 'test_fail', kind: 'node', next: 'choice', lines: [
        ({ skillPoints }, _, dispatch) => {
            // This is a helper function from discoteque lib
            // It awards one skill point to player and dispatches a toast popup
            awardSkill(dispatch, skillPoints);
            return { actorId: 'char_exampler', text: 'You\'ve failed... Not a problem! Take a skill point and I\'ll give another if you fail again!' };
    ] },
    { id: 'exit', kind: 'node', next: 'choice', lines: [
        (_, _gameState, dispatch) => {
            dispatch(setState({ ui: { isOver: true } }));
            return { actorId: 'char_exampler' , text: 'Bye-bye!' };
    ] }

// Create actors
export const actors: IActor<IGameState>[] = [
    { id: 'char_exampler', kind: 'actor', name: 'Exampler!', lines: [] },

// Create locations
export const locations: ILocation<IGameState>[] = [
    { id: 'loc_discoville', kind: 'location', name: 'Discoville!', next: 'beginning', lines: [
        { text: 'It\'s a beautiful day at Discoville today!' },
        { text: 'A friendly feller is approaching you.' }
    ] },

Create game config

// config.ts
import { EngineConfig } from "discoteque/lib/engine/types";
import { IGameState, reducer } from "./store";
import skills, { GameSkills, skillDescriptions } from "./skills";
import { nodes, actors, locations } from "./game";

const config: EngineConfig<IGameState, undefined, GameSkills> = {
    // Pass the initial skills
    skills: skills,
    // Set menu text values
    menu: {
        title: 'Example Story',
        description: 'An Example Discoteque Story',
    // Define how many points player can spend on start
    skillPointsOnStart: 3,
    // Define starting node
    startNode: 'loc_discoville',
    // Define time object
    chrono: {
        time: 750,
    // Give skills some desctiptions
    skillDescriptoins: skillDescriptions,
    // Provide game's reducer
    reducer: reducer,
    // Supply the actual script (nodes)
    nodes: [

export default config;

Use config in your app

Replace default project's index.tsx with this

// index.tsx
import makeApp from 'discoteque/lib';
import { injectGlobal } from 'emotion';
import config from './config';

import 'react-toastify/dist/ReactToastify.css';

// Set up default font
import font from "discoteque/src/assets/fonts/AnticSlab-Regular.woff2";
    @font-face {
        font-family: 'Antic Slab Regular';
        src:  url(${font}) format('woff2');
    body {
        font-family: 'Antic Slab Regular';

// Import your config file

// Make an app. Mounting onto DOM is already handled by App.


Run build

yarn build
yarn link
yarn link discoteque