Loreline Writer's Guide
This guide will teach you everything you need to write interactive stories with Loreline. No programming experience required. If you can write a screenplay or a choose-your-own-adventure book, you'll feel right at home.
Let's start with a simple example:
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!
Even if you've never seen such script before, you can probably understand what it does: it describes a scene in a café and gives the player choices that lead to different outcomes.
Core concepts
Let's explore how Loreline helps you create interactive stories. We'll start with the basic building blocks and gradually build up to more complex features.
Story structure and beats
Although you can write right at the beginning of a Loreline script, as your story becomes more complex, you'll want to organize it better. That's where Loreline beats come into play - sections that contain related scenes or moments. Think of beats as chapters or scenes in your story:
beat EnterCafe
The morning sun streams through the café windows as you step inside.
barista: <friendly> Welcome! I don't think I've seen you here before.
choice
Just looking around
barista: Take your time! I'm here when you're ready.
-> ExploreMenu
Actually, I could use some coffee
barista: <happy> You're in the right place!
-> TakeOrder
beat ExploreMenu
Beside you, a regular customer sips her drink contentedly.
sarah: Their lattes are amazing. I come here every morning.
barista: <cheerful> Sarah's right! Want to try one?
choice
Sure, I'll have what she's having
sarah: <pleased> Good choice!
-> TakeOrder
What else do you recommend?
-> TakeOrder
beat TakeOrder
barista: So, what can I get started for you?
choice
A latte sounds perfect
barista: <excited> Coming right up! I'll make it special for your first visit.
sarah: <smile> You won't regret it.
-> EndVisit
Just a regular coffee today
barista: Sometimes the classics are the best choice!
-> EndVisit
beat EndVisit
You find a cozy spot to enjoy your drink.
sarah: <friendly> Hope to see you around more often!
The arrow syntax (->) lets you move between beats, creating a branching storyline. Each beat can have its own narrative flow, choices, and consequences.
You can also end the story entirely using -> . (arrow to a dot):
beat EndVisit
barista: Thanks for coming! See you next time.
-> .
Characters and dialogue
When writing dialogue, you can define your characters along with their properties:
character barista
name: Alex
friendship: 0 // Track relationship with player
shiftStarted: true
character customer
name: Sam
visits: 0
favoriteDrink: null
Once defined, characters can speak using a simple syntax - their identifier, followed by a colon:
barista: Welcome to Coffee Dreams! What can I get you today?
customer: Just a regular coffee, please.
barista: Coming right up!
Dialogue can also span multiple lines by placing the text on indented lines after the colon:
barista:
Hey there, welcome to our cafe!
Got a special brew for you today.
Check out those limited edition Ethiopian coffee beans at the counter.
Writing story text
In Loreline, you can write narrative text naturally, just as you would in a book. You don't need any special markers - just write:
The warm aroma of coffee fills the café. Sunlight streams through the windows, casting long shadows across the wooden floor.
A gentle murmur of conversation fills the space.
Tags enclosed in angle brackets (<tag>) can be used in any text - whether it's dialogue or narrative:
barista: <friendly> Welcome back! Your usual?
customer: <tired> Yes please, I really need it today.
The machine <whirs>hums to life</whirs> as steam <hiss>escapes with a sharp sound</hiss>.
These tags can be used to express character emotions or change how text is displayed, depending on what's possible in your game or application.
Managing state
Interactive stories need to remember choices and track progress. Loreline uses state declarations for this. There are two types of state: persistent and temporary.
Persistent state
Persistent state remains throughout your story:
state
coffeeBeans: 100 // Track inventory
rushHour: false // Is it busy?
dayNumber: 1 // Which day of the story
weather: sunny // Current weather
You can change these values as your story progresses:
coffeeBeans -= 10 // Use some beans
rushHour = true // Start rush hour
dayNumber += 1 // Move to next day
weather = "rainy" // Change the weather
weather := rainy // Same thing, without quotes
=vs:=Use
=for numbers, booleans, and expressions:rushHour = true. Use:=for text without quotes:weather := rainy. This is especially useful when your text contains quotes, likespecial := The "Wake Up" Blend. Both support interpolation:greeting := Hello, $name.
State can also hold nested objects and arrays:
state
menu:
espresso: 3
latte: 5
cappuccino: 4
dailySpecials: [Ethiopian Roast, Vanilla Cold Brew]
Beat-local state
You can declare state inside a beat. It persists across visits to the beat but is scoped to that beat, so it won't clash with a top-level variable of the same name:
state
counter: 0 // Top-level counter
beat CoffeeShop
state
counter: 0 // Separate counter, local to this beat
counter += 1
barista: You've ordered $counter coffees in this shop!
Temporary state
Sometimes you want state that only exists within a specific beat. Use the new keyword to create temporary state that resets each time you enter the beat:
beat Counter
TakeOrder()
barista: Next!
-> Counter
beat TakeOrder
// orderTotal resets to 0 for each customer
// Without "new", the total would carry over between customers
new state
orderTotal: 0
barista: What can I get you?
choice
An espresso
orderTotal += 3
A latte
orderTotal += 5
barista: A croissant with that?
choice
Yes please
orderTotal += 2
No thanks
barista: That'll be $$${orderTotal}, thanks!
Here, orderTotal resets to 0 each time TakeOrder() is called. Without the new keyword, the total would carry over between customers.
Conditions and logic
Now that you can track state, you'll want your story to react to it. Loreline uses if, else if, and else to branch the narrative based on conditions.
Basic if blocks
An if block runs its indented content only when the condition is true:
if rushHour
The café is packed, with a line stretching to the door.
barista: <stressed> Bear with me, it's been a crazy morning!
You can add an else branch for the alternative:
if barista.friendship > 2
barista: <friendly> Hey! Good to see you again.
else
barista: Welcome! What can I get you?
else if chains
When you need to check several conditions in sequence, use else if:
if player.coffees == 0
barista: Ready for your first coffee of the day?
else if player.coffees > 3
barista: <concerned> Again? That's quite a lot of coffee today...
else
barista: Need another boost?
Loreline evaluates each condition from top to bottom and runs only the first branch that matches. If none match and there's an else, that branch runs instead.
Comparison operators
Conditions often compare values. Here are the operators you can use:
| Operator | Meaning | Example |
|---|---|---|
== |
equal to | if mood == "happy" |
!= |
not equal to | if favoriteRoast != null |
< |
less than | if coffeeBeans < 10 |
> |
greater than | if barista.friendship > 2 |
<= |
less than or equal to | if patience <= 0 |
>= |
greater than or equal to | if coffeeBeans >= 20 |
Combining conditions
You can combine conditions with and and or, or negate them with !:
| Operator | Meaning | Example |
|---|---|---|
and |
both must be true | if sunny and warm |
or |
at least one must be true | if rushHour or coffeeBeans < 10 |
! |
negation (not) | if !rushHour |
if sarah.present and james.present
Your friends are both here today.
if rushHour or coffeeBeans < 10
barista: <stressed> It's going to be a tough shift.
if !rushHour
The café is calm and quiet.
For complex expressions, parentheses can help make things clearer:
if (sarah.present or james.present) and !rushHour
A perfect time to catch up with a friend.
Loreline also accepts
&&and||as alternatives toandandor. Use whichever you prefer, butand/ortend to read more naturally in narrative scripts.
What counts as true or false
Most values are considered true, with a few exceptions that are false:
false0""(empty string)[](empty array)null
Everything else is true, including non-empty strings, non-zero numbers, and true itself. This lets you write concise conditions:
if player.name
barista: Hey, $player.name!
else
barista: Hey there! What's your name?
Here, player.name is null until it's been set, so the else branch runs for a player who hasn't introduced themselves yet.
Choices can also carry if conditions to show or hide options dynamically. This is covered in the next section.
Making choices interactive
The heart of interactive fiction is letting readers make choices:
beat OrderDrink
choice
Order a cappuccino
coffeeBeans -= 15
barista: <happy> One cappuccino coming right up!
-> PrepareDrink
Ask about tea options
barista: We have a lovely selection of green and herbal teas.
-> TeaMenu
Just browse the menu
You take your time reading through the extensive drink list.
-> DrinkMenu
As described in Conditions and logic, you can use if to make things conditional. Choices are no exception. Add an if at the end of an option to show it only when the condition is met:
beat SpecialMenu
choice
Order special roast if coffeeBeans >= 20
coffeeBeans -= 20
barista: Excellent choice! Our Ethiopian blend is amazing.
-> PrepareDrink
Chat with barista if barista.friendship > 2
barista: <friendly> Want to hear about my coffee journey?
-> BaristaChat
When a choice simply transitions to another beat without any extra logic, you can write it on a single line:
choice
Stay in the café -> CoffeeShop
Call it a day -> EndDay
Join $sarah if sarah.present -> SarahChat
Choices can also be nested. When a choice branch finishes without a -> transition, execution continues after the choice block:
barista: What would you like?
choice
A hot drink
choice
Espresso
barista: One espresso, coming right up!
Latte
barista: Great choice! Milk preference?
choice
Oat milk
barista: Our most popular option!
Regular milk
barista: Classic. Coming right up.
A cold drink
choice
Iced coffee
barista: Perfect for this weather!
Lemonade
barista: Fresh-squeezed, my favorite.
barista: I'll have that ready in just a moment.
The last line plays no matter which drink was chosen - all branches converge naturally after the outer choice block.
This kind of narrative structure is handled with
WeaveandGatherin Ink, while Loreline's approach is that indentation and nesting are enough to achieve the same thing.
Dismissing chosen options
Sometimes you want an option to disappear after the player picks it. For example, in a menu where each item can only be tried once. Prefix the option with - to make it dismissable:
beat Menu
barista: What'll it be?
choice
- Espresso
barista: One espresso!
-> Menu
- Latte
barista: One latte!
-> Menu
Leave
barista: See you!
Each time the player returns to Menu, the drinks they've already picked are greyed out. The Leave option has no - prefix, so it stays available every time.
This is useful for exploration menus, dialogue trees, or any "try each option" pattern where you want the player to work through the available options without repeating them.
Composing choices with insertions
As your story grows, you may want to reuse groups of choices across different beats. Choice insertions let you pull in choices from another beat using the + prefix:
beat CafeScene
choice
+ SeasonalDrinks
+ RegularMenu
Nothing for me, thanks
barista: No worries, let me know if you change your mind.
beat SeasonalDrinks
barista: Don't forget our seasonal specials!
choice
Hot spiced chocolate
barista: A perfect choice for the season!
Citrus tea
barista: Excellent, it's our newest addition.
beat RegularMenu
choice
Espresso
barista: One espresso, coming right up!
Latte
barista: Great choice!
When the player reaches the choice in CafeScene, they'll see the options from SeasonalDrinks and RegularMenu merged together with the "Nothing for me" option. Each inserted beat can also include dialogue that plays before its choices are shown.
Insertions are the equivalent of
Threadsin Ink: you collect choice options written in other sections, with the ability to trigger dialogue along the way, and reassemble everything into a single composed set of choices.
You can make an insertion conditional by adding if after the beat name, just like regular choice options:
beat CafeScene
choice
+ SeasonalDrinks if weekend
Espresso
barista: One espresso, coming right up!
When weekend is falsy, the options from SeasonalDrinks are not included at all. When it becomes truthy, they appear alongside the other options.
Calling beats as subroutines
You can call a beat like a function using parentheses. The called beat runs, and when it finishes, execution returns to where it was called:
character player
name: null
beat Introspection
if !player.name
What is my name?
ChooseName()
-> Introspection
else
Oh, I remember, my name is $player.name!
beat ChooseName
choice
Alex
player.name := Alex
Sam
player.name := Sam
Jamie
player.name := Jamie
Here, ChooseName() enters the ChooseName beat and lets the player pick a name. When the beat finishes, execution resumes where the call was made: the next line is -> Introspection, which restarts the beat from the top, this time with a name set.
Varying text across visits
When players revisit the same beat, you often want the text to change. Alternative blocks let you define variants and control how they're selected. Use -- to separate items within the block:
sequence
The café is quiet this early in the morning.
--
A few regulars have settled in with their newspapers.
--
The morning rush is just starting to pick up.
barista: What can I get you today?
The sequence keyword plays each item in order on successive visits, then sticks on the last one. Here, the first visit shows "The café is quiet…", the second "A few regulars…", and from the third visit onward, always "The morning rush…".
Five modes are available:
| Keyword | Behavior |
|---|---|
sequence |
Items in order, then stick on the last |
cycle |
Items in order, then loop back to the first |
once |
Items in order, then nothing |
pick |
A random item each time |
shuffle |
All items in a random order |
Here's cycle for a barista who rotates through greetings:
cycle
barista: Good morning! What'll it be?
--
barista: Welcome back! The usual?
--
barista: Hey again! Trying something new today?
once is useful for content that should only appear a limited number of times:
once
A bell chimes as you push the door open for the first time.
--
The familiar scent of coffee welcomes you back.
--
You nod at a few regulars you've gotten to know.
barista: Hey there!
After the third visit, the once block produces nothing and only "Hey there!" plays.
With a single item and no --, once is a simple way to show something only on the first visit:
once
A bell chimes as you push the door open for the first time.
barista: Hey there!
pick chooses a random item each time:
pick
Soft jazz plays from a speaker on the shelf.
--
The espresso machine hisses and sputters.
--
Laughter drifts from a table by the window.
And shuffle plays every item, but in a random order:
shuffle
You notice the chalkboard menu on the wall.
--
A cat is sleeping on the windowsill.
--
The barista is polishing glasses behind the counter.
Each item can contain multiple lines of text and dialogue, just like any other block:
cycle
barista: Today's special is a caramel macchiato.
It smells incredible.
--
barista: We've got a new cold brew on the menu.
A chalkboard sign announces the price.
--
barista: Try our seasonal pumpkin spice latte!
The warm spices fill the air.
Dynamic text
Make your text responsive to the game state using the $ symbol for variable interpolation:
barista: We have $coffeeBeans beans left in stock.
barista: That'll be ${coffeeBeans * 2} dollars for the lot!
Characters can also be referenced by their identifier, which will display their name property:
beat CloseShop
$barista begins cleaning up for the day. // Will show "Alex begins cleaning up for the day"
$customer waves goodbye as they leave. // Will show "Sam waves goodbye as they leave"
Beat names work the same way. MyBeat displays the beat's name, and you can call beat helper functions with dot notation:
beat CoffeeShop
CoffeeShop()
CoffeeShop()
if CoffeeShop.visits() > 1
You know this place well by now.
For more on beat-related functions like beat_visits and current_beat, see the Built-in Functions reference.
Escaping special characters
Since $ and < have special meaning in Loreline, you can escape them when you need the literal characters:
barista: That rare coffee is going to cost 9$$. Are you ok with that?
player: Damn, I only have 5\$ left...
Both $$ and \$ produce a literal $ in the output.
You can also escape angle brackets to prevent them from being treated as tags:
That's a high \<price> tag :(
Text vs. conditions
Most of the time, Loreline is smart enough to tell when if is part of your text rather than a condition. For example, this works just fine without any escaping:
beat WeatherReport
david: It's Friday once again, if you can believe it!
Loreline recognizes that if you can believe it! isn't a valid condition, so it keeps the entire line as dialogue text.
However, when the text after if happens to be a valid expression, it will be interpreted as a condition. In that case, wrap the text in double quotes if you just want it to be treated as text unambiguously:
state
tired: false
choice
Go outside if tired // choice shown only when tired is true
"Go outside if tired" // choice text is literally "Go outside if tired"
Use n to insert a line break within a single line of dialogue:
player: Can I pay...\nthe rest...\ntomorrow?
This displays as three separate lines:
Can I pay...
the rest...
tomorrow?
If you need a literal n in the output, escape the backslash with n.
Functions
Loreline supports functions that can be called from expressions, text interpolation, or as standalone statements. A function is called by its name followed by parentheses. For example, random is a built-in function that returns a random number between two values:
barista: Your order will be ready in $random(2, 5) minutes!
// Will display a random number between 2 and 5
Built-in functions
Loreline ships with 50+ built-in functions covering math, randomness, strings, arrays, maps, and more. Here are a few examples:
health = clamp(health + healing, 0, max_health)
if chance(4)
You find a rare gem on the ground!
if array_has(inventory, "golden key")
You unlock the ancient door.
greeting = " hello world ".trim().upper()
Many functions also support dot notation. For example, string_upper(name) can be written as name.upper().
For the complete list, see the Built-in Functions reference.
Defining your own functions
You can define your own functions outside of beats. A function has a name, optional parameters, and a body written in a general-purpose scripting syntax:
function add(a, b)
return a + b
state
apples: 7
oranges: 3
We have $apples apples and $oranges oranges, which makes a total of $add(apples, oranges) fruits!
Functions can access and modify state variables:
state
fruits: 2
function getFruit()
fruits = fruits + 1
You have $fruits fruits. // You have 2 fruits.
getFruit()
You have $fruits fruits. // You have 3 fruits.
Functions can use loops to build up results:
function enumerate(count, word)
var result = ""
for (i in 0...count)
if i > 0
result += ", "
result += "$word ${i + 1}"
return result
Here are all my items: $enumerate(3, "apple")
// Output: Here are all my items: apple 1, apple 2, apple 3
For a complete reference of the scripting syntax available inside functions (variables, control flow, operators, and more), see the Function Scripting page.
External functions
You can also declare functions without a body. These act as hooks for your game engine or application - the script declares them, and the host environment provides the actual implementation:
function playExplosion()
sarah: What's this green diamond? Wait, let me touch it...
james: Nooo don't touch it!
playExplosion()
james: Sarah? Sarah!!
Importing scripts
As your story grows, you can split it across multiple files using import statements:
import items
import characters/barista
import "scenes/intro.lor"
Imports load the contents of another .lor file into the current script. The .lor extension and quotes are optional - import characters/barista will look for characters/barista.lor in a characters subfolder.
Alternative syntax: braces
Throughout this guide, all examples use indentation to define blocks. Loreline also supports curly braces as an alternative:
beat CoffeeShop {
choice {
Order espresso {
barista: One espresso coming right up!
-> ProcessOrder
}
Order latte if !rushHour {
barista: Great choice! I'll make it extra foamy.
-> ProcessOrder
}
Leave -> EndDay
}
}
Both styles work everywhere blocks are used (beats, choices, state declarations, if/else). You can use whichever style you prefer, though indentation-based syntax tends to be more readable for narrative content.
Comments and organization
Keep your script organized with comments:
// Track customer loyalty
customer.visits += 1
/* Check if we should
trigger the special event */
if customer.visits > 10
-> LoyaltyReward
Advanced features
Here's a complex example putting multiple features together:
beat CoffeeTasting
state
cupsTasted: 0
favoriteRoast: null
lastImpression: ""
barista: <enthusiastic> Ready to explore our new roasts?
choice
Try light roast if cupsTasted < 3
cupsTasted += 1
lastImpression := bright and citrusy
The bright, citrusy notes dance on your tongue.
if chance(3) // 1 in 3 chance
favoriteRoast := light
barista: <happy> I see that spark in your eyes!
-> DiscussTaste
Try medium roast if cupsTasted < 3
cupsTasted += 1
lastImpression := nutty and balanced
A pleasant nuttiness fills your mouth.
-> DiscussTaste
Discuss coffee origins if barista.friendship > 1
barista: <passionate> Let me tell you about our farmers...
-> CoffeeOrigins
Finish tasting if cupsTasted > 0
if favoriteRoast != null
-> OrderFavorite
else
-> RegularOrder
beat DiscussTaste
barista: What do you think about the $lastImpression notes?
choice
Express enthusiasm
barista.friendship += 1
-> CoffeeTasting
Nod politely
-> CoffeeTasting
This syntax guide covered the main features of Loreline, but there's always more to discover as you write your own stories. Experiment with different combinations of these features to create rich narratives.
Happy writing!