Skip to content

Integrate quests into your game

This guide shows the two ways to pull @kiberon-labs/quests into a project and how to consume the quest graphs it produces. It assumes you have a TypeScript or JavaScript project and are comfortable with npm.

Terminal window
npm install @kiberon-labs/quests graph-grammar

graph-grammar is the engine that runs the grammars. Install it alongside as a peer runtime dependency.

Use this when you want to generate quests in code, pick which archetypes and layers to include, or extend the library with your own beats.

composeQuest picks an archetype core, composes the layers affine to it, runs the engine, and strips the generator’s scaffolding:

import { composeQuest, coreFor } from '@kiberon-labs/quests'
const quest = composeQuest(coreFor('rescue'), 42)
// quest.nodes / quest.edges: a grounded quest graph.

Pass a seed for a reproducible quest; omit it for a different quest each call.

To run the engine yourself, for example to choose an application strategy or to generate the quest partially as the player arrives at each beat, compose the grammar and run it:

import { composeGrammar, coreFor } from '@kiberon-labs/quests'
import { Engine } from 'graph-grammar'
const engine = new Engine(composeGrammar(coreFor('heist')))
engine.run()
const graph = engine.graph

The registry exposes every module with its metadata, so you can list what’s available or compose a custom subset:

import { REGISTRY, modulesForCore, QUEST_ARCHETYPES } from '@kiberon-labs/quests'
QUEST_ARCHETYPES // ['compete', 'defend', 'escape', …] the 14 cores
modulesForCore('slay') // the beats affine to a slay quest
REGISTRY.filter((m) => m.requires.includes('Combat')) // e.g. every combat-capable beat

Each registry entry carries its key, folder (the stage it acts on), and requires (the capabilities a beat needs, such as Combat or Dialogue). Filter on these to include only the beats your game can present.

Use this when you just want quests out of the box and don’t need to compose anything. Every archetype ships as a compiled grammar JSON; run it with the engine and nothing else from this package:

import { Engine, importGrammar } from 'graph-grammar'
import heist from '@kiberon-labs/quests/grammars/heist.grammar.json' with { type: 'json' }
const engine = new Engine(importGrammar(heist))
engine.run()
const graph = engine.graph

The grammars are available at @kiberon-labs/quests/grammars/<archetype>.grammar.json for each of the 14 archetypes. The raw per-module data (one JSON per beat, plus index.json and manifest.json describing capability requirements) is under @kiberon-labs/quests/data/*, so you can load individual beats or read the manifest without the TypeScript API at all.

Whichever path you take, the result is a graph, not a script. A game turns it into something playable by walking it: start at the Start node, follow begins then then edges along the spine, branch at Choice nodes (each choice edge is an option; options either rejoin the spine or diverge to their own End), and stop at an End. At each Step you read its action / objective and the world entities it links to (targets, involves, triggers) to drive your game’s side effects: spawn entities, set flags, present exposition.

The quest is grounded in a generated world, so the graph also contains the world entities the quest binds to (NPCs, places, items). Filter to the quest spine by walking out from Start; the linked world nodes are the concrete cast the quest reified against.

The real integration work is teaching the generator about your world. The module DSL is exported so you can write beats that match your entities and mechanics and compose them beside (or instead of) the built-in ones:

import { matchModule } from '@kiberon-labs/quests'
import { lit } from 'graph-grammar'
// A beat that fires only when a real, unstirred rival faction exists in the world.
export const rivalStirs = matchModule({
key: 'my-rival-stirs',
title: 'A rival makes their move',
stage: 'inciting',
description: 'A rival faction moves on the same matter.',
step: {
action: 'learn-of-rival',
objective: 'learn that a rival faction is moving on the same matter',
completion: 'the rival’s hand is now in play',
interaction: 'dialogue',
},
match: { nodes: [{ id: 'faction', label: 'Faction', where: [{ key: 'bent', op: 'exists' }] }] },
links: [{ to: 'faction', label: 'involves' }],
})

The builders (matchModule, branchMatchModule for a match plus a branching choice, beatModule for a plain stage beat, complicationModule, and the lower-level matchBeat / branchMatchBeat) all return module descriptors you can add to a composition. Because each beat only matches world state that actually exists, an unmatched beat simply doesn’t fire, so mixing your beats with the built-ins never produces a broken quest.

composeQuest(core, seed) and composeGrammar(core, seed) are deterministic for a given seed, which is useful for tests, reproducible bug reports, and shipping a fixed set of quests. Omit the seed to vary output per call.

Built by Kiberon Labs