Introduction to Loreline
Loreline is an open-source scripting language for writing interactive fiction. It handles branching dialogue, player choices, characters, and story state in a syntax designed to be readable by writers while still offering real programming tools (variables, conditionals, functions) when the story needs them.
Loreline works everywhere: game engines, web apps, or standalone projects. It adapts to your tools, the stories you write stay portable.
A first look
Here is a minimal Loreline script:
The warm aroma of coffee fills the café.
barista: Hi there! How are you doing today?
choice
Having a great day
barista: Wonderful! Coffee will make it even better.
Need caffeine...
barista: Say no more! Let me help with that.
Your name is Alex, right?
barista.name = "Alex"
barista: Oh, I didn't expect you'd remember it!
- Text on its own is narration.
barista: ...is dialogue spoken by a character.choicepresents options, and indentation determines what happens after each option is picked.
That's the core pattern. The Writer's Guide goes into this in detail.
Clean yet explicit by design
Loreline keeps additional punctuation to a minimum. Narrative text and dialogue can be written as-is, without any special delimiter. Jumping to another section is a simple ->. Assigning a variable is just =. Conditions don't need a closing keyword, indentation is enough. A choice option can carry an inline if that hides it when the condition isn't met.
At the same time, the main structural elements (beat, choice, character, state, if, else) are plain English keywords rather than symbols. They're easy to understand at a glance, without needing to learn what each special character means. This is a deliberate trade-off: readable words over cryptic punctuation.
Other tools lean more on symbols or delimiters: Yarn Spinner uses <<if>>...<<endif>> and <<set>>; Ink has its own set of markers for choices, knots, and diverts. Loreline tries to stay close to plain text, and the editor is smart enough to apply correct syntax highlighting from those minimal cues alone:
The café is quiet this morning.
choice
Order a coffee
coffeesOrdered += 1
if coffeesOrdered > 3
barista: <concerned> That's quite a lot of coffee today...
else
barista: One more coming right up!
Order a decaf if coffeesOrdered > 2
barista: Good call. I'll make it a nice one.
For writers, this means the script reads closer to plain language, with less visual noise between the story and the structure.
Story structure
A story is organized into beats, named sections that work like scenes or chapters. A beat can jump to another with ->, or call one with BeatName() as a detour that returns to the caller when finished:
beat EnterCafe
The morning sun streams through the café windows.
barista: Welcome! I don't think I've seen you here before.
TakeOrder()
barista: Enjoy your coffee!
-> EndScene
beat TakeOrder
barista: So, what can I get started for you?
Characters are a first-class concept: you declare them with fields that can be read and updated throughout the story:
character barista
name: Alex
mood: friendly
friendship: 0
Persistent data lives in a state block. Conditionals (if, else if, else) let the story react to earlier decisions, and choices can have inline conditions to show or hide options dynamically.
if barista.friendship > 2
barista: Hey! Good to see you again :)
Other features
Beyond the core syntax, Loreline also supports:
- Text tags: attach metadata like
<friendly>or<whisper>to any dialogue line, or use tags to format text, for the game engine to interpret however it wants.
barista: <friendly> Good to see you!
sarah: I'll have a <strong>very strong coffee</strong> today.
- Dynamic text: embed variables with
$and${}, and automatic pluralization with$count coin|coins.
sarah: Did you know $barista.name runs the café next door?
james: Great, let's go, I need to finish reading ${james.currentBooks[0]}.
barista: You've ordered $coffeesOrdered coffee|coffees so far today.
- Alternatives: vary text across repeated visits using five selection modes (
sequence,cycle,once,pick,shuffle).
pick
A jazz tune drifts softly from a speaker on the shelf.
--
The espresso machine hisses and sputters.
--
Laughter erupts from a table by the window.
- Functions: over 60 built-in functions for math, manipulating text, arrays, and maps. You can also define your own functions, directly in a Loreline script or expose them from the host engine.
barista: Your order will be ready in $random(2, 5) minutes!
// Will display a random number between 2 and 5
- Imports: split a story across multiple
.lorfiles to keep large projects organized.
import inventories
import characters
// Story that uses inventories (inventories.lor)
// and characters (characters.lor)...
- Save and restore: serialize the interpreter's full state at any point and restore it later, covering execution position, variables, character fields, and alternative block progression.
// Save the current state (e.g. at a checkpoint)
const saveData = interpreter.save();
localStorage.setItem('save', JSON.stringify(saveData));
// Later, restore and resume
const saved = JSON.parse(localStorage.getItem('save'));
interpreter = Loreline.resume(script, onDialogue, onChoice, onFinish, saved);
- Localization: a first-class feature of the language: tag lines with
#keysin the script, provide.lortranslation files per language, and the interpreter substitutes them at runtime. See the localization guide for details.
Platforms and integration
Loreline is compatible with any C# engine (including Unity), C++, JavaScript, TypeScript, Python, Lua, or JVM project. The API works the same way everywhere: load a Loreline script, then play it by providing callbacks for dialogue, choices, and story completion.
All targets share a common codebase written in Haxe, which ensures consistent behaviour across languages and lets updates ship everywhere at once with no major feature differences. A suite of over 160 test files covers the runtime to help keep it reliable as the language evolves.
import { Loreline } from 'loreline';
const script = Loreline.parse(source);
Loreline.play(script,
// Called to display dialogue
(interp, character, text, tags, callback) => {
if (character) {
const name = interp.getCharacterField(character, 'name');
console.log((name || character) + ': ' + text);
} else {
console.log(text);
}
callback();
},
// Called to display a choice
(interp, options, callback) => {
options.forEach((opt, i) => {
if (opt.enabled) console.log(' [' + (i + 1) + '] ' + opt.text);
});
callback(0);
},
// Called when execution ends
(interp) => {
console.log('--- The End ---');
}
);
| Platform | Notes |
|---|---|
| JavaScript / TypeScript | Works in Node.js and the browser. Available as an npm package. |
| C# / .NET | Integrates with Unity, Godot (.NET), and any .NET project. |
| C++ | Standalone library with no external dependencies. |
| Python | Works with Python 3.8+, no external dependencies. |
| Java / JVM | Works with Java 8+ (and Kotlin). Single JAR with no external dependencies. |
| Lua | Works with Lua 5.1+, no external dependencies. |
| Haxe | Available as a haxelib package with full API access. |
See the integration guides for setup instructions for each platform.
Tooling
- VS Code extension: syntax highlighting and a live preview panel for
.lorfiles. - Online playground: write and run Loreline scripts directly in the browser.
- CLI: a command-line tool for running
.lorfiles, available for macOS, Linux, and Windows.
Comparison with Ink and Yarn Spinner
Ink and Yarn Spinner are the two other tools commonly used for interactive narrative. Each takes a different approach to syntax and structure.
This comparison is in no way claiming one tool is better or worse than the others. Ink and Yarn Spinner are mature, well-documented, and have been used in shipped games for years. Loreline is newer and takes a different approach. The best choice comes down to team familiarity, target engine, and which scripting style fits how you want to write.
Here is the same short script written in all three languages:
state
coffeesOrdered: 0
character barista
name: Alex
beat CoffeeShop
The warm aroma of coffee fills the cafe.
barista: Hi there! What can I get you?
choice
A regular coffee
barista: Coming right up!
A decaf if coffeesOrdered > 2
barista: Switching to decaf? Probably wise.
coffeesOrdered += 1
if coffeesOrdered > 3
barista: That's quite a lot of coffee today...
VAR coffeesOrdered = 0
=== CoffeeShop ===
The warm aroma of coffee fills the cafe.
Alex: Hi there! What can I get you?
+ A regular coffee
Alex: Coming right up!
+ {coffeesOrdered > 2} A decaf
Alex: Switching to decaf? Probably wise.
-
~ coffeesOrdered = coffeesOrdered + 1
{coffeesOrdered > 3:
Alex: That's quite a lot of coffee today...
}
-> DONE
title: CoffeeShop
---
<<declare $coffeesOrdered = 0>>
The warm aroma of coffee fills the cafe.
Alex: Hi there! What can I get you?
-> A regular coffee
Alex: Coming right up!
-> A decaf <<if $coffeesOrdered > 2>>
Alex: Switching to decaf? Probably wise.
<<set $coffeesOrdered = $coffeesOrdered + 1>>
<<if $coffeesOrdered > 3>>
Alex: That's quite a lot of coffee today...
<<endif>>
===
All three scripts tell the same story with comparable length but different conventions. For a detailed feature-by-feature breakdown, see the detailed comparison.
Try Loreline
The fastest way to get started is the Playground, where you can write and run Loreline scripts directly in the browser, no setup needed. When you're ready to go further, the Writer's Guide covers the full language step by step.