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

Assignments vs. declarations. In an assignment (=, +=, -=…), the right-hand side is an expression. That's why "rainy" needs double quotes: without them, rainy would be read as a variable name. Numbers (100), booleans (true/false), and null are also expressions, so they don't need quotes.

This is different from state and character declarations (with :), where the value is not an expression: weather: sunny directly assigns the text "sunny". To use an expression in a declaration, use interpolation: name: $playerName or total: ${a + b}, just like in dialogue or narrative text.

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 CoffeeTasting
  // These values reset every time we enter CoffeeTasting
  new state
    cupsTasted: 0
    currentRoast: light
    enjoymentLevel: 5

  choice
    Try another sip if cupsTasted < 3
      cupsTasted += 1
      Interesting notes in this one...

    Finish tasting
      -> OrderDrink

In this example, cupsTasted, currentRoast, and enjoymentLevel reset to their initial values every time the player enters the CoffeeTasting beat.

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

Choices can be conditional - only available when certain conditions are 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.

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.

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, lets the player pick a name, then returns to Introspection where execution continues.

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"

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.

getFruit()

You have $fruits 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!