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-engineOptional Dependencies
For enhanced template functionality, you can install Handlebars:
npm install handlebars
npm install @types/handlebars # For TypeScript projectsNote: 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.
Basic Usage (Recommended - Async)
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(): VNEngineCreates a new VN engine instance. Multiple instances are supported.
Initialization (Recommended)
// 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(): TemplateEngineInfoCore 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 | nullEvent 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): UpgradePreviewReportUpgrade 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: 304. 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 scene5. Conditional Logic
check_health:
- if: "gt health 50"
then:
- "You feel healthy!"
else:
- "You need rest."6. Scene Jumps
ending:
- "The end!"
- goto: creditsAction Types
Variable Actions
setVar- Set a variable valueaddVar- Add to a numeric variable
Flag Actions
setFlag- Set a story flagclearFlag- 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_squareConsequence 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 enginesEvent-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 buildBuilt 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!