Skip to content

Loreline v0.11.0

Latest

Choose a tag to compare

@github-actions github-actions released this 31 Oct 18:19
· 0 commits to main since this release
og-banner

One more heavily packed update!

When Blocks

A new when block picks what to play by saliency: among the rules whose condition is true, it plays the one that requires the most. It can be used to write barks and reactive lines without a long chain of if/else, as well as moving away from strictly branching narratives by modularizing dialogues into storylets!

beat Counter
  when
    mood is "tired" and coffees > 3
      barista: Maybe switch to decaf?
    - visits > 1
      barista: Welcome back!
    + SeasonalLines if isWinter
    true
      barista: Hi, what can I get you?
  • A rule with more criteria wins over a more general one. A rule starting with - is played only once, and true makes a rule always eligible.
  • + Beat inserts the rules of another beat, like choice insertions do.
  • Besides the default strategy, when first and when pick are available, and custom strategies can be written in Loreline or provided by the host.
  • Rules are indexed and cached so large bark systems stay fast. prepareCaches() builds these caches ahead of time if you'd rather not pay for it on the first pick.

Story Metadata & IFID

A script can now start with a metadata block describing the story:

metadata
  title: The Coffee Shop
  author: Alex
  language: en
  ifid: 448E73DF-2D2F-47E7-A494-A46B40D4CFB3
  • The script reads it with $metadata.title, and the host with getMetadataField().
  • loreline ifid generates an IFID for a story and writes it in the metadata block.
  • The language field drives plurals (see below).

Plurals by Language

Plurals now follow the rules of each language, with up to six forms, and accept count thresholds for wording that goes beyond grammar. Plural pipes and plural() follow the same rules:

You ordered $count coffee|coffees.
You ordered ${plural(count, "coffee", "coffees")}.

The language comes from the language field of the story metadata. When a translation is loaded, translated lines follow the language of the translation, and lines it doesn't translate keep the rules of the source.

Forms are written in the order of the table below (the same order as gettext):

Languages Forms Which counts take each form
English, German, Spanish, Italian, Portuguese (Portugal) and any language not listed 2 1 · everything else (0, 2, 1.5...)
French, Armenian, Portuguese (Brazil, and pt alone) 2 0 and 1 (fractions below 2 too) · 2 and more
Japanese, Chinese, Korean, Vietnamese, Thai, Indonesian, Malay, Lao, Khmer, Burmese 1 every count
Russian, Ukrainian, Belarusian, Serbian, Croatian, Bosnian 3 1, 21, 31... (not 11) · 2-4, 22-24... (not 12-14) · everything else (0, 5-20, 25...)
Polish 3 1 · 2-4, 22-24... (not 12-14) · everything else (0, 5-21, 25...)
Czech, Slovak 3 1 · 2-4 · everything else
Lithuanian 3 1, 21, 31... (not 11) · 2-9, 22-29... (not 12-19) · everything else (0, 10-20, 30...)
Latvian 3 1, 21, 31... (not 11) · everything else · 0
Romanian 3 1 · 0, 2-19, 101-119... · everything else (20, 100...)
Slovenian 4 1, 101... · 2, 102... · 3-4, 103-104... · everything else
Irish 5 1 · 2 · 3-6 · 7-10 · everything else (0, 11 and more)
Arabic 6 0 · 1 · 2 · 3-10, 103-110... · 11-99, 111-199... · everything else (100-102...)

A few examples:

metadata
  language: pl

beat Start
  // 1 plik, 2 pliki, 5 plików, 12 plików, 22 pliki
  $count plik|pliki|plików
  $count (nowy plik|nowe pliki|nowych plików)
metadata
  language: fr

beat Start
  // 0 article, 1 article, 2 articles
  $count article|articles
metadata
  language: ar

beat Start
  // 0 → zero, 1 → one, 2 → two, 3 → few, 11 → many, 100 → other
  ${plural(count, "zero", "one", "two", "few", "many", "other")}

A few more details:

  • The sign is ignored: -1 takes the same form as 1.
  • A fraction takes the last form, except in languages where 0 and 1 go together, where anything below 2 takes the first form.
  • When a line gives fewer forms than its language uses, the last one given stands for the missing ones. In Polish, plik|pliki shows pliki for 5.
  • A story without a language field keeps the previous behavior: only 1 takes the first form.

Count Thresholds

A form can also start from a given count, whatever the language: write N: at the start of a form between parentheses, or put the number before the form in plural(). The highest threshold the count reaches wins, and the forms before the first threshold follow the rules of the language:

// 1 coffee, 3 coffees, 12 coffees, way too many
You drank $count (coffee|coffees|10:coffees, way too many).

// a coffee (1), a few coffees (2 to 4), far too many coffees (5 and more)
That's ${plural(count, "a coffee", 2, "a few coffees", 5, "far too many coffees")}.

When every form has a threshold, counts below the first one take its form: (2:a few|3:many) shows a few for 0 and 1. To write digits followed by a colon as text, escape them: (\10:30 slot|\10:30 slots).

Counting Without Showing the Count

A plural takes its count from the closest interpolation before it, and that includes one inside a tag. This lets a line pick the right form without showing the number, while the tag still reaches the game:

You see <$cups>(a cup|some cups) on the counter.

Names in Any Language

Characters, beats, state fields, functions and tags can now be named with the letters of any language, and emoji:

state
  咖啡: 0
  coins💰: 12

character 咖啡师
  name: 小林
  心情: "开心"

character Élodie
  name: Élodie

function 加倍(数量)
  return 数量 * 2

beat 开场
  咖啡师: 欢迎光临!

  Élodie: Un café, s'il te plaît.

  咖啡 += 1

  ${咖啡师.name}说你今天喝了 $咖啡 杯咖啡。

  You have ${coins💰} coins, worth ${加倍(coins💰)} points.

  -> 结束

beat 结束
  咖啡师: 再见!

In languages written without spaces, use ${...} to mark where an interpolated name ends, as in ${咖啡师.name}说. The same goes for a name followed by an emoji. The editor tools warn about lookalike letters and full-width colons that would change how a line is read.

Public Beats

public beat Name marks a beat the game starts or calls by name. It doesn't change how the story runs, but editors use it to know which beats are entry points.

Child Interpreters

spawn() creates a child interpreter that shares the story state with its parent, but plays on its own: a background conversation while the main dialogue goes on, an NPC reacting by itself, and so on. Children are saved and restored with their parent.

Save & Restore

  • The random generator is now saved with the game state, so a restored story makes the same random choices it would have made. seedRandom() allows reseeding it.
  • Shuffle alternatives resume where they left off after a restore.
  • The pending dialogue or choice is restored exactly as it was displayed.
  • Several fixes when insertions are involved, with tests that now save and restore at every possible point.

Same Results on Every Target

Values are now compared, tested in conditions, converted to text and used in arithmetic with the same rules on every target. Built-in functions, JSON, characters beyond ASCII, text tag offsets and plural pipes behave the same whether the story runs in JavaScript, C++, C#, Lua, PHP, the JVM or GDScript.

Language & Runtime Improvements

  • New word operators: is, is not and not, as in if mood is not "angry".
  • and and or now stop evaluating once the result is known.
  • A trailing if is read as a condition only when the rest of the line is a complete expression, so Tell me if it is true. stays text.
  • A line is read as an assignment only when it starts with what it assigns. Other lines holding an = stay text.
  • Characters can be passed as arguments to functions and beats, and a character can be declared by its identifier alone.
  • Text tags with attributes now work in unquoted narration.
  • New type_of() built-in function, and texts written as numbers are accepted in arithmetic.
  • Cyclic imports are read once, and errors inside imported files are reported in them.
  • Many performance improvements to the interpreter and to the analysis of large scripts.

Editor Tools

  • Hovers were redesigned: a title giving the kind of the name and where it lives, links to declarations, and beat hovers listing what the beat does by role (characters speaking, fields read or changed, beats it flows to).
  • The language server and the error messages speak French when the editor does, and loreline.js can show its errors in French on a web page. Many errors were also reworded to say what to write instead of using compiler jargon.
  • Syntax coloring was reviewed against the whole grammar, with new support for when blocks, metadata, word operators and directives in comments.
  • Completion offers keywords at the start of a statement, and stays quiet while a new name is being typed.
  • New loreline lsp command to run the language server from the CLI.

Breaking Changes

  • float() is renamed to number(), and random_float() to random_decimal().
  • Calling a function the script declares without a body is now an error when the host doesn't provide it. Loreline.externalFunctions() lists the functions a script expects.
  • Lines that are not assignments by the new rule (for example The score = 3.) are now played as text.