Skip to content

Project Structure ​

Understanding the structure of a LeftUnread scenario project is essential for creating and organizing your content.

Directory Overview ​

your-scenario-project/
├── src/                   # Source files for your scenario
│   ├── assets/           # Media files (images, audio, video)
│   ├── contacts/         # Contact definitions
│   ├── conversations/    # Conversation configurations
│   ├── gallery/          # Gallery item definitions
│   ├── ink/             # Ink script files
│   ├── news/            # News articles (markdown)
│   ├── notes/           # Notes/clues (markdown)
│   ├── locations/       # Map locations (JSON)
│   ├── scenario.json       # Scenario metadata
│   └── stats.json       # Optional end-screen stat labels
├── scripts/              # Build and publish scripts
│   ├── build-scenario.cjs  # Compiles and packages your scenario
│   └── publish-scenario.cjs # Uploads to LeftUnread server
├── dist/                 # Build output (generated)
├── .env                  # Environment variables (not committed)
├── .env.example         # Template for environment variables
└── package.json         # Node.js dependencies and scripts

Metadata filename

Use src/scenario.json for pack metadata. Older packs may still ship thread.json or story.json — keep those only if you need compatibility with an older tools release.

Contacts Directory ​

Contact files define the characters in your scenario. Each contact is a JSON file in src/contacts/.

Contact File Format ​

Example: src/contacts/alex.json

json
{
  "id": "alex",
  "name": "Alex Thompson",
  "avatar": "alex-avatar.jpg",
  "phoneNumber": "+44 7700 900124"
}

Fields:

  • id - Unique identifier (must match filename without .json)
  • name - Character's display name
  • avatar - Filename of avatar image in src/assets/
  • phoneNumber - Character's phone number (optional, for display)

Naming Convention

Use lowercase, hyphenated IDs (e.g., alex, family-group) for consistency.

Automatic Contact Unlocking

Contacts are automatically unlocked when a conversation containing that contact is unlocked. For example, if you unlock a conversation with #unlockConversation:alex, the contact "alex" will automatically appear in the player's contact list. You only need to use #unlockContact: if you want to unlock a contact separately from any conversation.

Conversations Directory ​

Conversation files link contacts to Ink scripts. Located in src/conversations/.

Conversation File Format ​

Individual Conversation: src/conversations/alex.json

json
{
  "id": "alex",
  "name": "Alex Thompson",
  "type": "individual",
  "contacts": ["alex"],
  "inkFile": "alex.ink",
  "initial": true
}

Group Conversation: src/conversations/family-group.json

json
{
  "id": "family-group",
  "name": "Family",
  "type": "group",
  "contacts": ["mum", "dad", "sister"],
  "inkFile": "family-group.ink",
  "initial": false
}

Fields:

  • id - Unique identifier (must match filename)
  • name - Conversation display name
  • type - Either "individual" or "group"
  • contacts - Array of contact IDs participating in this conversation
  • inkFile - Name of the Ink script file (in src/ink/)
  • initial - Whether this conversation starts immediately when the scenario begins

Ink Directory ​

Contains your narrative scripts written in Ink. Each .ink file corresponds to a conversation.

Example: src/ink/alex.ink

ink
// Variables
VAR police_contacted = false
VAR alex_knows_player = false

-> start

== start ==
Hey Sarah, have you seen my keys? #initial #read
Sarah? You were supposed to meet me an hour ago. #initial
-> conversation_choices

== conversation_choices ==
* [Hi, I found Sarah's phone]
  -> found_phone_response
* [Where was Sarah last seen?]
  -> last_seen_response
-> END

See the Ink Scripting Guide for detailed information.

Assets Directory ​

Store all media files in src/assets/:

  • Avatars - Character profile images (e.g., alex-avatar.jpg)
  • Scenario Images - Images referenced in messages
  • Audio Files - Voice messages, recordings (e.g., message.wav)
  • Video Files - Video clips, footage (e.g., footage.mp4)
  • Thumbnail - Scenario cover image (thumbnail.jpg)

Supported Formats:

  • Images: .jpg, .jpeg, .png, .gif, .webp
  • Audio: .wav, .mp3, .ogg, .m4a
  • Video: .mp4, .webm, .mov
  • Recommended avatar size: 200x200px to 500x500px
  • Recommended thumbnail size: 1200x630px

File References

When referencing assets in Ink scripts or JSON files, use just the filename (e.g., "alex-avatar.jpg"), not the full path.

News Directory ​

News articles are written in Markdown and appear in the in-game news feed. Located in src/news/.

Quick Overview ​

Each .md file in the news directory represents one news article with metadata tags at the top:

markdown
#initial
#title:Local Woman Reported Missing
#source:City News Network
#image:news-photo.jpg
#publishedAt:2025-10-22T10:00:00Z

**BREAKING**: A local woman has been reported missing...

Key Features ​

  • Markdown formatting - Full support for headings, lists, images, links, etc.
  • Metadata tags - Control title, source, images, and publication date
  • Initial unlock - Use #initial tag for articles available from the start
  • Timed unlocking - Unlock articles via Ink with #unlockNews:article-id[:delay]
  • State tracking - Check if articles are unlocked or read with external functions

Learn More

See the complete News System Guide for detailed documentation, examples, and best practices.

Notes Directory ​

Notes are written in Markdown and appear in the in-game notes app. Located in src/notes/.

Quick Overview ​

Each .md file in the notes directory represents one note with metadata tags at the top:

markdown
#initial
#title:Welcome to Your Notes

# Welcome to Your Notes

This is your personal notes app. Important information will appear here as you progress...

Key Features ​

  • Markdown formatting - Full support for headings, lists, tables, code blocks, etc.
  • Metadata tags - Control title and initial unlock status
  • Initial unlock - Use #initial tag for notes available from the start
  • Timed unlocking - Unlock notes via Ink with #unlockNote:note-id[:delay]
  • State tracking - Check if notes are unlocked or read with external functions

Common Use Cases ​

  • Clues and evidence - Important details players need to remember
  • Locations - Addresses, coordinates, access codes
  • Character information - Backgrounds, relationships, profiles
  • Timeline tracking - Sequence of events
  • Puzzle solutions - Codes, passwords, hints

Learn More

See the complete Notes System Guide for detailed documentation, examples, and best practices.

Gallery items (photos and videos) are defined in JSON files. Located in src/gallery/.

Quick Overview ​

Each .json file defines one gallery item with fields for type, asset, caption, and more:

json
{
  "type": "image",
  "asset": "crime-scene.jpg",
  "caption": "Evidence found at the warehouse",
  "takenAt": "2025-10-22T15:30:00Z",
  "initial": false
}

Key Features ​

  • Media types - Support for images and videos
  • Captions - Add descriptive text to provide context
  • Timestamps - Use takenAt to create a timeline of events
  • Initial unlock - Use "initial": true for items available from the start
  • Unlock via Ink - Use #gallery:item-id to unlock during gameplay
  • Dual display - Items appear in conversations and the gallery app

Common Use Cases ​

  • Evidence photos - Crime scenes, clues, documents
  • Security footage - CCTV, recordings, video evidence
  • Personal photos - Character selfies, group photos
  • Timeline creation - Visual sequence of events

Learn More

See the complete Gallery System Guide for detailed documentation, JSON field reference, and best practices.

Stats File (stats.json) ​

Optional. Defines human-readable labels for playthrough stats shown on the scenario completion screen.

Example: src/stats.json

json
{
  "stats": [
    {
      "id": "who_you_told",
      "label": "You told the group",
      "buckets": [
        { "value": "found_phone", "label": "You found Sarah's phone" },
        { "value": "call_police", "label": "You wanted to call the police" }
      ]
    }
  ]
}

Emit values from Ink with #stat:id:value, then finish with #completeScenario.

Learn More

See Scenario Stats & Completion for the full workflow and ending patterns.

Scripts Directory ​

build-scenario.cjs ​

Compiles your scenario:

  1. Finds all .ink files
  2. Compiles them to JSON using inkjs or inklecate
  3. Copies assets to build directory
  4. Generates manifest files
  5. Creates dist/scenario-build.zip

Usage:

bash
npm run build:scenario

Watch Mode:

bash
npm run build:scenario -- --watch

publish-scenario.cjs ​

Uploads your built scenario to a LeftUnread server:

  1. Runs the build script
  2. Reads credentials from .env
  3. Uploads scenario-build.zip via API

Usage:

bash
npm run publish:scenario

Environment Variables ​

The .env file stores your publishing credentials. Create it by copying .env.example:

bash
cp .env.example .env

Then fill in your details:

env
BASE_URL=https://leftunread.app
SCENARIO_ID=your-scenario-uuid-here
API_KEY=your-api-key-here

Getting Your Credentials ​

Scenario ID:

  1. Go to Scenarios Management
  2. Create your scenario
  3. Copy the UUID from the URL after creation

API Key:

  1. Go to Manage Passport Tokens
  2. Create a new token
  3. Copy the API key immediately (you won't see it again!)

Security

Never commit .env to version control! It contains sensitive API keys. Always add .env to your .gitignore file.

Build Output ​

After running npm run build:scenario, the dist/ directory contains:

dist/
└── scenario-build.zip       # Packaged scenario ready for upload

The zip file contains:

  • Compiled Ink scripts (.json)
  • All assets
  • Contact and conversation definitions
  • Optional stats.json (end-screen labels), if present in src/
  • Scenario metadata
  • Manifest files

Visual Studio Code with Ink Extension ​

For the best development experience, use Visual Studio Code with the Ink extension:

Installation:

  1. Download Visual Studio Code (free)
  2. Install and open VS Code
  3. Open Extensions (Ctrl+Shift+X / Cmd+Shift+X)
  4. Search for "Ink" by inkle
  5. Click Install

Benefits:

  • Syntax highlighting for .ink files
  • Error detection as you type
  • Auto-completion for Ink keywords
  • File navigation - jump to knot definitions
  • Integrated terminal - run build commands without leaving the editor

Opening Your Project:

  1. Open VS Code
  2. File → Open Folder
  3. Select your scenario project folder
  4. All files will appear in the sidebar

Useful VS Code Features:

  • Explorer (left sidebar) - Browse all project files
  • Search (Ctrl+Shift+F / Cmd+Shift+F) - Find text across all files
  • Terminal (Ctrl+`` / Cmd+``) - Run npm run build:scenario directly in VS Code
  • Split view - Edit multiple files side-by-side

Best Practices ​

Organization ​

  • One contact per file - Keep contact definitions separate
  • Match IDs to filenames - alex.json should have "id": "alex"
  • Group related conversations - Use clear naming for group chats

Naming Conventions ​

  • IDs: lowercase-with-hyphens (e.g., family-group)
  • Display Names: Proper Case (e.g., "Alex Thompson")
  • Files: Match the ID (e.g., alex.json, alex.ink)

Asset Management ​

  • Optimize images - Compress before adding to reduce file size
  • Consistent naming - Use descriptive names (e.g., alex-avatar.jpg)
  • Organize by type - Consider subdirectories for large projects
  • Test media files - Ensure audio/video files play correctly before publishing

Content Organization ​

  • News articles - Use realistic news writing style, include publication dates
  • Notes - Keep concise and scannable, format important info clearly
  • Gallery items - Add descriptive captions, organize by scenario progression
  • Initial content - Mark welcome/tutorial content with #initial tag

Version Control ​

Add to .gitignore:

.env
node_modules/
dist/

Next Steps ​

Released under the MIT License.