npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@series-inc/slinky-core

v1.0.0

Published

Core modules for the Slinky project

Downloads

40

Readme

@series-inc/slinky-core

The foundational engine for running Slinky interactive stories. This package provides the core runtime for compiling, parsing, and advancing Ink-based narratives with a custom directive system on top.

What It Does

slinky-core is a platform-agnostic narrative engine that sits between raw Ink story files and whatever rendering layer you build (Phaser scenes, React components, etc.). It handles:

  • Compiling Ink source into a runnable story
  • Parsing custom @DIRECTIVE annotations embedded in Ink content
  • Advancing the story frame by frame, where each frame is a "stopping point" containing directives, a single dialogue message, and available choices
  • Saving and loading story state via minimal snapshots and choice-history replay

It does not handle rendering, asset loading, or UI — those concerns belong to consumer packages like @series-inc/slinky-phaser or individual game applications.

Architecture

Ink Source (.ink file)
        │
        ▼
  ┌─────────────┐
  │ parseStory() │  ← Extracts @DIRECTIVE metadata, cleans content
  └──────┬──────┘
         │
         ▼
  ┌──────────────────────┐
  │ compileStoryStandard()│  ← Compiles Ink to JSON via inkjs
  └──────┬───────────────┘
         │
         ▼
  ┌─────────────────────────────────────┐
  │ StoryEngine (implements IStoryEngine)│  ← Composes services, manages story
  └──────┬──────────────────────────────┘
         │ composes
         ▼
  ┌─────────────────────────────────────┐
  │ Services (SOLID Architecture)        │
  │ • StoryStateManager                  │  ← State management
  │ • InkVariableManager                 │  ← Variables & visit counts
  │ • ChoiceHistoryManager               │  ← Choice history & replay
  │ • StoryInitializer (static)          │  ← Initialization
  └──────┬──────────────────────────────┘
         │ uses
         ▼
  ┌──────────────┐
  │ buildFrame() │  ← Static utility: reads lines, builds frames
  └──────────────┘

Key Concept: Frames

A SlinkyFrame is the atomic unit of story progression. Each time you call engine.continue(), the FrameBuilder reads lines from the Ink story until it hits dialogue or choices. Everything before that stopping point (scene changes, character updates, etc.) is collected as directives.

type SlinkyFrame = {
  directives: Directive[];         // @SCENE, @CHARACTER, etc.
  message: IStoryMessage | null;   // The dialogue line (speaker + text)
  choices: IChoice[];              // Player choices, if any
  metadata: FrameMetadata;         // canContinue, isEnd, path info, tags
};

Key Concept: Directives

Directives are lines in the Ink file that start with @ and carry structured parameters:

@SCENE_UPDATE: mode=avatar, background=bedroom, music=ambient-night
@CHARACTER_UPDATE: id=Alice, outfit=casual, side=left

slinky-core parses these into Directive objects but does not act on them. It is the consumer's responsibility to interpret directives and apply them to the rendering layer. The DirectiveRegistry pattern is provided for this purpose.

Note: Directive names and parameters are game-specific. The examples above are from the basic-game application. Your game may define different directives.

Core API

StoryEngine

The main entry point for running a story.

import { StoryEngine } from '@series-inc/slinky-core';

const engine = new StoryEngine();

// Initialize with raw Ink content
await engine.initialize(inkSource);

// Advance to the next frame
const frame = await engine.continue();

// Make a choice
const nextFrame = await engine.chooseIndex(0);

// Read/write Ink variables
engine.setInkVariable('player_name', 'Jordan');
const name = engine.getInkVariable('player_name');

// Navigate to a specific knot
engine.jumpToPath('chapter_2');

// Replay a sequence of choices (used for save restoration)
await engine.replayChoices(choiceHistory);

// Access current state
const state = engine.currentState;

Compiler

Compile raw Ink source to JSON, either standalone or with a file handler for multi-file stories.

import { compileStoryStandard, compileStoryWithFileHandler } from '@series-inc/slinky-core';

// Simple compilation
const storyJson = compileStoryStandard(inkSource);

// With file handler (for INCLUDE statements)
const storyJson = compileStoryWithFileHandler({
  storyContent: inkSource,
  storyPath: 'story.ink',
  fileHandler: myFileHandler,
});

Directive Parser

Parse story content to extract directives and optionally build metadata.

import { parseStory, parseDirective } from '@series-inc/slinky-core';

// Basic: just clean the content
const { content } = parseStory(inkSource);

// With metadata extraction
const { metadata, content } = parseStory(inkSource, {
  initMetadata: () => ({ characters: [], scenes: [] }),
  onDirective(directive, metadata, lineNumber) {
    if (directive.type === 'CHARACTER_SCHEMA') {
      metadata.characters.push(directive.params);
    }
  },
});

// Parse a single directive line
const directive = parseDirective('@SCENE_UPDATE: background=bedroom, music=morning');
// → { type: 'SCENE_UPDATE', params: { background: 'bedroom', music: 'morning' } }

DirectiveRegistry

A registry pattern for dispatching directives to type-safe handlers with Zod validation.

import { DirectiveRegistry, DirectiveHandler } from '@series-inc/slinky-core';
import { z } from 'zod';

// Define a handler
class SceneUpdateHandler extends DirectiveHandler<MyContext, z.infer<typeof schema>> {
  protected readonly directiveName = 'SCENE_UPDATE';
  protected readonly paramSchema = z.object({
    background: z.string().optional(),
    music: z.string().optional(),
    mode: z.enum(['avatar', 'chat', 'prose']).optional(),
  });

  protected async execute(params, context) {
    // Apply scene changes to your rendering layer
    await context.scene.updateScene(params);
  }
}

// Register and execute
const registry = new DirectiveRegistry<MyContext>();
registry.register('SCENE_UPDATE', new SceneUpdateHandler());

const result = await registry.execute(directive, context);

SaveModule

Manages save/load using minimal snapshots and choice-history replay. The module does not own storage — it only extracts and restores state. Persistence is the consumer's responsibility.

import { StoryEngine, SaveModule } from '@series-inc/slinky-core';

const engine = new StoryEngine();
const save = new SaveModule(engine, {
  persistentVariables: ['player_name', 'relationship_score'],
  debug: true,
});

// Take a snapshot
const snapshot = save.getSnapshot();
// → { version, currentPathString, variables, visitCounts, choiceHistory }

// Restore from snapshot (replays choices to reconstruct state)
await save.restoreSnapshot(snapshot);

// Inject cross-chapter variables before starting a new chapter
save.setInitialVariables({
  player_name: 'Jordan',
  relationship_score: 85,
});

Advanced Features

Dependency Injection for Testing

StoryEngine supports optional dependency injection, enabling you to mock services for testing:

import { StoryEngine, StoryStateManager, ChoiceHistoryManager } from '@series-inc/slinky-core';

// Create custom or mock services
const mockStateManager = new StoryStateManager();
const mockHistoryManager = new ChoiceHistoryManager();

// Inject services for testing
const engine = new StoryEngine({
  stateManager: mockStateManager,
  historyManager: mockHistoryManager,
});

// Engine uses your provided services instead of creating its own

Interface Abstraction

The package exports IStoryEngine interface for loose coupling between components:

import type { IStoryEngine } from '@series-inc/slinky-core';

// Depend on the interface, not the concrete class
function processStory(engine: IStoryEngine) {
  const frame = await engine.continue();
  // Your code works with any IStoryEngine implementation
}

Benefits:

  • Mock implementations for unit testing
  • Alternative StoryEngine implementations
  • Better separation of concerns
  • Easier refactoring

SOLID Architecture

StoryEngine is composed of focused, single-responsibility services:

| Service | Responsibility | Exported | |---------|---------------|----------| | StoryStateManager | Manages ephemeral story state (initiated, blocking, currentFrame, lastValidPath) | ✅ Yes | | InkVariableManager | Manages ink variables and visit counts | ✅ Yes | | ChoiceHistoryManager | Records and replays choice history with silent mode | ✅ Yes | | StoryInitializer | Static utility for story initialization from Ink content | ✅ Yes |

Each service can be tested in isolation and follows SOLID principles. Services are injected into StoryEngine via the constructor, enabling dependency injection patterns.

Static Utilities

For stateless operations, the package provides efficient static functions:

import { buildFrame } from '@series-inc/slinky-core';

// Build a frame from an inkjs Story instance
const frame = buildFrame(story);

Note: The FrameBuilder class is deprecated but still available for backward compatibility. New code should use the buildFrame() static function.

Exported Interfaces & Services

Interfaces

  • IStoryEngine - Public contract for StoryEngine, enabling interface-based programming
  • IStoryEngineServices - Service container type for dependency injection

Services

All services are exported and can be used independently or injected into StoryEngine:

import {
  StoryStateManager,
  InkVariableManager,
  ChoiceHistoryManager,
  StoryInitializer,
} from '@series-inc/slinky-core';

Provided Type Definitions

The package also exports consumer-facing types for common visual novel concepts. These are convenience types and are not used by the engine itself:

  • Scene types: SceneDefinition, Background, Scene, TransitionConfig, BackgroundPayload, MusicPayload
  • Character types: CharacterDefinition, Character, CharacterLook, ItemProp
  • Constants: TransitionType, BackgroundAnchor, BackgroundType, emotions, characterSides, itemPropTypes

Documentation

For Users

For game-specific directive documentation (like @SCENE, @CHARACTER, etc.), see the basic-game documentation.

For Developers & Contributors

  • AGENTS.md - Detailed guide for AI agents and developers working on slinky-core
    • Testing patterns and best practices
    • Common workflows and pitfalls
    • Architecture deep dive
  • CONTRIBUTING.md - Contribution guidelines for the Slinky monorepo

Build & Development

# Build the package
pnpm --filter @series-inc/slinky-core build

# Watch mode for development
pnpm --filter @series-inc/slinky-core dev

# Type checking
pnpm --filter @series-inc/slinky-core typecheck

The package outputs ESM with TypeScript declarations via tsdown. The single entry point is ./dist/index.mjs.

Dependencies

| Dependency | Purpose | |---|---| | inkjs | The Ink narrative scripting runtime (compiler + story engine) | | zod | Schema validation for directive parameters | | es-toolkit | Utility functions (e.g. cloneDeep) |