Skip to content

Open-source scripting language for writing interactive fictions

License

Notifications You must be signed in to change notification settings

jeremyfa/loreline

Repository files navigation

Loreline

Loreline is an open-source scripting language for writing interactive fictions.

Here's a most basic example of 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: 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.

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!

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
}

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

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
        }
    }
}

Dynamic text

Make your text responsive to the game state using the $ symbol for variable interpolation:

beat CheckInventory {
    barista: We have $coffeeBeans beans left in stock.
    Your total comes to ${price * quantity} dollars.
}

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"
}

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 {
    new 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!

Write and play Loreline scripts

Loreline scripts are written in .lor files. See CoffeeShop.lor and Minimal.lor as examples.

You can write these with any text editor, but the best option is using Visual Studio Code along with the Loreline Extension. This will make your editor support syntax highlighting of .lor files, which makes the content much more readable and easy to work with:

Minimal example, syntax highlighted in VSCode

Test using the command line interface

Using official binary

A binary to run loreline can be downloaded for your platform in the Releases page.

loreline play story.lor

Using haxelib

Alternatively, you can use haxelib:

haxelib install loreline
haxelib run loreline play story.lor

Embed loreline files in your game or application

Loreline runtime is written with the Haxe programming language, so it can be transpiled to many target languages such as Javascript, C++, C#, Java bytecode, PHP, Python...

At the moment, it's still early days of this project, so you'll need to use Haxe if you want to integrate loreline in your code, although it is planned in the foreseeable future to make it work out of the box in more languages!

Minimal haxe example

// Load script content
final content = File.getContent('story.lor');

// Parse the script
final script = Loreline.parse(content);

// Play the story
Loreline.play(
    script,

    // Called to display a text
    (_, character, text, tag, done) -> {
        if (character != null) {
            Sys.println(character + ': ' + text);
        }
        else {
            Sys.println(text);
        }
        done(); // Call done() when finished
    },

    // Called to prompt a choice
    (_, options, callback) -> {
        for (i in 0...options.length) {
            Sys.println((i + 1) + '. ' + options[i].text);
        }

        // Let the user make a choice
        final choice:Int = ...;
        callback(choice); // Call back with the choice index
    },

    // Called when the execution has finished
    _ -> {
        // Finished script execution
    }
);

You can also take a look at Cli.hx source code as another reference using Loreline.

About

Open-source scripting language for writing interactive fictions

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages