JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 3
  • Score
    100M100P100Q24960F
  • License MIT

A powerful, flexible TypeScript library for creating visual novels and interactive narratives

Package Exports

  • vn-engine
  • vn-engine/package.json

Readme

🎭 VN Engine Library

A powerful, flexible TypeScript library for creating visual novels and interactive narratives. Built with a pragmatic, variables-based architecture that can support any genre.

âœĻ Features

  • 📝 YAML-based scripting - Clean, readable narrative format
  • ðŸŽŪ Universal game state - Variables system supports any data structure
  • 🌟 Dual template engine - Full Handlebars support with robust fallback
  • 🔀 Choice tracking - Advanced branching narrative support
  • ðŸŽŊ Event-driven - React to game state changes
  • 🏗ïļ Framework-agnostic - Works with any UI framework or vanilla JS
  • ðŸ“ą TypeScript first - Full type safety and excellent DX
  • ðŸŠķ Lightweight - Zero required dependencies for basic functionality
  • 🔧 Robust fallbacks - Graceful degradation when optional libraries unavailable
  • 🚀 Script Upgrades & DLC - Hot-swappable content with validation and rollback
  • ⚡ Async-ready - Modern async/await patterns with backward compatibility

🚀 Quick Start

Installation

npm install vn-engine

Optional Dependencies

For enhanced template functionality, you can install Handlebars:

npm install handlebars
npm install @types/handlebars  # For TypeScript projects

Note: VN Engine works perfectly without Handlebars! It includes a built-in simple template engine that covers most use cases. Handlebars is only needed for advanced template features like loops and custom helpers.

import { createVNEngine } from 'vn-engine'

async function initializeGame() {
  // Create engine instance
  const vnEngine = createVNEngine()
  
  // Initialize async (detects and sets up Handlebars if available)
  await vnEngine.initialize()

  // Load a script
  const script = `
welcome:
  - "Hello, welcome to my visual novel!"
  - speaker: "Guide"
    say: "What's your name?"
    actions:
      - type: setVar
        key: player_name
        value: "Hero"
  - speaker: "{{player_name}}"
    say: "Nice to meet you!"
`

  // Set up event listeners
  vnEngine.on('stateChange', (result) => {
    console.log('New result:', result)
  })

  // Load and start
  vnEngine.loadScript(script)
  vnEngine.startScene('welcome')

  // Continue through dialogue
  vnEngine.continue()
}

initializeGame()

Synchronous Usage (Legacy Support)

import { createVNEngine } from 'vn-engine'

// Create engine instance (uses simple template engine by default)
const vnEngine = createVNEngine()

// Use immediately - no initialization required for basic functionality
vnEngine.loadScript(script)
vnEngine.startScene('welcome')

Template Engine Information

Check which template engine is active:

const engineInfo = vnEngine.getTemplateEngineInfo()
console.log(`Using ${engineInfo.type} template engine`)
console.log('Available features:', engineInfo.supportedFeatures)

if (engineInfo.type === 'handlebars') {
  console.log('✅ Full template functionality available')
  console.log('📊 Advanced helpers loaded:', engineInfo.helpersRegistered)
} else {
  console.log('â„đïļ Using simple template engine - basic functionality available')
  console.log('ðŸ’Ą Install handlebars for advanced features')
}

📚 API Reference

VNEngine Class

Factory Function

createVNEngine(): VNEngine

Creates a new VN engine instance. Multiple instances are supported.

// Async initialization - detects and configures Handlebars
await vnEngine.initialize(): Promise<void>

// Check if engine is ready for advanced templates
vnEngine.isTemplateEngineReady(): boolean

// Get template engine information
vnEngine.getTemplateEngineInfo(): TemplateEngineInfo

Core Methods

// Script management
loadScript(content: string, fileName?: string): void
startScene(sceneName: string): ScriptResult
continue(): ScriptResult
makeChoice(choiceIndex: number): ScriptResult
reset(): void

// State management  
getGameState(): SerializableGameState
setGameState(state: SerializableGameState): void

// State getters
getCurrentResult(): ScriptResult | null
getIsLoaded(): boolean
getError(): string | null

Event System

// Listen to events
const unsubscribe = vnEngine.on('stateChange', (result) => {
  // Handle state changes
})

vnEngine.on('error', (error) => {
  console.error('VN Error:', error)
})

vnEngine.on('loaded', () => {
  console.log('Script loaded successfully!')
})

// Clean up
unsubscribe()

Template Engine Types

interface TemplateEngineInfo {
  type: 'handlebars' | 'simple'
  isHandlebarsAvailable: boolean
  helpersRegistered: boolean
  supportedFeatures: {
    variables: boolean
    conditionals: boolean
    helpers: boolean
    loops: boolean
    partials: boolean
  }
}

Script Result Types

interface ScriptResult {
  type: 'display_dialogue' | 'show_choices' | 'scene_complete' | 'error'
  content?: string
  speaker?: string
  choices?: ChoiceOption[]
  canContinue?: boolean
  error?: string
}

🔄 Script Upgrades & DLC System

The VN Engine includes a powerful upgrade system that allows you to dynamically add or replace content without losing game state. Perfect for DLC, content updates, mods, and episodic releases.

Core Upgrade Methods

// Upgrade script with new content
upgradeScript(content: string, options?: ScriptUpgradeOptions): UpgradeResult

// Validate upgrade without applying changes  
validateUpgrade(content: string, options?: ScriptUpgradeOptions): ValidationResult

// Create preview of what upgrade would do
createUpgradePreview(content: string, options?: ScriptUpgradeOptions): UpgradePreviewReport

Upgrade Types

ScriptUpgradeOptions

interface ScriptUpgradeOptions {
  mode?: 'additive' | 'replace'        // How to handle new content
  namespace?: string                    // Prefix for scene names
  allowOverwrite?: string[]            // Scenes that can be replaced
  validateState?: boolean              // Check if current state remains valid
  dryRun?: boolean                    // Preview only, don't apply changes
}

UpgradeResult

interface UpgradeResult {
  success: boolean                     // Whether upgrade succeeded
  error?: UpgradeError                // Error details if failed
  addedScenes: string[]               // New scenes that were added
  replacedScenes: string[]            // Existing scenes that were replaced
  totalScenes: number                 // Total scenes after upgrade
  warnings: string[]                  // Non-critical issues
}

ValidationResult

interface ValidationResult {
  valid: boolean                       // Whether upgrade would succeed
  errors: UpgradeError[]              // Validation errors found
  warnings: string[]                  // Potential issues
  wouldAddScenes: string[]           // Scenes that would be added
  wouldReplaceScenes: string[]       // Scenes that would be replaced
}

UpgradeError

interface UpgradeError {
  code: 'SCENE_CONFLICT' | 'INVALID_REFERENCE' | 'STATE_INVALID' | 'PARSE_ERROR' | 'UNAUTHORIZED_OVERWRITE'
  message: string                      // Human-readable error message
  details: {                          // Specific error details
    conflictingScenes?: string[]
    invalidReferences?: string[]
    affectedState?: string[]
    unauthorizedOverwrites?: string[]
    parseErrors?: string[]
  }
}

Upgrade Modes

Additive Mode (Default)

Adds new content without replacing existing scenes. Conflicts result in errors.

// Add DLC content without touching base game
const dlcContent = `
dlc_new_area:
  - "Welcome to the secret garden!"
  - "This area was added in the DLC."

dlc_bonus_scene:
  - speaker: "New Character"
    say: "I wasn't in the original story!"
`

const result = vnEngine.upgradeScript(dlcContent, {
  mode: 'additive',
  namespace: 'dlc'  // Scenes become 'dlc_new_area', 'dlc_bonus_scene'
})

if (result.success) {
  console.log(`Added ${result.addedScenes.length} new scenes`)
}

Replace Mode

Allows replacing existing scenes with explicit permission.

// Update existing scenes with new content
const updatedContent = `
intro:
  - "Welcome to the Enhanced Edition!"
  - "This intro has been completely rewritten."

new_ending:
  - "This is a brand new ending!"
`

const result = vnEngine.upgradeScript(updatedContent, {
  mode: 'replace',
  allowOverwrite: ['intro'],  // Only 'intro' can be replaced
  validateState: true         // Ensure current game state remains valid
})

if (result.success) {
  console.log(`Replaced ${result.replacedScenes.length} scenes`)
  console.log(`Added ${result.addedScenes.length} new scenes`)
}

Advanced Upgrade Examples

Safe DLC Addition with Validation

const dlcContent = `
expansion_start:
  - "Welcome to the Northern Territories expansion!"
  - actions:
      - type: setFlag
        flag: expansion_unlocked
  - goto: expansion_hub

expansion_hub:
  - if: "hasFlag 'main_story_complete'"
    then:
      - "Since you've completed the main story, here's bonus content!"
    else:
      - "You can return here after completing the main story."
  - text: "Where would you like to go?"
    choices:
      - text: "Explore the Frozen Cave"
        goto: exp_frozen_cave
      - text: "Visit the Mountain Village" 
        goto: exp_mountain_village
      - text: "Return to main game"
        goto: town_square

exp_frozen_cave:
  - "The cave glistens with ancient ice..."
  - "This is completely new content!"

exp_mountain_village:
  - "High in the mountains, a small village thrives."
  - speaker: "Village Elder"
    say: "Welcome, traveler from the lowlands!"
`

// Validate before applying
const validation = vnEngine.validateUpgrade(dlcContent, {
  mode: 'additive',
  namespace: 'expansion',
  validateState: true
})

if (validation.valid) {
  const result = vnEngine.upgradeScript(dlcContent, {
    mode: 'additive',
    namespace: 'expansion'
  })
  
  if (result.success) {
    console.log('DLC installed successfully!')
    console.log(`New scenes: ${result.addedScenes.join(', ')}`)
    
    // Add transition to DLC from main game
    if (vnEngine.hasScene('town_square')) {
      // Could modify existing scenes to add DLC access
    }
  }
} else {
  console.error('DLC validation failed:', validation.errors)
}

Content Update with Scene Replacement

const contentUpdate = `
# Updated intro with better writing
intro:
  - "Welcome to Mystical Realms: Director's Cut!"
  - speaker: "Narrator"
    say: "This enhanced version features improved dialogue and new scenes."
  - actions:
      - type: setFlag
        flag: directors_cut
      - type: setVar
        key: version
        value: "2.0"
  - goto: character_creation

# New alternative ending
secret_ending:
  - if: "and (hasFlag 'directors_cut') (hasFlag 'found_all_secrets')"
    then:
      - "Congratulations! You've unlocked the secret ending!"
      - "This ending is only available in the Director's Cut."
    else:
      - goto: normal_ending

# Enhanced existing scene
character_creation:
  - "Choose your character class:"
  - text: "Enhanced character creation with new options:"
    choices:
      - text: "Warrior (Classic)"
        actions:
          - type: setVar
            key: player_class
            value: "warrior"
        goto: game_start
      - text: "Mage (Classic)" 
        actions:
          - type: setVar
            key: player_class
            value: "mage"
        goto: game_start
      - text: "Necromancer (NEW!)"
        condition: "hasFlag 'directors_cut'"
        actions:
          - type: setVar
            key: player_class
            value: "necromancer"
          - type: setFlag
            flag: chose_necromancer
        goto: necromancer_intro
`

// Preview the update first
const preview = vnEngine.createUpgradePreview(contentUpdate, {
  mode: 'replace',
  allowOverwrite: ['intro', 'character_creation'],
  validateState: true
})

console.log('Update Preview:', preview.summary)
console.log('Would add:', preview.details.wouldAdd)
console.log('Would replace:', preview.details.wouldReplace)

if (preview.valid) {
  const result = vnEngine.upgradeScript(contentUpdate, {
    mode: 'replace',
    allowOverwrite: ['intro', 'character_creation'],
    validateState: true
  })
  
  if (result.success) {
    console.log('Content update applied successfully!')
  }
}

Modding Support with Namespaces

// Community mod that adds new storyline
const communityMod = `
mod_start:
  - "Welcome to the Community Romance Mod!"
  - actions:
      - type: setFlag
        flag: romance_mod_active
  - goto: mod_romance_hub

mod_romance_hub:
  - speaker: "Mod Author"
    say: "This mod adds romance options to the base game!"
  - text: "Choose your romance interest:"
    choices:
      - text: "Mysterious Stranger"
        goto: mod_romance_stranger
      - text: "Childhood Friend"
        goto: mod_romance_friend
      - text: "Return to main game"
        goto: town_square

mod_romance_stranger:
  - speaker: "Stranger"
    say: "You intrigue me, {{player_name}}..."
  - "This is user-generated content!"

mod_romance_friend:
  - speaker: "Friend"
    say: "I've been waiting to tell you something..."
  - "Community mods can extend the story!"
`

// Install mod with clear namespace
const result = vnEngine.upgradeScript(communityMod, {
  mode: 'additive',
  namespace: 'romance_mod',
  validateState: true
})

if (result.success) {
  console.log('Romance mod installed!')
  
  // Mods are clearly separated
  const modScenes = vnEngine.getScenesByNamespace('romance_mod')
  console.log('Mod scenes:', modScenes.map(s => s.name))
  
  // Check what content is loaded
  const stats = vnEngine.getUpgradeStats()
  console.log('Content breakdown:', stats)
  // {
  //   totalScenes: 25,
  //   estimatedDLCScenes: 8,
  //   baseScenes: 17,
  //   namespaces: ['romance_mod', 'expansion']
  // }
}

Upgrade Safety Features

Automatic State Validation

// Engine automatically checks if current game state remains valid
const result = vnEngine.upgradeScript(newContent, {
  validateState: true  // Default: true
})

if (!result.success && result.error?.code === 'STATE_INVALID') {
  console.error('Upgrade would break current save game')
  console.error('Issues:', result.error.details.affectedState)
}

Dry Run Mode

// Test upgrade without applying changes
const dryRun = vnEngine.upgradeScript(newContent, {
  dryRun: true,
  mode: 'replace',
  allowOverwrite: ['intro']
})

console.log('Dry run results:')
console.log('Would add:', dryRun.addedScenes)
console.log('Would replace:', dryRun.replacedScenes)
console.log('Warnings:', dryRun.warnings)

// Only apply if dry run looks good
if (dryRun.success && dryRun.warnings.length === 0) {
  const realResult = vnEngine.upgradeScript(newContent, {
    mode: 'replace',
    allowOverwrite: ['intro']
  })
}

Error Handling and Rollback

// The engine automatically handles rollback on failure
try {
  const result = vnEngine.upgradeScript(problematicContent, {
    mode: 'replace',
    allowOverwrite: ['critical_scene']
  })
  
  if (!result.success) {
    console.error('Upgrade failed:', result.error?.message)
    
    switch (result.error?.code) {
      case 'SCENE_CONFLICT':
        console.error('Scene name conflicts:', result.error.details.conflictingScenes)
        break
      case 'INVALID_REFERENCE':
        console.error('Broken scene references:', result.error.details.invalidReferences)
        break
      case 'UNAUTHORIZED_OVERWRITE':
        console.error('Attempted to overwrite protected scenes:', result.error.details.unauthorizedOverwrites)
        break
      case 'STATE_INVALID':
        console.error('Would break current game state:', result.error.details.affectedState)
        break
      case 'PARSE_ERROR':
        console.error('YAML parsing errors:', result.error.details.parseErrors)
        break
    }
    
    // Game state is automatically restored to pre-upgrade condition
    console.log('Game state has been restored')
  }
} catch (error) {
  console.error('Unexpected upgrade error:', error)
  // Engine handles cleanup automatically
}

Upgrade Event System

// Listen for upgrade events
vnEngine.on('upgradeCompleted', (result: UpgradeResult) => {
  console.log('Upgrade completed successfully!')
  console.log(`Added: ${result.addedScenes.length}, Replaced: ${result.replacedScenes.length}`)
  
  // Notify UI about new content
  showUpgradeNotification(result)
})

vnEngine.on('upgradeFailed', (error: string) => {
  console.error('Upgrade failed:', error)
  showErrorDialog('Content update failed', error)
})

Content Management Utilities

// Check what content is currently loaded
console.log('Current scenes:', vnEngine.getSceneNames())
console.log('Total scene count:', vnEngine.getSceneCount())
console.log('Has DLC content:', vnEngine.hasDLCContent())

// Get content by namespace
const dlcScenes = vnEngine.getScenesByNamespace('dlc')
const modScenes = vnEngine.getScenesByNamespace('romance_mod')

// Get detailed statistics
const stats = vnEngine.getUpgradeStats()
console.log('Content breakdown:', {
  base: stats.baseScenes,
  dlc: stats.estimatedDLCScenes,
  total: stats.totalScenes,
  namespaces: stats.namespaces
})

// Check if specific content exists
if (vnEngine.hasScene('dlc_bonus_scene')) {
  console.log('DLC bonus scene is available')
}

Best Practices for Upgrades

1. Always Validate First

const validation = vnEngine.validateUpgrade(content, options)
if (validation.valid) {
  vnEngine.upgradeScript(content, options)
} else {
  console.error('Validation failed:', validation.errors)
}

2. Use Namespaces for Organization

// Clear organization with namespaces
vnEngine.upgradeScript(dlcContent, { namespace: 'winter_dlc' })
vnEngine.upgradeScript(modContent, { namespace: 'community_mod' })
vnEngine.upgradeScript(seasonalContent, { namespace: 'holiday_2024' })

3. Preserve Backward Compatibility

// Check for existing content before adding references
const updateContent = `
enhanced_intro:
  - if: "hasFlag 'directors_cut'"
    then:
      - "Enhanced edition features activated!"
    else:
      - "Welcome to the original game!"
  - goto: character_creation
`

4. Handle Dependencies

// Ensure prerequisite content exists
if (vnEngine.hasScene('main_story_complete')) {
  vnEngine.upgradeScript(epilogueContent, { namespace: 'epilogue' })
} else {
  console.warn('Main story required for epilogue DLC')
}

📝 Script Format

Basic Structure

Scripts are written in YAML with scenes as top-level keys:

scene_name:
  - instruction1
  - instruction2
  - instruction3

another_scene:
  - "Simple dialogue"
  - speaker: "Character"
    say: "Dialogue with speaker"

Instruction Types

1. Simple Dialogue

intro:
  - "This is simple narrator text."

2. Dialogue with Speaker

conversation:
  - speaker: "Alice"
    say: "Hello there!"
  - speaker: "Bob"  
    say: "Nice to meet you, Alice."

3. Actions

setup:
  - actions:
      - type: setVar
        key: player_name
        value: "Hero"
      - type: setFlag
        flag: game_started
      - type: addTime
        minutes: 30

4. Choices

decision:
  - text: "What do you choose?"
    choices:
      - text: "Go left"
        actions:
          - type: setFlag
            flag: went_left
        goto: left_path
      - text: "Go right"
        goto: right_path
      - text: "Stay here"
        # No goto = continue current scene

5. Conditional Logic

check_health:
  - if: "gt health 50"
    then:
      - "You feel healthy!"
    else:
      - "You need rest."

6. Scene Jumps

ending:
  - "The end!"
  - goto: credits

Action Types

Variable Actions

  • setVar - Set a variable value
  • addVar - Add to a numeric variable

Flag Actions

  • setFlag - Set a story flag
  • clearFlag - Remove a story flag

List Actions

  • addToList - Add item to an array

Time Actions

  • addTime - Add minutes to game time

Examples

actions_demo:
  - actions:
      # Variables
      - type: setVar
        key: player_name
        value: "Alice"
      - type: setVar  
        key: player.coins
        value: 100
      - type: addVar
        key: player.coins
        value: 50
      
      # Flags
      - type: setFlag
        flag: met_merchant
      - type: clearFlag
        flag: tutorial_mode
      
      # Lists
      - type: addToList
        list: inventory
        item: { name: "Sword", damage: 10 }
      
      # Time
      - type: addTime
        minutes: 15

ðŸŽĻ Template System

VN Engine features a dual template system that adapts to your needs:

Handlebars Mode (Full Features)

When Handlebars is installed and detected, you get access to all advanced template features:

# Full Handlebars functionality
advanced_templates:
  - "Hello {{player_name}}!"
  - "You have {{add coins bonus}} total coins."
  - "Inventory: {{#each inventory}}{{name}} {{/each}}"
  - "{{#if (gt player.level 10)}}You're experienced!{{/if}}"
  - "Random choice: {{sample choices}}"
  - "{{#hasFlag 'met_merchant'}}The merchant recognizes you.{{/hasFlag}}"

Simple Mode (Built-in Fallback)

When Handlebars isn't available, the engine uses a lightweight template system:

# Simple template features (always available)
simple_templates:
  - "Hello {{player_name}}!"
  - "{{#if player.healthy}}You feel great!{{else}}You need rest.{{/if}}"
  - "Health: {{player.health}}"
  - "Condition check: {{#if (gt player.level 5)}}High level{{/if}}"

Template Engine Detection

async function setupTemplates() {
  await vnEngine.initialize()
  
  const info = vnEngine.getTemplateEngineInfo()
  
  if (info.type === 'handlebars') {
    console.log('✅ Full template functionality available')
    console.log('Available helpers:', info.helpersRegistered ? 'Yes' : 'Basic only')
  } else {
    console.log('â„đïļ Using simple template engine')
    console.log('Supported features:', info.supportedFeatures)
  }
}

Advanced Template Features (Handlebars Required)

# Math helpers
calculations:
  - "Total: {{add player.coins bonus}}"
  - "Damage: {{multiply weapon.power player.strength}}"
  - "Random damage: {{randomInt 10 20}}"

# Array helpers  
inventory_display:
  - "Items: {{join inventory.names ', '}}"
  - "First item: {{first inventory}}"
  - "{{#each (take inventory 3)}}{{name}} {{/each}}"

# String helpers
text_formatting:
  - "{{uppercase player.name}} the {{titleCase player.class}}"
  - "{{truncate long_description 50}}"
  - "{{typewriter 'Mysterious text appears...' 30}}"

# VN-specific helpers
story_logic:
  - "{{#hasFlag 'met_merchant'}}The merchant recognizes you.{{/hasFlag}}"
  - "{{#playerChose 'helped villager'}}Your kindness is remembered.{{/playerChose}}"
  - "Welcome back, {{getVar 'player.name' 'Stranger'}}!"
  - "Time: {{formatTime gameTime}}"

# Comparison helpers
conditionals:
  - "{{#gt player.level 10}}You're experienced!{{/gt}}"
  - "{{#between player.health 25 75}}Your health is moderate.{{/between}}"
  - "{{#isEmpty inventory}}Your inventory is empty.{{/isEmpty}}"

Template Context

All game state is available in templates:

  • Variables: {{variable_name}}, {{object.property}}
  • Flags: {{hasFlag 'flag_name'}}
  • Choices: {{playerChose 'choice_text'}}
  • Helpers: Math, comparison, array, and VN-specific functions

Validating Templates

// Check if a template is valid for current engine
const validation = vnEngine.validateTemplate('{{gt player.level 5}}')

if (validation.valid) {
  console.log(`Template valid for ${validation.engine} engine`)
} else {
  console.warn(`Template error: ${validation.error}`)
  console.log('Available features:', validation.supportedFeatures)
}

ðŸŽŪ Game State Management

Universal Variables System

Store any data structure in the variables system:

// Set complex nested data
vnEngine.gameState.setVariable('player', {
  name: 'Alice',
  stats: { health: 100, level: 1 },
  inventory: [{ name: 'Sword', damage: 10 }]
})

// Access in templates: {{player.name}}, {{player.stats.health}}, {{player.inventory.0.name}}

Story Flags

Boolean flags for tracking story progression:

vnEngine.gameState.setStoryFlag('intro_completed')
vnEngine.gameState.hasStoryFlag('intro_completed') // true

// In templates: {{hasFlag 'intro_completed'}}, {{#hasFlag 'intro_completed'}}...{{/hasFlag}}

Choice History

Automatic tracking of all player decisions:

const history = vnEngine.gameState.getChoiceHistory()
// [{ scene: 'intro', choiceText: 'Help the stranger', timestamp: ... }]

// In templates: {{playerChose 'Help the stranger'}}, {{#playerChose 'Go to market' 'town_scene'}}...{{/playerChose}}

Save/Load System

// Save game state
const saveData = vnEngine.getGameState()
localStorage.setItem('save', JSON.stringify(saveData))

// Load game state  
const saveData = JSON.parse(localStorage.getItem('save'))
vnEngine.setGameState(saveData)

📋 Core Examples

Basic Story Structure

intro:
  - "Welcome to our story!"
  - actions:
      - type: setVar
        key: player_name
        value: "Hero"
      - type: setFlag
        flag: story_started
  - speaker: "Guide"
    say: "Hello, {{player_name}}!"
  - goto: first_choice

first_choice:
  - text: "What do you want to do?"
    choices:
      - text: "Explore the forest"
        actions:
          - type: setFlag
            flag: chose_forest
        goto: forest_scene
      - text: "Visit the town"
        goto: town_scene

forest_scene:
  - "You enter the mysterious forest..."
  - if: "hasFlag 'story_started'"
    then:
      - "Your adventure begins here."
    else:
      - "How did you get here?"

Character System Example

character_creation:
  - "Choose your class:"
  - text: "What are you?"
    choices:
      - text: "Warrior"
        actions:
          - type: setVar
            key: player
            value: { class: "warrior", health: 150, strength: 15 }
          - type: setFlag
            flag: warrior_class
        goto: game_start
      - text: "Mage"
        actions:
          - type: setVar
            key: player
            value: { class: "mage", health: 100, mana: 100 }
          - type: setFlag
            flag: mage_class
        goto: game_start

game_start:
  - "You are a {{player.class}} with {{player.health}} health."
  - "{{#hasFlag 'warrior_class'}}Your sword gleams in the sunlight.{{/hasFlag}}"
  - "{{#hasFlag 'mage_class'}}Magical energy flows through you.{{/hasFlag}}"

Shop System Example

shop:
  - speaker: "Merchant"
    say: "You have {{coins}} coins."
  - text: "What would you like?"
    choices:
      - text: "Sword (50 coins)"
        condition: "gte coins 50"
        actions:
          - type: addVar
            key: coins
            value: -50
          - type: addToList
            list: inventory
            item: { name: "Iron Sword", damage: 10 }
        goto: shop
      - text: "Potion (20 coins)"
        condition: "gte coins 20"
        actions:
          - type: addVar
            key: coins
            value: -20
          - type: addToList
            list: inventory
            item: { name: "Health Potion", healing: 50 }
        goto: shop
      - text: "Leave"
        goto: town_square

Consequence Tracking Example

village_choice:
  - "A stranger asks for help."
  - text: "Do you help them?"
    choices:
      - text: "Help the stranger"
        actions:
          - type: setFlag
            flag: helped_stranger
          - type: addVar
            key: reputation
            value: 1
        goto: help_result
      - text: "Ignore them"
        goto: ignore_result

later_scene:
  - if: "playerChose 'Help the stranger'"
    then:
      - "The stranger recognizes you and offers a reward!"
    else:
      - "The stranger looks at you with disappointment."
  - "Your reputation: {{reputation}}"

🔧 Development & Testing

Initialization Patterns

// Modern async pattern (recommended)
async function initGame() {
  const vnEngine = createVNEngine()
  await vnEngine.initialize()  // Detects Handlebars, sets up helpers
  // Engine ready with all features
}

// Legacy sync pattern (still supported)
function initGameSync() {
  const vnEngine = createVNEngine()
  // Engine ready with basic features immediately
  // Handlebars detection happens lazily
}

// Check engine status
function checkEngineStatus(vnEngine) {
  console.log('Template engine ready:', vnEngine.isTemplateEngineReady())
  console.log('Engine info:', vnEngine.getTemplateEngineInfo())
}

Error Handling

debug_scene:
  - "{{debug player 'Player State'}}"              # Console logging (Handlebars only)
  - "Name: {{getVar 'player.name' 'UNKNOWN'}}"     # Safe fallbacks (both engines)
  - "{{hasFlag 'nonexistent_flag'}}"               # Returns false safely (both engines)

Building & Testing Scripts

npm run build           # Build the library
npm run dev             # Start development server (for demo)
npm test                # Run full test suite (core, performance, edge cases on packaged version)
npm run type-check      # Check TypeScript types
npm run test:core       # Run only core functionality tests
npm run test:package    # Build library and run tests on the packaged version
npm run package:test    # Dry run npm pack to check package contents
npm run package:analyze # Build, pack, analyze package size, then cleanup
npm run demo            # Run demo application

ðŸ“Ķ Dependencies

Required (Core Functionality)

  • js-yaml - YAML parsing
  • lodash - Utility functions (used internally for robust operations)

Optional (Enhanced Features)

  • handlebars - Advanced template engine with helpers and loops

Zero Dependencies Mode

VN Engine can work with zero external dependencies by using a simplified YAML parser and template engine (future feature).

🚀 Advanced Features

Multiple Engine Instances

// Run multiple stories simultaneously
const mainStory = createVNEngine()
const sideQuest = createVNEngine()

await Promise.all([
  mainStory.initialize(),
  sideQuest.initialize()
])

mainStory.loadScript(mainScript)
sideQuest.loadScript(questScript)

// Each maintains separate state and template engines

Event-Driven UI Updates

const vnEngine = createVNEngine()
await vnEngine.initialize()

vnEngine.on('stateChange', (result) => {
  switch (result?.type) {
    case 'display_dialogue':
      showDialogue(result.speaker, result.content)
      break
    case 'show_choices':
      showChoices(result.choices)
      break
    case 'scene_complete':
      showSceneComplete()
      break
    case 'error':
      showError(result.error)
      break
  }
})

vnEngine.on('error', (error) => {
  console.error('VN Engine Error:', error)
})

vnEngine.on('loaded', () => {
  console.log('Script loaded successfully!')
})

Custom Template Helper Registration

// Only works when Handlebars is available
await vnEngine.initialize()

const templateEngine = vnEngine.getTemplateEngineInfo()
if (templateEngine.type === 'handlebars') {
  const handlebars = vnEngine.getHandlebarsInstance()
  
  // Register custom helper
  handlebars.registerHelper('customHelper', (value) => {
    return `Custom: ${value}`
  })
  
  console.log('Custom helper registered!')
} else {
  console.log('Custom helpers require Handlebars')
}

Template Engine Feature Detection

const vnEngine = createVNEngine()
await vnEngine.initialize()

// Check what features are available
const features = vnEngine.getTemplateEngineInfo().supportedFeatures

if (features.helpers) {
  console.log('Advanced helpers available')
  // Use complex template features
} else {
  console.log('Using simple templates')
  // Stick to basic variable interpolation
}

if (features.loops) {
  // Can use {{#each}} loops
} else {
  // Use simple conditionals only
}

ðŸŽŊ Use Cases

  • Visual Novels - Traditional VN games with complex branching
  • Interactive Fiction - Text-based adventures with state tracking
  • Educational Content - Interactive tutorials with progress tracking
  • RPGs - Dialogue systems and narrative branches
  • Choose-Your-Own-Adventure - Multi-path storytelling
  • Game Tutorials - Context-aware step-by-step guides
  • Chatbots - Stateful conversational interfaces
  • Training Simulations - Scenario-based learning with consequences
  • DLC & Content Updates - Seamless content expansion
  • Modding Support - Community-generated content with validation
  • Episodic Releases - Sequential content delivery
  • Progressive Web Apps - Lightweight narrative experiences
  • Content Management - Template-driven content systems

Template Compatibility

Always Compatible (Both Engines)

- "Hello {{player_name}}!"
- "Health: {{player.health}}"
- if: "gt player.level 5"
  then: ["You're experienced!"]

Handlebars Only

- "{{#each inventory}}{{name}} {{/each}}"
- "{{add coins bonus}}"
- "{{randomInt 1 6}}"
- "{{#hasFlag 'special'}}Secret content{{/hasFlag}}"

📄 License

MIT License - see LICENSE file for details.

ðŸĪ Contributing

Contributions welcome! Please read CONTRIBUTING.md for guidelines.

Development Setup

git clone <repository>
cd vn-engine
npm install

# For testing with Handlebars
npm install handlebars @types/handlebars

# Run tests
npm test

# Build library
npm run build

Built with âĪïļ for interactive storytelling

Note: VN Engine is designed to work perfectly out of the box with zero configuration. Install Handlebars for advanced features, or use the built-in simple template engine for lightweight projects!