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, like special := 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 to and and or. Use whichever you prefer, but and/or tend 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:

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 Weave and Gather in 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 Threads in 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!